Repository: pgjdbc/pgjdbc
Pull request #1657 (2019-12-20 11:53:32)

feat: Add maxResultBuffer property
Implementation of new property - maxResultBuffer. Enables max bytes read during reading result set.
Enable declare in two styles:
- as size of bytes (i.e. 100, 150M, 300K, 400G);
- as percent of max heap memory (i.e. 10p, 15pct, 20percent).
Also validates if defined size isn't bigger than enabled. Max possible size is 90% of max heap memory.
If maxResultBuffer property isn't declared, work of driver is not changed.

Commit made for Heimdalldata's request to solve memory problem during reading result set.

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing](https://github.com/pgjdbc/pgjdbc/blob/master/CONTRIBUTING.md) document?
* [ ] Have you checked to ensure there aren't other open [Pull Requests](../../pulls) for the same update/change? (Cause I don't think there is pull request for this change.)

<!-- You can erase any parts of this template not applicable to your Pull Request. -->

### New Feature Submissions:

1. [x] Does your submission pass tests?
2. [x] Does mvn checkstyle:check pass?

Tests added for created PGPropertyMaxResultBufferParser. If necessary, tests for using made property with database can be made, but I would be thankful for information, where that tests could be implemented (cause I still don't know exact structure of this project). And I would be glad for any comments about possibilities to improve my code.

davecramer: @adrklos Thanks for this. couple things:

The docs need to be updated as well. This isn't well documented that this even exists but it is here
https://github.com/pgjdbc/pgjdbc/blob/master/docs/documentation/head/connect.md 

Secondly; I'm wondering how difficult it would be to actually test failure with a response that is too large ? 
adrklos: @davecramer If by "too large" you mean larger than set maxResultBuffer, then it should be easy i.e. let's set maxResultBuffer to 300 so it would be 300 bytes and make a table in which we put 30 records with string of length 20. If reading of this table would be at once, driver will have to read like 600 bytes so during reading should return an exception that size of read bytes exceeded maxResultBuffer. On other side if result set gonna be read with fetch size like 10 it should work without problems.

And thanks for link to docs, I will update it.


Patch:
diff --git a/README.md b/README.md
index f2b9a159..c0f0626a 100644
--- a/README.md
+++ b/README.md
@@ -152,6 +152,7 @@ In addition to the standard connection parameters the driver supports a number o
 | preferQueryMode               | String  | extended | Specifies which mode is used to execute queries to database, possible values: extended, extendedForPrepared, extendedCacheEverything, simple |
 | reWriteBatchedInserts         | Boolean | false  | Enable optimization to rewrite and collapse compatible INSERT statements that are batched. |
 | escapeSyntaxCallMode          | String  | select   | Specifies how JDBC escape call syntax is transformed into underlying SQL (CALL/SELECT), for invoking procedures or functions (requires server version >= 11), possible values: select, callIfNoReturn, call |
+| maxResultBuffer               | String  | null    | Specifies size of result buffer in bytes, which can't be exceeded during reading result set. Can be specified as particular size (i.e. "100", "200M" "2G") or as percent of max heap memory (i.e. "10p", "20pct", "50percent") |
 
 ## Contributing
 For information on how to contribute to the project see the [Contributing Guidelines](CONTRIBUTING.md)
diff --git a/docs/documentation/head/connect.md b/docs/documentation/head/connect.md
index ec432538..aba51f2b 100644
--- a/docs/documentation/head/connect.md
+++ b/docs/documentation/head/connect.md
@@ -482,7 +482,17 @@ Connection conn = DriverManager.getConnection(url);
 
 	The default is `select` 
 
+* **maxResultBuffer** = String
+
+    Specifies size of result buffer in bytes, which can't be exceeded during reading result set. 
+    Property can be specified in two styles:
+    - as size of bytes (i.e. 100, 150M, 300K, 400G, 1T);
+    - as percent of max heap memory (i.e. 10p, 15pct, 20percent);
+    
+    A limit during setting of property is 90% of max heap memory. All given values, which gonna be higher than limit, gonna lowered to the limit.
     
+	By default, maxResultBuffer is not set (is null), what means that reading of results gonna be performed without limits.
+	
 <a name="unix sockets"></a>
 ## Unix sockets
 
diff --git a/pgjdbc/pom.xml b/pgjdbc/pom.xml
index c212d3fa..f6f2c9ff 100644
--- a/pgjdbc/pom.xml
+++ b/pgjdbc/pom.xml
@@ -33,7 +33,7 @@
     <jdbc.specification.version>4.2</jdbc.specification.version>
     <jdbc.specification.version.nodot>42</jdbc.specification.version.nodot>
     <skip.assembly>false</skip.assembly>
-    <checkstyle.version>8.27</checkstyle.version>
+    <checkstyle.version>8.28</checkstyle.version>
   </properties>
 
   <dependencies>
diff --git a/pgjdbc/src/main/java/org/postgresql/PGProperty.java b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
index be805c47..3088d8a5 100644
--- a/pgjdbc/src/main/java/org/postgresql/PGProperty.java
+++ b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
@@ -476,7 +476,14 @@ public enum PGProperty {
           + "When 'transaction' setting readOnly to 'true' will cause transactions to BEGIN READ ONLY if autocommit is 'false'. "
           + "When 'always' setting readOnly to 'true' will set the session to READ ONLY if autoCommit is 'true' "
           + "and the transaction to BEGIN READ ONLY if autocommit is 'false'.",
-      false, "ignore", "transaction", "always");
+      false, "ignore", "transaction", "always"),
+
+  /**
+   * Specifies size of buffer during fetching result set. Can be specified as specified size or
+   * percent of heap memory.
+   */
+  MAX_RESULT_BUFFER("maxResultBuffer", null,
+    "Specifies size of buffer during fetching result set. Can be specified as specified size or percent of heap memory.");
 
   private final String name;
   private final String defaultValue;
