Skip to content

Commit 06b7ffb

Browse files
committed
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 tarantool#41
1 parent 8aad007 commit 06b7ffb

File tree

4 files changed

+215
-32
lines changed

4 files changed

+215
-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
@@ -32,9 +32,6 @@ public abstract class AbstractJdbcIT {
3232
private static String URL = String.format("tarantool://%s:%d?user=%s&password=%s", host, port, user, pass);
3333

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

@@ -78,12 +75,15 @@ public abstract class AbstractJdbcIT {
7875
"X'010203040506'," + //LONGVARBINARY
7976
"'1983-03-14'," + //DATE
8077
"'12:01:06'," + //TIME
81-
"129479994)" //TIMESTAMP
78+
"129479994)", //TIMESTAMP
79+
80+
"CREATE TABLE test_compound(id1 INT, id2 INT, val VARCHAR(100), PRIMARY KEY (id2, id1))"
8281
};
8382

8483
private static String[] cleanSql = new String[] {
8584
"DROP TABLE IF EXISTS test",
86-
"DROP TABLE IF EXISTS test_types"
85+
"DROP TABLE IF EXISTS test_types",
86+
"DROP TABLE IF EXISTS test_compound"
8787
};
8888

8989
static Object[] testRow = new Object[] {
@@ -112,6 +112,7 @@ public abstract class AbstractJdbcIT {
112112

113113
@BeforeAll
114114
public static void setupEnv() throws Exception {
115+
sqlExec(cleanSql);
115116
sqlExec(initSql);
116117
}
117118

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,60 @@
1+
package org.tarantool.jdbc;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.function.Executable;
5+
import org.tarantool.TarantoolConnection;
6+
7+
import java.sql.DatabaseMetaData;
8+
import java.sql.SQLException;
9+
import java.util.Arrays;
10+
import java.util.Collections;
11+
import java.util.HashMap;
12+
import java.util.Properties;
13+
14+
import static org.junit.jupiter.api.Assertions.assertThrows;
15+
import static org.junit.jupiter.api.Assertions.assertTrue;
16+
import static org.mockito.Mockito.doReturn;
17+
import static org.mockito.Mockito.mock;
18+
import static org.tarantool.jdbc.SQLDatabaseMetadata.FORMAT_IDX;
19+
import static org.tarantool.jdbc.SQLDatabaseMetadata.INDEX_FORMAT_IDX;
20+
import static org.tarantool.jdbc.SQLDatabaseMetadata.SPACE_ID_IDX;
21+
import static org.tarantool.jdbc.SQLDatabaseMetadata._VINDEX;
22+
import static org.tarantool.jdbc.SQLDatabaseMetadata._VSPACE;
23+
24+
public class JdbcExceptionHandlingTest {
25+
/**
26+
* Simulates meta parsing error: missing "name" field in a space format for the primary key.
27+
*
28+
* @throws SQLException on failure.
29+
*/
30+
@Test
31+
public void testDatabaseMetaDataGetPrimaryKeysFormatError() throws SQLException {
32+
TarantoolConnection tntCon = mock(TarantoolConnection.class);
33+
SQLConnection conn = new SQLConnection(tntCon, "", new Properties());
34+
35+
Object[] spc = new Object[7];
36+
spc[FORMAT_IDX] = Collections.singletonList(new HashMap<String, Object>());
37+
spc[SPACE_ID_IDX] = 1000;
38+
39+
doReturn(Collections.singletonList(Arrays.asList(spc))).when(tntCon)
40+
.select(_VSPACE, 2, Collections.singletonList("TEST"), 0, 1, 0);
41+
42+
Object[] idx = new Object[6];
43+
idx[INDEX_FORMAT_IDX] = Collections.singletonList(
44+
new HashMap<String, Object>() {{ put("field", 0);}});
45+
46+
doReturn(Collections.singletonList(Arrays.asList(idx))).when(tntCon)
47+
.select(_VINDEX, 0, Arrays.asList(1000, 0), 0, 1, 0);
48+
49+
final DatabaseMetaData meta = conn.getMetaData();
50+
51+
Throwable t = assertThrows(SQLException.class, new Executable() {
52+
@Override
53+
public void execute() throws Throwable {
54+
meta.getPrimaryKeys(null, null, "TEST");
55+
}
56+
}, "Error processing metadata for table \"TEST\".");
57+
58+
assertTrue(t.getCause().getMessage().contains("Wrong value type"));
59+
}
60+
}

0 commit comments

Comments
 (0)