Repository: pgjdbc/pgjdbc
Pull request #732 (2017-01-16 21:50:02)

fix: improve handling of getBoolean and setObject
This merge the code used to cast to boolean in getBoolean and setObject, it allows only valid values and reject wrong values.

This follows the specification more close and proper support the boolean types of PostgreSQL.

codecov-io: ## [Current coverage](https://codecov.io/gh/pgjdbc/pgjdbc/pull/732?src=pr) is 64.23% (diff: 85.71%)
> Merging [#732](https://codecov.io/gh/pgjdbc/pgjdbc/pull/732?src=pr) into [master](https://codecov.io/gh/pgjdbc/pgjdbc/branch/master?src=pr) will decrease coverage by **<.01%**


```diff
@@             master       #732   diff @@
==========================================
  Files           163        164     +1   
  Lines         15142      15122    -20   
  Methods           0          0          
  Messages          0          0          
  Branches       2987       2973    -14   
==========================================
- Hits           9727       9713    -14   
  Misses         4182       4182          
+ Partials       1233       1227     -6   
```

> Powered by [Codecov](https://codecov.io?src=pr). Last update [e1a2578...649784a](https://codecov.io/gh/pgjdbc/pgjdbc/compare/e1a2578247684667a51bc34ee53449e4457755df...649784a91425dabd1be8b65a312131207bdb50ff?src=pr)
davecramer: On 24 January 2017 at 17:22, Vladimir Sitnikov <notifications@github.com>
wrote:

> *@vlsi* commented on this pull request.
> ------------------------------
>
> In pgjdbc/src/main/java/org/postgresql/jdbc/BooleanTypeUtil.java
> <https://github.com/pgjdbc/pgjdbc/pull/732#pullrequestreview-18282990>:
>
> > +  }
> +
> +  private static boolean from(final String strval) throws PSQLException {
> +    // Leading or trailing whitespace is ignored, and case does not matter.
> +    final String val = strval.trim();
> +    if ("1".equals(val) || "true".equalsIgnoreCase(val)
> +        || "t".equalsIgnoreCase(val) || "yes".equalsIgnoreCase(val)
> +        || "y".equalsIgnoreCase(val) || "on".equalsIgnoreCase(val)) {
> +      return true;
> +    }
> +    if ("0".equals(val) || "false".equalsIgnoreCase(val)
> +        || "f".equalsIgnoreCase(val) || "no".equalsIgnoreCase(val)
> +        || "n".equalsIgnoreCase(val) || "off".equalsIgnoreCase(val)) {
> +      return false;
> +    }
> +    throw cannotCastException("String");
>
> I wonder if adding actual "bad" value to the error message would count as
> a security violation.
> I think adding the value explicitly would simplify the debugging.
>
> Any thoughts?
>

The only way it might be a security problem is if the exception wasn't
caught. This is the programmers problem IMO

Dave Cramer

> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/pgjdbc/pgjdbc/pull/732#pullrequestreview-18282990>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAYz9p83KYdtC4vLXIMCsxi_-tdmhT0Mks5rVnmJgaJpZM4LlA2V>
> .
>

vlsi: >The only way it might be a security problem is if the exception wasn't
caught. This is the programmers problem IMO

In that case I think it makes sense to include the value to the exception, so programmer can understand and fix the problem.
jorsol: @vlsi , Apart from the value in the exception, I also included the logger so it's now even easier to debug and fix the problem. I also improve the boolean tests and included an appropriate javadoc to getBoolean.
vlsi: @jorsol , did you check performance implications of adding a debug logging to a success path?

Quick glance suggests it is not free: http://stackoverflow.com/a/39748222/1261287

I don't think there were similar cases when "getXXX" involved memory allocation through `logger.log(,  new Object[]{...})`

I'm inclined to remove that kind of debug logging from the hot path.
I would be fine if array allocation was guarded by the relevant `isLoggable` check.
jorsol: @vlsi , I checked now, and yes, there is a performance penalty, I simplify it to still be able to debug the value and don't hit the performance penalty when logging is off...

BTW there is always a performance penalty of having the logger enabled, but only if the level allows it. That's why in production its not recommended to have logging enabled or with a DEBUG (FINE) level, and it usage is only for that "debug".
```
        if (!isLoggable(level)) {
            return;
        }
```

Patch:
diff --git a/pgjdbc/src/main/java/org/postgresql/ds/PGPoolingDataSource.java b/pgjdbc/src/main/java/org/postgresql/ds/PGPoolingDataSource.java
index 7133f565..5e770ac4 100644
--- a/pgjdbc/src/main/java/org/postgresql/ds/PGPoolingDataSource.java
+++ b/pgjdbc/src/main/java/org/postgresql/ds/PGPoolingDataSource.java
@@ -54,7 +54,11 @@ import javax.sql.PooledConnection;
  * </p>
  *
  * @author Aaron Mulder (ammulder@chariotsolutions.com)
+ *
+ * @deprecated Since 42.0.0, instead of this class you should use a fully featured connection pool
+ * like HikariCP, vibur-dbcp, commons-dbcp, c3p0, etc.
  */
+@Deprecated
 public class PGPoolingDataSource extends BaseDataSource implements DataSource {
   protected static ConcurrentMap<String, PGPoolingDataSource> dataSources =
       new ConcurrentHashMap<String, PGPoolingDataSource>();
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/BooleanTypeUtil.java b/pgjdbc/src/main/java/org/postgresql/jdbc/BooleanTypeUtil.java
new file mode 100644
index 00000000..446dc9d1
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/BooleanTypeUtil.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2017, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.jdbc;
+
+import org.postgresql.util.GT;
+import org.postgresql.util.PSQLException;
+import org.postgresql.util.PSQLState;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Helper class to handle boolean type of PostgreSQL.
+ * <p>
+ * Based on values accepted by the PostgreSQL server:
+ * https://www.postgresql.org/docs/current/static/datatype-boolean.html
+ */
+class BooleanTypeUtil {
+
+  private static final Logger LOGGER = Logger.getLogger(BooleanTypeUtil.class.getName());
+
+  private BooleanTypeUtil() {
+  }
+
+  /**
+   * Cast an Object value to the corresponding boolean value.
+   *
+   * @param in Object to cast into boolean
+   * @return boolean value corresponding to the cast of the object
+   * @throws PSQLException PSQLState.CANNOT_COERCE
+   */
+  static boolean castToBoolean(final Object in) throws PSQLException {
+    LOGGER.log(Level.FINE, "Object to cast: {0}", in);
+    if (in instanceof Boolean) {
+      return (Boolean) in;
+    }
+    if (in instanceof String) {
+      return from((String) in);
+    }
+    if (in instanceof Character) {
+      return from((Character) in);
+    }
+    if (in instanceof Number) {
+      return from((Number) in, in.getClass().getName());
+    }
+    throw cannotCoerceException(in.getClass().getName());
+  }
+
+  private static boolean from(final String strval) throws PSQLException {
+    // Leading or trailing whitespace is ignored, and case does not matter.
+    final String val = strval.trim();
+    if ("1".equals(val) || "true".equalsIgnoreCase(val)
+        || "t".equalsIgnoreCase(val) || "yes".equalsIgnoreCase(val)
+        || "y".equalsIgnoreCase(val) || "on".equalsIgnoreCase(val)) {
+      return true;
+    }
+    if ("0".equals(val) || "false".equalsIgnoreCase(val)
+        || "f".equalsIgnoreCase(val) || "no".equalsIgnoreCase(val)
+        || "n".equalsIgnoreCase(val) || "off".equalsIgnoreCase(val)) {
+      return false;
+    }
+    throw cannotCoerceException("String", val);
+  }
+
+  private static boolean from(final Character charval) throws PSQLException {
+    if ('1' == charval || 't' == charval || 'T' == charval
+        || 'y' == charval || 'Y' == charval) {
+      return true;
+    }
+    if ('0' == charval || 'f' == charval || 'F' == charval
+        || 'n' == charval || 'N' == charval) {
+      return false;
+    }
+    throw cannotCoerceException("Character", charval.toString());
+  }
+
+  private static boolean from(final Number numval, final String className) throws PSQLException {
+    // Handles BigDecimal, Byte, Short, Integer, Long Float, Double
+    // based on the widening primitive conversions.
+    double value = numval.doubleValue();
+    if (value == 1.0d) {
+      return true;
+    }
+    if (value == 0.0d) {
+      return false;
+    }
+    throw cannotCoerceException(className, String.valueOf(numval));
+  }
+
+  private static PSQLException cannotCoerceException(final String fromType) {
+    LOGGER.log(Level.FINE, "Cannot cast type {0} to boolean", fromType);
+    return new PSQLException(GT.tr("Cannot cast type {0} to boolean", fromType),
+        PSQLState.CANNOT_COERCE);
+  }
+
+  private static PSQLException cannotCoerceException(final String fromType, final String value) {
+    LOGGER.log(Level.FINE, "Cannot cast type {0} to boolean: \"{1}\"", new Object[]{fromType, value});
+    return new PSQLException(GT.tr("Cannot cast type {0} to boolean: \"{1}\"", fromType, value),
+        PSQLState.CANNOT_COERCE);
+  }
+
+}
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgArray.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgArray.java
index f75d0ab2..19163491 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgArray.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgArray.java
@@ -577,9 +577,9 @@ public class PgArray implements java.sql.Array {
 
         if (dims > 1 || useObjects) {
           oa[length++] = o == null ? null
-              : (dims > 1 ? buildArray((PgArrayList) o, 0, -1) : PgResultSet.toBoolean((String) o));
+            : (dims > 1 ? buildArray((PgArrayList) o, 0, -1) : BooleanTypeUtil.castToBoolean((String) o));
         } else {
-          pa[length++] = o == null ? false : PgResultSet.toBoolean((String) o);
+          pa[length++] = o == null ? false : BooleanTypeUtil.castToBoolean((String) o);
         }
       }
     } else if (type == Types.SMALLINT || type == Types.INTEGER) {
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java
index 49268f43..495b7654 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java
@@ -206,9 +206,6 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement {
     checkClosed();
 
     int oid;
-    if (sqlType == Types.BOOLEAN) {
-      sqlType = Types.BIT;
-    }
     switch (sqlType) {
       case Types.SQLXML:
         oid = Oid.XML;
@@ -252,6 +249,7 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement {
       case Types.TIMESTAMP:
         oid = Oid.UNSPECIFIED;
         break;
+      case Types.BOOLEAN:
       case Types.BIT:
         oid = Oid.BOOL;
         break;
@@ -527,10 +525,6 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement {
       return;
     }
 
-    if (targetSqlType == Types.BOOLEAN) {
-      targetSqlType = Types.BIT;
-    }
-
     if (targetSqlType == Types.OTHER && in instanceof UUID
         && connection.haveMinimumServerVersion(ServerVersion.v8_3)) {
       setUuid(parameterIndex, (UUID) in);
@@ -649,8 +643,9 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement {
         }
         break;
       //#endif
+      case Types.BOOLEAN:
       case Types.BIT:
-        setBoolean(parameterIndex, castToBoolean(in));
+        setBoolean(parameterIndex, BooleanTypeUtil.castToBoolean(in));
         break;
       case Types.BINARY:
       case Types.VARBINARY:
@@ -877,74 +872,6 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement {
     throw cannotCastException(in.getClass().getName(), "BigDecimal");
   }
 
-  private static boolean castToBoolean(final Object in) throws SQLException {
-    if (in instanceof Boolean) {
-      return (Boolean) in;
-    }
-    if (in instanceof String) {
-      // Leading or trailing whitespace is ignored, and case does not matter.
-      final String strval = ((String) in).trim();
-      Boolean value = booleanTypeFromString(strval);
-      if (value == null) {
-        throw cannotCastException("String", "boolean");
-      }
-      return value;
-    }
-    if (in instanceof Character) {
-      Boolean value = null;
-      final Character charval = (Character) in;
-      if ('1' == charval || 't' == charval || 'T' == charval
-          || 'y' == charval || 'Y' == charval) {
-        value = Boolean.TRUE;
-      } else if ('0' == charval || 'f' == charval || 'F' == charval
-          || 'n' == charval || 'N' == charval) {
-        value = Boolean.FALSE;
-      } else if (value == null) {
-        throw cannotCastException("Character", "boolean");
-      }
-      return value;
-    }
-    if (in instanceof BigDecimal) {
-      return ((BigDecimal) in).signum() != 0;
-    }
-    if (in instanceof Number) {
-      return ((Number) in).longValue() != 0L;
-    }
-    if (in instanceof java.util.Date) {
-      return ((java.util.Date) in).getTime() != 0L;
-    }
-    if (in instanceof Clob) {
-      final String strval = asString((Clob) in).trim();
-      Boolean value = booleanTypeFromString(strval);
-      if (value == null) {
-        throw cannotCastException("Clob", "boolean");
-      }
-      return value;
-    }
-    throw cannotCastException(in.getClass().getName(), "boolean");
-  }
-
-  /**
-   * Based on values accepted by the PostgreSQL server:
-   * https://www.postgresql.org/docs/current/static/datatype-boolean.html
-   *
-   * @param val String with the value to "parse"
-   * @return Boolean if any valid value match, null otherwise.
-   */
-  private static Boolean booleanTypeFromString(final String val) {
-    if ("1".equals(val) || "true".equalsIgnoreCase(val)
-        || "t".equalsIgnoreCase(val) || "yes".equalsIgnoreCase(val)
-        || "y".equalsIgnoreCase(val) || "on".equalsIgnoreCase(val)) {
-      return Boolean.TRUE;
-    }
-    if ("0".equals(val) || "false".equalsIgnoreCase(val)
-        || "f".equalsIgnoreCase(val) || "no".equalsIgnoreCase(val)
-        || "n".equalsIgnoreCase(val) || "off".equalsIgnoreCase(val)) {
-      return Boolean.FALSE;
-    }
-    return null;
-  }
-
   private static String castToString(final Object in) throws SQLException {
     try {
       if (in instanceof String) {
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java
index 0ae52a7e..2d20e10a 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java
@@ -169,12 +169,10 @@ public class PgResultSet implements ResultSet, org.postgresql.PGRefCursorResultS
   protected Object internalGetObject(int columnIndex, Field field) throws SQLException {
     switch (getSQLType(columnIndex)) {
       case Types.BOOLEAN:
+      case Types.BIT:
         return getBoolean(columnIndex);
       case Types.SQLXML:
         return getSQLXML(columnIndex);
-      case Types.BIT:
-        // Also Types.BOOLEAN in JDBC3
-        return getBoolean(columnIndex) ? Boolean.TRUE : Boolean.FALSE;
       case Types.TINYINT:
       case Types.SMALLINT:
       case Types.INTEGER:
@@ -531,6 +529,31 @@ public class PgResultSet implements ResultSet, org.postgresql.PGRefCursorResultS
     return connection.getTimestampUtils().toTime(cal, string);
   }
 
+  //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
+  private LocalTime getLocalTime(int i) throws SQLException {
+    checkResultSet(i);
+    if (wasNullFlag) {
+      return null;
+    }
+
+    if (isBinary(i)) {
+      int col = i - 1;
+      int oid = fields[col].getOID();
+      if (oid == Oid.TIME) {
+        return connection.getTimestampUtils().toLocalTimeBin(this_row[col]);
+      } else {
+        throw new PSQLException(
+            GT.tr("Cannot convert the column of type {0} to requested type {1}.",
+                Oid.toString(oid), "time"),
+            PSQLState.DATA_TYPE_MISMATCH);
+      }
+    }
+
+    String string = getString(i);
+    return connection.getTimestampUtils().toLocalTime(string);
+  }
+  //#endif
+
 
   @Override
   public Timestamp getTimestamp(int i, java.util.Calendar cal) throws SQLException {
@@ -1893,6 +1916,29 @@ public class PgResultSet implements ResultSet, org.postgresql.PGRefCursorResultS
     }
   }
 
+  /**
+   * Retrieves the value of the designated column in the current row of this <code>ResultSet</code>
+   * object as a <code>boolean</code> in the Java programming language.
+   * <p>
+   * If the designated column has a Character datatype and is one of the following values: "1",
+   * "true", "t", "yes", "y" or "on", a value of <code>true</code> is returned. If the designated
+   * column has a Character datatype and is one of the following values: "0", "false", "f", "no",
+   * "n" or "off", a value of <code>false</code> is returned. Leading or trailing whitespace is
+   * ignored, and case does not matter.
+   * <p>
+   * If the designated column has a Numeric datatype and is a 1, a value of <code>true</code> is
+   * returned. If the designated column has a Numeric datatype and is a 0, a value of
+   * <code>false</code> is returned.
+   *
+   * @param columnIndex the first column is 1, the second is 2, ...
+   * @return the column value; if the value is SQL <code>NULL</code>, the value returned is
+   * <code>false</code>
+   * @exception SQLException if the columnIndex is not valid; if a database access error occurs or
+   * this method is called on a closed result set
+   * @see <a href="https://www.postgresql.org/docs/current/static/datatype-boolean.html">PostgreSQL
+   * Boolean Type</a>
+   */
+  @Override
   public boolean getBoolean(int columnIndex) throws SQLException {
     checkResultSet(columnIndex);
     if (wasNullFlag) {
@@ -1901,10 +1947,10 @@ public class PgResultSet implements ResultSet, org.postgresql.PGRefCursorResultS
 
     if (isBinary(columnIndex)) {
       int col = columnIndex - 1;
-      return readDoubleValue(this_row[col], fields[col].getOID(), "boolean") == 1;
+      return BooleanTypeUtil.castToBoolean(readDoubleValue(this_row[col], fields[col].getOID(), "boolean"));
     }
 
-    return toBoolean(getString(columnIndex));
+    return BooleanTypeUtil.castToBoolean(getString(columnIndex));
   }
 
   private static final BigInteger BYTEMAX = new BigInteger(Byte.toString(Byte.MAX_VALUE));
@@ -2415,6 +2461,7 @@ public class PgResultSet implements ResultSet, org.postgresql.PGRefCursorResultS
     return getString(findColumn(columnName));
   }
 
+  @Override
   public boolean getBoolean(String columnName) throws SQLException {
     return getBoolean(findColumn(columnName));
   }
@@ -2734,28 +2781,6 @@ public class PgResultSet implements ResultSet, org.postgresql.PGRefCursorResultS
 
   // ----------------- Formatting Methods -------------------
 
-  public static boolean toBoolean(String s) {
-    if (s != null) {
-      s = s.trim();
-
-      if (s.equalsIgnoreCase("t") || s.equalsIgnoreCase("true") || s.equals("1")) {
-        return true;
-      }
-
-      if (s.equalsIgnoreCase("f") || s.equalsIgnoreCase("false") || s.equals("0")) {
-        return false;
-      }
-
-      try {
-        if (Double.parseDouble(s) == 1) {
-          return true;
-        }
-      } catch (NumberFormatException e) {
-      }
-    }
-    return false; // SQL NULL
-  }
-
   private static final BigInteger INTMAX = new BigInteger(Integer.toString(Integer.MAX_VALUE));
   private static final BigInteger INTMIN = new BigInteger(Integer.toString(Integer.MIN_VALUE));
 
@@ -3266,11 +3291,7 @@ public class PgResultSet implements ResultSet, org.postgresql.PGRefCursorResultS
       }
     } else if (type == LocalTime.class) {
       if (sqlType == Types.TIME) {
-        Time timeValue = getTime(columnIndex);
-        if (wasNull()) {
-          return null;
-        }
-        return type.cast(timeValue.toLocalTime());
+        return type.cast(getLocalTime(columnIndex));
       } else {
         throw new SQLException("conversion to " + type + " from " + sqlType + " not supported");
       }
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/TimestampUtils.java b/pgjdbc/src/main/java/org/postgresql/jdbc/TimestampUtils.java
index 28bc06be..d0a3b174 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/TimestampUtils.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/TimestampUtils.java
@@ -25,6 +25,7 @@ import java.time.LocalTime;
 import java.time.OffsetDateTime;
 import java.time.ZoneOffset;
 import java.time.chrono.IsoEra;
+import java.time.format.DateTimeParseException;
 import java.time.temporal.ChronoField;
 //#endif
 import java.util.Calendar;
@@ -393,6 +394,27 @@ public class TimestampUtils {
   }
 
   //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
+  /**
+   * Parse a string and return a LocalTime representing its value.
+   *
+   * @param s The ISO formated time string to parse.
+   * @return null if s is null or a LocalTime of the parsed string s.
+   * @throws SQLException if there is a problem parsing s.
+   */
+  public LocalTime toLocalTime(String s) throws SQLException {
+    if (s == null) {
+      return null;
+    }
+    try {
+      return LocalTime.parse(s);
+    } catch (DateTimeParseException nfe) {
+      throw new PSQLException(
+          GT.tr("Bad value for type timestamp/date/time: {1}", s),
+          PSQLState.BAD_DATETIME_FORMAT, nfe);
+    }
+
+  }
+
   /**
    * Parse a string and return a LocalDateTime representing its value.
    *
@@ -888,6 +910,36 @@ public class TimestampUtils {
     return convertToTime(millis, tz); // Ensure date part is 1970-01-01
   }
 
+
+  //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
+  /**
+   * Returns the SQL Time object matching the given bytes with {@link Oid#TIME}.
+   *
+   * @param bytes The binary encoded time value.
+   * @return The parsed time object.
+   * @throws PSQLException If binary format could not be parsed.
+   */
+  public LocalTime toLocalTimeBin(byte[] bytes) throws PSQLException {
+    if (bytes.length != 8) {
+      throw new PSQLException(GT.tr("Unsupported binary encoding of {0}.", "time"),
+          PSQLState.BAD_DATETIME_FORMAT);
+    }
+
+    long micros;
+
+    if (usesDouble) {
+      double seconds = ByteConverter.float8(bytes, 0);
+
+      micros = (long) (seconds * 1000000d);
+    } else {
+      micros = ByteConverter.int8(bytes, 0);
+    }
+
+
+    return LocalTime.ofNanoOfDay(micros * 1000);
+  }
+  //#endif
+
   /**
    * Returns the SQL Timestamp object matching the given bytes with {@link Oid#TIMESTAMP} or
    * {@link Oid#TIMESTAMPTZ}.
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc2/optional/ConnectionPool.java b/pgjdbc/src/main/java/org/postgresql/jdbc2/optional/ConnectionPool.java
index 26adeb4b..b5c46fe4 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc2/optional/ConnectionPool.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc2/optional/ConnectionPool.java
@@ -7,5 +7,9 @@ package org.postgresql.jdbc2.optional;
 
 import org.postgresql.ds.PGConnectionPoolDataSource;
 
+/**
+ * @deprecated Please use {@link PGConnectionPoolDataSource}
+ */
+@Deprecated
 public class ConnectionPool extends PGConnectionPoolDataSource {
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc2/optional/PoolingDataSource.java b/pgjdbc/src/main/java/org/postgresql/jdbc2/optional/PoolingDataSource.java
index a6d04bd6..f47d373a 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc2/optional/PoolingDataSource.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc2/optional/PoolingDataSource.java
@@ -7,5 +7,9 @@ package org.postgresql.jdbc2.optional;
 
 import org.postgresql.ds.PGPoolingDataSource;
 
+/**
+ * @deprecated Since 42.0.0, see {@link PGPoolingDataSource}
+ */
+@Deprecated
 public class PoolingDataSource extends PGPoolingDataSource {
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc2/optional/SimpleDataSource.java b/pgjdbc/src/main/java/org/postgresql/jdbc2/optional/SimpleDataSource.java
index 650d0f4d..6ac00536 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc2/optional/SimpleDataSource.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc2/optional/SimpleDataSource.java
@@ -7,5 +7,9 @@ package org.postgresql.jdbc2.optional;
 
 import org.postgresql.ds.PGSimpleDataSource;
 
+/**
+ * @deprecated Please use {@link PGSimpleDataSource}
+ */
+@Deprecated
 public class SimpleDataSource extends PGSimpleDataSource {
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc3/Jdbc3ConnectionPool.java b/pgjdbc/src/main/java/org/postgresql/jdbc3/Jdbc3ConnectionPool.java
index f4c5cae1..950b0171 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc3/Jdbc3ConnectionPool.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc3/Jdbc3ConnectionPool.java
@@ -7,5 +7,9 @@ package org.postgresql.jdbc3;
 
 import org.postgresql.ds.PGConnectionPoolDataSource;
 
+/**
+ * @deprecated Please use {@link PGConnectionPoolDataSource}
+ */
+@Deprecated
 public class Jdbc3ConnectionPool extends PGConnectionPoolDataSource {
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc3/Jdbc3PoolingDataSource.java b/pgjdbc/src/main/java/org/postgresql/jdbc3/Jdbc3PoolingDataSource.java
index 5599a932..10b19204 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc3/Jdbc3PoolingDataSource.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc3/Jdbc3PoolingDataSource.java
@@ -7,5 +7,9 @@ package org.postgresql.jdbc3;
 
 import org.postgresql.ds.PGPoolingDataSource;
 
+/**
+ * @deprecated Since 42.0.0, see {@link PGPoolingDataSource}
+ */
+@Deprecated
 public class Jdbc3PoolingDataSource extends PGPoolingDataSource {
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc3/Jdbc3SimpleDataSource.java b/pgjdbc/src/main/java/org/postgresql/jdbc3/Jdbc3SimpleDataSource.java
index 7c959322..b04e28b3 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc3/Jdbc3SimpleDataSource.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc3/Jdbc3SimpleDataSource.java
@@ -7,5 +7,9 @@ package org.postgresql.jdbc3;
 
 import org.postgresql.ds.PGSimpleDataSource;
 
+/**
+ * @deprecated Please use {@link PGSimpleDataSource}
+ */
+@Deprecated
 public class Jdbc3SimpleDataSource extends PGSimpleDataSource {
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/util/PSQLState.java b/pgjdbc/src/main/java/org/postgresql/util/PSQLState.java
index 442db6c0..129af6ba 100644
--- a/pgjdbc/src/main/java/org/postgresql/util/PSQLState.java
+++ b/pgjdbc/src/main/java/org/postgresql/util/PSQLState.java
@@ -107,6 +107,7 @@ public class PSQLState implements java.io.Serializable {
   public final static PSQLState DATA_TYPE_MISMATCH = new PSQLState("42821");
   public final static PSQLState UNDEFINED_FUNCTION = new PSQLState("42883");
   public final static PSQLState INVALID_NAME = new PSQLState("42602");
+  public final static PSQLState CANNOT_COERCE = new PSQLState("42846");
 
   public final static PSQLState OUT_OF_MEMORY = new PSQLState("53200");
   public final static PSQLState OBJECT_NOT_IN_STATE = new PSQLState("55000");


Test patch:
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PreparedStatementTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PreparedStatementTest.java
index 37a5db17..614f4ae9 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PreparedStatementTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PreparedStatementTest.java
@@ -23,7 +23,6 @@ import org.junit.runners.Parameterized;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
-import java.io.File;
 import java.io.InputStream;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
@@ -61,6 +60,9 @@ public class PreparedStatementTest extends BaseTest4 {
     TestUtil.createTable(con, "texttable", "ch char(3), te text, vc varchar(3)");
     TestUtil.createTable(con, "intervaltable", "i interval");
     TestUtil.createTable(con, "inttable", "a int");
+    TestUtil.createTable(con, "bool_tab", "bool_val boolean, null_val boolean, tf_val boolean, "
+        + "truefalse_val boolean, yn_val boolean, yesno_val boolean, "
+        + "onoff_val boolean, onezero_val boolean");
   }
 
   @Override
@@ -69,6 +71,7 @@ public class PreparedStatementTest extends BaseTest4 {
     TestUtil.dropTable(con, "texttable");
     TestUtil.dropTable(con, "intervaltable");
     TestUtil.dropTable(con, "inttable");
+    TestUtil.dropTable(con, "bool_tab");
     super.tearDown();
   }
 
@@ -514,13 +517,16 @@ public class PreparedStatementTest extends BaseTest4 {
 
   @Test
   public void testBoolean() throws SQLException {
-    PreparedStatement pstmt = con.prepareStatement(
-        "CREATE TEMP TABLE bool_tab (bool_val boolean, null_val boolean, tf_val boolean, "
-        + "truefalse_val boolean, yn_val boolean, yesno_val boolean, onoff_val boolean, onezero_val boolean)");
-    pstmt.executeUpdate();
-    pstmt.close();
+    testBoolean(0);
+    testBoolean(1);
+    testBoolean(5);
+    testBoolean(-1);
+  }
+
+  public void testBoolean(int prepareThreshold) throws SQLException {
+    PreparedStatement pstmt = con.prepareStatement("insert into bool_tab values (?,?,?,?,?,?,?,?)");
+    ((org.postgresql.PGStatement) pstmt).setPrepareThreshold(prepareThreshold);
 
-    pstmt = con.prepareStatement("insert into bool_tab values (?,?,?,?,?,?,?,?)");
     // Test TRUE values
     pstmt.setBoolean(1, true);
     pstmt.setObject(1, Boolean.TRUE);
@@ -554,36 +560,20 @@ public class PreparedStatementTest extends BaseTest4 {
     pstmt.setObject(8, '0', Types.BOOLEAN);
     assertEquals("one row inserted, false values", 1, pstmt.executeUpdate());
     // Test weird values
-    pstmt.setObject(1, new java.util.Date(0), Types.BOOLEAN);
+    pstmt.setObject(1, (byte) 0, Types.BOOLEAN);
     pstmt.setObject(2, BigDecimal.ONE, Types.BOOLEAN);
     pstmt.setObject(3, 0L, Types.BOOLEAN);
     pstmt.setObject(4, 0x1, Types.BOOLEAN);
-    pstmt.setObject(5, 0, Types.BOOLEAN);
+    pstmt.setObject(5, new Float(0), Types.BOOLEAN);
+    pstmt.setObject(5, 1.0d, Types.BOOLEAN);
+    pstmt.setObject(5, 0.0f, Types.BOOLEAN);
     pstmt.setObject(6, Integer.valueOf("1"), Types.BOOLEAN);
+    pstmt.setObject(7, new java.math.BigInteger("0"), Types.BOOLEAN);
     pstmt.clearParameters();
-
-    try {
-      pstmt.setObject(1, "this is not boolean", Types.BOOLEAN);
-      fail();
-    } catch (SQLException e) {
-      assertEquals(e.getMessage(), "Cannot convert an instance of String to type boolean");
-    }
-    try {
-      pstmt.setObject(1, 'X', Types.BOOLEAN);
-      fail();
-    } catch (SQLException e) {
-      assertEquals(e.getMessage(), "Cannot convert an instance of Character to type boolean");
-    }
-    try {
-      File obj = new File("");
-      pstmt.setObject(1, obj, Types.BOOLEAN);
-      fail();
-    } catch (SQLException e) {
-      assertEquals(e.getMessage(), "Cannot convert an instance of java.io.File to type boolean");
-    }
-
     pstmt.close();
+
     pstmt = con.prepareStatement("select * from bool_tab");
+    ((org.postgresql.PGStatement) pstmt).setPrepareThreshold(prepareThreshold);
     ResultSet rs = pstmt.executeQuery();
 
     assertTrue(rs.next());
@@ -610,6 +600,83 @@ public class PreparedStatementTest extends BaseTest4 {
 
     rs.close();
     pstmt.close();
+
+    pstmt = con.prepareStatement("TRUNCATE TABLE bool_tab");
+    pstmt.executeUpdate();
+    pstmt.close();
+  }
+
+  @Test
+  public void testBadBoolean() throws SQLException {
+    PreparedStatement pstmt = con.prepareStatement("INSERT INTO bad_bool VALUES (?)");
+    try {
+      pstmt.setObject(1, "this is not boolean", Types.BOOLEAN);
+      fail();
+    } catch (SQLException e) {
+      assertEquals(e.getSQLState(), org.postgresql.util.PSQLState.CANNOT_COERCE.getState());
+    }
+    try {
+      pstmt.setObject(1, 'X', Types.BOOLEAN);
+      fail();
+    } catch (SQLException e) {
+      assertEquals(e.getSQLState(), org.postgresql.util.PSQLState.CANNOT_COERCE.getState());
+    }
+    try {
+      java.io.File obj = new java.io.File("");
+      pstmt.setObject(1, obj, Types.BOOLEAN);
+      fail();
+    } catch (SQLException e) {
+      assertEquals(e.getSQLState(), org.postgresql.util.PSQLState.CANNOT_COERCE.getState());
+    }
+    try {
+      pstmt.setObject(1, "1.0", Types.BOOLEAN);
+      fail();
+    } catch (SQLException e) {
+      assertEquals(e.getSQLState(), org.postgresql.util.PSQLState.CANNOT_COERCE.getState());
+    }
+    try {
+      pstmt.setObject(1, "-1", Types.BOOLEAN);
+      fail();
+    } catch (SQLException e) {
+      assertEquals(e.getSQLState(), org.postgresql.util.PSQLState.CANNOT_COERCE.getState());
+    }
+    try {
+      pstmt.setObject(1, "ok", Types.BOOLEAN);
+      fail();
+    } catch (SQLException e) {
+      assertEquals(e.getSQLState(), org.postgresql.util.PSQLState.CANNOT_COERCE.getState());
+    }
+    try {
+      pstmt.setObject(1, 0.99f, Types.BOOLEAN);
+      fail();
+    } catch (SQLException e) {
+      assertEquals(e.getSQLState(), org.postgresql.util.PSQLState.CANNOT_COERCE.getState());
+    }
+    try {
+      pstmt.setObject(1, -0.01d, Types.BOOLEAN);
+      fail();
+    } catch (SQLException e) {
+      assertEquals(e.getSQLState(), org.postgresql.util.PSQLState.CANNOT_COERCE.getState());
+    }
+    try {
+      pstmt.setObject(1, new java.sql.Date(0), Types.BOOLEAN);
+      fail();
+    } catch (SQLException e) {
+      assertEquals(e.getSQLState(), org.postgresql.util.PSQLState.CANNOT_COERCE.getState());
+    }
+    try {
+      pstmt.setObject(1, new java.math.BigInteger("1000"), Types.BOOLEAN);
+      fail();
+    } catch (SQLException e) {
+      assertEquals(e.getSQLState(), org.postgresql.util.PSQLState.CANNOT_COERCE.getState());
+    }
+    try {
+      pstmt.setObject(1, Math.PI, Types.BOOLEAN);
+      fail();
+    } catch (SQLException e) {
+      assertEquals(e.getSQLState(), org.postgresql.util.PSQLState.CANNOT_COERCE.getState());
+    }
+    pstmt.close();
   }
 
   @Test
@@ -664,18 +731,27 @@ public class PreparedStatementTest extends BaseTest4 {
     pstmt.setObject(2, minStringFloat, Types.FLOAT);
     pstmt.setNull(3, Types.FLOAT);
     pstmt.executeUpdate();
+    pstmt.setObject(1, "1.0", Types.FLOAT);
+    pstmt.setObject(2, "0.0", Types.FLOAT);
+    pstmt.setNull(3, Types.FLOAT);
+    pstmt.executeUpdate();
     pstmt.close();
 
     pstmt = con.prepareStatement("select * from float_tab");
     ResultSet rs = pstmt.executeQuery();
     assertTrue(rs.next());
 
-    assertTrue("expected true,received " + rs.getObject(1),
-        ((Double) rs.getObject(1)).equals(maxFloat));
-    assertTrue("expected false,received " + rs.getBoolean(2),
-        ((Double) rs.getObject(2)).equals(minFloat));
+    assertTrue(((Double) rs.getObject(1)).equals(maxFloat));
+    assertTrue(((Double) rs.getObject(2)).equals(minFloat));
+    assertTrue(rs.getDouble(1) == maxFloat);
+    assertTrue(rs.getDouble(2) == minFloat);
     rs.getFloat(3);
     assertTrue(rs.wasNull());
+
+    assertTrue(rs.next());
+    assertTrue("expected true, received " + rs.getBoolean(1), rs.getBoolean(1) == true);
+    assertTrue("expected false,received " + rs.getBoolean(2), rs.getBoolean(2) == false);
+
     rs.close();
     pstmt.close();
 
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/ResultSetTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/ResultSetTest.java
index 64113096..0675491c 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/ResultSetTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/ResultSetTest.java
@@ -50,21 +50,47 @@ public class ResultSetTest extends BaseTest4 {
     TestUtil.createTable(con, "testint", "a int");
     stmt.executeUpdate("INSERT INTO testint VALUES (12345)");
 
-    TestUtil.createTable(con, "testbool", "a boolean");
+    // Boolean Tests
+    TestUtil.createTable(con, "testboolstring", "a varchar(30), b boolean");
+    stmt.executeUpdate("INSERT INTO testboolstring VALUES('1 ', true)");
+    stmt.executeUpdate("INSERT INTO testboolstring VALUES('0', false)");
+    stmt.executeUpdate("INSERT INTO testboolstring VALUES(' t', true)");
+    stmt.executeUpdate("INSERT INTO testboolstring VALUES('f', false)");
+    stmt.executeUpdate("INSERT INTO testboolstring VALUES('True', true)");
+    stmt.executeUpdate("INSERT INTO testboolstring VALUES('      False   ', false)");
+    stmt.executeUpdate("INSERT INTO testboolstring VALUES('yes', true)");
+    stmt.executeUpdate("INSERT INTO testboolstring VALUES('  no  ', false)");
+    stmt.executeUpdate("INSERT INTO testboolstring VALUES('y', true)");
+    stmt.executeUpdate("INSERT INTO testboolstring VALUES('n', false)");
+    stmt.executeUpdate("INSERT INTO testboolstring VALUES('oN', true)");
+    stmt.executeUpdate("INSERT INTO testboolstring VALUES('oFf', false)");
+    stmt.executeUpdate("INSERT INTO testboolstring VALUES('OK', null)");
+    stmt.executeUpdate("INSERT INTO testboolstring VALUES('NOT', null)");
+    stmt.executeUpdate("INSERT INTO testboolstring VALUES('not a boolean', null)");
+    stmt.executeUpdate("INSERT INTO testboolstring VALUES('1.0', null)");
+    stmt.executeUpdate("INSERT INTO testboolstring VALUES('0.0', null)");
+
+    TestUtil.createTable(con, "testboolfloat", "a float4, b boolean");
+    stmt.executeUpdate("INSERT INTO testboolfloat VALUES('1.0'::real, true)");
+    stmt.executeUpdate("INSERT INTO testboolfloat VALUES('0.0'::real, false)");
+    stmt.executeUpdate("INSERT INTO testboolfloat VALUES(1.000::real, true)");
+    stmt.executeUpdate("INSERT INTO testboolfloat VALUES(0.000::real, false)");
+    stmt.executeUpdate("INSERT INTO testboolfloat VALUES('1.001'::real, null)");
+    stmt.executeUpdate("INSERT INTO testboolfloat VALUES('-1.001'::real, null)");
+    stmt.executeUpdate("INSERT INTO testboolfloat VALUES(123.4::real, null)");
+    stmt.executeUpdate("INSERT INTO testboolfloat VALUES(1.234e2::real, null)");
+    stmt.executeUpdate("INSERT INTO testboolfloat VALUES(100.00e-2::real, true)");
+
+    TestUtil.createTable(con, "testboolint", "a bigint, b boolean");
+    stmt.executeUpdate("INSERT INTO testboolint VALUES(1, true)");
+    stmt.executeUpdate("INSERT INTO testboolint VALUES(0, false)");
+    stmt.executeUpdate("INSERT INTO testboolint VALUES(-1, null)");
+    stmt.executeUpdate("INSERT INTO testboolint VALUES(9223372036854775807, null)");
+    stmt.executeUpdate("INSERT INTO testboolint VALUES(-9223372036854775808, null)");
+    // End Boolean Tests
 
     // TestUtil.createTable(con, "testbit", "a bit");
 
-    TestUtil.createTable(con, "testboolstring", "a varchar(30)");
-
-    stmt.executeUpdate("INSERT INTO testboolstring VALUES('true')");
-    stmt.executeUpdate("INSERT INTO testboolstring VALUES('false')");
-    stmt.executeUpdate("INSERT INTO testboolstring VALUES('t')");
-    stmt.executeUpdate("INSERT INTO testboolstring VALUES('f')");
-    stmt.executeUpdate("INSERT INTO testboolstring VALUES('1.0')");
-    stmt.executeUpdate("INSERT INTO testboolstring VALUES('0.0')");
-    stmt.executeUpdate("INSERT INTO testboolstring VALUES('TRUE')");
-    stmt.executeUpdate("INSERT INTO testboolstring VALUES('this is not true')");
-
     TestUtil.createTable(con, "testnumeric", "a numeric");
     stmt.executeUpdate("INSERT INTO testnumeric VALUES('1.0')");
     stmt.executeUpdate("INSERT INTO testnumeric VALUES('0.0')");
@@ -105,9 +131,10 @@ public class ResultSetTest extends BaseTest4 {
     TestUtil.dropTable(con, "testrs");
     TestUtil.dropTable(con, "teststring");
     TestUtil.dropTable(con, "testint");
-    TestUtil.dropTable(con, "testbool");
     // TestUtil.dropTable(con, "testbit");
     TestUtil.dropTable(con, "testboolstring");
+    TestUtil.dropTable(con, "testboolfloat");
+    TestUtil.dropTable(con, "testboolint");
     TestUtil.dropTable(con, "testnumeric");
     TestUtil.dropTable(con, "testpgobject");
     super.tearDown();
@@ -186,63 +213,52 @@ public class ResultSetTest extends BaseTest4 {
     assertEquals("12", new String(rs.getBytes(1)));
   }
 
-  public void booleanTests(boolean useServerPrepare) throws SQLException {
-    java.sql.PreparedStatement pstmt = con.prepareStatement("insert into testbool values (?)");
-    if (useServerPrepare) {
-      ((org.postgresql.PGStatement) pstmt).setUseServerPrepare(true);
-    }
-
-    pstmt.setObject(1, new Float(0), java.sql.Types.BIT);
-    pstmt.executeUpdate();
-
-    pstmt.setObject(1, new Float(1), java.sql.Types.BIT);
-    pstmt.executeUpdate();
-
-    pstmt.setObject(1, "False", java.sql.Types.BIT);
-    pstmt.executeUpdate();
-
-    pstmt.setObject(1, "True", java.sql.Types.BIT);
-    pstmt.executeUpdate();
-
-    ResultSet rs = con.createStatement().executeQuery("select * from testbool");
-    for (int i = 0; i < 2; i++) {
-      assertTrue(rs.next());
-      assertEquals(false, rs.getBoolean(1));
-      assertTrue(rs.next());
-      assertEquals(true, rs.getBoolean(1));
-    }
-
-    /*
-     * pstmt = con.prepareStatement("insert into testbit values (?)");
-     *
-     * pstmt.setObject(1, new Float(0), java.sql.Types.BIT); pstmt.executeUpdate();
-     *
-     * pstmt.setObject(1, new Float(1), java.sql.Types.BIT); pstmt.executeUpdate();
-     *
-     * pstmt.setObject(1, "false", java.sql.Types.BIT); pstmt.executeUpdate();
-     *
-     * pstmt.setObject(1, "true", java.sql.Types.BIT); pstmt.executeUpdate();
-     *
-     * rs = con.createStatement().executeQuery("select * from testbit");
-     *
-     * for (int i = 0;i<2; i++) { assertTrue(rs.next()); assertEquals(false, rs.getBoolean(1));
-     * assertTrue(rs.next()); assertEquals(true, rs.getBoolean(1)); }
-     */
-
-    rs = con.createStatement().executeQuery("select * from testboolstring");
-
-    for (int i = 0; i < 4; i++) {
-      assertTrue(rs.next());
-      assertEquals(true, rs.getBoolean(1));
-      assertTrue(rs.next());
-      assertEquals(false, rs.getBoolean(1));
-    }
+  @Test
+  public void testBooleanString() throws SQLException {
+    testBoolean("testboolstring", 0);
+    testBoolean("testboolstring", 1);
+    testBoolean("testboolstring", 5);
+    testBoolean("testboolstring", -1);
   }
 
   @Test
-  public void testBoolean() throws SQLException {
-    booleanTests(true);
-    booleanTests(false);
+  public void testBooleanFloat() throws SQLException {
+    testBoolean("testboolfloat", 0);
+    testBoolean("testboolfloat", 1);
+    testBoolean("testboolfloat", 5);
+    testBoolean("testboolfloat", -1);
+  }
+
+  @Test
+  public void testBooleanInt() throws SQLException {
+    testBoolean("testboolint", 0);
+    testBoolean("testboolint", 1);
+    testBoolean("testboolint", 5);
+    testBoolean("testboolint", -1);
+  }
+
+  public void testBoolean(String table, int prepareThreshold) throws SQLException {
+    PreparedStatement pstmt = con.prepareStatement("select a, b from " + table);
+    ((org.postgresql.PGStatement) pstmt).setPrepareThreshold(prepareThreshold);
+    ResultSet rs = pstmt.executeQuery();
+    while (rs.next()) {
+      rs.getBoolean(2);
+      Boolean expected = rs.wasNull() ? null : rs.getBoolean(2); // Hack to get SQL NULL
+      if (expected != null) {
+        assertEquals(expected, rs.getBoolean(1));
+      } else {
+        // expected value with null are bad values
+        try {
+          rs.getBoolean(1);
+          fail();
+        } catch (SQLException e) {
+          assertEquals(e.getSQLState(), org.postgresql.util.PSQLState.CANNOT_COERCE.getState());
+        }
+      }
+
+    }
+    rs.close();
+    pstmt.close();
   }
 
   @Test
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc42/GetObject310Test.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc42/GetObject310Test.java
index 8bf21a2f..36cd3aa7 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc42/GetObject310Test.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc42/GetObject310Test.java
@@ -7,6 +7,8 @@ package org.postgresql.test.jdbc42;
 
 import org.postgresql.test.TestUtil;
 import org.postgresql.test.jdbc2.BaseTest;
+import org.postgresql.util.PSQLException;
+import org.postgresql.util.PSQLState;
 
 import java.sql.ResultSet;
 import java.sql.SQLException;
@@ -74,12 +76,12 @@ public class GetObject310Test extends BaseTest {
    */
   public void testGetLocalTime() throws SQLException {
     Statement stmt = con.createStatement();
-    stmt.executeUpdate(TestUtil.insertSQL("table1","time_without_time_zone_column","TIME '04:05:06'"));
+    stmt.executeUpdate(TestUtil.insertSQL("table1","time_without_time_zone_column","TIME '04:05:06.123456'"));
 
     ResultSet rs = stmt.executeQuery(TestUtil.selectSQL("table1", "time_without_time_zone_column"));
     try {
       assertTrue(rs.next());
-      LocalTime localTime = LocalTime.of(4, 5, 6);
+      LocalTime localTime = LocalTime.of(4, 5, 6, 123456000);
       assertEquals(localTime, rs.getObject("time_without_time_zone_column", LocalTime.class));
       assertEquals(localTime, rs.getObject(1, LocalTime.class));
     } finally {
@@ -87,6 +89,50 @@ public class GetObject310Test extends BaseTest {
     }
   }
 
+  /**
+   * Test the behavior getObject for time columns with null.
+   */
+  public void testGetLocalTimeNull() throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.executeUpdate(TestUtil.insertSQL("table1","time_without_time_zone_column","NULL"));
+
+    ResultSet rs = stmt.executeQuery(TestUtil.selectSQL("table1", "time_without_time_zone_column"));
+    try {
+      assertTrue(rs.next());
+      assertNull(rs.getObject("time_without_time_zone_column", LocalTime.class));
+      assertNull(rs.getObject(1, LocalTime.class));
+    } finally {
+      rs.close();
+    }
+  }
+
+  /**
+   * Test the behavior getObject for time columns with null.
+   */
+  public void testGetLocalTimeInvalidType() throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.executeUpdate(TestUtil.insertSQL("table1","time_with_time_zone_column", "TIME '04:05:06.123456-08:00'"));
+
+    ResultSet rs = stmt.executeQuery(TestUtil.selectSQL("table1", "time_with_time_zone_column"));
+    try {
+      assertTrue(rs.next());
+      try {
+        assertNull(rs.getObject("time_with_time_zone_column", LocalTime.class));
+      } catch (PSQLException e) {
+        assertTrue(e.getSQLState().equals(PSQLState.DATA_TYPE_MISMATCH.getState())
+                || e.getSQLState().equals(PSQLState.BAD_DATETIME_FORMAT.getState()));
+      }
+      try {
+        assertNull(rs.getObject(1, LocalTime.class));
+      } catch (PSQLException e) {
+        assertTrue(e.getSQLState().equals(PSQLState.DATA_TYPE_MISMATCH.getState())
+                || e.getSQLState().equals(PSQLState.BAD_DATETIME_FORMAT.getState()));
+      }
+    } finally {
+      rs.close();
+    }
+  }
+
   /**
    * Test the behavior getObject for timestamp columns.
    */
@@ -106,17 +152,17 @@ public class GetObject310Test extends BaseTest {
             "1997-06-30T23:59:59", "1997-07-01T00:00:00", "2012-06-30T23:59:59", "2012-07-01T00:00:00",
             "2015-06-30T23:59:59", "2015-07-01T00:00:00", "2005-12-31T23:59:59", "2006-01-01T00:00:00",
             "2008-12-31T23:59:59", "2009-01-01T00:00:00", /* "2015-06-30T23:59:60", */ "2015-07-31T00:00:00",
-            "2015-07-31T00:00:01",
+            "2015-07-31T00:00:01", "2015-07-31T00:00:00.000001",
 
             // On 2000-03-26 02:00:00 Moscow went to DST, thus local time became 03:00:00
             "2000-03-26T01:59:59", "2000-03-26T02:00:00", "2000-03-26T02:00:01", "2000-03-26T02:59:59",
             "2000-03-26T03:00:00", "2000-03-26T03:00:01", "2000-03-26T03:59:59", "2000-03-26T04:00:00",
-            "2000-03-26T04:00:01",
+            "2000-03-26T04:00:01", "2000-03-26T04:00:00.000001",
 
             // On 2000-10-29 03:00:00 Moscow went to regular time, thus local time became 02:00:00
             "2000-10-29T01:59:59", "2000-10-29T02:00:00", "2000-10-29T02:00:01", "2000-10-29T02:59:59",
             "2000-10-29T03:00:00", "2000-10-29T03:00:01", "2000-10-29T03:59:59", "2000-10-29T04:00:00",
-            "2000-10-29T04:00:01");
+            "2000-10-29T04:00:01", "2000-10-29T04:00:00.000001");
 
     for (String zoneId : zoneIdsToTest) {
       ZoneId zone = ZoneId.of(zoneId);
@@ -160,12 +206,12 @@ public class GetObject310Test extends BaseTest {
   private void runGetOffsetDateTime(ZoneOffset offset) throws SQLException {
     Statement stmt = con.createStatement();
     try {
-      stmt.executeUpdate(TestUtil.insertSQL("table1","timestamp_with_time_zone_column","TIMESTAMP WITH TIME ZONE '2004-10-19 10:23:54" + offset.toString() + "'"));
+      stmt.executeUpdate(TestUtil.insertSQL("table1","timestamp_with_time_zone_column","TIMESTAMP WITH TIME ZONE '2004-10-19 10:23:54.123456" + offset.toString() + "'"));
 
       ResultSet rs = stmt.executeQuery(TestUtil.selectSQL("table1", "timestamp_with_time_zone_column"));
       try {
         assertTrue(rs.next());
-        LocalDateTime localDateTime = LocalDateTime.of(2004, 10, 19, 10, 23, 54);
+        LocalDateTime localDateTime = LocalDateTime.of(2004, 10, 19, 10, 23, 54, 123456000);
 
         OffsetDateTime offsetDateTime = localDateTime.atOffset(offset).withOffsetSameInstant(ZoneOffset.UTC);
         assertEquals(offsetDateTime, rs.getObject("timestamp_with_time_zone_column", OffsetDateTime.class));
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc42/SetObject310Test.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc42/SetObject310Test.java
index cf31125e..3bcf6f24 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc42/SetObject310Test.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc42/SetObject310Test.java
@@ -98,7 +98,7 @@ public class SetObject310Test {
     return readString(columnName);
   }
 
-  private void insertWithoutType(OffsetDateTime data, String columnName) throws SQLException {
+  private void insertWithoutType(Object data, String columnName) throws SQLException {
     insert(data, columnName, null);
   }
 
@@ -210,17 +210,17 @@ public class SetObject310Test {
             "1997-06-30T23:59:59", "1997-07-01T00:00:00", "2012-06-30T23:59:59", "2012-07-01T00:00:00",
             "2015-06-30T23:59:59", "2015-07-01T00:00:00", "2005-12-31T23:59:59", "2006-01-01T00:00:00",
             "2008-12-31T23:59:59", "2009-01-01T00:00:00", /* "2015-06-30T23:59:60", */ "2015-07-31T00:00:00",
-            "2015-07-31T00:00:01",
+            "2015-07-31T00:00:01", "2015-07-31T00:00:00.000001",
 
             // On 2000-03-26 02:00:00 Moscow went to DST, thus local time became 03:00:00
             "2000-03-26T01:59:59", "2000-03-26T02:00:00", "2000-03-26T02:00:01", "2000-03-26T02:59:59",
             "2000-03-26T03:00:00", "2000-03-26T03:00:01", "2000-03-26T03:59:59", "2000-03-26T04:00:00",
-            "2000-03-26T04:00:01",
+            "2000-03-26T04:00:01", "2000-03-26T04:00:00.000001",
 
             // On 2000-10-29 03:00:00 Moscow went to regular time, thus local time became 02:00:00
             "2000-10-29T01:59:59", "2000-10-29T02:00:00", "2000-10-29T02:00:01", "2000-10-29T02:59:59",
             "2000-10-29T03:00:00", "2000-10-29T03:00:01", "2000-10-29T03:59:59", "2000-10-29T04:00:00",
-            "2000-10-29T04:00:01");
+            "2000-10-29T04:00:01", "2000-10-29T04:00:00.000001");
   }
 
   private List<String> getZoneIdsToTest() {
@@ -281,11 +281,11 @@ public class SetObject310Test {
   public void testSetLocalDateTimeBc() throws SQLException {
     // use BC for funsies
     List<LocalDateTime> bcDates = new ArrayList<LocalDateTime>();
-    bcDates.add(LocalDateTime.parse("1997-06-30T23:59:59").with(ChronoField.ERA, IsoEra.BCE.getValue()));
-    bcDates.add(LocalDateTime.parse("0997-06-30T23:59:59").with(ChronoField.ERA, IsoEra.BCE.getValue()));
+    bcDates.add(LocalDateTime.parse("1997-06-30T23:59:59.999999").with(ChronoField.ERA, IsoEra.BCE.getValue()));
+    bcDates.add(LocalDateTime.parse("0997-06-30T23:59:59.999999").with(ChronoField.ERA, IsoEra.BCE.getValue()));
 
     for (LocalDateTime bcDate : bcDates) {
-      // -1997-06-30T23:59:59 -> 1997-06-30 23:59:59 BC
+      // -1997-06-30T23:59:59.999999 -> 1997-06-30 23:59:59.999999 BC
       String expected = bcDate.toString().substring(1).replace('T', ' ') + " BC";
       localTimestamps(ZoneOffset.UTC, bcDate, expected);
     }
@@ -313,6 +313,19 @@ public class SetObject310Test {
     assertEquals(expected, actual);
   }
 
+  /**
+   * Test the behavior setObject for time columns.
+   */
+  @Test
+  public void testSetLocalTimeAndReadBack() throws SQLException {
+    LocalTime data = LocalTime.parse("16:21:51.123456");
+
+    insertWithoutType(data, "time_without_time_zone_column");
+
+    String readBack = readString("time_without_time_zone_column");
+    assertEquals("16:21:51.123456", readBack);
+  }
+
   /**
    * Test the behavior setObject for time columns.
    */