From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from localhost (localhost [127.0.0.1]) by turing.freelists.org (Avenir Technologies Mail Multiplex) with ESMTP id 0728B29450 for ; Tue, 2 Oct 2018 13:09:11 -0400 (EDT) Received: from turing.freelists.org ([127.0.0.1]) by localhost (turing.freelists.org [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id ex5TACWbJGBw for ; Tue, 2 Oct 2018 13:09:10 -0400 (EDT) Received: from mail-lj1-f193.google.com (mail-lj1-f193.google.com [209.85.208.193]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by turing.freelists.org (Avenir Technologies Mail Multiplex) with ESMTPS id 88722281F3 for ; Tue, 2 Oct 2018 13:09:10 -0400 (EDT) Received: by mail-lj1-f193.google.com with SMTP id 63-v6so2474068ljs.4 for ; Tue, 02 Oct 2018 10:09:10 -0700 (PDT) Date: Tue, 2 Oct 2018 20:09:04 +0300 From: Sergei Kalashnikov Subject: [tarantool-patches] Re: [PATCH] jdbc: fix primary keys meta retrieval Message-ID: <20181002170902.GA20216@daedra.localdomain> References: <1537954408-3275-1-git-send-email-ztarvos@gmail.com> <20181001130427.zlugbqlg3madlo7r@tkn_work_nb> MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Disposition: inline In-Reply-To: <20181001130427.zlugbqlg3madlo7r@tkn_work_nb> Sender: tarantool-patches-bounce@freelists.org Errors-to: tarantool-patches-bounce@freelists.org Reply-To: tarantool-patches@freelists.org List-help: List-unsubscribe: List-software: Ecartis version 1.0.0 List-Id: tarantool-patches List-subscribe: List-owner: List-post: List-archive: To: Alexander Turenko Cc: tarantool-patches Hi Alexander! Thanks for the review. Please see my replies inline below. On Mon, Oct 01, 2018 at 04:04:27PM +0300, Alexander Turenko wrote: > Hi! > > Sorry for the late response. > > From [1]: > > > Retrieves a description of the given table's primary key columns. They are ordered by COLUMN_NAME. > > That is strange, but that is. Maybe we should also check the standard. > > [1]: https://docs.oracle.com/javase/7/docs/api/java/sql/DatabaseMetaData.html#getPrimaryKeys(java.lang.String,%20java.lang.String,%20java.lang.String) The standard lacks such details. It relies on the underlying Java API docs. And some vendors apply different ordering (e.g. table_name, pk_name, key_seq). I added the sorting in revised patch (please note how the definition of 'test_compound' table has changed in tests). I have noticed another deviation from specification. The order in which columns are returned in the resultset is not correct. The TABLE_CAT and TABLE_SCHEM must go first. I have fixed this as well. Plz see the new test 'testGetPrimaryKeysOrderOfColumns'. > > Tests are ok for me. > > Maybe we also should set some defined behaviour for the case when > 'catalog' and 'schema' are given for getPrimaryKeys. I think we should > ignore them and return nulls in TABLE_CAT and TABLE_SCHEM. I think we > should have corresponding test case. I agree. I see that, for instance, postgreSQL JDBC are doing that for catalog. I added a test 'testGetPrimaryKeysIgnoresCatalogSchema' for that. > > > > diff --git a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java > > index e43bcdc..790bbe7 100644 > > --- a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java > > +++ b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java > > @@ -774,12 +774,12 @@ public class SQLDatabaseMetadata implements DatabaseMetaData { > > List> fields = (List>) space.get(FORMAT_IDX); > > List> indexes = (List>) connection.connection.select(_VINDEX, 0, Arrays.asList(space.get(SPACE_ID_IDX), 0), 0, 1, 0); > > List primaryKey = indexes.get(0); > > - List> parts = (List>) primaryKey.get(INDEX_FORMAT_IDX); > > + List> parts = (List>) primaryKey.get(INDEX_FORMAT_IDX); > > I think we should check the type to give a proper error message in the > case when a user give 'native' tarantool space in parameters. > Corresponding test case should be added. > > Don't sure how it will be wrapped in Java, I just about the following: > > 6th field of _vspace tuple for a 'native' space: > > [[0, 'unsigned'], [1, 'unsigned']] > > (Don't sure whether it is list or map in the binary protocol; likely > map.) > > 6th field of _vspace tuple for a sql space: > > [{'sort_order': 'asc', 'type': 'scalar', 'field': 0, > 'nullable_action': 'abort', 'is_nullable': false}] > > What would going on if a user give us 'native' space name? I've made some tests. For the 6th field of '_vspace' tuple, I've got the same 'List' in both native and sql space cases, the difference was only in map contents. However for the 5th field of '_vindex' tuple, it is 'List' for the native space and 'List' for the sql space. So, I added extensive type checking and throw an exception in case unexpected type is encountered. See tests 'testGetPrimaryKeyNonSQLSpace' and 'testDatabaseMetaDataGetPrimaryKeysFormatError'. Are you certain that we should return the error in case we have been given a native space? I went for empty resultset instead. Hope you are OK with that. > > > for (int i = 0; i < parts.size(); i++) { > > - List part = parts.get(i); > > - int ordinal = ((Number) part.get(0)).intValue(); > > + Map part = parts.get(i); > > + int ordinal = ((Number) part.get("field")).intValue(); > > String column = (String) fields.get(ordinal).get("name"); > > Just note: it is not mandatory to space format fields to have a name, > but SQL spaces have them. Maybe a comment needed here. > OK, done. Please find below the fixed patch. ================================================================== >From 705c3ba3746cf5e196d9c58419348936817ea327 Mon Sep 17 00:00:00 2001 From: Sergei Kalashnikov Date: Tue, 2 Oct 2018 19:12:37 +0300 Subject: [PATCH] jdbc: fix primary keys meta retrieval Fixed several mistakes in get primary keys metadata API. Corrected type mismatch when parsing server response. Added sorting of result rows by column name. Fixed order of columns in result set. Wrapped errors into SQLException. Improved test coverage. Closes #41 --- https://github.com/tarantool/tarantool-java/issues/41 https://github.com/ztarvos/tarantool-java/commits/gh-41-fix-jdbc-pk-meta .../org/tarantool/jdbc/SQLDatabaseMetadata.java | 78 +++++++++++++---- .../java/org/tarantool/jdbc/AbstractJdbcIT.java | 11 +-- .../org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java | 98 ++++++++++++++++++++-- .../tarantool/jdbc/JdbcExceptionHandlingTest.java | 60 +++++++++++++ 4 files changed, 215 insertions(+), 32 deletions(-) create mode 100644 src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java diff --git a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java index e43bcdc..c8879c9 100644 --- a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java +++ b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java @@ -10,6 +10,7 @@ import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; @@ -765,29 +766,53 @@ public class SQLDatabaseMetadata implements DatabaseMetaData { @Override public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { - List> spaces = (List>) connection.connection.select(_VSPACE, 2, Arrays.asList(table), 0, 1, 0); + final List colNames = Arrays.asList( + "TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "COLUMN_NAME", "KEY_SEQ", "PK_NAME"); - List> rows = new ArrayList>(); + if (table == null || table.isEmpty()) + return emptyResultSet(colNames); + + try { + List spaces = connection.connection.select(_VSPACE, 2, Collections.singletonList(table), 0, 1, 0); + + if (spaces == null || spaces.size() == 0) + return emptyResultSet(colNames); - if (spaces != null && spaces.size() > 0) { - List space = spaces.get(0); - List> fields = (List>) space.get(FORMAT_IDX); - List> indexes = (List>) connection.connection.select(_VINDEX, 0, Arrays.asList(space.get(SPACE_ID_IDX), 0), 0, 1, 0); - List primaryKey = indexes.get(0); - List> parts = (List>) primaryKey.get(INDEX_FORMAT_IDX); + List space = ensureType(List.class, spaces.get(0)); + List fields = ensureType(List.class, space.get(FORMAT_IDX)); + int spaceId = ensureType(Number.class, space.get(SPACE_ID_IDX)).intValue(); + List indexes = connection.connection.select(_VINDEX, 0, Arrays.asList(spaceId, 0), 0, 1, 0); + List primaryKey = ensureType(List.class, indexes.get(0)); + List parts = ensureType(List.class, primaryKey.get(INDEX_FORMAT_IDX)); + + List> rows = new ArrayList>(); for (int i = 0; i < parts.size(); i++) { - List part = parts.get(i); - int ordinal = ((Number) part.get(0)).intValue(); - String column = (String) fields.get(ordinal).get("name"); - rows.add(Arrays.asList(table, column, i + 1, "primary", primaryKey.get(NAME_IDX))); + // For native spaces, the 'parts' is 'List of Lists'. + // We only accept SQL spaces, for which the parts is 'List of Maps'. + Map part = checkType(Map.class, parts.get(i)); + if (part == null) + return emptyResultSet(colNames); + + int ordinal = ensureType(Number.class, part.get("field")).intValue(); + Map field = ensureType(Map.class, fields.get(ordinal)); + // The 'name' field is optional in the format structure. But it is present for SQL space. + String column = ensureType(String.class, field.get("name")); + rows.add(Arrays.asList(null, null, table, column, i + 1, primaryKey.get(NAME_IDX))); } + // Sort results by column name. + Collections.sort(rows, new Comparator>() { + @Override + public int compare(List row0, List row1) { + String col0 = (String) row0.get(3); + String col1 = (String) row1.get(3); + return col0.compareTo(col1); + } + }); + return new SQLNullResultSet((JDBCBridge.mock(colNames, rows))); + } + catch (Throwable t) { + throw new SQLException("Error processing metadata for table \"" + table + "\".", t); } - return new SQLNullResultSet((JDBCBridge.mock( - Arrays.asList("TABLE_NAME", "COLUMN_NAME", "KEY_SEQ", "PK_NAME", - //nulls - "TABLE_CAT", "TABLE_SCHEM" - ), - rows))); } @Override @@ -1026,4 +1051,21 @@ public class SQLDatabaseMetadata implements DatabaseMetaData { public boolean isWrapperFor(Class iface) throws SQLException { throw new SQLFeatureNotSupportedException(); } + + private static T ensureType(Class cls, Object v) throws Exception { + if (v == null || !cls.isAssignableFrom(v.getClass())) { + throw new Exception(String.format("Wrong value type '%s', expected '%s'.", + v == null ? "null" : v.getClass().getName(), cls.getName())); + } + return cls.cast(v); + } + + private static T checkType(Class cls, Object v) { + return (v != null && cls.isAssignableFrom(v.getClass())) ? cls.cast(v) : null; + } + + private ResultSet emptyResultSet(List colNames) { + return new SQLNullResultSet((JDBCBridge.mock(colNames, Collections.>emptyList()))); + } + } diff --git a/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java b/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java index 21e3f53..3df1aa4 100644 --- a/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java +++ b/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java @@ -32,9 +32,6 @@ public abstract class AbstractJdbcIT { private static String URL = String.format("tarantool://%s:%d?user=%s&password=%s", host, port, user, pass); private static String[] initSql = new String[] { - "DROP TABLE IF EXISTS test", - "DROP TABLE IF EXISTS test_types", - "CREATE TABLE test(id INT PRIMARY KEY, val VARCHAR(100))", "INSERT INTO test VALUES (1, 'one'), (2, 'two'), (3, 'three')", @@ -78,12 +75,15 @@ public abstract class AbstractJdbcIT { "X'010203040506'," + //LONGVARBINARY "'1983-03-14'," + //DATE "'12:01:06'," + //TIME - "129479994)" //TIMESTAMP + "129479994)", //TIMESTAMP + + "CREATE TABLE test_compound(id1 INT, id2 INT, val VARCHAR(100), PRIMARY KEY (id2, id1))" }; private static String[] cleanSql = new String[] { "DROP TABLE IF EXISTS test", - "DROP TABLE IF EXISTS test_types" + "DROP TABLE IF EXISTS test_types", + "DROP TABLE IF EXISTS test_compound" }; static Object[] testRow = new Object[] { @@ -112,6 +112,7 @@ public abstract class AbstractJdbcIT { @BeforeAll public static void setupEnv() throws Exception { + sqlExec(cleanSql); sqlExec(initSql); } diff --git a/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java b/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java index b934bbe..78ff89b 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java @@ -1,11 +1,11 @@ package org.tarantool.jdbc; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeEach; import java.sql.DatabaseMetaData; import java.sql.ResultSet; +import java.sql.ResultSetMetaData; import java.sql.SQLException; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -45,6 +45,9 @@ public class JdbcDatabaseMetaDataIT extends AbstractJdbcIT { assertTrue(rs.next()); assertEquals("TEST_TYPES", rs.getString("TABLE_NAME")); + assertTrue(rs.next()); + assertEquals("TEST_COMPOUND", rs.getString("TABLE_NAME")); + assertFalse(rs.next()); rs.close(); @@ -84,23 +87,100 @@ public class JdbcDatabaseMetaDataIT extends AbstractJdbcIT { rs.close(); } - @Disabled(value="Test ignored, issue#41") @Test - public void testGetPrimaryKeys() throws SQLException { + public void testGetPrimaryKeys() throws Exception { ResultSet rs = meta.getPrimaryKeys(null, null, "TEST"); assertNotNull(rs); assertTrue(rs.next()); - assertNull(rs.getString("TABLE_CAT")); - assertNull(rs.getString("TABLE_SCHEM")); - assertEquals("TEST", rs.getString("TABLE_NAME")); - assertEquals("ID", rs.getString("COLUMN_NAME")); - assertEquals(1, rs.getInt("KEY_SEQ")); - assertEquals("pk_unnamed_TEST_1", rs.getString("PK_NAME")); + checkGetPrimaryKeysRow(rs, "TEST", "ID", "pk_unnamed_TEST_1", 1); + + assertFalse(rs.next()); + + rs.close(); + } + + @Test + public void testGetPrimaryKeysCompound() throws Exception { + ResultSet rs = meta.getPrimaryKeys(null, null, "TEST_COMPOUND"); + + assertNotNull(rs); + assertTrue(rs.next()); + checkGetPrimaryKeysRow(rs, "TEST_COMPOUND", "ID1", "pk_unnamed_TEST_COMPOUND_1", 2); + + assertTrue(rs.next()); + checkGetPrimaryKeysRow(rs, "TEST_COMPOUND", "ID2", "pk_unnamed_TEST_COMPOUND_1", 1); + + assertFalse(rs.next()); + rs.close(); + } + + @Test + public void testGetPrimaryKeysIgnoresCatalogSchema() throws Exception { + String[] vals = new String[] {null, "", "IGNORE"}; + for (String cat : vals) { + for (String schema : vals) { + ResultSet rs = meta.getPrimaryKeys(cat, schema, "TEST"); + + assertNotNull(rs); + assertTrue(rs.next()); + checkGetPrimaryKeysRow(rs, "TEST", "ID", "pk_unnamed_TEST_1", 1); + assertFalse(rs.next()); + rs.close(); + } + } + } + + @Test + public void testGetPrimaryKeysNotFound() throws SQLException { + String[] tables = new String[] {null, "", "NOSUCHTABLE"}; + for (String t : tables) { + ResultSet rs = meta.getPrimaryKeys(null, null, t); + assertNotNull(rs); + assertFalse(rs.next()); + rs.close(); + } + } + + @Test + public void testGetPrimaryKeyNonSQLSpace() throws SQLException { + ResultSet rs = meta.getPrimaryKeys(null, null, "_vspace"); + assertNotNull(rs); assertFalse(rs.next()); + rs.close(); + } + @Test + public void testGetPrimaryKeysOrderOfColumns() throws SQLException { + ResultSet rs = meta.getPrimaryKeys(null, null, "TEST"); + assertNotNull(rs); + ResultSetMetaData rsMeta = rs.getMetaData(); + assertEquals(6, rsMeta.getColumnCount()); + assertEquals("TABLE_CAT", rsMeta.getColumnName(1)); + assertEquals("TABLE_SCHEM", rsMeta.getColumnName(2)); + assertEquals("TABLE_NAME", rsMeta.getColumnName(3)); + assertEquals("COLUMN_NAME", rsMeta.getColumnName(4)); + assertEquals("KEY_SEQ", rsMeta.getColumnName(5)); + assertEquals("PK_NAME", rsMeta.getColumnName(6)); rs.close(); } + + private void checkGetPrimaryKeysRow(ResultSet rs, String table, String colName, String pkName, int seq) + throws Exception { + assertNull(rs.getString("TABLE_CAT")); + assertNull(rs.getString("TABLE_SCHEM")); + assertEquals(table, rs.getString("TABLE_NAME")); + assertEquals(colName, rs.getString("COLUMN_NAME")); + assertEquals(seq, rs.getInt("KEY_SEQ")); + assertEquals(pkName, rs.getString("PK_NAME")); + + assertNull(rs.getString(1)); + assertNull(rs.getString(2)); + assertEquals(table, rs.getString(3)); + assertEquals(colName, rs.getString(4)); + assertEquals(seq, rs.getInt(5)); + assertEquals(pkName, rs.getString(6)); + } } diff --git a/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java new file mode 100644 index 0000000..39d6326 --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java @@ -0,0 +1,60 @@ +package org.tarantool.jdbc; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.tarantool.TarantoolConnection; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.tarantool.jdbc.SQLDatabaseMetadata.FORMAT_IDX; +import static org.tarantool.jdbc.SQLDatabaseMetadata.INDEX_FORMAT_IDX; +import static org.tarantool.jdbc.SQLDatabaseMetadata.SPACE_ID_IDX; +import static org.tarantool.jdbc.SQLDatabaseMetadata._VINDEX; +import static org.tarantool.jdbc.SQLDatabaseMetadata._VSPACE; + +public class JdbcExceptionHandlingTest { + /** + * Simulates meta parsing error: missing "name" field in a space format for the primary key. + * + * @throws Exception on failure. + */ + @Test + public void testDatabaseMetaDataGetPrimaryKeysFormatError() throws Exception { + TarantoolConnection tntCon = mock(TarantoolConnection.class); + SQLConnection conn = new SQLConnection(tntCon, "", new Properties()); + + Object[] spc = new Object[7]; + spc[FORMAT_IDX] = Collections.singletonList(new HashMap()); + spc[SPACE_ID_IDX] = 1000; + + doReturn(Collections.singletonList(Arrays.asList(spc))).when(tntCon) + .select(_VSPACE, 2, Collections.singletonList("TEST"), 0, 1, 0); + + Object[] idx = new Object[6]; + idx[INDEX_FORMAT_IDX] = Collections.singletonList( + new HashMap() {{ put("field", 0);}}); + + doReturn(Collections.singletonList(Arrays.asList(idx))).when(tntCon) + .select(_VINDEX, 0, Arrays.asList(1000, 0), 0, 1, 0); + + final DatabaseMetaData meta = conn.getMetaData(); + + Throwable t = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + meta.getPrimaryKeys(null, null, "TEST"); + } + }, "Error processing metadata for table \"TEST\"."); + + assertTrue(t.getCause().getMessage().contains("Wrong value type")); + } +} -- 1.8.3.1