From 151b287732a551c380dcaa34f9c0549aeeb26208 Mon Sep 17 00:00:00 2001 From: MMeent Date: Thu, 29 Jul 2021 13:04:39 +0200 Subject: [PATCH] Fix issue #2215 (#2217) Correctly handle the non-signedness of OIDs in relation to signed java integers in the new OID-based type cache. Previously, this would cause 'bad value for type int'-errors, now this is correctly handled both ways in this part of the code. Co-authored-by: Matthias van de Meent --- .../java/org/postgresql/core/TypeInfo.java | 26 ++++++++++ .../postgresql/jdbc/PgDatabaseMetaData.java | 12 +++-- .../org/postgresql/jdbc/TypeInfoCache.java | 20 +++++-- .../test/jdbc42/DatabaseMetaDataTest.java | 52 +++++++++++++++++++ 4 files changed, 104 insertions(+), 6 deletions(-) diff --git a/pgjdbc/src/main/java/org/postgresql/core/TypeInfo.java b/pgjdbc/src/main/java/org/postgresql/core/TypeInfo.java index d8d7e9e740..0fa3ba8eda 100644 --- a/pgjdbc/src/main/java/org/postgresql/core/TypeInfo.java +++ b/pgjdbc/src/main/java/org/postgresql/core/TypeInfo.java @@ -116,4 +116,30 @@ void addCoreType(String pgTypeName, Integer oid, Integer sqlType, String javaCla * @throws SQLException if something goes wrong */ boolean requiresQuotingSqlType(int sqlType) throws SQLException; + + /** + *

Java Integers are signed 32-bit integers, but oids are unsigned 32-bit integers. + * We therefore read them as positive long values and then force them into signed integers + * (wrapping around into negative values when required) or we'd be unable to correctly + * handle the upper half of the oid space.

+ * + *

This function handles the mapping of uint32-values in the long to java integers, and + * throws for values that are out of range.

