From 95afef05ca5cbd278734813162d10e04a09cdcf3 Mon Sep 17 00:00:00 2001 From: Sergey Korotkov Date: Wed, 11 Sep 2024 14:23:20 +0700 Subject: [PATCH] IGNITE-6141 JDBC: add basic support for BLOB and CLOB types (#11492) Co-authored-by: rkondakov --- .../JdbcAbstractDmlStatementSelfTest.java | 26 +- .../JdbcAbstractUpdateStatementSelfTest.java | 8 +- .../ignite/internal/jdbc2/JdbcBlobTest.java | 3 + .../ignite/internal/jdbc2/JdbcClobTest.java | 452 ++++++++++++++++++ .../jdbc2/JdbcInsertStatementSelfTest.java | 59 ++- .../jdbc2/JdbcMergeStatementSelfTest.java | 48 +- .../jdbc2/JdbcPreparedStatementSelfTest.java | 54 ++- .../internal/jdbc2/JdbcResultSetSelfTest.java | 51 +- .../jdbc/suite/IgniteJdbcDriverTestSuite.java | 2 + .../JdbcThinAbstractDmlStatementSelfTest.java | 72 ++- ...bcThinAbstractUpdateStatementSelfTest.java | 8 +- .../jdbc/thin/JdbcThinConnectionSelfTest.java | 24 +- .../thin/JdbcThinInsertStatementSelfTest.java | 41 +- .../thin/JdbcThinMergeStatementSelfTest.java | 25 +- .../JdbcThinPreparedStatementSelfTest.java | 82 +++- .../jdbc/thin/JdbcThinResultSetSelfTest.java | 76 ++- .../jdbc/thin/JdbcThinConnection.java | 6 +- .../jdbc/thin/JdbcThinPreparedStatement.java | 8 +- .../internal/jdbc/thin/JdbcThinResultSet.java | 18 +- .../ignite/internal/jdbc2/JdbcBlob.java | 2 +- .../ignite/internal/jdbc2/JdbcClob.java | 326 +++++++++++++ .../ignite/internal/jdbc2/JdbcConnection.java | 2 +- .../internal/jdbc2/JdbcPreparedStatement.java | 4 +- .../ignite/internal/jdbc2/JdbcResultSet.java | 8 +- .../processors/query/h2/IgniteH2Indexing.java | 5 +- .../processors/query/h2/QueryParameters.java | 11 + 26 files changed, 1242 insertions(+), 179 deletions(-) create mode 100644 modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcClobTest.java create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcClob.java diff --git a/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcAbstractDmlStatementSelfTest.java b/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcAbstractDmlStatementSelfTest.java index c1142e1968520..d1ddd2ba0ba97 100644 --- a/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcAbstractDmlStatementSelfTest.java +++ b/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcAbstractDmlStatementSelfTest.java @@ -20,9 +20,11 @@ import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.sql.Blob; +import java.sql.Clob; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; +import java.util.Arrays; import java.util.Collections; import org.apache.ignite.cache.QueryEntity; import org.apache.ignite.cache.query.annotations.QuerySqlField; @@ -53,7 +55,7 @@ public abstract class JdbcAbstractDmlStatementSelfTest extends GridCommonAbstrac CFG_URL_PREFIX + "cache=" + DEFAULT_CACHE_NAME + "@modules/clients/src/test/config/jdbc-bin-config.xml"; /** SQL SELECT query for verification. */ - static final String SQL_SELECT = "select _key, id, firstName, lastName, age, data from Person"; + static final String SQL_SELECT = "select _key, id, firstName, lastName, age, data, text from Person"; /** Alias for _key */ private static final String KEY_ALIAS = "key"; @@ -112,6 +114,7 @@ final CacheConfiguration binaryCacheConfig() { e.addQueryField("firstName", String.class.getName(), null); e.addQueryField("lastName", String.class.getName(), null); e.addQueryField("data", byte[].class.getName(), null); + e.addQueryField("text", String.class.getName(), null); cache.setQueryEntities(Collections.singletonList(e)); @@ -189,6 +192,18 @@ static String str(byte[] arr) { } } + /** + * @param clob Clob. + */ + static String str(Clob clob) { + try { + return clob.getSubString(1, (int)clob.length()); + } + catch (SQLException e) { + throw new RuntimeException(e); + } + } + /** * Person. */ @@ -213,6 +228,10 @@ static class Person implements Serializable { @QuerySqlField private final byte[] data; + /** CLOB data. */ + @QuerySqlField + private final String text; + /** * @param id ID. * @param firstName First name. @@ -229,6 +248,7 @@ static class Person implements Serializable { this.lastName = lastName; this.age = age; this.data = getBytes(lastName); + this.text = firstName + " " + lastName; } /** {@inheritDoc} */ @@ -241,6 +261,8 @@ static class Person implements Serializable { if (id != person.id) return false; if (age != person.age) return false; if (firstName != null ? !firstName.equals(person.firstName) : person.firstName != null) return false; + if (data != null ? !Arrays.equals(data, person.data) : person.data != null) return false; + if (text != null ? !text.equals(person.text) : person.text != null) return false; return lastName != null ? lastName.equals(person.lastName) : person.lastName == null; } @@ -251,6 +273,8 @@ static class Person implements Serializable { result = 31 * result + (firstName != null ? firstName.hashCode() : 0); result = 31 * result + (lastName != null ? lastName.hashCode() : 0); result = 31 * result + age; + result = 31 * result + (data != null ? Arrays.hashCode(data) : 0); + result = 31 * result + (text != null ? text.hashCode() : 0); return result; } } diff --git a/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcAbstractUpdateStatementSelfTest.java b/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcAbstractUpdateStatementSelfTest.java index ace1be665614c..66d81a5d0bfab 100644 --- a/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcAbstractUpdateStatementSelfTest.java +++ b/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcAbstractUpdateStatementSelfTest.java @@ -24,10 +24,10 @@ */ public abstract class JdbcAbstractUpdateStatementSelfTest extends JdbcAbstractDmlStatementSelfTest { /** SQL query to populate cache. */ - private static final String ITEMS_SQL = "insert into Person(_key, id, firstName, lastName, age, data) values " + - "('p1', 1, 'John', 'White', 25, RAWTOHEX('White')), " + - "('p2', 2, 'Joe', 'Black', 35, RAWTOHEX('Black')), " + - "('p3', 3, 'Mike', 'Green', 40, RAWTOHEX('Green'))"; + private static final String ITEMS_SQL = "insert into Person(_key, id, firstName, lastName, age, data, text) values " + + "('p1', 1, 'John', 'White', 25, RAWTOHEX('White'), 'John White'), " + + "('p2', 2, 'Joe', 'Black', 35, RAWTOHEX('Black'), 'Joe Black'), " + + "('p3', 3, 'Mike', 'Green', 40, RAWTOHEX('Green'), 'Mike Green')"; /** {@inheritDoc} */ @Override protected void beforeTest() throws Exception { diff --git a/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcBlobTest.java b/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcBlobTest.java index 1680161e07a07..0b0119e48e9dc 100644 --- a/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcBlobTest.java +++ b/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcBlobTest.java @@ -113,6 +113,9 @@ public void testGetBytes() throws Exception { res = blob.getBytes(1, 0); assertEquals(0, res.length); + blob = new JdbcBlob(new byte[0]); + assertEquals(0, blob.getBytes(1, 0).length); + blob.free(); try { diff --git a/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcClobTest.java b/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcClobTest.java new file mode 100644 index 0000000000000..6dc5826d6e61f --- /dev/null +++ b/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcClobTest.java @@ -0,0 +1,452 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.internal.jdbc2; + +import java.io.InputStream; +import java.io.Reader; +import java.sql.Clob; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import org.apache.commons.io.IOUtils; +import org.junit.Assert; +import org.junit.Test; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.ignite.testframework.GridTestUtils.assertThrows; +import static org.junit.Assert.assertEquals; + +/** + * Test for JDBC CLOB. + */ +public class JdbcClobTest { + /** */ + static final String ERROR_CLOB_FREE = "Clob instance can't be used after free() has been called."; + + /** + * @throws Exception If failed. + */ + @Test + public void testLength() throws Exception { + JdbcClob clob = new JdbcClob("1234567890"); + + assertEquals(10, clob.length()); + + clob.free(); + assertThrows(null, clob::length, SQLException.class, ERROR_CLOB_FREE); + } + + /** + * @throws Exception If failed. + */ + @Test + public void testGetSubString() throws Exception { + JdbcClob clob = new JdbcClob("1234567890"); + + assertThrows(null, () -> clob.getSubString(-1, 1), SQLException.class, null); + + assertThrows(null, () -> clob.getSubString(0, 1), SQLException.class, null); + + assertThrows(null, () -> clob.getSubString(1, -1), SQLException.class, null); + + assertThrows(null, () -> clob.getSubString(1, 11), SQLException.class, null); + + assertEquals("", clob.getSubString(3, 0)); + + assertEquals("1", clob.getSubString(1, 1)); + + assertEquals("0", clob.getSubString(10, 1)); + + assertEquals("12345", clob.getSubString(1, 5)); + + assertEquals("34567", clob.getSubString(3, 5)); + + assertEquals("567890", clob.getSubString(5, 6)); + + assertEquals("1234567890", clob.getSubString(1, 10)); + + clob.free(); + assertThrows(null, () -> clob.getSubString(1, 10), SQLException.class, ERROR_CLOB_FREE); + } + + /** + * @throws Exception If failed. + */ + @Test + public void testGetCharacterStream() throws Exception { + JdbcClob clob = new JdbcClob("1234567890"); + + Reader cStream = clob.getCharacterStream(); + String res = IOUtils.toString(cStream); + assertEquals("1234567890", res); + + clob.free(); + assertThrows(null, () -> clob.getCharacterStream(), SQLException.class, ERROR_CLOB_FREE); + } + + /** + * @throws Exception If failed. + */ + @Test + public void testGetCharacterStreamWithParams() throws Exception { + JdbcClob clob = new JdbcClob("1234567890"); + + assertThrows(null, () -> clob.getCharacterStream(-1, 1), SQLException.class, null); + + assertThrows(null, () -> clob.getCharacterStream(0, 1), SQLException.class, null); + + assertThrows(null, () -> clob.getCharacterStream(1, -1), SQLException.class, null); + + assertThrows(null, () -> clob.getCharacterStream(1, 11), SQLException.class, null); + + Reader cStream = clob.getCharacterStream(1, 10); + String res = IOUtils.toString(cStream); + assertEquals("1234567890", res); + + cStream = clob.getCharacterStream(1, 1); + res = IOUtils.toString(cStream); + assertEquals("1", res); + + cStream = clob.getCharacterStream(10, 1); + res = IOUtils.toString(cStream); + assertEquals("0", res); + + cStream = clob.getCharacterStream(3, 5); + res = IOUtils.toString(cStream); + assertEquals("34567", res); + + cStream = clob.getCharacterStream(3, 0); + res = IOUtils.toString(cStream); + assertEquals("", res); + + clob.free(); + assertThrows(null, () -> clob.getCharacterStream(1, 1), SQLException.class, ERROR_CLOB_FREE); + } + + /** + * @throws Exception If failed. + */ + @Test + public void testGetAsciiStream() throws Exception { + JdbcClob clob = new JdbcClob("1234567890"); + byte[] bytes = IOUtils.toByteArray(clob.getAsciiStream()); + Assert.assertArrayEquals("1234567890".getBytes(UTF_8), bytes); + + clob.free(); + assertThrows(null, clob::getAsciiStream, SQLException.class, ERROR_CLOB_FREE); + + Clob emptyClob = new JdbcClob(""); + bytes = IOUtils.toByteArray(emptyClob.getAsciiStream()); + Assert.assertArrayEquals("".getBytes(UTF_8), bytes); + } + + /** + * @throws Exception If failed. + */ + @Test + public void testGetAsciiStreamForNonAsciiDataBufferedRead() throws Exception { + StringBuilder sb = new StringBuilder(); + + // Create string in a way which makes sure that all variants in + // JdbcClob.Utf8EncodedStringInputStream.encodeNextChunk() are covered. + // In particular the check for the surrogate element. + for (int i = 0; i < 3277; i++) { + sb.append("aa©😀"); + } + + Clob clob = new JdbcClob(sb.toString()); + + InputStream stream = clob.getAsciiStream(); + + assertThrows(null, () -> stream.read(null, 0, 1), NullPointerException.class, null); + + assertThrows(null, () -> stream.read(new byte[10], -1, 5), IndexOutOfBoundsException.class, null); + + assertThrows(null, () -> stream.read(new byte[10], 5, -1), IndexOutOfBoundsException.class, null); + + assertThrows(null, () -> stream.read(new byte[10], 11, 1), IndexOutOfBoundsException.class, null); + + assertThrows(null, () -> stream.read(new byte[10], 5, 6), IndexOutOfBoundsException.class, null); + + assertEquals(0, stream.read(new byte[10], 5, 0)); + + byte[] bytes = IOUtils.toByteArray(stream); + + String reencoded = new String(bytes, UTF_8); + + assertEquals(clob.getSubString(1, (int)clob.length()), reencoded); + } + + /** + * @throws Exception If failed. + */ + @Test + public void testGetAsciiStreamForNonAsciiDataReadByByte() throws Exception { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10; i++) { + sb.append("aa©😀"); + } + + Clob clob = new JdbcClob(sb.toString()); + + InputStream stream = clob.getAsciiStream(); + + int i = 0; + byte[] bytes = new byte[80]; + + byte val = (byte)stream.read(); + + while (val != -1) { + bytes[i++] = val; + + val = (byte)stream.read(); + } + + String reencoded = new String(bytes, UTF_8); + + assertEquals(clob.getSubString(1, (int)clob.length()), reencoded); + } + + /** + * @throws Exception If failed. + */ + @Test + public void testPositionWithStringPattern() throws Exception { + JdbcClob clob1 = new JdbcClob("1234567890"); + + assertThrows(null, () -> clob1.position("0", 0), SQLException.class, null); + + assertThrows(null, () -> clob1.position("0", -1), SQLException.class, null); + + assertEquals(1, clob1.position("", 1)); + + assertEquals(10, clob1.position("", 10)); + + assertEquals(11, clob1.position("", 100)); + + assertEquals(-1, clob1.position("a", 11)); + + assertEquals(1, clob1.position("1", 1)); + + assertEquals(5, clob1.position("56", 1)); + + assertEquals(5, clob1.position("56", 5)); + + assertEquals(-1, clob1.position("56", 6)); + + clob1.free(); + assertThrows(null, clob1::getAsciiStream, SQLException.class, ERROR_CLOB_FREE); + + Clob clob2 = new JdbcClob("abbabab"); + + assertEquals(5, clob2.position("b", 4)); + } + + /** + * @throws Exception If failed. + */ + @Test + public void testPositionWithClobPattern() throws Exception { + Clob clob = new JdbcClob("1234567890"); + + Clob patternClob = new JdbcClob("567"); + + assertThrows(null, () -> clob.position(patternClob, 0), SQLException.class, null); + + assertThrows(null, () -> clob.position(patternClob, -1), SQLException.class, null); + + assertEquals(5, clob.position(patternClob, 1)); + + assertEquals(5, clob.position(patternClob, 5)); + + assertEquals(-1, clob.position(patternClob, 6)); + + Clob patternClob2 = new JdbcClob("a"); + + assertEquals(-1, clob.position(patternClob2, 1)); + + clob.free(); + assertThrows(null, () -> clob.position(patternClob2, 5), SQLException.class, ERROR_CLOB_FREE); + + Clob clob2 = new JdbcClob("bbabbabba"); + + assertEquals(6, clob2.position(patternClob2, 5)); + } + + /** + * @throws Exception If failed. + */ + @Test + public void testSetString() throws Exception { + JdbcClob clob1 = new JdbcClob("1234567890"); + + assertThrows(null, () -> clob1.setString(-1, "a"), SQLException.class, null); + + assertThrows(null, () -> clob1.setString(0, "a"), SQLException.class, null); + + assertThrows(null, () -> clob1.setString(clob1.length() + 2, "a"), SQLException.class, null); + + assertThrows(null, () -> clob1.setString(1, null), SQLException.class, null); + + int written = clob1.setString(1, "a"); + assertEquals("a", clob1.getSubString(1, 1)); + assertEquals(1, written); + + written = clob1.setString(5, "abc"); + assertEquals("abc", clob1.getSubString(5, 3)); + assertEquals(3, written); + + written = clob1.setString(10, "def"); + assertEquals("def", clob1.getSubString(10, 3)); + assertEquals(3, written); + + clob1.free(); + assertThrows(null, () -> clob1.setString(1, "a"), SQLException.class, ERROR_CLOB_FREE); + + Clob clob2 = new JdbcClob("12345"); + written = clob2.setString(3, "abcd"); + assertEquals("12abcd", clob2.getSubString(1, (int)clob2.length())); + assertEquals(4, written); + + Clob clob3 = new JdbcClob("12345"); + written = clob3.setString(3, "ab"); + assertEquals("12ab5", clob3.getSubString(1, (int)clob3.length())); + assertEquals(2, written); + } + + /** + * @throws Exception If failed. + */ + @Test + public void testSetStringWithSubString() throws Exception { + JdbcClob clob = new JdbcClob("1234567890"); + + assertThrows(null, () -> clob.setString(-1, "a", 0, 1), SQLException.class, null); + + assertThrows(null, () -> clob.setString(0, "a", 0, 1), SQLException.class, null); + + assertThrows(null, () -> clob.setString(clob.length() + 2, "a", 0, 1), SQLException.class, null); + + assertThrows(null, () -> clob.setString(1, null, 0, 1), SQLException.class, null); + + assertThrows(null, () -> clob.setString(1, "a", -1, 1), SQLException.class, null); + + assertThrows(null, () -> clob.setString(1, "a", 0, -1), SQLException.class, null); + + assertThrows(null, () -> clob.setString(1, "abc", 1, 3), SQLException.class, null); + + clob.free(); + assertThrows(null, () -> clob.setString(1, "a", 0, 1), SQLException.class, ERROR_CLOB_FREE); + + Clob clob2 = new JdbcClob("1234567890"); + int written = clob2.setString(3, "abcd", 0, 1); + assertEquals("12a4567890", clob2.getSubString(1, (int)clob2.length())); + assertEquals(1, written); + + clob2 = new JdbcClob("1234567890"); + written = clob2.setString(1, "abcd", 0, 3); + assertEquals("abc4567890", clob2.getSubString(1, (int)clob2.length())); + assertEquals(3, written); + + clob2 = new JdbcClob("1234567890"); + written = clob2.setString(5, "abcd", 2, 2); + assertEquals("1234cd7890", clob2.getSubString(1, (int)clob2.length())); + assertEquals(2, written); + + clob2 = new JdbcClob("1234567890"); + written = clob2.setString(9, "abcd", 0, 4); + assertEquals("12345678abcd", clob2.getSubString(1, (int)clob2.length())); + assertEquals(4, written); + + clob2 = new JdbcClob("1234567890"); + written = clob2.setString(11, "abcd", 0, 4); + assertEquals("1234567890abcd", clob2.getSubString(1, (int)clob2.length())); + assertEquals(4, written); + } + + /** + * @throws Exception If failed. + */ + @Test + public void testTruncate() throws Exception { + JdbcClob clob = new JdbcClob("1234567890"); + + assertThrows(null, () -> { + clob.truncate(-1); + + return null; + }, SQLException.class, null); + + assertThrows(null, () -> { + clob.truncate(clob.length() + 1); + + return null; + }, SQLException.class, null); + + clob.truncate(9); + assertEquals("123456789", clob.getSubString(1, (int)clob.length())); + + clob.truncate(5); + assertEquals("12345", clob.getSubString(1, (int)clob.length())); + + clob.truncate(0); + assertEquals("", clob.getSubString(1, (int)clob.length())); + + clob.free(); + assertThrows(null, () -> { + clob.truncate(1); + + return null; + }, SQLException.class, ERROR_CLOB_FREE); + } + + /** + * @throws Exception If failed. + */ + @Test + public void testSetAsciiStream() throws Exception { + JdbcClob clob = new JdbcClob("1234567890"); + + assertThrows(null, () -> clob.setAsciiStream(1L), SQLFeatureNotSupportedException.class, null); + } + + /** + * @throws Exception If failed. + */ + @Test + public void testSetCharacterStream() throws Exception { + JdbcClob clob = new JdbcClob("1234567890"); + + assertThrows(null, () -> clob.setCharacterStream(1L), SQLFeatureNotSupportedException.class, null); + } + + /** + * @throws Exception If failed. + */ + @Test + public void testFree() throws Exception { + JdbcClob clob = new JdbcClob("1234567890"); + + clob.length(); + + clob.free(); + + clob.free(); + + assertThrows(null, clob::length, SQLException.class, ERROR_CLOB_FREE); + } +} diff --git a/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcInsertStatementSelfTest.java b/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcInsertStatementSelfTest.java index 6b39ccb587d5b..072155f4d3e42 100644 --- a/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcInsertStatementSelfTest.java +++ b/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcInsertStatementSelfTest.java @@ -18,6 +18,8 @@ package org.apache.ignite.internal.jdbc2; import java.sql.BatchUpdateException; +import java.sql.Blob; +import java.sql.Clob; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -36,14 +38,14 @@ */ public class JdbcInsertStatementSelfTest extends JdbcAbstractDmlStatementSelfTest { /** SQL query. */ - private static final String SQL = "insert into Person(_key, id, firstName, lastName, age, data) values " + - "('p1', 1, 'John', 'White', 25, RAWTOHEX('White')), " + - "('p2', 2, 'Joe', 'Black', 35, RAWTOHEX('Black')), " + - "('p3', 3, 'Mike', 'Green', 40, RAWTOHEX('Green'))"; + private static final String SQL = "insert into Person(_key, id, firstName, lastName, age, data, text) values " + + "('p1', 1, 'John', 'White', 25, RAWTOHEX('White'), 'John White'), " + + "('p2', 2, 'Joe', 'Black', 35, RAWTOHEX('Black'), 'Joe Black'), " + + "('p3', 3, 'Mike', 'Green', 40, RAWTOHEX('Green'), 'Mike Green')"; /** SQL query. */ - private static final String SQL_PREPARED = "insert into Person(_key, id, firstName, lastName, age, data) values " + - "(?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?)"; + private static final String SQL_PREPARED = "insert into Person(_key, id, firstName, lastName, age, data, text) " + + "values (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?)"; /** Statement. */ private Statement stmt; @@ -83,6 +85,7 @@ public class JdbcInsertStatementSelfTest extends JdbcAbstractDmlStatementSelfTes assertEquals("White", rs.getString("lastName")); assertEquals(25, rs.getInt("age")); assertEquals("White", str(getBytes(rs.getBlob("data")))); + assertEquals("John White", str(rs.getClob("text"))); break; case 2: @@ -91,6 +94,7 @@ public class JdbcInsertStatementSelfTest extends JdbcAbstractDmlStatementSelfTes assertEquals("Black", rs.getString("lastName")); assertEquals(35, rs.getInt("age")); assertEquals("Black", str(getBytes(rs.getBlob("data")))); + assertEquals("Joe Black", str(rs.getClob("text"))); break; case 3: @@ -99,6 +103,7 @@ public class JdbcInsertStatementSelfTest extends JdbcAbstractDmlStatementSelfTes assertEquals("Green", rs.getString("lastName")); assertEquals(40, rs.getInt("age")); assertEquals("Green", str(getBytes(rs.getBlob("data")))); + assertEquals("Mike Green", str(rs.getClob("text"))); break; case 4: @@ -107,6 +112,7 @@ public class JdbcInsertStatementSelfTest extends JdbcAbstractDmlStatementSelfTes assertEquals("Grey", rs.getString("lastName")); assertEquals(22, rs.getInt("age")); assertEquals("Grey", str(getBytes(rs.getBlob("data")))); + assertEquals("Leah Grey", str(rs.getClob("text"))); break; default: @@ -302,6 +308,9 @@ public void testClearBatch() throws Exception { private void formBatch(int id1, int id2) throws SQLException { int[] ids = new int[] { id1, id2 }; + Clob clob; + Blob blob; + int arg = 0; for (int id: ids) { String key = "p" + id; @@ -313,7 +322,14 @@ private void formBatch(int id1, int id2) throws SQLException { prepStmt.setString(arg + 3, "John"); prepStmt.setString(arg + 4, "White"); prepStmt.setInt(arg + 5, 25); - prepStmt.setBytes(arg + 6, getBytes("White")); + + blob = conn.createBlob(); + blob.setBytes(1, getBytes("White")); + prepStmt.setBlob(arg + 6, blob); + + clob = conn.createClob(); + clob.setString(1, "John White"); + prepStmt.setClob(arg + 7, clob); break; @@ -323,7 +339,14 @@ private void formBatch(int id1, int id2) throws SQLException { prepStmt.setString(arg + 3, "Joe"); prepStmt.setString(arg + 4, "Black"); prepStmt.setInt(arg + 5, 35); - prepStmt.setBytes(arg + 6, getBytes("Black")); + + blob = conn.createBlob(); + blob.setBytes(1, getBytes("Black")); + prepStmt.setBlob(arg + 6, blob); + + clob = conn.createClob(); + clob.setString(1, "Joe Black"); + prepStmt.setClob(arg + 7, clob); break; @@ -333,7 +356,14 @@ private void formBatch(int id1, int id2) throws SQLException { prepStmt.setString(arg + 3, "Mike"); prepStmt.setString(arg + 4, "Green"); prepStmt.setInt(arg + 5, 40); - prepStmt.setBytes(arg + 6, getBytes("Green")); + + blob = conn.createBlob(); + blob.setBytes(1, getBytes("Green")); + prepStmt.setBlob(arg + 6, blob); + + clob = conn.createClob(); + clob.setString(1, "Mike Green"); + prepStmt.setClob(arg + 7, clob); break; @@ -343,7 +373,14 @@ private void formBatch(int id1, int id2) throws SQLException { prepStmt.setString(arg + 3, "Leah"); prepStmt.setString(arg + 4, "Grey"); prepStmt.setInt(arg + 5, 22); - prepStmt.setBytes(arg + 6, getBytes("Grey")); + + blob = conn.createBlob(); + blob.setBytes(1, getBytes("Grey")); + prepStmt.setBlob(arg + 6, blob); + + clob = conn.createClob(); + clob.setString(1, "Leah Grey"); + prepStmt.setClob(arg + 7, clob); break; @@ -351,7 +388,7 @@ private void formBatch(int id1, int id2) throws SQLException { assert false; } - arg += 6; + arg += 7; } prepStmt.addBatch(); diff --git a/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcMergeStatementSelfTest.java b/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcMergeStatementSelfTest.java index 3923333a4e568..c5da516d957c6 100644 --- a/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcMergeStatementSelfTest.java +++ b/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcMergeStatementSelfTest.java @@ -30,14 +30,14 @@ */ public class JdbcMergeStatementSelfTest extends JdbcAbstractDmlStatementSelfTest { /** SQL query. */ - private static final String SQL = "merge into Person(_key, id, firstName, lastName, age, data) values " + - "('p1', 1, 'John', 'White', 25, RAWTOHEX('White')), " + - "('p2', 2, 'Joe', 'Black', 35, RAWTOHEX('Black')), " + - "('p3', 3, 'Mike', 'Green', 40, RAWTOHEX('Green'))"; + private static final String SQL = "merge into Person(_key, id, firstName, lastName, age, data, text) values " + + "('p1', 1, 'John', 'White', 25, RAWTOHEX('White'), 'John White'), " + + "('p2', 2, 'Joe', 'Black', 35, RAWTOHEX('Black'), 'Joe Black'), " + + "('p3', 3, 'Mike', 'Green', 40, RAWTOHEX('Green'), 'Mike Green')"; /** SQL query. */ - protected static final String SQL_PREPARED = "merge into Person(_key, id, firstName, lastName, age, data) values " + - "(?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?)"; + protected static final String SQL_PREPARED = "merge into Person(_key, id, firstName, lastName, age, data, text) " + + "values (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?)"; /** Statement. */ protected Statement stmt; @@ -77,6 +77,7 @@ public class JdbcMergeStatementSelfTest extends JdbcAbstractDmlStatementSelfTest assertEquals("White", rs.getString("lastName")); assertEquals(25, rs.getInt("age")); assertEquals("White", str(getBytes(rs.getBlob("data")))); + assertEquals("John White", str(rs.getClob("text"))); break; case 2: @@ -85,6 +86,7 @@ public class JdbcMergeStatementSelfTest extends JdbcAbstractDmlStatementSelfTest assertEquals("Black", rs.getString("lastName")); assertEquals(35, rs.getInt("age")); assertEquals("Black", str(getBytes(rs.getBlob("data")))); + assertEquals("Joe Black", str(rs.getClob("text"))); break; case 3: @@ -93,6 +95,7 @@ public class JdbcMergeStatementSelfTest extends JdbcAbstractDmlStatementSelfTest assertEquals("Green", rs.getString("lastName")); assertEquals(40, rs.getInt("age")); assertEquals("Green", str(getBytes(rs.getBlob("data")))); + assertEquals("Mike Green", str(rs.getClob("text"))); break; case 4: @@ -101,6 +104,7 @@ public class JdbcMergeStatementSelfTest extends JdbcAbstractDmlStatementSelfTest assertEquals("Grey", rs.getString("lastName")); assertEquals(22, rs.getInt("age")); assertEquals("Grey", str(getBytes(rs.getBlob("data")))); + assertEquals("Leah Grey", str(rs.getClob("text"))); break; default: @@ -159,13 +163,15 @@ public void testBatch() throws SQLException { prepStmt.setString(4, "White"); prepStmt.setInt(5, 25); prepStmt.setBytes(6, getBytes("White")); - - prepStmt.setString(7, "p2"); - prepStmt.setInt(8, 2); - prepStmt.setString(9, "Joe"); - prepStmt.setString(10, "Black"); - prepStmt.setInt(11, 35); - prepStmt.setBytes(12, getBytes("Black")); + prepStmt.setString(7, "John White"); + + prepStmt.setString(8, "p2"); + prepStmt.setInt(9, 2); + prepStmt.setString(10, "Joe"); + prepStmt.setString(11, "Black"); + prepStmt.setInt(12, 35); + prepStmt.setBytes(13, getBytes("Black")); + prepStmt.setString(14, "Joe Black"); prepStmt.addBatch(); prepStmt.setString(1, "p3"); @@ -174,13 +180,15 @@ public void testBatch() throws SQLException { prepStmt.setString(4, "Green"); prepStmt.setInt(5, 40); prepStmt.setBytes(6, getBytes("Green")); - - prepStmt.setString(7, "p4"); - prepStmt.setInt(8, 4); - prepStmt.setString(9, "Leah"); - prepStmt.setString(10, "Grey"); - prepStmt.setInt(11, 22); - prepStmt.setBytes(12, getBytes("Grey")); + prepStmt.setString(7, "Mike Green"); + + prepStmt.setString(8, "p4"); + prepStmt.setInt(9, 4); + prepStmt.setString(10, "Leah"); + prepStmt.setString(11, "Grey"); + prepStmt.setInt(12, 22); + prepStmt.setBytes(13, getBytes("Grey")); + prepStmt.setString(14, "Leah Grey"); prepStmt.addBatch(); diff --git a/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcPreparedStatementSelfTest.java b/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcPreparedStatementSelfTest.java index 98998d21f5466..4cbee28c77ba5 100644 --- a/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcPreparedStatementSelfTest.java +++ b/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcPreparedStatementSelfTest.java @@ -21,6 +21,7 @@ import java.math.BigDecimal; import java.net.URL; import java.sql.Blob; +import java.sql.Clob; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; @@ -38,7 +39,9 @@ import static java.sql.Types.BIGINT; import static java.sql.Types.BINARY; +import static java.sql.Types.BLOB; import static java.sql.Types.BOOLEAN; +import static java.sql.Types.CLOB; import static java.sql.Types.DATALINK; import static java.sql.Types.DATE; import static java.sql.Types.DOUBLE; @@ -108,6 +111,7 @@ public class JdbcPreparedStatementSelfTest extends GridCommonAbstractTest { o.strVal = "str"; o.arrVal = new byte[] {1}; o.blobVal = new byte[] {1}; + o.clobVal = "large str"; o.dateVal = new Date(1); o.timeVal = new Time(1); o.tsVal = new Timestamp(1); @@ -571,31 +575,45 @@ public void testBlob() throws Exception { ResultSet rs = stmt.executeQuery(); - int cnt = 0; + assertTrue(rs.next()); + assertEquals(1, rs.getInt("id")); + assertFalse(rs.next()); - while (rs.next()) { - if (cnt == 0) - assert rs.getInt("id") == 1; + stmt.setNull(1, BLOB); - cnt++; - } + rs = stmt.executeQuery(); - assertEquals(1, cnt); + assertTrue(rs.next()); + assertEquals(2, rs.getInt("id")); + assertFalse(rs.next()); + } - stmt.setNull(1, BINARY); + /** + * @throws Exception If failed. + */ + @Test + public void testClob() throws Exception { + stmt = conn.prepareStatement("select * from TestObject where clobVal is not distinct from ?"); - rs = stmt.executeQuery(); + Clob clob = conn.createClob(); - cnt = 0; + clob.setString(1, "large str"); - while (rs.next()) { - if (cnt == 0) - assert rs.getInt("id") == 2; + stmt.setClob(1, clob); - cnt++; - } + ResultSet rs = stmt.executeQuery(); - assert cnt == 1; + assertTrue(rs.next()); + assertEquals(1, rs.getInt("id")); + assertFalse(rs.next()); + + stmt.setNull(1, CLOB); + + rs = stmt.executeQuery(); + + assertTrue(rs.next()); + assertEquals(2, rs.getInt("id")); + assertFalse(rs.next()); } /** @@ -802,6 +820,10 @@ private static class TestObject implements Serializable { @QuerySqlField private byte[] blobVal; + /** */ + @QuerySqlField + private String clobVal; + /** */ @QuerySqlField private Date dateVal; diff --git a/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcResultSetSelfTest.java b/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcResultSetSelfTest.java index e65bacdc35cfb..1470a5cfcf3bf 100644 --- a/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcResultSetSelfTest.java +++ b/modules/clients/src/test/java/org/apache/ignite/internal/jdbc2/JdbcResultSetSelfTest.java @@ -22,6 +22,8 @@ import java.math.BigDecimal; import java.net.MalformedURLException; import java.net.URL; +import java.sql.Blob; +import java.sql.Clob; import java.sql.Date; import java.sql.DriverManager; import java.sql.ResultSet; @@ -46,6 +48,7 @@ import org.apache.ignite.testframework.GridTestUtils; import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; import org.jetbrains.annotations.Nullable; +import org.junit.Assert; import org.junit.Test; import static org.apache.ignite.IgniteJdbcDriver.CFG_URL_PREFIX; @@ -64,7 +67,7 @@ public class JdbcResultSetSelfTest extends GridCommonAbstractTest { private static final String SQL = "select id, boolVal, byteVal, shortVal, intVal, longVal, floatVal, " + "doubleVal, bigVal, strVal, arrVal, dateVal, timeVal, tsVal, urlVal, f1, f2, f3, _val, " + - "boolVal2, boolVal3, boolVal4 " + + "boolVal2, boolVal3, boolVal4, blobVal, clobVal " + "from TestObject where id = 1"; /** Statement. */ @@ -145,6 +148,8 @@ private TestObject createObjectWithData(int id) throws MalformedURLException { o.bigVal = new BigDecimal(1); o.strVal = "1"; o.arrVal = new byte[] {1}; + o.blobVal = new byte[] {1}; + o.clobVal = "str"; o.dateVal = new Date(1, 1, 1); o.timeVal = new Time(1, 1, 1); o.tsVal = new Timestamp(1); @@ -670,6 +675,38 @@ public void testArray() throws Exception { assert cnt == 1; } + /** + * @throws Exception If failed. + */ + @Test + public void testBlob() throws Exception { + ResultSet rs = stmt.executeQuery(SQL); + + assertTrue(rs.next()); + Blob blob = rs.getBlob("blobVal"); + Assert.assertArrayEquals(blob.getBytes(1, (int)blob.length()), new byte[] {1}); + + blob = rs.getBlob(23); + Assert.assertArrayEquals(blob.getBytes(1, (int)blob.length()), new byte[] {1}); + assertFalse(rs.next()); + } + + /** + * @throws Exception If failed. + */ + @Test + public void testClob() throws Exception { + ResultSet rs = stmt.executeQuery(SQL); + + assertTrue(rs.next()); + Clob clob = rs.getClob("clobVal"); + Assert.assertEquals("str", clob.getSubString(1, (int)clob.length())); + + clob = rs.getClob(24); + Assert.assertEquals("str", clob.getSubString(1, (int)clob.length())); + assertFalse(rs.next()); + } + /** * @throws Exception If failed. */ @@ -990,6 +1027,14 @@ private static class TestObject extends BaseTestObject { @QuerySqlField(index = false) private byte[] arrVal; + /** */ + @QuerySqlField(index = false) + private byte[] blobVal; + + /** */ + @QuerySqlField(index = false) + private String clobVal; + /** */ @QuerySqlField(index = false) private Date dateVal; @@ -1056,6 +1101,8 @@ private TestObject(int id) { if (timeVal != null ? !timeVal.equals(that.timeVal) : that.timeVal != null) return false; if (tsVal != null ? !tsVal.equals(that.tsVal) : that.tsVal != null) return false; if (urlVal != null ? !urlVal.equals(that.urlVal) : that.urlVal != null) return false; + if (!Arrays.equals(blobVal, that.blobVal)) return false; + if (clobVal != null ? !clobVal.equals(that.clobVal) : that.clobVal != null) return false; return true; } @@ -1082,6 +1129,8 @@ private TestObject(int id) { res = 31 * res + (f1 != null ? f1.hashCode() : 0); res = 31 * res + (f2 != null ? f2.hashCode() : 0); res = 31 * res + (f3 != null ? f3.hashCode() : 0); + res = 31 * res + (blobVal != null ? Arrays.hashCode(blobVal) : 0); + res = 31 * res + (clobVal != null ? clobVal.hashCode() : 0); return res; } diff --git a/modules/clients/src/test/java/org/apache/ignite/jdbc/suite/IgniteJdbcDriverTestSuite.java b/modules/clients/src/test/java/org/apache/ignite/jdbc/suite/IgniteJdbcDriverTestSuite.java index 450db60030de1..f5b133e5fade8 100644 --- a/modules/clients/src/test/java/org/apache/ignite/jdbc/suite/IgniteJdbcDriverTestSuite.java +++ b/modules/clients/src/test/java/org/apache/ignite/jdbc/suite/IgniteJdbcDriverTestSuite.java @@ -21,6 +21,7 @@ import org.apache.ignite.common.RunningQueryInfoCheckInitiatorTest; import org.apache.ignite.internal.jdbc2.JdbcBlobTest; import org.apache.ignite.internal.jdbc2.JdbcBulkLoadSelfTest; +import org.apache.ignite.internal.jdbc2.JdbcClobTest; import org.apache.ignite.internal.jdbc2.JdbcConnectionReopenTest; import org.apache.ignite.internal.jdbc2.JdbcDistributedJoinsQueryTest; import org.apache.ignite.internal.jdbc2.JdbcSchemaCaseSelfTest; @@ -125,6 +126,7 @@ JdbcBulkLoadSelfTest.class, JdbcSchemaCaseSelfTest.class, + JdbcClobTest.class, JdbcBlobTest.class, org.apache.ignite.internal.jdbc2.JdbcStreamingSelfTest.class, JdbcThinStreamingNotOrderedSelfTest.class, diff --git a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinAbstractDmlStatementSelfTest.java b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinAbstractDmlStatementSelfTest.java index 522fddd7aa1ce..a69b7a70d4a7b 100644 --- a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinAbstractDmlStatementSelfTest.java +++ b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinAbstractDmlStatementSelfTest.java @@ -18,9 +18,12 @@ package org.apache.ignite.jdbc.thin; import java.io.Serializable; +import java.sql.Blob; +import java.sql.Clob; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; +import java.util.Arrays; import java.util.Collections; import org.apache.ignite.cache.QueryEntity; import org.apache.ignite.cache.query.annotations.QuerySqlField; @@ -30,6 +33,7 @@ import org.apache.ignite.internal.binary.BinaryMarshaller; import org.apache.ignite.internal.util.typedef.F; +import static java.nio.charset.StandardCharsets.UTF_16; import static org.apache.ignite.cache.CacheMode.PARTITIONED; import static org.apache.ignite.cache.CacheWriteSynchronizationMode.FULL_SYNC; @@ -38,7 +42,7 @@ */ public abstract class JdbcThinAbstractDmlStatementSelfTest extends JdbcThinAbstractSelfTest { /** SQL SELECT query for verification. */ - static final String SQL_SELECT = "select _key, id, firstName, lastName, age from Person"; + static final String SQL_SELECT = "select _key, id, firstName, lastName, age, data, text from Person"; /** Connection. */ protected Connection conn; @@ -117,6 +121,8 @@ IgniteConfiguration getBinaryConfiguration(String igniteInstanceName) throws Exc e.addQueryField("age", Integer.class.getName(), null); e.addQueryField("firstName", String.class.getName(), null); e.addQueryField("lastName", String.class.getName(), null); + e.addQueryField("data", byte[].class.getName(), null); + e.addQueryField("text", String.class.getName(), null); ccfg.setQueryEntities(Collections.singletonList(e)); @@ -158,6 +164,8 @@ final CacheConfiguration binaryCacheConfig() { e.addQueryField("age", Integer.class.getName(), null); e.addQueryField("firstName", String.class.getName(), null); e.addQueryField("lastName", String.class.getName(), null); + e.addQueryField("data", byte[].class.getName(), null); + e.addQueryField("text", String.class.getName(), null); cache.setQueryEntities(Collections.singletonList(e)); @@ -171,6 +179,49 @@ CacheConfiguration cacheConfig() { return nonBinCacheConfig(); } + /** + * Helper to get test binary data as string UTF-16 encoding to be in sync with the RAWTOHEX function + * which uses UTF-16 for conversion strings to byte arrays. + * @param str String. + * @return Byte array with the UTF-16 encoding. + */ + static byte[] getBytes(String str) { + return str.getBytes(UTF_16); + } + + /** + * Helper to convert a binary data (which is a string UTF-16 encoding) back to string. + * @param arr Byte array with the UTF-16 encoding. + * @return String. + */ + static String str(byte[] arr) { + return new String(arr, UTF_16); + } + + /** + * @param blob Blob. + */ + static byte[] getBytes(Blob blob) { + try { + return blob.getBytes(1, (int)blob.length()); + } + catch (SQLException e) { + throw new RuntimeException(e); + } + } + + /** + * @param clob Clob. + */ + static String str(Clob clob) { + try { + return clob.getSubString(1, (int)clob.length()); + } + catch (SQLException e) { + throw new RuntimeException(e); + } + } + /** * Person. */ @@ -191,6 +242,14 @@ static class Person implements Serializable { @QuerySqlField private final int age; + /** Binary data (BLOB). */ + @QuerySqlField + private final byte[] data; + + /** CLOB. */ + @QuerySqlField + private final String text; + /** * @param id ID. * @param firstName First name. @@ -206,6 +265,8 @@ static class Person implements Serializable { this.firstName = firstName; this.lastName = lastName; this.age = age; + this.data = getBytes(lastName); + this.text = firstName + " " + lastName; } /** {@inheritDoc} */ @@ -218,16 +279,23 @@ static class Person implements Serializable { if (id != person.id) return false; if (age != person.age) return false; if (firstName != null ? !firstName.equals(person.firstName) : person.firstName != null) return false; - return lastName != null ? lastName.equals(person.lastName) : person.lastName == null; + if (lastName != null ? !lastName.equals(person.lastName) : person.lastName != null) return false; + if (data != null ? !Arrays.equals(data, person.data) : person.data != null) return false; + if (text != null ? !text.equals(person.text) : person.text != null) return false; + return true; } /** {@inheritDoc} */ @Override public int hashCode() { int result = id; + result = 31 * result + (firstName != null ? firstName.hashCode() : 0); result = 31 * result + (lastName != null ? lastName.hashCode() : 0); result = 31 * result + age; + result = 31 * result + (data != null ? Arrays.hashCode(data) : 0); + result = 31 * result + (text != null ? text.hashCode() : 0); + return result; } } diff --git a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinAbstractUpdateStatementSelfTest.java b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinAbstractUpdateStatementSelfTest.java index f71d18a1fa167..76e42d016c982 100644 --- a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinAbstractUpdateStatementSelfTest.java +++ b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinAbstractUpdateStatementSelfTest.java @@ -24,10 +24,10 @@ */ public abstract class JdbcThinAbstractUpdateStatementSelfTest extends JdbcThinAbstractDmlStatementSelfTest { /** SQL query to populate cache. */ - private static final String ITEMS_SQL = "insert into Person(_key, id, firstName, lastName, age) values " + - "('p1', 1, 'John', 'White', 25), " + - "('p2', 2, 'Joe', 'Black', 35), " + - "('p3', 3, 'Mike', 'Green', 40)"; + private static final String ITEMS_SQL = "insert into Person(_key, id, firstName, lastName, age, data, text) values " + + "('p1', 1, 'John', 'White', 25, RAWTOHEX('White'), 'John White'), " + + "('p2', 2, 'Joe', 'Black', 35, RAWTOHEX('Black'), 'Joe Black'), " + + "('p3', 3, 'Mike', 'Green', 40, RAWTOHEX('Green'), 'Mike Green')"; /** {@inheritDoc} */ @Override protected void beforeTest() throws Exception { diff --git a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinConnectionSelfTest.java b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinConnectionSelfTest.java index 6f97da4c8c9d5..c8abd5ea63dc4 100644 --- a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinConnectionSelfTest.java +++ b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinConnectionSelfTest.java @@ -1742,16 +1742,8 @@ public void testReleaseSavepoint() throws Exception { @Test public void testCreateClob() throws Exception { try (Connection conn = DriverManager.getConnection(urlWithPartitionAwarenessProp)) { - // Unsupported - assertThrows(log, - new Callable() { - @Override public Object call() throws Exception { - return conn.createClob(); - } - }, - SQLFeatureNotSupportedException.class, - "SQL-specific types are not supported" - ); + + assertNotNull(conn.createClob()); conn.close(); @@ -1773,16 +1765,8 @@ public void testCreateClob() throws Exception { @Test public void testCreateBlob() throws Exception { try (Connection conn = DriverManager.getConnection(urlWithPartitionAwarenessProp)) { - // Unsupported - assertThrows(log, - new Callable() { - @Override public Object call() throws Exception { - return conn.createBlob(); - } - }, - SQLFeatureNotSupportedException.class, - "SQL-specific types are not supported" - ); + + assertNotNull(conn.createBlob()); conn.close(); diff --git a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinInsertStatementSelfTest.java b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinInsertStatementSelfTest.java index c9f46abc2b3d1..9a787a6269aa0 100644 --- a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinInsertStatementSelfTest.java +++ b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinInsertStatementSelfTest.java @@ -17,6 +17,8 @@ package org.apache.ignite.jdbc.thin; +import java.sql.Blob; +import java.sql.Clob; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -35,23 +37,23 @@ */ public class JdbcThinInsertStatementSelfTest extends JdbcThinAbstractDmlStatementSelfTest { /** SQL query. */ - private static final String SQL = "insert into Person(_key, id, firstName, lastName, age) values " + - "('p1', 1, 'John', 'White', 25), " + - "('p2', 2, 'Joe', 'Black', 35), " + - "('p3', 3, 'Mike', 'Green', 40)"; + private static final String SQL = "insert into Person(_key, id, firstName, lastName, age, data, text) values " + + "('p1', 1, 'John', 'White', 25, RAWTOHEX('White'), 'John White'), " + + "('p2', 2, 'Joe', 'Black', 35, RAWTOHEX('Black'), 'Joe Black'), " + + "('p3', 3, 'Mike', 'Green', 40, RAWTOHEX('Green'), 'Mike Green')"; /** SQL query. */ - private static final String SQL_PREPARED = "insert into Person(_key, id, firstName, lastName, age) values " + - "(?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)"; + private static final String SQL_PREPARED = "insert into Person(_key, id, firstName, lastName, age, data, text) " + + "values (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?)"; /** Test logger. */ private static ListeningTestLogger srvLog; /** Arguments for prepared statement. */ private final Object[][] args = new Object[][] { - {"p1", 1, "John", "White", 25}, - {"p3", 3, "Mike", "Green", 40}, - {"p2", 2, "Joe", "Black", 35} + {"p1", 1, "John", "White", 25, getBytes("White"), "John White"}, + {"p3", 3, "Mike", "Green", 40, getBytes("Green"), "Mike Green"}, + {"p2", 2, "Joe", "Black", 35, getBytes("Black"), "Joe Black"} }; /** Statement. */ @@ -95,6 +97,14 @@ public class JdbcThinInsertStatementSelfTest extends JdbcThinAbstractDmlStatemen prepStmt.setString(paramCnt++, (String)arg[2]); prepStmt.setString(paramCnt++, (String)arg[3]); prepStmt.setInt(paramCnt++, (Integer)arg[4]); + + Blob blob = conn.createBlob(); + blob.setBytes(1, (byte[])arg[5]); + prepStmt.setBlob(paramCnt++, blob); + + Clob clob = conn.createClob(); + clob.setString(1, (String)arg[6]); + prepStmt.setClob(paramCnt++, clob); } } @@ -116,6 +126,8 @@ public class JdbcThinInsertStatementSelfTest extends JdbcThinAbstractDmlStatemen assertEquals("John", rs.getString("firstName")); assertEquals("White", rs.getString("lastName")); assertEquals(25, rs.getInt("age")); + assertEquals("White", str(getBytes(rs.getBlob("data")))); + assertEquals("John White", str(rs.getClob("text"))); break; case 2: @@ -123,6 +135,8 @@ public class JdbcThinInsertStatementSelfTest extends JdbcThinAbstractDmlStatemen assertEquals("Joe", rs.getString("firstName")); assertEquals("Black", rs.getString("lastName")); assertEquals(35, rs.getInt("age")); + assertEquals("Black", str(getBytes(rs.getBlob("data")))); + assertEquals("Joe Black", str(rs.getClob("text"))); break; case 3: @@ -130,13 +144,8 @@ public class JdbcThinInsertStatementSelfTest extends JdbcThinAbstractDmlStatemen assertEquals("Mike", rs.getString("firstName")); assertEquals("Green", rs.getString("lastName")); assertEquals(40, rs.getInt("age")); - break; - - case 4: - assertEquals("p4", rs.getString("_key")); - assertEquals("Leah", rs.getString("firstName")); - assertEquals("Grey", rs.getString("lastName")); - assertEquals(22, rs.getInt("age")); + assertEquals("Green", str(getBytes(rs.getBlob("data")))); + assertEquals("Mike Green", str(rs.getClob("text"))); break; default: diff --git a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinMergeStatementSelfTest.java b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinMergeStatementSelfTest.java index 065f81417b703..8bdf7c6163bf9 100644 --- a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinMergeStatementSelfTest.java +++ b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinMergeStatementSelfTest.java @@ -28,14 +28,14 @@ */ public class JdbcThinMergeStatementSelfTest extends JdbcThinAbstractDmlStatementSelfTest { /** SQL query. */ - private static final String SQL = "merge into Person(_key, id, firstName, lastName, age) values " + - "('p1', 1, 'John', 'White', 25), " + - "('p2', 2, 'Joe', 'Black', 35), " + - "('p3', 3, 'Mike', 'Green', 40)"; + private static final String SQL = "merge into Person(_key, id, firstName, lastName, age, data, text) values " + + "('p1', 1, 'John', 'White', 25, RAWTOHEX('White'), 'John White'), " + + "('p2', 2, 'Joe', 'Black', 35, RAWTOHEX('Black'), 'Joe Black'), " + + "('p3', 3, 'Mike', 'Green', 40, RAWTOHEX('Green'), 'Mike Green')"; /** SQL query. */ - protected static final String SQL_PREPARED = "merge into Person(_key, id, firstName, lastName, age) values " + - "(?, ?, ?, ?, ?), (?, ?, ?, ?, ?)"; + protected static final String SQL_PREPARED = "merge into Person(_key, id, firstName, lastName, age, data, text) values " + + "(?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?)"; /** Statement. */ protected Statement stmt; @@ -74,6 +74,8 @@ public class JdbcThinMergeStatementSelfTest extends JdbcThinAbstractDmlStatement assertEquals("John", rs.getString("firstName")); assertEquals("White", rs.getString("lastName")); assertEquals(25, rs.getInt("age")); + assertEquals("White", str(getBytes(rs.getBlob("data")))); + assertEquals("John White", str(rs.getClob("text"))); break; case 2: @@ -81,6 +83,8 @@ public class JdbcThinMergeStatementSelfTest extends JdbcThinAbstractDmlStatement assertEquals("Joe", rs.getString("firstName")); assertEquals("Black", rs.getString("lastName")); assertEquals(35, rs.getInt("age")); + assertEquals("Black", str(getBytes(rs.getBlob("data")))); + assertEquals("Joe Black", str(rs.getClob("text"))); break; case 3: @@ -88,13 +92,8 @@ public class JdbcThinMergeStatementSelfTest extends JdbcThinAbstractDmlStatement assertEquals("Mike", rs.getString("firstName")); assertEquals("Green", rs.getString("lastName")); assertEquals(40, rs.getInt("age")); - break; - - case 4: - assertEquals("p4", rs.getString("_key")); - assertEquals("Leah", rs.getString("firstName")); - assertEquals("Grey", rs.getString("lastName")); - assertEquals(22, rs.getInt("age")); + assertEquals("Green", str(getBytes(rs.getBlob("data")))); + assertEquals("Mike Green", str(rs.getClob("text"))); break; default: diff --git a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinPreparedStatementSelfTest.java b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinPreparedStatementSelfTest.java index 2becc1860827f..7982980491d6a 100644 --- a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinPreparedStatementSelfTest.java +++ b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinPreparedStatementSelfTest.java @@ -52,7 +52,9 @@ import static java.sql.Types.BIGINT; import static java.sql.Types.BINARY; +import static java.sql.Types.BLOB; import static java.sql.Types.BOOLEAN; +import static java.sql.Types.CLOB; import static java.sql.Types.DATE; import static java.sql.Types.DOUBLE; import static java.sql.Types.FLOAT; @@ -79,7 +81,7 @@ public class JdbcThinPreparedStatementSelfTest extends JdbcThinAbstractSelfTest /** SQL query. */ private static final String SQL_PART = "select id, boolVal, byteVal, shortVal, intVal, longVal, floatVal, " + - "doubleVal, bigVal, strVal, arrVal, dateVal, timeVal, tsVal, objVal " + + "doubleVal, bigVal, strVal, arrVal, dateVal, timeVal, tsVal, objVal, blobVal, clobVal " + "from TestObject "; /** Connection. */ @@ -128,6 +130,8 @@ public class JdbcThinPreparedStatementSelfTest extends JdbcThinAbstractSelfTest o.bigVal = new BigDecimal(1); o.strVal = "str"; o.arrVal = new byte[] {1}; + o.blobVal = new byte[] {1}; + o.clobVal = "large str"; o.dateVal = new Date(1); o.timeVal = new Time(1); o.tsVal = new Timestamp(1); @@ -854,6 +858,62 @@ public void testArray() throws Exception { assert cnt == 1; } + /** + * @throws Exception If failed. + */ + @Test + public void testBlob() throws Exception { + stmt = conn.prepareStatement(SQL_PART + " where blobVal is not distinct from ?"); + + Blob blob = conn.createBlob(); + + blob.setBytes(1, new byte[] {1}); + + stmt.setBlob(1, blob); + + ResultSet rs = stmt.executeQuery(); + + assertTrue(rs.next()); + assertEquals(1, rs.getInt("id")); + assertFalse(rs.next()); + + stmt.setNull(1, BLOB); + + rs = stmt.executeQuery(); + + assertTrue(rs.next()); + assertEquals(2, rs.getInt("id")); + assertFalse(rs.next()); + } + + /** + * @throws Exception If failed. + */ + @Test + public void testClob() throws Exception { + stmt = conn.prepareStatement(SQL_PART + " where clobVal is not distinct from ?"); + + Clob clob = conn.createClob(); + + clob.setString(1, "large str"); + + stmt.setClob(1, clob); + + ResultSet rs = stmt.executeQuery(); + + assertTrue(rs.next()); + assertEquals(1, rs.getInt("id")); + assertFalse(rs.next()); + + stmt.setNull(1, CLOB); + + rs = stmt.executeQuery(); + + assertTrue(rs.next()); + assertEquals(2, rs.getInt("id")); + assertFalse(rs.next()); + } + /** * @throws Exception If failed. */ @@ -1047,12 +1107,6 @@ public void testNotSupportedTypes() throws Exception { } }); - checkNotSupported(new RunnableX() { - @Override public void runx() throws Exception { - stmt.setBlob(1, (Blob)null); - } - }); - checkNotSupported(new RunnableX() { @Override public void runx() throws Exception { stmt.setBlob(1, (InputStream)null); @@ -1083,12 +1137,6 @@ public void testNotSupportedTypes() throws Exception { } }); - checkNotSupported(new RunnableX() { - @Override public void runx() throws Exception { - stmt.setClob(1, (Clob)null); - } - }); - checkNotSupported(new RunnableX() { @Override public void runx() throws Exception { stmt.setClob(1, (Reader)null); @@ -1204,6 +1252,14 @@ private static class TestObject implements Serializable { @QuerySqlField private byte[] arrVal; + /** */ + @QuerySqlField + private byte[] blobVal; + + /** */ + @QuerySqlField + private String clobVal; + /** */ @QuerySqlField private Date dateVal; diff --git a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinResultSetSelfTest.java b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinResultSetSelfTest.java index 50f1c1079d1b6..60210c335dba4 100644 --- a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinResultSetSelfTest.java +++ b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinResultSetSelfTest.java @@ -62,7 +62,7 @@ public class JdbcThinResultSetSelfTest extends JdbcThinAbstractSelfTest { /** SQL query. */ private static final String SQL = "select id, boolVal, byteVal, shortVal, intVal, longVal, floatVal, " + - "doubleVal, bigVal, strVal, arrVal, dateVal, timeVal, tsVal, objVal " + + "doubleVal, bigVal, strVal, arrVal, dateVal, timeVal, tsVal, objVal, blobVal, clobVal " + "from TestObject where id = 1"; /** Statement. */ @@ -144,6 +144,8 @@ private TestObject createObjectWithData(int id) throws MalformedURLException { o.bigVal = new BigDecimal(1); o.strVal = "1"; o.arrVal = new byte[] {1}; + o.blobVal = new byte[] {1}; + o.clobVal = "str"; o.dateVal = new Date(1, 1, 1); o.timeVal = new Time(1, 1, 1); o.tsVal = new Timestamp(1); @@ -599,6 +601,42 @@ public void testArray() throws Exception { assert cnt == 1; } + /** + * @throws Exception If failed. + */ + @Test + public void testBlob() throws Exception { + ResultSet rs = stmt.executeQuery(SQL); + + assertTrue(rs.next()); + + Blob blob = rs.getBlob("blobVal"); + Assert.assertArrayEquals(blob.getBytes(1, (int)blob.length()), new byte[] {1}); + + blob = rs.getBlob(16); + Assert.assertArrayEquals(blob.getBytes(1, (int)blob.length()), new byte[] {1}); + + assertFalse(rs.next()); + } + + /** + * @throws Exception If failed. + */ + @Test + public void testClob() throws Exception { + ResultSet rs = stmt.executeQuery(SQL); + + assertTrue(rs.next()); + + Clob clob = rs.getClob("clobVal"); + Assert.assertEquals("str", clob.getSubString(1, (int)clob.length())); + + clob = rs.getClob(17); + Assert.assertEquals("str", clob.getSubString(1, (int)clob.length())); + + assertFalse(rs.next()); + } + /** * @throws Exception If failed. */ @@ -828,30 +866,6 @@ public void testNotSupportedTypes() throws Exception { } }); - checkNotSupported(new RunnableX() { - @Override public void runx() throws Exception { - rs.getBlob(1); - } - }); - - checkNotSupported(new RunnableX() { - @Override public void runx() throws Exception { - rs.getBlob("id"); - } - }); - - checkNotSupported(new RunnableX() { - @Override public void runx() throws Exception { - rs.getClob(1); - } - }); - - checkNotSupported(new RunnableX() { - @Override public void runx() throws Exception { - rs.getClob("id"); - } - }); - checkNotSupported(new RunnableX() { @Override public void runx() throws Exception { rs.getCharacterStream(1); @@ -1740,6 +1754,14 @@ private static class TestObject implements Serializable { @QuerySqlField private byte[] arrVal; + /** */ + @QuerySqlField + private byte[] blobVal; + + /** */ + @QuerySqlField + private String clobVal; + /** */ @QuerySqlField private Date dateVal; @@ -1806,6 +1828,8 @@ private TestObject(int id) { if (timeVal != null ? !timeVal.equals(that.timeVal) : that.timeVal != null) return false; if (tsVal != null ? !tsVal.equals(that.tsVal) : that.tsVal != null) return false; if (urlVal != null ? !urlVal.equals(that.urlVal) : that.urlVal != null) return false; + if (!Arrays.equals(blobVal, that.blobVal)) return false; + if (clobVal != null ? !clobVal.equals(that.clobVal) : that.clobVal != null) return false; return true; } @@ -1832,6 +1856,8 @@ private TestObject(int id) { res = 31 * res + (objVal != null ? objVal.hashCode() : 0); res = 31 * res + (f2 != null ? f2.hashCode() : 0); res = 31 * res + (f3 != null ? f3.hashCode() : 0); + res = 31 * res + (blobVal != null ? Arrays.hashCode(blobVal) : 0); + res = 31 * res + (clobVal != null ? clobVal.hashCode() : 0); return res; } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java index 6a4668a000144..c5a905ac0649b 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java @@ -82,6 +82,8 @@ import org.apache.ignite.internal.binary.BinaryMetadata; import org.apache.ignite.internal.binary.BinaryMetadataHandler; import org.apache.ignite.internal.binary.BinaryTypeImpl; +import org.apache.ignite.internal.jdbc2.JdbcBlob; +import org.apache.ignite.internal.jdbc2.JdbcClob; import org.apache.ignite.internal.jdbc2.JdbcUtils; import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion; import org.apache.ignite.internal.processors.cache.GridCacheUtils; @@ -754,14 +756,14 @@ private void checkCursorOptions(int resSetType, int resSetConcurrency) throws SQ @Override public Clob createClob() throws SQLException { ensureNotClosed(); - throw new SQLFeatureNotSupportedException("SQL-specific types are not supported."); + return new JdbcClob(""); } /** {@inheritDoc} */ @Override public Blob createBlob() throws SQLException { ensureNotClosed(); - throw new SQLFeatureNotSupportedException("SQL-specific types are not supported."); + return new JdbcBlob(new byte[0]); } /** {@inheritDoc} */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinPreparedStatement.java b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinPreparedStatement.java index 6c795ddc4bb02..d64aaab423e67 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinPreparedStatement.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinPreparedStatement.java @@ -305,16 +305,12 @@ private void executeWithArguments(JdbcStatementType stmtType) throws SQLExceptio /** {@inheritDoc} */ @Override public void setBlob(int paramIdx, Blob x) throws SQLException { - ensureNotClosed(); - - throw new SQLFeatureNotSupportedException("SQL-specific types are not supported."); + setBytes(paramIdx, x.getBytes(1, (int)x.length())); } /** {@inheritDoc} */ @Override public void setClob(int paramIdx, Clob x) throws SQLException { - ensureNotClosed(); - - throw new SQLFeatureNotSupportedException("SQL-specific types are not supported."); + setString(paramIdx, x.getSubString(1, (int)x.length())); } /** {@inheritDoc} */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinResultSet.java b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinResultSet.java index b75734992e410..1ba7affc76177 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinResultSet.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinResultSet.java @@ -46,6 +46,8 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import org.apache.ignite.internal.jdbc2.JdbcBlob; +import org.apache.ignite.internal.jdbc2.JdbcClob; import org.apache.ignite.internal.processors.odbc.SqlStateCode; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcColumnMeta; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcQueryCloseRequest; @@ -1296,16 +1298,12 @@ else if (cls == String.class || cls == Character.class) { /** {@inheritDoc} */ @Override public Blob getBlob(int colIdx) throws SQLException { - ensureNotClosed(); - - throw new SQLFeatureNotSupportedException("SQL-specific types are not supported."); + return new JdbcBlob(getBytes(colIdx)); } /** {@inheritDoc} */ @Override public Clob getClob(int colIdx) throws SQLException { - ensureNotClosed(); - - throw new SQLFeatureNotSupportedException("SQL-specific types are not supported."); + return new JdbcClob(getString(colIdx)); } /** {@inheritDoc} */ @@ -1329,16 +1327,12 @@ else if (cls == String.class || cls == Character.class) { /** {@inheritDoc} */ @Override public Blob getBlob(String colLb) throws SQLException { - ensureNotClosed(); - - throw new SQLFeatureNotSupportedException("SQL-specific types are not supported."); + return new JdbcBlob(getBytes(colLb)); } /** {@inheritDoc} */ @Override public Clob getClob(String colLb) throws SQLException { - ensureNotClosed(); - - throw new SQLFeatureNotSupportedException("SQL-specific types are not supported."); + return new JdbcClob(getString(colLb)); } /** {@inheritDoc} */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcBlob.java b/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcBlob.java index 17e9b13d00c72..ef2b6b5bd3abc 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcBlob.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcBlob.java @@ -54,7 +54,7 @@ public JdbcBlob(byte[] arr) { @Override public byte[] getBytes(long pos, int len) throws SQLException { ensureNotClosed(); - if (pos < 1 || arr.length - pos < 0 || len < 0) + if (pos < 1 || (arr.length - pos < 0 && arr.length > 0) || len < 0) throw new SQLException("Invalid argument. Position can't be less than 1 or " + "greater than size of underlying byte array. Requested length also can't be negative " + "" + "[pos=" + pos + ", len=" + len + ']'); diff --git a/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcClob.java b/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcClob.java new file mode 100644 index 0000000000000..ecef1abd6ab76 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcClob.java @@ -0,0 +1,326 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.internal.jdbc2; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.StringReader; +import java.io.Writer; +import java.sql.Clob; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import org.apache.ignite.internal.util.typedef.internal.U; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * CLOB implementation for Ignite JDBC driver. + */ +public class JdbcClob implements Clob { + /** CLOB's character sequence. */ + private String chars; + + /** + * @param chars CLOB's character sequence. + */ + public JdbcClob(String chars) { + this.chars = chars; + } + + /** {@inheritDoc} */ + @Override public long length() throws SQLException { + ensureNotClosed(); + + return chars.length(); + } + + /** {@inheritDoc} */ + @Override public String getSubString(long pos, int len) throws SQLException { + ensureNotClosed(); + + long zeroBasedPos = pos - 1; + + if (zeroBasedPos < 0 || len < 0 || zeroBasedPos + len > chars.length()) + throw new SQLException("Invalid argument. Position should be greater than 0. Length should not be " + + "negative. Position + length should be less than CLOB size [pos=" + pos + ", length=" + len + ']'); + + return getSubStringInternal((int)zeroBasedPos, len); + } + + /** {@inheritDoc} */ + @Override public Reader getCharacterStream() throws SQLException { + ensureNotClosed(); + + return new StringReader(chars); + } + + /** {@inheritDoc} */ + @Override public Reader getCharacterStream(long pos, long len) throws SQLException { + return new StringReader(getSubString(pos, (int)len)); + } + + /** {@inheritDoc} */ + @Override public InputStream getAsciiStream() throws SQLException { + ensureNotClosed(); + + // Encode to UTF-8 since Ignite internally stores strings in UTF-8 by default. + return new Utf8EncodedStringInputStream(chars); + } + + /** {@inheritDoc} */ + @Override public long position(String searchStr, long start) throws SQLException { + ensureNotClosed(); + + if (start < 1) + throw new SQLException("Invalid argument. Start position should be greater than zero [start=" + + start + ']'); + + long zeroBasedIdx = positionInternal(searchStr, start - 1); + + return zeroBasedIdx == -1 ? -1 : zeroBasedIdx + 1; + } + + /** {@inheritDoc} */ + @Override public long position(Clob searchStr, long start) throws SQLException { + return position(searchStr.getSubString(1, (int)searchStr.length()), start); + } + + /** {@inheritDoc} */ + @Override public int setString(long pos, String str) throws SQLException { + ensureNotClosed(); + + long zeroBasedPos = pos - 1; + + if (zeroBasedPos < 0 || str == null || zeroBasedPos > chars.length()) + throw new SQLException("Invalid argument. Position should be greater than zero. " + + "Position should not exceed CLOB length+1. Source string should not be null " + + "[pos=" + pos + ", str=" + str + ']'); + + return setStringInternal((int)zeroBasedPos, str); + } + + /** {@inheritDoc} */ + @Override public int setString(long pos, String str, int off, int len) throws SQLException { + ensureNotClosed(); + + long zeroBasedPos = pos - 1; + + if (zeroBasedPos < 0 || str == null || zeroBasedPos > chars.length() || off < 0 || len < 0 || off + len > str.length()) + throw new SQLException("Invalid argument. Position should be greater than zero. " + + "Position should not exceed CLOB length+1. Source string should not be null. " + + "Offset and length shouldn't be negative. Offset + length should not exceed source string length " + + "[pos=" + pos + ", str=" + str + ", offset=" + off + ", len=" + len + ']'); + + return setStringInternal((int)zeroBasedPos, str, off, len); + } + + /** {@inheritDoc} */ + @Override public void truncate(long len) throws SQLException { + ensureNotClosed(); + + if (len < 0 || len > chars.length()) + throw new SQLException("Invalid argument. Truncation length should not be negative. Truncation length " + + "should not exceed data length [len=" + len + ']'); + + chars = chars.substring(0, (int)len); + } + + /** {@inheritDoc} */ + @Override public OutputStream setAsciiStream(long pos) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + /** {@inheritDoc} */ + @Override public Writer setCharacterStream(long pos) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + /** {@inheritDoc} */ + @Override public void free() throws SQLException { + chars = null; + } + + /** + * Ensures CLOB hasn't been closed. + */ + private void ensureNotClosed() throws SQLException { + if (chars == null) + throw new SQLException("Clob instance can't be used after free() has been called."); + } + + /** + * Internal getSubString implementation with zero-based position parameter. + */ + private String getSubStringInternal(int zeroBasedPos, int len) { + return chars.substring(zeroBasedPos, zeroBasedPos + len); + } + + /** + * Internal position implementation with zero-based start parameter. + */ + private long positionInternal(String searchStr, long zeroBasedStart) { + return chars.indexOf(searchStr, (int)zeroBasedStart); + } + + /** + * Internal setString implementation with zero-based position parameter. + */ + private int setStringInternal(int zeroBasedPos, String str) { + StringBuilder strBuilder = new StringBuilder(chars); + + // Ensure string buffer capacity + if (zeroBasedPos + str.length() > chars.length()) + strBuilder.setLength(zeroBasedPos + str.length()); + + strBuilder.replace(zeroBasedPos, zeroBasedPos + str.length(), str); + + chars = strBuilder.toString(); + + return str.length(); + } + + /** + * Internal setString implementation with zero-based position parameter. + */ + private int setStringInternal(int zeroBasedPos, String str, int off, int len) { + StringBuilder strBuilder = new StringBuilder(chars); + + // Ensure string buffer capacity + if (zeroBasedPos + str.length() > chars.length()) + strBuilder.setLength(zeroBasedPos + str.length()); + + String replaceStr = str.substring(off, off + len); + strBuilder.replace(zeroBasedPos, zeroBasedPos + replaceStr.length(), replaceStr); + + chars = strBuilder.toString(); + + return replaceStr.length(); + } + + /** + * Input stream which encodes the given string to UTF-8. + * To save memory for large strings it does it by chunks. + */ + private static class Utf8EncodedStringInputStream extends InputStream { + /** String to encode. */ + private final String chars; + + /** String length. */ + private final int length; + + /** Start index of the next chunk (substring) to be encoded. */ + private int charsPos; + + /** Default chunk size. */ + private static final int DEFAULT_CHUNK_SIZE = 8192; + + /** Buffer containing the current chunk encoding. */ + private byte[] buf; + + /** Current position in the buffer - index of the next byte to be read from the input stream. */ + private int bufPos; + + /** + * @param chars String to be encoded. + */ + Utf8EncodedStringInputStream(String chars) { + this.chars = chars; + + length = chars.length(); + charsPos = 0; + } + + /** {@inheritDoc} */ + @Override public synchronized int read() { + if (buf == null || buf.length == 0 || bufPos >= buf.length) { + if (charsPos >= length) + return -1; + + bufPos = 0; + + encodeNextChunk(); + } + + return buf[bufPos++] & 0xFF; + } + + /** {@inheritDoc} */ + @Override public synchronized int read(byte[] b, int off, int len) { + if (b == null) + throw new NullPointerException(); + + if (off < 0 || len < 0 || len > b.length - off) + throw new IndexOutOfBoundsException(String.format("Range [%s, %= buf.length) { + if (charsPos >= length) + return i > 0 ? i : -1; + + bufPos = 0; + + encodeNextChunk(); + } + + int encodedChunkSize = Math.min(len - i, buf.length - bufPos); + + U.arrayCopy(buf, bufPos, b, off + i, encodedChunkSize); + + bufPos += encodedChunkSize; + i += encodedChunkSize; + } + + return i; + } + + /** + * Encodes the next chunk of the string. + *

+ * Makes sure that chunk doesn't contain the malformed surrogate element at the end + * (high surrogate that is not followed by a low surrogate). + */ + private void encodeNextChunk() { + int remainingSize = chars.length() - charsPos; + + assert remainingSize > 0; + + int chunkSize; + + if (remainingSize <= DEFAULT_CHUNK_SIZE) { + chunkSize = remainingSize; + } + else if (Character.isHighSurrogate(chars.charAt(charsPos + DEFAULT_CHUNK_SIZE - 1))) { + chunkSize = DEFAULT_CHUNK_SIZE + 1; + } + else { + chunkSize = DEFAULT_CHUNK_SIZE; + } + + String subs = chars.substring(charsPos, charsPos + chunkSize); + buf = subs.getBytes(UTF_8); + + charsPos += chunkSize; + } + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcConnection.java b/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcConnection.java index e785e4164d756..31ccf65535137 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcConnection.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcConnection.java @@ -702,7 +702,7 @@ private Ignite getIgnite(String cfgUrl) throws IgniteCheckedException { @Override public Clob createClob() throws SQLException { ensureNotClosed(); - throw new SQLFeatureNotSupportedException("SQL-specific types are not supported."); + return new JdbcClob(""); } /** {@inheritDoc} */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcPreparedStatement.java b/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcPreparedStatement.java index 858a1b63f8b3e..dbb0c03134435 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcPreparedStatement.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcPreparedStatement.java @@ -267,9 +267,7 @@ public class JdbcPreparedStatement extends JdbcStatement implements PreparedStat /** {@inheritDoc} */ @Override public void setClob(int paramIdx, Clob x) throws SQLException { - ensureNotClosed(); - - throw new SQLFeatureNotSupportedException("SQL-specific types are not supported."); + setString(paramIdx, x.getSubString(1, (int)x.length())); } /** {@inheritDoc} */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcResultSet.java b/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcResultSet.java index eab488c31c3fe..53746e50c333f 100755 --- a/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcResultSet.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcResultSet.java @@ -1299,9 +1299,7 @@ else if (cls == String.class || cls == Character.class) { /** {@inheritDoc} */ @Override public Clob getClob(int colIdx) throws SQLException { - ensureNotClosed(); - - throw new SQLFeatureNotSupportedException("SQL-specific types are not supported."); + return new JdbcClob(getString(colIdx)); } /** {@inheritDoc} */ @@ -1330,9 +1328,7 @@ else if (cls == String.class || cls == Character.class) { /** {@inheritDoc} */ @Override public Clob getClob(String colLb) throws SQLException { - ensureNotClosed(); - - throw new SQLFeatureNotSupportedException("SQL-specific types are not supported."); + return new JdbcClob(getString(colLb)); } /** {@inheritDoc} */ diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/IgniteH2Indexing.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/IgniteH2Indexing.java index 8e91c71a71172..65600be66a71e 100644 --- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/IgniteH2Indexing.java +++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/IgniteH2Indexing.java @@ -122,6 +122,7 @@ import org.apache.ignite.internal.util.lang.IgniteThrowableSupplier; import org.apache.ignite.internal.util.typedef.F; import org.apache.ignite.internal.util.typedef.X; +import org.apache.ignite.internal.util.typedef.internal.S; import org.apache.ignite.internal.util.typedef.internal.U; import org.apache.ignite.lang.IgniteBiClosure; import org.apache.ignite.lang.IgniteBiTuple; @@ -1090,14 +1091,14 @@ private List>> executeDml( if (roEx != null) { throw new IgniteSQLException( "Failed to execute DML statement. Cluster in read-only mode [stmt=" + qryDesc.sql() + - ", params=" + Arrays.deepToString(qryParams.arguments()) + "]", + ", params=" + S.toString(QueryParameters.class, qryParams) + "]", IgniteQueryErrorCode.CLUSTER_READ_ONLY_MODE_ENABLED, e ); } throw new IgniteSQLException("Failed to execute DML statement [stmt=" + qryDesc.sql() + - ", params=" + Arrays.deepToString(qryParams.arguments()) + "]", e); + ", params=" + S.toString(QueryParameters.class, qryParams) + "]", e); } finally { runningQueryManager().unregister(qryId, failReason); diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/QueryParameters.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/QueryParameters.java index e59e512826e0e..69d46618de11f 100644 --- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/QueryParameters.java +++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/QueryParameters.java @@ -20,16 +20,21 @@ import java.util.List; import org.apache.ignite.internal.processors.query.NestedTxMode; +import org.apache.ignite.internal.util.tostring.GridToStringInclude; +import org.apache.ignite.internal.util.typedef.internal.S; /** * Query parameters which vary between requests having the same execution plan. Essentially, these are the arguments * of original {@link org.apache.ignite.cache.query.SqlFieldsQuery} which are not part of {@link QueryDescriptor}. */ +@GridToStringInclude(sensitive = true) public class QueryParameters { /** Arguments. */ + @GridToStringInclude(sensitive = true) private final Object[] args; /** Partitions. */ + @GridToStringInclude(sensitive = true) private final int[] parts; /** Timeout. */ @@ -51,6 +56,7 @@ public class QueryParameters { private final boolean autoCommit; /** Batched arguments. */ + @GridToStringInclude(sensitive = true) private final List batchedArgs; /** @@ -194,4 +200,9 @@ public QueryParameters toSingleBatchedArguments(Object[] args) { this.updateBatchSize ); } + + /** {@inheritDoc} */ + @Override public String toString() { + return S.toString(QueryParameters.class, this); + } }