diff --git a/pgjdbc/src/main/java/org/postgresql/core/FixedLengthOutputStream.java b/pgjdbc/src/main/java/org/postgresql/core/FixedLengthOutputStream.java
new file mode 100644
index 00000000..a2f4a254
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/core/FixedLengthOutputStream.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2020, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.core;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * A stream that refuses to write more than a maximum number of bytes.
+ */
+public class FixedLengthOutputStream extends OutputStream {
+
+  private final int size;
+  private final OutputStream target;
+  private int written;
+
+  public FixedLengthOutputStream(int size, OutputStream target) {
+    this.size = size;
+    this.target = target;
+  }
+
+  @Override
+  public void write(int b) throws IOException {
+    verifyAllowed(1);
+    written++;
+    target.write(b);
+  }
+
+  public void write(byte[] buf, int offset, int len) throws IOException {
+    if ((offset < 0) || (len < 0) || ((offset + len) > buf.length)) {
+      throw new IndexOutOfBoundsException();
+    } else if (len == 0) {
+      return;
+    }
+    verifyAllowed(len);
+    target.write(buf, offset, len);
+    written += len;
+  }
+
+  public int remaining() {
+    return size - written;
+  }
+
+  private void verifyAllowed(int wanted) throws IOException {
+    if (remaining() < wanted) {
+      throw new IOException("Attempt to write more than the specified " + size + " bytes");
+    }
+  }
+}
diff --git a/pgjdbc/src/main/java/org/postgresql/core/PGStream.java b/pgjdbc/src/main/java/org/postgresql/core/PGStream.java
index af7f227a..984c58e3 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/PGStream.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/PGStream.java
@@ -5,8 +5,10 @@
 
 package org.postgresql.core;
 
+import org.postgresql.util.ByteStreamWriter;
 import org.postgresql.util.GT;
 import org.postgresql.util.HostSpec;
+import org.postgresql.util.PGPropertyMaxResultBufferParser;
 import org.postgresql.util.PSQLException;
 import org.postgresql.util.PSQLState;
 
