Permalink
Browse files

fix: NPE in DatabaseMetaData.getTypeInfo when types are dropped concu…

…rrently

DatabaseMetaData.getTypeInfo used several queries to load the data, thus the first of them
might observe some oid, then subsequent selects might fail to see it if the type was dropped.

The solution is not 100% bulletproof, however getTypeInfo always returns full info, thus it
is more likely to fail in a user-visible way.

closes #530
  • Loading branch information...
vlsi committed Mar 12, 2016
1 parent fbabfa4 commit f3b0fd007ca2fc186626c4ee5157015932c4a359
@@ -106,4 +106,13 @@ public void addCoreType(String pgTypeName, Integer oid, Integer sqlType, String
public boolean requiresQuoting(int oid) throws SQLException;
/**
* Returns true if particular sqlType requires quoting.
* This method is used internally by the driver, so it might disappear without notice.
*
* @param sqlType sql type as in java.sql.Types
* @return true if the type requires quoting
* @throws SQLException if something goes wrong
*/
boolean requiresQuotingSqlType(int sqlType) throws SQLException;
}
@@ -2777,12 +2777,16 @@ private static void addACLPrivileges(String acl, Map<String, Map<String, List<St
int typeOid = (int) rs.getLong(2);
tuple[0] = connection.encodeString(typname);
int sqlType = connection.getTypeInfo().getSQLType(typname);
tuple[1] =
connection.encodeString(Integer.toString(connection.getTypeInfo().getSQLType(typname)));
connection.encodeString(Integer.toString(sqlType));
tuple[2] = connection
.encodeString(Integer.toString(connection.getTypeInfo().getMaximumPrecision(typeOid)));
if (connection.getTypeInfo().requiresQuoting(typeOid)) {
// Using requiresQuoting(oid) would might trigger select statements that might fail with NPE
// if oid in question is being dropped.
// requiresQuotingSqlType is not bulletproof, however, it solves the most visible NPE.
if (connection.getTypeInfo().requiresQuotingSqlType(sqlType)) {
tuple[3] = bliteral;
tuple[4] = bliteral;
}
@@ -847,6 +847,18 @@ public int getMaximumPrecision(int oid) {
public boolean requiresQuoting(int oid) throws SQLException {
int sqlType = getSQLType(oid);
return requiresQuotingSqlType(sqlType);
}
/**
* Returns true if particular sqlType requires quoting.
* This method is used internally by the driver, so it might disappear without notice.
*
* @param sqlType sql type as in java.sql.Types
* @return true if the type requires quoting
* @throws SQLException if something goes wrong
*/
public boolean requiresQuotingSqlType(int sqlType) throws SQLException {
switch (sqlType) {
case Types.BIGINT:
case Types.DOUBLE:
@@ -49,6 +49,8 @@ public static TestSuite suite() throws Exception {
// Connectivity/Protocols
suite.addTest(new JUnit4TestAdapter(TypeCacheDLLStressTest.class));
// ResultSet
suite.addTestSuite(ResultSetTest.class);
suite.addTestSuite(ResultSetMetaDataTest.class);
@@ -0,0 +1,97 @@
package org.postgresql.test.jdbc2;
import org.postgresql.test.TestUtil;
import org.junit.Test;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class TypeCacheDLLStressTest extends BaseTest4 {
private final static int DURATION = Integer.getInteger("TypeCacheDLLStressTest.DURATION", 5);
Connection con2;
@Override
protected void updateProperties(Properties props) {
try {
con2 = TestUtil.openDB(props);
} catch (Exception e) {
throw new IllegalStateException("Unable to open second DB connection", e);
}
}
@Override
public void setUp() throws Exception {
super.setUp();
TestUtil.createTable(con, "create_and_drop_table", "user_id serial PRIMARY KEY");
}
@Override
public void tearDown() throws SQLException {
TestUtil.closeDB(con2);
}
@Test
public void createDropTableAndGetTypeInfo() throws Throwable {
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<Void> typeInfoCache = executor.submit(new Callable<Void>() {
public Void call() throws Exception {
while (!Thread.currentThread().isInterrupted()) {
ResultSet rs = con.getMetaData().getTypeInfo();
rs.close();
}
return null;
}
});
Future<Void> createAndDrop = executor.submit(new Callable<Void>() {
public Void call() throws Exception {
Statement stmt = con2.createStatement();
while (!Thread.currentThread().isInterrupted()) {
stmt.execute("drop TABLE create_and_drop_table");
stmt.execute("CREATE TABLE create_and_drop_table"
+ "( user_id serial PRIMARY KEY, username VARCHAR (50) UNIQUE NOT NULL"
+ ", password VARCHAR (50) NOT NULL, email VARCHAR (355) UNIQUE NOT NULL"
+ ", created_on TIMESTAMP NOT NULL, last_login TIMESTAMP)");
}
return null;
}
});
try {
typeInfoCache.get(DURATION, TimeUnit.SECONDS);
} catch (ExecutionException e) {
createAndDrop.cancel(true);
throw e.getCause();
} catch (TimeoutException e) {
// Test is expected to run as long as it can
}
typeInfoCache.cancel(true);
createAndDrop.cancel(true);
try {
createAndDrop.get(DURATION, TimeUnit.SECONDS);
} catch (ExecutionException e) {
throw e.getCause();
} catch (TimeoutException e) {
// Test is expected to run as long as it can
} catch (CancellationException e) {
// Ignore
}
}
}

0 comments on commit f3b0fd0

Please sign in to comment.