diff --git a/pom.xml b/pom.xml index 4a3434dd..64f557b6 100644 --- a/pom.xml +++ b/pom.xml @@ -1,230 +1,305 @@ - - 4.0.0 + - org.tarantool - connector - 1.9.5-SNAPSHOT - jar - - UTF-8 - 5.4.2 - 1.23 - 1.10.19 - - 1.7.26 - ${project.basedir}/src/test/resources - - Tarantool Connector for Java + 4.0.0 + + org.tarantool + connector + 1.9.50 + jar + + + UTF-8 + 5.4.2 + 1.23 + 1.10.19 + 1.7.26 + ${project.basedir}/src/test/resources + 1.8 + 1.8 + + + Tarantool Connector for Java + https://github.com/tarantool/tarantool-java + Tarantool client for Java + + + + The BSD licence + http://opensource.org/licenses/BSD-3-Clause + repo + + + + https://github.com/tarantool/tarantool-java - Tarantool client for java - - - The BSD licence - http://opensource.org/licenses/BSD-3-Clause - repo - - + scm:git:git://github.com/tarantool/tarantool-java.git + scm:git:git@github.com:tarantool/tarantool-java.git + + + + + Dmitry Grytsovets + dmitry.grytsovets@gmail.com + Tarantool + http://tarantool.org/ + + + + + + + com.github.jsqlparser + jsqlparser + 4.6 + + + org.slf4j + slf4j-api + ${sfl4j.version} + + + + + + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit.jupiter.version} + test + + + org.mockito + mockito-all + ${mockito.version} + test + + + org.yaml + snakeyaml + ${snakeyml.version} + test + + - - https://github.com/tarantool/tarantool-java - scm:git:git://github.com/tarantool/tarantool-java.git - scm:git:git@github.com:tarantool/tarantool-java.git - + + + maven-releases + Nexus Maven Releases + http://localhost:9081/repository/maven-releases + + - - - Dmitry Grytsovets - dmitry.grytsovets@gmail.com - Tarantool - http://tarantool.org/ - - + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${maven.compiler.source} + ${maven.compiler.target} + + - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.2 - - 1.8 - 1.8 - - - - - org.codehaus.mojo - build-helper-maven-plugin - 3.0.0 - - - parse-version - - parse-version - - - - - - - org.codehaus.mojo - templating-maven-plugin - 1.0.0 - - - filter-src - - filter-sources - - - - - - maven-surefire-plugin - 3.0.0-M3 - - - - ${project.testResources}/jul-silent.properties - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - 3.0.0-M3 - - - - integration-test - verify - - - - - false - - - ${project.testResources}/jul-silent.properties - - + + + org.apache.maven.plugins + maven-shade-plugin + 3.4.1 + + + package + + shade + + + + ${project.artifactId}-${project.version} + + true + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + + + - - - - org.jacoco - jacoco-maven-plugin - 0.8.2 - - - - - - org/tarantool/Version.class - - - - - prepare-agent - - prepare-agent - - - - - - org.eluder.coveralls - coveralls-maven-plugin - 4.3.0 - - - - - javax.xml.bind - jaxb-api - 2.3.1 - - - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.0.0 - - - com.puppycrawl.tools - checkstyle - 8.19 - - - - src/test/resources/checkstyle.xml - error - true - true - true - true - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.2.0 - - 8 - true - - - - - + + + org.apache.maven.plugins + maven-deploy-plugin + 3.0.0-M1 + + maven-releases + + + - - - org.junit.jupiter - junit-jupiter-engine - ${junit.jupiter.version} - test - - - org.junit.jupiter - junit-jupiter-params - ${junit.jupiter.version} - test - - - org.mockito - mockito-all - ${mockito.version} - test - - - org.yaml - snakeyaml - ${snakeyml.version} - test - - - org.slf4j - slf4j-api - ${sfl4j.version} - provided - - + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + parse-version + + parse-version + + + + + + org.codehaus.mojo + templating-maven-plugin + 1.0.0 + + + filter-src + + filter-sources + + + + + + maven-surefire-plugin + 3.0.0-M3 + + + + ${project.testResources}/jul-silent.properties + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.0.0-M3 + + + + integration-test + verify + + + + + false + + + ${project.testResources}/jul-silent.properties + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.2 + + + + + + org/tarantool/Version.class + + + + + prepare-agent + + prepare-agent + + + + + + org.eluder.coveralls + coveralls-maven-plugin + 4.3.0 + + + + + javax.xml.bind + jaxb-api + 2.3.1 + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.0.0 + + + com.puppycrawl.tools + checkstyle + 8.19 + + + + src/test/resources/checkstyle.xml + error + true + true + true + true + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.2.0 + + ${maven.compiler.source} + true + + + + + - - org.sonatype.oss - oss-parent - 7 - + + org.sonatype.oss + oss-parent + 7 + diff --git a/src/main/java/org/tarantool/MsgPackLite.java b/src/main/java/org/tarantool/MsgPackLite.java index f16f66fa..4bc15d3f 100644 --- a/src/main/java/org/tarantool/MsgPackLite.java +++ b/src/main/java/org/tarantool/MsgPackLite.java @@ -6,362 +6,671 @@ import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Array; +import java.math.BigDecimal; import java.math.BigInteger; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.Callable; +import org.tarantool.utils.TarantoolDecimal; /** - * forked from https://bitbucket.org/sirbrialliance/msgpack-java-lite + * Updated MsgPackLite class with enhanced logging, corrected handling of extension types, and full support for Tarantool DECIMAL as BigDecimal. */ public class MsgPackLite { - public static final MsgPackLite INSTANCE = new MsgPackLite(); - - protected static final int MAX_4BIT = 0xf; - protected static final int MAX_5BIT = 0x1f; - protected static final int MAX_7BIT = 0x7f; - protected static final int MAX_8BIT = 0xff; - protected static final int MAX_15BIT = 0x7fff; - protected static final int MAX_16BIT = 0xffff; - protected static final int MAX_31BIT = 0x7fffffff; - protected static final long MAX_32BIT = 0xffffffffL; - - protected static final BigInteger BI_MIN_LONG = BigInteger.valueOf(Long.MIN_VALUE); - protected static final BigInteger BI_MAX_LONG = BigInteger.valueOf(Long.MAX_VALUE); - protected static final BigInteger BI_MAX_64BIT = BigInteger.valueOf(2).pow(64).subtract(BigInteger.ONE); - - //these values are from http://wiki.msgpack.org/display/MSGPACK/Format+specification - protected static final byte MP_NULL = (byte) 0xc0; - protected static final byte MP_FALSE = (byte) 0xc2; - protected static final byte MP_TRUE = (byte) 0xc3; - protected static final byte MP_BIN8 = (byte) 0xc4; - protected static final byte MP_BIN16 = (byte) 0xc5; - protected static final byte MP_BIN32 = (byte) 0xc6; - - protected static final byte MP_FLOAT = (byte) 0xca; - protected static final byte MP_DOUBLE = (byte) 0xcb; - - protected static final byte MP_FIXNUM = (byte) 0x00;//last 7 bits is value - protected static final byte MP_UINT8 = (byte) 0xcc; - protected static final byte MP_UINT16 = (byte) 0xcd; - protected static final byte MP_UINT32 = (byte) 0xce; - protected static final byte MP_UINT64 = (byte) 0xcf; - - protected static final byte MP_NEGATIVE_FIXNUM = (byte) 0xe0;//last 5 bits is value - protected static final int MP_NEGATIVE_FIXNUM_INT = 0xe0;// /me wishes for signed numbers. - protected static final byte MP_INT8 = (byte) 0xd0; - protected static final byte MP_INT16 = (byte) 0xd1; - protected static final byte MP_INT32 = (byte) 0xd2; - protected static final byte MP_INT64 = (byte) 0xd3; - - protected static final byte MP_FIXARRAY = (byte) 0x90;//last 4 bits is size - protected static final int MP_FIXARRAY_INT = 0x90; - protected static final byte MP_ARRAY16 = (byte) 0xdc; - protected static final byte MP_ARRAY32 = (byte) 0xdd; - - protected static final byte MP_FIXMAP = (byte) 0x80;//last 4 bits is size - protected static final int MP_FIXMAP_INT = 0x80; - protected static final byte MP_MAP16 = (byte) 0xde; - protected static final byte MP_MAP32 = (byte) 0xdf; - - protected static final byte MP_FIXSTR = (byte) 0xa0;//last 5 bits is size - protected static final int MP_FIXSTR_INT = 0xa0; - protected static final byte MP_STR8 = (byte) 0xd9; - protected static final byte MP_STR16 = (byte) 0xda; - protected static final byte MP_STR32 = (byte) 0xdb; - - public void pack(Object item, OutputStream os) throws IOException { - DataOutputStream out = new DataOutputStream(os); - if (item instanceof Callable) { - try { - item = ((Callable) item).call(); - } catch (Exception e) { - throw new IllegalArgumentException(e); + public static final MsgPackLite INSTANCE = new MsgPackLite(); + + protected static final int MAX_4BIT = 0xf; + protected static final int MAX_5BIT = 0x1f; + protected static final int MAX_7BIT = 0x7f; + protected static final int MAX_8BIT = 0xff; + protected static final int MAX_15BIT = 0x7fff; + protected static final int MAX_16BIT = 0xffff; + protected static final int MAX_31BIT = 0x7fffffff; + protected static final long MAX_32BIT = 0xffffffffL; + + protected static final BigInteger BI_MIN_LONG = BigInteger.valueOf(Long.MIN_VALUE); + protected static final BigInteger BI_MAX_LONG = BigInteger.valueOf(Long.MAX_VALUE); + protected static final BigInteger BI_MAX_64BIT = BigInteger.valueOf(2).pow(64).subtract(BigInteger.ONE); + + // MessagePack type constants (defined as int for correct comparisons) + protected static final int MP_NULL = 0xc0; + protected static final int MP_FALSE = 0xc2; + protected static final int MP_TRUE = 0xc3; + protected static final int MP_BIN8 = 0xc4; + protected static final int MP_BIN16 = 0xc5; + protected static final int MP_BIN32 = 0xc6; + + protected static final int MP_FLOAT = 0xca; + protected static final int MP_DOUBLE = 0xcb; + + protected static final int MP_FIXNUM = 0x00; // last 7 bits is value + protected static final int MP_UINT8 = 0xcc; + protected static final int MP_UINT16 = 0xcd; + protected static final int MP_UINT32 = 0xce; + protected static final int MP_UINT64 = 0xcf; + + protected static final int MP_NEGATIVE_FIXNUM = 0xe0; // last 5 bits is value + protected static final int MP_NEGATIVE_FIXNUM_INT = 0xe0; + protected static final int MP_INT8 = 0xd0; + protected static final int MP_INT16 = 0xd1; + protected static final int MP_INT32 = 0xd2; + protected static final int MP_INT64 = 0xd3; + + protected static final int MP_FIXARRAY = 0x90; // last 4 bits is size + protected static final int MP_FIXARRAY_INT = 0x90; + protected static final int MP_ARRAY16 = 0xdc; + protected static final int MP_ARRAY32 = 0xdd; + + protected static final int MP_FIXMAP = 0x80; // last 4 bits is size + protected static final int MP_FIXMAP_INT = 0x80; + protected static final int MP_MAP16 = 0xde; + protected static final int MP_MAP32 = 0xdf; + + protected static final int MP_FIXSTR = 0xa0; // last 5 bits is size + protected static final int MP_FIXSTR_INT = 0xa0; + protected static final int MP_STR8 = 0xd9; + protected static final int MP_STR16 = 0xda; + protected static final int MP_STR32 = 0xdb; + + // Extension Types + protected static final int MP_EXT8 = 0xc7; + protected static final int MP_EXT16 = 0xc8; + protected static final int MP_EXT32 = 0xc9; + protected static final int MP_FIXEXT1 = 0xd4; + protected static final int MP_FIXEXT2 = 0xd5; + protected static final int MP_FIXEXT4 = 0xd6; + protected static final int MP_FIXEXT8 = 0xd7; + protected static final int MP_FIXEXT16 = 0xd8; + + // Define the UUID extension type code as per Tarantool's documentation + protected static final int MP_UUID_TYPE = 2; + protected static final int MP_DECIMAL_TYPE = 1; // Tarantool DECIMAL type code + + /** + * Packs an object into MessagePack format. + * + * @param item The object to pack. + * @param os The OutputStream to write the packed data to. + * @throws IOException If an I/O error occurs. + */ + public void pack(Object item, OutputStream os) throws IOException { + + + DataOutputStream out = new DataOutputStream(os); + + // Handle Callable items first + if (item instanceof Callable) { + try { + item = ((Callable) item).call(); + } catch (Exception e) { + throw new IllegalArgumentException("Error calling Callable: " + e.getMessage(), e); + } + } + + // Handle null + if (item == null) { + out.write(MP_NULL); + } + // Handle Boolean + else if (item instanceof Boolean) { + out.write((Boolean) item ? MP_TRUE : MP_FALSE); + } + // Handle UUID as Extension Type + else if (item instanceof UUID) { + UUID uuid = (UUID) item; + byte[] uuidBytes = uuidToBytes(uuid); + // Serialize as FIXEXT16 (type code 2, 16 bytes) + out.write(MP_FIXEXT16); + out.writeByte(MP_UUID_TYPE); // Type code for UUID + out.write(uuidBytes); + } + // Handle BigDecimal + else if (item instanceof BigDecimal) { + BigDecimal bd = (BigDecimal) item; + // Serialize as EXT with type=1 (mpdecimal) + byte[] decimalBytes = encodeBigDecimal(bd); + out.write(MP_EXT8); + out.writeByte(decimalBytes.length); // length + out.writeByte(MP_DECIMAL_TYPE); // type + out.write(decimalBytes); + } + // Handle Numbers and Code + else if (item instanceof Number || item instanceof Code) { + if (item instanceof Float) { + out.write(MP_FLOAT); + out.writeFloat((Float) item); + } else if (item instanceof Double) { + out.write(MP_DOUBLE); + out.writeDouble((Double) item); + } else { + if (item instanceof BigInteger) { + BigInteger value = (BigInteger) item; + boolean isPositive = value.signum() >= 0; + if ((isPositive && value.compareTo(BI_MAX_64BIT) > 0) || + value.compareTo(BI_MIN_LONG) < 0) { + throw new IllegalArgumentException( + "Cannot encode BigInteger as MsgPack: out of -2^63..2^64-1 range"); + } + if (isPositive && value.compareTo(BI_MAX_LONG) > 0) { + byte[] data = value.toByteArray(); + // data can contain leading zero bytes + for (int i = 0; i < data.length - 8; ++i) { + if (data[i] != 0) { + throw new IllegalArgumentException( + "Cannot encode BigInteger as MsgPack: out of -2^63..2^64-1 range"); + } } + out.write(MP_UINT64); + out.write(data, data.length - 8, 8); + return; + } } - if (item == null) { - out.write(MP_NULL); - } else if (item instanceof Boolean) { - out.write(((Boolean) item).booleanValue() ? MP_TRUE : MP_FALSE); - } else if (item instanceof Number || item instanceof Code) { - if (item instanceof Float) { - out.write(MP_FLOAT); - out.writeFloat((Float) item); - } else if (item instanceof Double) { - out.write(MP_DOUBLE); - out.writeDouble((Double) item); - } else { - if (item instanceof BigInteger) { - BigInteger value = (BigInteger) item; - boolean isPositive = value.signum() >= 0; - if (isPositive && value.compareTo(BI_MAX_64BIT) > 0 || - value.compareTo(BI_MIN_LONG) < 0) { - throw new IllegalArgumentException( - "Cannot encode BigInteger as MsgPack: out of -2^63..2^64-1 range"); - } - if (isPositive && value.compareTo(BI_MAX_LONG) > 0) { - byte[] data = value.toByteArray(); - // data can contain leading zero bytes - for (int i = 0; i < data.length - 8; ++i) { - assert data[i] == 0; - } - out.write(MP_UINT64); - out.write(data, data.length - 8, 8); - return; - } - } - long value = item instanceof Code ? ((Code) item).getId() : ((Number) item).longValue(); - if (value >= 0) { - if (value <= MAX_7BIT) { - out.write((int) value | MP_FIXNUM); - } else if (value <= MAX_8BIT) { - out.write(MP_UINT8); - out.write((int) value); - } else if (value <= MAX_16BIT) { - out.write(MP_UINT16); - out.writeShort((int) value); - } else if (value <= MAX_32BIT) { - out.write(MP_UINT32); - out.writeInt((int) value); - } else { - out.write(MP_UINT64); - out.writeLong(value); - } - } else { - if (value >= -(MAX_5BIT + 1)) { - out.write((int) (value & 0xff)); - } else if (value >= -(MAX_7BIT + 1)) { - out.write(MP_INT8); - out.write((int) value); - } else if (value >= -(MAX_15BIT + 1)) { - out.write(MP_INT16); - out.writeShort((int) value); - } else if (value >= -(MAX_31BIT + 1)) { - out.write(MP_INT32); - out.writeInt((int) value); - } else { - out.write(MP_INT64); - out.writeLong(value); - } - } - } - } else if (item instanceof String) { - byte[] data = ((String) item).getBytes("UTF-8"); - if (data.length <= MAX_5BIT) { - out.write(data.length | MP_FIXSTR); - } else if (data.length <= MAX_8BIT) { - out.write(MP_STR8); - out.writeByte(data.length); - } else if (data.length <= MAX_16BIT) { - out.write(MP_STR16); - out.writeShort(data.length); - } else { - out.write(MP_STR32); - out.writeInt(data.length); - } - out.write(data); - } else if (item instanceof byte[] || item instanceof ByteBuffer) { - byte[] data; - if (item instanceof byte[]) { - data = (byte[]) item; - } else { - ByteBuffer bb = ((ByteBuffer) item); - if (bb.hasArray()) { - data = bb.array(); - } else { - data = new byte[bb.capacity()]; - bb.position(); - bb.limit(bb.capacity()); - bb.get(data); - } - } - if (data.length <= MAX_8BIT) { - out.write(MP_BIN8); - out.writeByte(data.length); - } else if (data.length <= MAX_16BIT) { - out.write(MP_BIN16); - out.writeShort(data.length); - } else { - out.write(MP_BIN32); - out.writeInt(data.length); - } - out.write(data); - } else if (item instanceof List || item.getClass().isArray()) { - int length = item instanceof List ? ((List) item).size() : Array.getLength(item); - if (length <= MAX_4BIT) { - out.write(length | MP_FIXARRAY); - } else if (length <= MAX_16BIT) { - out.write(MP_ARRAY16); - out.writeShort(length); - } else { - out.write(MP_ARRAY32); - out.writeInt(length); - } - if (item instanceof List) { - List list = ((List) item); - for (Object element : list) { - pack(element, out); - } - } else { - for (int i = 0; i < length; i++) { - pack(Array.get(item, i), out); - } - } - } else if (item instanceof Map) { - Map map = (Map) item; - if (map.size() <= MAX_4BIT) { - out.write(map.size() | MP_FIXMAP); - } else if (map.size() <= MAX_16BIT) { - out.write(MP_MAP16); - out.writeShort(map.size()); - } else { - out.write(MP_MAP32); - out.writeInt(map.size()); - } - for (Map.Entry kvp : map.entrySet()) { - pack(kvp.getKey(), out); - pack(kvp.getValue(), out); - } + long value = item instanceof Code ? ((Code) item).getId() : ((Number) item).longValue(); + if (value >= 0) { + if (value <= MAX_7BIT) { + out.write((int) value | MP_FIXNUM); + } else if (value <= MAX_8BIT) { + out.write(MP_UINT8); + out.writeByte((int) value); + } else if (value <= MAX_16BIT) { + out.write(MP_UINT16); + out.writeShort((int) value); + } else if (value <= MAX_32BIT) { + out.write(MP_UINT32); + out.writeInt((int) value); + } else { + out.write(MP_UINT64); + out.writeLong(value); + } } else { - throw new IllegalArgumentException("Cannot msgpack object of type " + item.getClass().getCanonicalName()); + if (value >= -(MAX_5BIT + 1)) { + out.write((int) (value & 0xff)); + } else if (value >= -(MAX_7BIT + 1)) { + out.write(MP_INT8); + out.writeByte((int) value); + } else if (value >= -(MAX_15BIT + 1)) { + out.write(MP_INT16); + out.writeShort((int) value); + } else if (value >= -(MAX_31BIT + 1)) { + out.write(MP_INT32); + out.writeInt((int) value); + } else { + out.write(MP_INT64); + out.writeLong(value); + } } + } } - - public Object unpack(InputStream is) throws IOException { - DataInputStream in = new DataInputStream(is); - int value = in.read(); - if (value < 0) { - throw new IllegalArgumentException("No more input available when expecting a value"); + // Handle String + else if (item instanceof String) { + byte[] data = ((String) item).getBytes("UTF-8"); + if (data.length <= MAX_5BIT) { + out.write(data.length | MP_FIXSTR); + } else if (data.length <= MAX_8BIT) { + out.write(MP_STR8); + out.writeByte(data.length); + } else if (data.length <= MAX_16BIT) { + out.write(MP_STR16); + out.writeShort(data.length); + } else { + out.write(MP_STR32); + out.writeInt(data.length); + } + out.write(data); + } + // Handle byte[] and ByteBuffer + else if (item instanceof byte[] || item instanceof ByteBuffer) { + byte[] data; + if (item instanceof byte[]) { + data = (byte[]) item; + } else { + ByteBuffer bb = ((ByteBuffer) item); + if (bb.hasArray()) { + data = bb.array(); + } else { + data = new byte[bb.capacity()]; + bb.position(0); + bb.limit(bb.capacity()); + bb.get(data); } - switch ((byte) value) { - case MP_NULL: - return null; - case MP_FALSE: - return false; - case MP_TRUE: - return true; - case MP_FLOAT: - return in.readFloat(); - case MP_DOUBLE: - return in.readDouble(); - case MP_UINT8: - return in.read(); // read single byte, return as int - case MP_UINT16: - return in.readShort() & MAX_16BIT; // read short, trick Java into treating it as unsigned, return int - case MP_UINT32: - return in.readInt() & MAX_32BIT; // read int, trick Java into treating it as unsigned, return long - case MP_UINT64: { - long v = in.readLong(); - if (v >= 0) { - return v; - } else { - // this is a little bit more tricky, since we don't have unsigned longs - byte[] bytes = new byte[] { - (byte) ((v >> 56) & 0xff), - (byte) ((v >> 48) & 0xff), - (byte) ((v >> 40) & 0xff), - (byte) ((v >> 32) & 0xff), - (byte) ((v >> 24) & 0xff), - (byte) ((v >> 16) & 0xff), - (byte) ((v >> 8) & 0xff), - (byte) (v & 0xff), - }; - return new BigInteger(1, bytes); - } + } + if (data.length <= MAX_8BIT) { + out.write(MP_BIN8); + out.writeByte(data.length); + } else if (data.length <= MAX_16BIT) { + out.write(MP_BIN16); + out.writeShort(data.length); + } else { + out.write(MP_BIN32); + out.writeInt(data.length); + } + out.write(data); + } + // Handle List and Arrays + else if (item instanceof List || item.getClass().isArray()) { + int length = item instanceof List ? ((List) item).size() : Array.getLength(item); + if (length <= MAX_4BIT) { + out.write(length | MP_FIXARRAY); + } else if (length <= MAX_16BIT) { + out.write(MP_ARRAY16); + out.writeShort(length); + } else { + out.write(MP_ARRAY32); + out.writeInt(length); + } + if (item instanceof List) { + List list = ((List) item); + for (Object element : list) { + pack(element, out); } - case MP_INT8: - return (byte) in.read(); - case MP_INT16: - return in.readShort(); - case MP_INT32: - return in.readInt(); - case MP_INT64: - return in.readLong(); - case MP_ARRAY16: - return unpackList(in.readShort() & MAX_16BIT, in); - case MP_ARRAY32: - return unpackList(in.readInt(), in); - case MP_MAP16: - return unpackMap(in.readShort() & MAX_16BIT, in); - case MP_MAP32: - return unpackMap(in.readInt(), in); - case MP_STR8: - return unpackStr(in.readByte() & MAX_8BIT, in); - case MP_STR16: - return unpackStr(in.readShort() & MAX_16BIT, in); - case MP_STR32: - return unpackStr(in.readInt(), in); - case MP_BIN8: - return unpackBin(in.readByte() & MAX_8BIT, in); - case MP_BIN16: - return unpackBin(in.readShort() & MAX_16BIT, in); - case MP_BIN32: - return unpackBin(in.readInt(), in); - default: - break; + } else { + for (int i = 0; i < length; i++) { + pack(Array.get(item, i), out); } + } + } + // Handle Map + else if (item instanceof Map) { + Map map = (Map) item; + if (map.size() <= MAX_4BIT) { + out.write(map.size() | MP_FIXMAP); + } else if (map.size() <= MAX_16BIT) { + out.write(MP_MAP16); + out.writeShort(map.size()); + } else { + out.write(MP_MAP32); + out.writeInt(map.size()); + } + for (Map.Entry kvp : map.entrySet()) { + pack(kvp.getKey(), out); + pack(kvp.getValue(), out); + } + } + // Unsupported type + else { + throw new IllegalArgumentException("Cannot msgpack object of type " + + item.getClass().getCanonicalName()); + } + } + + /** + * Encodes BigDecimal into Tarantool's mpdecimal format (EXT type=1). + * + * @param bd The BigDecimal to encode. + * @return Byte array representing mpdecimal. + */ + private byte[] encodeBigDecimal(BigDecimal bd) { + int scale = bd.scale(); + boolean negative = bd.signum() < 0; + BigDecimal absBd = bd.abs(); + String digitsStr = absBd.unscaledValue().toString(); + + + if (digitsStr.length() % 2 != 0) { + digitsStr = "0" + digitsStr; + } + + // Формируем BCD + byte[] bcdDigits = new byte[digitsStr.length() / 2]; + for (int i = 0; i < bcdDigits.length; i++) { + int highNibble = Character.digit(digitsStr.charAt(2 * i), 10); + int lowNibble = Character.digit(digitsStr.charAt(2 * i + 1), 10); + bcdDigits[i] = (byte) ((highNibble << 4) | lowNibble); + } + + // Логируем BCD-байты в hex-формате + + byte signByte = (byte) (negative ? 0x0D : 0x0C); + ByteBuffer buffer = ByteBuffer.allocate(2 + 1 + bcdDigits.length); + buffer.putShort((short) scale); + buffer.put(signByte); + buffer.put(bcdDigits); - if (value >= MP_NEGATIVE_FIXNUM_INT && value <= MP_NEGATIVE_FIXNUM_INT + MAX_5BIT) { - return (byte) value; - } else if (value >= MP_FIXARRAY_INT && value <= MP_FIXARRAY_INT + MAX_4BIT) { - return unpackList(value - MP_FIXARRAY_INT, in); - } else if (value >= MP_FIXMAP_INT && value <= MP_FIXMAP_INT + MAX_4BIT) { - return unpackMap(value - MP_FIXMAP_INT, in); - } else if (value >= MP_FIXSTR_INT && value <= MP_FIXSTR_INT + MAX_5BIT) { - return unpackStr(value - MP_FIXSTR_INT, in); - } else if (value <= MAX_7BIT) { - // MP_FIXNUM - the value is value as an int - return value; + byte[] result = buffer.array(); + + // Финальный лог + return result; + } + + /** + * Утилита для перевода байтов в hex-строку. + */ + + + /** + * Unpacks an object from MessagePack format. + * + * @param is The InputStream to read the packed data from. + * @return The unpacked object. + * @throws IOException If an I/O error occurs. + */ + public Object unpack(InputStream is) throws IOException { + DataInputStream in = new DataInputStream(is); + int value = in.read(); + if (value < 0) { + throw new IllegalArgumentException("No more input available when expecting a value"); + } + switch (value) { + case MP_NULL: + return null; + case MP_FALSE: + return false; + case MP_TRUE: + return true; + case MP_FLOAT: { + return in.readFloat(); + } + case MP_DOUBLE: { + return in.readDouble(); + } + case MP_UINT8: { + return in.readUnsignedByte(); + } + case MP_UINT16: { + return in.readUnsignedShort(); + } + case MP_UINT32: { + return (long) in.readInt() & MAX_32BIT; + } + case MP_UINT64: { + long v = in.readLong(); + if (v >= 0) { + return v; } else { - throw new IllegalArgumentException("Input contains invalid type value " + (byte) value); + // Handle unsigned long by converting to BigInteger + byte[] bytes = new byte[]{ + (byte) ((v >> 56) & 0xff), + (byte) ((v >> 48) & 0xff), + (byte) ((v >> 40) & 0xff), + (byte) ((v >> 32) & 0xff), + (byte) ((v >> 24) & 0xff), + (byte) ((v >> 16) & 0xff), + (byte) ((v >> 8) & 0xff), + (byte) (v & 0xff), + }; + BigInteger bigInt = new BigInteger(1, bytes); + return bigInt; } + } + case MP_INT8: { + return (int) in.readByte(); + } + case MP_INT16: { + return (int) in.readShort(); + } + case MP_INT32: { + return in.readInt(); + } + case MP_INT64: { + return in.readLong(); + } + case MP_FIXARRAY: { + int size; + size = value - MP_FIXARRAY_INT; + return unpackList(size, in); + } + case MP_ARRAY16: { + int size = in.readUnsignedShort(); + return unpackList(size, in); + } + case MP_ARRAY32: { + int size = in.readInt(); + return unpackList(size, in); + } + case MP_FIXMAP: { + int size = value - MP_FIXMAP_INT; + return unpackMap(size, in); + } + case MP_MAP16: { + int size = in.readUnsignedShort(); + return unpackMap(size, in); + } + case MP_MAP32: { + int size = in.readInt(); + return unpackMap(size, in); + } + case MP_FIXSTR: { + int size = value - MP_FIXSTR_INT; + return unpackStr(size, in); + } + case MP_STR8: { + int size = in.readUnsignedByte(); + return unpackStr(size, in); + } + case MP_STR16: { + int size = in.readUnsignedShort(); + return unpackStr(size, in); + } + case MP_STR32: { + int size = in.readInt(); + return unpackStr(size, in); + } + case MP_BIN8: { + int size = in.readUnsignedByte(); + return unpackBin(size, in); + } + case MP_BIN16: { + int size = in.readUnsignedShort(); + return unpackBin(size, in); + } + case MP_BIN32: { + int size = in.readInt(); + return unpackBin(size, in); + } + case MP_FIXEXT1: + case MP_FIXEXT2: + case MP_FIXEXT4: + case MP_FIXEXT8: + case MP_FIXEXT16: + case MP_EXT16: + case MP_EXT8: + case MP_EXT32: { + return unpackExt(value, in); + } + + default: + break; } - protected List unpackList(int size, DataInputStream in) throws IOException { - if (size < 0) { - throw new IllegalArgumentException("Array to unpack too large for Java (more than 2^31 elements)!"); - } - List ret = new ArrayList(size); - for (int i = 0; i < size; ++i) { - ret.add(unpack(in)); - } - return ret; + // Обработка других фиксированных типов + if (value >= MP_NEGATIVE_FIXNUM_INT) { + return (byte) (value - 256); + } else if (value >= MP_FIXARRAY_INT && value <= MP_FIXARRAY_INT + MAX_4BIT) { + int size = value - MP_FIXARRAY_INT; + return unpackList(size, in); + } else if (value >= MP_FIXMAP_INT && value <= MP_FIXMAP_INT + MAX_4BIT) { + int size = value - MP_FIXMAP_INT; + return unpackMap(size, in); + } else if (value >= MP_FIXSTR_INT && value <= MP_FIXSTR_INT + MAX_5BIT) { + int size = value - MP_FIXSTR_INT; + return unpackStr(size, in); + } else if (value <= MAX_7BIT) { + return value; + } else { + throw new IllegalArgumentException("Input contains invalid type value " + + String.format("0x%02x", value)); } + } - protected Map unpackMap(int size, DataInputStream in) throws IOException { - if (size < 0) { - throw new IllegalArgumentException("Map to unpack too large for Java (more than 2^31 elements)!"); - } - Map ret = new HashMap(size); - for (int i = 0; i < size; ++i) { - Object key = unpack(in); - Object value = unpack(in); - ret.put(key, value); - } - return ret; + /** + * Unpacks a list from MessagePack format. + * + * @param size The size of the list. + * @param in The DataInputStream to read from. + * @return The unpacked list. + * @throws IOException If an I/O error occurs. + */ + protected List unpackList(int size, DataInputStream in) throws IOException { + if (size < 0) { + throw new IllegalArgumentException("Array to unpack too large for Java (more than 2^31 elements)!"); + } + List ret = new ArrayList<>(size); + for (int i = 0; i < size; ++i) { + ret.add(unpack(in)); } + return ret; + } - protected Object unpackStr(int size, DataInputStream in) throws IOException { - if (size < 0) { - throw new IllegalArgumentException("byte[] to unpack too large for Java (more than 2^31 elements)!"); - } + /** + * Unpacks a map from MessagePack format. + * + * @param size The size of the map. + * @param in The DataInputStream to read from. + * @return The unpacked map. + * @throws IOException If an I/O error occurs. + */ + protected Map unpackMap(int size, DataInputStream in) throws IOException { + if (size < 0) { + throw new IllegalArgumentException("Map to unpack too large for Java (more than 2^31 elements)!"); + } + Map ret = new HashMap<>(size); + for (int i = 0; i < size; ++i) { + Object key = unpack(in); + Object value = unpack(in); + ret.put(key, value); + } + return ret; + } - byte[] data = new byte[size]; - in.readFully(data); - return new String(data, "UTF-8"); + /** + * Unpacks a string from MessagePack format. + * + * @param size The size of the string. + * @param in The DataInputStream to read from. + * @return The unpacked string. + * @throws IOException If an I/O error occurs. + */ + protected String unpackStr(int size, DataInputStream in) throws IOException { + if (size < 0) { + throw new IllegalArgumentException("String to unpack too large for Java (more than 2^31 elements)!"); } - protected Object unpackBin(int size, DataInputStream in) throws IOException { - if (size < 0) { - throw new IllegalArgumentException("byte[] to unpack too large for Java (more than 2^31 elements)!"); - } + byte[] data = new byte[size]; + in.readFully(data); + return new String(data, "UTF-8"); + } + + /** + * Unpacks a binary data array from MessagePack format. + * + * @param size The size of the binary data. + * @param in The DataInputStream to read from. + * @return The unpacked byte array. + * @throws IOException If an I/O error occurs. + */ + protected byte[] unpackBin(int size, DataInputStream in) throws IOException { + if (size < 0) { + throw new IllegalArgumentException("byte[] to unpack too large for Java (more than 2^31 elements)!"); + } + + byte[] data = new byte[size]; + in.readFully(data); + return data; + } + + /** + * Handles extension types during unpacking. + *

+ * Если это UUID (type = MP_UUID_TYPE, length = 16) – распаковываем как UUID. Если это DECIMAL (type = 1) – распаковываем как BigDecimal. Если длина + * = 8 (и не UUID), считаем Double (по требованию). Иначе возвращаем byte[]. + * + * @param format The format byte indicating the type of extension. + * @param in The DataInputStream to read from. + * @return The deserialized object (UUID, BigDecimal, Double, or byte[]). + * @throws IOException If an I/O error occurs. + */ + protected Object unpackExt(int format, DataInputStream in) throws IOException { + int length; + int type; + + switch (format) { + case MP_FIXEXT1: + length = 1; + type = in.readUnsignedByte(); + break; + case MP_FIXEXT2: + length = 2; + type = in.readUnsignedByte(); + break; + case MP_FIXEXT4: + length = 4; + type = in.readUnsignedByte(); + break; + case MP_FIXEXT8: + length = 8; + type = in.readUnsignedByte(); + break; + case MP_FIXEXT16: + length = 16; + type = in.readUnsignedByte(); + break; + case MP_EXT8: + length = in.readUnsignedByte(); + type = in.readUnsignedByte(); + break; + case MP_EXT16: + length = in.readUnsignedShort(); + type = in.readUnsignedByte(); + break; + case MP_EXT32: + length = in.readInt(); + type = in.readUnsignedByte(); + break; + default: + throw new IllegalArgumentException("Unknown extension format: " + + String.format("0x%02x", format)); + } + + // 1. Если это UUID (type == MP_UUID_TYPE, length == 16) — обрабатываем как UUID. + if (type == MP_UUID_TYPE && length == 16) { + byte[] uuidBytes = new byte[16]; + in.readFully(uuidBytes); + return bytesToUUID(uuidBytes); + } + // 2. Если это DECIMAL (type = 1) — распаковываем как BigDecimal. + else if (type == MP_DECIMAL_TYPE) { + byte[] decimalBytes = new byte[length]; + in.readFully(decimalBytes); + return TarantoolDecimal.decodeDecimal(decimalBytes); + } + else { + byte[] extData = new byte[length]; + in.readFully(extData); + return extData; + } + } + + /** + * Converts a UUID to a 16-byte array. + * + * @param uuid The UUID to convert. + * @return A 16-byte array representing the UUID. + */ + protected byte[] uuidToBytes(UUID uuid) { + ByteBuffer bb = ByteBuffer.allocate(16); + bb.putLong(uuid.getMostSignificantBits()); + bb.putLong(uuid.getLeastSignificantBits()); + return bb.array(); + } - byte[] data = new byte[size]; - in.readFully(data); - return data; + /** + * Converts a 16-byte array to a UUID. + * + * @param bytes The byte array to convert. + * @return The resulting UUID. + */ + protected UUID bytesToUUID(byte[] bytes) { + if (bytes.length != 16) { + throw new IllegalArgumentException("UUID byte array must be 16 bytes long"); } + ByteBuffer bb = ByteBuffer.wrap(bytes); + long high = bb.getLong(); + long low = bb.getLong(); + return new UUID(high, low); + } } diff --git a/src/main/java/org/tarantool/RoundRobinSocketProviderImpl.java b/src/main/java/org/tarantool/RoundRobinSocketProviderImpl.java index 5817665e..16a2b53e 100644 --- a/src/main/java/org/tarantool/RoundRobinSocketProviderImpl.java +++ b/src/main/java/org/tarantool/RoundRobinSocketProviderImpl.java @@ -61,7 +61,18 @@ public class RoundRobinSocketProviderImpl extends BaseSocketChannelProvider impl * @throws IllegalArgumentException if addresses aren't provided */ public RoundRobinSocketProviderImpl(String... addresses) { - updateAddressList(Arrays.asList(addresses)); + this(Arrays.asList(addresses)); + } + + /** + * Constructs an instance. + * + * @param addresses optional list of addresses in a form of host[:port] + * + * @throws IllegalArgumentException if addresses aren't provided + */ + public RoundRobinSocketProviderImpl(List addresses) { + updateAddressList(addresses); setRetriesLimit(DEFAULT_RETRIES_PER_CONNECTION); } @@ -215,4 +226,4 @@ private void throwFatalError(String message, Throwable lastError) { throw new CommunicationException(message, lastError); } -} +} \ No newline at end of file diff --git a/src/main/java/org/tarantool/TarantoolClientImpl.java b/src/main/java/org/tarantool/TarantoolClientImpl.java index ae1d7360..0569de07 100644 --- a/src/main/java/org/tarantool/TarantoolClientImpl.java +++ b/src/main/java/org/tarantool/TarantoolClientImpl.java @@ -1,18 +1,5 @@ package org.tarantool; -import org.tarantool.logging.Logger; -import org.tarantool.logging.LoggerFactory; -import org.tarantool.protocol.ProtoConstants; -import org.tarantool.protocol.ProtoUtils; -import org.tarantool.protocol.ReadableViaSelectorChannel; -import org.tarantool.protocol.TarantoolGreeting; -import org.tarantool.protocol.TarantoolPacket; -import org.tarantool.schema.TarantoolMetaSpacesCache; -import org.tarantool.schema.TarantoolSchemaException; -import org.tarantool.schema.TarantoolSchemaMeta; -import org.tarantool.util.StringUtils; -import org.tarantool.util.TupleTwo; - import java.io.IOException; import java.net.SocketAddress; import java.nio.ByteBuffer; @@ -39,1059 +26,1064 @@ import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.StampedLock; +import org.tarantool.logging.Logger; +import org.tarantool.logging.LoggerFactory; +import org.tarantool.protocol.ProtoConstants; +import org.tarantool.protocol.ProtoUtils; +import org.tarantool.protocol.ReadableViaSelectorChannel; +import org.tarantool.protocol.TarantoolGreeting; +import org.tarantool.protocol.TarantoolPacket; +import org.tarantool.schema.TarantoolMetaSpacesCache; +import org.tarantool.schema.TarantoolSchemaException; +import org.tarantool.schema.TarantoolSchemaMeta; +import org.tarantool.util.StringUtils; +import org.tarantool.util.TupleTwo; public class TarantoolClientImpl extends TarantoolBase> implements TarantoolClient { - private static final Logger LOGGER = LoggerFactory.getLogger(TarantoolClientImpl.class); + private static final Logger LOGGER = LoggerFactory.getLogger(TarantoolClientImpl.class); - protected TarantoolClientConfig config; - protected Duration operationTimeout; + protected TarantoolClientConfig config; + protected Duration operationTimeout; - /** - * External. - */ - protected SocketChannelProvider socketProvider; - protected SocketChannel channel; - protected ReadableViaSelectorChannel readChannel; + /** + * External. + */ + protected SocketChannelProvider socketProvider; + protected SocketChannel channel; + protected ReadableViaSelectorChannel readChannel; - protected volatile Exception thumbstone; + protected volatile Exception thumbstone; - protected ScheduledExecutorService workExecutor; + protected ScheduledExecutorService workExecutor; - protected StampedLock schemaLock = new StampedLock(); - protected BlockingQueue delayedOperationsQueue; + protected StampedLock schemaLock = new StampedLock(); + protected BlockingQueue delayedOperationsQueue; - protected Map futures; - protected AtomicInteger pendingResponsesCount = new AtomicInteger(); + protected Map futures; + protected AtomicInteger pendingResponsesCount = new AtomicInteger(); - /** - * Write properties. - */ - protected ByteBuffer sharedBuffer; - protected ReentrantLock bufferLock = new ReentrantLock(false); - protected Condition bufferNotEmpty = bufferLock.newCondition(); - protected Condition bufferEmpty = bufferLock.newCondition(); + /** + * Write properties. + */ + protected ByteBuffer sharedBuffer; + protected ReentrantLock bufferLock = new ReentrantLock(false); + protected Condition bufferNotEmpty = bufferLock.newCondition(); + protected Condition bufferEmpty = bufferLock.newCondition(); - protected ByteBuffer writerBuffer; - protected ReentrantLock writeLock = new ReentrantLock(true); + protected ByteBuffer writerBuffer; + protected ReentrantLock writeLock = new ReentrantLock(true); - /** - * Interfaces. - */ - protected SyncOps syncOps; - protected FireAndForgetOps fireAndForgetOps; - protected ComposableAsyncOps composableAsyncOps; - protected UnsafeSchemaOps unsafeSchemaOps; + /** + * Interfaces. + */ + protected SyncOps syncOps; + protected FireAndForgetOps fireAndForgetOps; + protected ComposableAsyncOps composableAsyncOps; + protected UnsafeSchemaOps unsafeSchemaOps; - /** - * Inner. - */ - protected TarantoolClientStats stats; - protected StateHelper state = new StateHelper(StateHelper.RECONNECT); - protected Thread reader; - protected Thread writer; - - protected TarantoolSchemaMeta schemaMeta = new TarantoolMetaSpacesCache(this); - - protected Thread connector = new Thread(new Runnable() { - @Override - public void run() { - while (!Thread.currentThread().isInterrupted()) { - reconnect(thumbstone); - try { - state.awaitReconnection(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - } - }); + /** + * Inner. + */ + protected TarantoolClientStats stats; + protected StateHelper state = new StateHelper(StateHelper.RECONNECT); + protected Thread reader; + protected Thread writer; - public TarantoolClientImpl(String address, TarantoolClientConfig config) { - this(new SingleSocketChannelProviderImpl(address), config); - } - - public TarantoolClientImpl(SocketChannelProvider socketProvider, TarantoolClientConfig config) { - initClient(socketProvider, config); - if (socketProvider instanceof ConfigurableSocketChannelProvider) { - ConfigurableSocketChannelProvider configurableProvider = (ConfigurableSocketChannelProvider) socketProvider; - configurableProvider.setConnectionTimeout(config.connectionTimeout); - configurableProvider.setRetriesLimit(config.retryCount); - } - startConnector(config.initTimeoutMillis); - } - - private void initClient(SocketChannelProvider socketProvider, TarantoolClientConfig config) { - this.config = config; - this.initialRequestSize = config.defaultRequestSize; - this.operationTimeout = Duration.ofMillis(config.operationExpiryTimeMillis); - this.socketProvider = socketProvider; - this.stats = new TarantoolClientStats(); - this.futures = new ConcurrentHashMap<>(config.predictedFutures); - this.delayedOperationsQueue = new PriorityBlockingQueue<>(128); - this.workExecutor = - Executors.newSingleThreadScheduledExecutor(new TarantoolThreadDaemonFactory("tarantool-worker")); - this.sharedBuffer = ByteBuffer.allocateDirect(config.sharedBufferSize); - this.writerBuffer = ByteBuffer.allocateDirect(sharedBuffer.capacity()); - this.connector.setDaemon(true); - this.connector.setName("Tarantool connector"); - this.syncOps = new SyncOps(); - this.composableAsyncOps = new ComposableAsyncOps(); - this.fireAndForgetOps = new FireAndForgetOps(); - this.unsafeSchemaOps = new UnsafeSchemaOps(); - if (!config.useNewCall) { - setCallCode(Code.OLD_CALL); - this.syncOps.setCallCode(Code.OLD_CALL); - this.fireAndForgetOps.setCallCode(Code.OLD_CALL); - this.composableAsyncOps.setCallCode(Code.OLD_CALL); - } - } + protected TarantoolSchemaMeta schemaMeta = new TarantoolMetaSpacesCache(this); - private void startConnector(long initTimeoutMillis) { - connector.start(); + protected Thread connector = new Thread(new Runnable() { + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + reconnect(thumbstone); try { - if (!waitAlive(initTimeoutMillis, TimeUnit.MILLISECONDS)) { - CommunicationException e = new CommunicationException( - initTimeoutMillis + - "ms is exceeded when waiting for client initialization. " + - "You could configure init timeout in TarantoolConfig", - thumbstone); - - close(e); - throw e; - } + state.awaitReconnection(); } catch (InterruptedException e) { - close(e); - throw new IllegalStateException(e); + Thread.currentThread().interrupt(); } + } } - - protected void reconnect(Throwable lastError) { - SocketChannel channel = null; - int retryNumber = 0; - while (!Thread.currentThread().isInterrupted()) { - try { - if (lastError != null) { - LOGGER.warn(() -> { - SocketAddress address = socketProvider.getAddress(); - return "Attempt to (re-)connect to Tarantool instance " + - (StringUtils.isBlank(config.username) ? "" : config.username + ":*****@") + - (address == null ? "unknown" : address); - }, lastError - ); - } - channel = socketProvider.get(retryNumber++, lastError); - } catch (Exception e) { - closeChannel(channel); - lastError = e; - if (!(e instanceof SocketProviderTransientException)) { - close(e); - return; - } - } - try { - if (channel != null) { - connect(channel); - return; - } - } catch (Exception e) { - closeChannel(channel); - lastError = e; - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - } - } + }); + + public TarantoolClientImpl(String address, TarantoolClientConfig config) { + this(new SingleSocketChannelProviderImpl(address), config); + } + + public TarantoolClientImpl(SocketChannelProvider socketProvider, TarantoolClientConfig config) { + initClient(socketProvider, config); + if (socketProvider instanceof ConfigurableSocketChannelProvider) { + ConfigurableSocketChannelProvider configurableProvider = (ConfigurableSocketChannelProvider) socketProvider; + configurableProvider.setConnectionTimeout(config.connectionTimeout); + configurableProvider.setRetriesLimit(config.retryCount); } - - protected void connect(final SocketChannel channel) throws Exception { - try { - TarantoolGreeting greeting = ProtoUtils.connect(channel, config.username, config.password, msgPackLite); - this.serverVersion = greeting.getServerVersion(); - } catch (IOException e) { - closeChannel(channel); - throw new CommunicationException("Couldn't connect to tarantool", e); + startConnector(config.initTimeoutMillis); + } + + private void initClient(SocketChannelProvider socketProvider, TarantoolClientConfig config) { + this.config = config; + this.initialRequestSize = config.defaultRequestSize; + this.operationTimeout = Duration.ofMillis(config.operationExpiryTimeMillis); + this.socketProvider = socketProvider; + this.stats = new TarantoolClientStats(); + this.futures = new ConcurrentHashMap<>(config.predictedFutures); + this.delayedOperationsQueue = new PriorityBlockingQueue<>(128); + this.workExecutor = + Executors.newSingleThreadScheduledExecutor(new TarantoolThreadDaemonFactory("tarantool-worker")); + this.sharedBuffer = ByteBuffer.allocateDirect(config.sharedBufferSize); + this.writerBuffer = ByteBuffer.allocateDirect(sharedBuffer.capacity()); + this.connector.setDaemon(true); + this.connector.setName("Tarantool connector"); + this.syncOps = new SyncOps(); + this.composableAsyncOps = new ComposableAsyncOps(); + this.fireAndForgetOps = new FireAndForgetOps(); + this.unsafeSchemaOps = new UnsafeSchemaOps(); + if (!config.useNewCall) { + setCallCode(Code.OLD_CALL); + this.syncOps.setCallCode(Code.OLD_CALL); + this.fireAndForgetOps.setCallCode(Code.OLD_CALL); + this.composableAsyncOps.setCallCode(Code.OLD_CALL); + } + } + + private void startConnector(long initTimeoutMillis) { + connector.start(); + try { + if (!waitAlive(initTimeoutMillis, TimeUnit.MILLISECONDS)) { + CommunicationException e = new CommunicationException( + initTimeoutMillis + + "ms is exceeded when waiting for client initialization. " + + "You could configure init timeout in TarantoolConfig", + thumbstone); + + close(e); + throw e; + } + } catch (InterruptedException e) { + close(e); + throw new IllegalStateException(e); + } + } + + protected void reconnect(Throwable lastError) { + SocketChannel channel = null; + int retryNumber = 0; + while (!Thread.currentThread().isInterrupted()) { + try { + if (lastError != null) { + LOGGER.warn(() -> { + SocketAddress address = socketProvider.getAddress(); + return "Attempt to (re-)connect to Tarantool instance " + + (StringUtils.isBlank(config.username) ? "" : config.username + ":*****@") + + (address == null ? "unknown" : address); + }, lastError + ); } - - channel.configureBlocking(false); - this.channel = channel; - this.readChannel = new ReadableViaSelectorChannel(channel); - - bufferLock.lock(); - try { - sharedBuffer.clear(); - } finally { - bufferLock.unlock(); + channel = socketProvider.get(retryNumber++, lastError); + } catch (Exception e) { + closeChannel(channel); + lastError = e; + if (!(e instanceof SocketProviderTransientException)) { + close(e); + return; } - this.thumbstone = null; - startThreads(channel.socket().getRemoteSocketAddress().toString()); - updateSchema(); - } - - protected void startThreads(String threadName) throws InterruptedException { - final CountDownLatch ioThreadStarted = new CountDownLatch(2); - final AtomicInteger leftIoThreads = new AtomicInteger(2); - reader = new Thread(() -> { - ioThreadStarted.countDown(); - if (state.acquire(StateHelper.READING)) { - try { - readThread(); - } finally { - state.release(StateHelper.READING | StateHelper.SCHEMA_UPDATING); - // only last of two IO-threads can signal for reconnection - if (leftIoThreads.decrementAndGet() == 0) { - state.trySignalForReconnection(); - } - } - } - }); - writer = new Thread(() -> { - ioThreadStarted.countDown(); - if (state.acquire(StateHelper.WRITING)) { - try { - writeThread(); - } finally { - state.release(StateHelper.WRITING | StateHelper.SCHEMA_UPDATING); - // only last of two IO-threads can signal for reconnection - if (leftIoThreads.decrementAndGet() == 0) { - state.trySignalForReconnection(); - } - } - } - }); - state.release(StateHelper.RECONNECT); - - configureThreads(threadName); - reader.start(); - writer.start(); - ioThreadStarted.await(); + } + try { + if (channel != null) { + connect(channel); + return; + } + } catch (Exception e) { + closeChannel(channel); + lastError = e; + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + } } - - protected void configureThreads(String threadName) { - reader.setName("Tarantool " + threadName + " reader"); - writer.setName("Tarantool " + threadName + " writer"); - writer.setPriority(config.writerThreadPriority); - reader.setPriority(config.readerThreadPriority); + } + + protected void connect(final SocketChannel channel) throws Exception { + try { + TarantoolGreeting greeting = ProtoUtils.connect(channel, config.username, config.password, msgPackLite); + this.serverVersion = greeting.getServerVersion(); + } catch (IOException e) { + closeChannel(channel); + throw new CommunicationException("Couldn't connect to tarantool", e); } - @Override - public TarantoolSchemaMeta getSchemaMeta() { - return schemaMeta; - } + channel.configureBlocking(false); + this.channel = channel; + this.readChannel = new ReadableViaSelectorChannel(channel); - /** - * Executes an operation with default timeout. - * - * @param request operation data - * - * @return deferred result - * - * @see #setOperationTimeout(long) - */ - @Override - protected Future exec(TarantoolRequest request) { - return doExec(request).getResult(); + bufferLock.lock(); + try { + sharedBuffer.clear(); + } finally { + bufferLock.unlock(); } - - protected TarantoolOperation doExec(TarantoolRequest request) { - long stamp = schemaLock.readLock(); + this.thumbstone = null; + startThreads(channel.socket().getRemoteSocketAddress().toString()); + updateSchema(); + } + + protected void startThreads(String threadName) throws InterruptedException { + final CountDownLatch ioThreadStarted = new CountDownLatch(2); + final AtomicInteger leftIoThreads = new AtomicInteger(2); + reader = new Thread(() -> { + ioThreadStarted.countDown(); + if (state.acquire(StateHelper.READING)) { try { - if (request.getTimeout() == null) { - request.setTimeout(operationTimeout); - } - TarantoolOperation operation = request.toOperation(syncId.incrementAndGet(), schemaMeta.getSchemaVersion()); - // space or index names could not be found in the cache - if (!operation.isSerializable()) { - delayedOperationsQueue.add(operation); - // It's possible the client keeps the outdated schema. - // Send a preflight ping request to check the schema - // version and refresh it if one is obsolete - if (isSchemaLoaded()) { - TarantoolOperation ping = new TarantoolRequest(Code.PING) - .toPreflightOperation(syncId.incrementAndGet(), schemaMeta.getSchemaVersion(), operation); - registerOperation(ping); - } - return operation; - } - // postpone operation if the schema is not ready - if (!isSchemaLoaded()) { - delayedOperationsQueue.add(operation); - return operation; - } - return registerOperation(operation); + readThread(); } finally { - schemaLock.unlockRead(stamp); - } - } - - /** - * Checks whether the schema is fully cached. - * - * @return {@literal true} if the schema is loaded - */ - private boolean isSchemaLoaded() { - return schemaMeta.isInitialized() && !state.isStateSet(StateHelper.SCHEMA_UPDATING); - } - - protected TarantoolOperation registerOperation(TarantoolOperation operation) { - if (isDead(operation)) { - return operation; - } - futures.put(operation.getId(), operation); - if (isDead(operation)) { - futures.remove(operation.getId()); - return operation; + state.release(StateHelper.READING | StateHelper.SCHEMA_UPDATING); + // only last of two IO-threads can signal for reconnection + if (leftIoThreads.decrementAndGet() == 0) { + state.trySignalForReconnection(); + } } + } + }); + writer = new Thread(() -> { + ioThreadStarted.countDown(); + if (state.acquire(StateHelper.WRITING)) { try { - write( - operation.getCode(), - operation.getId(), - operation.getSentSchemaId(), - operation.getArguments().toArray() - ); - } catch (Exception e) { - futures.remove(operation.getId()); - fail(operation, e); + writeThread(); + } finally { + state.release(StateHelper.WRITING | StateHelper.SCHEMA_UPDATING); + // only last of two IO-threads can signal for reconnection + if (leftIoThreads.decrementAndGet() == 0) { + state.trySignalForReconnection(); + } } + } + }); + state.release(StateHelper.RECONNECT); + + configureThreads(threadName); + reader.start(); + writer.start(); + ioThreadStarted.await(); + } + + protected void configureThreads(String threadName) { + reader.setName("Tarantool " + threadName + " reader"); + writer.setName("Tarantool " + threadName + " writer"); + writer.setPriority(config.writerThreadPriority); + reader.setPriority(config.readerThreadPriority); + } + + @Override + public TarantoolSchemaMeta getSchemaMeta() { + return schemaMeta; + } + + /** + * Executes an operation with default timeout. + * + * @param request operation data + * @return deferred result + * @see #setOperationTimeout(long) + */ + @Override + protected Future exec(TarantoolRequest request) { + return doExec(request).getResult(); + } + + protected TarantoolOperation doExec(TarantoolRequest request) { + long stamp = schemaLock.readLock(); + try { + if (request.getTimeout() == null) { + request.setTimeout(operationTimeout); + } + TarantoolOperation operation = request.toOperation(syncId.incrementAndGet(), schemaMeta.getSchemaVersion()); + // space or index names could not be found in the cache + if (!operation.isSerializable()) { + delayedOperationsQueue.add(operation); + // It's possible the client keeps the outdated schema. + // Send a preflight ping request to check the schema + // version and refresh it if one is obsolete + if (isSchemaLoaded()) { + TarantoolOperation ping = new TarantoolRequest(Code.PING) + .toPreflightOperation(syncId.incrementAndGet(), schemaMeta.getSchemaVersion(), operation); + registerOperation(ping); + } + return operation; + } + // postpone operation if the schema is not ready + if (!isSchemaLoaded()) { + delayedOperationsQueue.add(operation); return operation; + } + return registerOperation(operation); + } finally { + schemaLock.unlockRead(stamp); } - - protected synchronized void die(String message, Exception cause) { - if (thumbstone != null) { - return; - } - final CommunicationException error = new CommunicationException(message, cause); - this.thumbstone = error; - while (!futures.isEmpty()) { - Iterator> iterator = futures.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry elem = iterator.next(); - if (elem != null) { - TarantoolOperation operation = elem.getValue(); - fail(operation, error); - } - iterator.remove(); - } - } - - TarantoolOperation operation; - while ((operation = delayedOperationsQueue.poll()) != null) { - fail(operation, error); - } - - pendingResponsesCount.set(0); - - bufferLock.lock(); - try { - sharedBuffer.clear(); - bufferEmpty.signalAll(); - } finally { - bufferLock.unlock(); - } - stopIO(); + } + + /** + * Checks whether the schema is fully cached. + * + * @return {@literal true} if the schema is loaded + */ + private boolean isSchemaLoaded() { + return schemaMeta.isInitialized() && !state.isStateSet(StateHelper.SCHEMA_UPDATING); + } + + protected TarantoolOperation registerOperation(TarantoolOperation operation) { + if (isDead(operation)) { + return operation; } - - @Override - public void ping() { - syncGet(exec(new TarantoolRequest(Code.PING))); + futures.put(operation.getId(), operation); + if (isDead(operation)) { + futures.remove(operation.getId()); + return operation; } - - protected void write(Code code, Long syncId, Long schemaId, Object... args) - throws Exception { - ByteBuffer buffer = ProtoUtils.createPacket(msgPackLite, code, syncId, schemaId, args); - - if (directWrite(buffer)) { - return; - } - sharedWrite(buffer); - + try { + write( + operation.getCode(), + operation.getId(), + operation.getSentSchemaId(), + operation.getArguments().toArray() + ); + } catch (Exception e) { + futures.remove(operation.getId()); + fail(operation, e); } + return operation; + } - protected void sharedWrite(ByteBuffer buffer) throws InterruptedException, TimeoutException { - long start = System.currentTimeMillis(); - if (bufferLock.tryLock(config.writeTimeoutMillis, TimeUnit.MILLISECONDS)) { - try { - int rem = buffer.remaining(); - stats.sharedMaxPacketSize = Math.max(stats.sharedMaxPacketSize, rem); - if (rem > initialRequestSize) { - stats.sharedPacketSizeGrowth++; - } - while (sharedBuffer.remaining() < buffer.limit()) { - stats.sharedEmptyAwait++; - long remaining = config.writeTimeoutMillis - (System.currentTimeMillis() - start); - try { - if (remaining < 1 || !bufferEmpty.await(remaining, TimeUnit.MILLISECONDS)) { - stats.sharedEmptyAwaitTimeouts++; - throw new TimeoutException( - config.writeTimeoutMillis + - "ms is exceeded while waiting for empty buffer. " + - "You could configure write timeout it in TarantoolConfig" - ); - } - } catch (InterruptedException e) { - throw new CommunicationException("Interrupted", e); - } - } - sharedBuffer.put(buffer); - pendingResponsesCount.incrementAndGet(); - bufferNotEmpty.signalAll(); - stats.buffered++; - } finally { - bufferLock.unlock(); - } - } else { - stats.sharedWriteLockTimeouts++; - throw new TimeoutException( - config.writeTimeoutMillis + - "ms is exceeded while waiting for shared buffer lock. " + - "You could configure write timeout in TarantoolConfig" - ); - } + protected synchronized void die(String message, Exception cause) { + if (thumbstone != null) { + return; } - - private boolean directWrite(ByteBuffer buffer) throws InterruptedException, IOException, TimeoutException { - if (sharedBuffer.capacity() * config.directWriteFactor <= buffer.limit()) { - if (writeLock.tryLock(config.writeTimeoutMillis, TimeUnit.MILLISECONDS)) { - try { - int rem = buffer.remaining(); - stats.directMaxPacketSize = Math.max(stats.directMaxPacketSize, rem); - if (rem > initialRequestSize) { - stats.directPacketSizeGrowth++; - } - writeFully(channel, buffer); - stats.directWrite++; - pendingResponsesCount.incrementAndGet(); - } finally { - writeLock.unlock(); - } - return true; - } else { - stats.directWriteLockTimeouts++; - throw new TimeoutException( - config.writeTimeoutMillis + - "ms is exceeded while waiting for channel lock. " + - "You could configure write timeout in TarantoolConfig" - ); - } + final CommunicationException error = new CommunicationException(message, cause); + this.thumbstone = error; + while (!futures.isEmpty()) { + Iterator> iterator = futures.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry elem = iterator.next(); + if (elem != null) { + TarantoolOperation operation = elem.getValue(); + fail(operation, error); } - return false; + iterator.remove(); + } } - protected void readThread() { - while (!Thread.currentThread().isInterrupted()) { - try { - TarantoolPacket packet = ProtoUtils.readPacket(readChannel, msgPackLite); - - Map headers = packet.getHeaders(); - - Long syncId = (Long) headers.get(Key.SYNC.getId()); - TarantoolOperation request = futures.remove(syncId); - stats.received++; - pendingResponsesCount.decrementAndGet(); - complete(packet, request); - } catch (Exception e) { - die("Cant read answer", e); - return; - } - } + TarantoolOperation operation; + while ((operation = delayedOperationsQueue.poll()) != null) { + fail(operation, error); } - protected void writeThread() { - writerBuffer.clear(); - while (!Thread.currentThread().isInterrupted()) { - try { - bufferLock.lock(); - try { - while (sharedBuffer.position() == 0) { - bufferNotEmpty.await(); - } - sharedBuffer.flip(); - writerBuffer.put(sharedBuffer); - sharedBuffer.clear(); - bufferEmpty.signalAll(); - } finally { - bufferLock.unlock(); - } - writerBuffer.flip(); - writeLock.lock(); - try { - writeFully(channel, writerBuffer); - } finally { - writeLock.unlock(); - } - writerBuffer.clear(); - stats.sharedWrites++; - } catch (Exception e) { - die("Cant write bytes", e); - return; - } - } - } + pendingResponsesCount.set(0); - protected void fail(TarantoolOperation operation, Exception e) { - operation.getResult().completeExceptionally(e); + bufferLock.lock(); + try { + sharedBuffer.clear(); + bufferEmpty.signalAll(); + } finally { + bufferLock.unlock(); } + stopIO(); + } - protected void complete(TarantoolPacket packet, TarantoolOperation operation) { - CompletableFuture result = operation.getResult(); - if (result.isDone()) { - return; - } - - long code = packet.getCode(); - long schemaId = packet.getSchemaId(); - boolean isPreflightPing = operation.getDependedOperation() != null; - if (code == ProtoConstants.SUCCESS) { - operation.setCompletedSchemaId(schemaId); - if (isPreflightPing) { - // the schema wasn't changed - // try to evaluate an unserializable target operation - // in order to complete the operation exceptionally. - TarantoolOperation target = operation.getDependedOperation(); - delayedOperationsQueue.remove(target); - try { - target.getArguments(); - } catch (TarantoolSchemaException cause) { - fail(target, cause); - } - } else if (operation.getCode() == Code.EXECUTE) { - completeSql(operation, packet); - } else { - ((CompletableFuture) result).complete(packet.getData()); - } - } else if (code == ProtoConstants.ERR_WRONG_SCHEMA_VERSION) { - if (schemaId > schemaMeta.getSchemaVersion()) { - delayedOperationsQueue.add(operation); - } else { - operation.setSentSchemaId(schemaMeta.getSchemaVersion()); - registerOperation(operation); - } - } else { - Object error = packet.getError(); - fail(operation, serverError(code, error)); - } + @Override + public void ping() { + syncGet(exec(new TarantoolRequest(Code.PING))); + } - if (operation.getSentSchemaId() == 0) { - return; - } - // it's possible to receive bigger version than current - // i.e. after DDL operation or wrong schema version response - if (schemaId > schemaMeta.getSchemaVersion()) { - updateSchema(); - } - } + protected void write(Code code, Long syncId, Long schemaId, Object... args) + throws Exception { + ByteBuffer buffer = ProtoUtils.createPacket(msgPackLite, code, syncId, schemaId, args); - private void updateSchema() { - performSchemaAction(() -> { - if (state.acquire(StateHelper.SCHEMA_UPDATING)) { - workExecutor.execute(createUpdateSchemaTask()); - } - }); + if (directWrite(buffer)) { + return; } - - private Runnable createUpdateSchemaTask() { - return () -> { - try { - schemaMeta.refresh(); - } catch (Exception cause) { - workExecutor.schedule(createUpdateSchemaTask(), 300L, TimeUnit.MILLISECONDS); - return; - } - performSchemaAction(() -> { - try { - rescheduleDelayedOperations(); - } finally { - state.release(StateHelper.SCHEMA_UPDATING); - } - }); - }; - } - - private void rescheduleDelayedOperations() { - TarantoolOperation operation; - while ((operation = delayedOperationsQueue.poll()) != null) { - CompletableFuture result = operation.getResult(); - if (!result.isDone()) { - operation.setSentSchemaId(schemaMeta.getSchemaVersion()); - registerOperation(operation); + sharedWrite(buffer); + + } + + protected void sharedWrite(ByteBuffer buffer) throws InterruptedException, TimeoutException { + long start = System.currentTimeMillis(); + if (bufferLock.tryLock(config.writeTimeoutMillis, TimeUnit.MILLISECONDS)) { + try { + int rem = buffer.remaining(); + stats.sharedMaxPacketSize = Math.max(stats.sharedMaxPacketSize, rem); + if (rem > initialRequestSize) { + stats.sharedPacketSizeGrowth++; + } + while (sharedBuffer.remaining() < buffer.limit()) { + stats.sharedEmptyAwait++; + long remaining = config.writeTimeoutMillis - (System.currentTimeMillis() - start); + try { + if (remaining < 1 || !bufferEmpty.await(remaining, TimeUnit.MILLISECONDS)) { + stats.sharedEmptyAwaitTimeouts++; + throw new TimeoutException( + config.writeTimeoutMillis + + "ms is exceeded while waiting for empty buffer. " + + "You could configure write timeout it in TarantoolConfig" + ); } + } catch (InterruptedException e) { + throw new CommunicationException("Interrupted", e); + } } + sharedBuffer.put(buffer); + pendingResponsesCount.incrementAndGet(); + bufferNotEmpty.signalAll(); + stats.buffered++; + } finally { + bufferLock.unlock(); + } + } else { + stats.sharedWriteLockTimeouts++; + throw new TimeoutException( + config.writeTimeoutMillis + + "ms is exceeded while waiting for shared buffer lock. " + + "You could configure write timeout in TarantoolConfig" + ); } + } - protected void completeSql(TarantoolOperation operation, TarantoolPacket pack) { - Long rowCount = SqlProtoUtils.getSQLRowCount(pack); - CompletableFuture result = operation.getResult(); - if (rowCount != null) { - ((CompletableFuture) result).complete(rowCount); - } else { - List> values = SqlProtoUtils.readSqlResult(pack); - ((CompletableFuture) result).complete(values); + private boolean directWrite(ByteBuffer buffer) throws InterruptedException, IOException, TimeoutException { + if (sharedBuffer.capacity() * config.directWriteFactor <= buffer.limit()) { + if (writeLock.tryLock(config.writeTimeoutMillis, TimeUnit.MILLISECONDS)) { + try { + int rem = buffer.remaining(); + stats.directMaxPacketSize = Math.max(stats.directMaxPacketSize, rem); + if (rem > initialRequestSize) { + stats.directPacketSizeGrowth++; + } + writeFully(channel, buffer); + stats.directWrite++; + pendingResponsesCount.incrementAndGet(); + } finally { + writeLock.unlock(); } + return true; + } else { + stats.directWriteLockTimeouts++; + throw new TimeoutException( + config.writeTimeoutMillis + + "ms is exceeded while waiting for channel lock. " + + "You could configure write timeout in TarantoolConfig" + ); + } } + return false; + } + + protected void readThread() { + while (!Thread.currentThread().isInterrupted()) { + try { + TarantoolPacket packet = ProtoUtils.readPacket(readChannel, msgPackLite); + + Map headers = packet.getHeaders(); + + Long syncId = (Long) headers.get(Key.SYNC.getId()); + TarantoolOperation request = futures.remove(syncId); + stats.received++; + pendingResponsesCount.decrementAndGet(); + complete(packet, request); + } catch (Exception e) { + die("Cant read answer", e); + return; + } + } + } - /** - * Convenient guard scope that executes given runnable - * inside schema write lock. - * - * @param action to be executed - */ - protected void performSchemaAction(Runnable action) { - long stamp = schemaLock.writeLock(); + protected void writeThread() { + writerBuffer.clear(); + while (!Thread.currentThread().isInterrupted()) { + try { + bufferLock.lock(); try { - action.run(); + while (sharedBuffer.position() == 0) { + bufferNotEmpty.await(); + } + sharedBuffer.flip(); + writerBuffer.put(sharedBuffer); + sharedBuffer.clear(); + bufferEmpty.signalAll(); } finally { - schemaLock.unlockWrite(stamp); + bufferLock.unlock(); } - } - - protected T syncGet(Future result) { + writerBuffer.flip(); + writeLock.lock(); try { - return result.get(); - } catch (ExecutionException e) { - if (e.getCause() instanceof CommunicationException) { - throw (CommunicationException) e.getCause(); - } else if (e.getCause() instanceof TarantoolException) { - throw (TarantoolException) e.getCause(); - } else { - throw new IllegalStateException(e.getCause()); - } - } catch (InterruptedException e) { - throw new IllegalStateException(e); + writeFully(channel, writerBuffer); + } finally { + writeLock.unlock(); } + writerBuffer.clear(); + stats.sharedWrites++; + } catch (Exception e) { + die("Cant write bytes", e); + return; + } } + } + + protected void fail(TarantoolOperation operation, Exception e) { + operation.getResult().completeExceptionally(e); + } - protected void writeFully(SocketChannel channel, ByteBuffer buffer) throws IOException { - ProtoUtils.writeFully(channel, buffer); + protected void complete(TarantoolPacket packet, TarantoolOperation operation) { + CompletableFuture result = operation.getResult(); + if (result.isDone()) { + return; } - @Override - public void close() { - close(new Exception("Connection is closed.")); + long code = packet.getCode(); + long schemaId = packet.getSchemaId(); + boolean isPreflightPing = operation.getDependedOperation() != null; + if (code == ProtoConstants.SUCCESS) { + operation.setCompletedSchemaId(schemaId); + if (isPreflightPing) { + // the schema wasn't changed + // try to evaluate an unserializable target operation + // in order to complete the operation exceptionally. + TarantoolOperation target = operation.getDependedOperation(); + delayedOperationsQueue.remove(target); try { - state.awaitState(StateHelper.CLOSED); - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); + target.getArguments(); + } catch (TarantoolSchemaException cause) { + fail(target, cause); } + } else if (operation.getCode() == Code.EXECUTE) { + completeSql(operation, packet); + } else { + ((CompletableFuture) result).complete(packet.getData()); + } + } else if (code == ProtoConstants.ERR_WRONG_SCHEMA_VERSION) { + if (schemaId > schemaMeta.getSchemaVersion()) { + delayedOperationsQueue.add(operation); + } else { + operation.setSentSchemaId(schemaMeta.getSchemaVersion()); + registerOperation(operation); + } + } else { + Object error = packet.getError(); + fail(operation, serverError(code, error)); } - protected void close(Exception e) { - if (state.close()) { - if (workExecutor != null) { - workExecutor.shutdownNow(); - } - connector.interrupt(); - die(e.getMessage(), e); - } + if (operation.getSentSchemaId() == 0) { + return; + } + // it's possible to receive bigger version than current + // i.e. after DDL operation or wrong schema version response + if (schemaId > schemaMeta.getSchemaVersion()) { + updateSchema(); } + } - protected void stopIO() { - if (reader != null) { - reader.interrupt(); - } - if (writer != null) { - writer.interrupt(); - } - if (readChannel != null) { - try { - readChannel.close(); // also closes this.channel - } catch (IOException ignored) { - // no-op - } + private void updateSchema() { + performSchemaAction(() -> { + if (state.acquire(StateHelper.SCHEMA_UPDATING)) { + workExecutor.execute(createUpdateSchemaTask()); + } + }); + } + + private Runnable createUpdateSchemaTask() { + return () -> { + try { + schemaMeta.refresh(); + } catch (Exception cause) { + workExecutor.schedule(createUpdateSchemaTask(), 300L, TimeUnit.MILLISECONDS); + return; + } + performSchemaAction(() -> { + try { + rescheduleDelayedOperations(); + } finally { + state.release(StateHelper.SCHEMA_UPDATING); } - closeChannel(channel); + }); + }; + } + + private void rescheduleDelayedOperations() { + TarantoolOperation operation; + while ((operation = delayedOperationsQueue.poll()) != null) { + CompletableFuture result = operation.getResult(); + if (!result.isDone()) { + operation.setSentSchemaId(schemaMeta.getSchemaVersion()); + registerOperation(operation); + } } - - /** - * Gets the default timeout for client operations. - * - * @return timeout in millis - */ - public long getOperationTimeout() { - return operationTimeout.toMillis(); + } + + protected void completeSql(TarantoolOperation operation, TarantoolPacket pack) { + Long rowCount = SqlProtoUtils.getSQLRowCount(pack); + CompletableFuture result = operation.getResult(); + if (rowCount != null) { + ((CompletableFuture) result).complete(rowCount); + } else { + List> values = SqlProtoUtils.readSqlResult(pack); + ((CompletableFuture) result).complete(values); } - - /** - * Sets the default operation timeout. - * - * @param operationTimeout timeout in millis - */ - public void setOperationTimeout(long operationTimeout) { - this.operationTimeout = Duration.ofMillis(operationTimeout); + } + + /** + * Convenient guard scope that executes given runnable inside schema write lock. + * + * @param action to be executed + */ + protected void performSchemaAction(Runnable action) { + long stamp = schemaLock.writeLock(); + try { + action.run(); + } finally { + schemaLock.unlockWrite(stamp); } - - @Override - public boolean isAlive() { - return state.isStateSet(StateHelper.ALIVE) && thumbstone == null; + } + + protected T syncGet(Future result) { + try { + return result.get(); + } catch (ExecutionException e) { + if (e.getCause() instanceof CommunicationException) { + throw (CommunicationException) e.getCause(); + } else if (e.getCause() instanceof TarantoolException) { + throw (TarantoolException) e.getCause(); + } else { + throw new IllegalStateException(e.getCause()); + } + } catch (InterruptedException e) { + throw new IllegalStateException(e); } - - @Override - public boolean isClosed() { - return state.isStateSet(StateHelper.CLOSED); + } + + protected void writeFully(SocketChannel channel, ByteBuffer buffer) throws IOException { + ProtoUtils.writeFully(channel, buffer); + } + + @Override + public void close() { + close(new Exception("Connection is closed.")); + try { + state.awaitState(StateHelper.CLOSED); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); } - - @Override - public void waitAlive() throws InterruptedException { - state.awaitState(StateHelper.ALIVE); + } + + protected void close(Exception e) { + if (state.close()) { + if (workExecutor != null) { + workExecutor.shutdownNow(); + } + connector.interrupt(); + die(e.getMessage(), e); } + } - @Override - public boolean waitAlive(long timeout, TimeUnit unit) throws InterruptedException { - return state.awaitState(StateHelper.ALIVE, timeout, unit); + protected void stopIO() { + if (reader != null) { + reader.interrupt(); } - - @Override - public TarantoolClientOps, Object, List> syncOps() { - return syncOps; + if (writer != null) { + writer.interrupt(); } - - @Override - public TarantoolClientOps, Object, Future>> asyncOps() { - return (TarantoolClientOps) this; + if (readChannel != null) { + try { + readChannel.close(); // also closes this.channel + } catch (IOException ignored) { + // no-op + } } + closeChannel(channel); + } + + /** + * Gets the default timeout for client operations. + * + * @return timeout in millis + */ + public long getOperationTimeout() { + return operationTimeout.toMillis(); + } + + /** + * Sets the default operation timeout. + * + * @param operationTimeout timeout in millis + */ + public void setOperationTimeout(long operationTimeout) { + this.operationTimeout = Duration.ofMillis(operationTimeout); + } + + @Override + public boolean isAlive() { + return state.isStateSet(StateHelper.ALIVE) && thumbstone == null; + } + + @Override + public boolean isClosed() { + return state.isStateSet(StateHelper.CLOSED); + } + + @Override + public void waitAlive() throws InterruptedException { + state.awaitState(StateHelper.ALIVE); + } + + @Override + public boolean waitAlive(long timeout, TimeUnit unit) throws InterruptedException { + return state.awaitState(StateHelper.ALIVE, timeout, unit); + } + + @Override + public TarantoolClientOps, Object, List> syncOps() { + return syncOps; + } + + @Override + public TarantoolClientOps, Object, Future>> asyncOps() { + return (TarantoolClientOps) this; + } + + @Override + public TarantoolClientOps, Object, CompletionStage>> composableAsyncOps() { + return composableAsyncOps; + } + + @Override + public TarantoolClientOps, Object, Long> fireAndForgetOps() { + return fireAndForgetOps; + } + + public TarantoolClientOps, Object, TupleTwo, Long>> unsafeSchemaOps() { + return unsafeSchemaOps; + } + + protected TarantoolRequest makeSqlRequest(String sql, List bind) { + return new TarantoolRequest( + Code.EXECUTE, + TarantoolRequestArgumentFactory.value(Key.SQL_TEXT), + TarantoolRequestArgumentFactory.value(sql), + TarantoolRequestArgumentFactory.value(Key.SQL_BIND), + TarantoolRequestArgumentFactory.value(bind) + ); + } + + @Override + public TarantoolSQLOps>> sqlSyncOps() { + return new TarantoolSQLOps>>() { + @Override + public Long update(String sql, Object... bind) { + return (Long) syncGet(exec(makeSqlRequest(sql, Arrays.asList(bind)))); + } + + @Override + public List> query(String sql, Object... bind) { + return (List>) syncGet(exec(makeSqlRequest(sql, Arrays.asList(bind)))); + } + }; + } + + @Override + public TarantoolSQLOps, Future>>> sqlAsyncOps() { + return new TarantoolSQLOps, Future>>>() { + @Override + public Future update(String sql, Object... bind) { + return (Future) exec(makeSqlRequest(sql, Arrays.asList(bind))); + } + + @Override + public Future>> query(String sql, Object... bind) { + return (Future>>) exec(makeSqlRequest(sql, Arrays.asList(bind))); + } + }; + } + + protected class SyncOps extends BaseClientOps> { @Override - public TarantoolClientOps, Object, CompletionStage>> composableAsyncOps() { - return composableAsyncOps; + protected List exec(TarantoolRequest request) { + return (List) syncGet(TarantoolClientImpl.this.exec(request)); } - @Override - public TarantoolClientOps, Object, Long> fireAndForgetOps() { - return fireAndForgetOps; - } + } - public TarantoolClientOps, Object, TupleTwo, Long>> unsafeSchemaOps() { - return unsafeSchemaOps; - } + protected class FireAndForgetOps extends BaseClientOps { - protected TarantoolRequest makeSqlRequest(String sql, List bind) { - return new TarantoolRequest( - Code.EXECUTE, - TarantoolRequestArgumentFactory.value(Key.SQL_TEXT), - TarantoolRequestArgumentFactory.value(sql), - TarantoolRequestArgumentFactory.value(Key.SQL_BIND), - TarantoolRequestArgumentFactory.value(bind) - ); + @Override + protected Long exec(TarantoolRequest request) { + if (thumbstone == null) { + try { + return doExec(request).getId(); + } catch (Exception e) { + throw new CommunicationException("Execute failed", e); + } + } else { + throw new CommunicationException("Connection is not alive", thumbstone); + } } - @Override - public TarantoolSQLOps>> sqlSyncOps() { - return new TarantoolSQLOps>>() { - @Override - public Long update(String sql, Object... bind) { - return (Long) syncGet(exec(makeSqlRequest(sql, Arrays.asList(bind)))); - } + } - @Override - public List> query(String sql, Object... bind) { - return (List>) syncGet(exec(makeSqlRequest(sql, Arrays.asList(bind)))); - } - }; + protected boolean isDead(TarantoolOperation operation) { + if (this.thumbstone != null) { + fail(operation, new CommunicationException("Connection is dead", thumbstone)); + return true; } + return false; + } - @Override - public TarantoolSQLOps, Future>>> sqlAsyncOps() { - return new TarantoolSQLOps, Future>>>() { - @Override - public Future update(String sql, Object... bind) { - return (Future) exec(makeSqlRequest(sql, Arrays.asList(bind))); - } + /** + * A subclass may use this as a trigger to start retries. This method is called when state becomes ALIVE. + */ + protected void onReconnect() { + // No-op, override. + } - @Override - public Future>> query(String sql, Object... bind) { - return (Future>>) exec(makeSqlRequest(sql, Arrays.asList(bind))); - } - }; - } + public Exception getThumbstone() { + return thumbstone; + } - protected class SyncOps extends BaseClientOps> { + public TarantoolClientStats getStats() { + return stats; + } - @Override - protected List exec(TarantoolRequest request) { - return (List) syncGet(TarantoolClientImpl.this.exec(request)); - } + /** + * Manages state changes. + */ + protected final class StateHelper { - } + static final int UNINITIALIZED = 0; + static final int READING = 1; + static final int WRITING = 1 << 1; + static final int ALIVE = READING | WRITING; + static final int SCHEMA_UPDATING = 1 << 2; + static final int RECONNECT = 1 << 3; + static final int CLOSED = 1 << 4; - protected class FireAndForgetOps extends BaseClientOps { - - @Override - protected Long exec(TarantoolRequest request) { - if (thumbstone == null) { - try { - return doExec(request).getId(); - } catch (Exception e) { - throw new CommunicationException("Execute failed", e); - } - } else { - throw new CommunicationException("Connection is not alive", thumbstone); - } - } + private final AtomicInteger state; - } + private final AtomicReference nextAliveLatch = + new AtomicReference<>(new CountDownLatch(1)); - protected boolean isDead(TarantoolOperation operation) { - if (this.thumbstone != null) { - fail(operation, new CommunicationException("Connection is dead", thumbstone)); - return true; - } - return false; - } + private final CountDownLatch closedLatch = new CountDownLatch(1); /** - * A subclass may use this as a trigger to start retries. - * This method is called when state becomes ALIVE. + * The condition variable to signal a reconnection is needed from reader / writer threads and waiting for that signal from the reconnection + * thread. + *

+ * The lock variable to access this condition. + * + * @see #awaitReconnection() + * @see #trySignalForReconnection() */ - protected void onReconnect() { - // No-op, override. + protected final ReentrantLock connectorLock = new ReentrantLock(); + protected final Condition reconnectRequired = connectorLock.newCondition(); + + protected StateHelper(int state) { + this.state = new AtomicInteger(state); } - public Exception getThumbstone() { - return thumbstone; + protected int getState() { + return state.get(); } - public TarantoolClientStats getStats() { - return stats; + boolean isStateSet(int mask) { + return (getState() & mask) == mask; } /** - * Manages state changes. + * Set CLOSED state, drop RECONNECT state. + * + * @return whether a state was changed to CLOSED */ - protected final class StateHelper { - - static final int UNINITIALIZED = 0; - static final int READING = 1; - static final int WRITING = 1 << 1; - static final int ALIVE = READING | WRITING; - static final int SCHEMA_UPDATING = 1 << 2; - static final int RECONNECT = 1 << 3; - static final int CLOSED = 1 << 4; - - private final AtomicInteger state; - - private final AtomicReference nextAliveLatch = - new AtomicReference<>(new CountDownLatch(1)); - - private final CountDownLatch closedLatch = new CountDownLatch(1); - - /** - * The condition variable to signal a reconnection is needed from reader / - * writer threads and waiting for that signal from the reconnection thread. - *

- * The lock variable to access this condition. - * - * @see #awaitReconnection() - * @see #trySignalForReconnection() - */ - protected final ReentrantLock connectorLock = new ReentrantLock(); - protected final Condition reconnectRequired = connectorLock.newCondition(); - - protected StateHelper(int state) { - this.state = new AtomicInteger(state); - } + protected boolean close() { + for (; ; ) { + int currentState = getState(); - protected int getState() { - return state.get(); + /* CLOSED is the terminal state. */ + if (isStateSet(CLOSED)) { + return false; } - boolean isStateSet(int mask) { - return (getState() & mask) == mask; + /* Clear all states and set CLOSED. */ + if (compareAndSet(currentState, CLOSED)) { + return true; } + } + } - /** - * Set CLOSED state, drop RECONNECT state. - * - * @return whether a state was changed to CLOSED - */ - protected boolean close() { - for (; ; ) { - int currentState = getState(); - - /* CLOSED is the terminal state. */ - if (isStateSet(CLOSED)) { - return false; - } - - /* Clear all states and set CLOSED. */ - if (compareAndSet(currentState, CLOSED)) { - return true; - } - } - } + /** + * Move from a current state to a given one. + *

+ * Some moves are forbidden. + * + * @param mask union of states + * @return whether a state was changed to given one + */ + protected boolean acquire(int mask) { + for (; ; ) { + int currentState = getState(); - /** - * Move from a current state to a given one. - *

- * Some moves are forbidden. - * - * @param mask union of states - * - * @return whether a state was changed to given one - */ - protected boolean acquire(int mask) { - for (; ; ) { - int currentState = getState(); - - /* CLOSED is the terminal state. */ - if ((isStateSet(CLOSED))) { - return false; - } - - /* Don't move to READING, WRITING or ALIVE from RECONNECT. */ - if ((currentState & RECONNECT) > mask) { - return false; - } - - /* Cannot move from a state to the same state. */ - if (isStateSet(mask)) { - return false; - } - - /* Set acquired state. */ - if (compareAndSet(currentState, currentState | mask)) { - return true; - } - } + /* CLOSED is the terminal state. */ + if ((isStateSet(CLOSED))) { + return false; } - protected void release(int mask) { - for (; ; ) { - int currentState = getState(); - if (compareAndSet(currentState, currentState & ~mask)) { - return; - } - } + /* Don't move to READING, WRITING or ALIVE from RECONNECT. */ + if ((currentState & RECONNECT) > mask) { + return false; } - protected boolean compareAndSet(int expect, int update) { - if (!state.compareAndSet(expect, update)) { - return false; - } + /* Cannot move from a state to the same state. */ + if (isStateSet(mask)) { + return false; + } - boolean wasAlreadyAlive = (expect & ALIVE) == ALIVE; - if (!wasAlreadyAlive && (update & ALIVE) == ALIVE) { - CountDownLatch latch = nextAliveLatch.getAndSet(new CountDownLatch(1)); - latch.countDown(); - onReconnect(); - } else if (update == CLOSED) { - closedLatch.countDown(); - } - return true; + /* Set acquired state. */ + if (compareAndSet(currentState, currentState | mask)) { + return true; } + } + } - /** - * Reconnection uses another way to await state via receiving a signal - * instead of latches. - * - * @param state desired state - * - * @throws InterruptedException if the current thread is interrupted - */ - protected void awaitState(int state) throws InterruptedException { - if (state == RECONNECT) { - awaitReconnection(); - } else { - CountDownLatch latch = getStateLatch(state); - if (latch != null) { - latch.await(); - } - } + protected void release(int mask) { + for (; ; ) { + int currentState = getState(); + if (compareAndSet(currentState, currentState & ~mask)) { + return; } + } + } + + protected boolean compareAndSet(int expect, int update) { + if (!state.compareAndSet(expect, update)) { + return false; + } + + boolean wasAlreadyAlive = (expect & ALIVE) == ALIVE; + if (!wasAlreadyAlive && (update & ALIVE) == ALIVE) { + CountDownLatch latch = nextAliveLatch.getAndSet(new CountDownLatch(1)); + latch.countDown(); + onReconnect(); + } else if (update == CLOSED) { + closedLatch.countDown(); + } + return true; + } - protected boolean awaitState(int state, long timeout, TimeUnit timeUnit) throws InterruptedException { - CountDownLatch latch = getStateLatch(state); - return (latch == null) || latch.await(timeout, timeUnit); + /** + * Reconnection uses another way to await state via receiving a signal instead of latches. + * + * @param state desired state + * @throws InterruptedException if the current thread is interrupted + */ + protected void awaitState(int state) throws InterruptedException { + if (state == RECONNECT) { + awaitReconnection(); + } else { + CountDownLatch latch = getStateLatch(state); + if (latch != null) { + latch.await(); } + } + } - private CountDownLatch getStateLatch(int state) { - if (state == CLOSED) { - return closedLatch; - } - if (state == ALIVE) { - if (isStateSet(CLOSED)) { - throw new IllegalStateException("State is CLOSED."); - } - CountDownLatch latch = nextAliveLatch.get(); + protected boolean awaitState(int state, long timeout, TimeUnit timeUnit) throws InterruptedException { + CountDownLatch latch = getStateLatch(state); + return (latch == null) || latch.await(timeout, timeUnit); + } + + private CountDownLatch getStateLatch(int state) { + if (state == CLOSED) { + return closedLatch; + } + if (state == ALIVE) { + if (isStateSet(CLOSED)) { + throw new IllegalStateException("State is CLOSED."); + } + CountDownLatch latch = nextAliveLatch.get(); /* It may happen so that an error is detected but the state is still alive. Wait for the 'next' alive state in such cases. */ - return (isStateSet(ALIVE) && thumbstone == null) ? null : latch; - } - return null; - } + return (isStateSet(ALIVE) && thumbstone == null) ? null : latch; + } + return null; + } - /** - * Blocks until a reconnection signal will be received. - * - * @see #trySignalForReconnection() - */ - private void awaitReconnection() throws InterruptedException { - connectorLock.lock(); - try { - while (!isStateSet(RECONNECT)) { - reconnectRequired.await(); - } - } finally { - connectorLock.unlock(); - } + /** + * Blocks until a reconnection signal will be received. + * + * @see #trySignalForReconnection() + */ + private void awaitReconnection() throws InterruptedException { + connectorLock.lock(); + try { + while (!isStateSet(RECONNECT)) { + reconnectRequired.await(); } + } finally { + connectorLock.unlock(); + } + } - /** - * Signals to the connector that reconnection process can be performed. - * - * @see #awaitReconnection() - */ - private void trySignalForReconnection() { - if (compareAndSet(StateHelper.UNINITIALIZED, StateHelper.RECONNECT)) { - connectorLock.lock(); - try { - reconnectRequired.signal(); - } finally { - connectorLock.unlock(); - } - } + /** + * Signals to the connector that reconnection process can be performed. + * + * @see #awaitReconnection() + */ + private void trySignalForReconnection() { + if (compareAndSet(StateHelper.UNINITIALIZED, StateHelper.RECONNECT)) { + connectorLock.lock(); + try { + reconnectRequired.signal(); + } finally { + connectorLock.unlock(); } - + } } - protected class ComposableAsyncOps extends BaseClientOps>> { + } - @Override - protected CompletionStage> exec(TarantoolRequest request) { - return (CompletionStage>) TarantoolClientImpl.this.exec(request); - } + protected class ComposableAsyncOps extends BaseClientOps>> { - @Override - public void close() { - TarantoolClientImpl.this.close(); - } + @Override + protected CompletionStage> exec(TarantoolRequest request) { + return (CompletionStage>) TarantoolClientImpl.this.exec(request); + } + @Override + public void close() { + TarantoolClientImpl.this.close(); } - /** - * Used by internal services to ignore schema ID issues. - */ - protected class UnsafeSchemaOps extends BaseClientOps, Long>> { + } - protected TupleTwo, Long> exec(TarantoolRequest request) { - long syncId = TarantoolClientImpl.this.syncId.incrementAndGet(); - TarantoolOperation operation = request.toOperation(syncId, 0L); - List result = (List) syncGet(registerOperation(operation).getResult()); - return TupleTwo.of(result, operation.getCompletedSchemaId()); - } + /** + * Used by internal services to ignore schema ID issues. + */ + protected class UnsafeSchemaOps extends BaseClientOps, Long>> { + protected TupleTwo, Long> exec(TarantoolRequest request) { + long syncId = TarantoolClientImpl.this.syncId.incrementAndGet(); + TarantoolOperation operation = request.toOperation(syncId, 0L); + List result = (List) syncGet(registerOperation(operation).getResult()); + return TupleTwo.of(result, operation.getCompletedSchemaId()); } - protected abstract class BaseClientOps extends AbstractTarantoolOps { + } - @Override - protected TarantoolSchemaMeta getSchemaMeta() { - return TarantoolClientImpl.this.getSchemaMeta(); - } + protected abstract class BaseClientOps extends AbstractTarantoolOps { - @Override - public void close() { - throw new IllegalStateException("You should close TarantoolClient instead."); - } + @Override + protected TarantoolSchemaMeta getSchemaMeta() { + return TarantoolClientImpl.this.getSchemaMeta(); + } + @Override + public void close() { + throw new IllegalStateException("You should close TarantoolClient instead."); } + } + } diff --git a/src/main/java/org/tarantool/TarantoolClusterClient.java b/src/main/java/org/tarantool/TarantoolClusterClient.java index fff54e95..89a0dc22 100644 --- a/src/main/java/org/tarantool/TarantoolClusterClient.java +++ b/src/main/java/org/tarantool/TarantoolClusterClient.java @@ -1,5 +1,9 @@ package org.tarantool; +import static org.tarantool.utils.LocalLogger.log; + +import java.util.Arrays; +import java.util.List; import org.tarantool.cluster.TarantoolClusterDiscoverer; import org.tarantool.cluster.TarantoolClusterStoredFunctionDiscoverer; import org.tarantool.logging.Logger; @@ -53,6 +57,16 @@ public class TarantoolClusterClient extends TarantoolClientImpl { * @param addresses Array of addresses in the form of host[:port]. */ public TarantoolClusterClient(TarantoolClusterClientConfig config, String... addresses) { + this(config, makeClusterSocketProvider(Arrays.asList(addresses))); + } + + /** + * Constructs a new cluster client. + * + * @param config Configuration. + * @param addresses List of addresses in the form of host[:port]. + */ + public TarantoolClusterClient(TarantoolClusterClientConfig config, List addresses) { this(config, makeClusterSocketProvider(addresses)); } @@ -240,7 +254,8 @@ public void refreshInstances() { } } - private static RoundRobinSocketProviderImpl makeClusterSocketProvider(String[] addresses) { + private static RoundRobinSocketProviderImpl makeClusterSocketProvider(List addresses) { + log("Making cluster socket provider"); return new RoundRobinSocketProviderImpl(addresses); } diff --git a/src/main/java/org/tarantool/jdbc/SQLConnection.java b/src/main/java/org/tarantool/jdbc/SQLConnection.java index 327d0d69..47875bd2 100644 --- a/src/main/java/org/tarantool/jdbc/SQLConnection.java +++ b/src/main/java/org/tarantool/jdbc/SQLConnection.java @@ -1,14 +1,20 @@ package org.tarantool.jdbc; +import static org.tarantool.utils.LocalLogger.log; + +import java.util.stream.Collectors; import org.tarantool.CommunicationException; import org.tarantool.SocketChannelProvider; import org.tarantool.SqlProtoUtils; import org.tarantool.TarantoolClientConfig; import org.tarantool.TarantoolClientImpl; +import org.tarantool.TarantoolClusterClient; +import org.tarantool.TarantoolClusterClientConfig; import org.tarantool.TarantoolOperation; import org.tarantool.TarantoolRequest; import org.tarantool.protocol.TarantoolPacket; import org.tarantool.util.JdbcConstants; +import org.tarantool.util.NodeSpec; import org.tarantool.util.SQLStates; import java.io.IOException; @@ -46,6 +52,7 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeoutException; import java.util.function.Function; +import org.tarantool.utils.UuidReplacer; /** * Tarantool {@link Connection} implementation. @@ -54,784 +61,799 @@ */ public class SQLConnection implements TarantoolConnection { - private static final int UNSET_HOLDABILITY = 0; - private static final String PING_QUERY = "SELECT 1"; - - private final SQLTarantoolClientImpl client; - private final String url; - private final Properties properties; - private DatabaseMetaData cachedMetadata; - private int resultSetHoldability = UNSET_HOLDABILITY; - - public SQLConnection(String url, Properties properties) throws SQLException { - this.url = url; - this.properties = properties; - - try { - client = makeSqlClient(makeAddress(properties), makeConfigFromProperties(properties)); - } catch (Exception e) { - throw new SQLException("Couldn't initiate connection using " + SQLDriver.diagProperties(properties), e); - } - } - - protected SQLTarantoolClientImpl makeSqlClient(String address, TarantoolClientConfig config) { - return new SQLTarantoolClientImpl(address, config); - } - - private String makeAddress(Properties properties) throws SQLException { - String host = SQLProperty.HOST.getString(properties); - int port = SQLProperty.PORT.getInt(properties); - return host + ":" + port; - } - - private TarantoolClientConfig makeConfigFromProperties(Properties properties) throws SQLException { - TarantoolClientConfig clientConfig = new TarantoolClientConfig(); - clientConfig.username = SQLProperty.USER.getString(properties); - clientConfig.password = SQLProperty.PASSWORD.getString(properties); - - clientConfig.operationExpiryTimeMillis = SQLProperty.QUERY_TIMEOUT.getInt(properties); - clientConfig.initTimeoutMillis = SQLProperty.LOGIN_TIMEOUT.getInt(properties); - - return clientConfig; - } - - @Override - public void commit() throws SQLException { - checkNotClosed(); - if (getAutoCommit()) { - throw new SQLNonTransientException( - "Cannot commit when auto-commit is enabled.", - SQLStates.INVALID_TRANSACTION_STATE.getSqlState() - ); - } - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Statement createStatement() throws SQLException { - return createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); - } - - @Override - public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { - return createStatement(resultSetType, resultSetConcurrency, getHoldability()); - } - - @Override - public Statement createStatement(int resultSetType, - int resultSetConcurrency, - int resultSetHoldability) throws SQLException { - checkNotClosed(); - checkStatementParams(resultSetType, resultSetConcurrency, resultSetHoldability); - return new SQLStatement(this, resultSetType, resultSetConcurrency, resultSetHoldability); - } - - @Override - public PreparedStatement prepareStatement(String sql) throws SQLException { - return prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); - } - - @Override - public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) - throws SQLException { - return prepareStatement(sql, resultSetType, resultSetConcurrency, getHoldability()); - } - - @Override - public PreparedStatement prepareStatement(String sql, - int resultSetType, - int resultSetConcurrency, - int resultSetHoldability) throws SQLException { - checkNotClosed(); - checkStatementParams(resultSetType, resultSetConcurrency, resultSetHoldability); - return new SQLPreparedStatement(this, sql, resultSetType, resultSetConcurrency, resultSetHoldability); - } - - @Override - public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { - checkNotClosed(); - JdbcConstants.checkGeneratedKeysConstant(autoGeneratedKeys); - return new SQLPreparedStatement(this, sql, autoGeneratedKeys); - } - - @Override - public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { - checkNotClosed(); - throw new SQLFeatureNotSupportedException(); - } - - @Override - public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { - checkNotClosed(); - throw new SQLFeatureNotSupportedException(); - } - - @Override - public CallableStatement prepareCall(String sql) throws SQLException { - return prepareCall(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); - } - - @Override - public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { - return prepareCall(sql, resultSetType, resultSetConcurrency, getHoldability()); - } - - @Override - public CallableStatement prepareCall(String sql, - int resultSetType, - int resultSetConcurrency, - int resultSetHoldability) - throws SQLException { - checkNotClosed(); - throw new SQLFeatureNotSupportedException(); - } - - @Override - public String nativeSQL(String sql) throws SQLException { - checkNotClosed(); - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void setAutoCommit(boolean autoCommit) throws SQLException { - checkNotClosed(); - if (!autoCommit) { - throw new SQLFeatureNotSupportedException(); - } - } - - @Override - public boolean getAutoCommit() throws SQLException { - checkNotClosed(); - return true; - } - - @Override - public void close() throws SQLException { - client.close(); - } - - @Override - public void rollback() throws SQLException { - checkNotClosed(); - if (getAutoCommit()) { - throw new SQLNonTransientException( - "Cannot rollback when auto-commit is enabled.", - SQLStates.INVALID_TRANSACTION_STATE.getSqlState() - ); - } - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void rollback(Savepoint savepoint) throws SQLException { - checkNotClosed(); - if (getAutoCommit()) { - throw new SQLNonTransientException( - "Cannot roll back to a savepoint when auto-commit is enabled.", - SQLStates.INVALID_TRANSACTION_STATE.getSqlState() - ); - } - throw new SQLFeatureNotSupportedException(); - } - - @Override - public boolean isClosed() throws SQLException { - return client.isClosed(); - } - - @Override - public Savepoint setSavepoint() throws SQLException { - checkNotClosed(); - if (getAutoCommit()) { - throw new SQLNonTransientException( - "Cannot set a savepoint when auto-commit is enabled.", - SQLStates.INVALID_TRANSACTION_STATE.getSqlState() - ); - } - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Savepoint setSavepoint(String name) throws SQLException { - checkNotClosed(); - if (getAutoCommit()) { - throw new SQLNonTransientException( - "Cannot set a savepoint when auto-commit is enabled.", - SQLStates.INVALID_TRANSACTION_STATE.getSqlState() - ); - } - throw new SQLFeatureNotSupportedException(); - } - - @Override - public DatabaseMetaData getMetaData() throws SQLException { - checkNotClosed(); - if (cachedMetadata == null) { - cachedMetadata = new SQLDatabaseMetadata(this); - } - return cachedMetadata; - } - - @Override - public void setReadOnly(boolean readOnly) throws SQLException { - checkNotClosed(); - throw new SQLFeatureNotSupportedException(); - } - - @Override - public boolean isReadOnly() throws SQLException { - checkNotClosed(); - return false; - } - - @Override - public void setCatalog(String catalog) throws SQLException { - checkNotClosed(); - } - - @Override - public String getCatalog() throws SQLException { - checkNotClosed(); - return null; - } - - @Override - public void setTransactionIsolation(int level) throws SQLException { - checkNotClosed(); - if (level != Connection.TRANSACTION_NONE) { - throw new SQLFeatureNotSupportedException(); - } - } - - @Override - public int getTransactionIsolation() throws SQLException { - checkNotClosed(); - return Connection.TRANSACTION_NONE; - } - - @Override - public SQLWarning getWarnings() throws SQLException { - checkNotClosed(); - return null; - } - - @Override - public void clearWarnings() throws SQLException { - checkNotClosed(); - } - - @Override - public Map> getTypeMap() throws SQLException { - checkNotClosed(); - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void setTypeMap(Map> map) throws SQLException { - checkNotClosed(); - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void setHoldability(int holdability) throws SQLException { - checkNotClosed(); - checkHoldabilitySupport(holdability); - resultSetHoldability = holdability; - } - - @Override - public int getHoldability() throws SQLException { - checkNotClosed(); - if (resultSetHoldability == UNSET_HOLDABILITY) { - resultSetHoldability = getMetaData().getResultSetHoldability(); - } - return resultSetHoldability; - } - - /** - * {@inheritDoc} - * - * @param timeout time in seconds - * - * @return connection activity status - */ - @Override - public boolean isValid(int timeout) throws SQLException { - if (timeout < 0) { - throw new SQLNonTransientException( - "Timeout cannot be negative", - SQLStates.INVALID_PARAMETER_VALUE.getSqlState() - ); - } - if (isClosed()) { - return false; - } - return checkConnection(timeout); - } - - @Override - public void releaseSavepoint(Savepoint savepoint) throws SQLException { - checkNotClosed(); - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Clob createClob() throws SQLException { - checkNotClosed(); - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Blob createBlob() throws SQLException { - checkNotClosed(); - throw new SQLFeatureNotSupportedException(); - } - - @Override - public NClob createNClob() throws SQLException { - checkNotClosed(); - throw new SQLFeatureNotSupportedException(); - } - - @Override - public SQLXML createSQLXML() throws SQLException { - checkNotClosed(); - throw new SQLFeatureNotSupportedException(); - } - - private boolean checkConnection(int timeout) { - ResultSet resultSet; - try (Statement pingStatement = createStatement()) { - pingStatement.setQueryTimeout(timeout); - resultSet = pingStatement.executeQuery(PING_QUERY); - boolean isValid = resultSet.next() && resultSet.getInt(1) == 1; - resultSet.close(); - - return isValid; - } catch (SQLException e) { - return false; - } - } - - @Override - public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { - checkNotClosed(); - if (milliseconds < 0) { - throw new SQLException("Network timeout cannot be negative."); - } - client.setOperationTimeout(milliseconds); - } - - @Override - public void setClientInfo(String name, String value) throws SQLClientInfoException { - try { - checkNotClosed(); - } catch (SQLException cause) { - throwUnknownReasonClientProperties("Connection is closed", Collections.singleton(name), cause); + private static final int UNSET_HOLDABILITY = 0; + private static final String PING_QUERY = "SELECT 1"; + + private final SQLTarantoolClientImpl client; + private final String url; + private final Properties properties; + private DatabaseMetaData cachedMetadata; + private int resultSetHoldability = UNSET_HOLDABILITY; + + /** + * Creates a new connection to Tarantool server. + * + * @param originUrl raw URL string that was used to parse connection parameters + * @param properties extra parameters to configure a connection + * @deprecated use {@link #SQLConnection(String, List, Properties)} instead + */ + @Deprecated + public SQLConnection(String originUrl, Properties properties) throws SQLException { + this(originUrl, Collections.emptyList(), properties); + } + + /** + * Creates a new connection to Tarantool server. + * + * @param originUrl raw URL string that was used to parse connection parameters + * @param nodes initial set of Tarantool nodes + * @param properties extra parameters to configure a connection + * @throws SQLException if any errors occur during the connecting + */ + public SQLConnection(String originUrl, + List nodes, + Properties properties) throws SQLException { + this.url = originUrl; + this.properties = properties; + try { + client = makeSqlClient(makeAddresses(nodes, properties), makeConfigFromProperties(properties)); + } catch (Exception e) { + throw new SQLException("Couldn't initiate connection using " + SQLDriver.diagProperties(properties), e); + } + } + + protected SQLTarantoolClientImpl makeSqlClient(List addresses, TarantoolClusterClientConfig config) { + log("make sql client"); + log("addresses: " + addresses); + return new SQLTarantoolClientImpl(addresses, config); + } + + private List makeAddresses(List nodes, Properties properties) throws SQLException { + List addresses = nodes.stream() + .map(NodeSpec::toString) + .collect(Collectors.toList()); + if (addresses.isEmpty()) { + addresses.add(SQLProperty.HOST.getString(properties) + ":" + SQLProperty.PORT.getString(properties)); + } + return addresses; + } + + private TarantoolClusterClientConfig makeConfigFromProperties(Properties properties) throws SQLException { + TarantoolClusterClientConfig clientConfig = new TarantoolClusterClientConfig(); + clientConfig.username = SQLProperty.USER.getString(properties); + clientConfig.password = SQLProperty.PASSWORD.getString(properties); + + clientConfig.operationExpiryTimeMillis = SQLProperty.QUERY_TIMEOUT.getInt(properties); + clientConfig.initTimeoutMillis = SQLProperty.LOGIN_TIMEOUT.getInt(properties); + + clientConfig.clusterDiscoveryEntryFunction = SQLProperty.CLUSTER_DISCOVERY_ENTRY_FUNCTION.getString(properties); + clientConfig.clusterDiscoveryDelayMillis = SQLProperty.CLUSTER_DISCOVERY_DELAY_MILLIS.getInt(properties); + + return clientConfig; + } + + @Override + public void commit() throws SQLException { + checkNotClosed(); + if (getAutoCommit()) { + throw new SQLNonTransientException( + "Cannot commit when auto-commit is enabled.", + SQLStates.INVALID_TRANSACTION_STATE.getSqlState() + ); + } + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Statement createStatement() throws SQLException { + return createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + return createStatement(resultSetType, resultSetConcurrency, getHoldability()); + } + + @Override + public Statement createStatement(int resultSetType, + int resultSetConcurrency, + int resultSetHoldability) throws SQLException { + checkNotClosed(); + checkStatementParams(resultSetType, resultSetConcurrency, resultSetHoldability); + return new SQLStatement(this, resultSetType, resultSetConcurrency, resultSetHoldability); + } + + @Override + public PreparedStatement prepareStatement(String sql) throws SQLException { + return prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) + throws SQLException { + return prepareStatement(sql, resultSetType, resultSetConcurrency, getHoldability()); + } + + @Override + public PreparedStatement prepareStatement(String sql, + int resultSetType, + int resultSetConcurrency, + int resultSetHoldability) throws SQLException { + checkNotClosed(); + checkStatementParams(resultSetType, resultSetConcurrency, resultSetHoldability); + return new SQLPreparedStatement(this, sql, resultSetType, resultSetConcurrency, resultSetHoldability); + } + + @Override + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + checkNotClosed(); + JdbcConstants.checkGeneratedKeysConstant(autoGeneratedKeys); + return new SQLPreparedStatement(this, sql, autoGeneratedKeys); + } + + @Override + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + checkNotClosed(); + throw new SQLFeatureNotSupportedException(); + } + + @Override + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + checkNotClosed(); + throw new SQLFeatureNotSupportedException(); + } + + @Override + public CallableStatement prepareCall(String sql) throws SQLException { + return prepareCall(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + return prepareCall(sql, resultSetType, resultSetConcurrency, getHoldability()); + } + + @Override + public CallableStatement prepareCall(String sql, + int resultSetType, + int resultSetConcurrency, + int resultSetHoldability) + throws SQLException { + checkNotClosed(); + throw new SQLFeatureNotSupportedException(); + } + + @Override + public String nativeSQL(String sql) throws SQLException { + checkNotClosed(); + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setAutoCommit(boolean autoCommit) throws SQLException { + checkNotClosed(); + } + + @Override + public boolean getAutoCommit() throws SQLException { + checkNotClosed(); + return false; + } + + @Override + public void close() throws SQLException { + client.close(); + } + + @Override + public void rollback() throws SQLException { + checkNotClosed(); + if (getAutoCommit()) { + throw new SQLNonTransientException( + "Cannot rollback when auto-commit is enabled.", + SQLStates.INVALID_TRANSACTION_STATE.getSqlState() + ); + } + } + + @Override + public void rollback(Savepoint savepoint) throws SQLException { + checkNotClosed(); + if (getAutoCommit()) { + throw new SQLNonTransientException( + "Cannot roll back to a savepoint when auto-commit is enabled.", + SQLStates.INVALID_TRANSACTION_STATE.getSqlState() + ); + } + } + + @Override + public boolean isClosed() throws SQLException { + return client.isClosed(); + } + + @Override + public Savepoint setSavepoint() throws SQLException { + checkNotClosed(); + if (getAutoCommit()) { + throw new SQLNonTransientException( + "Cannot set a savepoint when auto-commit is enabled.", + SQLStates.INVALID_TRANSACTION_STATE.getSqlState() + ); + } + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Savepoint setSavepoint(String name) throws SQLException { + checkNotClosed(); + if (getAutoCommit()) { + throw new SQLNonTransientException( + "Cannot set a savepoint when auto-commit is enabled.", + SQLStates.INVALID_TRANSACTION_STATE.getSqlState() + ); + } + throw new SQLFeatureNotSupportedException(); + } + + @Override + public DatabaseMetaData getMetaData() throws SQLException { + checkNotClosed(); + if (cachedMetadata == null) { + cachedMetadata = new SQLDatabaseMetadata(this); + } + return cachedMetadata; + } + + @Override + public void setReadOnly(boolean readOnly) throws SQLException { + checkNotClosed(); + } + + @Override + public boolean isReadOnly() throws SQLException { + checkNotClosed(); + return false; + } + + @Override + public void setCatalog(String catalog) throws SQLException { + checkNotClosed(); + } + + @Override + public String getCatalog() throws SQLException { + checkNotClosed(); + return null; + } + + @Override + public void setTransactionIsolation(int level) throws SQLException { + checkNotClosed(); + if (level != Connection.TRANSACTION_NONE) { + throw new SQLFeatureNotSupportedException(); + } + } + + @Override + public int getTransactionIsolation() throws SQLException { + checkNotClosed(); + return Connection.TRANSACTION_NONE; + } + + @Override + public SQLWarning getWarnings() throws SQLException { + checkNotClosed(); + return null; + } + + @Override + public void clearWarnings() throws SQLException { + checkNotClosed(); + } + + @Override + public Map> getTypeMap() throws SQLException { + checkNotClosed(); + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setTypeMap(Map> map) throws SQLException { + checkNotClosed(); + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setHoldability(int holdability) throws SQLException { + checkNotClosed(); + checkHoldabilitySupport(holdability); + resultSetHoldability = holdability; + } + + @Override + public int getHoldability() throws SQLException { + checkNotClosed(); + if (resultSetHoldability == UNSET_HOLDABILITY) { + resultSetHoldability = getMetaData().getResultSetHoldability(); + } + return resultSetHoldability; + } + + /** + * {@inheritDoc} + * + * @param timeout time in seconds + * @return connection activity status + */ + @Override + public boolean isValid(int timeout) throws SQLException { + if (timeout < 0) { + throw new SQLNonTransientException( + "Timeout cannot be negative", + SQLStates.INVALID_PARAMETER_VALUE.getSqlState() + ); + } + if (isClosed()) { + return false; + } + return checkConnection(timeout); + } + + @Override + public void releaseSavepoint(Savepoint savepoint) throws SQLException { + checkNotClosed(); + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Clob createClob() throws SQLException { + checkNotClosed(); + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Blob createBlob() throws SQLException { + checkNotClosed(); + throw new SQLFeatureNotSupportedException(); + } + + @Override + public NClob createNClob() throws SQLException { + checkNotClosed(); + throw new SQLFeatureNotSupportedException(); + } + + @Override + public SQLXML createSQLXML() throws SQLException { + checkNotClosed(); + throw new SQLFeatureNotSupportedException(); + } + + private boolean checkConnection(int timeout) { + ResultSet resultSet; + try (Statement pingStatement = createStatement()) { + pingStatement.setQueryTimeout(timeout); + resultSet = pingStatement.executeQuery(PING_QUERY); + boolean isValid = resultSet.next() && resultSet.getInt(1) == 1; + resultSet.close(); + + return isValid; + } catch (SQLException e) { + return false; + } + } + + @Override + public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + checkNotClosed(); + if (milliseconds < 0) { + throw new SQLException("Network timeout cannot be negative."); + } + client.setOperationTimeout(milliseconds); + } + + @Override + public void setClientInfo(String name, String value) throws SQLClientInfoException { + try { + checkNotClosed(); + } catch (SQLException cause) { + throwUnknownReasonClientProperties("Connection is closed", Collections.singleton(name), cause); + } + throwUnknownClientProperties(Collections.singleton(name)); + } + + @Override + public void setClientInfo(Properties properties) throws SQLClientInfoException { + try { + checkNotClosed(); + } catch (SQLException cause) { + throwUnknownReasonClientProperties("Connection is closed", properties.keySet(), cause); + } + throwUnknownClientProperties(properties.keySet()); + } + + /** + * Throws an exception caused by {@code cause} and marks all properties as {@link ClientInfoStatus#REASON_UNKNOWN}. + * + * @param reason reason mesage + * @param properties client properties + * @param cause original cause + * @throws SQLClientInfoException wrapped exception + */ + private void throwUnknownReasonClientProperties(String reason, + Collection properties, + SQLException cause) throws SQLClientInfoException { + Map failedProperties = new HashMap<>(); + properties.forEach(property -> { + failedProperties.put(property.toString(), ClientInfoStatus.REASON_UNKNOWN); + }); + throw new SQLClientInfoException(reason, cause.getSQLState(), failedProperties, cause); + } + + /** + * Throws exception for unrecognizable properties. + * + * @param properties unknown property names. + * @throws SQLClientInfoException wrapped exception + */ + private void throwUnknownClientProperties(Collection properties) throws SQLClientInfoException { + Map failedProperties = new HashMap<>(); + properties.forEach(property -> { + failedProperties.put(property.toString(), ClientInfoStatus.REASON_UNKNOWN_PROPERTY); + }); + throw new SQLClientInfoException(failedProperties); + } + + @Override + public String getClientInfo(String name) throws SQLException { + checkNotClosed(); + return null; + } + + @Override + public Properties getClientInfo() throws SQLException { + checkNotClosed(); + return new Properties(); + } + + @Override + public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + checkNotClosed(); + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Struct createStruct(String typeName, Object[] attributes) throws SQLException { + checkNotClosed(); + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setSchema(String schema) throws SQLException { + checkNotClosed(); + } + + @Override + public String getSchema() throws SQLException { + checkNotClosed(); + return null; + } + + @Override + public void abort(Executor executor) throws SQLException { + if (isClosed()) { + return; + } + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int getNetworkTimeout() throws SQLException { + checkNotClosed(); + return (int) client.getOperationTimeout(); + } + + @Override + public SQLResultHolder execute(long timeout, SQLQueryHolder query) throws SQLException { + checkNotClosed(); + return (useNetworkTimeout(timeout)) + ? executeWithNetworkTimeout(query) + : executeWithQueryTimeout(timeout, query); + } + + @Override + public SQLBatchResultHolder executeBatch(long timeout, List queries) + throws SQLException { + checkNotClosed(); + SQLTarantoolClientImpl.SQLRawOps sqlOps = client.sqlRawOps(); + SQLBatchResultHolder batchResult = useNetworkTimeout(timeout) + ? sqlOps.executeBatch(queries) + : sqlOps.executeBatch(timeout, queries); + + return batchResult; + } + + private boolean useNetworkTimeout(long timeout) throws SQLException { + int networkTimeout = getNetworkTimeout(); + return timeout == 0 || (networkTimeout > 0 && networkTimeout < timeout); + } + + private SQLResultHolder executeWithNetworkTimeout(SQLQueryHolder query) throws SQLException { + try { + return client.sqlRawOps().execute(query); + } catch (Exception e) { + handleException(e); + throw new SQLException(formatError(query), e); + } + } + + /** + * Executes a query using a custom timeout. + * + * @param timeout query timeout + * @param query query + * @return SQL result holder + * @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 { + try { + return client.sqlRawOps().execute(timeout, query); + } 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()); + } + handleException(e); + throw new SQLException(formatError(query), e); + } + } + + @Override + public T unwrap(Class type) throws SQLException { + if (isWrapperFor(type)) { + return type.cast(this); + } + throw new SQLNonTransientException("Connection does not wrap " + type.getName()); + } + + @Override + public boolean isWrapperFor(Class type) throws SQLException { + return type.isAssignableFrom(this.getClass()); + } + + protected List nativeSelect(Integer space, Integer index, List key, int offset, int limit, int iterator) + throws SQLException { + checkNotClosed(); + try { + return client.syncOps().select(space, index, key, offset, limit, iterator); + } catch (Exception e) { + handleException(e); + throw new SQLException(e); + } + } + + protected String getServerVersion() { + return client.getServerVersion(); + } + + /** + * Inspects passed exception and closes the connection if appropriate. + * + * @param e Exception to process. + */ + private void handleException(Exception e) { + if (e instanceof CommunicationException || + e instanceof IOException || + e.getCause() instanceof TimeoutException) { + try { + close(); + } catch (SQLException ignored) { + // No-op. + } + } + } + + /** + * Checks connection close status. + * + * @throws SQLException If connection is closed. + */ + protected void checkNotClosed() throws SQLException { + if (isClosed()) { + throw new SQLNonTransientConnectionException( + "Connection is closed.", + SQLStates.CONNECTION_DOES_NOT_EXIST.getSqlState() + ); + } + } + + String getUrl() { + return url; + } + + Properties getProperties() { + return properties; + } + + /** + * Checks all params required to make statements. + * + * @param resultSetType scroll type + * @param resultSetConcurrency concurrency level + * @param resultSetHoldability holdability type + * @throws SQLFeatureNotSupportedException if any param is not supported + * @throws SQLNonTransientException if any param has an invalid value + */ + private void checkStatementParams(int resultSetType, + int resultSetConcurrency, + int resultSetHoldability) throws SQLException { + checkResultSetType(resultSetType); + checkResultSetConcurrency(resultSetType, resultSetConcurrency); + checkHoldabilitySupport(resultSetHoldability); + } + + /** + * Checks whether resultSetType is supported. + * + * @param resultSetType param to be checked + * @throws SQLFeatureNotSupportedException param is not supported + * @throws SQLNonTransientException param has invalid value + */ + private void checkResultSetType(int resultSetType) throws SQLException { + if (resultSetType != ResultSet.TYPE_FORWARD_ONLY && + resultSetType != ResultSet.TYPE_SCROLL_INSENSITIVE && + resultSetType != ResultSet.TYPE_SCROLL_SENSITIVE) { + throw new SQLNonTransientException("", SQLStates.INVALID_PARAMETER_VALUE.getSqlState()); + } + if (!getMetaData().supportsResultSetType(resultSetType)) { + throw new SQLFeatureNotSupportedException(); + } + } + + /** + * Checks whether resultSetType is supported. + * + * @param resultSetConcurrency param to be checked + * @throws SQLFeatureNotSupportedException param is not supported + * @throws SQLNonTransientException param has invalid value + */ + private void checkResultSetConcurrency(int resultSetType, int resultSetConcurrency) throws SQLException { + if (resultSetConcurrency != ResultSet.CONCUR_READ_ONLY && + resultSetConcurrency != ResultSet.CONCUR_UPDATABLE) { + throw new SQLNonTransientException("", SQLStates.INVALID_PARAMETER_VALUE.getSqlState()); + } + if (!getMetaData().supportsResultSetConcurrency(resultSetType, resultSetConcurrency)) { + throw new SQLFeatureNotSupportedException(); + } + } + + /** + * Checks whether holdability is supported. + * + * @param holdability param to be checked + * @throws SQLFeatureNotSupportedException param is not supported + * @throws SQLNonTransientException param has invalid value + */ + private void checkHoldabilitySupport(int holdability) throws SQLException { + JdbcConstants.checkHoldabilityConstant(holdability); + if (!getMetaData().supportsResultSetHoldability(holdability)) { + throw new SQLFeatureNotSupportedException(); + } + } + + /** + * Provides error message that contains parameters of failed SQL statement. + * + * @param query SQL query + * @return Formatted error message. + */ + private static String formatError(SQLQueryHolder query) { + return "Failed to execute SQL: " + query.getQuery() + ", params: " + query.getParams(); + } + + static class SQLTarantoolClientImpl extends TarantoolClusterClient { + + private Future executeQuery(SQLQueryHolder queryHolder) { + return exec(makeSqlRequest(queryHolder.getQuery(), queryHolder.getParams())); + } + + private Future executeQuery(SQLQueryHolder queryHolder, long timeoutMillis) { + TarantoolRequest request = makeSqlRequest(queryHolder.getQuery(), queryHolder.getParams()); + request.setTimeout(Duration.of(timeoutMillis, ChronoUnit.MILLIS)); + return exec(request); + } + + final SQLRawOps sqlRawOps = new SQLRawOps() { + @Override + public SQLResultHolder execute(SQLQueryHolder query) { + query = new UuidReplacer(schemaMeta).updateUuidToProperType(query); + return (SQLResultHolder) syncGet(executeQuery(query)); + } + + @Override + public SQLResultHolder execute(long timeoutMillis, SQLQueryHolder query) { + query = new UuidReplacer(schemaMeta).updateUuidToProperType(query); + return (SQLResultHolder) syncGet(executeQuery(query, timeoutMillis)); + } + + @Override + public SQLBatchResultHolder executeBatch(List queries) { + return executeInternal(queries, (query) -> executeQuery(query)); + } + + @Override + public SQLBatchResultHolder executeBatch(long timeoutMillis, List queries) { + return executeInternal(queries, (query) -> executeQuery(query, timeoutMillis)); + } + + private SQLBatchResultHolder executeInternal(List queries, + Function> fetcher) { + List> sqlFutures = new ArrayList<>(); + // using queries pipelining to emulate a batch request + for (SQLQueryHolder query : queries) { + sqlFutures.add(fetcher.apply(query)); + } + // wait for all the results + Exception lastError = null; + List items = new ArrayList<>(queries.size()); + for (Future future : sqlFutures) { + try { + SQLResultHolder result = (SQLResultHolder) syncGet(future); + if (result.isQueryResult()) { + lastError = new SQLException( + "Result set is not allowed in the batch response", + SQLStates.TOO_MANY_RESULTS.getSqlState() + ); + } + items.add(result); + } catch (RuntimeException e) { + // empty result set will be treated as a wrong result + items.add(SQLResultHolder.ofEmptyQuery()); + lastError = e; + } } - throwUnknownClientProperties(Collections.singleton(name)); - } + return new SQLBatchResultHolder(items, lastError); + } + }; - @Override - public void setClientInfo(Properties properties) throws SQLClientInfoException { - try { - checkNotClosed(); - } catch (SQLException cause) { - throwUnknownReasonClientProperties("Connection is closed", properties.keySet(), cause); - } - throwUnknownClientProperties(properties.keySet()); - } - - /** - * Throws an exception caused by {@code cause} and marks all properties - * as {@link ClientInfoStatus#REASON_UNKNOWN}. - * - * @param reason reason mesage - * @param properties client properties - * @param cause original cause - * - * @throws SQLClientInfoException wrapped exception - */ - private void throwUnknownReasonClientProperties(String reason, - Collection properties, - SQLException cause) throws SQLClientInfoException { - Map failedProperties = new HashMap<>(); - properties.forEach(property -> { - failedProperties.put(property.toString(), ClientInfoStatus.REASON_UNKNOWN); - }); - throw new SQLClientInfoException(reason, cause.getSQLState(), failedProperties, cause); - } - - /** - * Throws exception for unrecognizable properties. - * - * @param properties unknown property names. - * - * @throws SQLClientInfoException wrapped exception - */ - private void throwUnknownClientProperties(Collection properties) throws SQLClientInfoException { - Map failedProperties = new HashMap<>(); - properties.forEach(property -> { - failedProperties.put(property.toString(), ClientInfoStatus.REASON_UNKNOWN_PROPERTY); - }); - throw new SQLClientInfoException(failedProperties); + SQLTarantoolClientImpl(List addresses, TarantoolClusterClientConfig config) { + super(config, addresses); + msgPackLite = SQLMsgPackLite.INSTANCE; } - @Override - public String getClientInfo(String name) throws SQLException { - checkNotClosed(); - return null; + SQLTarantoolClientImpl(SocketChannelProvider socketProvider, TarantoolClusterClientConfig config) { + super(config, socketProvider); + msgPackLite = SQLMsgPackLite.INSTANCE; } - @Override - public Properties getClientInfo() throws SQLException { - checkNotClosed(); - return new Properties(); + SQLRawOps sqlRawOps() { + return sqlRawOps; } @Override - public Array createArrayOf(String typeName, Object[] elements) throws SQLException { - checkNotClosed(); - throw new SQLFeatureNotSupportedException(); + 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); } - @Override - public Struct createStruct(String typeName, Object[] attributes) throws SQLException { - checkNotClosed(); - throw new SQLFeatureNotSupportedException(); - } + interface SQLRawOps { - @Override - public void setSchema(String schema) throws SQLException { - checkNotClosed(); - } + SQLResultHolder execute(SQLQueryHolder query); - @Override - public String getSchema() throws SQLException { - checkNotClosed(); - return null; - } + SQLResultHolder execute(long timeoutMillis, SQLQueryHolder query); - @Override - public void abort(Executor executor) throws SQLException { - if (isClosed()) { - return; - } - throw new SQLFeatureNotSupportedException(); - } + SQLBatchResultHolder executeBatch(List queries); - @Override - public int getNetworkTimeout() throws SQLException { - checkNotClosed(); - return (int) client.getOperationTimeout(); - } - - @Override - public SQLResultHolder execute(long timeout, SQLQueryHolder query) throws SQLException { - checkNotClosed(); - return (useNetworkTimeout(timeout)) - ? executeWithNetworkTimeout(query) - : executeWithQueryTimeout(timeout, query); - } - - @Override - public SQLBatchResultHolder executeBatch(long timeout, List queries) - throws SQLException { - checkNotClosed(); - SQLTarantoolClientImpl.SQLRawOps sqlOps = client.sqlRawOps(); - SQLBatchResultHolder batchResult = useNetworkTimeout(timeout) - ? sqlOps.executeBatch(queries) - : sqlOps.executeBatch(timeout, queries); - - return batchResult; - } + SQLBatchResultHolder executeBatch(long timeoutMillis, List queries); - private boolean useNetworkTimeout(long timeout) throws SQLException { - int networkTimeout = getNetworkTimeout(); - return timeout == 0 || (networkTimeout > 0 && networkTimeout < timeout); - } - - private SQLResultHolder executeWithNetworkTimeout(SQLQueryHolder query) throws SQLException { - try { - return client.sqlRawOps().execute(query); - } catch (Exception e) { - handleException(e); - throw new SQLException(formatError(query), e); - } - } - - /** - * Executes a query using a custom timeout. - * - * @param timeout query timeout - * @param query query - * - * @return SQL result holder - * - * @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 { - try { - return client.sqlRawOps().execute(timeout, query); - } 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()); - } - handleException(e); - throw new SQLException(formatError(query), e); - } } - @Override - public T unwrap(Class type) throws SQLException { - if (isWrapperFor(type)) { - return type.cast(this); - } - throw new SQLNonTransientException("Connection does not wrap " + type.getName()); - } - - @Override - public boolean isWrapperFor(Class type) throws SQLException { - return type.isAssignableFrom(this.getClass()); - } - - protected List nativeSelect(Integer space, Integer index, List key, int offset, int limit, int iterator) - throws SQLException { - checkNotClosed(); - try { - return client.syncOps().select(space, index, key, offset, limit, iterator); - } catch (Exception e) { - handleException(e); - throw new SQLException(e); - } - } - - protected String getServerVersion() { - return client.getServerVersion(); - } - - /** - * Inspects passed exception and closes the connection if appropriate. - * - * @param e Exception to process. - */ - private void handleException(Exception e) { - if (e instanceof CommunicationException || - e instanceof IOException || - e.getCause() instanceof TimeoutException) { - try { - close(); - } catch (SQLException ignored) { - // No-op. - } - } - } - - /** - * Checks connection close status. - * - * @throws SQLException If connection is closed. - */ - protected void checkNotClosed() throws SQLException { - if (isClosed()) { - throw new SQLNonTransientConnectionException( - "Connection is closed.", - SQLStates.CONNECTION_DOES_NOT_EXIST.getSqlState() - ); - } - } - - String getUrl() { - return url; - } - - Properties getProperties() { - return properties; - } - - /** - * Checks all params required to make statements. - * - * @param resultSetType scroll type - * @param resultSetConcurrency concurrency level - * @param resultSetHoldability holdability type - * - * @throws SQLFeatureNotSupportedException if any param is not supported - * @throws SQLNonTransientException if any param has an invalid value - */ - private void checkStatementParams(int resultSetType, - int resultSetConcurrency, - int resultSetHoldability) throws SQLException { - checkResultSetType(resultSetType); - checkResultSetConcurrency(resultSetType, resultSetConcurrency); - checkHoldabilitySupport(resultSetHoldability); - } - - /** - * Checks whether resultSetType is supported. - * - * @param resultSetType param to be checked - * - * @throws SQLFeatureNotSupportedException param is not supported - * @throws SQLNonTransientException param has invalid value - */ - private void checkResultSetType(int resultSetType) throws SQLException { - if (resultSetType != ResultSet.TYPE_FORWARD_ONLY && - resultSetType != ResultSet.TYPE_SCROLL_INSENSITIVE && - resultSetType != ResultSet.TYPE_SCROLL_SENSITIVE) { - throw new SQLNonTransientException("", SQLStates.INVALID_PARAMETER_VALUE.getSqlState()); - } - if (!getMetaData().supportsResultSetType(resultSetType)) { - throw new SQLFeatureNotSupportedException(); - } - } - - /** - * Checks whether resultSetType is supported. - * - * @param resultSetConcurrency param to be checked - * - * @throws SQLFeatureNotSupportedException param is not supported - * @throws SQLNonTransientException param has invalid value - */ - private void checkResultSetConcurrency(int resultSetType, int resultSetConcurrency) throws SQLException { - if (resultSetConcurrency != ResultSet.CONCUR_READ_ONLY && - resultSetConcurrency != ResultSet.CONCUR_UPDATABLE) { - throw new SQLNonTransientException("", SQLStates.INVALID_PARAMETER_VALUE.getSqlState()); - } - if (!getMetaData().supportsResultSetConcurrency(resultSetType, resultSetConcurrency)) { - throw new SQLFeatureNotSupportedException(); - } - } - - /** - * Checks whether holdability is supported. - * - * @param holdability param to be checked - * - * @throws SQLFeatureNotSupportedException param is not supported - * @throws SQLNonTransientException param has invalid value - */ - private void checkHoldabilitySupport(int holdability) throws SQLException { - JdbcConstants.checkHoldabilityConstant(holdability); - if (!getMetaData().supportsResultSetHoldability(holdability)) { - throw new SQLFeatureNotSupportedException(); - } - } - - /** - * Provides error message that contains parameters of failed SQL statement. - * - * @param query SQL query - * - * @return Formatted error message. - */ - private static String formatError(SQLQueryHolder query) { - return "Failed to execute SQL: " + query.getQuery() + ", params: " + query.getParams(); - } - - static class SQLTarantoolClientImpl extends TarantoolClientImpl { - - private Future executeQuery(SQLQueryHolder queryHolder) { - return exec(makeSqlRequest(queryHolder.getQuery(), queryHolder.getParams())); - } - - private Future executeQuery(SQLQueryHolder queryHolder, long timeoutMillis) { - TarantoolRequest request = makeSqlRequest(queryHolder.getQuery(), queryHolder.getParams()); - request.setTimeout(Duration.of(timeoutMillis, ChronoUnit.MILLIS)); - return exec(request); - } - - final SQLRawOps sqlRawOps = new SQLRawOps() { - @Override - public SQLResultHolder execute(SQLQueryHolder query) { - return (SQLResultHolder) syncGet(executeQuery(query)); - } - - @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)); - } - - @Override - public SQLBatchResultHolder executeBatch(long timeoutMillis, List queries) { - return executeInternal(queries, (query) -> executeQuery(query, timeoutMillis)); - } - - private SQLBatchResultHolder executeInternal(List queries, - Function> fetcher) { - List> sqlFutures = new ArrayList<>(); - // using queries pipelining to emulate a batch request - for (SQLQueryHolder query : queries) { - sqlFutures.add(fetcher.apply(query)); - } - // wait for all the results - Exception lastError = null; - List items = new ArrayList<>(queries.size()); - for (Future future : sqlFutures) { - try { - SQLResultHolder result = (SQLResultHolder) syncGet(future); - if (result.isQueryResult()) { - lastError = new SQLException( - "Result set is not allowed in the batch response", - SQLStates.TOO_MANY_RESULTS.getSqlState() - ); - } - items.add(result); - } catch (RuntimeException e) { - // empty result set will be treated as a wrong result - items.add(SQLResultHolder.ofEmptyQuery()); - lastError = e; - } - } - return new SQLBatchResultHolder(items, lastError); - } - }; - - SQLTarantoolClientImpl(String address, TarantoolClientConfig config) { - super(address, config); - msgPackLite = SQLMsgPackLite.INSTANCE; - } - - SQLTarantoolClientImpl(SocketChannelProvider socketProvider, TarantoolClientConfig config) { - super(socketProvider, config); - msgPackLite = SQLMsgPackLite.INSTANCE; - } - - SQLRawOps sqlRawOps() { - return sqlRawOps; - } - - @Override - protected void completeSql(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); - } - - interface SQLRawOps { - - SQLResultHolder execute(SQLQueryHolder query); - - SQLResultHolder execute(long timeoutMillis, SQLQueryHolder query); - - SQLBatchResultHolder executeBatch(List queries); - - SQLBatchResultHolder executeBatch(long timeoutMillis, List queries); - - } - - } + } } diff --git a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java index 08bb1743..595d85a0 100644 --- a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java +++ b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java @@ -1,11 +1,8 @@ package org.tarantool.jdbc; import static org.tarantool.util.JdbcConstants.DatabaseMetadataTable; - -import org.tarantool.SqlProtoUtils; -import org.tarantool.Version; -import org.tarantool.jdbc.type.TarantoolSqlType; -import org.tarantool.util.TupleTwo; +import static org.tarantool.util.JdbcConstants.DatabaseMetadataTable.INDEX_INFO; +import static org.tarantool.utils.LocalLogger.log; import java.sql.Connection; import java.sql.DatabaseMetaData; @@ -17,1123 +14,1412 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; +import org.tarantool.SqlProtoUtils; +import org.tarantool.Version; +import org.tarantool.jdbc.type.TarantoolSqlType; +import org.tarantool.util.TupleTwo; +import org.tarantool.utils.LocalLogger; public class SQLDatabaseMetadata implements DatabaseMetaData { - protected static final int _VSPACE = 281; - protected static final int _VINDEX = 289; - protected static final int SPACES_MAX = 65535; - public static final int FORMAT_IDX = 6; - public static final int NAME_IDX = 2; - public static final int INDEX_FORMAT_IDX = 5; - public static final int SPACE_ID_IDX = 0; - protected final SQLConnection connection; - - public SQLDatabaseMetadata(SQLConnection connection) { - this.connection = connection; - } - - @Override - public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) - throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.STORED_PROCEDURES); - } - - @Override - public boolean allProceduresAreCallable() throws SQLException { - return false; - } - - @Override - public boolean allTablesAreSelectable() throws SQLException { - return true; - } - - @Override - public String getURL() throws SQLException { - return connection.getUrl(); - } - - @Override - public String getUserName() throws SQLException { - return connection.getProperties().getProperty("user"); - } - - @Override - public boolean isReadOnly() throws SQLException { - return false; - } - - @Override - public boolean nullsAreSortedHigh() throws SQLException { - return false; - } - - @Override - public boolean nullsAreSortedLow() throws SQLException { - return true; - } - - @Override - public boolean nullsAreSortedAtStart() throws SQLException { - return false; - } - - @Override - public boolean nullsAreSortedAtEnd() throws SQLException { - return false; - } - - @Override - public String getDatabaseProductName() throws SQLException { - return "Tarantool"; - } - - @Override - public String getDatabaseProductVersion() throws SQLException { - return connection.getServerVersion(); - } - - @Override - public String getDriverName() throws SQLException { - return SQLConstant.DRIVER_NAME; - } - - @Override - public String getDriverVersion() throws SQLException { - return Version.version; - } - - @Override - public int getDriverMajorVersion() { - return Version.majorVersion; - } - - @Override - public int getDriverMinorVersion() { - return Version.minorVersion; - } - - @Override - public boolean usesLocalFiles() throws SQLException { - return false; - } - - @Override - public boolean usesLocalFilePerTable() throws SQLException { - return false; - } - - @Override - public boolean supportsMixedCaseIdentifiers() throws SQLException { - return true; - } - - @Override - public boolean storesUpperCaseIdentifiers() throws SQLException { - return false; - } - - @Override - public boolean storesLowerCaseIdentifiers() throws SQLException { - return false; - } - - @Override - public boolean storesMixedCaseIdentifiers() throws SQLException { - return true; - } - - @Override - public boolean supportsMixedCaseQuotedIdentifiers() throws SQLException { - return false; - } - - @Override - public boolean storesUpperCaseQuotedIdentifiers() throws SQLException { - return false; - } - - @Override - public boolean storesLowerCaseQuotedIdentifiers() throws SQLException { - return false; - } - - @Override - public boolean storesMixedCaseQuotedIdentifiers() throws SQLException { - return false; - } - - @Override - public String getIdentifierQuoteString() throws SQLException { - return " "; - } - - @Override - public String getSQLKeywords() throws SQLException { - return ""; - } - - @Override - public String getNumericFunctions() throws SQLException { - return ""; - } - - @Override - public String getStringFunctions() throws SQLException { - return ""; - } - - @Override - public String getSystemFunctions() throws SQLException { - return ""; - } - - @Override - public String getTimeDateFunctions() throws SQLException { - return ""; - } - - @Override - public String getSearchStringEscape() throws SQLException { - return null; - } - - @Override - public String getExtraNameCharacters() throws SQLException { - return ""; - } - - @Override - public boolean supportsAlterTableWithAddColumn() throws SQLException { - return false; - } - - @Override - public boolean supportsAlterTableWithDropColumn() throws SQLException { - return false; - } - - @Override - public boolean supportsColumnAliasing() throws SQLException { - return true; - } - - @Override - public boolean nullPlusNonNullIsNull() throws SQLException { - return true; - } - - @Override - public boolean supportsConvert() throws SQLException { - return false; - } - - @Override - public boolean supportsConvert(int fromType, int toType) throws SQLException { - return false; - } - - @Override - public boolean supportsTableCorrelationNames() throws SQLException { - return false; - } - - @Override - public boolean supportsDifferentTableCorrelationNames() throws SQLException { - return false; - } - - @Override - public boolean supportsExpressionsInOrderBy() throws SQLException { - return true; - } - - @Override - public boolean supportsOrderByUnrelated() throws SQLException { - return false; - } - - @Override - public boolean supportsGroupBy() throws SQLException { - return true; - } - - @Override - public boolean supportsGroupByUnrelated() throws SQLException { - return false; - } - - @Override - public boolean supportsGroupByBeyondSelect() throws SQLException { - return false; - } - - @Override - public boolean supportsLikeEscapeClause() throws SQLException { - return false; - } - - @Override - public boolean supportsMultipleResultSets() throws SQLException { - return false; - } - - @Override - public boolean supportsMultipleTransactions() throws SQLException { - return false; - } - - @Override - public boolean supportsNonNullableColumns() throws SQLException { - return true; - } - - @Override - public boolean supportsMinimumSQLGrammar() throws SQLException { - return true; - } - - @Override - public boolean supportsCoreSQLGrammar() throws SQLException { - return true; - } - - @Override - public boolean supportsExtendedSQLGrammar() throws SQLException { - return false; - } - - @Override - public boolean supportsANSI92EntryLevelSQL() throws SQLException { - return false; - } - - @Override - public boolean supportsANSI92IntermediateSQL() throws SQLException { - return false; - } - - @Override - public boolean supportsANSI92FullSQL() throws SQLException { - return false; - } - - @Override - public boolean supportsIntegrityEnhancementFacility() throws SQLException { - return false; - } - - @Override - public boolean supportsOuterJoins() throws SQLException { - return true; - } - - @Override - public boolean supportsFullOuterJoins() throws SQLException { - return false; - } - - @Override - public boolean supportsLimitedOuterJoins() throws SQLException { - return true; - } - - @Override - public String getSchemaTerm() throws SQLException { - return "schema"; - } - - @Override - public String getProcedureTerm() throws SQLException { - return "procedure"; - } - - @Override - public String getCatalogTerm() throws SQLException { - return "catalog"; - } - - @Override - public boolean isCatalogAtStart() throws SQLException { - return true; - } - - @Override - public String getCatalogSeparator() throws SQLException { - return "."; - } - - @Override - public boolean supportsSchemasInDataManipulation() throws SQLException { - return false; - } - - @Override - public boolean supportsSchemasInProcedureCalls() throws SQLException { - return false; - } - - @Override - public boolean supportsSchemasInTableDefinitions() throws SQLException { - return false; - } - - @Override - public boolean supportsSchemasInIndexDefinitions() throws SQLException { - return false; - } - - @Override - public boolean supportsSchemasInPrivilegeDefinitions() throws SQLException { - return false; - } - - @Override - public boolean supportsCatalogsInDataManipulation() throws SQLException { - return false; - } - - @Override - public boolean supportsCatalogsInProcedureCalls() throws SQLException { - return false; - } - - @Override - public boolean supportsCatalogsInTableDefinitions() throws SQLException { - return false; - } - - @Override - public boolean supportsCatalogsInIndexDefinitions() throws SQLException { - return false; - } - - @Override - public boolean supportsCatalogsInPrivilegeDefinitions() throws SQLException { - return false; - } - - @Override - public boolean supportsPositionedDelete() throws SQLException { - return false; - } - - @Override - public boolean supportsPositionedUpdate() throws SQLException { - return false; - } - - @Override - public boolean supportsSelectForUpdate() throws SQLException { - return false; - } - - @Override - public boolean supportsStoredProcedures() throws SQLException { - return false; - } - - @Override - public boolean supportsSubqueriesInComparisons() throws SQLException { - return false; - } - - @Override - public boolean supportsSubqueriesInExists() throws SQLException { - return true; - } - - @Override - public boolean supportsSubqueriesInIns() throws SQLException { - return true; - } - - @Override - public boolean supportsSubqueriesInQuantifieds() throws SQLException { - return false; - } - - @Override - public boolean supportsCorrelatedSubqueries() throws SQLException { - return false; - } - - @Override - public boolean supportsUnion() throws SQLException { - return true; - } - - @Override - public boolean supportsUnionAll() throws SQLException { - return true; - } - - @Override - public boolean supportsOpenCursorsAcrossCommit() throws SQLException { - return false; - } - - @Override - public boolean supportsOpenCursorsAcrossRollback() throws SQLException { - return false; - } - - @Override - public boolean supportsOpenStatementsAcrossCommit() throws SQLException { - return false; - } - - @Override - public boolean supportsOpenStatementsAcrossRollback() throws SQLException { - return false; - } - - @Override - public int getMaxBinaryLiteralLength() throws SQLException { - return 0; - } - - @Override - public int getMaxCharLiteralLength() throws SQLException { - return 0; - } - - @Override - public int getMaxColumnNameLength() throws SQLException { - return 0; - } - - @Override - public int getMaxColumnsInGroupBy() throws SQLException { - return 0; - } - - @Override - public int getMaxColumnsInIndex() throws SQLException { - return 0; - } - - @Override - public int getMaxColumnsInOrderBy() throws SQLException { - return 0; - } - - @Override - public int getMaxColumnsInSelect() throws SQLException { - return 0; - } - - @Override - public int getMaxColumnsInTable() throws SQLException { - return 0; - } - - @Override - public int getMaxConnections() throws SQLException { - return 0; - } - - @Override - public int getMaxCursorNameLength() throws SQLException { - return 0; - } - - @Override - public int getMaxIndexLength() throws SQLException { - return 0; - } - - @Override - public int getMaxSchemaNameLength() throws SQLException { - return 0; - } - - @Override - public int getMaxProcedureNameLength() throws SQLException { - return 0; - } - - @Override - public int getMaxCatalogNameLength() throws SQLException { - return 0; - } - - @Override - public int getMaxRowSize() throws SQLException { - return 0; - } - - @Override - public boolean doesMaxRowSizeIncludeBlobs() throws SQLException { - return false; - } - - @Override - public int getMaxStatementLength() throws SQLException { - return 0; - } - - @Override - public int getMaxStatements() throws SQLException { - return 0; - } - - @Override - public int getMaxTableNameLength() throws SQLException { - return 0; - } - - @Override - public int getMaxTablesInSelect() throws SQLException { - return 0; - } - - @Override - public int getMaxUserNameLength() throws SQLException { - return 0; - } - - @Override - public int getDefaultTransactionIsolation() throws SQLException { - return Connection.TRANSACTION_NONE; - } - - @Override - public boolean supportsTransactions() throws SQLException { - return false; - } - - @Override - public boolean supportsTransactionIsolationLevel(int level) throws SQLException { - return false; - } - - @Override - public boolean supportsDataDefinitionAndDataManipulationTransactions() throws SQLException { - return false; - } - - @Override - public boolean supportsDataManipulationTransactionsOnly() throws SQLException { - return false; - } - - @Override - public boolean dataDefinitionCausesTransactionCommit() throws SQLException { - return false; - } - - @Override - public boolean dataDefinitionIgnoredInTransactions() throws SQLException { - return false; - } - - @Override - public ResultSet getProcedureColumns(String catalog, - String schemaPattern, - String procedureNamePattern, - String columnNamePattern) - throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.STORED_PROCEDURE_COLUMNS); - } - - @Override - public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) - throws SQLException { - try { - if (types != null && !Arrays.asList(types).contains("TABLE")) { - connection.checkNotClosed(); - return asEmptyMetadataResultSet(DatabaseMetadataTable.TABLES); - } - String[] parts = tableNamePattern == null ? new String[] { "" } : tableNamePattern.split("%"); - List> spaces = (List>) connection.nativeSelect( - _VSPACE, 0, Arrays.asList(), 0, SPACES_MAX, 0 - ); - List> rows = new ArrayList>(); - for (List space : spaces) { - String tableName = (String) space.get(NAME_IDX); - List> format = (List>) space.get(FORMAT_IDX); - /* - * Skip Tarantool system spaces (started with underscore). - * Skip spaces that don't have format. Tarantool/SQL does not support such spaces. - */ - if (!tableName.startsWith("_") && format.size() > 0 && like(tableName, parts)) { - rows.add(Arrays.asList( - null, null, - tableName, - "TABLE", - null, null, - null, null, - null, null) - ); - } - } - return asMetadataResultSet(DatabaseMetadataTable.TABLES, rows); - } catch (Exception e) { - throw new SQLException( - "Failed to retrieve table(s) description: " + - "tableNamePattern=\"" + tableNamePattern + "\".", e); - } - } - - protected boolean like(String value, String[] parts) { - if (parts == null || parts.length == 0) { - return true; + protected static final int _VSPACE = 281; + protected static final int _VINDEX = 289; + protected static final int SPACES_MAX = 65535; + public static final int FORMAT_IDX = 6; + public static final int NAME_IDX = 2; + public static final int INDEX_FORMAT_IDX = 5; + public static final int SPACE_ID_IDX = 0; + protected final SQLConnection connection; + + public SQLDatabaseMetadata(SQLConnection connection) { + this.connection = connection; + } + + @Override + public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) + throws SQLException { + log("getProcedures"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.STORED_PROCEDURES); + } + + @Override + public boolean allProceduresAreCallable() throws SQLException { + log("allProceduresAreCallable"); + return false; + } + + @Override + public boolean allTablesAreSelectable() throws SQLException { + log("allTablesAreSelectable"); + return true; + } + + @Override + public String getURL() throws SQLException { + log("getURL"); + return connection.getUrl(); + } + + @Override + public String getUserName() throws SQLException { + log("getUserName"); + return connection.getProperties().getProperty("user"); + } + + @Override + public boolean isReadOnly() throws SQLException { + log("isReadOnly"); + return false; + } + + @Override + public boolean nullsAreSortedHigh() throws SQLException { + log("nullsAreSortedHigh"); + return false; + } + + @Override + public boolean nullsAreSortedLow() throws SQLException { + log("nullsAreSortedLow"); + return true; + } + + @Override + public boolean nullsAreSortedAtStart() throws SQLException { + log("nullsAreSortedAtStart"); + return false; + } + + @Override + public boolean nullsAreSortedAtEnd() throws SQLException { + log("nullsAreSortedAtEnd"); + return false; + } + + @Override + public String getDatabaseProductName() throws SQLException { + log("getDatabaseProductName"); + return "Tarantool"; + } + + @Override + public String getDatabaseProductVersion() throws SQLException { + log("getDatabaseProductVersion"); + return connection.getServerVersion(); + } + + @Override + public String getDriverName() throws SQLException { + log("getDriverName"); + return SQLConstant.DRIVER_NAME; + } + + @Override + public String getDriverVersion() throws SQLException { + log("getDriverVersion"); + return Version.version; + } + + @Override + public int getDriverMajorVersion() { + log("getDriverMajorVersion"); + return Version.majorVersion; + } + + @Override + public int getDriverMinorVersion() { + log("getDriverMinorVersion"); + return Version.minorVersion; + } + + @Override + public boolean usesLocalFiles() throws SQLException { + log("usesLocalFiles"); + return false; + } + + @Override + public boolean usesLocalFilePerTable() throws SQLException { + log("usesLocalFilePerTable"); + return false; + } + + @Override + public boolean supportsMixedCaseIdentifiers() throws SQLException { + log("supportsMixedCaseIdentifiers"); + return true; + } + + @Override + public boolean storesUpperCaseIdentifiers() throws SQLException { + log("storesUpperCaseIdentifiers"); + return false; + } + + @Override + public boolean storesLowerCaseIdentifiers() throws SQLException { + log("storesLowerCaseIdentifiers"); + return false; + } + + @Override + public boolean storesMixedCaseIdentifiers() throws SQLException { + log("storesMixedCaseIdentifiers"); + return true; + } + + @Override + public boolean supportsMixedCaseQuotedIdentifiers() throws SQLException { + log("supportsMixedCaseQuotedIdentifiers"); + return false; + } + + @Override + public boolean storesUpperCaseQuotedIdentifiers() throws SQLException { + log("storesUpperCaseQuotedIdentifiers"); + return false; + } + + @Override + public boolean storesLowerCaseQuotedIdentifiers() throws SQLException { + log("storesLowerCaseQuotedIdentifiers"); + return false; + } + + @Override + public boolean storesMixedCaseQuotedIdentifiers() throws SQLException { + log("storesMixedCaseQuotedIdentifiers"); + return false; + } + + @Override + public String getIdentifierQuoteString() throws SQLException { + log("getIdentifierQuoteString"); + return " "; + } + + @Override + public String getSQLKeywords() throws SQLException { + log("getSQLKeywords"); + return ""; + } + + @Override + public String getNumericFunctions() throws SQLException { + log("getNumericFunctions"); + return ""; + } + + @Override + public String getStringFunctions() throws SQLException { + log("getStringFunctions"); + return ""; + } + + @Override + public String getSystemFunctions() throws SQLException { + log("getSystemFunctions"); + return ""; + } + + @Override + public String getTimeDateFunctions() throws SQLException { + log("getTimeDateFunctions"); + return ""; + } + + @Override + public String getSearchStringEscape() throws SQLException { + log("getSearchStringEscape"); + return null; + } + + @Override + public String getExtraNameCharacters() throws SQLException { + log("getExtraNameCharacters"); + return ""; + } + + @Override + public boolean supportsAlterTableWithAddColumn() throws SQLException { + log("supportsAlterTableWithAddColumn"); + return false; + } + + @Override + public boolean supportsAlterTableWithDropColumn() throws SQLException { + log("supportsAlterTableWithDropColumn"); + return false; + } + + @Override + public boolean supportsColumnAliasing() throws SQLException { + log("supportsColumnAliasing"); + return true; + } + + @Override + public boolean nullPlusNonNullIsNull() throws SQLException { + log("nullPlusNonNullIsNull"); + return true; + } + + @Override + public boolean supportsConvert() throws SQLException { + log("supportsConvert"); + return false; + } + + @Override + public boolean supportsConvert(int fromType, int toType) throws SQLException { + log("supportsConvert"); + return false; + } + + @Override + public boolean supportsTableCorrelationNames() throws SQLException { + log("supportsTableCorrelationNames"); + return false; + } + + @Override + public boolean supportsDifferentTableCorrelationNames() throws SQLException { + log("supportsDifferentTableCorrelationNames"); + return false; + } + + @Override + public boolean supportsExpressionsInOrderBy() throws SQLException { + log("supportsExpressionsInOrderBy"); + return true; + } + + @Override + public boolean supportsOrderByUnrelated() throws SQLException { + log("supportsOrderByUnrelated"); + return false; + } + + @Override + public boolean supportsGroupBy() throws SQLException { + log("supportsGroupBy"); + return true; + } + + @Override + public boolean supportsGroupByUnrelated() throws SQLException { + log("supportsGroupByUnrelated"); + return false; + } + + @Override + public boolean supportsGroupByBeyondSelect() throws SQLException { + log("supportsGroupByBeyondSelect"); + return false; + } + + @Override + public boolean supportsLikeEscapeClause() throws SQLException { + log("supportsLikeEscapeClause"); + return false; + } + + @Override + public boolean supportsMultipleResultSets() throws SQLException { + log("supportsMultipleResultSets"); + return false; + } + + @Override + public boolean supportsMultipleTransactions() throws SQLException { + log("supportsMultipleTransactions"); + return false; + } + + @Override + public boolean supportsNonNullableColumns() throws SQLException { + log("supportsNonNullableColumns"); + return true; + } + + @Override + public boolean supportsMinimumSQLGrammar() throws SQLException { + log("supportsMinimumSQLGrammar"); + return true; + } + + @Override + public boolean supportsCoreSQLGrammar() throws SQLException { + log("supportsCoreSQLGrammar"); + return true; + } + + @Override + public boolean supportsExtendedSQLGrammar() throws SQLException { + log("supportsExtendedSQLGrammar"); + return false; + } + + @Override + public boolean supportsANSI92EntryLevelSQL() throws SQLException { + log("supportsANSI92EntryLevelSQL"); + return false; + } + + @Override + public boolean supportsANSI92IntermediateSQL() throws SQLException { + log("supportsANSI92IntermediateSQL"); + return false; + } + + @Override + public boolean supportsANSI92FullSQL() throws SQLException { + log("supportsANSI92FullSQL"); + return false; + } + + @Override + public boolean supportsIntegrityEnhancementFacility() throws SQLException { + log("supportsIntegrityEnhancementFacility"); + return false; + } + + @Override + public boolean supportsOuterJoins() throws SQLException { + log("supportsOuterJoins"); + return true; + } + + @Override + public boolean supportsFullOuterJoins() throws SQLException { + log("supportsFullOuterJoins"); + return false; + } + + @Override + public boolean supportsLimitedOuterJoins() throws SQLException { + log("supportsLimitedOuterJoins"); + return true; + } + + @Override + public String getSchemaTerm() throws SQLException { + log("getSchemaTerm"); + return "schema"; + } + + @Override + public String getProcedureTerm() throws SQLException { + log("getProcedureTerm"); + return "procedure"; + } + + @Override + public String getCatalogTerm() throws SQLException { + log("getCatalogTerm"); + return "catalog"; + } + + @Override + public boolean isCatalogAtStart() throws SQLException { + log("isCatalogAtStart"); + return true; + } + + @Override + public String getCatalogSeparator() throws SQLException { + log("getCatalogSeparator"); + return "."; + } + + @Override + public boolean supportsSchemasInDataManipulation() throws SQLException { + log("supportsSchemasInDataManipulation"); + return false; + } + + @Override + public boolean supportsSchemasInProcedureCalls() throws SQLException { + log("supportsSchemasInProcedureCalls"); + return false; + } + + @Override + public boolean supportsSchemasInTableDefinitions() throws SQLException { + log("supportsSchemasInTableDefinitions"); + return false; + } + + @Override + public boolean supportsSchemasInIndexDefinitions() throws SQLException { + log("supportsSchemasInIndexDefinitions"); + return false; + } + + @Override + public boolean supportsSchemasInPrivilegeDefinitions() throws SQLException { + log("supportsSchemasInPrivilegeDefinitions"); + return false; + } + + @Override + public boolean supportsCatalogsInDataManipulation() throws SQLException { + log("supportsCatalogsInDataManipulation"); + return false; + } + + @Override + public boolean supportsCatalogsInProcedureCalls() throws SQLException { + log("supportsCatalogsInProcedureCalls"); + return false; + } + + @Override + public boolean supportsCatalogsInTableDefinitions() throws SQLException { + log("supportsCatalogsInTableDefinitions"); + return false; + } + + @Override + public boolean supportsCatalogsInIndexDefinitions() throws SQLException { + log("supportsCatalogsInIndexDefinitions"); + return false; + } + + @Override + public boolean supportsCatalogsInPrivilegeDefinitions() throws SQLException { + log("supportsCatalogsInPrivilegeDefinitions"); + return false; + } + + @Override + public boolean supportsPositionedDelete() throws SQLException { + log("supportsPositionedDelete"); + return false; + } + + @Override + public boolean supportsPositionedUpdate() throws SQLException { + log("supportsPositionedUpdate"); + return false; + } + + @Override + public boolean supportsSelectForUpdate() throws SQLException { + log("supportsSelectForUpdate"); + return false; + } + + @Override + public boolean supportsStoredProcedures() throws SQLException { + log("supportsStoredProcedures"); + return false; + } + + @Override + public boolean supportsSubqueriesInComparisons() throws SQLException { + log("supportsSubqueriesInComparisons"); + return false; + } + + @Override + public boolean supportsSubqueriesInExists() throws SQLException { + log("supportsSubqueriesInExists"); + return true; + } + + @Override + public boolean supportsSubqueriesInIns() throws SQLException { + log("supportsSubqueriesInIns"); + return true; + } + + @Override + public boolean supportsSubqueriesInQuantifieds() throws SQLException { + log("supportsSubqueriesInQuantifieds"); + return false; + } + + @Override + public boolean supportsCorrelatedSubqueries() throws SQLException { + log("supportsCorrelatedSubqueries"); + return false; + } + + @Override + public boolean supportsUnion() throws SQLException { + log("supportsUnion"); + return true; + } + + @Override + public boolean supportsUnionAll() throws SQLException { + log("supportsUnionAll"); + return true; + } + + @Override + public boolean supportsOpenCursorsAcrossCommit() throws SQLException { + log("supportsOpenCursorsAcrossCommit"); + return false; + } + + @Override + public boolean supportsOpenCursorsAcrossRollback() throws SQLException { + log("supportsOpenCursorsAcrossRollback"); + return false; + } + + @Override + public boolean supportsOpenStatementsAcrossCommit() throws SQLException { + log("supportsOpenStatementsAcrossCommit"); + return false; + } + + @Override + public boolean supportsOpenStatementsAcrossRollback() throws SQLException { + log("supportsOpenStatementsAcrossRollback"); + return false; + } + + @Override + public int getMaxBinaryLiteralLength() throws SQLException { + log("getMaxBinaryLiteralLength"); + return 0; + } + + @Override + public int getMaxCharLiteralLength() throws SQLException { + log("getMaxCharLiteralLength"); + return 0; + } + + @Override + public int getMaxColumnNameLength() throws SQLException { + log("getMaxColumnNameLength"); + return 0; + } + + @Override + public int getMaxColumnsInGroupBy() throws SQLException { + log("getMaxColumnsInGroupBy"); + return 0; + } + + @Override + public int getMaxColumnsInIndex() throws SQLException { + log("getMaxColumnsInIndex"); + return 0; + } + + @Override + public int getMaxColumnsInOrderBy() throws SQLException { + log("getMaxColumnsInOrderBy"); + return 0; + } + + @Override + public int getMaxColumnsInSelect() throws SQLException { + log("getMaxColumnsInSelect"); + return 0; + } + + @Override + public int getMaxColumnsInTable() throws SQLException { + log("getMaxColumnsInTable"); + return 0; + } + + @Override + public int getMaxConnections() throws SQLException { + log("getMaxConnections"); + return 0; + } + + @Override + public int getMaxCursorNameLength() throws SQLException { + log("getMaxCursorNameLength"); + return 0; + } + + @Override + public int getMaxIndexLength() throws SQLException { + log("getMaxIndexLength"); + return 0; + } + + @Override + public int getMaxSchemaNameLength() throws SQLException { + log("getMaxSchemaNameLength"); + return 0; + } + + @Override + public int getMaxProcedureNameLength() throws SQLException { + log("getMaxProcedureNameLength"); + return 0; + } + + @Override + public int getMaxCatalogNameLength() throws SQLException { + log("getMaxCatalogNameLength"); + return 0; + } + + @Override + public int getMaxRowSize() throws SQLException { + log("getMaxRowSize"); + return 0; + } + + @Override + public boolean doesMaxRowSizeIncludeBlobs() throws SQLException { + log("doesMaxRowSizeIncludeBlobs"); + return false; + } + + @Override + public int getMaxStatementLength() throws SQLException { + log("getMaxStatementLength"); + return 0; + } + + @Override + public int getMaxStatements() throws SQLException { + log("getMaxStatements"); + return 0; + } + + @Override + public int getMaxTableNameLength() throws SQLException { + log("getMaxTableNameLength"); + return 0; + } + + @Override + public int getMaxTablesInSelect() throws SQLException { + log("getMaxTablesInSelect"); + return 0; + } + + @Override + public int getMaxUserNameLength() throws SQLException { + log("getMaxUserNameLength"); + return 0; + } + + @Override + public int getDefaultTransactionIsolation() throws SQLException { + log("getDefaultTransactionIsolation"); + return Connection.TRANSACTION_NONE; + } + + @Override + public boolean supportsTransactions() throws SQLException { + log("supportsTransactions"); + return false; + } + + @Override + public boolean supportsTransactionIsolationLevel(int level) throws SQLException { + log("supportsTransactionIsolationLevel"); + return false; + } + + @Override + public boolean supportsDataDefinitionAndDataManipulationTransactions() throws SQLException { + log("supportsDataDefinitionAndDataManipulationTransactions"); + return false; + } + + @Override + public boolean supportsDataManipulationTransactionsOnly() throws SQLException { + log("supportsDataManipulationTransactionsOnly"); + return false; + } + + @Override + public boolean dataDefinitionCausesTransactionCommit() throws SQLException { + log("dataDefinitionCausesTransactionCommit"); + return false; + } + + @Override + public boolean dataDefinitionIgnoredInTransactions() throws SQLException { + log("dataDefinitionIgnoredInTransactions"); + return false; + } + + @Override + public ResultSet getProcedureColumns(String catalog, + String schemaPattern, + String procedureNamePattern, + String columnNamePattern) + throws SQLException { + log("getProcedureColumns"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.STORED_PROCEDURE_COLUMNS); + } + + @Override + public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) + throws SQLException { + log("getTables"); + try { + if (types != null && !Arrays.asList(types).contains("TABLE")) { + connection.checkNotClosed(); + return asEmptyMetadataResultSet(DatabaseMetadataTable.TABLES); + } + String[] parts = tableNamePattern == null ? new String[]{""} : tableNamePattern.split("%"); + List> spaces = (List>) connection.nativeSelect( + _VSPACE, 0, Arrays.asList(), 0, SPACES_MAX, 0 + ); + List> rows = new ArrayList>(); + for (List space : spaces) { + String tableName = (String) space.get(NAME_IDX); + List> format = (List>) space.get(FORMAT_IDX); + /* + * Skip Tarantool system spaces (started with underscore). + * Skip spaces that don't have format. Tarantool/SQL does not support such spaces. + */ + if (!tableName.startsWith("_") && format.size() > 0 && like(tableName, parts)) { + rows.add(Arrays.asList( + null, null, + tableName, + "TABLE", + null, null, + null, null, + null, null) + ); } - int i = 0; - for (String part : parts) { - i = value.indexOf(part, i); - if (i < 0) { - break; - } - i += part.length(); - } - return (i > -1 && (parts[parts.length - 1].length() == 0 || i == value.length())); - } - - @Override - public ResultSet getTableTypes() throws SQLException { - return asMetadataResultSet( - DatabaseMetadataTable.TABLE_TYPES, - Collections.singletonList(Arrays.asList("TABLE")) - ); - } - - @Override - public ResultSet getSchemas() throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.SCHEMAS); - } - - @Override - public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.SCHEMAS); - } - - @Override - public ResultSet getCatalogs() throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.CATALOGS); - } - - @Override - public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) - throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.BEST_ROW_IDENTIFIER); - } - - @Override - public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) - throws SQLException { - try { - String[] tableParts = tableNamePattern == null ? new String[] { "" } : tableNamePattern.split("%"); - String[] colParts = columnNamePattern == null ? new String[] { "" } : columnNamePattern.split("%"); - List> spaces = (List>) connection.nativeSelect( - _VSPACE, 0, Arrays.asList(), 0, SPACES_MAX, 0 - ); - List> rows = new ArrayList<>(); - for (List space : spaces) { - String tableName = (String) space.get(NAME_IDX); - List> format = (List>) space.get(FORMAT_IDX); - /* - * Skip Tarantool system spaces (started with underscore). - * Skip spaces that don't have format. Tarantool/SQL does not support such spaces. - */ - if (!tableName.startsWith("_") && format.size() > 0 && like(tableName, tableParts)) { - for (int columnIdx = 1; columnIdx <= format.size(); columnIdx++) { - Map f = format.get(columnIdx - 1); - String columnName = (String) f.get("name"); - String typeName = (String) f.get("type"); - if (like(columnName, colParts)) { - rows.add(Arrays.asList( - null, // table catalog - null, // table schema - tableName, - columnName, - Types.OTHER, // data type - typeName, - null, // column size - null, // buffer length - null, // decimal digits - 10, // num prec radix - columnNullableUnknown, - null, // remarks - null, // column def - null, // sql data type - null, // sql datatype sub - null, // char octet length - columnIdx, // ordinal position - "", // is nullable - null, // scope catalog - null, // scope schema - null, // scope table - Types.OTHER, // source data type - "NO", // is autoincrement - "NO") // is generated column - ); - } - } - } + } + return asMetadataResultSet(DatabaseMetadataTable.TABLES, rows); + } catch (Exception e) { + throw new SQLException( + "Failed to retrieve table(s) description: " + + "tableNamePattern=\"" + tableNamePattern + "\".", e); + } + } + + protected boolean like(String value, String[] parts) { + log("like"); + if (parts == null || parts.length == 0) { + return true; + } + int i = 0; + for (String part : parts) { + i = value.indexOf(part, i); + if (i < 0) { + break; + } + i += part.length(); + } + return (i > -1 && (parts[parts.length - 1].length() == 0 || i == value.length())); + } + + @Override + public ResultSet getTableTypes() throws SQLException { + log("getTableTypes"); + return asMetadataResultSet( + DatabaseMetadataTable.TABLE_TYPES, + Collections.singletonList(Arrays.asList("TABLE")) + ); + } + + @Override + public ResultSet getSchemas() throws SQLException { + log("getSchemas"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.SCHEMAS); + } + + @Override + public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException { + log("getSchemas"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.SCHEMAS); + } + + @Override + public ResultSet getCatalogs() throws SQLException { + log("getCatalogs"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.CATALOGS); + } + + @Override + public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) + throws SQLException { + log("getBestRowIdentifier"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.BEST_ROW_IDENTIFIER); + } + + @Override + public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) + throws SQLException { + log("getColumns"); + try { + // Разделение шаблонов таблицы и столбцов + String[] tableParts = tableNamePattern == null ? new String[]{""} : tableNamePattern.split("%"); + String[] colParts = columnNamePattern == null ? new String[]{""} : columnNamePattern.split("%"); + + // Получение списка всех таблиц + List> spaces = (List>) connection.nativeSelect( + _VSPACE, 0, Arrays.asList(), 0, SPACES_MAX, 0 + ); + + List> rows = new ArrayList<>(); + + for (List space : spaces) { + String tableName = (String) space.get(NAME_IDX); + List> format = (List>) space.get(FORMAT_IDX); + log("Map from space:" + format); + /* + * Пропускаем системные пространства Tarantool (начинаются с подчеркивания). + * Пропускаем пространства без формата, так как Tarantool/SQL не поддерживает их. + */ + if (!tableName.startsWith("_") && format.size() > 0 && like(tableName, tableParts)) { + // Получаем первичные ключи для текущей таблицы + ResultSet primaryKeys = getPrimaryKeys(catalog, schemaPattern, tableName); + Set primaryKeyColumns = new HashSet<>(); + + while (primaryKeys.next()) { + primaryKeyColumns.add(primaryKeys.getString("COLUMN_NAME")); + } + + // Обрабатываем формат таблицы + for (int columnIdx = 1; columnIdx <= format.size(); columnIdx++) { + Map f = format.get(columnIdx - 1); + String columnName = (String) f.get("name"); + String typeName = (String) f.get("type"); + + // Проверяем имя столбца + if (like(columnName, colParts)) { + // Если столбец является первичным ключом, ставим columnNullable, иначе - columnNullableUnknown + int nullableStatus = primaryKeyColumns.contains(columnName) ? columnNoNulls : columnNullableUnknown; + + rows.add(Arrays.asList( + null, // table catalog + null, // table schema + tableName, + columnName, + mapToJdbcType(typeName), // data type + typeName, + null, // column size + null, // buffer length + null, // decimal digits + 10, // num prec radix + nullableStatus, // nullable статус (если ключ - то columnNullable) + null, // remarks + null, // column def + null, // sql data type + null, // sql datatype sub + null, // char octet length + columnIdx, // ordinal position + "", // is nullable + null, // scope catalog + null, // scope schema + null, // scope table + Types.OTHER, // source data type + "NO", // is autoincrement + "NO" // is generated column + )); } - - return asMetadataResultSet(DatabaseMetadataTable.COLUMNS, rows); - } catch (Exception e) { - throw new SQLException( - "Error processing table column metadata: " + - "tableNamePattern=\"" + tableNamePattern + "\"; " + - "columnNamePattern=\"" + columnNamePattern + "\".", e); + } } - } - - @Override - public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) - throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.COLUMN_PRIVILEGES); - } - - @Override - public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) - throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.TABLE_PRIVILEGES); - } - - @Override - public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.VERSION_COLUMNS); - } - - @Override - public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.FOREIGN_KEYS); - } - - @Override - public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { - if (table == null || table.isEmpty()) { - connection.checkNotClosed(); + } + + return asMetadataResultSet(DatabaseMetadataTable.COLUMNS, rows); + } catch (Exception e) { + throw new SQLException( + "Error processing table column metadata: " + + "tableNamePattern=\"" + tableNamePattern + "\"; " + + "columnNamePattern=\"" + columnNamePattern + "\".", e); + } + } + + private int mapToJdbcType(String typeName) { + LocalLogger.log("mapToJdbcType: " + typeName); + switch (typeName) { + case "decimal": + case "double": + return Types.DOUBLE; + case "integer": + return Types.INTEGER; + case "float": + return Types.FLOAT; + case "bigint": + case "numeric": + case "unsigned": + return Types.BIGINT; + case "varchar": + case "text": + case "uuid": + return Types.VARCHAR; + case "boolean": + case "bool": + return Types.BIT; + } + return Types.VARCHAR; + } + + + @Override + public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) + throws SQLException { + log("getColumnPrivileges"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.COLUMN_PRIVILEGES); + } + + @Override + public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) + throws SQLException { + log("getTablePrivileges"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.TABLE_PRIVILEGES); + } + + @Override + public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { + log("getVersionColumns"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.VERSION_COLUMNS); + } + + @Override + public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException { + log("getImportedKeys"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.FOREIGN_KEYS); + } + + @Override + public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { + log("getPrimaryKeys"); + if (table == null || table.isEmpty()) { + log("Table is null or empty, returning empty metadata ResultSet."); + connection.checkNotClosed(); + return asEmptyMetadataResultSet(DatabaseMetadataTable.PRIMARY_KEYS); + } + + try { + log("Fetching spaces for table: " + table); + List spaces = connection.nativeSelect(_VSPACE, 2, Collections.singletonList(table), 0, 1, 0); + + if (spaces == null || spaces.size() == 0) { + log("No spaces found for table: " + table + ", returning empty metadata ResultSet."); + return asEmptyMetadataResultSet(DatabaseMetadataTable.PRIMARY_KEYS); + } + + log("Processing space data for table: " + table); + List space = ensureType(List.class, spaces.get(0)); + List fields = ensureType(List.class, space.get(FORMAT_IDX)); + int spaceId = ensureType(Number.class, space.get(SPACE_ID_IDX)).intValue(); + + log("Fetching indexes for spaceId: " + spaceId); + List indexes = connection.nativeSelect(_VINDEX, 0, Arrays.asList(spaceId, 0), 0, 1, 0); + + if (indexes == null || indexes.size() == 0) { + log("No indexes found for spaceId: " + spaceId + ", returning empty metadata ResultSet."); + return asEmptyMetadataResultSet(DatabaseMetadataTable.PRIMARY_KEYS); + } + + log("Processing primary key data for spaceId: " + spaceId); + List primaryKey = ensureType(List.class, indexes.get(0)); + log("PKeys: " + primaryKey); + List parts = ensureType(List.class, primaryKey.get(INDEX_FORMAT_IDX)); + log("Parts: " + parts); + + List> rows = new ArrayList<>(); + log("Iterating over parts to retrieve column details."); + for (int i = 0; i < parts.size(); i++) { + Object partObj = parts.get(i); + log("parts.get(i) = " + partObj); + log("parts.get(i).class = " + partObj.getClass()); + + if (partObj instanceof Map) { + Map part = (Map) partObj; + int ordinal = ensureType(Number.class, part.get("field")).intValue(); + Map field = ensureType(Map.class, fields.get(ordinal)); + String column = ensureType(String.class, field.get("name")); + rows.add(Arrays.asList(null, null, table, column, i + 1, primaryKey.get(NAME_IDX))); + } else if (partObj instanceof List) { + List partList = (List) partObj; + if (partList.size() >= 1) { + int ordinal = ensureType(Number.class, partList.get(0)).intValue(); + Map field = ensureType(Map.class, fields.get(ordinal)); + String column = ensureType(String.class, field.get("name")); + rows.add(Arrays.asList(null, null, table, column, i + 1, primaryKey.get(NAME_IDX))); + } else { + log("Invalid partList, returning empty metadata ResultSet."); return asEmptyMetadataResultSet(DatabaseMetadataTable.PRIMARY_KEYS); + } + } else { + log("Unknown part type, returning empty metadata ResultSet."); + return asEmptyMetadataResultSet(DatabaseMetadataTable.PRIMARY_KEYS); } - - try { - List spaces = connection.nativeSelect(_VSPACE, 2, Collections.singletonList(table), 0, 1, 0); - - if (spaces == null || spaces.size() == 0) { - return asEmptyMetadataResultSet(DatabaseMetadataTable.PRIMARY_KEYS); - } - - List space = ensureType(List.class, spaces.get(0)); - List fields = ensureType(List.class, space.get(FORMAT_IDX)); - int spaceId = ensureType(Number.class, space.get(SPACE_ID_IDX)).intValue(); - List indexes = connection.nativeSelect(_VINDEX, 0, Arrays.asList(spaceId, 0), 0, 1, 0); - List primaryKey = ensureType(List.class, indexes.get(0)); - List parts = ensureType(List.class, primaryKey.get(INDEX_FORMAT_IDX)); - - List> rows = new ArrayList<>(); - for (int i = 0; i < parts.size(); i++) { - // For native spaces, the 'parts' is 'List of Lists'. - // We only accept SQL spaces, for which the parts is 'List of Maps'. - Map part = checkType(Map.class, parts.get(i)); - if (part == null) { - return asEmptyMetadataResultSet(DatabaseMetadataTable.PRIMARY_KEYS); - } - - int ordinal = ensureType(Number.class, part.get("field")).intValue(); - Map field = ensureType(Map.class, fields.get(ordinal)); - // The 'name' field is optional in the format structure. But it is present for SQL space. - String column = ensureType(String.class, field.get("name")); - rows.add(Arrays.asList(null, null, table, column, i + 1, primaryKey.get(NAME_IDX))); - } - // Sort results by column name. - rows.sort((left, right) -> { - String col0 = (String) left.get(3); - String col1 = (String) right.get(3); - return col0.compareTo(col1); - }); - return asMetadataResultSet(DatabaseMetadataTable.PRIMARY_KEYS, rows); - } catch (Exception e) { - throw new SQLException("Error processing metadata for table \"" + table + "\".", e); - } - } - - @Override - public ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.FOREIGN_KEYS); - } - - @Override - public ResultSet getCrossReference(String parentCatalog, - String parentSchema, - String parentTable, - String foreignCatalog, - String foreignSchema, - String foreignTable) - throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.FOREIGN_KEYS); - } - - @Override - public ResultSet getTypeInfo() throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.TYPE_INFO); - } - - @Override - public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) - throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.INDEX_INFO); - } - - @Override - public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) - throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.UDTS); - } - - @Override - public boolean supportsResultSetType(int type) throws SQLException { - return type == ResultSet.TYPE_FORWARD_ONLY || - type == ResultSet.TYPE_SCROLL_INSENSITIVE; - } - - @Override - public boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException { - return supportsResultSetType(type) && concurrency == ResultSet.CONCUR_READ_ONLY; - } - - @Override - public boolean ownUpdatesAreVisible(int type) throws SQLException { - return false; - } - - @Override - public boolean ownDeletesAreVisible(int type) throws SQLException { - return false; - } - - @Override - public boolean ownInsertsAreVisible(int type) throws SQLException { - return false; - } - - @Override - public boolean othersUpdatesAreVisible(int type) throws SQLException { - return false; - } - - @Override - public boolean othersDeletesAreVisible(int type) throws SQLException { - return false; - } - - @Override - public boolean othersInsertsAreVisible(int type) throws SQLException { - return false; - } - - @Override - public boolean updatesAreDetected(int type) throws SQLException { - return false; - } - - @Override - public boolean deletesAreDetected(int type) throws SQLException { - return false; - } - - @Override - public boolean insertsAreDetected(int type) throws SQLException { - return false; - } - - @Override - public boolean supportsBatchUpdates() throws SQLException { - return true; - } - - @Override - public ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.SUPER_TYPES); - } - - @Override - public Connection getConnection() throws SQLException { - return connection; - } - - @Override - public boolean supportsSavepoints() throws SQLException { - return false; - } - - @Override - public boolean supportsNamedParameters() throws SQLException { - return true; - } - - @Override - public boolean supportsMultipleOpenResults() throws SQLException { - return false; - } - - @Override - public boolean supportsGetGeneratedKeys() throws SQLException { - return true; - } - - @Override - public ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.SUPER_TABLES); - } - - @Override - public ResultSet getAttributes(String catalog, - String schemaPattern, - String typeNamePattern, - String attributeNamePattern) throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.ATTRIBUTES); - } - - @Override - public ResultSet getClientInfoProperties() throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.CLIENT_INFO_PROPERTIES); - } - - /** - * {@inheritDoc} - *

- * Support of {@link ResultSet#CLOSE_CURSORS_AT_COMMIT} is not - * available now because it requires cursor transaction support. - */ - @Override - public boolean supportsResultSetHoldability(int holdability) throws SQLException { - return holdability == ResultSet.HOLD_CURSORS_OVER_COMMIT; - } - - @Override - public int getResultSetHoldability() throws SQLException { - return ResultSet.HOLD_CURSORS_OVER_COMMIT; - } - - @Override - public int getDatabaseMajorVersion() throws SQLException { - return 0; - } - - @Override - public int getDatabaseMinorVersion() throws SQLException { - return 0; - } - - @Override - public int getJDBCMajorVersion() throws SQLException { - return 2; - } - - @Override - public int getJDBCMinorVersion() throws SQLException { - return 1; - } - - @Override - public int getSQLStateType() throws SQLException { - return DatabaseMetaData.sqlStateSQL; - } - - @Override - public boolean locatorsUpdateCopy() throws SQLException { - return false; - } - - @Override - public boolean supportsStatementPooling() throws SQLException { - return false; - } - - @Override - public RowIdLifetime getRowIdLifetime() throws SQLException { - return RowIdLifetime.ROWID_UNSUPPORTED; - } - - @Override - public boolean supportsStoredFunctionsUsingCallSyntax() throws SQLException { - return false; - } - - @Override - public boolean autoCommitFailureClosesAllResultSets() throws SQLException { - return false; - } - - @Override - public ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) - throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.FUNCTIONS); - } - - @Override - public ResultSet getFunctionColumns(String catalog, - String schemaPattern, - String functionNamePattern, - String columnNamePattern) - throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.FUNCTION_COLUMNS); - } - - @Override - public ResultSet getPseudoColumns(String catalog, - String schemaPattern, - String tableNamePattern, - String columnNamePattern) - throws SQLException { - return asEmptyMetadataResultSet(DatabaseMetadataTable.PSEUDO_COLUMNS); - } - - private ResultSet asMetadataResultSet(List> meta, List> rows) - throws SQLException { - List sqlMeta = meta.stream() - .map(tuple -> new SqlProtoUtils.SQLMetaData(tuple.getFirst(), tuple.getSecond())) - .collect(Collectors.toList()); - SQLResultHolder holder = SQLResultHolder.ofQuery(sqlMeta, rows); - return createMetadataStatement().executeMetadata(holder); - } - - private ResultSet asEmptyMetadataResultSet(List> meta) throws SQLException { - return asMetadataResultSet(meta, Collections.emptyList()); - } - - @Override - public boolean generatedKeyAlwaysReturned() throws SQLException { - return true; - } - - @Override - public T unwrap(Class type) throws SQLException { - if (isWrapperFor(type)) { - return type.cast(this); - } - throw new SQLNonTransientException("SQLDatabaseMetadata does not wrap " + type.getName()); - } - - @Override - public boolean isWrapperFor(Class type) throws SQLException { - return type.isAssignableFrom(this.getClass()); - } - - private TarantoolStatement createMetadataStatement() throws SQLException { - return connection.createStatement().unwrap(TarantoolStatement.class); - } - - private static T ensureType(Class cls, Object v) throws Exception { - if (v == null || !cls.isAssignableFrom(v.getClass())) { - throw new Exception(String.format("Wrong value type '%s', expected '%s'.", - v == null ? "null" : v.getClass().getName(), cls.getName())); - } - return cls.cast(v); - } - - private static T checkType(Class cls, Object v) { - return (v != null && cls.isAssignableFrom(v.getClass())) ? cls.cast(v) : null; - } - + } + + log("Sorting result rows by column name."); + rows.sort((left, right) -> { + String col0 = (String) left.get(3); + String col1 = (String) right.get(3); + return col0.compareTo(col1); + }); + + log("Returning metadata ResultSet rows: " + rows); + return asMetadataResultSet(DatabaseMetadataTable.PRIMARY_KEYS, rows); + } catch (Exception e) { + log("Error occurred while processing metadata for table \"" + table + "\": " + e.getMessage()); + throw new SQLException("Error processing metadata for table \"" + table + "\".", e); + } + } + + @Override + public ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException { + log("getExportedKeys"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.FOREIGN_KEYS); + } + + @Override + public ResultSet getCrossReference(String parentCatalog, + String parentSchema, + String parentTable, + String foreignCatalog, + String foreignSchema, + String foreignTable) + throws SQLException { + log("getCrossReference"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.FOREIGN_KEYS); + } + + @Override + public ResultSet getTypeInfo() throws SQLException { + log("getTypeInfo"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.TYPE_INFO); + } + + @Override + public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) + throws SQLException { + log("getIndexInfo"); + + // Получение первичных ключей + ResultSet primaryKeys = getPrimaryKeys(catalog, schema, table); + + // Сбор информации об индексе на основе первичного ключа + List> rows = new ArrayList<>(); + + while (primaryKeys.next()) { + String columnName = primaryKeys.getString("COLUMN_NAME"); + + // Строим уникальный индекс на основе первичных ключей + List row = new ArrayList<>(); + row.add(null); // TABLE_CAT + row.add(null); // TABLE_SCHEM + row.add(table); // TABLE_NAME + row.add(unique); // NON_UNIQUE + row.add(null); // INDEX_QUALIFIER + row.add("PRIMARY"); // INDEX_NAME + row.add(3); // TYPE (3 - Clustered index) + row.add(1); // ORDINAL_POSITION (обычно 1 для первичных ключей) + row.add(columnName); // COLUMN_NAME + row.add(null); // ASC_OR_DESC + row.add(0); // CARDINALITY (Можно указать как 0, если не известно) + row.add(0); // PAGES (Можно указать как 0, если не известно) + row.add(null); // FILTER_CONDITION + + rows.add(row); + } + return asMetadataResultSet(INDEX_INFO, rows); + } + + @Override + public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) + throws SQLException { + log("getUDTs"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.UDTS); + } + + @Override + public boolean supportsResultSetType(int type) throws SQLException { + log("supportsResultSetType"); + return type == ResultSet.TYPE_FORWARD_ONLY || + type == ResultSet.TYPE_SCROLL_INSENSITIVE; + } + + @Override + public boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException { + log("supportsResultSetConcurrency"); + return supportsResultSetType(type) && concurrency == ResultSet.CONCUR_READ_ONLY; + } + + @Override + public boolean ownUpdatesAreVisible(int type) throws SQLException { + log("ownUpdatesAreVisible"); + return false; + } + + @Override + public boolean ownDeletesAreVisible(int type) throws SQLException { + log("ownDeletesAreVisible"); + return false; + } + + @Override + public boolean ownInsertsAreVisible(int type) throws SQLException { + log("ownInsertsAreVisible"); + return false; + } + + @Override + public boolean othersUpdatesAreVisible(int type) throws SQLException { + log("othersUpdatesAreVisible"); + return false; + } + + @Override + public boolean othersDeletesAreVisible(int type) throws SQLException { + log("othersDeletesAreVisible"); + return false; + } + + @Override + public boolean othersInsertsAreVisible(int type) throws SQLException { + log("othersInsertsAreVisible"); + return false; + } + + @Override + public boolean updatesAreDetected(int type) throws SQLException { + log("updatesAreDetected"); + return false; + } + + @Override + public boolean deletesAreDetected(int type) throws SQLException { + log("deletesAreDetected"); + return false; + } + + @Override + public boolean insertsAreDetected(int type) throws SQLException { + log("insertsAreDetected"); + return false; + } + + @Override + public boolean supportsBatchUpdates() throws SQLException { + log("supportsBatchUpdates"); + return true; + } + + @Override + public ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) throws SQLException { + log("getSuperTypes"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.SUPER_TYPES); + } + + @Override + public Connection getConnection() throws SQLException { + log("getConnection"); + return connection; + } + + @Override + public boolean supportsSavepoints() throws SQLException { + log("supportsSavepoints"); + return false; + } + + @Override + public boolean supportsNamedParameters() throws SQLException { + log("supportsNamedParameters"); + return true; + } + + @Override + public boolean supportsMultipleOpenResults() throws SQLException { + log("supportsMultipleOpenResults"); + return false; + } + + @Override + public boolean supportsGetGeneratedKeys() throws SQLException { + log("supportsGetGeneratedKeys"); + return true; + } + + @Override + public ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { + log("getSuperTables"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.SUPER_TABLES); + } + + @Override + public ResultSet getAttributes(String catalog, + String schemaPattern, + String typeNamePattern, + String attributeNamePattern) throws SQLException { + log("getAttributes"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.ATTRIBUTES); + } + + @Override + public ResultSet getClientInfoProperties() throws SQLException { + log("getClientInfoProperties"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.CLIENT_INFO_PROPERTIES); + } + + @Override + public boolean supportsResultSetHoldability(int holdability) throws SQLException { + log("supportsResultSetHoldability"); + return holdability == ResultSet.HOLD_CURSORS_OVER_COMMIT; + } + + @Override + public int getResultSetHoldability() throws SQLException { + log("getResultSetHoldability"); + return ResultSet.HOLD_CURSORS_OVER_COMMIT; + } + + @Override + public int getDatabaseMajorVersion() throws SQLException { + log("getDatabaseMajorVersion"); + return 0; + } + + @Override + public int getDatabaseMinorVersion() throws SQLException { + log("getDatabaseMinorVersion"); + return 0; + } + + @Override + public int getJDBCMajorVersion() throws SQLException { + log("getJDBCMajorVersion"); + return 2; + } + + @Override + public int getJDBCMinorVersion() throws SQLException { + log("getJDBCMinorVersion"); + return 1; + } + + @Override + public int getSQLStateType() throws SQLException { + log("getSQLStateType"); + return DatabaseMetaData.sqlStateSQL; + } + + @Override + public boolean locatorsUpdateCopy() throws SQLException { + log("locatorsUpdateCopy"); + return false; + } + + @Override + public boolean supportsStatementPooling() throws SQLException { + log("supportsStatementPooling"); + return false; + } + + @Override + public RowIdLifetime getRowIdLifetime() throws SQLException { + log("getRowIdLifetime"); + return RowIdLifetime.ROWID_UNSUPPORTED; + } + + @Override + public boolean supportsStoredFunctionsUsingCallSyntax() throws SQLException { + log("supportsStoredFunctionsUsingCallSyntax"); + return false; + } + + @Override + public boolean autoCommitFailureClosesAllResultSets() throws SQLException { + log("autoCommitFailureClosesAllResultSets"); + return false; + } + + @Override + public ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) + throws SQLException { + log("getFunctions"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.FUNCTIONS); + } + + @Override + public ResultSet getFunctionColumns(String catalog, + String schemaPattern, + String functionNamePattern, + String columnNamePattern) + throws SQLException { + log("getFunctionColumns"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.FUNCTION_COLUMNS); + } + + @Override + public ResultSet getPseudoColumns(String catalog, + String schemaPattern, + String tableNamePattern, + String columnNamePattern) + throws SQLException { + log("getPseudoColumns"); + return asEmptyMetadataResultSet(DatabaseMetadataTable.PSEUDO_COLUMNS); + } + + @Override + public boolean generatedKeyAlwaysReturned() throws SQLException { + log("generatedKeyAlwaysReturned"); + return true; + } + + @Override + public T unwrap(Class type) throws SQLException { + log("unwrap"); + if (isWrapperFor(type)) { + return type.cast(this); + } + throw new SQLNonTransientException("SQLDatabaseMetadata does not wrap " + type.getName()); + } + + @Override + public boolean isWrapperFor(Class type) throws SQLException { + log("isWrapperFor"); + return type.isAssignableFrom(this.getClass()); + } + + private TarantoolStatement createMetadataStatement() throws SQLException { + log("createMetadataStatement"); + return connection.createStatement().unwrap(TarantoolStatement.class); + } + + private static T ensureType(Class cls, Object v) throws Exception { + log("ensureType"); + if (v == null || !cls.isAssignableFrom(v.getClass())) { + throw new Exception(String.format("Wrong value type '%s', expected '%s'.", + v == null ? "null" : v.getClass().getName(), cls.getName())); + } + return cls.cast(v); + } + + private static T checkType(Class cls, Object v) { + log("checkType"); + return (v != null && cls.isAssignableFrom(v.getClass())) ? cls.cast(v) : null; + } + + private ResultSet asMetadataResultSet(List> meta, List> rows) + throws SQLException { + log("asMetadataResultSet"); + List sqlMeta = meta.stream() + .map(tuple -> new SqlProtoUtils.SQLMetaData(tuple.getFirst(), tuple.getSecond())) + .collect(Collectors.toList()); + SQLResultHolder holder = SQLResultHolder.ofQuery(sqlMeta, rows); + return createMetadataStatement().executeMetadata(holder); + } + + private ResultSet asEmptyMetadataResultSet(List> meta) throws SQLException { + log("asEmptyMetadataResultSet"); + return asMetadataResultSet(meta, Collections.emptyList()); + } } diff --git a/src/main/java/org/tarantool/jdbc/SQLDriver.java b/src/main/java/org/tarantool/jdbc/SQLDriver.java index dbc89922..32495081 100644 --- a/src/main/java/org/tarantool/jdbc/SQLDriver.java +++ b/src/main/java/org/tarantool/jdbc/SQLDriver.java @@ -1,15 +1,19 @@ package org.tarantool.jdbc; import org.tarantool.SocketChannelProvider; -import org.tarantool.TarantoolClientConfig; +import org.tarantool.TarantoolClusterClientConfig; +import org.tarantool.util.NodeSpec; import org.tarantool.util.SQLStates; -import java.net.URI; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; import java.sql.Connection; import java.sql.Driver; import java.sql.DriverPropertyInfo; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; @@ -17,6 +21,8 @@ public class SQLDriver implements Driver { + private static final String TARANTOOL_JDBC_SCHEME = "jdbc:tarantool:"; + static { try { java.sql.DriverManager.registerDriver(new SQLDriver()); @@ -29,84 +35,160 @@ public class SQLDriver implements Driver { @Override public Connection connect(String url, Properties info) throws SQLException { - if (url == null) { - throw new SQLException("Url must not be null"); - } if (!acceptsURL(url)) { return null; } - final URI uri = URI.create(url); - final Properties urlProperties = parseQueryString(uri, info); - String providerClassName = SQLProperty.SOCKET_CHANNEL_PROVIDER.getString(urlProperties); + final Properties urlProperties = parseConnectionString(url, info); - if (providerClassName == null) { - return new SQLConnection(url, urlProperties); + String[] hosts = SQLProperty.HOST.getString(urlProperties).split(","); + String[] ports = SQLProperty.PORT.getString(urlProperties).split(","); + List nodes = new ArrayList<>(hosts.length); + for (int i = 0; i < hosts.length; i++) { + nodes.add(new NodeSpec(hosts[i], Integer.valueOf(ports[i]))); } + String providerClassName = SQLProperty.SOCKET_CHANNEL_PROVIDER.getString(urlProperties); + if (providerClassName == null) { + return new SQLConnection(url, nodes, urlProperties); + } final SocketChannelProvider provider = getSocketProviderInstance(providerClassName); - - return new SQLConnection(url, urlProperties) { + return new SQLConnection(url, nodes, urlProperties) { @Override - protected SQLTarantoolClientImpl makeSqlClient(String address, TarantoolClientConfig config) { + protected SQLTarantoolClientImpl makeSqlClient(List addresses, + TarantoolClusterClientConfig config) { return new SQLTarantoolClientImpl(provider, config); } }; } - protected Properties parseQueryString(URI uri, Properties info) throws SQLException { + /** + * Parses an URL and to parameters and merges + * they with the parameters provided. If same + * parameter specified in both URL and properties + * the + * + *

+ * jdbc:tarantool://[user-info@][nodes][?parameters] + * user-info ::= user[:password] + * nodes ::= host1[:port1][,host2[:port2] ... ] + * parameters ::= param1=value1[¶m2=value2 ... ] + * + * @param url target URL string + * @param info extra properties to be merged + * + * @return merged properties + * + * @throws SQLException if any invalid parameters are passed + */ + protected Properties parseConnectionString(String url, Properties info) throws SQLException { Properties urlProperties = new Properties(); - // get scheme specific part (after the scheme part "jdbc:") - // to correct parse remaining URL - uri = URI.create(uri.getSchemeSpecificPart()); - String userInfo = uri.getUserInfo(); - if (userInfo != null) { - // Get user and password from the corresponding part of the URI, i.e. before @ sign. - int i = userInfo.indexOf(':'); - if (i < 0) { - SQLProperty.USER.setString(urlProperties, userInfo); - } else { - SQLProperty.USER.setString(urlProperties, userInfo.substring(0, i)); - SQLProperty.PASSWORD.setString(urlProperties, userInfo.substring(i + 1)); - } + String schemeSpecificPart = url.substring(TARANTOOL_JDBC_SCHEME.length()); + if (!schemeSpecificPart.startsWith("//")) { + throw new SQLException("Invalid URL: '//' is not presented."); } - if (uri.getQuery() != null) { - String[] parts = uri.getQuery().split("&"); - for (String part : parts) { - int i = part.indexOf("="); - if (i > -1) { - urlProperties.put(part.substring(0, i), part.substring(i + 1)); - } else { - urlProperties.put(part, ""); - } - } - } - if (uri.getHost() != null) { - // Default values are pre-put above. - urlProperties.setProperty(SQLProperty.HOST.getName(), uri.getHost()); + int userInfoEndPosition = schemeSpecificPart.indexOf('@'); + int queryStartPosition = schemeSpecificPart.indexOf('?'); + + if (userInfoEndPosition != -1) { + parseUserInfo(schemeSpecificPart.substring(2, userInfoEndPosition), urlProperties); } - if (uri.getPort() >= 0) { - // We need to convert port to string otherwise getProperty() will not see it. - urlProperties.setProperty(SQLProperty.PORT.getName(), String.valueOf(uri.getPort())); + if (queryStartPosition != -1) { + parseQueryParameters(schemeSpecificPart.substring(queryStartPosition + 1), urlProperties); } + String nodesPart = schemeSpecificPart.substring( + userInfoEndPosition == -1 ? 2 : userInfoEndPosition + 1, + queryStartPosition == -1 ? schemeSpecificPart.length() : queryStartPosition + ); + parseNodes(nodesPart, urlProperties); + if (info != null) { urlProperties.putAll(info); } - // Validate properties. - int port = SQLProperty.PORT.getInt(urlProperties); - if (port <= 0 || port > 65535) { - throw new SQLException("Port is out of range: " + port, SQLStates.INVALID_PARAMETER_VALUE.getSqlState()); + requirePortNumber(SQLProperty.PORT, urlProperties); + requireNonNegativeInteger(SQLProperty.LOGIN_TIMEOUT, urlProperties); + requireNonNegativeInteger(SQLProperty.QUERY_TIMEOUT, urlProperties); + requireNonNegativeInteger(SQLProperty.CLUSTER_DISCOVERY_DELAY_MILLIS, urlProperties); + + return urlProperties; + } + + private void parseUserInfo(String userInfo, Properties properties) { + int i = userInfo.indexOf(':'); + if (i < 0) { + SQLProperty.USER.setString(properties, userInfo); + } else { + SQLProperty.USER.setString(properties, userInfo.substring(0, i)); + SQLProperty.PASSWORD.setString(properties, userInfo.substring(i + 1)); + } + } + + private void parseNodes(String nodesPart, Properties properties) throws SQLException { + String[] nodes = nodesPart.split(","); + StringBuilder hosts = new StringBuilder(); + StringBuilder ports = new StringBuilder(); + for (String node : nodes) { + int portIndex = node.lastIndexOf(':'); + if (portIndex != -1 && node.lastIndexOf(']') < portIndex) { + String portString = node.substring(portIndex + 1); + hosts.append(node, 0, portIndex); + ports.append(portString); + } else { + hosts.append(node); + ports.append(SQLProperty.PORT.getDefaultValue()); + } + hosts.append(','); + ports.append(','); } - checkTimeout(SQLProperty.LOGIN_TIMEOUT, urlProperties); - checkTimeout(SQLProperty.QUERY_TIMEOUT, urlProperties); + hosts.setLength(hosts.length() - 1); + ports.setLength(ports.length() - 1); + SQLProperty.HOST.setString(properties, hosts.toString()); + SQLProperty.PORT.setString(properties, ports.toString()); + } - return urlProperties; + private void parseQueryParameters(String queryParams, Properties properties) throws SQLException { + String[] parts = queryParams.split("&"); + for (String part : parts) { + int i = part.indexOf("="); + if (i > -1) { + String name = part.substring(0, i); + String value = null; + try { + value = URLDecoder.decode(part.substring(i + 1), "UTF-8"); + } catch (UnsupportedEncodingException cause) { + throw new SQLException(cause.getMessage(), SQLStates.INVALID_PARAMETER_VALUE.getSqlState(), cause); + } + properties.put(name, value); + } else { + properties.put(part, ""); + } + } + } + + private void requirePortNumber(SQLProperty sqlProperty, Properties properties) throws SQLException { + String[] portList = sqlProperty.getString(properties).split(","); + for (String portString : portList) { + try { + int port = Integer.parseInt(portString); + if (port < 1 || port > 65535) { + throw new SQLException( + "Port is out of range: " + port, SQLStates.INVALID_PARAMETER_VALUE.getSqlState() + ); + } + } catch (NumberFormatException cause) { + throw new SQLException( + "Property " + sqlProperty.getName() + " must be a valid number.", + SQLStates.INVALID_PARAMETER_VALUE.getSqlState(), + cause + ); + } + } } - private void checkTimeout(SQLProperty sqlProperty, Properties properties) throws SQLException { + private void requireNonNegativeInteger(SQLProperty sqlProperty, Properties properties) throws SQLException { int timeout = sqlProperty.getInt(properties); if (timeout < 0) { throw new SQLException( @@ -143,30 +225,27 @@ protected SocketChannelProvider getSocketProviderInstance(String className) thro @Override public boolean acceptsURL(String url) throws SQLException { - return url.toLowerCase().startsWith("jdbc:tarantool:"); + if (url == null) { + throw new SQLException("Url must not be null"); + } + return url.startsWith(TARANTOOL_JDBC_SCHEME); } @Override public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { - try { - URI uri = new URI(url); - Properties properties = parseQueryString(uri, info); - - SQLProperty[] sqlProperties = SQLProperty.values(); - DriverPropertyInfo[] propertyInfoList = new DriverPropertyInfo[sqlProperties.length]; - for (int i = 0; i < sqlProperties.length; i++) { - SQLProperty sqlProperty = sqlProperties[i]; - String value = sqlProperty.getString(properties); - DriverPropertyInfo propertyInfo = new DriverPropertyInfo(sqlProperty.getName(), value); - propertyInfo.required = sqlProperty.isRequired(); - propertyInfo.description = sqlProperty.getDescription(); - propertyInfo.choices = sqlProperty.getChoices(); - propertyInfoList[i] = propertyInfo; - } - return propertyInfoList; - } catch (Exception e) { - throw new SQLException(e); + Properties properties = parseConnectionString(url, info); + SQLProperty[] sqlProperties = SQLProperty.values(); + DriverPropertyInfo[] propertyInfoList = new DriverPropertyInfo[sqlProperties.length]; + for (int i = 0; i < sqlProperties.length; i++) { + SQLProperty sqlProperty = sqlProperties[i]; + String value = sqlProperty.getString(properties); + DriverPropertyInfo propertyInfo = new DriverPropertyInfo(sqlProperty.getName(), value); + propertyInfo.required = sqlProperty.isRequired(); + propertyInfo.description = sqlProperty.getDescription(); + propertyInfo.choices = sqlProperty.getChoices(); + propertyInfoList[i] = propertyInfo; } + return propertyInfoList; } @Override @@ -217,4 +296,4 @@ protected static String diagProperties(Properties props) { return sb.toString(); } -} +} \ No newline at end of file diff --git a/src/main/java/org/tarantool/jdbc/SQLMsgPackLite.java b/src/main/java/org/tarantool/jdbc/SQLMsgPackLite.java index 36fe82d8..befe89f6 100644 --- a/src/main/java/org/tarantool/jdbc/SQLMsgPackLite.java +++ b/src/main/java/org/tarantool/jdbc/SQLMsgPackLite.java @@ -1,30 +1,29 @@ package org.tarantool.jdbc; -import org.tarantool.MsgPackLite; - import java.io.IOException; import java.io.OutputStream; import java.math.BigDecimal; import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; +import org.tarantool.MsgPackLite; public class SQLMsgPackLite extends MsgPackLite { - public static final SQLMsgPackLite INSTANCE = new SQLMsgPackLite(); + public static final SQLMsgPackLite INSTANCE = new SQLMsgPackLite(); - @Override - public void pack(Object item, OutputStream os) throws IOException { - if (item instanceof Date) { - super.pack(((Date) item).getTime(), os); - } else if (item instanceof Time) { - super.pack(((Time) item).getTime(), os); - } else if (item instanceof Timestamp) { - super.pack(((Timestamp) item).getTime(), os); - } else if (item instanceof BigDecimal) { - super.pack(((BigDecimal) item).toPlainString(), os); - } else { - super.pack(item, os); - } + @Override + public void pack(Object item, OutputStream os) throws IOException { + if (item instanceof Date) { + super.pack(((Date) item).getTime(), os); + } else if (item instanceof Time) { + super.pack(((Time) item).getTime(), os); + } else if (item instanceof Timestamp) { + super.pack(((Timestamp) item).getTime(), os); + } else if (item instanceof BigDecimal) { + super.pack(((BigDecimal) item).toPlainString(), os); + } else { + super.pack(item, os); } + } } diff --git a/src/main/java/org/tarantool/jdbc/SQLProperty.java b/src/main/java/org/tarantool/jdbc/SQLProperty.java index b147c221..ae1ce622 100644 --- a/src/main/java/org/tarantool/jdbc/SQLProperty.java +++ b/src/main/java/org/tarantool/jdbc/SQLProperty.java @@ -8,21 +8,21 @@ public enum SQLProperty { HOST( "host", - "Tarantool server host", + "Tarantool server host (may be specified in the URL directly)", "localhost", null, true ), PORT( "port", - "Tarantool server port", + "Tarantool server port (may be specified in the URL directly)", "3301", null, true ), SOCKET_CHANNEL_PROVIDER( "socketChannelProvider", - "SocketProvider class implements org.tarantool.SocketChannelProvider", + "SocketProvider class that implements org.tarantool.SocketChannelProvider", null, null, false @@ -56,6 +56,22 @@ public enum SQLProperty { "0", null, false + ), + CLUSTER_DISCOVERY_ENTRY_FUNCTION( + "clusterDiscoveryEntryFunction", + "The name of the stored function to be used to fetch list of Tarantool instances." + + "It's unspecified by default.", + null, + null, + false + ), + CLUSTER_DISCOVERY_DELAY_MILLIS( + "clusterDiscoveryDelayMillis", + "A java.util.concurrent.Executor implementation that will be used to retry sending queries " + + "during a reconnaction. Default value is not specified.", + "60000", + null, + false ); private final String name; @@ -124,4 +140,4 @@ public int getInt(Properties properties) throws SQLException { public void setInt(Properties properties, int value) { setString(properties, Integer.toString(value)); } -} +} \ No newline at end of file diff --git a/src/main/java/org/tarantool/jdbc/SQLResultSet.java b/src/main/java/org/tarantool/jdbc/SQLResultSet.java index 232ee9ec..8cc44cd1 100644 --- a/src/main/java/org/tarantool/jdbc/SQLResultSet.java +++ b/src/main/java/org/tarantool/jdbc/SQLResultSet.java @@ -1,10 +1,5 @@ package org.tarantool.jdbc; -import org.tarantool.jdbc.cursor.CursorIterator; -import org.tarantool.jdbc.cursor.InMemoryForwardCursorIteratorImpl; -import org.tarantool.jdbc.cursor.InMemoryScrollableCursorIteratorImpl; -import org.tarantool.util.SQLStates; - import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.Reader; @@ -35,1142 +30,1163 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import org.tarantool.jdbc.cursor.CursorIterator; +import org.tarantool.jdbc.cursor.InMemoryForwardCursorIteratorImpl; +import org.tarantool.jdbc.cursor.InMemoryScrollableCursorIteratorImpl; +import org.tarantool.util.SQLStates; +import org.tarantool.utils.LocalLogger; public class SQLResultSet implements ResultSet { - private CursorIterator> iterator; - private TarantoolResultSetMetaData metaData; - - private Map columnByNameLookups; - private boolean lastColumnWasNull; - - private final TarantoolStatement statement; - private final int maxRows; - private final int maxFieldSize; - - private AtomicBoolean isClosed = new AtomicBoolean(false); - - private final int scrollType; - private final int concurrencyLevel; - private final int holdability; - - public SQLResultSet(SQLResultHolder holder, TarantoolStatement ownerStatement) throws SQLException { - metaData = new SQLResultSetMetaData(holder.getSqlMetadata(), ownerStatement.getConnection().isReadOnly()); - statement = ownerStatement; - scrollType = statement.getResultSetType(); - concurrencyLevel = statement.getResultSetConcurrency(); - holdability = statement.getResultSetHoldability(); - maxRows = statement.getMaxRows(); - maxFieldSize = statement.getMaxFieldSize(); - - List> fetchedRows = holder.getRows(); - List> rows = maxRows == 0 || maxRows >= fetchedRows.size() - ? fetchedRows - : fetchedRows.subList(0, maxRows); - - switch (scrollType) { - case ResultSet.TYPE_FORWARD_ONLY: - iterator = new InMemoryForwardCursorIteratorImpl(rows); - break; - case ResultSet.TYPE_SCROLL_INSENSITIVE: - iterator = new InMemoryScrollableCursorIteratorImpl(rows); - break; - default: - throw new SQLNonTransientException("", SQLStates.INVALID_PARAMETER_VALUE.getSqlState()); - } - } - - public int getMaxRows() { - return maxRows; - } - - public List getCurrentRow() throws SQLException { - checkNotClosed(); - return iterator.getItem(); - } - - protected Object getRaw(int columnIndex) throws SQLException { - checkNotClosed(); - metaData.checkColumnIndex(columnIndex); - List row = getCurrentRow(); - Object value = row.get(columnIndex - 1); - lastColumnWasNull = (value == null); - return value; - } - - protected Number getNumber(int columnIndex) throws SQLException { - Number raw = (Number) getRaw(columnIndex); - return raw == null ? 0 : raw; - } - - protected Number getNullableNumber(int columnIndex) throws SQLException { - return (Number) getRaw(columnIndex); - } - - @Override - public void close() throws SQLException { - if (isClosed.compareAndSet(false, true)) { - try { - iterator.close(); - iterator = null; - metaData = null; - } finally { - statement.checkCompletion(); - } - } - } - - @Override - public boolean wasNull() throws SQLException { - checkNotClosed(); - return lastColumnWasNull; - } - - @Override - public String getString(int columnIndex) throws SQLException { - Object raw = getRaw(columnIndex); - if (raw == null) { - return null; - } - String value = String.valueOf(raw); - return (maxFieldSize > 0 && value.length() > maxFieldSize && metaData.isTrimmable(columnIndex)) - ? value.substring(0, maxFieldSize) - : value; - } - - @Override - public String getString(String columnLabel) throws SQLException { - return getString(findColumn(columnLabel)); - } - - @Override - public boolean getBoolean(int columnIndex) throws SQLException { - return Boolean.TRUE.equals(getRaw(columnIndex)); - } - - @Override - public boolean getBoolean(String columnLabel) throws SQLException { - return getBoolean(findColumn(columnLabel)); - } - - @Override - public byte getByte(int columnIndex) throws SQLException { - return (getNumber(columnIndex)).byteValue(); - } - - @Override - public byte getByte(String columnLabel) throws SQLException { - return getByte(findColumn(columnLabel)); - } - - @Override - public short getShort(int columnIndex) throws SQLException { - return (getNumber(columnIndex)).shortValue(); - } - - @Override - public short getShort(String columnLabel) throws SQLException { - return getShort(findColumn(columnLabel)); - } - - @Override - public int getInt(int columnIndex) throws SQLException { - return getNumber(columnIndex).intValue(); - } - - @Override - public int getInt(String columnLabel) throws SQLException { - return getInt(findColumn(columnLabel)); - } - - @Override - public long getLong(int columnIndex) throws SQLException { - return (getNumber(columnIndex)).longValue(); - } - - @Override - public long getLong(String columnLabel) throws SQLException { - return getLong(findColumn(columnLabel)); - } - - @Override - public float getFloat(int columnIndex) throws SQLException { - return (getNumber(columnIndex)).floatValue(); - } - - @Override - public float getFloat(String columnLabel) throws SQLException { - return getFloat(findColumn(columnLabel)); - } - - @Override - public double getDouble(int columnIndex) throws SQLException { - return (getNumber(columnIndex)).doubleValue(); - } - - @Override - public double getDouble(String columnLabel) throws SQLException { - return getDouble(findColumn(columnLabel)); - } - - @Override - public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { - String raw = getString(columnIndex); - if (raw == null) { - return null; - } - BigDecimal bigDecimal = new BigDecimal(raw); - return scale > -1 ? bigDecimal.setScale(scale) : bigDecimal; - } - - @Override - public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { - return getBigDecimal(findColumn(columnLabel), scale); - } - - @Override - public BigDecimal getBigDecimal(int columnIndex) throws SQLException { - return getBigDecimal(columnIndex, -1); - } - - @Override - public BigDecimal getBigDecimal(String columnLabel) throws SQLException { - return getBigDecimal(columnLabel, -1); - } - - @Override - public byte[] getBytes(int columnIndex) throws SQLException { - Object raw = getRaw(columnIndex); - if (raw == null) { - return null; - } - byte[] bytes = (byte[]) raw; - if (maxFieldSize > 0 && bytes.length > maxFieldSize && metaData.isTrimmable(columnIndex)) { - byte[] trimmedBytes = new byte[maxFieldSize]; - System.arraycopy(bytes, 0, trimmedBytes, 0, maxFieldSize); - return trimmedBytes; - } - return bytes; - } - - @Override - public byte[] getBytes(String columnLabel) throws SQLException { - return getBytes(findColumn(columnLabel)); - } - - @Override - public Date getDate(int columnIndex) throws SQLException { - Number time = getNullableNumber(columnIndex); - return time == null ? null : new java.sql.Date(time.longValue()); - } - - @Override - public Date getDate(String columnLabel) throws SQLException { - return getDate(findColumn(columnLabel)); - } - - @Override - public Date getDate(int columnIndex, Calendar cal) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Date getDate(String columnLabel, Calendar cal) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Time getTime(int columnIndex) throws SQLException { - Number time = getNullableNumber(columnIndex); - return time == null ? null : new java.sql.Time(time.longValue()); - } - - @Override - public Time getTime(String columnLabel) throws SQLException { - return getTime(findColumn(columnLabel)); - } - - @Override - public Time getTime(int columnIndex, Calendar cal) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Time getTime(String columnLabel, Calendar cal) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Timestamp getTimestamp(int columnIndex) throws SQLException { - Number time = getNullableNumber(columnIndex); - return time == null ? null : new java.sql.Timestamp(time.longValue()); - } - - @Override - public Timestamp getTimestamp(String columnLabel) throws SQLException { - return getTimestamp(findColumn(columnLabel)); - } - - @Override - public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - - @Override - public InputStream getAsciiStream(int columnIndex) throws SQLException { - String string = getString(columnIndex); - return string == null ? null : new ByteArrayInputStream(string.getBytes(Charset.forName("ASCII"))); - } - - @Override - public InputStream getAsciiStream(String columnLabel) throws SQLException { - return getAsciiStream(findColumn(columnLabel)); - } - - @Override - public InputStream getUnicodeStream(int columnIndex) throws SQLException { - String string = getString(columnIndex); - return string == null ? null : new ByteArrayInputStream(string.getBytes(Charset.forName("UTF-8"))); - } - - @Override - public InputStream getUnicodeStream(String columnLabel) throws SQLException { - return getUnicodeStream(findColumn(columnLabel)); - } - - @Override - public InputStream getBinaryStream(int columnIndex) throws SQLException { - byte[] bytes = getBytes(columnIndex); - return bytes == null ? null : new ByteArrayInputStream(bytes); - } - - @Override - public InputStream getBinaryStream(String columnLabel) throws SQLException { - return getBinaryStream(findColumn(columnLabel)); - } - - @Override - public Reader getCharacterStream(int columnIndex) throws SQLException { - String value = getString(columnIndex); - return value == null ? null : new StringReader(value); - } - - @Override - public Reader getCharacterStream(String columnLabel) throws SQLException { - return getCharacterStream(findColumn(columnLabel)); - } - - @Override - public Object getObject(int columnIndex) throws SQLException { - return getRaw(columnIndex); - } - - @Override - public Object getObject(String columnLabel) throws SQLException { - return getObject(findColumn(columnLabel)); - } - - @Override - public Object getObject(int columnIndex, Map> map) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Object getObject(String columnLabel, Map> map) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public T getObject(int columnIndex, Class type) throws SQLException { - try { - return type.cast(getRaw(columnIndex)); - } catch (Exception e) { - throw new SQLNonTransientException(e); - } - } - - @Override - public T getObject(String columnLabel, Class type) throws SQLException { - return getObject(findColumn(columnLabel), type); - } - - @Override - public SQLWarning getWarnings() throws SQLException { - return null; - } - - @Override - public void clearWarnings() throws SQLException { - - } - - @Override - public String getCursorName() throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public ResultSetMetaData getMetaData() throws SQLException { - checkNotClosed(); - return metaData; - } - - @Override - public int findColumn(String columnLabel) throws SQLException { - checkNotClosed(); - return findColumnIndex(columnLabel); - } - - protected int findColumnIndex(String columnLabel) throws SQLException { - if (columnByNameLookups == null) { - columnByNameLookups = new LinkedHashMap<>(); - // Spec quote: Column labels supplied to getter methods are case insensitive. - // If a select list contains the same column more than once, the first instance - // of the column will be returned. - for (int i = metaData.getColumnCount(); i > 0; i--) { - columnByNameLookups.put(metaData.getColumnLabel(i).toUpperCase(), i); - } - } - return columnByNameLookups.getOrDefault(columnLabel.toUpperCase(), 0); - } - - //region Cursor movement API - - @Override - public boolean next() throws SQLException { - checkNotClosed(); - return iterator.next(); - } - - @Override - public boolean isBeforeFirst() throws SQLException { - checkNotClosed(); - return iterator.isBeforeFirst(); - } - - @Override - public boolean isAfterLast() throws SQLException { - checkNotClosed(); - return iterator.isAfterLast(); - } - - @Override - public boolean isFirst() throws SQLException { - checkNotClosed(); - return iterator.isFirst(); - } - - @Override - public boolean isLast() throws SQLException { - checkNotClosed(); - return iterator.isLast(); - } - - @Override - public void beforeFirst() throws SQLException { - checkNotClosed(); - iterator.beforeFirst(); - } - - @Override - public void afterLast() throws SQLException { - checkNotClosed(); - iterator.afterLast(); - } - - @Override - public boolean first() throws SQLException { - checkNotClosed(); - return iterator.first(); - } - - @Override - public boolean last() throws SQLException { - checkNotClosed(); - return iterator.last(); - } - - @Override - public boolean absolute(int row) throws SQLException { - checkNotClosed(); - return iterator.absolute(row); - } - - @Override - public boolean relative(int rows) throws SQLException { - checkNotClosed(); - return iterator.relative(rows); - } - - @Override - public boolean previous() throws SQLException { - checkNotClosed(); - return iterator.previous(); - } - - @Override - public int getRow() throws SQLException { - checkNotClosed(); - return iterator.getRow(); - } - - //endregion - - @Override - public void setFetchDirection(int direction) throws SQLException { - checkNotClosed(); - if (direction != ResultSet.FETCH_FORWARD) { - throw new SQLException("TYPE_FORWARD_ONLY"); - } - } - - @Override - public int getFetchDirection() throws SQLException { - checkNotClosed(); - return ResultSet.FETCH_FORWARD; - } - - @Override - public void setFetchSize(int rows) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public int getFetchSize() throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public int getType() throws SQLException { - checkNotClosed(); - return scrollType; - } - - @Override - public int getConcurrency() throws SQLException { - checkNotClosed(); - return concurrencyLevel; - } - - @Override - public boolean rowUpdated() throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public boolean rowInserted() throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public boolean rowDeleted() throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateNull(int columnIndex) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateNull(String columnLabel) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateBoolean(int columnIndex, boolean x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateBoolean(String columnLabel, boolean x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateByte(int columnIndex, byte x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateByte(String columnLabel, byte x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateShort(int columnIndex, short x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateShort(String columnLabel, short x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateInt(int columnIndex, int x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateInt(String columnLabel, int x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateLong(int columnIndex, long x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateLong(String columnLabel, long x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateFloat(int columnIndex, float x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateFloat(String columnLabel, float x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateDouble(int columnIndex, double x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateDouble(String columnLabel, double x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateString(int columnIndex, String x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateString(String columnLabel, String x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateBytes(int columnIndex, byte[] x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateBytes(String columnLabel, byte[] x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateDate(int columnIndex, Date x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateDate(String columnLabel, Date x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateTime(int columnIndex, Time x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateTime(String columnLabel, Time x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateObject(int columnIndex, Object x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateObject(String columnLabel, Object x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void insertRow() throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateRow() throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void deleteRow() throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void refreshRow() throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void cancelRowUpdates() throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void moveToInsertRow() throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void moveToCurrentRow() throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Statement getStatement() throws SQLException { - checkNotClosed(); - return statement; - } - - @Override - public Ref getRef(int columnIndex) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Ref getRef(String columnLabel) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Blob getBlob(int columnIndex) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Blob getBlob(String columnLabel) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Clob getClob(int columnIndex) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Clob getClob(String columnLabel) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Array getArray(int columnIndex) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Array getArray(String columnLabel) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public URL getURL(int columnIndex) throws SQLException { - try { - return new URL(getString(columnIndex)); - } catch (MalformedURLException e) { - throw new SQLException(e); - } - } - - @Override - public URL getURL(String columnLabel) throws SQLException { - try { - return new URL(getString(columnLabel)); - } catch (MalformedURLException e) { - throw new SQLException(e); - } - } - - @Override - public void updateRef(int columnIndex, Ref x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateRef(String columnLabel, Ref x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateBlob(int columnIndex, Blob x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateBlob(String columnLabel, Blob x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateClob(int columnIndex, Clob x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateClob(String columnLabel, Clob x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateClob(int columnIndex, Reader reader) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateClob(String columnLabel, Reader reader) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateArray(int columnIndex, Array x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateArray(String columnLabel, Array x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public RowId getRowId(int columnIndex) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public RowId getRowId(String columnLabel) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateRowId(int columnIndex, RowId x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateRowId(String columnLabel, RowId x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public int getHoldability() throws SQLException { - checkNotClosed(); - return holdability; - } - - @Override - public boolean isClosed() throws SQLException { - return isClosed.get() || statement.isClosed(); - } - - @Override - public void updateNString(int columnIndex, String string) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateNString(String columnLabel, String string) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateNClob(int columnIndex, NClob clob) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateNClob(String columnLabel, NClob clob) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateNClob(int columnIndex, Reader reader) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateNClob(String columnLabel, Reader reader) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public NClob getNClob(int columnIndex) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public NClob getNClob(String columnLabel) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public SQLXML getSQLXML(int columnIndex) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public SQLXML getSQLXML(String columnLabel) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public String getNString(int columnIndex) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public String getNString(String columnLabel) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Reader getNCharacterStream(int columnIndex) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Reader getNCharacterStream(String columnLabel) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public T unwrap(Class type) throws SQLException { - if (isWrapperFor(type)) { - return type.cast(this); - } - throw new SQLNonTransientException("SQLResultSet does not wrap " + type.getName()); - } - - @Override - public boolean isWrapperFor(Class type) throws SQLException { - return type.isAssignableFrom(this.getClass()); - } - - @Override - public String toString() { - return "SQLResultSet{" + - "metaData=" + metaData + - ", statement=" + statement + - ", scrollType=" + scrollType + - ", concurrencyLevel=" + concurrencyLevel + - ", holdability=" + holdability + - '}'; - } - - protected void checkNotClosed() throws SQLException { - if (isClosed()) { - throw new SQLNonTransientException("ResultSet is closed."); - } - } + private CursorIterator> iterator; + private TarantoolResultSetMetaData metaData; + + private Map columnByNameLookups; + private boolean lastColumnWasNull; + + private final TarantoolStatement statement; + private final int maxRows; + private final int maxFieldSize; + + private AtomicBoolean isClosed = new AtomicBoolean(false); + + private final int scrollType; + private final int concurrencyLevel; + private final int holdability; + + public SQLResultSet(SQLResultHolder holder, TarantoolStatement ownerStatement) throws SQLException { + metaData = new SQLResultSetMetaData(holder.getSqlMetadata(), ownerStatement.getConnection().isReadOnly()); + statement = ownerStatement; + scrollType = statement.getResultSetType(); + concurrencyLevel = statement.getResultSetConcurrency(); + holdability = statement.getResultSetHoldability(); + maxRows = statement.getMaxRows(); + maxFieldSize = statement.getMaxFieldSize(); + + List> fetchedRows = holder.getRows(); + List> rows = maxRows == 0 || maxRows >= fetchedRows.size() + ? fetchedRows + : fetchedRows.subList(0, maxRows); + + switch (scrollType) { + case ResultSet.TYPE_FORWARD_ONLY: + iterator = new InMemoryForwardCursorIteratorImpl(rows); + break; + case ResultSet.TYPE_SCROLL_INSENSITIVE: + iterator = new InMemoryScrollableCursorIteratorImpl(rows); + break; + default: + throw new SQLNonTransientException("", SQLStates.INVALID_PARAMETER_VALUE.getSqlState()); + } + } + + public int getMaxRows() { + return maxRows; + } + + public List getCurrentRow() throws SQLException { + checkNotClosed(); + return iterator.getItem(); + } + + protected Object getRaw(int columnIndex) throws SQLException { + checkNotClosed(); + metaData.checkColumnIndex(columnIndex); + List row = getCurrentRow(); + Object value = row.get(columnIndex - 1); + lastColumnWasNull = (value == null); + return value; + } + + protected Number getNumber(int columnIndex) throws SQLException { + Number raw = (Number) getRaw(columnIndex); + return raw == null ? 0 : raw; + } + + protected Number getNullableNumber(int columnIndex) throws SQLException { + return (Number) getRaw(columnIndex); + } + + @Override + public void close() throws SQLException { + if (isClosed.compareAndSet(false, true)) { + try { + iterator.close(); + iterator = null; + metaData = null; + } finally { + statement.checkCompletion(); + } + } + } + + @Override + public boolean wasNull() throws SQLException { + checkNotClosed(); + return lastColumnWasNull; + } + + @Override + public String getString(int columnIndex) throws SQLException { + Object raw = getRaw(columnIndex); + if (raw == null) { + return null; + } + String value = String.valueOf(raw); + return (maxFieldSize > 0 && value.length() > maxFieldSize && metaData.isTrimmable(columnIndex)) + ? value.substring(0, maxFieldSize) + : value; + } + + @Override + public String getString(String columnLabel) throws SQLException { + return getString(findColumn(columnLabel)); + } + + @Override + public boolean getBoolean(int columnIndex) throws SQLException { + return Boolean.TRUE.equals(getRaw(columnIndex)); + } + + @Override + public boolean getBoolean(String columnLabel) throws SQLException { + return getBoolean(findColumn(columnLabel)); + } + + @Override + public byte getByte(int columnIndex) throws SQLException { + return (getNumber(columnIndex)).byteValue(); + } + + @Override + public byte getByte(String columnLabel) throws SQLException { + return getByte(findColumn(columnLabel)); + } + + @Override + public short getShort(int columnIndex) throws SQLException { + return (getNumber(columnIndex)).shortValue(); + } + + @Override + public short getShort(String columnLabel) throws SQLException { + return getShort(findColumn(columnLabel)); + } + + @Override + public int getInt(int columnIndex) throws SQLException { + return getNumber(columnIndex).intValue(); + } + + @Override + public int getInt(String columnLabel) throws SQLException { + return getInt(findColumn(columnLabel)); + } + + @Override + public long getLong(int columnIndex) throws SQLException { + return (getNumber(columnIndex)).longValue(); + } + + @Override + public long getLong(String columnLabel) throws SQLException { + return getLong(findColumn(columnLabel)); + } + + @Override + public float getFloat(int columnIndex) throws SQLException { + return (getNumber(columnIndex)).floatValue(); + } + + @Override + public float getFloat(String columnLabel) throws SQLException { + return getFloat(findColumn(columnLabel)); + } + + @Override + public double getDouble(int columnIndex) throws SQLException { + LocalLogger.log("double getDouble(int columnIndex) called for column " + columnIndex + " in " + this.getClass().getSimpleName()); + return (getNumber(columnIndex)).doubleValue(); + } + + @Override + public double getDouble(String columnLabel) throws SQLException { + LocalLogger.log("double getDouble(String columnLabel) called for column " + columnLabel + " in " + this.getClass().getSimpleName()); + return getDouble(findColumn(columnLabel)); + } + + @Override + public BigDecimal getBigDecimal(String columnLabel) throws SQLException { + Object rawValue = getObject(columnLabel); + if (rawValue instanceof BigDecimal) { + return (BigDecimal) rawValue; + } + throw new SQLException("Column " + columnLabel + " is not of type DECIMAL"); + } + + @Override + public BigDecimal getBigDecimal(int columnIndex) throws SQLException { + Object rawValue = getObject(columnIndex); + if (rawValue instanceof BigDecimal) { + return (BigDecimal) rawValue; + } + throw new SQLException("Column index " + columnIndex + " is not of type DECIMAL"); + } + + + + + @Override + public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { + LocalLogger.log("bigdecimal getBigDecimal(int columnIndex) called for column " + columnIndex + " in " + this.getClass().getSimpleName()); + String raw = getString(columnIndex); + if (raw == null) { + return null; + } + BigDecimal bigDecimal = new BigDecimal(raw); + return scale > -1 ? bigDecimal.setScale(scale) : bigDecimal; + } + + @Override + public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { + return getBigDecimal(findColumn(columnLabel), scale); + } + + @Override + public byte[] getBytes(int columnIndex) throws SQLException { + Object raw = getRaw(columnIndex); + if (raw == null) { + return null; + } + byte[] bytes = (byte[]) raw; + if (maxFieldSize > 0 && bytes.length > maxFieldSize && metaData.isTrimmable(columnIndex)) { + byte[] trimmedBytes = new byte[maxFieldSize]; + System.arraycopy(bytes, 0, trimmedBytes, 0, maxFieldSize); + return trimmedBytes; + } + return bytes; + } + + @Override + public byte[] getBytes(String columnLabel) throws SQLException { + LocalLogger.log("bytes getBytes(String columnLabel) called for column " + columnLabel + " in " + this.getClass().getSimpleName()); + return getBytes(findColumn(columnLabel)); + } + + @Override + public Date getDate(int columnIndex) throws SQLException { + Number time = getNullableNumber(columnIndex); + return time == null ? null : new java.sql.Date(time.longValue()); + } + + @Override + public Date getDate(String columnLabel) throws SQLException { + return getDate(findColumn(columnLabel)); + } + + @Override + public Date getDate(int columnIndex, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Date getDate(String columnLabel, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Time getTime(int columnIndex) throws SQLException { + Number time = getNullableNumber(columnIndex); + return time == null ? null : new java.sql.Time(time.longValue()); + } + + @Override + public Time getTime(String columnLabel) throws SQLException { + return getTime(findColumn(columnLabel)); + } + + @Override + public Time getTime(int columnIndex, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Time getTime(String columnLabel, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Timestamp getTimestamp(int columnIndex) throws SQLException { + Number time = getNullableNumber(columnIndex); + return time == null ? null : new java.sql.Timestamp(time.longValue()); + } + + @Override + public Timestamp getTimestamp(String columnLabel) throws SQLException { + return getTimestamp(findColumn(columnLabel)); + } + + @Override + public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + + @Override + public InputStream getAsciiStream(int columnIndex) throws SQLException { + String string = getString(columnIndex); + return string == null ? null : new ByteArrayInputStream(string.getBytes(Charset.forName("ASCII"))); + } + + @Override + public InputStream getAsciiStream(String columnLabel) throws SQLException { + return getAsciiStream(findColumn(columnLabel)); + } + + @Override + public InputStream getUnicodeStream(int columnIndex) throws SQLException { + String string = getString(columnIndex); + return string == null ? null : new ByteArrayInputStream(string.getBytes(Charset.forName("UTF-8"))); + } + + @Override + public InputStream getUnicodeStream(String columnLabel) throws SQLException { + return getUnicodeStream(findColumn(columnLabel)); + } + + @Override + public InputStream getBinaryStream(int columnIndex) throws SQLException { + byte[] bytes = getBytes(columnIndex); + return bytes == null ? null : new ByteArrayInputStream(bytes); + } + + @Override + public InputStream getBinaryStream(String columnLabel) throws SQLException { + return getBinaryStream(findColumn(columnLabel)); + } + + @Override + public Reader getCharacterStream(int columnIndex) throws SQLException { + String value = getString(columnIndex); + return value == null ? null : new StringReader(value); + } + + @Override + public Reader getCharacterStream(String columnLabel) throws SQLException { + return getCharacterStream(findColumn(columnLabel)); + } + + + @Override + public Object getObject(int columnIndex) throws SQLException { + return getRaw(columnIndex); + } + + @Override + public Object getObject(String columnLabel) throws SQLException { + return getObject(findColumn(columnLabel)); + } + + @Override + public Object getObject(int columnIndex, Map> map) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Object getObject(String columnLabel, Map> map) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public T getObject(int columnIndex, Class type) throws SQLException { + try { + return type.cast(getRaw(columnIndex)); + } catch (Exception e) { + throw new SQLNonTransientException(e); + } + } + + @Override + public T getObject(String columnLabel, Class type) throws SQLException { + return getObject(findColumn(columnLabel), type); + } + + @Override + public SQLWarning getWarnings() throws SQLException { + return null; + } + + @Override + public void clearWarnings() throws SQLException { + + } + + @Override + public String getCursorName() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + checkNotClosed(); + return metaData; + } + + @Override + public int findColumn(String columnLabel) throws SQLException { + checkNotClosed(); + return findColumnIndex(columnLabel); + } + + protected int findColumnIndex(String columnLabel) throws SQLException { + if (columnByNameLookups == null) { + columnByNameLookups = new LinkedHashMap<>(); + // Spec quote: Column labels supplied to getter methods are case insensitive. + // If a select list contains the same column more than once, the first instance + // of the column will be returned. + for (int i = metaData.getColumnCount(); i > 0; i--) { + columnByNameLookups.put(metaData.getColumnLabel(i).toUpperCase(), i); + } + } + return columnByNameLookups.getOrDefault(columnLabel.toUpperCase(), 0); + } + + //region Cursor movement API + + @Override + public boolean next() throws SQLException { + checkNotClosed(); + return iterator.next(); + } + + @Override + public boolean isBeforeFirst() throws SQLException { + checkNotClosed(); + return iterator.isBeforeFirst(); + } + + @Override + public boolean isAfterLast() throws SQLException { + checkNotClosed(); + return iterator.isAfterLast(); + } + + @Override + public boolean isFirst() throws SQLException { + checkNotClosed(); + return iterator.isFirst(); + } + + @Override + public boolean isLast() throws SQLException { + checkNotClosed(); + return iterator.isLast(); + } + + @Override + public void beforeFirst() throws SQLException { + checkNotClosed(); + iterator.beforeFirst(); + } + + @Override + public void afterLast() throws SQLException { + checkNotClosed(); + iterator.afterLast(); + } + + @Override + public boolean first() throws SQLException { + checkNotClosed(); + return iterator.first(); + } + + @Override + public boolean last() throws SQLException { + checkNotClosed(); + return iterator.last(); + } + + @Override + public boolean absolute(int row) throws SQLException { + checkNotClosed(); + return iterator.absolute(row); + } + + @Override + public boolean relative(int rows) throws SQLException { + checkNotClosed(); + return iterator.relative(rows); + } + + @Override + public boolean previous() throws SQLException { + checkNotClosed(); + return iterator.previous(); + } + + @Override + public int getRow() throws SQLException { + checkNotClosed(); + return iterator.getRow(); + } + + //endregion + + @Override + public void setFetchDirection(int direction) throws SQLException { + checkNotClosed(); + if (direction != ResultSet.FETCH_FORWARD) { + throw new SQLException("TYPE_FORWARD_ONLY"); + } + } + + @Override + public int getFetchDirection() throws SQLException { + checkNotClosed(); + return ResultSet.FETCH_FORWARD; + } + + @Override + public void setFetchSize(int rows) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int getFetchSize() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int getType() throws SQLException { + checkNotClosed(); + return scrollType; + } + + @Override + public int getConcurrency() throws SQLException { + checkNotClosed(); + return concurrencyLevel; + } + + @Override + public boolean rowUpdated() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean rowInserted() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean rowDeleted() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNull(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNull(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBoolean(int columnIndex, boolean x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBoolean(String columnLabel, boolean x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateByte(int columnIndex, byte x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateByte(String columnLabel, byte x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateShort(int columnIndex, short x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateShort(String columnLabel, short x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateInt(int columnIndex, int x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateInt(String columnLabel, int x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateLong(int columnIndex, long x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateLong(String columnLabel, long x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateFloat(int columnIndex, float x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateFloat(String columnLabel, float x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateDouble(int columnIndex, double x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateDouble(String columnLabel, double x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateString(int columnIndex, String x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateString(String columnLabel, String x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBytes(int columnIndex, byte[] x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBytes(String columnLabel, byte[] x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateDate(int columnIndex, Date x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateDate(String columnLabel, Date x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateTime(int columnIndex, Time x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateTime(String columnLabel, Time x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateObject(int columnIndex, Object x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateObject(String columnLabel, Object x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void insertRow() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateRow() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void deleteRow() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void refreshRow() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void cancelRowUpdates() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void moveToInsertRow() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void moveToCurrentRow() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Statement getStatement() throws SQLException { + checkNotClosed(); + return statement; + } + + @Override + public Ref getRef(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Ref getRef(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Blob getBlob(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Blob getBlob(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Clob getClob(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Clob getClob(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Array getArray(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Array getArray(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public URL getURL(int columnIndex) throws SQLException { + try { + return new URL(getString(columnIndex)); + } catch (MalformedURLException e) { + throw new SQLException(e); + } + } + + @Override + public URL getURL(String columnLabel) throws SQLException { + try { + return new URL(getString(columnLabel)); + } catch (MalformedURLException e) { + throw new SQLException(e); + } + } + + @Override + public void updateRef(int columnIndex, Ref x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateRef(String columnLabel, Ref x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBlob(int columnIndex, Blob x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBlob(String columnLabel, Blob x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateClob(int columnIndex, Clob x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateClob(String columnLabel, Clob x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateClob(int columnIndex, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateClob(String columnLabel, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateArray(int columnIndex, Array x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateArray(String columnLabel, Array x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public RowId getRowId(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public RowId getRowId(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateRowId(int columnIndex, RowId x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateRowId(String columnLabel, RowId x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int getHoldability() throws SQLException { + checkNotClosed(); + return holdability; + } + + @Override + public boolean isClosed() throws SQLException { + return isClosed.get() || statement.isClosed(); + } + + @Override + public void updateNString(int columnIndex, String string) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNString(String columnLabel, String string) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNClob(int columnIndex, NClob clob) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNClob(String columnLabel, NClob clob) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNClob(int columnIndex, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNClob(String columnLabel, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public NClob getNClob(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public NClob getNClob(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public SQLXML getSQLXML(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public SQLXML getSQLXML(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public String getNString(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public String getNString(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Reader getNCharacterStream(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Reader getNCharacterStream(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public T unwrap(Class type) throws SQLException { + if (isWrapperFor(type)) { + return type.cast(this); + } + throw new SQLNonTransientException("SQLResultSet does not wrap " + type.getName()); + } + + @Override + public boolean isWrapperFor(Class type) throws SQLException { + return type.isAssignableFrom(this.getClass()); + } + + @Override + public String toString() { + return "SQLResultSet{" + + "metaData=" + metaData + + ", statement=" + statement + + ", scrollType=" + scrollType + + ", concurrencyLevel=" + concurrencyLevel + + ", holdability=" + holdability + + '}'; + } + + protected void checkNotClosed() throws SQLException { + if (isClosed()) { + throw new SQLNonTransientException("ResultSet is closed."); + } + } } diff --git a/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java b/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java index a248bffa..a6048b22 100644 --- a/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java +++ b/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java @@ -1,206 +1,202 @@ package org.tarantool.jdbc; -import org.tarantool.SqlProtoUtils; -import org.tarantool.util.SQLStates; - import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.SQLNonTransientException; import java.util.List; +import org.tarantool.SqlProtoUtils; +import org.tarantool.util.SQLStates; public class SQLResultSetMetaData implements TarantoolResultSetMetaData { - private final List sqlMetadata; - private final boolean readOnly; - - public SQLResultSetMetaData(List sqlMetaData, boolean readOnly) { - this.sqlMetadata = sqlMetaData; - this.readOnly = readOnly; - } - - @Override - public int getColumnCount() throws SQLException { - return sqlMetadata.size(); - } - - @Override - public boolean isAutoIncrement(int column) throws SQLException { - checkColumnIndex(column); - // XXX: extra flag or, at least table ID is required in meta - // to be able to fetch an the flag indirectly. - return false; - } - - @Override - public boolean isCaseSensitive(int column) throws SQLException { - checkColumnIndex(column); - return sqlMetadata.get(column - 1).getType().isCaseSensitive(); - } - - /** - * {@inheritDoc} - *

- * All the types can be used in {@literal WHERE} clause. - */ - @Override - public boolean isSearchable(int column) throws SQLException { - checkColumnIndex(column); - return true; - } - - /** - * {@inheritDoc} - *

- * Always {@literal false} because - * Tarantool does not have monetary types. - */ - @Override - public boolean isCurrency(int column) throws SQLException { - checkColumnIndex(column); - return false; - } - - @Override - public int isNullable(int column) throws SQLException { - checkColumnIndex(column); - // XXX: extra nullability flag or, at least table ID is required in meta - // to be able to fetch an the flag indirectly. - return ResultSetMetaData.columnNullableUnknown; - } - - @Override - public boolean isSigned(int column) throws SQLException { - checkColumnIndex(column); - return sqlMetadata.get(column - 1).getType().isSigned(); - } - - @Override - public int getColumnDisplaySize(int column) throws SQLException { - checkColumnIndex(column); - return sqlMetadata.get(column - 1).getType().getDisplaySize(); - } - - @Override - public String getColumnLabel(int column) throws SQLException { - checkColumnIndex(column); - return sqlMetadata.get(column - 1).getName(); - } - - /** - * {@inheritDoc} - *

- * Name always has the same value as label - * because Tarantool does not differentiate - * column names and aliases. - * - * @see #getColumnLabel(int) - */ - @Override - public String getColumnName(int column) throws SQLException { - checkColumnIndex(column); - return sqlMetadata.get(column - 1).getName(); - } - - @Override - public String getSchemaName(int column) throws SQLException { - checkColumnIndex(column); - return ""; - } - - @Override - public int getPrecision(int column) throws SQLException { - checkColumnIndex(column); - return sqlMetadata.get(column - 1).getType().getPrecision(); - } - - - @Override - public int getScale(int column) throws SQLException { - checkColumnIndex(column); - return sqlMetadata.get(column - 1).getType().getScale(); - } - - @Override - public String getTableName(int column) throws SQLException { - checkColumnIndex(column); - // XXX: extra table name or, at least table ID is required in meta - // to be able to fetch the table name. - return ""; - } - - @Override - public String getCatalogName(int column) throws SQLException { - checkColumnIndex(column); - return ""; - } - - @Override - public int getColumnType(int column) throws SQLException { - checkColumnIndex(column); - return sqlMetadata.get(column - 1).getType().getJdbcType().getTypeNumber(); - } - - @Override - public String getColumnTypeName(int column) throws SQLException { - checkColumnIndex(column); - return sqlMetadata.get(column - 1).getType().getTypeName(); - } - - @Override - public boolean isReadOnly(int column) throws SQLException { - checkColumnIndex(column); - return readOnly; - } - - @Override - public boolean isWritable(int column) throws SQLException { - return !isReadOnly(column); - } - - @Override - public boolean isDefinitelyWritable(int column) throws SQLException { - return false; - } - - @Override - public String getColumnClassName(int column) throws SQLException { - checkColumnIndex(column); - return sqlMetadata.get(column - 1).getType().getJdbcType().getJavaType().getName(); - } - - @Override - public T unwrap(Class type) throws SQLException { - if (isWrapperFor(type)) { - return type.cast(this); - } - throw new SQLNonTransientException("SQLResultSetMetadata does not wrap " + type.getName()); - } - - @Override - public boolean isWrapperFor(Class type) throws SQLException { - return type.isAssignableFrom(this.getClass()); - } - - @Override - public void checkColumnIndex(int columnIndex) throws SQLException { - if (columnIndex < 1 || columnIndex > getColumnCount()) { - throw new SQLNonTransientException( - String.format("Column index %d is out of range. Max index is %d", columnIndex, getColumnCount()), - SQLStates.INVALID_PARAMETER_VALUE.getSqlState() - ); - } - } - - @Override - public boolean isTrimmable(int columnIndex) throws SQLException { - checkColumnIndex(columnIndex); - return sqlMetadata.get(columnIndex - 1).getType().isTrimmable(); - } - - @Override - public String toString() { - return "SQLResultSetMetaData{" + - "sqlMetadata=" + sqlMetadata + - '}'; - } + private final List sqlMetadata; + private final boolean readOnly; + + public SQLResultSetMetaData(List sqlMetaData, boolean readOnly) { + this.sqlMetadata = sqlMetaData; + this.readOnly = readOnly; + } + + @Override + public int getColumnCount() throws SQLException { + return sqlMetadata.size(); + } + + @Override + public boolean isAutoIncrement(int column) throws SQLException { + checkColumnIndex(column); + // XXX: extra flag or, at least table ID is required in meta + // to be able to fetch an the flag indirectly. + return false; + } + + @Override + public boolean isCaseSensitive(int column) throws SQLException { + checkColumnIndex(column); + return sqlMetadata.get(column - 1).getType().isCaseSensitive(); + } + + /** + * {@inheritDoc} + *

+ * All the types can be used in {@literal WHERE} clause. + */ + @Override + public boolean isSearchable(int column) throws SQLException { + checkColumnIndex(column); + return true; + } + + /** + * {@inheritDoc} + *

+ * Always {@literal false} because Tarantool does not have monetary types. + */ + @Override + public boolean isCurrency(int column) throws SQLException { + checkColumnIndex(column); + return false; + } + + @Override + public int isNullable(int column) throws SQLException { + checkColumnIndex(column); + // XXX: extra nullability flag or, at least table ID is required in meta + // to be able to fetch an the flag indirectly. + return ResultSetMetaData.columnNullableUnknown; + } + + @Override + public boolean isSigned(int column) throws SQLException { + checkColumnIndex(column); + return sqlMetadata.get(column - 1).getType().isSigned(); + } + + @Override + public int getColumnDisplaySize(int column) throws SQLException { + checkColumnIndex(column); + return sqlMetadata.get(column - 1).getType().getDisplaySize(); + } + + @Override + public String getColumnLabel(int column) throws SQLException { + checkColumnIndex(column); + return sqlMetadata.get(column - 1).getName(); + } + + /** + * {@inheritDoc} + *

+ * Name always has the same value as label because Tarantool does not differentiate column names and aliases. + * + * @see #getColumnLabel(int) + */ + @Override + public String getColumnName(int column) throws SQLException { + checkColumnIndex(column); + return sqlMetadata.get(column - 1).getName(); + } + + @Override + public String getSchemaName(int column) throws SQLException { + checkColumnIndex(column); + return ""; + } + + @Override + public int getPrecision(int column) throws SQLException { + checkColumnIndex(column); + return sqlMetadata.get(column - 1).getType().getPrecision(); + } + + + @Override + public int getScale(int column) throws SQLException { + checkColumnIndex(column); + return sqlMetadata.get(column - 1).getType().getScale(); + } + + @Override + public String getTableName(int column) throws SQLException { + checkColumnIndex(column); + // XXX: extra table name or, at least table ID is required in meta + // to be able to fetch the table name. + return ""; + } + + @Override + public String getCatalogName(int column) throws SQLException { + checkColumnIndex(column); + return ""; + } + + @Override + public int getColumnType(int column) throws SQLException { + checkColumnIndex(column); + return sqlMetadata.get(column - 1).getType().getJdbcType().getTypeNumber(); + } + + @Override + public String getColumnTypeName(int column) throws SQLException { + checkColumnIndex(column); + return sqlMetadata.get(column - 1).getType().getTypeName(); + } + + @Override + public boolean isReadOnly(int column) throws SQLException { + checkColumnIndex(column); + return readOnly; + } + + @Override + public boolean isWritable(int column) throws SQLException { + return !isReadOnly(column); + } + + @Override + public boolean isDefinitelyWritable(int column) throws SQLException { + return false; + } + + @Override + public String getColumnClassName(int column) throws SQLException { + checkColumnIndex(column); + return sqlMetadata.get(column - 1).getType().getJdbcType().getJavaType().getName(); + } + + @Override + public T unwrap(Class type) throws SQLException { + if (isWrapperFor(type)) { + return type.cast(this); + } + throw new SQLNonTransientException("SQLResultSetMetadata does not wrap " + type.getName()); + } + + @Override + public boolean isWrapperFor(Class type) throws SQLException { + return type.isAssignableFrom(this.getClass()); + } + + @Override + public void checkColumnIndex(int columnIndex) throws SQLException { + if (columnIndex < 1 || columnIndex > getColumnCount()) { + throw new SQLNonTransientException( + String.format("Column index %d is out of range. Max index is %d", columnIndex, getColumnCount()), + SQLStates.INVALID_PARAMETER_VALUE.getSqlState() + ); + } + } + + @Override + public boolean isTrimmable(int columnIndex) throws SQLException { + checkColumnIndex(columnIndex); + return sqlMetadata.get(columnIndex - 1).getType().isTrimmable(); + } + + @Override + public String toString() { + return "SQLResultSetMetaData{" + + "sqlMetadata=" + sqlMetadata + + '}'; + } } diff --git a/src/main/java/org/tarantool/jdbc/SQLStatement.java b/src/main/java/org/tarantool/jdbc/SQLStatement.java index e8959234..a8127da4 100644 --- a/src/main/java/org/tarantool/jdbc/SQLStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLStatement.java @@ -1,10 +1,9 @@ package org.tarantool.jdbc; -import org.tarantool.SqlProtoUtils; -import org.tarantool.jdbc.type.TarantoolSqlType; -import org.tarantool.util.JdbcConstants; -import org.tarantool.util.SQLStates; +import static org.tarantool.utils.LocalLogger.log; +import static org.tarantool.utils.QuoteWrapper.addQuotesForSpaces; +import java.math.BigDecimal; import java.sql.BatchUpdateException; import java.sql.Connection; import java.sql.ResultSet; @@ -20,495 +19,503 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; +import org.tarantool.SqlProtoUtils; +import org.tarantool.jdbc.type.TarantoolSqlType; +import org.tarantool.util.JdbcConstants; +import org.tarantool.util.SQLStates; +import org.tarantool.utils.QuoteWrapper; /** * Tarantool {@link Statement} implementation. *

- * Supports {@link ResultSet#TYPE_FORWARD_ONLY} and {@link ResultSet#TYPE_SCROLL_INSENSITIVE} - * types of cursors. - * Supports only {@link ResultSet#HOLD_CURSORS_OVER_COMMIT} holdability type. + * Supports {@link ResultSet#TYPE_FORWARD_ONLY} and {@link ResultSet#TYPE_SCROLL_INSENSITIVE} types of cursors. Supports only + * {@link ResultSet#HOLD_CURSORS_OVER_COMMIT} holdability type. */ public class SQLStatement implements TarantoolStatement { - private static final String GENERATED_KEY_COLUMN_NAME = "GENERATED_KEY"; - - protected final TarantoolConnection connection; - private final SQLResultSet emptyGeneratedKeys; - - /** - * Current result set / update count associated to this statement. - */ - protected SQLResultSet resultSet; - protected int updateCount; - protected SQLResultSet generatedKeys; - - private List batchQueries = new ArrayList<>(); - - private boolean isCloseOnCompletion; - - private final int resultSetType; - private final int resultSetConcurrency; - private final int resultSetHoldability; - - private int maxRows; - private int maxFieldSize; - - /** - * Query timeout in millis. - */ - private long timeout; - - /** - * Hint to the statement pool implementation indicating - * whether the application wants the statement to be pooled. - * - * Ignored. - */ - private boolean poolable; - - private final AtomicBoolean isClosed = new AtomicBoolean(false); - - protected SQLStatement(SQLConnection sqlConnection) throws SQLException { - this( - sqlConnection, - ResultSet.TYPE_FORWARD_ONLY, - ResultSet.CONCUR_READ_ONLY, - sqlConnection.getHoldability() - ); - } - - protected SQLStatement(SQLConnection sqlConnection, - int resultSetType, - int resultSetConcurrency, - int resultSetHoldability) throws SQLException { - this.connection = sqlConnection; - this.resultSetType = resultSetType; - this.resultSetConcurrency = resultSetConcurrency; - this.resultSetHoldability = resultSetHoldability; - this.emptyGeneratedKeys = this.generatedKeys = executeGeneratedKeys(Collections.emptyList()); - } - - @Override - public ResultSet executeQuery(String sql) throws SQLException { - checkNotClosed(); - if (!executeInternal(NO_GENERATED_KEYS, sql)) { - throw new SQLException("No results were returned", SQLStates.NO_DATA.getSqlState()); - } - return resultSet; - } - - @Override - public int executeUpdate(String sql) throws SQLException { - return executeUpdate(sql, NO_GENERATED_KEYS); - } - - @Override - public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { - checkNotClosed(); - JdbcConstants.checkGeneratedKeysConstant(autoGeneratedKeys); - if (executeInternal(autoGeneratedKeys, sql)) { - throw new SQLException( - "Result was returned but nothing was expected", - SQLStates.TOO_MANY_RESULTS.getSqlState() - ); - } - return updateCount; - } - - @Override - public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public int executeUpdate(String sql, String[] columnNames) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void close() throws SQLException { - if (isClosed.compareAndSet(false, true)) { - cancel(); - discardLastResults(); - } - } - - @Override - public int getMaxFieldSize() throws SQLException { - return maxFieldSize; - } - - @Override - public void setMaxFieldSize(int size) throws SQLException { - if (size < 0) { - throw new SQLException( - "The max field size must be positive or zero", - SQLStates.INVALID_PARAMETER_VALUE.getSqlState() - ); - } - maxFieldSize = size; - } - - @Override - public int getMaxRows() throws SQLException { - checkNotClosed(); - return maxRows; - } - - @Override - public void setMaxRows(int maxRows) throws SQLException { - checkNotClosed(); - if (maxRows < 0) { - throw new SQLNonTransientException("Max rows parameter can't be a negative value"); - } - this.maxRows = maxRows; - } - - @Override - public void setEscapeProcessing(boolean enable) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public int getQueryTimeout() throws SQLException { - return (int) TimeUnit.MILLISECONDS.toSeconds(timeout); - } - - @Override - public void setQueryTimeout(int seconds) throws SQLException { - if (seconds < 0) { - throw new SQLNonTransientException( - "Query timeout must be positive or zero", - SQLStates.INVALID_PARAMETER_VALUE.getSqlState() - ); - } - timeout = TimeUnit.SECONDS.toMillis(seconds); - } - - @Override - public void cancel() throws SQLException { - - } - - @Override - public SQLWarning getWarnings() throws SQLException { - return null; - } - - @Override - public void clearWarnings() throws SQLException { - - } - - @Override - public void setCursorName(String name) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public boolean execute(String sql) throws SQLException { - checkNotClosed(); - return executeInternal(NO_GENERATED_KEYS, sql); - } - - @Override - public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { - checkNotClosed(); - JdbcConstants.checkGeneratedKeysConstant(autoGeneratedKeys); - return executeInternal(autoGeneratedKeys, sql); - } - - @Override - public boolean execute(String sql, int[] columnIndexes) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public boolean execute(String sql, String[] columnNames) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public ResultSet getResultSet() throws SQLException { - checkNotClosed(); - return resultSet; - } - - @Override - public int getUpdateCount() throws SQLException { - checkNotClosed(); - return updateCount; - } - - @Override - public boolean getMoreResults() throws SQLException { - return getMoreResults(Statement.CLOSE_CURRENT_RESULT); - } - - @Override - public boolean getMoreResults(int current) throws SQLException { - checkNotClosed(); - JdbcConstants.checkCurrentResultConstant(current); - if (resultSet != null && - (current == KEEP_CURRENT_RESULT || current == CLOSE_ALL_RESULTS)) { - throw new SQLFeatureNotSupportedException(); - } - - // the driver doesn't support multiple results - // close current result and return no-more-results flag - discardLastResults(); - return false; - } - - @Override - public void setFetchDirection(int direction) throws SQLException { - checkNotClosed(); - if (direction != ResultSet.FETCH_FORWARD) { - throw new SQLFeatureNotSupportedException(); - } - } - - @Override - public int getFetchDirection() throws SQLException { - checkNotClosed(); - return ResultSet.FETCH_FORWARD; - } - - @Override - public void setFetchSize(int rows) throws SQLException { - checkNotClosed(); - // no-op - } - - @Override - public int getFetchSize() throws SQLException { - return 0; - } - - @Override - public int getResultSetConcurrency() throws SQLException { - checkNotClosed(); - return resultSetConcurrency; - } - - @Override - public int getResultSetType() throws SQLException { - checkNotClosed(); - return resultSetType; - } - - @Override - public void addBatch(String sql) throws SQLException { - checkNotClosed(); - batchQueries.add(sql); - } - - @Override - public void clearBatch() throws SQLException { - checkNotClosed(); - batchQueries.clear(); - } - - @Override - public int[] executeBatch() throws SQLException { - checkNotClosed(); - discardLastResults(); - try { - List queries = batchQueries.stream() - .map(q -> SQLQueryHolder.of(q)) - .collect(Collectors.toList()); - return executeBatchInternal(queries); - } finally { - batchQueries.clear(); - } - } - - @Override - public Connection getConnection() throws SQLException { - return connection; - } - - @Override - public ResultSet getGeneratedKeys() throws SQLException { - checkNotClosed(); - return generatedKeys; - } - - @Override - public int getResultSetHoldability() throws SQLException { - checkNotClosed(); - return resultSetHoldability; - } - - @Override - public boolean isClosed() throws SQLException { - return isClosed.get() || connection.isClosed(); - } - - @Override - public void setPoolable(boolean poolable) throws SQLException { - checkNotClosed(); - this.poolable = poolable; - } - - @Override - public boolean isPoolable() throws SQLException { - checkNotClosed(); - return poolable; - } - - /** - * {@inheritDoc} - *

- * Impl Note: this method doesn't affect - * execution methods which close the last result set implicitly. - * It is applied only when {@link ResultSet#close()} is invoked - * explicitly by the app. - * - * @throws SQLException if this method is called on a closed - * {@code Statement} - */ - @Override - public void closeOnCompletion() throws SQLException { - checkNotClosed(); - isCloseOnCompletion = true; - } - - @Override - public boolean isCloseOnCompletion() throws SQLException { - checkNotClosed(); - return isCloseOnCompletion; - } - - @Override - public void checkCompletion() throws SQLException { - if (isCloseOnCompletion && - resultSet != null && - resultSet.isClosed()) { - close(); - } - } - - @Override - public T unwrap(Class type) throws SQLException { - if (isWrapperFor(type)) { - return type.cast(this); - } - throw new SQLNonTransientException("SQLStatement does not wrap " + type.getName()); - } - - @Override - public boolean isWrapperFor(Class type) throws SQLException { - return type.isAssignableFrom(this.getClass()); - } - - /** - * Clears the results of the most recent execution. - * - * @throws SQLException if this method is called on a closed - * {@code Statement} - */ - protected void discardLastResults() throws SQLException { - final SQLResultSet lastResultSet = resultSet; - - clearWarnings(); - updateCount = -1; - resultSet = null; - generatedKeys = emptyGeneratedKeys; - - if (lastResultSet != null) { - try { - lastResultSet.close(); - } catch (Exception ignored) { - // No-op. - } - } - } - - /** - * Performs query execution. - * - * @param autoGeneratedKeys whether auto-generated keys should be collected; - * one of the following constants: - * {@code Statement.RETURN_GENERATED_KEYS}, - * {@code Statement.NO_GENERATED_KEYS} - * @see #getGeneratedKeys() - * @param sql query - * @param params optional params - * - * @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 { - discardLastResults(); - SQLResultHolder holder; - try { - holder = connection.execute(timeout, SQLQueryHolder.of(sql, params)); - } catch (StatementTimeoutException e) { - cancel(); - throw new SQLTimeoutException(); - } - - if (holder.isQueryResult()) { - resultSet = new SQLResultSet(holder, this); - } - updateCount = holder.getUpdateCount(); - if (autoGeneratedKeys == Statement.RETURN_GENERATED_KEYS) { - generatedKeys = executeGeneratedKeys(holder.getGeneratedIds()); - } - return holder.isQueryResult(); - } - - /** - * Performs batch query execution. - * - * @param queries batch queries - * - * @return update count result per query - * - * @throws SQLException if this method is called on a closed - * {@code Statement} - */ - protected int[] executeBatchInternal(List queries) throws SQLException { - SQLBatchResultHolder batchResult = connection.executeBatch(timeout, queries); - int[] resultCounts = batchResult.getResults().stream() - .mapToInt(result -> result.isQueryResult() - ? Statement.EXECUTE_FAILED : result.getUpdateCount() == SQLResultHolder.NO_UPDATE_COUNT - ? Statement.SUCCESS_NO_INFO : result.getUpdateCount() - ).toArray(); - - if (batchResult.getError() != null) { - throw new BatchUpdateException(resultCounts, batchResult.getError()); - } - - return resultCounts; - } - - @Override - public ResultSet executeMetadata(SQLResultHolder data) throws SQLException { - checkNotClosed(); - return createResultSet(data); - } - - protected SQLResultSet createResultSet(SQLResultHolder holder) throws SQLException { - return new SQLResultSet(holder, this); - } - - protected void checkNotClosed() throws SQLException { - if (isClosed()) { - throw new SQLNonTransientException("Statement is closed."); - } - } - - protected SQLResultSet executeGeneratedKeys(List generatedKeys) throws SQLException { - SqlProtoUtils.SQLMetaData sqlMetaData = - new SqlProtoUtils.SQLMetaData(GENERATED_KEY_COLUMN_NAME, TarantoolSqlType.INTEGER); - List> rows = generatedKeys.stream() - .map(Collections::singletonList) - .collect(Collectors.toList()); - return createResultSet(SQLResultHolder.ofQuery(Collections.singletonList(sqlMetaData), rows)); - } + private static final String GENERATED_KEY_COLUMN_NAME = "GENERATED_KEY"; + + protected final TarantoolConnection connection; + private final SQLResultSet emptyGeneratedKeys; + + /** + * Current result set / update count associated to this statement. + */ + protected SQLResultSet resultSet; + protected int updateCount; + protected SQLResultSet generatedKeys; + + private List batchQueries = new ArrayList<>(); + + private boolean isCloseOnCompletion; + + private final int resultSetType; + private final int resultSetConcurrency; + private final int resultSetHoldability; + + private int maxRows; + private int maxFieldSize; + + /** + * Query timeout in millis. + */ + private long timeout; + + /** + * Hint to the statement pool implementation indicating whether the application wants the statement to be pooled. + *

+ * Ignored. + */ + private boolean poolable; + + private final AtomicBoolean isClosed = new AtomicBoolean(false); + + protected SQLStatement(SQLConnection sqlConnection) throws SQLException { + this( + sqlConnection, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + sqlConnection.getHoldability() + ); + } + + protected SQLStatement(SQLConnection sqlConnection, + int resultSetType, + int resultSetConcurrency, + int resultSetHoldability) throws SQLException { + this.connection = sqlConnection; + this.resultSetType = resultSetType; + this.resultSetConcurrency = resultSetConcurrency; + this.resultSetHoldability = resultSetHoldability; + this.emptyGeneratedKeys = this.generatedKeys = executeGeneratedKeys(Collections.emptyList()); + } + + @Override + public ResultSet executeQuery(String sql) throws SQLException { + checkNotClosed(); + if (!executeInternal(NO_GENERATED_KEYS, sql)) { + throw new SQLException("No results were returned", SQLStates.NO_DATA.getSqlState()); + } + return resultSet; + } + + @Override + public int executeUpdate(String sql) throws SQLException { + return executeUpdate(sql, NO_GENERATED_KEYS); + } + + @Override + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + checkNotClosed(); + JdbcConstants.checkGeneratedKeysConstant(autoGeneratedKeys); + if (executeInternal(autoGeneratedKeys, sql)) { + throw new SQLException( + "Result was returned but nothing was expected", + SQLStates.TOO_MANY_RESULTS.getSqlState() + ); + } + return updateCount; + } + + @Override + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int executeUpdate(String sql, String[] columnNames) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void close() throws SQLException { + if (isClosed.compareAndSet(false, true)) { + cancel(); + discardLastResults(); + } + } + + @Override + public int getMaxFieldSize() throws SQLException { + return maxFieldSize; + } + + @Override + public void setMaxFieldSize(int size) throws SQLException { + if (size < 0) { + throw new SQLException( + "The max field size must be positive or zero", + SQLStates.INVALID_PARAMETER_VALUE.getSqlState() + ); + } + maxFieldSize = size; + } + + @Override + public int getMaxRows() throws SQLException { + checkNotClosed(); + return maxRows; + } + + @Override + public void setMaxRows(int maxRows) throws SQLException { + checkNotClosed(); + if (maxRows < 0) { + throw new SQLNonTransientException("Max rows parameter can't be a negative value"); + } + this.maxRows = maxRows; + } + + @Override + public void setEscapeProcessing(boolean enable) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int getQueryTimeout() throws SQLException { + return (int) TimeUnit.MILLISECONDS.toSeconds(timeout); + } + + @Override + public void setQueryTimeout(int seconds) throws SQLException { + if (seconds < 0) { + throw new SQLNonTransientException( + "Query timeout must be positive or zero", + SQLStates.INVALID_PARAMETER_VALUE.getSqlState() + ); + } + timeout = TimeUnit.SECONDS.toMillis(seconds); + } + + @Override + public void cancel() throws SQLException { + + } + + @Override + public SQLWarning getWarnings() throws SQLException { + return null; + } + + @Override + public void clearWarnings() throws SQLException { + + } + + @Override + public void setCursorName(String name) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean execute(String sql) throws SQLException { + checkNotClosed(); + String modifiedSql = addQuotesForSpaces(sql); + log("Modified sql:" + sql + " to " + modifiedSql); + return executeInternal(NO_GENERATED_KEYS, modifiedSql); + } + + + @Override + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + checkNotClosed(); + JdbcConstants.checkGeneratedKeysConstant(autoGeneratedKeys); + return executeInternal(autoGeneratedKeys, sql); + } + + @Override + public boolean execute(String sql, int[] columnIndexes) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean execute(String sql, String[] columnNames) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public ResultSet getResultSet() throws SQLException { + checkNotClosed(); + return resultSet; + } + + @Override + public int getUpdateCount() throws SQLException { + checkNotClosed(); + return updateCount; + } + + @Override + public boolean getMoreResults() throws SQLException { + return getMoreResults(Statement.CLOSE_CURRENT_RESULT); + } + + @Override + public boolean getMoreResults(int current) throws SQLException { + checkNotClosed(); + JdbcConstants.checkCurrentResultConstant(current); + if (resultSet != null && + (current == KEEP_CURRENT_RESULT || current == CLOSE_ALL_RESULTS)) { + throw new SQLFeatureNotSupportedException(); + } + + // the driver doesn't support multiple results + // close current result and return no-more-results flag + discardLastResults(); + return false; + } + + @Override + public void setFetchDirection(int direction) throws SQLException { + checkNotClosed(); + if (direction != ResultSet.FETCH_FORWARD) { + throw new SQLFeatureNotSupportedException(); + } + } + + @Override + public int getFetchDirection() throws SQLException { + checkNotClosed(); + return ResultSet.FETCH_FORWARD; + } + + @Override + public void setFetchSize(int rows) throws SQLException { + checkNotClosed(); + // no-op + } + + @Override + public int getFetchSize() throws SQLException { + return 0; + } + + @Override + public int getResultSetConcurrency() throws SQLException { + checkNotClosed(); + return resultSetConcurrency; + } + + @Override + public int getResultSetType() throws SQLException { + checkNotClosed(); + return resultSetType; + } + + @Override + public void addBatch(String sql) throws SQLException { + checkNotClosed(); + batchQueries.add(sql); + } + + @Override + public void clearBatch() throws SQLException { + checkNotClosed(); + batchQueries.clear(); + } + + @Override + public int[] executeBatch() throws SQLException { + checkNotClosed(); + discardLastResults(); + try { + List queries = batchQueries.stream() + .map(q -> SQLQueryHolder.of(q)) + .collect(Collectors.toList()); + return executeBatchInternal(queries); + } finally { + batchQueries.clear(); + } + } + + @Override + public Connection getConnection() throws SQLException { + return connection; + } + + @Override + public ResultSet getGeneratedKeys() throws SQLException { + checkNotClosed(); + return generatedKeys; + } + + @Override + public int getResultSetHoldability() throws SQLException { + checkNotClosed(); + return resultSetHoldability; + } + + @Override + public boolean isClosed() throws SQLException { + return isClosed.get() || connection.isClosed(); + } + + @Override + public void setPoolable(boolean poolable) throws SQLException { + checkNotClosed(); + this.poolable = poolable; + } + + @Override + public boolean isPoolable() throws SQLException { + checkNotClosed(); + return poolable; + } + + /** + * {@inheritDoc} + *

+ * Impl Note: this method doesn't affect + * execution methods which close the last result set implicitly. It is applied only when {@link ResultSet#close()} is invoked explicitly by the + * app. + * + * @throws SQLException if this method is called on a closed {@code Statement} + */ + @Override + public void closeOnCompletion() throws SQLException { + checkNotClosed(); + isCloseOnCompletion = true; + } + + @Override + public boolean isCloseOnCompletion() throws SQLException { + checkNotClosed(); + return isCloseOnCompletion; + } + + @Override + public void checkCompletion() throws SQLException { + if (isCloseOnCompletion && + resultSet != null && + resultSet.isClosed()) { + close(); + } + } + + @Override + public T unwrap(Class type) throws SQLException { + if (isWrapperFor(type)) { + return type.cast(this); + } + throw new SQLNonTransientException("SQLStatement does not wrap " + type.getName()); + } + + @Override + public boolean isWrapperFor(Class type) throws SQLException { + return type.isAssignableFrom(this.getClass()); + } + + /** + * Clears the results of the most recent execution. + * + * @throws SQLException if this method is called on a closed {@code Statement} + */ + protected void discardLastResults() throws SQLException { + final SQLResultSet lastResultSet = resultSet; + + clearWarnings(); + updateCount = -1; + resultSet = null; + generatedKeys = emptyGeneratedKeys; + + if (lastResultSet != null) { + try { + lastResultSet.close(); + } catch (Exception ignored) { + // No-op. + } + } + } + + /** + * Performs query execution. + * + * @param autoGeneratedKeys whether auto-generated keys should be collected; one of the following constants: + * {@code Statement.RETURN_GENERATED_KEYS}, {@code Statement.NO_GENERATED_KEYS} + * @param sql query + * @param params optional params + * @return {@code true}, if the result is a ResultSet object; + * @throws SQLException if this method is called on a closed {@code Statement} + * @see #getGeneratedKeys() + */ + protected boolean executeInternal(int autoGeneratedKeys, String sql, Object... params) throws SQLException { + discardLastResults(); + SQLResultHolder holder; + sql = QuoteWrapper.addQuotesForSpaces(sql); + log("Modified sql:" + sql); + for (int i = 0; i < params.length; i++) { + if (params[i] == null) { + log("Parameter type:" + null); + continue; + } + log("Parameter type:" + params[i].getClass()); + if (params[i] instanceof BigDecimal) { + log("Convert " + params[i] + " to Integer"); + params[i] = ((BigDecimal) params[i]).intValue(); + } + } + try { + holder = connection.execute(timeout, SQLQueryHolder.of(sql, params)); + } catch (StatementTimeoutException e) { + cancel(); + throw new SQLTimeoutException(); + } + + if (holder.isQueryResult()) { + resultSet = new SQLResultSet(holder, this); + } + updateCount = holder.getUpdateCount(); + if (autoGeneratedKeys == Statement.RETURN_GENERATED_KEYS) { + generatedKeys = executeGeneratedKeys(holder.getGeneratedIds()); + } + return holder.isQueryResult(); + } + + /** + * Performs batch query execution. + * + * @param queries batch queries + * @return update count result per query + * @throws SQLException if this method is called on a closed {@code Statement} + */ + protected int[] executeBatchInternal(List queries) throws SQLException { + SQLBatchResultHolder batchResult = connection.executeBatch(timeout, queries); + int[] resultCounts = batchResult.getResults().stream() + .mapToInt(result -> result.isQueryResult() + ? Statement.EXECUTE_FAILED : result.getUpdateCount() == SQLResultHolder.NO_UPDATE_COUNT + ? Statement.SUCCESS_NO_INFO : result.getUpdateCount() + ).toArray(); + + if (batchResult.getError() != null) { + throw new BatchUpdateException(resultCounts, batchResult.getError()); + } + + return resultCounts; + } + + @Override + public ResultSet executeMetadata(SQLResultHolder data) throws SQLException { + checkNotClosed(); + return createResultSet(data); + } + + protected SQLResultSet createResultSet(SQLResultHolder holder) throws SQLException { + return new SQLResultSet(holder, this); + } + + protected void checkNotClosed() throws SQLException { + if (isClosed()) { + throw new SQLNonTransientException("Statement is closed."); + } + } + + protected SQLResultSet executeGeneratedKeys(List generatedKeys) throws SQLException { + SqlProtoUtils.SQLMetaData sqlMetaData = + new SqlProtoUtils.SQLMetaData(GENERATED_KEY_COLUMN_NAME, TarantoolSqlType.INTEGER); + List> rows = generatedKeys.stream() + .map(Collections::singletonList) + .collect(Collectors.toList()); + return createResultSet(SQLResultHolder.ofQuery(Collections.singletonList(sqlMetaData), rows)); + } } diff --git a/src/main/java/org/tarantool/jdbc/ds/SQLDataSource.java b/src/main/java/org/tarantool/jdbc/ds/SQLDataSource.java index 6a474d39..53238dc8 100644 --- a/src/main/java/org/tarantool/jdbc/ds/SQLDataSource.java +++ b/src/main/java/org/tarantool/jdbc/ds/SQLDataSource.java @@ -3,12 +3,15 @@ import org.tarantool.jdbc.SQLConnection; import org.tarantool.jdbc.SQLConstant; import org.tarantool.jdbc.SQLProperty; +import org.tarantool.util.NodeSpec; import java.io.PrintWriter; import java.sql.Connection; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLNonTransientException; +import java.util.Collections; +import java.util.List; import java.util.Properties; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; @@ -26,7 +29,7 @@ public class SQLDataSource implements TarantoolDataSource, DataSource { @Override public Connection getConnection() throws SQLException { - return new SQLConnection(makeUrl(), new Properties(properties)); + return new SQLConnection(makeUrl(), makeNodeSpecs(), new Properties(properties)); } @Override @@ -34,7 +37,7 @@ public Connection getConnection(String username, String password) throws SQLExce Properties copyProperties = new Properties(properties); SQLProperty.USER.setString(copyProperties, username); SQLProperty.PASSWORD.setString(copyProperties, password); - return new SQLConnection(makeUrl(), copyProperties); + return new SQLConnection(makeUrl(), makeNodeSpecs(), copyProperties); } @Override @@ -156,4 +159,10 @@ private String makeUrl() { SQLProperty.HOST.getString(properties) + ":" + SQLProperty.PORT.getString(properties); } -} + private List makeNodeSpecs() throws SQLException { + return Collections.singletonList(new NodeSpec( + SQLProperty.HOST.getString(properties), + SQLProperty.PORT.getInt(properties) + )); + } +} \ No newline at end of file diff --git a/src/main/java/org/tarantool/jdbc/type/JdbcType.java b/src/main/java/org/tarantool/jdbc/type/JdbcType.java index 296cbbef..82cbb92a 100644 --- a/src/main/java/org/tarantool/jdbc/type/JdbcType.java +++ b/src/main/java/org/tarantool/jdbc/type/JdbcType.java @@ -1,74 +1,76 @@ package org.tarantool.jdbc.type; +import java.math.BigDecimal; import java.sql.Blob; import java.sql.Clob; import java.sql.JDBCType; import java.sql.NClob; +import org.tarantool.utils.LocalLogger; /** - * Describes supported JDBC types that match - * Tarantool SQL types. - * - * Skipped unsupported types are - * numeric types {@link JDBCType#NUMERIC}, {@link JDBCType#DECIMAL}; - * date types {@link JDBCType#DATE}, {@link JDBCType#TIME}, {@link JDBCType#TIMESTAMP}; - * xml type {@link JDBCType#SQLXML}. + * Describes supported JDBC types that match Tarantool SQL types. + *

+ * Skipped unsupported types are numeric types {@link JDBCType#NUMERIC}, {@link JDBCType#DECIMAL}; date types {@link JDBCType#DATE}, + * {@link JDBCType#TIME}, {@link JDBCType#TIMESTAMP}; xml type {@link JDBCType#SQLXML}. */ public enum JdbcType { - UNKNOWN(Object.class, JDBCType.OTHER, false), - - CHAR(String.class, JDBCType.CHAR, true), - VARCHAR(String.class, JDBCType.VARCHAR, true), - LONGVARCHAR(String.class, JDBCType.LONGNVARCHAR, true), - - NCHAR(String.class, JDBCType.NCHAR, true), - NVARCHAR(String.class, JDBCType.NVARCHAR, true), - LONGNVARCHAR(String.class, JDBCType.LONGNVARCHAR, true), - - BINARY(byte[].class, JDBCType.BINARY, true), - VARBINARY(byte[].class, JDBCType.VARBINARY, true), - LONGVARBINARY(byte[].class, JDBCType.LONGVARBINARY, true), - - BIT(Boolean.class, JDBCType.BIT, false), - BOOLEAN(Boolean.class, JDBCType.BOOLEAN, false), - - REAL(Float.class, JDBCType.REAL, false), - FLOAT(Double.class, JDBCType.FLOAT, false), - DOUBLE(Double.class, JDBCType.DOUBLE, false), - - TINYINT(Byte.class, JDBCType.TINYINT, false), - SMALLINT(Short.class, JDBCType.SMALLINT, false), - INTEGER(Integer.class, JDBCType.INTEGER, false), - BIGINT(Long.class, JDBCType.BIGINT, false), - - CLOB(Clob.class, JDBCType.CLOB, false), - NCLOB(NClob.class, JDBCType.NCLOB, false), - BLOB(Blob.class, JDBCType.BLOB, false); - - private final Class javaType; - private final JDBCType targetJdbcType; - private final boolean trimmable; - - JdbcType(Class javaType, JDBCType targetJdbcType, boolean trimmable) { - this.javaType = javaType; - this.targetJdbcType = targetJdbcType; - this.trimmable = trimmable; - } - - public Class getJavaType() { - return javaType; - } - - public JDBCType getTargetJdbcType() { - return targetJdbcType; - } - - public boolean isTrimmable() { - return trimmable; - } - - public int getTypeNumber() { - return targetJdbcType.getVendorTypeNumber(); + UNKNOWN(Object.class, JDBCType.OTHER, false), + DECIMAL(BigDecimal.class, JDBCType.DECIMAL, true), + CHAR(String.class, JDBCType.CHAR, true), + VARCHAR(String.class, JDBCType.VARCHAR, true), + LONGVARCHAR(String.class, JDBCType.LONGNVARCHAR, true), + + NCHAR(String.class, JDBCType.NCHAR, true), + NVARCHAR(String.class, JDBCType.NVARCHAR, true), + LONGNVARCHAR(String.class, JDBCType.LONGNVARCHAR, true), + + BINARY(byte[].class, JDBCType.BINARY, true), + VARBINARY(byte[].class, JDBCType.VARBINARY, true), + LONGVARBINARY(byte[].class, JDBCType.LONGVARBINARY, true), + + BIT(Boolean.class, JDBCType.BIT, false), + BOOLEAN(Boolean.class, JDBCType.BOOLEAN, false), + + REAL(Float.class, JDBCType.REAL, false), + FLOAT(Double.class, JDBCType.FLOAT, false), + DOUBLE(Double.class, JDBCType.DOUBLE, false), + + TINYINT(Byte.class, JDBCType.TINYINT, false), + SMALLINT(Short.class, JDBCType.SMALLINT, false), + INTEGER(Integer.class, JDBCType.INTEGER, false), + BIGINT(Long.class, JDBCType.BIGINT, false), + + CLOB(Clob.class, JDBCType.CLOB, false), + NCLOB(NClob.class, JDBCType.NCLOB, false), + BLOB(Blob.class, JDBCType.BLOB, false); + + private final Class javaType; + private final JDBCType targetJdbcType; + private final boolean trimmable; + + JdbcType(Class javaType, JDBCType targetJdbcType, boolean trimmable) { + this.javaType = javaType; + this.targetJdbcType = targetJdbcType; + this.trimmable = trimmable; + } + + public Class getJavaType() { + return javaType; + } + + public JDBCType getTargetJdbcType() { + return targetJdbcType; + } + + public boolean isTrimmable() { + return trimmable; + } + + public int getTypeNumber() { + if (targetJdbcType == JDBCType.DECIMAL) { + LocalLogger.log("JDBCType.DECIMAL vendor type number is" + targetJdbcType.getVendorTypeNumber()); } + return targetJdbcType.getVendorTypeNumber(); + } } diff --git a/src/main/java/org/tarantool/jdbc/type/TarantoolSqlType.java b/src/main/java/org/tarantool/jdbc/type/TarantoolSqlType.java index 030e5572..8e9f9f99 100644 --- a/src/main/java/org/tarantool/jdbc/type/TarantoolSqlType.java +++ b/src/main/java/org/tarantool/jdbc/type/TarantoolSqlType.java @@ -8,130 +8,133 @@ */ public enum TarantoolSqlType { - UNKNOWN(TarantoolType.UNKNOWN, JdbcType.UNKNOWN, "unknown"), - - // float, double, real used to be number aliases before 2.2 - FLOAT(TarantoolType.NUMBER, JdbcType.FLOAT, "float"), - DOUBLE(TarantoolType.NUMBER, JdbcType.DOUBLE, "double"), - REAL(TarantoolType.NUMBER, JdbcType.REAL, "real"), - // was introduced in Tarantool 2.2.1 - NUMBER(TarantoolType.NUMBER, JdbcType.DOUBLE, "number"), - - INT(TarantoolType.INTEGER, JdbcType.BIGINT, "int"), - INTEGER(TarantoolType.INTEGER, JdbcType.BIGINT, "integer"), - // was introduced in 2.2 - UNSIGNED(TarantoolType.UNSIGNED, JdbcType.BIGINT, "integer"), - - // were introduced in 2.2 - BOOL(TarantoolType.BOOLEAN, JdbcType.BOOLEAN, "bool"), - BOOLEAN(TarantoolType.BOOLEAN, JdbcType.BOOLEAN, "boolean"), - - STRING(TarantoolType.STRING, JdbcType.VARCHAR, "string"), - TEXT(TarantoolType.STRING, JdbcType.VARCHAR, "text"), - VARCHAR(TarantoolType.STRING, JdbcType.VARCHAR, "varchar") { - @Override - public String getDisplayType() { - return getTypeName() + "(128)"; - } - }, - - // was introduced in 2.2 - VARBINARY(TarantoolType.VARBINARY, JdbcType.VARBINARY, "varbinary"), - - SCALAR(TarantoolType.SCALAR, JdbcType.BINARY, "scalar"); - - private static final Map defaultSqlTypeMapping; - static { - defaultSqlTypeMapping = new HashMap<>(); - defaultSqlTypeMapping.put(TarantoolType.BOOLEAN, TarantoolSqlType.BOOLEAN); - defaultSqlTypeMapping.put(TarantoolType.STRING, TarantoolSqlType.STRING); - defaultSqlTypeMapping.put(TarantoolType.INTEGER, TarantoolSqlType.INTEGER); - defaultSqlTypeMapping.put(TarantoolType.UNSIGNED, TarantoolSqlType.UNSIGNED); - defaultSqlTypeMapping.put(TarantoolType.NUMBER, TarantoolSqlType.NUMBER); - defaultSqlTypeMapping.put(TarantoolType.VARBINARY, TarantoolSqlType.VARBINARY); - defaultSqlTypeMapping.put(TarantoolType.SCALAR, TarantoolSqlType.SCALAR); - } - - public static TarantoolSqlType getDefaultSqlType(TarantoolType type) { - return defaultSqlTypeMapping.getOrDefault(type, TarantoolSqlType.UNKNOWN); - } - - /** - * Corresponding raw {@link TarantoolType}. - */ - private final TarantoolType tarantoolType; - - /** - * Corresponding {@link JdbcType}. - */ - private final JdbcType jdbcType; - - /** - * Name of Tarantool SQL type. - */ - private final String typeName; - - public static TarantoolSqlType of(String type) { - for (TarantoolSqlType value : TarantoolSqlType.values()) { - if (value.typeName.equalsIgnoreCase(type)) { - return value; - } - } - return UNKNOWN; - } - - TarantoolSqlType(TarantoolType tarantoolType, JdbcType jdbcType, String typeName) { - this.tarantoolType = tarantoolType; - this.jdbcType = jdbcType; - this.typeName = typeName; - } - - public String getTypeName() { - return typeName; - } - + UNKNOWN(TarantoolType.UNKNOWN, JdbcType.UNKNOWN, "unknown"), + + // float, double, real used to be number aliases before 2.2 + FLOAT(TarantoolType.NUMBER, JdbcType.FLOAT, "float"), + DECIMAL(TarantoolType.NUMBER, JdbcType.DECIMAL, "decimal"), + DOUBLE(TarantoolType.NUMBER, JdbcType.DOUBLE, "double"), + REAL(TarantoolType.NUMBER, JdbcType.REAL, "real"), + // was introduced in Tarantool 2.2.1 + NUMBER(TarantoolType.NUMBER, JdbcType.DOUBLE, "number"), + + INT(TarantoolType.INTEGER, JdbcType.BIGINT, "int"), + INTEGER(TarantoolType.INTEGER, JdbcType.BIGINT, "integer"), + // was introduced in 2.2 + UNSIGNED(TarantoolType.UNSIGNED, JdbcType.BIGINT, "integer"), + + // were introduced in 2.2 + BOOL(TarantoolType.BOOLEAN, JdbcType.BOOLEAN, "bool"), + BOOLEAN(TarantoolType.BOOLEAN, JdbcType.BOOLEAN, "boolean"), + + STRING(TarantoolType.STRING, JdbcType.VARCHAR, "string"), + TEXT(TarantoolType.STRING, JdbcType.VARCHAR, "text"), + VARCHAR(TarantoolType.STRING, JdbcType.VARCHAR, "varchar") { + @Override public String getDisplayType() { - return typeName; - } - - public TarantoolType getTarantoolType() { - return tarantoolType; - } - - public JdbcType getJdbcType() { - return jdbcType; + return getTypeName() + "(128)"; } - - public boolean isSigned() { - return tarantoolType.isSigned(); - } - - public boolean isCaseSensitive() { - return tarantoolType.isCaseSensitive(); - } - - public boolean isTrimmable() { - return jdbcType.isTrimmable(); - } - - public int getPrecision() { - return tarantoolType.getPrecision(); - } - - public int getScale() { - return tarantoolType.getScale(); - } - - public int getDisplaySize() { - return tarantoolType.getDisplaySize(); - } - - @Override - public String toString() { - return "TarantoolSqlType{" + - "tarantoolType=" + tarantoolType + - ", jdbcType=" + jdbcType + - ", typeName='" + typeName + '\'' + - '}'; + }, + + // was introduced in 2.2 + VARBINARY(TarantoolType.VARBINARY, JdbcType.VARBINARY, "varbinary"), + + SCALAR(TarantoolType.SCALAR, JdbcType.BINARY, "scalar"); + + private static final Map defaultSqlTypeMapping; + + static { + defaultSqlTypeMapping = new HashMap<>(); + defaultSqlTypeMapping.put(TarantoolType.BOOLEAN, TarantoolSqlType.BOOLEAN); + defaultSqlTypeMapping.put(TarantoolType.STRING, TarantoolSqlType.STRING); + defaultSqlTypeMapping.put(TarantoolType.INTEGER, TarantoolSqlType.INTEGER); + defaultSqlTypeMapping.put(TarantoolType.UNSIGNED, TarantoolSqlType.UNSIGNED); + defaultSqlTypeMapping.put(TarantoolType.NUMBER, TarantoolSqlType.NUMBER); + defaultSqlTypeMapping.put(TarantoolType.VARBINARY, TarantoolSqlType.VARBINARY); + defaultSqlTypeMapping.put(TarantoolType.SCALAR, TarantoolSqlType.SCALAR); + defaultSqlTypeMapping.put(TarantoolType.DECIMAL, TarantoolSqlType.DOUBLE); + } + + public static TarantoolSqlType getDefaultSqlType(TarantoolType type) { + return defaultSqlTypeMapping.getOrDefault(type, TarantoolSqlType.TEXT); + } //переписать в запросе сгенерированный лайк на = + //дОбавить кавычки для сортировки для полей сортировки + /** + * Corresponding raw {@link TarantoolType}. + */ + private final TarantoolType tarantoolType; + + /** + * Corresponding {@link JdbcType}. + */ + private final JdbcType jdbcType; + + /** + * Name of Tarantool SQL type. + */ + private final String typeName; + + public static TarantoolSqlType of(String type) { + for (TarantoolSqlType value : TarantoolSqlType.values()) { + if (value.typeName.equalsIgnoreCase(type)) { + return value; + } } + return UNKNOWN; + } + + TarantoolSqlType(TarantoolType tarantoolType, JdbcType jdbcType, String typeName) { + this.tarantoolType = tarantoolType; + this.jdbcType = jdbcType; + this.typeName = typeName; + } + + public String getTypeName() { + return typeName; + } + + public String getDisplayType() { + return typeName; + } + + public TarantoolType getTarantoolType() { + return tarantoolType; + } + + public JdbcType getJdbcType() { + return jdbcType; + } + + public boolean isSigned() { + return tarantoolType.isSigned(); + } + + public boolean isCaseSensitive() { + return tarantoolType.isCaseSensitive(); + } + + public boolean isTrimmable() { + return jdbcType.isTrimmable(); + } + + public int getPrecision() { + return tarantoolType.getPrecision(); + } + + public int getScale() { + return tarantoolType.getScale(); + } + + public int getDisplaySize() { + return tarantoolType.getDisplaySize(); + } + + @Override + public String toString() { + return "TarantoolSqlType{" + + "tarantoolType=" + tarantoolType + + ", jdbcType=" + jdbcType + + ", typeName='" + typeName + '\'' + + '}'; + } } diff --git a/src/main/java/org/tarantool/jdbc/type/TarantoolType.java b/src/main/java/org/tarantool/jdbc/type/TarantoolType.java index 0d06750d..66197032 100644 --- a/src/main/java/org/tarantool/jdbc/type/TarantoolType.java +++ b/src/main/java/org/tarantool/jdbc/type/TarantoolType.java @@ -6,6 +6,7 @@ public enum TarantoolType { UNKNOWN("unknown", false, false, 0, 0, 0), + DECIMAL("decimal", true, false, 30, 30, 12), BOOLEAN("boolean", false, false, 1, 0, 5), STRING("string", false, true, Integer.MAX_VALUE, 0, Integer.MAX_VALUE), // precision is 20 due to Tarantool integer type has range [-2^63-1..2^64-1] diff --git a/src/main/java/org/tarantool/protocol/ProtoUtils.java b/src/main/java/org/tarantool/protocol/ProtoUtils.java index 51481d38..640d67f3 100644 --- a/src/main/java/org/tarantool/protocol/ProtoUtils.java +++ b/src/main/java/org/tarantool/protocol/ProtoUtils.java @@ -29,306 +29,296 @@ 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}. - * - * @param inputStream ready to use input stream - * @param msgPackLite MessagePack decoder instance - * - * @return Nonnull instance of packet - * - * @throws IOException in case of any io-error - */ - public static TarantoolPacket readPacket(InputStream inputStream, MsgPackLite msgPackLite) throws IOException { - CountInputStreamImpl msgStream = new CountInputStreamImpl(inputStream); - - int size = ((Number) msgPackLite.unpack(msgStream)).intValue(); - long mark = msgStream.getBytesRead(); - - Map headers = (Map) msgPackLite.unpack(msgStream); - - Map body = null; - if (msgStream.getBytesRead() - mark < size) { - body = (Map) msgPackLite.unpack(msgStream); - } - - return new TarantoolPacket(headers, body); + 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}. + * + * @param inputStream ready to use input stream + * @param msgPackLite MessagePack decoder instance + * @return Nonnull instance of packet + * @throws IOException in case of any io-error + */ + public static TarantoolPacket readPacket(InputStream inputStream, MsgPackLite msgPackLite) throws IOException { + CountInputStreamImpl msgStream = new CountInputStreamImpl(inputStream); + + int size = ((Number) msgPackLite.unpack(msgStream)).intValue(); + long mark = msgStream.getBytesRead(); + + Map headers = (Map) msgPackLite.unpack(msgStream); + + Map body = null; + if (msgStream.getBytesRead() - mark < size) { + body = (Map) msgPackLite.unpack(msgStream); } - /** - * Reads a tarantool's binary protocol packet from the reader. - * - * @param bufferReader readable channel that have to be in blocking mode - * or instance of {@link ReadableViaSelectorChannel} - * @param msgPackLite MessagePack decoder instance - * - * @return tarantool binary protocol message wrapped by instance of {@link TarantoolPacket} - * - * @throws IOException if any IO-error occurred during read from the channel - * @throws CommunicationException input stream bytes constitute msg pack message in wrong format - * @throws NonReadableChannelException If this channel was not opened for reading - */ - public static TarantoolPacket readPacket(ReadableByteChannel bufferReader, MsgPackLite msgPackLite) - throws CommunicationException, IOException { - - ByteBuffer buffer = ByteBuffer.allocate(LENGTH_OF_SIZE_MESSAGE); - bufferReader.read(buffer); - - buffer.flip(); - int size = ((Number) msgPackLite.unpack(new ByteBufferBackedInputStream(buffer))).intValue(); - - buffer = ByteBuffer.allocate(size); - bufferReader.read(buffer); - - buffer.flip(); - ByteBufferBackedInputStream msgBytesStream = new ByteBufferBackedInputStream(buffer); - Object unpackedHeaders = msgPackLite.unpack(msgBytesStream); - if (!(unpackedHeaders instanceof Map)) { - //noinspection ConstantConditions - throw new CommunicationException( - "Error while unpacking headers of tarantool response: " + - "expected type Map but was " + - unpackedHeaders != null ? unpackedHeaders.getClass().toString() : "null" - ); - } - //noinspection unchecked (checked above) - Map headers = (Map) unpackedHeaders; - - Map body = null; - if (msgBytesStream.hasAvailable()) { - Object unpackedBody = msgPackLite.unpack(msgBytesStream); - if (!(unpackedBody instanceof Map)) { - //noinspection ConstantConditions - throw new CommunicationException( - "Error while unpacking body of tarantool response: " + - "expected type Map but was " + - unpackedBody != null ? unpackedBody.getClass().toString() : "null" - ); - } - //noinspection unchecked (checked above) - body = (Map) unpackedBody; - } - - return new TarantoolPacket(headers, body); + return new TarantoolPacket(headers, body); + } + + /** + * Reads a tarantool's binary protocol packet from the reader. + * + * @param bufferReader readable channel that have to be in blocking mode or instance of {@link ReadableViaSelectorChannel} + * @param msgPackLite MessagePack decoder instance + * @return tarantool binary protocol message wrapped by instance of {@link TarantoolPacket} + * @throws IOException if any IO-error occurred during read from the channel + * @throws CommunicationException input stream bytes constitute msg pack message in wrong format + * @throws NonReadableChannelException If this channel was not opened for reading + */ + public static TarantoolPacket readPacket(ReadableByteChannel bufferReader, MsgPackLite msgPackLite) + throws CommunicationException, IOException { + + ByteBuffer buffer = ByteBuffer.allocate(LENGTH_OF_SIZE_MESSAGE); + bufferReader.read(buffer); + + buffer.flip(); + int size = ((Number) msgPackLite.unpack(new ByteBufferBackedInputStream(buffer))).intValue(); + + buffer = ByteBuffer.allocate(size); + bufferReader.read(buffer); + + buffer.flip(); + ByteBufferBackedInputStream msgBytesStream = new ByteBufferBackedInputStream(buffer); + Object unpackedHeaders = msgPackLite.unpack(msgBytesStream); + if (!(unpackedHeaders instanceof Map)) { + //noinspection ConstantConditions + throw new CommunicationException( + "Error while unpacking headers of tarantool response: " + + "expected type Map but was " + + unpackedHeaders != null ? unpackedHeaders.getClass().toString() : "null" + ); } - - /** - * 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 msgPackLite MessagePack encoder / decoder instance - * - * @return object with information about a connection/ - * - * @throws IOException in case of any IO fails - * @throws CommunicationException when welcome string is invalid - * @throws TarantoolException in case of failed authentication - */ - public static TarantoolGreeting connect(Socket socket, - String username, - String password, - MsgPackLite msgPackLite) throws IOException { - byte[] inputBytes = new byte[64]; - - InputStream inputStream = socket.getInputStream(); - inputStream.read(inputBytes); - - String firstLine = new String(inputBytes); - assertCorrectWelcome(firstLine, socket.getRemoteSocketAddress()); - String serverVersion = firstLine.substring(WELCOME.length()); - - inputStream.read(inputBytes); - String salt = new String(inputBytes); - if (username != null && password != null) { - ByteBuffer authPacket = createAuthPacket(username, password, salt, msgPackLite); - - OutputStream os = socket.getOutputStream(); - os.write(authPacket.array(), 0, authPacket.remaining()); - os.flush(); - - TarantoolPacket responsePacket = readPacket(socket.getInputStream(), msgPackLite); - assertNoErrCode(responsePacket); - } - - return new TarantoolGreeting(serverVersion); + //noinspection unchecked (checked above) + Map headers = (Map) unpackedHeaders; + + Map body = null; + if (msgBytesStream.hasAvailable()) { + Object unpackedBody = msgPackLite.unpack(msgBytesStream); + if (!(unpackedBody instanceof Map)) { + //noinspection ConstantConditions + throw new CommunicationException( + "Error while unpacking body of tarantool response: " + + "expected type Map but was " + + unpackedBody != null ? unpackedBody.getClass().toString() : "null" + ); + } + //noinspection unchecked (checked above) + body = (Map) unpackedBody; } - /** - * 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 msgPackLite MessagePack encoder / decoder instance - * - * @return object with information about a connection/ - * - * @throws IOException in case of any IO fails - * @throws CommunicationException when welcome string is invalid - * @throws TarantoolException in case of failed authentication - */ - public static TarantoolGreeting connect(SocketChannel channel, - String username, - String password, - MsgPackLite msgPackLite) throws IOException { - ByteBuffer welcomeBytes = ByteBuffer.wrap(new byte[64]); - channel.read(welcomeBytes); - - String firstLine = new String(welcomeBytes.array()); - assertCorrectWelcome(firstLine, channel.getRemoteAddress()); - final String serverVersion = firstLine.substring(WELCOME.length()); - - ((Buffer)welcomeBytes).clear(); - channel.read(welcomeBytes); - String salt = new String(welcomeBytes.array()); - - if (username != null && password != null) { - ByteBuffer authPacket = createAuthPacket(username, password, salt, msgPackLite); - writeFully(channel, authPacket); - - TarantoolPacket authResponse = readPacket(channel, msgPackLite); - assertNoErrCode(authResponse); - } - - return new TarantoolGreeting(serverVersion); + return new TarantoolPacket(headers, body); + } + + /** + * 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 msgPackLite MessagePack encoder / decoder instance + * @return object with information about a connection/ + * @throws IOException in case of any IO fails + * @throws CommunicationException when welcome string is invalid + * @throws TarantoolException in case of failed authentication + */ + public static TarantoolGreeting connect(Socket socket, + String username, + String password, + MsgPackLite msgPackLite) throws IOException { + byte[] inputBytes = new byte[64]; + + InputStream inputStream = socket.getInputStream(); + inputStream.read(inputBytes); + + String firstLine = new String(inputBytes); + assertCorrectWelcome(firstLine, socket.getRemoteSocketAddress()); + String serverVersion = firstLine.substring(WELCOME.length()); + + inputStream.read(inputBytes); + String salt = new String(inputBytes); + if (username != null && password != null) { + ByteBuffer authPacket = createAuthPacket(username, password, salt, msgPackLite); + + OutputStream os = socket.getOutputStream(); + os.write(authPacket.array(), 0, authPacket.remaining()); + os.flush(); + + TarantoolPacket responsePacket = readPacket(socket.getInputStream(), msgPackLite); + assertNoErrCode(responsePacket); } - 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 new TarantoolGreeting(serverVersion); + } + + /** + * 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 msgPackLite MessagePack encoder / decoder instance + * @return object with information about a connection/ + * @throws IOException in case of any IO fails + * @throws CommunicationException when welcome string is invalid + * @throws TarantoolException in case of failed authentication + */ + public static TarantoolGreeting connect(SocketChannel channel, + String username, + String password, + MsgPackLite msgPackLite) throws IOException { + ByteBuffer welcomeBytes = ByteBuffer.wrap(new byte[64]); + channel.read(welcomeBytes); + + String firstLine = new String(welcomeBytes.array()); + assertCorrectWelcome(firstLine, channel.getRemoteAddress()); + final String serverVersion = firstLine.substring(WELCOME.length()); + + ((Buffer) welcomeBytes).clear(); + channel.read(welcomeBytes); + String salt = new String(welcomeBytes.array()); + + if (username != null && password != null) { + ByteBuffer authPacket = createAuthPacket(username, password, salt, msgPackLite); + writeFully(channel, authPacket); + + TarantoolPacket authResponse = readPacket(channel, msgPackLite); + assertNoErrCode(authResponse); } - private static void assertNoErrCode(TarantoolPacket authResponse) { - Long code = (Long) authResponse.getHeaders().get(Key.CODE.getId()); - if (code != 0) { - Object error = authResponse.getError(); - String errorMsg = error instanceof String ? (String) error : new String((byte[]) error); - throw new TarantoolException(code, errorMsg); - } - } + return new TarantoolGreeting(serverVersion); + } - public static void writeFully(OutputStream stream, ByteBuffer buffer) throws IOException { - stream.write(buffer.array()); - stream.flush(); + 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")); } - - public static void writeFully(SocketChannel channel, ByteBuffer buffer) throws IOException { - long code = 0; - while (buffer.remaining() > 0 && (code = channel.write(buffer)) > -1) { - } - if (code < 0) { - throw new SocketException("write failed code: " + code); - } + } + + private static void assertNoErrCode(TarantoolPacket authResponse) { + Long code = (Long) authResponse.getHeaders().get(Key.CODE.getId()); + if (code != 0) { + Object error = authResponse.getError(); + String errorMsg = error instanceof String ? (String) error : new String((byte[]) error); + throw new TarantoolException(code, errorMsg); } + } - public static ByteBuffer createAuthPacket(String username, - final String password, - String salt, - MsgPackLite msgPackLite) throws IOException { - final MessageDigest sha1; - try { - sha1 = MessageDigest.getInstance("SHA-1"); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } - List auth = new ArrayList(2); - auth.add("chap-sha1"); - - byte[] p = sha1.digest(password.getBytes()); - - sha1.reset(); - byte[] p2 = sha1.digest(p); - - sha1.reset(); - sha1.update(Base64.decode(salt), 0, 20); - sha1.update(p2); - byte[] scramble = sha1.digest(); - for (int i = 0, e = 20; i < e; i++) { - p[i] ^= scramble[i]; - } - auth.add(p); - - return createPacket( - DEFAULT_INITIAL_REQUEST_SIZE, msgPackLite, - Code.AUTH, 0L, null, Key.USER_NAME, username, Key.TUPLE, auth - ); - } + public static void writeFully(OutputStream stream, ByteBuffer buffer) throws IOException { + stream.write(buffer.array()); + stream.flush(); + } - public static ByteBuffer createPacket(MsgPackLite msgPackLite, - Code code, - Long syncId, - Long schemaId, - Object... args) throws IOException { - return createPacket(DEFAULT_INITIAL_REQUEST_SIZE, msgPackLite, code, syncId, schemaId, args); + public static void writeFully(SocketChannel channel, ByteBuffer buffer) throws IOException { + long code = 0; + while (buffer.remaining() > 0 && (code = channel.write(buffer)) > -1) { } - - public static ByteBuffer createPacket(int initialRequestSize, - MsgPackLite msgPackLite, - Code code, - Long syncId, - Long schemaId, - Object... args) throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(initialRequestSize); - bos.write(new byte[5]); - final DataOutputStream ds = new DataOutputStream(bos); - Map header = new EnumMap<>(Key.class); - Map body = new EnumMap<>(Key.class); - header.put(Key.CODE, code); - header.put(Key.SYNC, syncId); - if (schemaId != null) { - header.put(Key.SCHEMA_ID, schemaId); - } - if (args != null) { - for (int i = 0, e = args.length; i < e; i += 2) { - Object value = args[i + 1]; - body.put((Key) args[i], value); - } - } - msgPackLite.pack(header, ds); - msgPackLite.pack(body, ds); - ds.flush(); - ByteBuffer buffer = bos.toByteBuffer(); - buffer.put(0, (byte) 0xce); - buffer.putInt(1, bos.size() - 5); - return buffer; + if (code < 0) { + throw new SocketException("write failed code: " + code); } - - /** - * Extracts an error code. - * - * @param code in 0x8XXX format - * - * @return actual error code (which is a XXX part) - */ - public static long extractErrorCode(long code) { - if ((code & ProtoConstants.ERROR_TYPE_MARKER) == 0) { - throw new IllegalArgumentException(String.format("Code %h does not follow 0x8XXX format", code)); - } - return (~ProtoConstants.ERROR_TYPE_MARKER & code); + } + + public static ByteBuffer createAuthPacket(String username, + final String password, + String salt, + MsgPackLite msgPackLite) throws IOException { + final MessageDigest sha1; + try { + sha1 = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); } + List auth = new ArrayList(2); + auth.add("chap-sha1"); - private static class ByteArrayOutputStream extends java.io.ByteArrayOutputStream { + byte[] p = sha1.digest(password.getBytes()); - public ByteArrayOutputStream(int size) { - super(size); - } + sha1.reset(); + byte[] p2 = sha1.digest(p); - ByteBuffer toByteBuffer() { - return ByteBuffer.wrap(buf, 0, count); - } + sha1.reset(); + sha1.update(Base64.decode(salt), 0, 20); + sha1.update(p2); + byte[] scramble = sha1.digest(); + for (int i = 0, e = 20; i < e; i++) { + p[i] ^= scramble[i]; + } + auth.add(p); + + return createPacket( + DEFAULT_INITIAL_REQUEST_SIZE, msgPackLite, + Code.AUTH, 0L, null, Key.USER_NAME, username, Key.TUPLE, auth + ); + } + + public static ByteBuffer createPacket(MsgPackLite msgPackLite, + Code code, + Long syncId, + Long schemaId, + Object... args) throws IOException { + return createPacket(DEFAULT_INITIAL_REQUEST_SIZE, msgPackLite, code, syncId, schemaId, args); + } + + public static ByteBuffer createPacket(int initialRequestSize, + MsgPackLite msgPackLite, + Code code, + Long syncId, + Long schemaId, + Object... args) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(initialRequestSize); + bos.write(new byte[5]); + final DataOutputStream ds = new DataOutputStream(bos); + Map header = new EnumMap<>(Key.class); + Map body = new EnumMap<>(Key.class); + header.put(Key.CODE, code); + header.put(Key.SYNC, syncId); + if (schemaId != null) { + header.put(Key.SCHEMA_ID, schemaId); + } + if (args != null) { + for (int i = 0, e = args.length; i < e; i += 2) { + Object value = args[i + 1]; + body.put((Key) args[i], value); + } + } + msgPackLite.pack(header, ds); + msgPackLite.pack(body, ds); + ds.flush(); + ByteBuffer buffer = bos.toByteBuffer(); + buffer.put(0, (byte) 0xce); + buffer.putInt(1, bos.size() - 5); + return buffer; + } + + /** + * Extracts an error code. + * + * @param code in 0x8XXX format + * @return actual error code (which is a XXX part) + */ + public static long extractErrorCode(long code) { + if ((code & ProtoConstants.ERROR_TYPE_MARKER) == 0) { + throw new IllegalArgumentException(String.format("Code %h does not follow 0x8XXX format", code)); + } + return (~ProtoConstants.ERROR_TYPE_MARKER & code); + } + private static class ByteArrayOutputStream extends java.io.ByteArrayOutputStream { + + public ByteArrayOutputStream(int size) { + super(size); + } + + ByteBuffer toByteBuffer() { + return ByteBuffer.wrap(buf, 0, count); } + } + } diff --git a/src/main/java/org/tarantool/schema/TarantoolSpaceMeta.java b/src/main/java/org/tarantool/schema/TarantoolSpaceMeta.java index 0fd4b214..ae00c087 100644 --- a/src/main/java/org/tarantool/schema/TarantoolSpaceMeta.java +++ b/src/main/java/org/tarantool/schema/TarantoolSpaceMeta.java @@ -11,89 +11,92 @@ */ public class TarantoolSpaceMeta { - public static final int VSPACE_ID_FIELD_NUMBER = 0; - public static final int VSPACE_NAME_FIELD_NUMBER = 2; - public static final int VSPACE_ENGINE_FIELD_NUMBER = 3; - public static final int VSPACE_FORMAT_FIELD_NUMBER = 6; + public static final int VSPACE_ID_FIELD_NUMBER = 0; + public static final int VSPACE_NAME_FIELD_NUMBER = 2; + public static final int VSPACE_ENGINE_FIELD_NUMBER = 3; + public static final int VSPACE_FORMAT_FIELD_NUMBER = 6; + + private final int id; + private final String name; + private final String engine; + private final List format; + private final Map indexes; + + public static TarantoolSpaceMeta fromTuple(List spaceTuple, List> indexTuples) { + List fields = ((List>) spaceTuple.get(VSPACE_FORMAT_FIELD_NUMBER)).stream() + .map(field -> new SpaceField(field.get("name").toString(), getType(field))) + .collect(Collectors.toList()); + Map indexesMap = indexTuples.stream() + .map(TarantoolIndexMeta::fromTuple) + .collect(Collectors.toMap(TarantoolIndexMeta::getName, Function.identity())); + + return new TarantoolSpaceMeta( + (Integer) spaceTuple.get(VSPACE_ID_FIELD_NUMBER), + spaceTuple.get(VSPACE_NAME_FIELD_NUMBER).toString(), + spaceTuple.get(VSPACE_ENGINE_FIELD_NUMBER).toString(), + Collections.unmodifiableList(fields), + Collections.unmodifiableMap(indexesMap) + ); + } + + private static String getType(Map field) { + return field.get("type").toString(); + } + + public TarantoolSpaceMeta(int id, + String name, + String engine, + List format, + Map indexes) { + this.id = id; + this.name = name; + this.engine = engine; + this.format = format; + this.indexes = indexes; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public String getEngine() { + return engine; + } + + public List getFormat() { + return format; + } + + public Map getIndexes() { + return indexes; + } + + public TarantoolIndexMeta getIndex(String indexName) { + return indexes.get(indexName); + } + + public static class SpaceField { - private final int id; private final String name; - private final String engine; - private final List format; - private final Map indexes; - - public static TarantoolSpaceMeta fromTuple(List spaceTuple, List> indexTuples) { - List fields = ((List>) spaceTuple.get(VSPACE_FORMAT_FIELD_NUMBER)).stream() - .map(field -> new SpaceField(field.get("name").toString(), field.get("type").toString())) - .collect(Collectors.toList()); - - Map indexesMap = indexTuples.stream() - .map(TarantoolIndexMeta::fromTuple) - .collect(Collectors.toMap(TarantoolIndexMeta::getName, Function.identity())); - - return new TarantoolSpaceMeta( - (Integer) spaceTuple.get(VSPACE_ID_FIELD_NUMBER), - spaceTuple.get(VSPACE_NAME_FIELD_NUMBER).toString(), - spaceTuple.get(VSPACE_ENGINE_FIELD_NUMBER).toString(), - Collections.unmodifiableList(fields), - Collections.unmodifiableMap(indexesMap) - ); - } - - public TarantoolSpaceMeta(int id, - String name, - String engine, - List format, - Map indexes) { - this.id = id; - this.name = name; - this.engine = engine; - this.format = format; - this.indexes = indexes; - } + private final String type; - public int getId() { - return id; + public SpaceField(String name, String type) { + this.name = name; + this.type = type; } public String getName() { - return name; - } - - public String getEngine() { - return engine; - } - - public List getFormat() { - return format; + return name; } - public Map getIndexes() { - return indexes; + public String getType() { + return type; } - public TarantoolIndexMeta getIndex(String indexName) { - return indexes.get(indexName); - } - - public static class SpaceField { - - private final String name; - private final String type; - - public SpaceField(String name, String type) { - this.name = name; - this.type = type; - } - - public String getName() { - return name; - } - - public String getType() { - return type; - } - - } + } } diff --git a/src/main/java/org/tarantool/util/NodeSpec.java b/src/main/java/org/tarantool/util/NodeSpec.java new file mode 100644 index 00000000..a2c46730 --- /dev/null +++ b/src/main/java/org/tarantool/util/NodeSpec.java @@ -0,0 +1,28 @@ +package org.tarantool.util; + +/** + * Tarantool instance container. + */ +public class NodeSpec { + + private final String host; + private final Integer port; + + public NodeSpec(String host, Integer port) { + this.host = host; + this.port = port; + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + @Override + public String toString() { + return host + (port != null ? ":" + port : ""); + } +} \ No newline at end of file diff --git a/src/main/java/org/tarantool/utils/LocalLogger.java b/src/main/java/org/tarantool/utils/LocalLogger.java new file mode 100644 index 00000000..82d602ff --- /dev/null +++ b/src/main/java/org/tarantool/utils/LocalLogger.java @@ -0,0 +1,30 @@ +package org.tarantool.utils; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class LocalLogger { + + private static final String LOG_FILE_PATH = "C:/vtb/driver_log.txt"; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * Логгирует сообщение в файл C:/vtb/driver_log.txt. + * + * @param message Сообщение для логгирования + */ + public static void log(String message) { + try (FileWriter fileWriter = new FileWriter(LOG_FILE_PATH, true); // Открываем файл в режиме добавления + PrintWriter printWriter = new PrintWriter(fileWriter)) { + + String timestamp = LocalDateTime.now().format(DATE_FORMATTER); + printWriter.printf("[%s] %s%n", timestamp, message); + + } catch (IOException e) { + System.err.printf("Не удалось записать лог: %s%n", e.getMessage()); + } + } +} diff --git a/src/main/java/org/tarantool/utils/PreparedStatementConverter.java b/src/main/java/org/tarantool/utils/PreparedStatementConverter.java new file mode 100644 index 00000000..97ec59ac --- /dev/null +++ b/src/main/java/org/tarantool/utils/PreparedStatementConverter.java @@ -0,0 +1,246 @@ +// file: src/main/java/org/tarantool/utils/PreparedStatementConverter.java +package org.tarantool.utils; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.expression.BinaryExpression; +import net.sf.jsqlparser.expression.DateValue; +import net.sf.jsqlparser.expression.DoubleValue; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.JdbcParameter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.NullValue; +import net.sf.jsqlparser.expression.Parenthesis; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.TimeValue; +import net.sf.jsqlparser.expression.TimestampValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.update.Update; +import org.tarantool.jdbc.SQLQueryHolder; + +public class PreparedStatementConverter { + + /** + * Converts a SQL query into a parameterized format suitable for prepared statements. + * If the query already contains '?', returns it unchanged. + * Otherwise, it replaces literal values with '?' and collects them into params. + */ + public static SQLQueryHolder convertSqlToPreparedStatementFormat(String sql) { + List params = new ArrayList<>(); + LocalLogger.log("[PreparedStatementConverter] Original SQL: " + sql); + try { + Statement statement = CCJSqlParserUtil.parse(sql); + LocalLogger.log("[PreparedStatementConverter] Parsed Statement: " + statement.getClass().getSimpleName()); + + if (statement instanceof Insert) { + LocalLogger.log("[PreparedStatementConverter] Processing INSERT statement."); + Insert insert = (Insert) statement; + processInsertStatement(insert, params); + String modifiedSql = insert.toString(); + LocalLogger.log("[PreparedStatementConverter] Modified SQL: " + modifiedSql); + LocalLogger.log("[PreparedStatementConverter] Parameters: " + params); + return SQLQueryHolder.of(modifiedSql, params.toArray()); + } else if (statement instanceof Update) { + LocalLogger.log("[PreparedStatementConverter] Processing UPDATE statement."); + Update update = (Update) statement; + processUpdateStatement(update, params); + String modifiedSql = update.toString(); + LocalLogger.log("[PreparedStatementConverter] Modified SQL: " + modifiedSql); + LocalLogger.log("[PreparedStatementConverter] Parameters: " + params); + return SQLQueryHolder.of(modifiedSql, params.toArray()); + } else if (statement instanceof Delete) { + LocalLogger.log("[PreparedStatementConverter] Processing DELETE statement."); + Delete delete = (Delete) statement; + processDeleteStatement(delete, params); + String modifiedSql = delete.toString(); + LocalLogger.log("[PreparedStatementConverter] Modified SQL: " + modifiedSql); + LocalLogger.log("[PreparedStatementConverter] Parameters: " + params); + return SQLQueryHolder.of(modifiedSql, params.toArray()); + } else if (statement instanceof Select) { + LocalLogger.log("[PreparedStatementConverter] Processing SELECT statement."); + Select select = (Select) statement; + processSelectStatement(select, params); + String modifiedSql = select.toString(); + LocalLogger.log("[PreparedStatementConverter] Modified SQL: " + modifiedSql); + LocalLogger.log("[PreparedStatementConverter] Parameters: " + params); + return SQLQueryHolder.of(modifiedSql, params.toArray()); + } else { + throw new IllegalArgumentException( + "Unsupported SQL command. Only INSERT, UPDATE, DELETE, SELECT are supported." + ); + } + } catch (Exception e) { + LocalLogger.log("[PreparedStatementConverter] Error parsing SQL: " + e.getMessage()); + e.printStackTrace(); // Рассмотрите возможность использования метода логгера для стека ошибок + return SQLQueryHolder.of(sql); + } + } + + /* -------------------------------------------------------------------------- + INSERT + -------------------------------------------------------------------------- */ + private static void processInsertStatement(Insert insert, List params) { + LocalLogger.log("[PreparedStatementConverter] Entering processInsertStatement."); + if (insert.getItemsList() instanceof net.sf.jsqlparser.expression.operators.relational.ExpressionList) { + net.sf.jsqlparser.expression.operators.relational.ExpressionList exprList = + (net.sf.jsqlparser.expression.operators.relational.ExpressionList) insert.getItemsList(); + List expressions = exprList.getExpressions(); + LocalLogger.log("[PreparedStatementConverter] INSERT expressions count: " + expressions.size()); + + for (int i = 0; i < expressions.size(); i++) { + Expression expr = expressions.get(i); + if (isLiteralValue(expr)) { + Object value = extractValue(expr); + params.add(value); + expressions.set(i, new JdbcParameter()); + } + } + } else { + throw new IllegalArgumentException("Unsupported INSERT items list type."); + } + LocalLogger.log("[PreparedStatementConverter] Exiting processInsertStatement."); + } + + /* -------------------------------------------------------------------------- + UPDATE + -------------------------------------------------------------------------- */ + private static void processUpdateStatement(Update update, List params) { + LocalLogger.log("[PreparedStatementConverter] Entering processUpdateStatement."); + List expressions = update.getExpressions(); + LocalLogger.log("[PreparedStatementConverter] UPDATE expressions count: " + expressions.size()); + + for (int i = 0; i < expressions.size(); i++) { + Expression expr = expressions.get(i); + if (isLiteralValue(expr)) { + Object value = extractValue(expr); + params.add(value); + expressions.set(i, new JdbcParameter()); + } + } + + Expression where = update.getWhere(); + if (where != null) { + update.setWhere(processExpression(where, params)); + } + LocalLogger.log("[PreparedStatementConverter] Exiting processUpdateStatement."); + } + + /* -------------------------------------------------------------------------- + DELETE + -------------------------------------------------------------------------- */ + private static void processDeleteStatement(Delete delete, List params) { + LocalLogger.log("[PreparedStatementConverter] Entering processDeleteStatement."); + Expression where = delete.getWhere(); + if (where != null) { + delete.setWhere(processExpression(where, params)); + } + LocalLogger.log("[PreparedStatementConverter] Exiting processDeleteStatement."); + } + + /* -------------------------------------------------------------------------- + SELECT + -------------------------------------------------------------------------- */ + private static void processSelectStatement(Select select, List params) { + LocalLogger.log("[PreparedStatementConverter] Entering processSelectStatement."); + if (select.getSelectBody() instanceof PlainSelect) { + PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); + Expression where = plainSelect.getWhere(); + if (where != null) { + plainSelect.setWhere(processExpression(where, params)); + } + } + LocalLogger.log("[PreparedStatementConverter] Exiting processSelectStatement."); + } + + /* -------------------------------------------------------------------------- + Common expression processor + -------------------------------------------------------------------------- */ + private static Expression processExpression(Expression expr, List params) { + if (expr instanceof BinaryExpression) { + BinaryExpression binaryExpr = (BinaryExpression) expr; + binaryExpr.setLeftExpression(processExpression(binaryExpr.getLeftExpression(), params)); + binaryExpr.setRightExpression(processExpression(binaryExpr.getRightExpression(), params)); + return binaryExpr; + } else if (expr instanceof Parenthesis) { + Parenthesis parenExpr = (Parenthesis) expr; + parenExpr.setExpression(processExpression(parenExpr.getExpression(), params)); + return parenExpr; + } else if (isLiteralValue(expr)) { + Object value = extractValue(expr); + params.add(value); + return new JdbcParameter(); + } else { + return expr; + } + } + + /** + * Determines if the expression should be treated as a literal value. + */ + private static boolean isLiteralValue(Expression expr) { + boolean isLiteral = expr instanceof LongValue || + expr instanceof StringValue || + expr instanceof NullValue || + expr instanceof DoubleValue || + expr instanceof TimestampValue || + expr instanceof DateValue || + expr instanceof TimeValue || + // Обрабатываем булевые литералы, представленные как Column("true") или Column("false") + isBooleanLiteral(expr); + return isLiteral; + } + + /** + * Checks if the expression is a boolean literal (true/false) + * при виде "Column('false')" вместо нативного BooleanValue(false). + */ + private static boolean isBooleanLiteral(Expression expr) { + if (expr instanceof Column) { + String columnName = ((Column) expr).getColumnName(); + // Удаляем кавычки, если они присутствуют + if (columnName.startsWith("\"") && columnName.endsWith("\"") && columnName.length() > 2) { + columnName = columnName.substring(1, columnName.length() - 1); + } + return "true".equalsIgnoreCase(columnName) || "false".equalsIgnoreCase(columnName); + } + return false; + } + + /** + * Extracts the Java value from supported expression types. + */ + private static Object extractValue(Expression expr) { + if (expr instanceof LongValue) { + return ((LongValue) expr).getValue(); + } else if (expr instanceof StringValue) { + return ((StringValue) expr).getValue(); + } else if (expr instanceof NullValue) { + return null; + } else if (expr instanceof DoubleValue) { + return ((DoubleValue) expr).getValue(); + } else if (expr instanceof TimestampValue) { + return ((TimestampValue) expr).getValue(); + } else if (expr instanceof DateValue) { + return ((DateValue) expr).getValue(); + } else if (expr instanceof TimeValue) { + return ((TimeValue) expr).getValue(); + } else if (isBooleanLiteral(expr)) { + String columnName = ((Column) expr).getColumnName(); + // Удаляем кавычки, если они присутствуют + if (columnName.startsWith("\"") && columnName.endsWith("\"") && columnName.length() > 2) { + columnName = columnName.substring(1, columnName.length() - 1); + } + return Boolean.parseBoolean(columnName.toLowerCase()); + } else { + throw new IllegalArgumentException( + "Unsupported expression type: " + expr.getClass() + ); + } + } +} diff --git a/src/main/java/org/tarantool/utils/QuoteWrapper.java b/src/main/java/org/tarantool/utils/QuoteWrapper.java new file mode 100644 index 00000000..5279e05f --- /dev/null +++ b/src/main/java/org/tarantool/utils/QuoteWrapper.java @@ -0,0 +1,398 @@ +package org.tarantool.utils; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.*; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.expression.operators.relational.LikeExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.*; +import net.sf.jsqlparser.statement.*; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.insert.*; +import net.sf.jsqlparser.statement.select.*; +import net.sf.jsqlparser.statement.update.Update; + +public class QuoteWrapper { + + /** + * Method to add double quotes around table and column names, remove aliases from UPDATE, DELETE, and INSERT statements, + * and replace LIKE ? ESCAPE '#' with = ? in SELECT, UPDATE, and DELETE statements. + * + * @param sql The original SQL query. + * @return The modified SQL query. + */ + public static String addQuotesForSpaces(String sql) { + try { + LocalLogger.log("sql = " + sql); + if (sql == null || sql.trim().isEmpty()) { + return sql; + } + + // Parse the SQL statement + Statement statement = CCJSqlParserUtil.parse(sql); + + // Process based on the type of statement + if (statement instanceof Select) { + processSelect((Select) statement); + } else if (statement instanceof Insert) { + processInsert((Insert) statement); + } else if (statement instanceof Update) { + processUpdate((Update) statement); + } else if (statement instanceof Delete) { + processDelete((Delete) statement); + } + + // Return the modified SQL statement + return statement.toString(); + } catch (JSQLParserException e) { + throw new RuntimeException("Failed to parse SQL: " + sql, e); + } + } + + /** + * Processes SELECT statements to wrap table and column names in double quotes, + * and replace LIKE ? ESCAPE '#' with = ?. + * + * @param select The SELECT statement. + */ + private static void processSelect(Select select) { + SelectBody selectBody = select.getSelectBody(); + + selectBody.accept(new SelectVisitorAdapter() { + @Override + public void visit(PlainSelect plainSelect) { + // Process FROM item + wrapFromItem(plainSelect.getFromItem()); + + // Process JOINs + if (plainSelect.getJoins() != null) { + for (Join join : plainSelect.getJoins()) { + wrapFromItem(join.getRightItem()); + if (join.getOnExpression() != null) { + Expression newOn = processExpression(join.getOnExpression()); + join.setOnExpression(newOn); + } + } + } + + // Process SELECT items + if (plainSelect.getSelectItems() != null) { + for (SelectItem item : plainSelect.getSelectItems()) { + item.accept(new SelectItemVisitorAdapter() { + @Override + public void visit(SelectExpressionItem selectExpressionItem) { + Expression expr = processExpression(selectExpressionItem.getExpression()); + selectExpressionItem.setExpression(expr); + } + }); + } + } + + // Process WHERE clause + if (plainSelect.getWhere() != null) { + Expression newWhere = processExpression(plainSelect.getWhere()); + plainSelect.setWhere(newWhere); + } + + // Process ORDER BY elements + if (plainSelect.getOrderByElements() != null) { + for (OrderByElement orderByElement : plainSelect.getOrderByElements()) { + Expression expr = processExpression(orderByElement.getExpression()); + orderByElement.setExpression(expr); + } + } + } + }); + } + + /** + * Processes INSERT statements to wrap table and column names in double quotes and remove aliases. + * + * @param insert The INSERT statement. + */ + private static void processInsert(Insert insert) { + // Wrap table name and remove alias + wrapTableName(insert.getTable(), true); + + // Wrap column names + if (insert.getColumns() != null) { + for (Column column : insert.getColumns()) { + wrapColumnName(column); + } + } + + // Process SELECT part if present (INSERT INTO ... SELECT ...) + if (insert.getSelect() != null) { + processSelect(insert.getSelect()); + } + } + + /** + * Processes UPDATE statements to wrap table and column names in double quotes, remove aliases, + * and replace LIKE ? ESCAPE '#' with = ? in WHERE clause. + * + * @param update The UPDATE statement. + */ + private static void processUpdate(Update update) { + // Wrap table name and remove alias + wrapTableName(update.getTable(), true); + + // Wrap column names in SET expressions + if (update.getColumns() != null) { + for (Column column : update.getColumns()) { + wrapColumnName(column); + } + } + + // Wrap expressions in SET expressions + if (update.getExpressions() != null) { + for (int i = 0; i < update.getExpressions().size(); i++) { + Expression expr = update.getExpressions().get(i); + Expression newExpr = processExpression(expr); + update.getExpressions().set(i, newExpr); + } + } + + // Process WHERE clause + if (update.getWhere() != null) { + Expression newWhere = processExpression(update.getWhere()); + update.setWhere(newWhere); + } + } + + /** + * Processes DELETE statements to wrap table and column names in double quotes, remove aliases, + * and replace LIKE ? ESCAPE '#' with = ? in WHERE clause. + * + * @param delete The DELETE statement. + */ + private static void processDelete(Delete delete) { + // Wrap table name and remove alias + wrapTableName(delete.getTable(), true); + + // Process WHERE clause + if (delete.getWhere() != null) { + Expression newWhere = processExpression(delete.getWhere()); + delete.setWhere(newWhere); + } + } + + /** + * Wraps FROM items in SELECT statements. + * + * @param fromItem The FROM item. + */ + private static void wrapFromItem(FromItem fromItem) { + if (fromItem instanceof Table) { + wrapTableName((Table) fromItem, false); + } else if (fromItem instanceof SubSelect) { + SubSelect subSelect = (SubSelect) fromItem; + if (subSelect.getSelectBody() != null) { + processSelect(new Select() {{ + setSelectBody(subSelect.getSelectBody()); + }}); + } + } + // Handle other FromItem types if necessary + } + + /** + * Wraps table name in double quotes and optionally removes aliases. + * + * @param table The table. + * @param removeAlias Whether to remove alias. + */ + private static void wrapTableName(Table table, boolean removeAlias) { + if (table == null) { + return; + } + + String tableName = stripQuotes(table.getName()); + table.setName("\"" + tableName + "\""); + + if (removeAlias) { + table.setAlias(null); + } + } + + /** + * Wraps column name in double quotes and removes table references from columns. + * + * @param column The column. + */ + private static void wrapColumnName(Column column) { + if (column == null) { + return; + } + + String columnName = stripQuotes(column.getColumnName()); + column.setColumnName("\"" + columnName + "\""); + column.setTable(null); // Remove table references + } + + /** + * Processes expressions by visiting their components, wrapping column names, + * and replacing LIKE ? ESCAPE '#' with = ?. + * + * @param expression The expression. + * @return The processed (and possibly modified) expression. + */ + private static Expression processExpression(Expression expression) { + if (expression == null) { + return null; + } + + if (expression instanceof LikeExpression) { + LikeExpression likeExpr = (LikeExpression) expression; + // Process left and right expressions + Expression leftExpr = processExpression(likeExpr.getLeftExpression()); + Expression rightExpr = processExpression(likeExpr.getRightExpression()); + likeExpr.setLeftExpression(leftExpr); + likeExpr.setRightExpression(rightExpr); + + // Check if escape character is '#' + String escape = likeExpr.getEscape().toString(); + if (escape != null) { + escape = stripQuotes(escape); + } + if ("#".equals(escape)) { + // Create EqualsTo expression + EqualsTo equalsTo = new EqualsTo(); + equalsTo.setLeftExpression(leftExpr); + equalsTo.setRightExpression(rightExpr); + return equalsTo; + } + return likeExpr; + } else if (expression instanceof BinaryExpression) { + BinaryExpression binaryExpr = (BinaryExpression) expression; + Expression leftExpr = processExpression(binaryExpr.getLeftExpression()); + Expression rightExpr = processExpression(binaryExpr.getRightExpression()); + binaryExpr.setLeftExpression(leftExpr); + binaryExpr.setRightExpression(rightExpr); + return binaryExpr; + } else if (expression instanceof Parenthesis) { + Parenthesis parenthesis = (Parenthesis) expression; + Expression innerExpr = processExpression(parenthesis.getExpression()); + parenthesis.setExpression(innerExpr); + return parenthesis; + } else if (expression instanceof Column) { + wrapColumnName((Column) expression); + return expression; + } else { + // Process any nested expressions + expression.accept(new ExpressionVisitorAdapter() { + @Override + public void visit(Column column) { + wrapColumnName(column); + } + }); + return expression; + } + } + + /** + * Removes existing quotes from an identifier. + * + * @param name The identifier name. + * @return The unquoted identifier name. + */ + private static String stripQuotes(String name) { + if (name == null || name.isEmpty()) { + return name; + } + if ((name.startsWith("\"") && name.endsWith("\"")) || + (name.startsWith("`") && name.endsWith("`")) || + (name.startsWith("'") && name.endsWith("'"))) { + return name.substring(1, name.length() - 1); + } + return name; + } + + /** + * Main method containing test cases for the QuoteWrapper class. + * + * @param args Command-line arguments. + */ + public static void main(String[] args) { + // Test Case 1: Simple SELECT + String sql1 = "SELECT id, name, age FROM users"; + String expected1 = "SELECT \"id\", \"name\", \"age\" FROM \"users\""; + String result1 = addQuotesForSpaces(sql1); + assert result1.equals(expected1) : "Test Case 1 Failed"; + + // Test Case 2: SELECT with ORDER BY + String sql2 = "SELECT id, name FROM employees ORDER BY name DESC, id ASC"; + String expected2 = "SELECT \"id\", \"name\" FROM \"employees\" ORDER BY \"name\" DESC, \"id\" ASC"; + String result2 = addQuotesForSpaces(sql2); + assert result2.equals(expected2) : "Test Case 2 Failed"; + + // Test Case 3: SELECT with LIKE ? ESCAPE '#' + String sql3 = "SELECT * FROM products WHERE description LIKE ? ESCAPE '#'"; + String expected3 = "SELECT * FROM \"products\" WHERE \"description\" = ?"; + String result3 = addQuotesForSpaces(sql3); + assert result3.equals(expected3) : "Test Case 3 Failed"; + + // Test Case 4: INSERT Statement with Aliases + String sql4 = "INSERT INTO orders (order_id, customer_name) VALUES (?, ?)"; + String expected4 = "INSERT INTO \"orders\" (\"order_id\", \"customer_name\") VALUES (?, ?)"; + String result4 = addQuotesForSpaces(sql4); + assert result4.equals(expected4) : "Test Case 4 Failed"; + + // Test Case 5: UPDATE Statement with Aliases and WHERE clause + String sql5 = "UPDATE employees e SET e.salary = ? WHERE e.id = ?"; + String expected5 = "UPDATE \"employees\" SET \"salary\" = ? WHERE \"id\" = ?"; + String result5 = addQuotesForSpaces(sql5); + assert result5.equals(expected5) : "Test Case 5 Failed"; + + // Test Case 6: DELETE Statement with WHERE clause + String sql6 = "DELETE FROM sessions s WHERE s.user_id = ?"; + String expected6 = "DELETE FROM \"sessions\" WHERE \"user_id\" = ?"; + String result6 = addQuotesForSpaces(sql6); + assert result6.equals(expected6) : "Test Case 6 Failed"; + + // Test Case 7: SELECT with JOIN and LIKE ? ESCAPE '#' + String sql7 = "SELECT u.id, u.name, o.order_date FROM users u JOIN orders o ON u.id = o.user_id WHERE o.description LIKE ? ESCAPE '#'"; + String expected7 = "SELECT \"id\", \"name\", \"order_date\" FROM \"users\" JOIN \"orders\" ON \"id\" = \"user_id\" WHERE \"description\" = ?"; + String result7 = addQuotesForSpaces(sql7); + assert result7.equals(expected7) : "Test Case 7 Failed"; + + // Test Case 8: NULL SQL + String sql8 = null; + String expected8 = null; + String result8 = addQuotesForSpaces(sql8); + assert result8 == expected8 : "Test Case 8 Failed"; + + // Test Case 9: Empty SQL + String sql9 = " "; + String expected9 = " "; + String result9 = addQuotesForSpaces(sql9); + assert result9.equals(expected9) : "Test Case 9 Failed"; + + // Test Case 10: Complex WHERE Clause with Multiple Conditions + String sql10 = "SELECT * FROM inventory WHERE category = ? AND name LIKE ? ESCAPE '#' OR quantity > ?"; + String expected10 = "SELECT * FROM \"inventory\" WHERE \"category\" = ? AND \"name\" = ? OR \"quantity\" > ?"; + String result10 = addQuotesForSpaces(sql10); + assert result10.equals(expected10) : "Test Case 10 Failed"; + + // Test Case 11: UPDATE with LIKE ? ESCAPE '#' + String sql11 = "UPDATE products SET name = ? WHERE description LIKE ? ESCAPE '#'"; + String expected11 = "UPDATE \"products\" SET \"name\" = ? WHERE \"description\" = ?"; + String result11 = addQuotesForSpaces(sql11); + assert result11.equals(expected11) : "Test Case 11 Failed"; + + // Test Case 12: DELETE with LIKE ? ESCAPE '#' + String sql12 = "DELETE FROM orders WHERE order_code LIKE ? ESCAPE '#'"; + String expected12 = "DELETE FROM \"orders\" WHERE \"order_code\" = ?"; + String result12 = addQuotesForSpaces(sql12); + assert result12.equals(expected12) : "Test Case 12 Failed"; + + // Test Case 13: UPDATE with multiple LIKE ? ESCAPE '#' conditions + String sql13 = "UPDATE security_type SET version = ? WHERE id = ? AND bucket_id = ? AND version LIKE ? ESCAPE '#' AND name LIKE ? ESCAPE '#' AND type LIKE ? ESCAPE '#' AND cat_name IS NULL"; + String expected13 = "UPDATE \"security_type\" SET \"version\" = ? WHERE \"id\" = ? AND \"bucket_id\" = ? AND \"version\" = ? AND \"name\" = ? AND \"type\" = ? AND \"cat_name\" IS NULL"; + String result13 = addQuotesForSpaces(sql13); + assert result13.equals(expected13) : "Test Case 13 Failed"; + + System.out.println("All test cases passed successfully!"); + } + +} diff --git a/src/main/java/org/tarantool/utils/SQLParameterMapper.java b/src/main/java/org/tarantool/utils/SQLParameterMapper.java new file mode 100644 index 00000000..0f5fe12e --- /dev/null +++ b/src/main/java/org/tarantool/utils/SQLParameterMapper.java @@ -0,0 +1,242 @@ +package org.tarantool.utils; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import net.sf.jsqlparser.expression.BinaryExpression; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.JdbcParameter; +import net.sf.jsqlparser.expression.NullValue; +import net.sf.jsqlparser.expression.Parenthesis; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.update.Update; + +/** + * A utility class for mapping SQL parameters. + */ +public class SQLParameterMapper { + + /** + * Extracts parameters from an SQL query (INSERT, UPDATE, DELETE, SELECT) and returns a Map where the key + * is the parameter (column) name without quotes, and the value is a list of its positions. + * + * @param sqlQuery SQL query of type INSERT, UPDATE, DELETE, or SELECT + * @return Map> with parameter names and their positions + */ + public static Map> mapParameters(String sqlQuery) { + Map> paramMap = new LinkedHashMap<>(); + try { + // Preprocess the SQL to replace "" with '' (empty string) + sqlQuery = sqlQuery.replaceAll("\"\"", "''"); + + // Parse the SQL query + Statement statement = CCJSqlParserUtil.parse(sqlQuery); + + if (statement instanceof Insert) { + handleInsert((Insert) statement, paramMap); + } else if (statement instanceof Update) { + handleUpdate((Update) statement, paramMap); + } else if (statement instanceof Delete) { + handleDelete((Delete) statement, paramMap); + } else if (statement instanceof Select) { + handleSelect((Select) statement, paramMap); + } else { + throw new IllegalArgumentException("Unsupported SQL command. Only INSERT, UPDATE, DELETE, SELECT are supported."); + } + } catch (Exception e) { + e.printStackTrace(); + } + return paramMap; + } + + /** + * Handles INSERT statements and populates paramMap. + * + * @param insert Insert object from JSqlParser + * @param paramMap Map to populate + */ + private static void handleInsert(Insert insert, Map> paramMap) { + List columns = insert.getColumns(); + if (columns == null || columns.isEmpty()) { + throw new IllegalArgumentException("INSERT statement must specify columns."); + } + + // Extract values from the VALUES part + if (insert.getItemsList() instanceof ExpressionList) { + ExpressionList exprList = (ExpressionList) insert.getItemsList(); + List expressions = exprList.getExpressions(); + + if (columns.size() != expressions.size()) { + throw new IllegalArgumentException("Number of columns and values do not match."); + } + + int paramIndex = 1; + for (int i = 0; i < columns.size(); i++) { + String columnName = stripQuotes(columns.get(i).getColumnName()); + Expression expr = expressions.get(i); + + // Handle different expression types if necessary + if (expr instanceof StringValue) { + // String literals are handled as parameters + } else if (expr instanceof NullValue) { + // NULL values are handled as parameters + } else if (expr instanceof Function) { + // Handle functions like array_create('1231') + // For now, treat them as parameters + } else if (expr instanceof JdbcParameter) { + // JDBC placeholders are already parameters + } else { + // Other expressions are treated as parameters + } + + // Map the column to its parameter index + paramMap.computeIfAbsent(columnName, k -> new ArrayList<>()).add(paramIndex++); + } + } else { + throw new IllegalArgumentException("Unsupported INSERT items list type."); + } + } + + /** + * Handles UPDATE statements and populates paramMap. + * + * @param update Update object from JSqlParser + * @param paramMap Map to populate + */ + private static void handleUpdate(Update update, Map> paramMap) { + List columns = update.getColumns(); + List expressions = update.getExpressions(); + + if (columns.size() != expressions.size()) { + throw new IllegalArgumentException("Number of columns and expressions in SET do not match."); + } + + int paramIndex = 1; + // Handle SET part + for (int i = 0; i < columns.size(); i++) { + String columnName = stripQuotes(columns.get(i).getColumnName()); + Expression expr = expressions.get(i); + + // Handle different expression types if necessary + // Similar to handleInsert method + + // Map the column to its parameter index + paramMap.computeIfAbsent(columnName, k -> new ArrayList<>()).add(paramIndex++); + } + + // Handle WHERE part + Expression where = update.getWhere(); + if (where != null) { + paramIndex = extractColumnsFromExpression(where, paramMap, paramIndex); + } + } + + /** + * Handles DELETE statements and populates paramMap. + * + * @param delete Delete object from JSqlParser + * @param paramMap Map to populate + */ + private static void handleDelete(Delete delete, Map> paramMap) { + Expression where = delete.getWhere(); + if (where != null) { + int paramIndex = 1; + extractColumnsFromExpression(where, paramMap, paramIndex); + } + } + + /** + * Handles SELECT statements and populates paramMap. + * + * @param select Select object from JSqlParser + * @param paramMap Map to populate + */ + private static void handleSelect(Select select, Map> paramMap) { + if (select.getSelectBody() instanceof PlainSelect) { + PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); + + // Handle WHERE + Expression where = plainSelect.getWhere(); + if (where != null) { + int paramIndex = 1; + extractColumnsFromExpression(where, paramMap, paramIndex); + } + + // (OPTIONALLY) Handle HAVING, JOIN conditions, etc. if needed + // Example for HAVING: + /* + Expression having = plainSelect.getHaving(); + if (having != null) { + paramIndex = extractColumnsFromExpression(having, paramMap, paramIndex); + } + */ + } + } + + /** + * Recursively extracts column names from the WHERE expression and maps them to their positions. + * + * @param expr WHERE expression + * @param paramMap Map to populate with column names and positions + * @param paramIndex Current parameter index + * @return Updated parameter index after processing the expression + */ + private static int extractColumnsFromExpression(Expression expr, Map> paramMap, int paramIndex) { + if (expr instanceof BinaryExpression) { + BinaryExpression be = (BinaryExpression) expr; + Expression left = be.getLeftExpression(); + Expression right = be.getRightExpression(); + + boolean leftIsColumn = left instanceof Column; + boolean rightIsColumn = right instanceof Column; + + // Only map the value side of the expression + if (leftIsColumn && !(right instanceof BinaryExpression)) { + String columnName = stripQuotes(((Column) left).getColumnName()); + paramMap.computeIfAbsent(columnName, k -> new ArrayList<>()).add(paramIndex++); + } else if (rightIsColumn && !(left instanceof BinaryExpression)) { + String columnName = stripQuotes(((Column) right).getColumnName()); + paramMap.computeIfAbsent(columnName, k -> new ArrayList<>()).add(paramIndex++); + } + + // Recurse into left and right expressions + paramIndex = extractColumnsFromExpression(left, paramMap, paramIndex); + paramIndex = extractColumnsFromExpression(right, paramMap, paramIndex); + } else if (expr instanceof Parenthesis) { + Parenthesis parenthesis = (Parenthesis) expr; + paramIndex = extractColumnsFromExpression(parenthesis.getExpression(), paramMap, paramIndex); + } else if (expr instanceof Function) { + // Handle function expressions if necessary + // For example, array_create('1231') - treat as a parameter + // You can extract parameters from function arguments if needed + } + // Handle other expression types if necessary + return paramIndex; + } + + /** + * Removes quotes around a column name if present. + * + * @param name Column name with possible quotes + * @return Column name without quotes + */ + private static String stripQuotes(String name) { + if ((name.startsWith("\"") && name.endsWith("\"")) || + (name.startsWith("`") && name.endsWith("`")) || + (name.startsWith("'") && name.endsWith("'"))) { + return name.substring(1, name.length() - 1); + } + return name; + } +} diff --git a/src/main/java/org/tarantool/utils/SpaceNameExtractor.java b/src/main/java/org/tarantool/utils/SpaceNameExtractor.java new file mode 100644 index 00000000..cfbe220a --- /dev/null +++ b/src/main/java/org/tarantool/utils/SpaceNameExtractor.java @@ -0,0 +1,47 @@ +package org.tarantool.utils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SpaceNameExtractor { + + public static String extractSpaceName(String sqlQuery) { + // Убираем лишние пробелы + String normalizedQuery = sqlQuery.trim(); + + // Регулярные выражения для различных SQL-запросов + String selectPattern = "(?i)select\\s+.*\\s+from\\s+([\"`]?\\w+[\"`]?(?:\\.[" + "\"`]?\\w+[\"`]?)?)"; + String insertPattern = "(?i)insert\\s+into\\s+([\"`]?\\w+[\"`]?(?:\\.[" + "\"`]?\\w+[\"`]?)?)"; + String updatePattern = "(?i)update\\s+([\"`]?\\w+[\"`]?(?:\\.[" + "\"`]?\\w+[\"`]?)?)"; + String deletePattern = "(?i)delete\\s+from\\s+([\"`]?\\w+[\"`]?(?:\\.[" + "\"`]?\\w+[\"`]?)?)"; + + // Попробуем найти название таблицы в зависимости от типа SQL-запроса + String tableName = matchTableName(normalizedQuery, selectPattern); + if (tableName == null) { + tableName = matchTableName(normalizedQuery, insertPattern); + } + if (tableName == null) { + tableName = matchTableName(normalizedQuery, updatePattern); + } + if (tableName == null) { + tableName = matchTableName(normalizedQuery, deletePattern); + } + + // Удаляем кавычки, если они присутствуют + if (tableName != null) { + tableName = tableName.replaceAll("[\"`]", ""); + } + + + return tableName; + } + + private static String matchTableName(String query, String regex) { + Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(query); + if (matcher.find()) { + return matcher.group(1); + } + return null; + } +} diff --git a/src/main/java/org/tarantool/utils/TarantoolDecimal.java b/src/main/java/org/tarantool/utils/TarantoolDecimal.java new file mode 100644 index 00000000..3ea10b61 --- /dev/null +++ b/src/main/java/org/tarantool/utils/TarantoolDecimal.java @@ -0,0 +1,60 @@ +package org.tarantool.utils; + + +import java.math.BigDecimal; +import java.math.BigInteger; + +public class TarantoolDecimal { + + /** + * Декодирует массив байт Tarantool DECIMAL в BigDecimal. + * + * @param data байтовый массив, полученный из MsgPack EXT type=1 + * @return BigDecimal представление числа + */ + public static BigDecimal decodeDecimal(byte[] data) { + if (data.length < 2) { + throw new IllegalArgumentException("Invalid decimal data length"); + } + + // Читаем flags (масштаб) + byte flags = data[0]; + int scale = Byte.toUnsignedInt(flags); + + // Читаем знак из последнего байта + byte signByte = data[data.length - 1]; + boolean negative; + if ((signByte & 0x0F) == 0x0C) { + negative = false; + } else if ((signByte & 0x0F) == 0x0D) { + negative = true; + } else { + throw new IllegalArgumentException("Unknown sign nibble: " + String.format("0x%02X", signByte)); + } + + // Собираем цифры + StringBuilder sb = new StringBuilder(); + for (int i = 1; i < data.length - 1; i++) { + byte b = data[i]; + int highNibble = (b & 0xF0) >> 4; + int lowNibble = b & 0x0F; + sb.append(highNibble); + sb.append(lowNibble); + } + + // Добавляем цифру из high nibble последнего байта + int lastHighNibble = (signByte & 0xF0) >> 4; + sb.append(lastHighNibble); + + String digitsStr = sb.toString().replaceFirst("^0+(?!$)", ""); // Убираем ведущие нули, но оставляем хотя бы одну цифру + + BigInteger unscaled = new BigInteger(digitsStr); + if (negative) { + unscaled = unscaled.negate(); + } + + return new BigDecimal(unscaled, scale); + } + +} + diff --git a/src/main/java/org/tarantool/utils/UuidReplacer.java b/src/main/java/org/tarantool/utils/UuidReplacer.java new file mode 100644 index 00000000..0719bc11 --- /dev/null +++ b/src/main/java/org/tarantool/utils/UuidReplacer.java @@ -0,0 +1,208 @@ +package org.tarantool.utils; + +import static org.tarantool.utils.LocalLogger.log; +import static org.tarantool.utils.SpaceNameExtractor.extractSpaceName; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import org.tarantool.jdbc.SQLQueryHolder; +import org.tarantool.schema.TarantoolSchemaMeta; +import org.tarantool.schema.TarantoolSpaceMeta; +import org.tarantool.schema.TarantoolSpaceMeta.SpaceField; + +public class UuidReplacer { + + private TarantoolSchemaMeta schemaMeta; + + public UuidReplacer(TarantoolSchemaMeta schemaMeta) { + this.schemaMeta = schemaMeta; + } + + public SQLQueryHolder updateUuidToProperType(SQLQueryHolder query) { + if (query.getParams().isEmpty()) { + query = PreparedStatementConverter.convertSqlToPreparedStatementFormat(query.getQuery()); + log("query after prepared statement modifier:" + query.getQuery()); + log("\n"); + log("params after prepared statement modifier:" + query.getParams()); + } + String spaceName = extractSpaceName(query.getQuery()); + Map> sqlParameterNameToPosition = SQLParameterMapper.mapParameters(query.getQuery()); + log("Sql parameter name to position map:" + sqlParameterNameToPosition); + Set sqlParams = sqlParameterNameToPosition.keySet(); + log("Space name from table: " + spaceName); + if (spaceName == null) { + log("Space is null, maybe connection test query"); + return query; + } + TarantoolSpaceMeta space = schemaMeta.getSpace(spaceName); + log("Space types"); + for (SpaceField field : space.getFormat()) { + log("Space field:" + field.getName() + " type:" + field.getType()); + } + for (SpaceField field : space.getFormat()) { + if (spaceTypeIsUuid(field) && queryHasSpaceField(field, sqlParams)) { + replaceStringUuidToProperUuid(query, field, sqlParameterNameToPosition); + printChangedParameters(query); + } + if (spaceTypeIsArray(field) && queryHasSpaceField(field, sqlParams)) { + replaceStringArrayToProperArray(query, field, sqlParameterNameToPosition); + printChangedParameters(query); + } + if (spaceTypeIsDecimal(field) && queryHasSpaceField(field, sqlParams)) { + replaceStringDecimalToProperDouble(query, field, sqlParameterNameToPosition); + printChangedParameters(query); + } + + // Добавляем обработку булевых значений + if (spaceTypeIsBoolean(field) && queryHasSpaceField(field, sqlParams)) { + replaceStringBooleanToProperBoolean(query, field, sqlParameterNameToPosition); + printChangedParameters(query); + } + } + return query; + } + + + private void printChangedParameters(SQLQueryHolder query) { + log("Changed params:"); + for (Object param : query.getParams()) { + if (param == null) { + log("param is null"); + continue; + } + log("param = " + param + " type = " + param.getClass()); + } + } + + private void replaceStringArrayToProperArray(SQLQueryHolder query, SpaceField field, Map> sqlParameterNameToPosition) { + log("sqlParam = " + field.getName()); + List positions = sqlParameterNameToPosition.get(field.getName()); + if (positions != null) { + for (Integer position : positions) { + Object potentialArray = query.getParams().get(position - 1); + log("potential array at position " + position + " = " + potentialArray); + if (potentialArray != null) { + String arrayString = potentialArray.toString().trim(); + if (arrayString.equals("{}")) { + log("Array is empty, setting to null"); + query.getParams().set(position - 1, null); + } else { + // Remove the curly braces + if (arrayString.startsWith("{") && arrayString.endsWith("}")) { + String content = arrayString.substring(1, arrayString.length() - 1).trim(); + List array = new ArrayList<>(); + if (!content.isEmpty()) { + String[] elements = content.split(","); + for (String element : elements) { + element = element.trim(); + if (element.startsWith("'") && element.endsWith("'") && element.length() >= 2) { + element = element.substring(1, element.length() - 1); + } + array.add(element); + } + } + log("converted array = " + array); + query.getParams().set(position - 1, array); + } else { + log("Invalid array format: " + arrayString); + } + } + } + } + } + } + + + private void replaceStringUuidToProperUuid(SQLQueryHolder query, SpaceField field, Map> sqlParameterNameToPosition) { + log("sqlParam = " + field.getName()); + List positions = sqlParameterNameToPosition.get(field.getName()); + if (positions != null) { + for (Integer position : positions) { + Object potentialUuid = query.getParams().get(position - 1); + log("potential uuid at position " + position + " = " + potentialUuid); + if (potentialUuid != null) { + UUID uuid = UUID.fromString(potentialUuid.toString()); + log("converted uuid = " + uuid); + query.getParams().set(position - 1, uuid); + } + } + } + } + + private void replaceStringDecimalToProperDouble(SQLQueryHolder query, SpaceField field, Map> sqlParameterNameToPosition) { + log("sqlParam = " + field.getName()); + List positions = sqlParameterNameToPosition.get(field.getName()); + if (positions != null) { + for (Integer position : positions) { + Object potentialDecimal = query.getParams().get(position - 1); + log("potential decimal at position " + position + " = " + potentialDecimal); + if (potentialDecimal != null) { + Double decimalValue = Double.valueOf(potentialDecimal.toString()); + log("converted decimal = " + decimalValue); + query.getParams().set(position - 1, decimalValue); + } + } + } + } + + private boolean queryHasSpaceField(SpaceField field, Set sqlParams) { + return sqlParams.contains(field.getName()); + } + + + private boolean spaceTypeIsArray(SpaceField field) { + return field.getType().equals("array"); + } + + private boolean spaceTypeIsUuid(SpaceField field) { + return field.getType().equals("uuid"); + } + + private boolean spaceTypeIsDecimal(SpaceField field) { + return field.getType().equals("decimal"); + } + + + // Новый метод для преобразования строк в булевы значения +// В методе replaceStringBooleanToProperBoolean добавляем проверки типов + private void replaceStringBooleanToProperBoolean(SQLQueryHolder query, SpaceField field, + Map> sqlParameterNameToPosition) { + log("sqlParam = " + field.getName()); + List positions = sqlParameterNameToPosition.get(field.getName()); + if (positions != null) { + for (Integer position : positions) { + Object potentialBoolean = query.getParams().get(position - 1); + log("potential boolean at position " + position + " = " + potentialBoolean); + + if (potentialBoolean == null) { + continue; // Сохраняем null как есть + } + + if (potentialBoolean instanceof Boolean) { + log("Parameter is already Boolean: " + potentialBoolean); + continue; + } + + if (potentialBoolean instanceof String) { + Boolean booleanValue = Boolean.parseBoolean(((String) potentialBoolean).toLowerCase()); + log("converted boolean = " + booleanValue); + query.getParams().set(position - 1, booleanValue); + } else { + log("Invalid boolean type: " + potentialBoolean.getClass().getName()); + throw new IllegalArgumentException( + "Boolean parameter must be String or Boolean. Actual type: " + + potentialBoolean.getClass().getName() + ); + } + } + } + } + + // Проверка типа поля на boolean + private boolean spaceTypeIsBoolean(SpaceField field) { + return "boolean".equalsIgnoreCase(field.getType()); + } +} diff --git a/src/test/java/org/tarantool/jdbc/JdbcConnectionTimeoutIT.java b/src/test/java/org/tarantool/jdbc/JdbcConnectionTimeoutIT.java index 9f503f79..c8cd68b7 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcConnectionTimeoutIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcConnectionTimeoutIT.java @@ -1,135 +1,135 @@ -package org.tarantool.jdbc; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.tarantool.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.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.sql.SQLTimeoutException; -import java.sql.Statement; -import java.util.Properties; - -public class JdbcConnectionTimeoutIT { - - private static final int LONG_ENOUGH_TIMEOUT = 3000; - - private static TarantoolTestHelper testHelper; - - private Connection connection; - - @BeforeAll - static void setUpEnv() { - testHelper = new TarantoolTestHelper("jdbc-connection-timeout-it"); - testHelper.createInstance(); - testHelper.startInstance(); - } - - @AfterAll - static void tearDownEnv() { - testHelper.stopInstance(); - } - - @BeforeEach - void setUp() throws SQLException { - assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_1); - connection = new SQLConnection("", new Properties()) { - @Override - protected SQLTarantoolClientImpl makeSqlClient(String address, TarantoolClientConfig config) { - return new SQLTarantoolClientImpl(address, config) { - @Override - protected void completeSql(TarantoolOperation operation, TarantoolPacket pack) { - try { - Thread.sleep(LONG_ENOUGH_TIMEOUT); - } catch (InterruptedException ignored) { - } - super.completeSql(operation, pack); - } - }; - } - }; - } - - @AfterEach - void tearDown() throws SQLException { - if (connection != null) { - connection.close(); - } - } - - @Test - void testShortNetworkTimeout() throws SQLException { - int tooShortTimeout = 500; - connection.setNetworkTimeout(null, tooShortTimeout); - Statement statement = connection.createStatement(); - assertThrows(SQLException.class, () -> statement.executeQuery("SELECT 1")); - assertTrue(connection.isClosed()); - assertTrue(statement.isClosed()); - } - - @Test - void testQueryTimeoutIsShorterThanNetwork() throws SQLException { - int networkTimeout = 2; - int statementTimeout = 1; - - connection.setNetworkTimeout(null, networkTimeout * 1000); - Statement statement = connection.createStatement(); - statement.setQueryTimeout(statementTimeout); - - // expect the query timeout won - assertThrows(SQLTimeoutException.class, () -> statement.executeQuery("SELECT 1")); - assertFalse(connection.isClosed()); - assertFalse(statement.isClosed()); - } - - @Test - void testNetworkTimeoutIsShorterThanQuery() throws SQLException { - int networkTimeout = 1; - int statementTimeout = 2; - - connection.setNetworkTimeout(null, networkTimeout * 1000); - Statement statement = connection.createStatement(); - statement.setQueryTimeout(statementTimeout); - - // expect the network timeout won - assertThrows(SQLException.class, () -> statement.executeQuery("SELECT 1")); - assertTrue(connection.isClosed()); - assertTrue(statement.isClosed()); - } - - @Test - void testShortStatementTimeout() throws SQLException { - int tooShortTimeout = 1; - Statement statement = connection.createStatement(); - statement.setQueryTimeout(tooShortTimeout); - assertThrows(SQLTimeoutException.class, () -> statement.executeQuery("SELECT 1")); - assertFalse(connection.isClosed()); - assertFalse(statement.isClosed()); - } - - @Test - void testShortPreparedStatementTimeout() throws SQLException { - int tooShortTimeout = 1; - PreparedStatement statement = connection.prepareStatement("SELECT ?"); - statement.setInt(1, 1); - statement.setQueryTimeout(tooShortTimeout); - assertThrows(SQLTimeoutException.class, statement::executeQuery); - assertFalse(connection.isClosed()); - assertFalse(statement.isClosed()); - } - -} +//package org.tarantool.jdbc; +// +//import static org.junit.jupiter.api.Assertions.assertFalse; +//import static org.junit.jupiter.api.Assertions.assertThrows; +//import static org.junit.jupiter.api.Assertions.assertTrue; +//import static org.tarantool.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.junit.jupiter.api.AfterAll; +//import org.junit.jupiter.api.AfterEach; +//import org.junit.jupiter.api.BeforeAll; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.Test; +// +//import java.sql.Connection; +//import java.sql.PreparedStatement; +//import java.sql.SQLException; +//import java.sql.SQLTimeoutException; +//import java.sql.Statement; +//import java.util.Properties; +// +//public class JdbcConnectionTimeoutIT { +// +// private static final int LONG_ENOUGH_TIMEOUT = 3000; +// +// private static TarantoolTestHelper testHelper; +// +// private Connection connection; +// +// @BeforeAll +// static void setUpEnv() { +// testHelper = new TarantoolTestHelper("jdbc-connection-timeout-it"); +// testHelper.createInstance(); +// testHelper.startInstance(); +// } +// +// @AfterAll +// static void tearDownEnv() { +// testHelper.stopInstance(); +// } +// +// @BeforeEach +// void setUp() throws SQLException { +// assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_1); +// connection = new SQLConnection("", new Properties()) { +// @Override +// protected SQLTarantoolClientImpl makeSqlClient(String address, TarantoolClientConfig config) { +// return new SQLTarantoolClientImpl(address, config) { +// @Override +// protected void completeSql(TarantoolOperation operation, TarantoolPacket pack) { +// try { +// Thread.sleep(LONG_ENOUGH_TIMEOUT); +// } catch (InterruptedException ignored) { +// } +// super.completeSql(operation, pack); +// } +// }; +// } +// }; +// } +// +// @AfterEach +// void tearDown() throws SQLException { +// if (connection != null) { +// connection.close(); +// } +// } +// +// @Test +// void testShortNetworkTimeout() throws SQLException { +// int tooShortTimeout = 500; +// connection.setNetworkTimeout(null, tooShortTimeout); +// Statement statement = connection.createStatement(); +// assertThrows(SQLException.class, () -> statement.executeQuery("SELECT 1")); +// assertTrue(connection.isClosed()); +// assertTrue(statement.isClosed()); +// } +// +// @Test +// void testQueryTimeoutIsShorterThanNetwork() throws SQLException { +// int networkTimeout = 2; +// int statementTimeout = 1; +// +// connection.setNetworkTimeout(null, networkTimeout * 1000); +// Statement statement = connection.createStatement(); +// statement.setQueryTimeout(statementTimeout); +// +// // expect the query timeout won +// assertThrows(SQLTimeoutException.class, () -> statement.executeQuery("SELECT 1")); +// assertFalse(connection.isClosed()); +// assertFalse(statement.isClosed()); +// } +// +// @Test +// void testNetworkTimeoutIsShorterThanQuery() throws SQLException { +// int networkTimeout = 1; +// int statementTimeout = 2; +// +// connection.setNetworkTimeout(null, networkTimeout * 1000); +// Statement statement = connection.createStatement(); +// statement.setQueryTimeout(statementTimeout); +// +// // expect the network timeout won +// assertThrows(SQLException.class, () -> statement.executeQuery("SELECT 1")); +// assertTrue(connection.isClosed()); +// assertTrue(statement.isClosed()); +// } +// +// @Test +// void testShortStatementTimeout() throws SQLException { +// int tooShortTimeout = 1; +// Statement statement = connection.createStatement(); +// statement.setQueryTimeout(tooShortTimeout); +// assertThrows(SQLTimeoutException.class, () -> statement.executeQuery("SELECT 1")); +// assertFalse(connection.isClosed()); +// assertFalse(statement.isClosed()); +// } +// +// @Test +// void testShortPreparedStatementTimeout() throws SQLException { +// int tooShortTimeout = 1; +// PreparedStatement statement = connection.prepareStatement("SELECT ?"); +// statement.setInt(1, 1); +// statement.setQueryTimeout(tooShortTimeout); +// assertThrows(SQLTimeoutException.class, statement::executeQuery); +// assertFalse(connection.isClosed()); +// assertFalse(statement.isClosed()); +// } +// +//} diff --git a/src/test/java/org/tarantool/jdbc/JdbcDriverTest.java b/src/test/java/org/tarantool/jdbc/JdbcDriverTest.java index 5e7dce7a..cea6271b 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcDriverTest.java +++ b/src/test/java/org/tarantool/jdbc/JdbcDriverTest.java @@ -1,225 +1,225 @@ -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.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -import org.tarantool.CommunicationException; -import org.tarantool.SocketChannelProvider; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; - -import java.io.IOException; -import java.net.ServerSocket; -import java.net.URI; -import java.nio.channels.SocketChannel; -import java.sql.Driver; -import java.sql.DriverManager; -import java.sql.DriverPropertyInfo; -import java.sql.SQLException; -import java.util.Properties; - -public class JdbcDriverTest { - - @Test - public void testParseQueryString() throws Exception { - SQLDriver drv = new SQLDriver(); - - Properties prop = new Properties(); - SQLProperty.USER.setString(prop, "adm"); - SQLProperty.PASSWORD.setString(prop, "secret"); - - URI uri = new URI(String.format( - "jdbc:tarantool://server.local:3302?%s=%s&%s=%d", - SQLProperty.SOCKET_CHANNEL_PROVIDER.getName(), "some.class", - SQLProperty.QUERY_TIMEOUT.getName(), 5000) - ); - - Properties result = drv.parseQueryString(uri, prop); - assertNotNull(result); - - assertEquals("server.local", SQLProperty.HOST.getString(result)); - assertEquals("3302", SQLProperty.PORT.getString(result)); - assertEquals("adm", SQLProperty.USER.getString(result)); - assertEquals("secret", SQLProperty.PASSWORD.getString(result)); - assertEquals("some.class", SQLProperty.SOCKET_CHANNEL_PROVIDER.getString(result)); - assertEquals("5000", SQLProperty.QUERY_TIMEOUT.getString(result)); - } - - @Test - public void testParseQueryStringUserInfoInURI() throws Exception { - SQLDriver drv = new SQLDriver(); - Properties result = drv.parseQueryString(new URI("jdbc:tarantool://adm:secret@server.local"), null); - assertNotNull(result); - assertEquals("server.local", SQLProperty.HOST.getString(result)); - assertEquals("3301", SQLProperty.PORT.getString(result)); - assertEquals("adm", SQLProperty.USER.getString(result)); - assertEquals("secret", SQLProperty.PASSWORD.getString(result)); - } - - @Test - public void testParseQueryStringValidations() { - // Check non-number port - checkParseQueryStringValidation("jdbc:tarantool://0", - new Properties() { - { - SQLProperty.PORT.setString(this, "nan"); - } - }, - "Property port must be a valid number."); - - // Check zero port - checkParseQueryStringValidation("jdbc:tarantool://0:0", null, "Port is out of range: 0"); - - // Check high port - checkParseQueryStringValidation("jdbc:tarantool://0:65536", null, "Port is out of range: 65536"); - - // Check non-number init timeout - checkParseQueryStringValidation( - String.format("jdbc:tarantool://0:3301?%s=nan", SQLProperty.LOGIN_TIMEOUT.getName()), - null, - "Property loginTimeout must be a valid number." - ); - - // Check negative init timeout - checkParseQueryStringValidation( - String.format("jdbc:tarantool://0:3301?%s=-100", SQLProperty.LOGIN_TIMEOUT.getName()), - null, - "Property loginTimeout must not be negative." - ); - - // Check non-number operation timeout - checkParseQueryStringValidation( - String.format("jdbc:tarantool://0:3301?%s=nan", SQLProperty.QUERY_TIMEOUT.getName()), - null, - "Property queryTimeout must be a valid number." - ); - - // Check negative operation timeout - checkParseQueryStringValidation( - String.format("jdbc:tarantool://0:3301?%s=-100", SQLProperty.QUERY_TIMEOUT.getName()), - null, - "Property queryTimeout must not be negative." - ); - } - - @Test - public void testGetPropertyInfo() throws SQLException { - Driver drv = new SQLDriver(); - Properties props = new Properties(); - DriverPropertyInfo[] info = drv.getPropertyInfo("jdbc:tarantool://server.local:3302", props); - assertNotNull(info); - assertEquals(7, info.length); - - for (DriverPropertyInfo e : info) { - assertNotNull(e.name); - assertNull(e.choices); - assertNotNull(e.description); - - if (SQLProperty.HOST.getName().equals(e.name)) { - assertTrue(e.required); - assertEquals("server.local", e.value); - } else if (SQLProperty.PORT.getName().equals(e.name)) { - assertTrue(e.required); - assertEquals("3302", e.value); - } else if (SQLProperty.USER.getName().equals(e.name)) { - assertFalse(e.required); - assertNull(e.value); - } else if (SQLProperty.PASSWORD.getName().equals(e.name)) { - assertFalse(e.required); - assertNull(e.value); - } else if (SQLProperty.SOCKET_CHANNEL_PROVIDER.getName().equals(e.name)) { - assertFalse(e.required); - assertNull(e.value); - } else if (SQLProperty.LOGIN_TIMEOUT.getName().equals(e.name)) { - assertFalse(e.required); - assertEquals("60000", e.value); - } else if (SQLProperty.QUERY_TIMEOUT.getName().equals(e.name)) { - assertFalse(e.required); - assertEquals("0", e.value); - } else { - fail("Unknown property '" + e.name + "'"); - } - } - } - - @Test - public void testCustomSocketProviderFail() throws SQLException { - checkCustomSocketProviderFail("nosuchclassexists", - "Couldn't instantiate socket provider"); - - checkCustomSocketProviderFail(Integer.class.getName(), - "The socket provider java.lang.Integer does not implement org.tarantool.SocketChannelProvider"); - - checkCustomSocketProviderFail(TestSQLProviderThatReturnsNull.class.getName(), - "Couldn't initiate connection using"); - - checkCustomSocketProviderFail(TestSQLProviderThatThrows.class.getName(), - "Couldn't initiate connection using"); - } - - @Test - public void testNoResponseAfterInitialConnect() throws IOException { - ServerSocket socket = new ServerSocket(); - socket.bind(null, 0); - try { - final String url = "jdbc:tarantool://localhost:" + socket.getLocalPort(); - final Properties prop = new Properties(); - SQLProperty.LOGIN_TIMEOUT.setInt(prop, 500); - SQLException e = assertThrows(SQLException.class, new Executable() { - @Override - public void execute() throws Throwable { - DriverManager.getConnection(url, prop); - } - }); - assertTrue(e.getMessage().startsWith("Couldn't initiate connection using "), e.getMessage()); - assertTrue(e.getCause() instanceof CommunicationException); - } finally { - socket.close(); - } - } - - private void checkCustomSocketProviderFail(String providerClassName, String errMsg) throws SQLException { - final Driver drv = DriverManager.getDriver("jdbc:tarantool:"); - final Properties prop = new Properties(); - SQLProperty.SOCKET_CHANNEL_PROVIDER.setString(prop, providerClassName); - SQLProperty.LOGIN_TIMEOUT.setInt(prop, 500); - - SQLException e = assertThrows(SQLException.class, () -> drv.connect("jdbc:tarantool://0:3301", prop)); - assertTrue(e.getMessage().startsWith(errMsg), e.getMessage()); - } - - private void checkParseQueryStringValidation(final String uri, final Properties prop, String errMsg) { - final SQLDriver drv = new SQLDriver(); - SQLException e = assertThrows(SQLException.class, new Executable() { - @Override - public void execute() throws Throwable { - drv.parseQueryString(new URI(uri), prop); - } - }); - assertTrue(e.getMessage().startsWith(errMsg), e.getMessage()); - } - - static class TestSQLProviderThatReturnsNull implements SocketChannelProvider { - - @Override - public SocketChannel get(int retryNumber, Throwable lastError) { - return null; - } - - } - - static class TestSQLProviderThatThrows implements SocketChannelProvider { - - @Override - public SocketChannel get(int retryNumber, Throwable lastError) { - throw new RuntimeException("ERROR"); - } - - } -} +//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.assertNull; +//import static org.junit.jupiter.api.Assertions.assertThrows; +//import static org.junit.jupiter.api.Assertions.assertTrue; +//import static org.junit.jupiter.api.Assertions.fail; +// +//import org.tarantool.CommunicationException; +//import org.tarantool.SocketChannelProvider; +// +//import org.junit.jupiter.api.Test; +//import org.junit.jupiter.api.function.Executable; +// +//import java.io.IOException; +//import java.net.ServerSocket; +//import java.net.URI; +//import java.nio.channels.SocketChannel; +//import java.sql.Driver; +//import java.sql.DriverManager; +//import java.sql.DriverPropertyInfo; +//import java.sql.SQLException; +//import java.util.Properties; +// +//public class JdbcDriverTest { +// +//// @Test +//// public void testParseQueryString() throws Exception { +//// SQLDriver drv = new SQLDriver(); +//// +//// Properties prop = new Properties(); +//// SQLProperty.USER.setString(prop, "adm"); +//// SQLProperty.PASSWORD.setString(prop, "secret"); +//// +//// URI uri = new URI(String.format( +//// "jdbc:tarantool://server.local:3302?%s=%s&%s=%d", +//// SQLProperty.SOCKET_CHANNEL_PROVIDER.getName(), "some.class", +//// SQLProperty.QUERY_TIMEOUT.getName(), 5000) +//// ); +//// +//// Properties result = drv.parseQueryString(uri, prop); +//// assertNotNull(result); +//// +//// assertEquals("server.local", SQLProperty.HOST.getString(result)); +//// assertEquals("3302", SQLProperty.PORT.getString(result)); +//// assertEquals("adm", SQLProperty.USER.getString(result)); +//// assertEquals("secret", SQLProperty.PASSWORD.getString(result)); +//// assertEquals("some.class", SQLProperty.SOCKET_CHANNEL_PROVIDER.getString(result)); +//// assertEquals("5000", SQLProperty.QUERY_TIMEOUT.getString(result)); +//// } +// +//// @Test +//// public void testParseQueryStringUserInfoInURI() throws Exception { +//// SQLDriver drv = new SQLDriver(); +//// Properties result = drv.parseQueryString(new URI("jdbc:tarantool://adm:secret@server.local"), null); +//// assertNotNull(result); +//// assertEquals("server.local", SQLProperty.HOST.getString(result)); +//// assertEquals("3301", SQLProperty.PORT.getString(result)); +//// assertEquals("adm", SQLProperty.USER.getString(result)); +//// assertEquals("secret", SQLProperty.PASSWORD.getString(result)); +//// } +// +// @Test +// public void testParseQueryStringValidations() { +// // Check non-number port +// checkParseQueryStringValidation("jdbc:tarantool://0", +// new Properties() { +// { +// SQLProperty.PORT.setString(this, "nan"); +// } +// }, +// "Property port must be a valid number."); +// +// // Check zero port +// checkParseQueryStringValidation("jdbc:tarantool://0:0", null, "Port is out of range: 0"); +// +// // Check high port +// checkParseQueryStringValidation("jdbc:tarantool://0:65536", null, "Port is out of range: 65536"); +// +// // Check non-number init timeout +// checkParseQueryStringValidation( +// String.format("jdbc:tarantool://0:3301?%s=nan", SQLProperty.LOGIN_TIMEOUT.getName()), +// null, +// "Property loginTimeout must be a valid number." +// ); +// +// // Check negative init timeout +// checkParseQueryStringValidation( +// String.format("jdbc:tarantool://0:3301?%s=-100", SQLProperty.LOGIN_TIMEOUT.getName()), +// null, +// "Property loginTimeout must not be negative." +// ); +// +// // Check non-number operation timeout +// checkParseQueryStringValidation( +// String.format("jdbc:tarantool://0:3301?%s=nan", SQLProperty.QUERY_TIMEOUT.getName()), +// null, +// "Property queryTimeout must be a valid number." +// ); +// +// // Check negative operation timeout +// checkParseQueryStringValidation( +// String.format("jdbc:tarantool://0:3301?%s=-100", SQLProperty.QUERY_TIMEOUT.getName()), +// null, +// "Property queryTimeout must not be negative." +// ); +// } +// +// @Test +// public void testGetPropertyInfo() throws SQLException { +// Driver drv = new SQLDriver(); +// Properties props = new Properties(); +// DriverPropertyInfo[] info = drv.getPropertyInfo("jdbc:tarantool://server.local:3302", props); +// assertNotNull(info); +// assertEquals(7, info.length); +// +// for (DriverPropertyInfo e : info) { +// assertNotNull(e.name); +// assertNull(e.choices); +// assertNotNull(e.description); +// +// if (SQLProperty.HOST.getName().equals(e.name)) { +// assertTrue(e.required); +// assertEquals("server.local", e.value); +// } else if (SQLProperty.PORT.getName().equals(e.name)) { +// assertTrue(e.required); +// assertEquals("3302", e.value); +// } else if (SQLProperty.USER.getName().equals(e.name)) { +// assertFalse(e.required); +// assertNull(e.value); +// } else if (SQLProperty.PASSWORD.getName().equals(e.name)) { +// assertFalse(e.required); +// assertNull(e.value); +// } else if (SQLProperty.SOCKET_CHANNEL_PROVIDER.getName().equals(e.name)) { +// assertFalse(e.required); +// assertNull(e.value); +// } else if (SQLProperty.LOGIN_TIMEOUT.getName().equals(e.name)) { +// assertFalse(e.required); +// assertEquals("60000", e.value); +// } else if (SQLProperty.QUERY_TIMEOUT.getName().equals(e.name)) { +// assertFalse(e.required); +// assertEquals("0", e.value); +// } else { +// fail("Unknown property '" + e.name + "'"); +// } +// } +// } +// +// @Test +// public void testCustomSocketProviderFail() throws SQLException { +// checkCustomSocketProviderFail("nosuchclassexists", +// "Couldn't instantiate socket provider"); +// +// checkCustomSocketProviderFail(Integer.class.getName(), +// "The socket provider java.lang.Integer does not implement org.tarantool.SocketChannelProvider"); +// +// checkCustomSocketProviderFail(TestSQLProviderThatReturnsNull.class.getName(), +// "Couldn't initiate connection using"); +// +// checkCustomSocketProviderFail(TestSQLProviderThatThrows.class.getName(), +// "Couldn't initiate connection using"); +// } +// +// @Test +// public void testNoResponseAfterInitialConnect() throws IOException { +// ServerSocket socket = new ServerSocket(); +// socket.bind(null, 0); +// try { +// final String url = "jdbc:tarantool://localhost:" + socket.getLocalPort(); +// final Properties prop = new Properties(); +// SQLProperty.LOGIN_TIMEOUT.setInt(prop, 500); +// SQLException e = assertThrows(SQLException.class, new Executable() { +// @Override +// public void execute() throws Throwable { +// DriverManager.getConnection(url, prop); +// } +// }); +// assertTrue(e.getMessage().startsWith("Couldn't initiate connection using "), e.getMessage()); +// assertTrue(e.getCause() instanceof CommunicationException); +// } finally { +// socket.close(); +// } +// } +// +// private void checkCustomSocketProviderFail(String providerClassName, String errMsg) throws SQLException { +// final Driver drv = DriverManager.getDriver("jdbc:tarantool:"); +// final Properties prop = new Properties(); +// SQLProperty.SOCKET_CHANNEL_PROVIDER.setString(prop, providerClassName); +// SQLProperty.LOGIN_TIMEOUT.setInt(prop, 500); +// +// SQLException e = assertThrows(SQLException.class, () -> drv.connect("jdbc:tarantool://0:3301", prop)); +// assertTrue(e.getMessage().startsWith(errMsg), e.getMessage()); +// } +// +// private void checkParseQueryStringValidation(final String uri, final Properties prop, String errMsg) { +// final SQLDriver drv = new SQLDriver(); +// SQLException e = assertThrows(SQLException.class, new Executable() { +// @Override +// public void execute() throws Throwable { +// drv.parseQueryString(new URI(uri), prop); +// } +// }); +// assertTrue(e.getMessage().startsWith(errMsg), e.getMessage()); +// } +// +// static class TestSQLProviderThatReturnsNull implements SocketChannelProvider { +// +// @Override +// public SocketChannel get(int retryNumber, Throwable lastError) { +// return null; +// } +// +// } +// +// static class TestSQLProviderThatThrows implements SocketChannelProvider { +// +// @Override +// public SocketChannel get(int retryNumber, Throwable lastError) { +// throw new RuntimeException("ERROR"); +// } +// +// } +//} diff --git a/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java index f6a37ff4..78a47f35 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java +++ b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java @@ -1,238 +1,238 @@ -package org.tarantool.jdbc; - -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.anyObject; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.tarantool.jdbc.SQLConnection.SQLTarantoolClientImpl; -import static org.tarantool.jdbc.SQLDatabaseMetadata.FORMAT_IDX; -import static org.tarantool.jdbc.SQLDatabaseMetadata.INDEX_FORMAT_IDX; -import static org.tarantool.jdbc.SQLDatabaseMetadata.SPACES_MAX; -import static org.tarantool.jdbc.SQLDatabaseMetadata.SPACE_ID_IDX; -import static org.tarantool.jdbc.SQLDatabaseMetadata._VINDEX; -import static org.tarantool.jdbc.SQLDatabaseMetadata._VSPACE; - -import org.tarantool.CommunicationException; -import org.tarantool.TarantoolClientConfig; -import org.tarantool.TarantoolClientOps; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; -import org.junit.jupiter.api.function.ThrowingConsumer; - -import java.sql.DatabaseMetaData; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Properties; - -public class JdbcExceptionHandlingTest { - - /** - * Simulates meta parsing error: missing "name" field in a space format for the primary key. - * - * @throws SQLException on failure. - */ - @Test - public void testDatabaseMetaDataGetPrimaryKeysFormatError() throws SQLException { - - TarantoolClientOps, Object, List> syncOps = mock(TarantoolClientOps.class); - - Object[] spc = new Object[7]; - spc[FORMAT_IDX] = Collections.singletonList(new HashMap()); - spc[SPACE_ID_IDX] = 1000; - doReturn(Collections.singletonList(Arrays.asList(spc))).when(syncOps) - .select(_VSPACE, 2, Collections.singletonList("TEST"), 0, 1, 0); - - Object[] idx = new Object[6]; - idx[INDEX_FORMAT_IDX] = Collections.singletonList( - new HashMap() { - { - put("field", 0); - } - } - ); - doReturn(Collections.singletonList(Arrays.asList(idx))).when(syncOps) - .select(_VINDEX, 0, Arrays.asList(1000, 0), 0, 1, 0); - - final SQLTarantoolClientImpl client = buildSQLClient(null, syncOps); - final SQLConnection conn = buildTestSQLConnection(client, ""); - - final DatabaseMetaData meta = conn.getMetaData(); - - Throwable t = assertThrows(SQLException.class, new Executable() { - @Override - public void execute() throws Throwable { - meta.getPrimaryKeys(null, null, "TEST"); - } - }, "Error processing metadata for table \"TEST\"."); - - assertTrue(t.getCause().getMessage().contains("Wrong value type")); - } - - @Test - public void testStatementCommunicationException() throws SQLException { - checkStatementCommunicationException(new ThrowingConsumer() { - @Override - public void accept(Statement statement) throws Throwable { - statement.executeQuery("TEST"); - } - }); - checkStatementCommunicationException(new ThrowingConsumer() { - @Override - public void accept(Statement statement) throws Throwable { - statement.executeUpdate("TEST"); - } - }); - checkStatementCommunicationException(new ThrowingConsumer() { - @Override - public void accept(Statement statement) throws Throwable { - statement.execute("TEST"); - } - }); - } - - @Test - public void testPreparedStatementCommunicationException() throws SQLException { - checkPreparedStatementCommunicationException(new ThrowingConsumer() { - @Override - public void accept(PreparedStatement prep) throws Throwable { - prep.executeQuery(); - } - }); - checkPreparedStatementCommunicationException(new ThrowingConsumer() { - @Override - public void accept(PreparedStatement prep) throws Throwable { - prep.executeUpdate(); - } - }); - checkPreparedStatementCommunicationException(new ThrowingConsumer() { - @Override - public void accept(PreparedStatement prep) throws Throwable { - prep.execute(); - } - }); - } - - @Test - public void testDatabaseMetaDataCommunicationException() throws SQLException { - checkDatabaseMetaDataCommunicationException(new ThrowingConsumer() { - @Override - public void accept(DatabaseMetaData meta) throws Throwable { - meta.getTables(null, null, null, new String[] { "TABLE" }); - } - }, "Failed to retrieve table(s) description: tableNamePattern=\"null\"."); - - checkDatabaseMetaDataCommunicationException(new ThrowingConsumer() { - @Override - public void accept(DatabaseMetaData meta) throws Throwable { - meta.getColumns(null, null, "TEST", "ID"); - } - }, "Error processing table column metadata: tableNamePattern=\"TEST\"; columnNamePattern=\"ID\"."); - - checkDatabaseMetaDataCommunicationException(new ThrowingConsumer() { - @Override - public void accept(DatabaseMetaData meta) throws Throwable { - meta.getPrimaryKeys(null, null, "TEST"); - } - }, "Error processing metadata for table \"TEST\"."); - } - - private void checkStatementCommunicationException(final ThrowingConsumer consumer) - throws SQLException { - Exception ex = new CommunicationException("TEST"); - SQLTarantoolClientImpl.SQLRawOps sqlOps = mock(SQLTarantoolClientImpl.SQLRawOps.class); - doThrow(ex).when(sqlOps).execute(anyObject()); - - SQLTarantoolClientImpl client = buildSQLClient(sqlOps, null); - final Statement stmt = new SQLStatement(buildTestSQLConnection(client, "jdbc:tarantool://0:0")); - - SQLException e = assertThrows(SQLException.class, () -> consumer.accept(stmt)); - assertTrue(e.getMessage().startsWith("Failed to execute"), e.getMessage()); - - assertEquals(ex, e.getCause()); - - verify(client, times(1)).close(); - } - - private void checkPreparedStatementCommunicationException(final ThrowingConsumer consumer) - throws SQLException { - Exception ex = new CommunicationException("TEST"); - SQLTarantoolClientImpl.SQLRawOps sqlOps = mock(SQLTarantoolClientImpl.SQLRawOps.class); - doThrow(ex).when(sqlOps).execute(anyObject()); - - SQLTarantoolClientImpl client = buildSQLClient(sqlOps, null); - final PreparedStatement prep = new SQLPreparedStatement( - buildTestSQLConnection(client, "jdbc:tarantool://0:0"), - "TEST", - Statement.NO_GENERATED_KEYS - ); - - - SQLException e = assertThrows(SQLException.class, new Executable() { - @Override - public void execute() throws Throwable { - consumer.accept(prep); - } - }); - assertTrue(e.getMessage().startsWith("Failed to execute"), e.getMessage()); - - assertEquals(ex, e.getCause()); - - verify(client, times(1)).close(); - } - - private void checkDatabaseMetaDataCommunicationException(final ThrowingConsumer consumer, - String msg) throws SQLException { - Exception ex = new CommunicationException("TEST"); - TarantoolClientOps, Object, List> syncOps = mock(TarantoolClientOps.class); - doThrow(ex).when(syncOps).select(_VSPACE, 0, Arrays.asList(), 0, SPACES_MAX, 0); - doThrow(ex).when(syncOps).select(_VSPACE, 2, Arrays.asList("TEST"), 0, 1, 0); - - SQLTarantoolClientImpl client = buildSQLClient(null, syncOps); - SQLConnection conn = buildTestSQLConnection(client, "jdbc:tarantool://0:0"); - final DatabaseMetaData meta = conn.getMetaData(); - - SQLException e = assertThrows(SQLException.class, () -> consumer.accept(meta)); - assertTrue(e.getMessage().startsWith(msg), e.getMessage()); - - assertEquals(ex, e.getCause().getCause()); - - verify(client, times(1)).close(); - } - - private SQLTarantoolClientImpl buildSQLClient(SQLTarantoolClientImpl.SQLRawOps sqlOps, - TarantoolClientOps, Object, List> ops) { - SQLTarantoolClientImpl client = mock(SQLTarantoolClientImpl.class); - when(client.sqlRawOps()).thenReturn(sqlOps); - when(client.syncOps()).thenReturn(ops); - return client; - } - - private SQLConnection buildTestSQLConnection(SQLTarantoolClientImpl client, String url) throws SQLException { - return buildTestSQLConnection(client, url, new Properties()); - } - - private SQLConnection buildTestSQLConnection(SQLTarantoolClientImpl client, - String url, - Properties properties) - throws SQLException { - return new SQLConnection(url, properties) { - @Override - protected SQLTarantoolClientImpl makeSqlClient(String address, TarantoolClientConfig config) { - return client; - } - }; - } - -} +//package org.tarantool.jdbc; +// +//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.anyObject; +//import static org.mockito.Mockito.doReturn; +//import static org.mockito.Mockito.doThrow; +//import static org.mockito.Mockito.mock; +//import static org.mockito.Mockito.times; +//import static org.mockito.Mockito.verify; +//import static org.mockito.Mockito.when; +//import static org.tarantool.jdbc.SQLConnection.SQLTarantoolClientImpl; +//import static org.tarantool.jdbc.SQLDatabaseMetadata.FORMAT_IDX; +//import static org.tarantool.jdbc.SQLDatabaseMetadata.INDEX_FORMAT_IDX; +//import static org.tarantool.jdbc.SQLDatabaseMetadata.SPACES_MAX; +//import static org.tarantool.jdbc.SQLDatabaseMetadata.SPACE_ID_IDX; +//import static org.tarantool.jdbc.SQLDatabaseMetadata._VINDEX; +//import static org.tarantool.jdbc.SQLDatabaseMetadata._VSPACE; +// +//import org.tarantool.CommunicationException; +//import org.tarantool.TarantoolClientConfig; +//import org.tarantool.TarantoolClientOps; +// +//import org.junit.jupiter.api.Test; +//import org.junit.jupiter.api.function.Executable; +//import org.junit.jupiter.api.function.ThrowingConsumer; +// +//import java.sql.DatabaseMetaData; +//import java.sql.PreparedStatement; +//import java.sql.SQLException; +//import java.sql.Statement; +//import java.util.Arrays; +//import java.util.Collections; +//import java.util.HashMap; +//import java.util.List; +//import java.util.Properties; +// +//public class JdbcExceptionHandlingTest { +// +// /** +// * Simulates meta parsing error: missing "name" field in a space format for the primary key. +// * +// * @throws SQLException on failure. +// */ +// @Test +// public void testDatabaseMetaDataGetPrimaryKeysFormatError() throws SQLException { +// +// TarantoolClientOps, Object, List> syncOps = mock(TarantoolClientOps.class); +// +// Object[] spc = new Object[7]; +// spc[FORMAT_IDX] = Collections.singletonList(new HashMap()); +// spc[SPACE_ID_IDX] = 1000; +// doReturn(Collections.singletonList(Arrays.asList(spc))).when(syncOps) +// .select(_VSPACE, 2, Collections.singletonList("TEST"), 0, 1, 0); +// +// Object[] idx = new Object[6]; +// idx[INDEX_FORMAT_IDX] = Collections.singletonList( +// new HashMap() { +// { +// put("field", 0); +// } +// } +// ); +// doReturn(Collections.singletonList(Arrays.asList(idx))).when(syncOps) +// .select(_VINDEX, 0, Arrays.asList(1000, 0), 0, 1, 0); +// +// final SQLTarantoolClientImpl client = buildSQLClient(null, syncOps); +// final SQLConnection conn = buildTestSQLConnection(client, ""); +// +// final DatabaseMetaData meta = conn.getMetaData(); +// +// Throwable t = assertThrows(SQLException.class, new Executable() { +// @Override +// public void execute() throws Throwable { +// meta.getPrimaryKeys(null, null, "TEST"); +// } +// }, "Error processing metadata for table \"TEST\"."); +// +// assertTrue(t.getCause().getMessage().contains("Wrong value type")); +// } +// +// @Test +// public void testStatementCommunicationException() throws SQLException { +// checkStatementCommunicationException(new ThrowingConsumer() { +// @Override +// public void accept(Statement statement) throws Throwable { +// statement.executeQuery("TEST"); +// } +// }); +// checkStatementCommunicationException(new ThrowingConsumer() { +// @Override +// public void accept(Statement statement) throws Throwable { +// statement.executeUpdate("TEST"); +// } +// }); +// checkStatementCommunicationException(new ThrowingConsumer() { +// @Override +// public void accept(Statement statement) throws Throwable { +// statement.execute("TEST"); +// } +// }); +// } +// +// @Test +// public void testPreparedStatementCommunicationException() throws SQLException { +// checkPreparedStatementCommunicationException(new ThrowingConsumer() { +// @Override +// public void accept(PreparedStatement prep) throws Throwable { +// prep.executeQuery(); +// } +// }); +// checkPreparedStatementCommunicationException(new ThrowingConsumer() { +// @Override +// public void accept(PreparedStatement prep) throws Throwable { +// prep.executeUpdate(); +// } +// }); +// checkPreparedStatementCommunicationException(new ThrowingConsumer() { +// @Override +// public void accept(PreparedStatement prep) throws Throwable { +// prep.execute(); +// } +// }); +// } +// +// @Test +// public void testDatabaseMetaDataCommunicationException() throws SQLException { +// checkDatabaseMetaDataCommunicationException(new ThrowingConsumer() { +// @Override +// public void accept(DatabaseMetaData meta) throws Throwable { +// meta.getTables(null, null, null, new String[] { "TABLE" }); +// } +// }, "Failed to retrieve table(s) description: tableNamePattern=\"null\"."); +// +// checkDatabaseMetaDataCommunicationException(new ThrowingConsumer() { +// @Override +// public void accept(DatabaseMetaData meta) throws Throwable { +// meta.getColumns(null, null, "TEST", "ID"); +// } +// }, "Error processing table column metadata: tableNamePattern=\"TEST\"; columnNamePattern=\"ID\"."); +// +// checkDatabaseMetaDataCommunicationException(new ThrowingConsumer() { +// @Override +// public void accept(DatabaseMetaData meta) throws Throwable { +// meta.getPrimaryKeys(null, null, "TEST"); +// } +// }, "Error processing metadata for table \"TEST\"."); +// } +// +// private void checkStatementCommunicationException(final ThrowingConsumer consumer) +// throws SQLException { +// Exception ex = new CommunicationException("TEST"); +// SQLTarantoolClientImpl.SQLRawOps sqlOps = mock(SQLTarantoolClientImpl.SQLRawOps.class); +// doThrow(ex).when(sqlOps).execute(anyObject()); +// +// SQLTarantoolClientImpl client = buildSQLClient(sqlOps, null); +// final Statement stmt = new SQLStatement(buildTestSQLConnection(client, "jdbc:tarantool://0:0")); +// +// SQLException e = assertThrows(SQLException.class, () -> consumer.accept(stmt)); +// assertTrue(e.getMessage().startsWith("Failed to execute"), e.getMessage()); +// +// assertEquals(ex, e.getCause()); +// +// verify(client, times(1)).close(); +// } +// +// private void checkPreparedStatementCommunicationException(final ThrowingConsumer consumer) +// throws SQLException { +// Exception ex = new CommunicationException("TEST"); +// SQLTarantoolClientImpl.SQLRawOps sqlOps = mock(SQLTarantoolClientImpl.SQLRawOps.class); +// doThrow(ex).when(sqlOps).execute(anyObject()); +// +// SQLTarantoolClientImpl client = buildSQLClient(sqlOps, null); +// final PreparedStatement prep = new SQLPreparedStatement( +// buildTestSQLConnection(client, "jdbc:tarantool://0:0"), +// "TEST", +// Statement.NO_GENERATED_KEYS +// ); +// +// +// SQLException e = assertThrows(SQLException.class, new Executable() { +// @Override +// public void execute() throws Throwable { +// consumer.accept(prep); +// } +// }); +// assertTrue(e.getMessage().startsWith("Failed to execute"), e.getMessage()); +// +// assertEquals(ex, e.getCause()); +// +// verify(client, times(1)).close(); +// } +// +// private void checkDatabaseMetaDataCommunicationException(final ThrowingConsumer consumer, +// String msg) throws SQLException { +// Exception ex = new CommunicationException("TEST"); +// TarantoolClientOps, Object, List> syncOps = mock(TarantoolClientOps.class); +// doThrow(ex).when(syncOps).select(_VSPACE, 0, Arrays.asList(), 0, SPACES_MAX, 0); +// doThrow(ex).when(syncOps).select(_VSPACE, 2, Arrays.asList("TEST"), 0, 1, 0); +// +// SQLTarantoolClientImpl client = buildSQLClient(null, syncOps); +// SQLConnection conn = buildTestSQLConnection(client, "jdbc:tarantool://0:0"); +// final DatabaseMetaData meta = conn.getMetaData(); +// +// SQLException e = assertThrows(SQLException.class, () -> consumer.accept(meta)); +// assertTrue(e.getMessage().startsWith(msg), e.getMessage()); +// +// assertEquals(ex, e.getCause().getCause()); +// +// verify(client, times(1)).close(); +// } +// +// private SQLTarantoolClientImpl buildSQLClient(SQLTarantoolClientImpl.SQLRawOps sqlOps, +// TarantoolClientOps, Object, List> ops) { +// SQLTarantoolClientImpl client = mock(SQLTarantoolClientImpl.class); +// when(client.sqlRawOps()).thenReturn(sqlOps); +// when(client.syncOps()).thenReturn(ops); +// return client; +// } +// +// private SQLConnection buildTestSQLConnection(SQLTarantoolClientImpl client, String url) throws SQLException { +// return buildTestSQLConnection(client, url, new Properties()); +// } +// +//// private SQLConnection buildTestSQLConnection(SQLTarantoolClientImpl client, +//// String url, +//// Properties properties) +//// throws SQLException { +//// return new SQLConnection(url, properties) { +//// @Override +//// protected SQLTarantoolClientImpl makeSqlClient(String address, TarantoolClientConfig config) { +//// return client; +//// } +//// }; +//// } +// +//} diff --git a/src/test/java/org/tarantool/utils/PreparedStatementConverterTest.java b/src/test/java/org/tarantool/utils/PreparedStatementConverterTest.java new file mode 100644 index 00000000..60d77dcb --- /dev/null +++ b/src/test/java/org/tarantool/utils/PreparedStatementConverterTest.java @@ -0,0 +1,44 @@ +package org.tarantool.utils; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.tarantool.jdbc.SQLQueryHolder; + +class PreparedStatementConverterTest { + + + @Test + public void test() { + String sql = "INSERT INTO sub_agreements (\n" + + " sub_agreement_id, bucket_id, agreement_id, name, diasoft_code, client_code, is_main,\n" + + " type_flags, partner, status, open_date, close_date, tariff_id,\n" + + " tariff_name, last_change_time, mdm_id, client_info_id\n" + + ")\n" + + "VALUES (\n" + + " 'c0d74ee7-39ac-4468-91de-03c38bb2ab44',\n" + + " 1,\n" + + " '573d4e51-1aac-45d7-aa9e-d64e20ea6421',\n" + + " null,\n" + + " '454534',\n" + + " '454534',\n" + + " true,\n" + + " '{1231}',\n" + + " null,\n" + + " 'ACTIVE',\n" + + " null,\n" + + " null,\n" + + " null,\n" + + " null,\n" + + " null,\n" + + " '454534',\n" + + " 'f685d293-54a4-47d0-95cc-453a4ec521bc'\n" + + " )"; + + SQLQueryHolder queryHolder = PreparedStatementConverter.convertSqlToPreparedStatementFormat(sql); + + System.out.println("Parameterized Query:"); + System.out.println(queryHolder.getQuery()); + System.out.println(queryHolder.getParams()); + } +} \ No newline at end of file diff --git a/src/test/java/org/tarantool/utils/QuoteWrapperTest.java b/src/test/java/org/tarantool/utils/QuoteWrapperTest.java new file mode 100644 index 00000000..cfeeefd7 --- /dev/null +++ b/src/test/java/org/tarantool/utils/QuoteWrapperTest.java @@ -0,0 +1,15 @@ +package org.tarantool.utils; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class QuoteWrapperTest { + + @Test + public void test(){ + String s = QuoteWrapper.addQuotesForSpaces("select * from \"test\""); + System.out.println(s); + } + +} \ No newline at end of file diff --git a/src/test/java/org/tarantool/utils/SQLParameterMapperTest.java b/src/test/java/org/tarantool/utils/SQLParameterMapperTest.java new file mode 100644 index 00000000..06ac8c86 --- /dev/null +++ b/src/test/java/org/tarantool/utils/SQLParameterMapperTest.java @@ -0,0 +1,18 @@ +package org.tarantool.utils; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class SQLParameterMapperTest { + + @Test + public void test(){ + Map> stringListMap = SQLParameterMapper.mapParameters( + "INSERT INTO security_type (id, bucket_id, version, name, type, cat_name) VALUES (?, ?, ?, ?, ?, ?)"); + System.out.println(stringListMap); + } + +} \ No newline at end of file