@@ -53,6 +55,9 @@ public class PGStream implements Closeable, Flushable {
   private Encoding encoding;
   private Writer encodingWriter;
 
+  private long maxResultBuffer = -1;
+  private long resultBufferByteCount = 0;
+
   /**
    * Constructor: Connect to the PostgreSQL back end and return a stream connection.
    *
@@ -311,6 +316,32 @@ public class PGStream implements Closeable, Flushable {
     }
   }
 
+  /**
+   * Send a fixed-size array of bytes to the backend. If {@code length < siz}, pad with zeros. If
+   * {@code length > siz}, truncate the array.
+   *
+   * @param writer the stream writer to invoke to send the bytes
+   * @throws IOException if an I/O error occurs
+   */
+  public void send(ByteStreamWriter writer) throws IOException {
+    final FixedLengthOutputStream fixedLengthStream = new FixedLengthOutputStream(writer.getLength(), pgOutput);
+    try {
+      writer.writeTo(new ByteStreamWriter.ByteStreamTarget() {
+        @Override
+        public OutputStream getOutputStream() {
+          return fixedLengthStream;
+        }
+      });
+    } catch (IOException ioe) {
+      throw ioe;
+    } catch (Exception re) {
+      throw new IOException("Error writing bytes to stream", re);
+    }
+    for (int i = fixedLengthStream.remaining(); i > 0; i--) {
+      pgOutput.write(0);
+    }
+  }
+
   /**
    * Receives a single character from the backend, without advancing the current protocol stream
    * position.
@@ -436,12 +467,16 @@ public class PGStream implements Closeable, Flushable {
    *
    * @return tuple from the back end
    * @throws IOException if a data I/O error occurs
+   * @throws SQLException if read more bytes than set maxResultBuffer
    */
-  public byte[][] receiveTupleV3() throws IOException, OutOfMemoryError {
-    receiveInteger4(); // MESSAGE SIZE
+  public byte[][] receiveTupleV3() throws IOException, OutOfMemoryError, SQLException {
+    int messageSize = receiveInteger4(); // MESSAGE SIZE
     int nf = receiveInteger2();
+    //size = messageSize - 4 bytes of message size - 2 bytes of field count - 4 bytes for each column length
+    int dataToReadSize = messageSize - 4 - 2 - 4 * nf;
     byte[][] answer = new byte[nf][];
 
+    increaseByteCounter(dataToReadSize);
     OutOfMemoryError oom = null;
     for (int i = 0; i < nf; ++i) {
       int size = receiveInteger4();
@@ -592,4 +627,41 @@ public class PGStream implements Closeable, Flushable {
   public int getNetworkTimeout() throws IOException {
     return connection.getSoTimeout();
   }
+
+  /**
+   * Method to set MaxResultBuffer inside PGStream.
+   *
+   * @param value value of new max result buffer as string (cause we can expect % or chars to use
+   *              multiplier)
+   * @throws PSQLException exception returned when occurred parsing problem.
+   */
+  public void setMaxResultBuffer(String value) throws PSQLException {
+    maxResultBuffer = PGPropertyMaxResultBufferParser.parseProperty(value);
+  }
+
+  /**
+   * Method to clear count of byte buffer.
+   */
+  public void clearResultBufferCount() {
+    resultBufferByteCount = 0;
+  }
+
+  /**
+   * Method to increase actual count of buffer. If buffer count is bigger than max result buffer
+   * limit, then gonna return an exception.
+   *
+   * @param value size of bytes to add to byte buffer.
+   * @throws SQLException exception returned when result buffer count is bigger than max result
+   *                          buffer.
+   */
+  private void increaseByteCounter(long value) throws SQLException {
+    if (maxResultBuffer != -1) {
+      resultBufferByteCount += value;
+      if (resultBufferByteCount > maxResultBuffer) {
+        throw new PSQLException(GT.tr(
+          "Result set exceeded maxResultBuffer limit. Received:  {0}; Current limit: {1}",
+          String.valueOf(resultBufferByteCount), String.valueOf(maxResultBuffer)),PSQLState.COMMUNICATION_ERROR);
+      }
+    }
+  }
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/core/ParameterList.java b/pgjdbc/src/main/java/org/postgresql/core/ParameterList.java
index de73c512..9c25868a 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/ParameterList.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/ParameterList.java
@@ -6,6 +6,8 @@
 
 package org.postgresql.core;
 
+import org.postgresql.util.ByteStreamWriter;
+
 import java.io.InputStream;
 import java.sql.SQLException;
 
@@ -122,6 +124,16 @@ public interface ParameterList {
    */
   void setBytea(int index, InputStream stream) throws SQLException;
 
+  /**
+   * Binds a binary bytea value stored as a ByteStreamWriter. The parameter's type is implicitly set to
+   * 'bytea'. The stream should remain valid until query execution has completed.
+   *
+   * @param index the 1-based parameter index to bind.
+   * @param writer a writer that can write the bytes for the parameter
+   * @throws SQLException on error or if <code>index</code> is out of range
+   */
+  void setBytea(int index, ByteStreamWriter writer) throws SQLException;
+
   /**
    * Binds a text value stored as an InputStream that is a valid UTF-8 byte stream.
    * Any byte-order marks (BOM) in the stream are passed to the backend.
diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/CompositeParameterList.java b/pgjdbc/src/main/java/org/postgresql/core/v3/CompositeParameterList.java
index 438c3121..0fabdc30 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/v3/CompositeParameterList.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/v3/CompositeParameterList.java
@@ -7,6 +7,7 @@
 package org.postgresql.core.v3;
 
 import org.postgresql.core.ParameterList;
+import org.postgresql.util.ByteStreamWriter;
 import org.postgresql.util.GT;
 import org.postgresql.util.PSQLException;
 import org.postgresql.util.PSQLState;
@@ -107,6 +108,11 @@ class CompositeParameterList implements V3ParameterList {
     subparams[sub].setBytea(index - offsets[sub], stream);
   }
 
+  public void setBytea(int index, ByteStreamWriter writer) throws SQLException {
+    int sub = findSubParam(index);
+    subparams[sub].setBytea(index - offsets[sub], writer);
+  }
+
   public void setText(int index, InputStream stream) throws SQLException {
     int sub = findSubParam(index);
     subparams[sub].setText(index - offsets[sub], stream);
diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
index 61850f78..f876fc6d 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
@@ -97,6 +97,9 @@ public class ConnectionFactoryImpl extends ConnectionFactory {
       newStream.getSocket().setSoTimeout(socketTimeout * 1000);
     }
 
+    String maxResultBuffer = PGProperty.MAX_RESULT_BUFFER.get(info);
+    newStream.setMaxResultBuffer(maxResultBuffer);
+
     // Enable TCP keep-alive probe if required.
     boolean requireTCPKeepAlive = PGProperty.TCP_KEEP_ALIVE.getBoolean(info);
     newStream.getSocket().setKeepAlive(requireTCPKeepAlive);
diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java b/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java
index 63cbc074..5d2e0910 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java
@@ -2204,8 +2204,9 @@ public class QueryExecutorImpl extends QueryExecutorBase {
                   new PSQLException(GT.tr("Ran out of memory retrieving query results."),
                       PSQLState.OUT_OF_MEMORY, oome));
             }
+          } catch (SQLException e) {
+            handler.handleError(e);
           }
-
           if (!noResults) {
             if (tuples == null) {
               tuples = new ArrayList<byte[][]>();
@@ -2300,6 +2301,7 @@ public class QueryExecutorImpl extends QueryExecutorBase {
           receiveRFQ();
           if (!pendingExecuteQueue.isEmpty() && pendingExecuteQueue.peekFirst().asSimple) {
             tuples = null;
+            pgStream.clearResultBufferCount();
 
             ExecuteRequest executeRequest = pendingExecuteQueue.removeFirst();
             // Simple queries might return several resultsets, thus we clear
diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/SimpleParameterList.java b/pgjdbc/src/main/java/org/postgresql/core/v3/SimpleParameterList.java
index 8d53eafa..1ce49996 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/v3/SimpleParameterList.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/v3/SimpleParameterList.java
@@ -14,6 +14,7 @@ import org.postgresql.geometric.PGbox;
 import org.postgresql.geometric.PGpoint;
 import org.postgresql.jdbc.UUIDArrayAssistant;
 import org.postgresql.util.ByteConverter;
+import org.postgresql.util.ByteStreamWriter;
 import org.postgresql.util.GT;
 import org.postgresql.util.PSQLException;
 import org.postgresql.util.PSQLState;
@@ -146,6 +147,11 @@ class SimpleParameterList implements V3ParameterList {
     bind(index, new StreamWrapper(stream), Oid.BYTEA, BINARY);
   }
 
+  @Override
+  public void setBytea(int index, ByteStreamWriter writer) throws SQLException {
+    bind(index, writer, Oid.BYTEA, BINARY);
+  }
+
   @Override
   public void setText(int index, InputStream stream) throws SQLException {
     bind(index, new StreamWrapper(stream), Oid.TEXT, TEXT);
@@ -291,6 +297,14 @@ class SimpleParameterList implements V3ParameterList {
     pgStream.sendStream(wrapper.getStream(), wrapper.getLength());
   }
 
+  //
+  // byte stream writer support
+  //
+
+  private static void streamBytea(PGStream pgStream, ByteStreamWriter writer) throws IOException {
+    pgStream.send(writer);
+  }
+
   public int[] getTypeOIDs() {
     return paramTypes;
   }
@@ -352,6 +366,11 @@ class SimpleParameterList implements V3ParameterList {
       return ((StreamWrapper) paramValues[index]).getLength();
     }
 
+    // Binary-format bytea?
+    if (paramValues[index] instanceof ByteStreamWriter) {
+      return ((ByteStreamWriter) paramValues[index]).getLength();
+    }
+
     // Already encoded?
     if (encoded[index] == null) {
       // Encode value and compute actual length using UTF-8.
@@ -381,6 +400,12 @@ class SimpleParameterList implements V3ParameterList {
       return;
     }
 
+    // Streamed bytea?
+    if (paramValues[index] instanceof ByteStreamWriter) {
+      streamBytea(pgStream, (ByteStreamWriter) paramValues[index]);
+      return;
+    }
+
     // Encoded string.
     if (encoded[index] == null) {
       encoded[index] = Utils.encodeUTF8((String) paramValues[index]);
diff --git a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
index e72aac5e..cc5d3021 100644
--- a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
+++ b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
@@ -1511,6 +1511,14 @@ public abstract class BaseDataSource implements CommonDataSource, Referenceable
     PGProperty.HIDE_UNPRIVILEGED_OBJECTS.set(properties, hideUnprivileged);
   }
 
+  public String getMaxResultBuffer() {
+    return PGProperty.MAX_RESULT_BUFFER.get(properties);
+  }
+
+  public void setMaxResultBuffer(String maxResultBuffer) {
+    PGProperty.MAX_RESULT_BUFFER.set(properties, maxResultBuffer);
+  }
+
   //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.1"
   public java.util.logging.Logger getParentLogger() {
     return Logger.getLogger("org.postgresql");
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java
index 1dc994e8..fd25a280 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java
@@ -18,6 +18,7 @@ import org.postgresql.core.v3.BatchedQuery;
 import org.postgresql.largeobject.LargeObject;
 import org.postgresql.largeobject.LargeObjectManager;
 import org.postgresql.util.ByteConverter;
+import org.postgresql.util.ByteStreamWriter;
 import org.postgresql.util.GT;
 import org.postgresql.util.HStoreConverter;
 import org.postgresql.util.PGBinaryObject;
@@ -363,6 +364,10 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement {
     preparedParameters.setBytea(parameterIndex, copy, 0, x.length);
   }
 
+  private void setByteStreamWriter(int parameterIndex, ByteStreamWriter x) throws SQLException {
+    preparedParameters.setBytea(parameterIndex, x);
+  }
+
   public void setDate(int parameterIndex, java.sql.Date x) throws SQLException {
     setDate(parameterIndex, x, null);
   }
@@ -930,6 +935,8 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement {
       setDouble(parameterIndex, (Double) x);
     } else if (x instanceof byte[]) {
       setBytes(parameterIndex, (byte[]) x);
+    } else if (x instanceof ByteStreamWriter) {
+      setByteStreamWriter(parameterIndex, (ByteStreamWriter) x);
     } else if (x instanceof java.sql.Date) {
       setDate(parameterIndex, (java.sql.Date) x);
     } else if (x instanceof Time) {
diff --git a/pgjdbc/src/main/java/org/postgresql/util/PGInterval.java b/pgjdbc/src/main/java/org/postgresql/util/PGInterval.java
index 0e9ccfad..439300d8 100644
--- a/pgjdbc/src/main/java/org/postgresql/util/PGInterval.java
+++ b/pgjdbc/src/main/java/org/postgresql/util/PGInterval.java
@@ -17,10 +17,10 @@ import java.util.StringTokenizer;
 public class PGInterval extends PGobject implements Serializable, Cloneable {
 
   private int years;
-  private byte months;
-  private byte days;
-  private byte hours;
-  private byte minutes;
+  private int months;
+  private int days;
+  private int hours;
+  private int minutes;
   private int wholeSeconds;
   private int microSeconds;
 
@@ -280,7 +280,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
    * @param months months to set
    */
   public void setMonths(int months) {
-    this.months = (byte) months;
+    this.months = months;
   }
 
   /**
@@ -298,7 +298,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
    * @param days days to set
    */
   public void setDays(int days) {
-    this.days = (byte)days;
+    this.days = days;
   }
 
   /**
@@ -316,7 +316,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
    * @param hours hours to set
    */
   public void setHours(int hours) {
-    this.hours = (byte)hours;
+    this.hours = hours;
   }
 
   /**
@@ -334,7 +334,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
    * @param minutes minutes to set
    */
   public void setMinutes(int minutes) {
-    this.minutes = (byte)minutes;
+    this.minutes = minutes;
   }
 
   /**
diff --git a/pgjdbc/src/main/java/org/postgresql/util/PGPropertyMaxResultBufferParser.java b/pgjdbc/src/main/java/org/postgresql/util/PGPropertyMaxResultBufferParser.java
new file mode 100644
index 00000000..353095ce
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/util/PGPropertyMaxResultBufferParser.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (c) 2019, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.util;
+
+import java.lang.management.ManagementFactory;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class PGPropertyMaxResultBufferParser {
+
+  private static final Logger LOGGER = Logger.getLogger(PGPropertyMaxResultBufferParser.class.getName());
+
+  private static final String[] PERCENT_PHRASES = new String[]{
+    "p",
+    "pct",
+    "percent"
+  };
+
+  /**
+   * Method to parse value of max result buffer size.
+   *
+   * @param value string containing size of bytes with optional multiplier (T, G, M or K) or percent
+   *              value to declare max percent of heap memory to use.
+   * @return value of max result buffer size.
+   * @throws PSQLException Exception when given value can't be parsed.
+   */
+  public static long parseProperty(String value) throws PSQLException {
+    long result = -1;
+    if (checkIfValueContainsPercent(value)) {
+      result = parseBytePercentValue(value);
+    } else if (checkIfValueExistsToBeParsed(value)) {
+      result = parseByteValue(value);
+    }
+    result = adjustResultSize(result);
+    return result;
+  }
+
+  /**
+   * Method to check if given value can contain percent declaration of size of max result buffer.
+   *
+   * @param value Value to check.
+   * @return Result if value contains percent.
+   */
+  private static boolean checkIfValueContainsPercent(String value) {
+    return (value != null) && (getPercentPhraseLengthIfContains(value) != -1);
+  }
+
+  /**
+   * Method to get percent value of max result buffer size dependable on actual free memory. This
+   * method doesn't check other possibilities of value declaration.
+   *
+   * @param value string containing percent used to define max result buffer.
+   * @return percent value of max result buffer size.
+   * @throws PSQLException Exception when given value can't be parsed.
+   */
+  private static long parseBytePercentValue(String value) throws PSQLException {
+    long result = -1;
+    int length;
+
+    if (checkIfValueExistsToBeParsed(value)) {
+      length = getPercentPhraseLengthIfContains(value);
+
+      if (length == -1) {
+        throwExceptionAboutParsingError(
+            "Received MaxResultBuffer parameter can't be parsed. Value received to parse: {0}",
+            value);
+      }
+
+      result = calculatePercentOfMemory(value, length);
+    }
+    return result;
+  }
+
+  /**
+   * Method to get length of percent phrase existing in given string, only if one of phrases exist
+   * on the length of string.
+   *
+   * @param valueToCheck String which is gonna be checked if contains percent phrase.
+   * @return Length of phrase inside string, returns -1 when no phrase found.
+   */
+  private static int getPercentPhraseLengthIfContains(String valueToCheck) {
+    int result = -1;
+    for (String phrase : PERCENT_PHRASES) {
+      int indx = getPhraseLengthIfContains(valueToCheck, phrase);
+      if (indx != -1) {
+        result = indx;
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Method to get length of given phrase in given string to check, method checks if phrase exist on
+   * the end of given string.
+   *
+   * @param valueToCheck String which gonna be checked if contains phrase.
+   * @param phrase       Phrase to be looked for on the end of given string.
+   * @return Length of phrase inside string, returns -1 when phrase wasn't found.
+   */
+  private static int getPhraseLengthIfContains(String valueToCheck, String phrase) {
+    int searchValueLength = phrase.length();
+
+    if (valueToCheck.length() > searchValueLength) {
+      String subValue = valueToCheck.substring(valueToCheck.length() - searchValueLength);
+      if (subValue.equals(phrase)) {
+        return searchValueLength;
+      }
+    }
+    return -1;
+  }
+
+  /**
+   * Method to calculate percent of given max heap memory.
+   *
+   * @param value               String which contains percent + percent phrase which gonna be used
+   *                            during calculations.
+   * @param percentPhraseLength Length of percent phrase inside given value.
+   * @return Size of byte buffer based on percent of max heap memory.
+   */
+  private static long calculatePercentOfMemory(String value, int percentPhraseLength) {
+    String realValue = value.substring(0, value.length() - percentPhraseLength);
+    double percent = Double.parseDouble(realValue) / 100;
+    long result = (long) (percent * ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax());
+    return result;
+  }
+
+  /**
+   * Method to check if given value has any chars to be parsed.
+   *
+   * @param value Value to be checked.
+   * @return Result if value can be parsed.
+   */
+  private static boolean checkIfValueExistsToBeParsed(String value) {
+    return value != null && value.length() != 0;
+  }
+
+  /**
+   * Method to get size based on given string value. String can contains just a number or number +
+   * multiplier sign (like T, G, M or K).
+   *
+   * @param value Given string to be parsed.
+   * @return Size based on given string.
+   * @throws PSQLException Exception when given value can't be parsed.
+   */
+  private static long parseByteValue(String value) throws PSQLException {
+    long result = -1;
+    long multiplier = 1;
+    long mul = 1000;
+    String realValue;
+    char sign = value.charAt(value.length() - 1);
+
+    switch (sign) {
+
+      case 'T':
+      case 't':
+        multiplier *= mul;
+
+      case 'G':
+      case 'g':
+        multiplier *= mul;
+
+      case 'M':
+      case 'm':
+        multiplier *= mul;
+
+      case 'K':
+      case 'k':
+        multiplier *= mul;
+        realValue = value.substring(0, value.length() - 1);
+        result = Integer.parseInt(realValue) * multiplier;
+        break;
+
+      case '%':
+        return result;
+
+      default:
+        if (sign >= '0' && sign <= '9') {
+          result = Long.parseLong(value);
+        } else {
+          throwExceptionAboutParsingError(
+              "Received MaxResultBuffer parameter can't be parsed. Value received to parse: {0}",
+              value);
+        }
+        break;
+    }
+    return result;
+  }
+
+  /**
+   * Method to adjust result memory limit size. If given memory is larger than 90% of max heap
+   * memory then it gonna be reduced to 90% of max heap memory.
+   *
+   * @param value Size to be adjusted.
+   * @return Adjusted size (original size or 90% of max heap memory)
+   */
+  private static long adjustResultSize(long value) {
+    if (value > 0.9 * ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax()) {
+      long newResult = (long) (0.9 * ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax());
+
+      LOGGER.log(Level.WARNING, GT.tr(
+          "WARNING! Required to allocate {0} bytes, which exceeded possible heap memory size. Assigned {1} bytes as limit.",
+          String.valueOf(value), String.valueOf(newResult)));
+
+      value = newResult;
+    }
+    return value;
+  }
+
+  /**
+   * Method to throw message for parsing MaxResultBuffer.
+   *
+   * @param message Message to be added to exception.
+   * @param values  Values to be put inside exception message.
+   * @throws PSQLException Exception when given value can't be parsed.
+   */
+  private static void throwExceptionAboutParsingError(String message, Object... values) throws PSQLException {
+    throw new PSQLException(GT.tr(
+      message,
+      values),
+      PSQLState.SYNTAX_ERROR);
+  }
+}


Test patch:
diff --git a/pgjdbc/src/main/java/org/postgresql/util/ByteBufferByteStreamWriter.java b/pgjdbc/src/main/java/org/postgresql/util/ByteBufferByteStreamWriter.java
new file mode 100644
index 00000000..be25be54
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/util/ByteBufferByteStreamWriter.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2020, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.util;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.WritableByteChannel;
+
+/**
+ * A {@link ByteStreamWriter} that writes a {@link ByteBuffer java.nio.ByteBuffer} to a byte array
+ * parameter.
+ */
+public class ByteBufferByteStreamWriter implements ByteStreamWriter {
+
+  private final ByteBuffer buf;
+  private final int length;
+
+  /**
+   * Construct the writer with the given {@link ByteBuffer}
+   *
+   * @param buf the buffer to use.
+   */
+  public ByteBufferByteStreamWriter(ByteBuffer buf) {
+    this.buf = buf;
+    this.length = buf.remaining();
+  }
+
+  @Override
+  public int getLength() {
+    return length;
+  }
+
+  @Override
+  public void writeTo(ByteStreamTarget target) throws IOException {
+    // this _does_ involve some copying to a temporary buffer, but that's unavoidable
+    // as OutputStream itself only accepts single bytes or heap allocated byte arrays
+    WritableByteChannel c = Channels.newChannel(target.getOutputStream());
+    try {
+      c.write(buf);
+    } finally {
+      c.close();
+    }
+  }
+}
diff --git a/pgjdbc/src/main/java/org/postgresql/util/ByteStreamWriter.java b/pgjdbc/src/main/java/org/postgresql/util/ByteStreamWriter.java
new file mode 100644
index 00000000..798f938a
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/util/ByteStreamWriter.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2020, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * A class that can be used to set a byte array parameter by writing to an OutputStream.
+ *
+ * <p>The intended use case is wanting to write data to a byte array parameter that is stored off
+ * heap in a direct memory pool or in some other form that is inconvenient to assemble into a single
+ * heap-allocated buffer.</p>
+ * <p> Users should write their own implementation depending on the
+ * original data source. The driver provides a built-in implementation supporting the {@link
+ * java.nio.ByteBuffer} class, see {@link ByteBufferByteStreamWriter}.</p>
+ * <p> Intended usage is to simply pass in an instance using
+ * {@link java.sql.PreparedStatement#setObject(int, Object)}:</p>
+ * <pre>
+ *     int bufLength = someBufferObject.length();
+ *     preparedStatement.setObject(1, new MyByteStreamWriter(bufLength, someBufferObject));
+ * </pre>
+ * <p>The length must be known ahead of the stream being written to. </p>
+ * <p>This provides the application more control over memory management than calling
+ * {@link java.sql.PreparedStatement#setBinaryStream(int, InputStream)} as with the latter the
+ * caller has no control over the buffering strategy. </p>
+ */
+public interface ByteStreamWriter {
+
+  /**
+   * Returns the length of the stream.
+   *
+   * <p> This must be known ahead of calling {@link #writeTo(ByteStreamTarget)}. </p>
+   *
+   * @return the number of bytes in the stream.
+   */
+  int getLength();
+
+  /**
+   * Write the data to the provided {@link OutputStream}.
+   *
+   * <p> Should not write more than {@link #getLength()} bytes. If attempted, the provided stream
+   * will throw an {@link java.io.IOException}. </p>
+   *
+   * @param target the stream to write the data to
+   * @throws IOException if the underlying stream throws or there is some other error.
+   */
+  void writeTo(ByteStreamTarget target) throws IOException;
+
+  /**
+   * Provides a target to write bytes to.
+   */
+  interface ByteStreamTarget {
+
+    /**
+     * Provides an output stream to write bytes to.
+     *
+     * @return an output stream
+     */
+    OutputStream getOutputStream();
+  }
+}
diff --git a/pgjdbc/src/test/java/org/postgresql/test/core/FixedLengthOutputStreamTest.java b/pgjdbc/src/test/java/org/postgresql/test/core/FixedLengthOutputStreamTest.java
new file mode 100644
index 00000000..60587951
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/core/FixedLengthOutputStreamTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2020, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.core;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import org.postgresql.core.FixedLengthOutputStream;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.sql.SQLException;
+
+public class FixedLengthOutputStreamTest {
+
+  private ByteArrayOutputStream targetStream;
+  private FixedLengthOutputStream fixedLengthStream;
+
+  @Before
+  public void setUp() throws Exception {
+    targetStream = new ByteArrayOutputStream();
+    fixedLengthStream = new FixedLengthOutputStream(10, targetStream);
+  }
+
+  @After
+  public void tearDown() throws SQLException {
+  }
+
+  private void verifyExpectedOutput(byte[] expected) {
+    assertArrayEquals("Incorrect data written to target stream",
+        expected, targetStream.toByteArray());
+  }
+
+  @Test
+  public void testSingleByteWrites() throws IOException {
+    fixedLengthStream.write((byte) 1);
+    assertEquals("Incorrect remaining value", 9, fixedLengthStream.remaining());
+    fixedLengthStream.write((byte) 2);
+    assertEquals("Incorrect remaining value", 8, fixedLengthStream.remaining());
+    verifyExpectedOutput(new byte[]{1, 2});
+  }
+
+  @Test
+  public void testMultipleByteWrites() throws IOException {
+    fixedLengthStream.write(new byte[]{1, 2, 3, 4});
+    assertEquals("Incorrect remaining value", 6, fixedLengthStream.remaining());
+    fixedLengthStream.write(new byte[]{5, 6, 7, 8});
+    assertEquals("Incorrect remaining value", 2, fixedLengthStream.remaining());
+    verifyExpectedOutput(new byte[]{1, 2, 3, 4, 5, 6, 7, 8});
+  }
+
+  @Test
+  public void testSingleByteOverLimit() throws IOException {
+    byte[] data = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
+    fixedLengthStream.write(data);
+    assertEquals("Incorrect remaining value", 0, fixedLengthStream.remaining());
+    try {
+      fixedLengthStream.write((byte) 'a');
+      fail("Expected exception not thrown");
+    } catch (IOException e) {
+      assertEquals("Incorrect exception message",
+          "Attempt to write more than the specified 10 bytes", e.getMessage());
+    }
+    assertEquals("Incorrect remaining value after exception",
+        0, fixedLengthStream.remaining());
+    verifyExpectedOutput(data);
+  }
+
+  @Test
+  public void testMultipleBytesOverLimit() throws IOException {
+    byte[] data = new byte[]{1, 2, 3, 4, 5, 6, 7, 8};
+    fixedLengthStream.write(data);
+    assertEquals(2, fixedLengthStream.remaining());
+    try {
+      fixedLengthStream.write(new byte[]{'a', 'b', 'c', 'd'});
+      fail("Expected exception not thrown");
+    } catch (IOException e) {
+      assertEquals("Incorrect exception message",
+          "Attempt to write more than the specified 10 bytes", e.getMessage());
+    }
+    assertEquals("Incorrect remaining value after exception",
+        2, fixedLengthStream.remaining());
+    verifyExpectedOutput(data);
+  }
+}
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/IntervalTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/IntervalTest.java
index ce3f4138..73517478 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/IntervalTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/IntervalTest.java
@@ -174,6 +174,15 @@ public class IntervalTest {
     assertEquals(-15, pgi.getHours());
     assertEquals(57, pgi.getMinutes());
     assertEquals(-12.1, pgi.getSeconds(), 0);
+
+    // Unjustified interval test
+    pgi = new PGInterval("@ 0 years 0 mons 0 days 900 hours 0 mins 0.00 secs");
+    assertEquals(0, pgi.getYears());
+    assertEquals(0, pgi.getMonths());
+    assertEquals(0, pgi.getDays());
+    assertEquals(900, pgi.getHours());
+    assertEquals(0, pgi.getMinutes());
+    assertEquals(0, pgi.getSeconds(), 0);
   }
 
   private Calendar getStartCalendar() {
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/Jdbc2TestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/Jdbc2TestSuite.java
index 4e01958d..ed097fad 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/Jdbc2TestSuite.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/Jdbc2TestSuite.java
@@ -16,13 +16,17 @@ import org.postgresql.jdbc.DeepBatchedInsertStatementTest;
 import org.postgresql.jdbc.NoColumnMetadataIssue1613Test;
 import org.postgresql.jdbc.PgSQLXMLTest;
 import org.postgresql.jdbc.PrimitiveArraySupportTest;
+import org.postgresql.test.core.FixedLengthOutputStreamTest;
 import org.postgresql.test.core.JavaVersionTest;
 import org.postgresql.test.core.LogServerMessagePropertyTest;
 import org.postgresql.test.core.NativeQueryBindLengthTest;
 import org.postgresql.test.core.OptionsPropertyTest;
+import org.postgresql.test.util.ByteBufferByteStreamWriterTest;
+import org.postgresql.test.util.ByteStreamWriterTest;
 import org.postgresql.test.util.ExpressionPropertiesTest;
 import org.postgresql.test.util.HostSpecTest;
 import org.postgresql.test.util.LruCacheTest;
+import org.postgresql.test.util.PGPropertyMaxResultBufferParserTest;
 import org.postgresql.test.util.ServerVersionParseTest;
 import org.postgresql.test.util.ServerVersionTest;
 import org.postgresql.util.ReaderInputStreamTest;
@@ -80,8 +84,12 @@ import org.junit.runners.Suite;
     OidValueOfTest.class,
     OptionsPropertyTest.class,
     OuterJoinSyntaxTest.class,
+    FixedLengthOutputStreamTest.class,
+    ByteStreamWriterTest.class,
+    ByteBufferByteStreamWriterTest.class,
     ParameterStatusTest.class,
     ParserTest.class,
+    PGPropertyMaxResultBufferParserTest.class,
     PGPropertyTest.class,
     PGTimestampTest.class,
     PGTimeTest.class,
diff --git a/pgjdbc/src/test/java/org/postgresql/test/util/ByteBufferByteStreamWriterTest.java b/pgjdbc/src/test/java/org/postgresql/test/util/ByteBufferByteStreamWriterTest.java
new file mode 100644
index 00000000..6893f6b8
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/util/ByteBufferByteStreamWriterTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2020, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.util;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import org.postgresql.util.ByteBufferByteStreamWriter;
+import org.postgresql.util.ByteStreamWriter;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+
+public class ByteBufferByteStreamWriterTest {
+
+  private ByteArrayOutputStream targetStream;
+  private byte[] data;
+  private ByteBufferByteStreamWriter writer;
+
+  @Before
+  public void setUp() throws Exception {
+    targetStream = new ByteArrayOutputStream();
+    data = new byte[] { 1, 2, 3, 4 };
+    ByteBuffer buffer = ByteBuffer.wrap(data);
+    writer = new ByteBufferByteStreamWriter(buffer);
+  }
+
+  @Test
+  public void testReportsLengthCorrectly() {
+    assertEquals("Incorrect length reported", 4, writer.getLength());
+  }
+
+  @Test
+  public void testCopiesDataCorrectly() throws IOException {
+    writer.writeTo(target(targetStream));
+    byte[] written = targetStream.toByteArray();
+    assertArrayEquals("Incorrect data written to target stream", data, written);
+  }
+
+  @Test
+  public void testPropagatesException() throws IOException {
+    final IOException e = new IOException("oh no");
+    OutputStream errorStream = new OutputStream() {
+      @Override
+      public void write(int b) throws IOException {
+        throw e;
+      }
+    };
+    try {
+      writer.writeTo(target(errorStream));
+      fail("No exception thrown");
+    } catch (IOException caught) {
+      assertEquals("Exception was thrown that wasn't the expected one", caught, e);
+    }
+  }
+
+  private static ByteStreamWriter.ByteStreamTarget target(final OutputStream stream) {
+    return new ByteStreamWriter.ByteStreamTarget() {
+      @Override
+      public OutputStream getOutputStream() {
+        return stream;
+      }
+    };
+  }
+}
diff --git a/pgjdbc/src/test/java/org/postgresql/test/util/ByteStreamWriterTest.java b/pgjdbc/src/test/java/org/postgresql/test/util/ByteStreamWriterTest.java
new file mode 100644
index 00000000..6f3d771e
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/util/ByteStreamWriterTest.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (c) 2020, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.util;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.postgresql.test.TestUtil;
+import org.postgresql.test.jdbc2.BaseTest4;
+import org.postgresql.util.ByteBufferByteStreamWriter;
+import org.postgresql.util.ByteStreamWriter;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Random;
+
+public class ByteStreamWriterTest extends BaseTest4 {
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    assumeByteaSupported();
+    TestUtil.createTempTable(con, "images", "img bytea");
+  }
+
+  private ByteBuffer testData(int size) {
+    ByteBuffer data = ByteBuffer.allocate(size);
+    Random random = new Random(31459);
+    while (data.remaining() > 0) {
+      data.putLong(random.nextLong());
+    }
+    data.rewind();
+    return data;
+  }
+
+  private void insertStream(ByteBuffer testData) throws Exception {
+    insertStream(testData, null);
+  }
+
+  private void insertStream(ByteBuffer testData, Integer lengthOverride) throws Exception {
+    insertStream(new TestByteBufferByteStreamWriter(testData, lengthOverride));
+  }
+
+  private void insertStream(ByteStreamWriter writer) throws Exception {
+    PreparedStatement updatePS = con.prepareStatement(TestUtil.insertSQL("images", "img", "?"));
+    try {
+      updatePS.setObject(1, writer);
+      updatePS.executeUpdate();
+    } finally {
+      updatePS.close();
+    }
+  }
+
+  private void validateContent(ByteBuffer data) throws Exception {
+    validateContent(data.array());
+  }
+
+  private void validateContent(byte [] data) throws Exception {
+    PreparedStatement selectPS = con.prepareStatement(TestUtil.selectSQL("images", "img"));
+    try {
+      ResultSet rs = selectPS.executeQuery();
+      try {
+        rs.next();
+        byte[] actualData = rs.getBytes(1);
+        assertArrayEquals("Sent and received data are not the same", data, actualData);
+      } finally {
+        rs.close();
+      }
+    } finally {
+      selectPS.close();
+    }
+  }
+
+  @Test
+  public void testEmpty() throws Exception {
+    ByteBuffer testData = testData(0);
+    insertStream(testData);
+    validateContent(testData);
+  }
+
+  @Test
+  public void testLength2Kb() throws Exception {
+    ByteBuffer testData = testData(2 * 1024);
+    insertStream(testData);
+    validateContent(testData);
+  }
+
+  @Test
+  public void testLength10Kb() throws Exception {
+    ByteBuffer testData = testData(10 * 1024);
+    insertStream(testData);
+    validateContent(testData);
+  }
+
+  @Test
+  public void testLength100Kb() throws Exception {
+    ByteBuffer testData = testData(100 * 1024);
+    insertStream(testData);
+    validateContent(testData);
+  }
+
+  @Test
+  public void testLength200Kb() throws Exception {
+    ByteBuffer testData = testData(200 * 1024);
+    insertStream(testData);
+    validateContent(testData);
+  }
+
+  @Test
+  public void testLengthGreaterThanContent() throws Exception {
+    ByteBuffer testData = testData(8);
+    insertStream(testData, 10);
+    byte[] expectedData = new byte[10];
+    testData.rewind();
+    testData.get(expectedData, 0, 8);
+    // other two bytes are zeroed out, which the jvm does for us automatically
+    validateContent(expectedData);
+  }
+
+  @Test
+  public void testLengthLessThanContent() throws Exception {
+    ByteBuffer testData = testData(8);
+    try {
+      insertStream(testData, 4);
+      fail("did not throw exception when too much content");
+    } catch (SQLException e) {
+      Throwable cause = e.getCause();
+      assertTrue("cause wan't an IOException", cause instanceof IOException);
+      assertEquals("Incorrect exception message",
+          cause.getMessage(), "Attempt to write more than the specified 4 bytes");
+    }
+  }
+
+  @Test
+  public void testIOExceptionPassedThroughAsCause() throws Exception {
+    IOException e = new IOException("oh no");
+    try {
+      insertStream(new ExceptionThrowingByteStreamWriter(e));
+      fail("did not throw exception when IOException thrown");
+    } catch (SQLException sqle) {
+      Throwable cause = sqle.getCause();
+      assertEquals("Incorrect exception cause", e, cause);
+    }
+  }
+
+  @Test
+  public void testRuntimeExceptionPassedThroughAsIOException() throws Exception {
+    RuntimeException e = new RuntimeException("oh no");
+    try {
+      insertStream(new ExceptionThrowingByteStreamWriter(e));
+      fail("did not throw exception when RuntimeException thrown");
+    } catch (SQLException sqle) {
+      Throwable cause = sqle.getCause();
+      assertTrue("cause wan't an IOException", cause instanceof IOException);
+      assertEquals("Incorrect exception message",
+          cause.getMessage(), "Error writing bytes to stream");
+      Throwable nestedCause = cause.getCause();
+      assertEquals("Incorrect exception cause", e, nestedCause);
+    }
+  }
+
+  /**
+   * Allows testing where reported length doesn't match what the stream writer attempts
+   */
+  private static class TestByteBufferByteStreamWriter extends ByteBufferByteStreamWriter {
+
+    private final Integer lengthOverride;
+
+    private TestByteBufferByteStreamWriter(ByteBuffer buf, Integer lengthOverride) {
+      super(buf);
+      this.lengthOverride = lengthOverride;
+    }
+
+    @Override
+    public int getLength() {
+      return lengthOverride != null ? lengthOverride : super.getLength();
+    }
+  }
+
+  private static class ExceptionThrowingByteStreamWriter implements ByteStreamWriter {
+
+    private final Throwable cause;
+
+    private ExceptionThrowingByteStreamWriter(Throwable cause) {
+      assertTrue(cause instanceof RuntimeException || cause instanceof IOException);
+      this.cause = cause;
+    }
+
+    @Override
+    public int getLength() {
+      return 1;
+    }
+
+    @Override
+    public void writeTo(ByteStreamTarget target) throws IOException {
+      if (cause instanceof RuntimeException) {
+        throw (RuntimeException) cause;
+      } else if (cause instanceof IOException) {
+        throw (IOException) cause;
+      }
+    }
+  }
+
+}
diff --git a/pgjdbc/src/test/java/org/postgresql/test/util/PGPropertyMaxResultBufferParserTest.java b/pgjdbc/src/test/java/org/postgresql/test/util/PGPropertyMaxResultBufferParserTest.java
new file mode 100644
index 00000000..910dcade
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/util/PGPropertyMaxResultBufferParserTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2019, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.util;
+
+import static org.junit.Assert.fail;
+
+import org.postgresql.util.PGPropertyMaxResultBufferParser;
+import org.postgresql.util.PSQLException;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.lang.management.ManagementFactory;
+import java.util.Arrays;
+import java.util.Collection;
+
+@RunWith(Parameterized.class)
+public class PGPropertyMaxResultBufferParserTest {
+
+  @Parameterized.Parameter(0)
+  public String valueToParse;
+
+  @Parameterized.Parameter(1)
+  public long expectedResult;
+
+  @Parameterized.Parameters(name = "{index}: Test with valueToParse={0}, expectedResult={1}")
+  public static Collection<Object[]> data() {
+    Object[][] data = new Object[][]{
+      {"100", 100L},
+      {"10K", 10L * 1000},
+      {"25M", 25L * 1000 * 1000},
+      //next two should be too big
+      {"35G", (long) (0.90 * ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax())},
+      {"1T", (long) (0.90 * ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax())},
+      //percent test
+      {"5p", (long) (0.05 * ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax())},
+      {"10pct", (long) (0.10 * ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax())},
+      {"15percent",
+        (long) (0.15 * ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax())},
+      //for testing empty property
+      {"", -1},
+      {null, -1}
+    };
+    return Arrays.asList(data);
+  }
+
+  @Test
+  public void testGetMaxResultBufferValue() {
+    try {
+      long result = PGPropertyMaxResultBufferParser.parseProperty(valueToParse);
+      Assert.assertEquals(expectedResult, result);
+    } catch (PSQLException e) {
+      //shouldn't occur
+      fail();
+    }
+  }
+
+  @Test(expected = PSQLException.class)
+  public void testGetMaxResultBufferValueException() throws PSQLException {
+    long result = PGPropertyMaxResultBufferParser.parseProperty("abc");
+    fail();
+  }
+
+}