Skip to content

Commit 075cd05

Browse files
ztarvosTotktonada
authored andcommitted
jdbc: fix primary keys meta retrieval
Fixed several mistakes in get primary keys metadata API. Corrected type mismatch when parsing server response. Added sorting of result rows by column name. Fixed order of columns in result set. Wrapped errors into SQLException. Improved test coverage. Closes #41 ---- Rebase fixes (Alexander Turenko): * Start / stop jdk-testing instance using TarantoolControl in JdbcExceptionHandlingTest.
1 parent 495e458 commit 075cd05

File tree

4 files changed

+238
-32
lines changed

4 files changed

+238
-32
lines changed

src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java

+60-18
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.util.ArrayList;
1111
import java.util.Arrays;
1212
import java.util.Collections;
13+
import java.util.Comparator;
1314
import java.util.List;
1415
import java.util.Map;
1516

@@ -765,29 +766,53 @@ public ResultSet getVersionColumns(String catalog, String schema, String table)
765766

766767
@Override
767768
public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException {
768-
List<List<Object>> spaces = (List<List<Object>>) connection.connection.select(_VSPACE, 2, Arrays.asList(table), 0, 1, 0);
769+
final List<String> colNames = Arrays.asList(
770+
"TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "COLUMN_NAME", "KEY_SEQ", "PK_NAME");
769771

770-
List<List<Object>> rows = new ArrayList<List<Object>>();
772+
if (table == null || table.isEmpty())
773+
return emptyResultSet(colNames);
774+
775+
try {
776+
List spaces = connection.connection.select(_VSPACE, 2, Collections.singletonList(table), 0, 1, 0);
777+
778+
if (spaces == null || spaces.size() == 0)
779+
return emptyResultSet(colNames);
771780

772-
if (spaces != null && spaces.size() > 0) {
773-
List<Object> space = spaces.get(0);
774-
List<Map<String, Object>> fields = (List<Map<String, Object>>) space.get(FORMAT_IDX);
775-
List<List<Object>> indexes = (List<List<Object>>) connection.connection.select(_VINDEX, 0, Arrays.asList(space.get(SPACE_ID_IDX), 0), 0, 1, 0);
776-
List<Object> primaryKey = indexes.get(0);
777-
List<List<Object>> parts = (List<List<Object>>) primaryKey.get(INDEX_FORMAT_IDX);
781+
List space = ensureType(List.class, spaces.get(0));
782+
List fields = ensureType(List.class, space.get(FORMAT_IDX));
783+
int spaceId = ensureType(Number.class, space.get(SPACE_ID_IDX)).intValue();
784+
List indexes = connection.connection.select(_VINDEX, 0, Arrays.asList(spaceId, 0), 0, 1, 0);
785+
List primaryKey = ensureType(List.class, indexes.get(0));
786+
List parts = ensureType(List.class, primaryKey.get(INDEX_FORMAT_IDX));
787+
788+
List<List<Object>> rows = new ArrayList<List<Object>>();
778789
for (int i = 0; i < parts.size(); i++) {
779-
List<Object> part = parts.get(i);
780-
int ordinal = ((Number) part.get(0)).intValue();
781-
String column = (String) fields.get(ordinal).get("name");
782-
rows.add(Arrays.asList(table, column, i + 1, "primary", primaryKey.get(NAME_IDX)));
790+
// For native spaces, the 'parts' is 'List of Lists'.
791+
// We only accept SQL spaces, for which the parts is 'List of Maps'.
792+
Map part = checkType(Map.class, parts.get(i));
793+
if (part == null)
794+
return emptyResultSet(colNames);
795+
796+
int ordinal = ensureType(Number.class, part.get("field")).intValue();
797+
Map field = ensureType(Map.class, fields.get(ordinal));
798+
// The 'name' field is optional in the format structure. But it is present for SQL space.
799+
String column = ensureType(String.class, field.get("name"));
800+
rows.add(Arrays.asList(null, null, table, column, i + 1, primaryKey.get(NAME_IDX)));
783801
}
802+
// Sort results by column name.
803+
Collections.sort(rows, new Comparator<List<Object>>() {
804+
@Override
805+
public int compare(List<Object> row0, List<Object> row1) {
806+
String col0 = (String) row0.get(3);
807+
String col1 = (String) row1.get(3);
808+
return col0.compareTo(col1);
809+
}
810+
});
811+
return new SQLNullResultSet((JDBCBridge.mock(colNames, rows)));
812+
}
813+
catch (Throwable t) {
814+
throw new SQLException("Error processing metadata for table \"" + table + "\".", t);
784815
}
785-
return new SQLNullResultSet((JDBCBridge.mock(
786-
Arrays.asList("TABLE_NAME", "COLUMN_NAME", "KEY_SEQ", "PK_NAME",
787-
//nulls
788-
"TABLE_CAT", "TABLE_SCHEM"
789-
),
790-
rows)));
791816
}
792817

793818
@Override
@@ -1026,4 +1051,21 @@ public <T> T unwrap(Class<T> iface) throws SQLException {
10261051
public boolean isWrapperFor(Class<?> iface) throws SQLException {
10271052
throw new SQLFeatureNotSupportedException();
10281053
}
1054+
1055+
private static <T> T ensureType(Class<T> cls, Object v) throws Exception {
1056+
if (v == null || !cls.isAssignableFrom(v.getClass())) {
1057+
throw new Exception(String.format("Wrong value type '%s', expected '%s'.",
1058+
v == null ? "null" : v.getClass().getName(), cls.getName()));
1059+
}
1060+
return cls.cast(v);
1061+
}
1062+
1063+
private static <T> T checkType(Class<T> cls, Object v) {
1064+
return (v != null && cls.isAssignableFrom(v.getClass())) ? cls.cast(v) : null;
1065+
}
1066+
1067+
private ResultSet emptyResultSet(List<String> colNames) {
1068+
return new SQLNullResultSet((JDBCBridge.mock(colNames, Collections.<List<Object>>emptyList())));
1069+
}
1070+
10291071
}

src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java

+6-5
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@ public abstract class AbstractJdbcIT {
3434
private static String URL = String.format("tarantool://%s:%d?user=%s&password=%s", host, port, user, pass);
3535

3636
private static String[] initSql = new String[] {
37-
"DROP TABLE IF EXISTS test",
38-
"DROP TABLE IF EXISTS test_types",
39-
4037
"CREATE TABLE test(id INT PRIMARY KEY, val VARCHAR(100))",
4138
"INSERT INTO test VALUES (1, 'one'), (2, 'two'), (3, 'three')",
4239

@@ -80,12 +77,15 @@ public abstract class AbstractJdbcIT {
8077
"X'010203040506'," + //LONGVARBINARY
8178
"'1983-03-14'," + //DATE
8279
"'12:01:06'," + //TIME
83-
"129479994)" //TIMESTAMP
80+
"129479994)", //TIMESTAMP
81+
82+
"CREATE TABLE test_compound(id1 INT, id2 INT, val VARCHAR(100), PRIMARY KEY (id2, id1))"
8483
};
8584

8685
private static String[] cleanSql = new String[] {
8786
"DROP TABLE IF EXISTS test",
88-
"DROP TABLE IF EXISTS test_types"
87+
"DROP TABLE IF EXISTS test_types",
88+
"DROP TABLE IF EXISTS test_compound"
8989
};
9090

9191
static Object[] testRow = new Object[] {
@@ -119,6 +119,7 @@ public static void setupEnv() throws Exception {
119119
control.start("jdk-testing");
120120
control.waitStarted("jdk-testing");
121121

122+
sqlExec(cleanSql);
122123
sqlExec(initSql);
123124
}
124125

src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java

+89-9
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package org.tarantool.jdbc;
22

3-
import org.junit.jupiter.api.Disabled;
43
import org.junit.jupiter.api.Test;
54
import org.junit.jupiter.api.BeforeEach;
65

76
import java.sql.DatabaseMetaData;
87
import java.sql.ResultSet;
8+
import java.sql.ResultSetMetaData;
99
import java.sql.SQLException;
1010

1111
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -18,7 +18,7 @@ public class JdbcDatabaseMetaDataIT extends AbstractJdbcIT {
1818
private DatabaseMetaData meta;
1919

2020
@BeforeEach
21-
public void setUp() throws Exception {
21+
public void setUp() throws SQLException {
2222
meta = conn.getMetaData();
2323
}
2424

@@ -45,6 +45,9 @@ public void testGetAllTables() throws SQLException {
4545
assertTrue(rs.next());
4646
assertEquals("TEST_TYPES", rs.getString("TABLE_NAME"));
4747

48+
assertTrue(rs.next());
49+
assertEquals("TEST_COMPOUND", rs.getString("TABLE_NAME"));
50+
4851
assertFalse(rs.next());
4952

5053
rs.close();
@@ -84,23 +87,100 @@ public void testGetColumns() throws SQLException {
8487
rs.close();
8588
}
8689

87-
@Disabled(value="Test ignored, issue#41")
8890
@Test
8991
public void testGetPrimaryKeys() throws SQLException {
9092
ResultSet rs = meta.getPrimaryKeys(null, null, "TEST");
9193

9294
assertNotNull(rs);
9395
assertTrue(rs.next());
9496

95-
assertNull(rs.getString("TABLE_CAT"));
96-
assertNull(rs.getString("TABLE_SCHEM"));
97-
assertEquals("TEST", rs.getString("TABLE_NAME"));
98-
assertEquals("ID", rs.getString("COLUMN_NAME"));
99-
assertEquals(1, rs.getInt("KEY_SEQ"));
100-
assertEquals("pk_unnamed_TEST_1", rs.getString("PK_NAME"));
97+
checkGetPrimaryKeysRow(rs, "TEST", "ID", "pk_unnamed_TEST_1", 1);
98+
99+
assertFalse(rs.next());
100+
101+
rs.close();
102+
}
103+
104+
@Test
105+
public void testGetPrimaryKeysCompound() throws SQLException {
106+
ResultSet rs = meta.getPrimaryKeys(null, null, "TEST_COMPOUND");
107+
108+
assertNotNull(rs);
109+
assertTrue(rs.next());
110+
checkGetPrimaryKeysRow(rs, "TEST_COMPOUND", "ID1", "pk_unnamed_TEST_COMPOUND_1", 2);
111+
112+
assertTrue(rs.next());
113+
checkGetPrimaryKeysRow(rs, "TEST_COMPOUND", "ID2", "pk_unnamed_TEST_COMPOUND_1", 1);
114+
115+
assertFalse(rs.next());
101116

117+
rs.close();
118+
}
119+
120+
@Test
121+
public void testGetPrimaryKeysIgnoresCatalogSchema() throws SQLException {
122+
String[] vals = new String[] {null, "", "IGNORE"};
123+
for (String cat : vals) {
124+
for (String schema : vals) {
125+
ResultSet rs = meta.getPrimaryKeys(cat, schema, "TEST");
126+
127+
assertNotNull(rs);
128+
assertTrue(rs.next());
129+
checkGetPrimaryKeysRow(rs, "TEST", "ID", "pk_unnamed_TEST_1", 1);
130+
assertFalse(rs.next());
131+
rs.close();
132+
}
133+
}
134+
}
135+
136+
@Test
137+
public void testGetPrimaryKeysNotFound() throws SQLException {
138+
String[] tables = new String[] {null, "", "NOSUCHTABLE"};
139+
for (String t : tables) {
140+
ResultSet rs = meta.getPrimaryKeys(null, null, t);
141+
assertNotNull(rs);
142+
assertFalse(rs.next());
143+
rs.close();
144+
}
145+
}
146+
147+
@Test
148+
public void testGetPrimaryKeyNonSQLSpace() throws SQLException {
149+
ResultSet rs = meta.getPrimaryKeys(null, null, "_vspace");
150+
assertNotNull(rs);
102151
assertFalse(rs.next());
152+
rs.close();
153+
}
103154

155+
@Test
156+
public void testGetPrimaryKeysOrderOfColumns() throws SQLException {
157+
ResultSet rs = meta.getPrimaryKeys(null, null, "TEST");
158+
assertNotNull(rs);
159+
ResultSetMetaData rsMeta = rs.getMetaData();
160+
assertEquals(6, rsMeta.getColumnCount());
161+
assertEquals("TABLE_CAT", rsMeta.getColumnName(1));
162+
assertEquals("TABLE_SCHEM", rsMeta.getColumnName(2));
163+
assertEquals("TABLE_NAME", rsMeta.getColumnName(3));
164+
assertEquals("COLUMN_NAME", rsMeta.getColumnName(4));
165+
assertEquals("KEY_SEQ", rsMeta.getColumnName(5));
166+
assertEquals("PK_NAME", rsMeta.getColumnName(6));
104167
rs.close();
105168
}
169+
170+
private void checkGetPrimaryKeysRow(ResultSet rs, String table, String colName, String pkName, int seq)
171+
throws SQLException {
172+
assertNull(rs.getString("TABLE_CAT"));
173+
assertNull(rs.getString("TABLE_SCHEM"));
174+
assertEquals(table, rs.getString("TABLE_NAME"));
175+
assertEquals(colName, rs.getString("COLUMN_NAME"));
176+
assertEquals(seq, rs.getInt("KEY_SEQ"));
177+
assertEquals(pkName, rs.getString("PK_NAME"));
178+
179+
assertNull(rs.getString(1));
180+
assertNull(rs.getString(2));
181+
assertEquals(table, rs.getString(3));
182+
assertEquals(colName, rs.getString(4));
183+
assertEquals(seq, rs.getInt(5));
184+
assertEquals(pkName, rs.getString(6));
185+
}
106186
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package org.tarantool.jdbc;
2+
3+
import org.junit.jupiter.api.AfterAll;
4+
import org.junit.jupiter.api.BeforeAll;
5+
import org.junit.jupiter.api.Test;
6+
import org.junit.jupiter.api.function.Executable;
7+
import org.tarantool.TarantoolConnection;
8+
9+
import java.sql.DatabaseMetaData;
10+
import java.sql.SQLException;
11+
import java.util.Arrays;
12+
import java.util.Collections;
13+
import java.util.HashMap;
14+
import java.util.Properties;
15+
16+
import static org.junit.jupiter.api.Assertions.assertThrows;
17+
import static org.junit.jupiter.api.Assertions.assertTrue;
18+
import static org.mockito.Mockito.doReturn;
19+
import static org.mockito.Mockito.mock;
20+
import static org.tarantool.jdbc.SQLDatabaseMetadata.FORMAT_IDX;
21+
import static org.tarantool.jdbc.SQLDatabaseMetadata.INDEX_FORMAT_IDX;
22+
import static org.tarantool.jdbc.SQLDatabaseMetadata.SPACE_ID_IDX;
23+
import static org.tarantool.jdbc.SQLDatabaseMetadata._VINDEX;
24+
import static org.tarantool.jdbc.SQLDatabaseMetadata._VSPACE;
25+
26+
import org.tarantool.TarantoolControl;
27+
28+
public class JdbcExceptionHandlingTest {
29+
protected static TarantoolControl control;
30+
31+
/**
32+
* We cannot mock TarantoolConnection constructor, so need listening
33+
* tarantool instance to prevent a test failure.
34+
*/
35+
@BeforeAll
36+
public static void setupEnv() throws Exception {
37+
control = new TarantoolControl();
38+
control.start("jdk-testing");
39+
control.waitStarted("jdk-testing");
40+
}
41+
42+
@AfterAll
43+
public static void teardownEnv() throws Exception {
44+
control.stop("jdk-testing");
45+
control.waitStopped("jdk-testing");
46+
}
47+
48+
/**
49+
* Simulates meta parsing error: missing "name" field in a space format for the primary key.
50+
*
51+
* @throws SQLException on failure.
52+
*/
53+
@Test
54+
public void testDatabaseMetaDataGetPrimaryKeysFormatError() throws SQLException {
55+
TarantoolConnection tntCon = mock(TarantoolConnection.class);
56+
SQLConnection conn = new SQLConnection(tntCon, "", new Properties());
57+
58+
Object[] spc = new Object[7];
59+
spc[FORMAT_IDX] = Collections.singletonList(new HashMap<String, Object>());
60+
spc[SPACE_ID_IDX] = 1000;
61+
62+
doReturn(Collections.singletonList(Arrays.asList(spc))).when(tntCon)
63+
.select(_VSPACE, 2, Collections.singletonList("TEST"), 0, 1, 0);
64+
65+
Object[] idx = new Object[6];
66+
idx[INDEX_FORMAT_IDX] = Collections.singletonList(
67+
new HashMap<String, Object>() {{ put("field", 0);}});
68+
69+
doReturn(Collections.singletonList(Arrays.asList(idx))).when(tntCon)
70+
.select(_VINDEX, 0, Arrays.asList(1000, 0), 0, 1, 0);
71+
72+
final DatabaseMetaData meta = conn.getMetaData();
73+
74+
Throwable t = assertThrows(SQLException.class, new Executable() {
75+
@Override
76+
public void execute() throws Throwable {
77+
meta.getPrimaryKeys(null, null, "TEST");
78+
}
79+
}, "Error processing metadata for table \"TEST\".");
80+
81+
assertTrue(t.getCause().getMessage().contains("Wrong value type"));
82+
}
83+
}

0 commit comments

Comments
 (0)