+ * + * @param oid the oid as a long. + * @return the (internal) signed integer representation of the (unsigned) oid. + * @throws SQLException if the long has a value outside of the range representable by uint32 + */ + int longOidToInt(long oid) throws SQLException; + + /** + * Java Integers are signed 32-bit integers, but oids are unsigned 32-bit integers. + * We must therefore first map the (internal) integer representation to a positive long + * value before sending it to postgresql, or we would be unable to correctly handle the + * upper half of the oid space because these negative values are disallowed as OID values. + * + * @param oid the (signed) integer oid to convert into a long. + * @return the non-negative value of this oid, stored as a java long. + */ + long intOidToLong(int oid); } diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java index a45c64e317..c90af9da47 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java @@ -2666,12 +2666,18 @@ public ResultSet getUDTs(@Nullable String catalog, @Nullable String schemaPatter + java.sql.Types.DISTINCT + " end as data_type, pg_catalog.obj_description(t.oid, 'pg_type') " + "as remarks, CASE WHEN t.typtype = 'd' then (select CASE"; + TypeInfo typeInfo = connection.getTypeInfo(); StringBuilder sqlwhen = new StringBuilder(); - for (Iterator i = connection.getTypeInfo().getPGTypeOidsWithSQLTypes(); i.hasNext(); ) { + for (Iterator i = typeInfo.getPGTypeOidsWithSQLTypes(); i.hasNext(); ) { Integer typOid = i.next(); - int sqlType = connection.getTypeInfo().getSQLType(typOid); - sqlwhen.append(" when t.oid = ").append(typOid).append(" then ").append(sqlType); + // NB: Java Integers are signed 32-bit integers, but oids are unsigned 32-bit integers. + // We must therefore map it to a positive long value before writing it into the query, + // or we'll be unable to correctly handle ~ half of the oid space. + long longTypOid = typeInfo.intOidToLong(typOid); + int sqlType = typeInfo.getSQLType(typOid); + + sqlwhen.append(" when t.oid = ").append(longTypOid).append(" then ").append(sqlType); } sql += sqlwhen.toString(); diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/TypeInfoCache.java b/pgjdbc/src/main/java/org/postgresql/jdbc/TypeInfoCache.java index a120c3bcdf..ee17232149 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/TypeInfoCache.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/TypeInfoCache.java @@ -279,7 +279,7 @@ public void cacheSQLTypes() throws SQLException { pgNameToSQLType.put(typeName, type); } - Integer typeOid = castNonNull(rs.getInt("oid")); + Integer typeOid = longOidToInt(castNonNull(rs.getLong("oid"))); if (!oidToSQLType.containsKey(typeOid)) { oidToSQLType.put(typeOid, type); } @@ -310,11 +310,11 @@ public synchronized int getSQLType(int typeOid) throws SQLException { return i; } - LOGGER.log(Level.FINEST, "querying SQL typecode for pg type oid '{0}'", typeOid); + LOGGER.log(Level.FINEST, "querying SQL typecode for pg type oid '{0}'", intOidToLong(typeOid)); PreparedStatement getTypeInfoStatement = prepareGetTypeInfoStatement(); - getTypeInfoStatement.setInt(1, typeOid); + getTypeInfoStatement.setLong(1, intOidToLong(typeOid)); // Go through BaseStatement to avoid transaction start. if (!((BaseStatement) getTypeInfoStatement) @@ -973,4 +973,18 @@ public boolean requiresQuotingSqlType(int sqlType) throws SQLException { } return true; } + + @Override + public int longOidToInt(long oid) throws SQLException { + if ((oid & 0xFFFF_FFFF_0000_0000L) != 0) { + throw new PSQLException(GT.tr("Value is not an OID: {0}", oid), PSQLState.NUMERIC_VALUE_OUT_OF_RANGE); + } + + return (int) oid; + } + + @Override + public long intOidToLong(int oid) { + return ((long) oid) & 0xFFFFFFFFL; + } } diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc42/DatabaseMetaDataTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc42/DatabaseMetaDataTest.java index 7a4c9071c3..ee225dd0b1 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/jdbc42/DatabaseMetaDataTest.java +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc42/DatabaseMetaDataTest.java @@ -9,7 +9,11 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import org.postgresql.core.TypeInfo; +import org.postgresql.jdbc.PgConnection; import org.postgresql.test.TestUtil; +import org.postgresql.util.PSQLException; +import org.postgresql.util.PSQLState; import org.junit.After; import org.junit.Before; @@ -18,6 +22,7 @@ import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; +import java.sql.SQLException; import java.sql.Types; public class DatabaseMetaDataTest { @@ -102,4 +107,51 @@ public void testGetCorrectSQLTypeForShadowedTypes() throws Exception { assertFalse(rs.next()); } + + @Test + public void testLargeOidIsHandledCorrectly() throws SQLException { + TypeInfo ti = conn.unwrap(PgConnection.class).getTypeInfo(); + + try { + ti.getSQLType((int) 4294967295L); // (presumably) unused OID 4294967295, which is 2**32 - 1 + } catch (PSQLException ex) { + assertEquals(ex.getSQLState(), PSQLState.NO_DATA.getState()); + } + } + + @Test + public void testOidConversion() throws SQLException { + TypeInfo ti = conn.unwrap(PgConnection.class).getTypeInfo(); + int oid = 0; + long loid = 0; + assertEquals(oid, ti.longOidToInt(loid)); + assertEquals(loid, ti.intOidToLong(oid)); + + oid = Integer.MAX_VALUE; + loid = Integer.MAX_VALUE; + assertEquals(oid, ti.longOidToInt(loid)); + assertEquals(loid, ti.intOidToLong(oid)); + + oid = Integer.MIN_VALUE; + loid = 1L << 31; + assertEquals(oid, ti.longOidToInt(loid)); + assertEquals(loid, ti.intOidToLong(oid)); + + oid = -1; + loid = 0xFFFFFFFFL; + assertEquals(oid, ti.longOidToInt(loid)); + assertEquals(loid, ti.intOidToLong(oid)); + } + + @Test(expected = PSQLException.class) + public void testOidConversionThrowsForNegativeLongValues() throws SQLException { + TypeInfo ti = conn.unwrap(PgConnection.class).getTypeInfo(); + ti.longOidToInt(-1); + } + + @Test(expected = PSQLException.class) + public void testOidConversionThrowsForTooLargeLongValues() throws SQLException { + TypeInfo ti = conn.unwrap(PgConnection.class).getTypeInfo(); + ti.longOidToInt(1L << 32); + } }