pgjdbc/pgjdbc GitHub issues and pull requests (mirror)  
help / color / mirror / Atom feed
From: vlsi (@vlsi) <[email protected]>
To: pgjdbc/pgjdbc <[email protected]>
Subject: [pgjdbc/pgjdbc] PR #4195: feat(jdbc): add direction-aware binarySend/binaryReceive properties
Date: Wed, 17 Jun 2026 20:24:44 +0000
Message-ID: <[email protected]> (raw)

## Why

The existing `binaryTransfer`, `binaryTransferEnable`, and `binaryTransferDisable` properties cannot tell the **send** direction (parameters sent to the server) apart from the **receive** direction (results read back), even though the driver already keeps the two OID sets separate internally. The two directions have a different nature — for example, the driver excludes `date` from binary on send to preserve sub-day precision for timestamp targets, but that has no bearing on receive — so a single knob cannot express what users actually need.

Splitting the configuration by direction also makes [#3062](https://github.com/pgjdbc/pgjdbc/pull/3062) easier to configure, since that work needs per-direction control over which types travel in binary.

This change can land either before #3062 or as part of #3062.

## What

Two new connection properties with an `oid:mode` format, where `oid` is a type name or OID number:

- `binarySend` — modes `auto`, `force`, `disable`.
- `binaryReceive` — modes `auto`, `disable`.

Semantics per type and direction:

- `force` (send only) adds the type to the binary set; best-effort, same risk as `binaryTransferEnable` (the server may reject binary for a type it cannot send that way, and for temporal types it can change the stored value).
- `disable` keeps the type in text and stops the legacy properties from re-enabling it.
- `auto` resets the type to the driver's built-in default for that direction. It overrides the legacy properties too — including `binaryTransferDisable` — so the type also leaves the disabled set that `addDataType` and other later opt-ins consult.

Precedence: a per-type mode here wins over the legacy `binaryTransfer` / `binaryTransferEnable` / `binaryTransferDisable` for that type and direction. The legacy properties are unchanged and keep working.

Scope for this version: modes apply to top-level types only; `auto` may change between driver versions; recursion into composite and array element types can follow later. No codec-layer or `typsend` work is involved — this is a thin wrapper over the per-direction OID sets the driver already maintains.

Also adds matching `BaseDataSource` getters/setters and documents the properties in `README.md` and `docs/content/documentation/use.md`.

## How to verify

```
./gradlew --quiet :postgresql:classes :postgresql:style
./gradlew --quiet :postgresql:test --tests org.postgresql.test.jdbc2.BinaryDirectionPropertiesTest
```

`BinaryDirectionPropertiesTest` covers `force`/`disable`/`auto`, precedence over the legacy properties in both directions, OID-by-name and OID-by-number, and rejection of invalid modes/syntax. It needs a live PostgreSQL (`localhost:5432`, database/user `test`).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

diff --git a/README.md b/README.md
index b7ebf9954c..04a2001f49 100644
--- a/README.md
+++ b/README.md
@@ -117,6 +117,8 @@ In addition to the standard connection parameters the driver supports a number o
 | binaryTransfer                | Boolean |          true           | Enable binary transfer for supported built-in types if possible. Setting this to false disables any binary transfer unless it's individually activated for each type with `binaryTransferEnable`. Whether it is possible to use binary transfer at all depends on server side prepared statements (see `prepareThreshold` ).                 |
 | binaryTransferEnable          | String |           ""            | Comma separated list of types to enable binary transfer. Either OID numbers or names.                                                                                                                                                                                                                                                         |
 | binaryTransferDisable         | String |           ""            | Comma separated list of types to disable binary transfer. Either OID numbers or names. Overrides values in the driver default set and values set with binaryTransferEnable.                                                                                                                                                                   |
+| binarySend                    | String |           ""            | Per-type binary format for parameters sent to the server. Comma separated `oid:mode` entries, where `oid` is a type name or OID number and `mode` is `auto`, `force`, or `disable`. Every mode overrides `binaryTransfer`, `binaryTransferEnable`, and `binaryTransferDisable` for that type when sending. `auto` resets the type to the driver's built-in default, which may change between versions; `disable` keeps the type in text; `force` is best-effort and may fail at the server or change the stored value for temporal types. |
+| binaryReceive                 | String |           ""            | Per-type binary format for results received from the server. Comma separated `oid:mode` entries, where `oid` is a type name or OID number and `mode` is `auto` or `disable`. Every mode overrides `binaryTransfer`, `binaryTransferEnable`, and `binaryTransferDisable` for that type when receiving. `auto` resets the type to the driver's built-in default, which may change between versions; `disable` keeps the type in text. |
 | prepareThreshold              | Integer |            5            | Determine the number of `PreparedStatement` executions required before switching over to use server side prepared statements. The default is five, meaning start using server side prepared statements on the fifth execution of the same `PreparedStatement` object. A value of -1 activates server side prepared statements and forces binary transfer for enabled types (see `binaryTransfer` ). |
 | preparedStatementCacheQueries | Integer |           256           | Specifies the maximum number of entries in per-connection cache of prepared statements. A value of 0 disables the cache.                                                                                                                                                                                                                     |
 | preparedStatementCacheSizeMiB | Integer |            5            | Specifies the maximum size (in megabytes) of a per-connection prepared statement cache. A value of 0 disables the cache.                                                                                                                                                                                                                     |
diff --git a/docs/content/documentation/use.md b/docs/content/documentation/use.md
index 315dff6672..43fd76aefe 100644
--- a/docs/content/documentation/use.md
+++ b/docs/content/documentation/use.md
@@ -212,6 +212,23 @@ Comma separated list of types to enable binary transfer. Either OID numbers or n
 Comma separated list of types to disable binary transfer. Either OID numbers or names.
 Overrides values in the driver default set and values set with binaryTransferEnable.
 
+* **`binarySend (`*String*`)`** *Default `empty string`*\
+Per-type binary format for parameters sent to the server.
+Comma separated list of `oid:mode` entries, where `oid` is a type name or OID number and `mode` is `auto`, `force`, or `disable`.
+Every mode overrides `binaryTransfer`, `binaryTransferEnable`, and `binaryTransferDisable` for that type when sending.
+`auto` resets the type to the driver's built-in default, which may change between driver versions.
+`disable` keeps the type in text.
+`force` is best-effort: like `binaryTransferEnable`, it may request the binary format for a type the server cannot send in binary, which then fails at the server; for temporal types it can also change the value the server stores (for example, binary `date` loses the sub-day precision the text path keeps for timestamp targets).
+In this version, modes apply to top-level types only; recursion into composite and array element types may follow later, and only widens what `force`/`disable` reach.
+
+* **`binaryReceive (`*String*`)`** *Default `empty string`*\
+Per-type binary format for results received from the server.
+Comma separated list of `oid:mode` entries, where `oid` is a type name or OID number and `mode` is `auto` or `disable`.
+Every mode overrides `binaryTransfer`, `binaryTransferEnable`, and `binaryTransferDisable` for that type when receiving.
+`auto` resets the type to the driver's built-in default, which may change between driver versions.
+`disable` keeps the type in text.
+In this version, modes apply to top-level types only; recursion into composite and array element types may follow later, and only widens what `disable` reaches.
+
 * **`databaseMetadataCacheFields (`*int*`)`** *Default `65536`*\
 Specifies the maximum number of fields to be cached per connection.
 A value of `0` disables the cache.
diff --git a/pgjdbc/src/main/java/org/postgresql/PGProperty.java b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
index bb5a9e51b6..98029fa96c 100644
--- a/pgjdbc/src/main/java/org/postgresql/PGProperty.java
+++ b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
@@ -108,6 +108,38 @@ public enum PGProperty {
       false,
       new String[]{"always", "never", "conservative"}),
 
+  /**
+   * Per-type binary format for results received from the server.
+   */
+  BINARY_RECEIVE(
+      "binaryReceive",
+      "",
+      "Per-type binary format for results received from the server. "
+          + "Comma separated list of `oid:mode` entries, where `oid` is a type name or OID number "
+          + "and `mode` is `auto` or `disable`. "
+          + "Every mode overrides `binaryTransfer`, `binaryTransferEnable`, and "
+          + "`binaryTransferDisable` for that type when receiving. `auto` resets the type to the "
+          + "driver's built-in default, which may change between driver versions. "
+          + "Modes apply to top-level types only."),
+
+  /**
+   * Per-type binary format for parameters sent to the server.
+   */
+  BINARY_SEND(
+      "binarySend",
+      "",
+      "Per-type binary format for parameters sent to the server. "
+          + "Comma separated list of `oid:mode` entries, where `oid` is a type name or OID number "
+          + "and `mode` is `auto`, `force`, or `disable`. "
+          + "Every mode overrides `binaryTransfer`, `binaryTransferEnable`, and "
+          + "`binaryTransferDisable` for that type when sending. `auto` resets the type to the "
+          + "driver's built-in default, which may change between driver versions. "
+          + "`force` is best-effort: like `binaryTransferEnable`, it may request the binary format "
+          + "for a type the server cannot send in binary, which then fails at the server, and for "
+          + "temporal types it can change the value the server stores (for example, binary `date` "
+          + "loses the sub-day precision that the text path keeps for timestamp targets). "
+          + "Modes apply to top-level types only."),
+
   /**
    * Use binary format for sending and receiving data if possible.
    */
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 4a5dd4ce14..c17a1734a0 100644
--- a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
+++ b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
@@ -966,6 +966,40 @@ public String getBinaryTransferDisable() {
     return castNonNull(PGProperty.BINARY_TRANSFER_DISABLE.getOrDefault(properties));
   }
 
+  /**
+   * @param spec per-type binary format for parameters sent to the server, as {@code oid:mode}
+   *     entries
+   * @see PGProperty#BINARY_SEND
+   */
+  public void setBinarySend(@Nullable String spec) {
+    PGProperty.BINARY_SEND.set(properties, spec);
+  }
+
+  /**
+   * @return per-type binary format for parameters sent to the server
+   * @see PGProperty#BINARY_SEND
+   */
+  public String getBinarySend() {
+    return castNonNull(PGProperty.BINARY_SEND.getOrDefault(properties));
+  }
+
+  /**
+   * @param spec per-type binary format for results received from the server, as {@code oid:mode}
+   *     entries
+   * @see PGProperty#BINARY_RECEIVE
+   */
+  public void setBinaryReceive(@Nullable String spec) {
+    PGProperty.BINARY_RECEIVE.set(properties, spec);
+  }
+
+  /**
+   * @return per-type binary format for results received from the server
+   * @see PGProperty#BINARY_RECEIVE
+   */
+  public String getBinaryReceive() {
+    return castNonNull(PGProperty.BINARY_RECEIVE.getOrDefault(properties));
+  }
+
   /**
    * @return string type
    * @see PGProperty#STRING_TYPE
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
index c6351adacc..66adb313b2 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
@@ -197,9 +197,12 @@ private enum ReadOnlyBehavior {
   protected boolean forcebinary;
 
   /**
-   * Oids for which binary transfer should be disabled.
+   * Oids kept in text on each direction: the union of the legacy {@code binaryTransferDisable} set
+   * and the {@code disable} entries of {@code binarySend}/{@code binaryReceive}. Later opt-ins such
+   * as {@code addDataType} honour these so a disabled type is not silently re-enabled.
    */
-  private final Set<? extends Integer> binaryDisabledOids;
+  private final Set<Integer> binarySendDisabledOids;
+  private final Set<Integer> binaryReceiveDisabledOids;
 
   private int rsHoldability = ResultSet.CLOSE_CURSORS_AT_COMMIT;
   private int savepointId;
@@ -313,26 +316,37 @@ public PgConnection(HostSpec[] hostSpecs,
       // "cached plan must not change result type" to callers.
       queryExecutor.setFlushCacheOnDdl(PGProperty.FLUSH_CACHE_ON_DDL.getBoolean(info));
 
-      // get oids that support binary transfer
-      Set<Integer> binaryOids = getBinaryEnabledOids(info);
-      // get oids that should be disabled from transfer
-      binaryDisabledOids = getBinaryDisabledOids(info);
-      // if there are any, remove them from the enabled ones
-      if (!binaryDisabledOids.isEmpty()) {
-        binaryOids.removeAll(binaryDisabledOids);
-      }
-
-      // split for receive and send for better control
-      Set<Integer> useBinarySendForOids = new HashSet<>(binaryOids);
+      // Step 1: parse the legacy binaryTransfer* properties and lay them out into the
+      // per-direction send/receive sets (both identical at this point).
+      Set<Integer> legacyBinaryOids = getBinaryEnabledOids(info);
+      Set<? extends Integer> legacyDisabledOids = getBinaryDisabledOids(info);
+      legacyBinaryOids.removeAll(legacyDisabledOids);
+      Set<Integer> useBinarySendForOids = new HashSet<>(legacyBinaryOids);
+      Set<Integer> useBinaryReceiveForOids = new HashSet<>(legacyBinaryOids);
 
-      Set<Integer> useBinaryReceiveForOids = new HashSet<>(binaryOids);
-
-      /*
-       * Does not pass unit tests because unit tests expect setDate to have millisecond accuracy
-       * whereas the binary transfer only supports date accuracy.
-       */
+      // Step 2: the driver never sends DATE in binary by default, because the text path keeps the
+      // sub-day precision that setDate needs for timestamp targets. Model it as date=disable on send.
       useBinarySendForOids.remove(Oid.DATE);
 
+      // The built-in defaults that binarySend/binaryReceive `auto` resets a type to. The send
+      // default follows the same DATE rule. Computed here, so the parser stays free of type hardcodes.
+      Set<Integer> sendDefaultOids = new HashSet<>(SUPPORTED_BINARY_OIDS);
+      sendDefaultOids.remove(Oid.DATE);
+      Set<Integer> receiveDefaultOids = SUPPORTED_BINARY_OIDS;
+
+      // Step 3: parse the new binarySend/binaryReceive properties and update the per-direction sets.
+      // These override the legacy properties for the types they mention; `auto` resets to the
+      // supplied default set. The disabled sets start from the legacy binaryTransferDisable set so
+      // that later opt-ins such as addDataType keep honouring every disabled type.
+      Set<Integer> sendDisabledOids = new HashSet<>(legacyDisabledOids);
+      Set<Integer> receiveDisabledOids = new HashSet<>(legacyDisabledOids);
+      applyBinaryDirectionOverrides(info, PGProperty.BINARY_SEND, useBinarySendForOids,
+          sendDefaultOids, true, sendDisabledOids);
+      applyBinaryDirectionOverrides(info, PGProperty.BINARY_RECEIVE, useBinaryReceiveForOids,
+          receiveDefaultOids, false, receiveDisabledOids);
+      binarySendDisabledOids = sendDisabledOids;
+      binaryReceiveDisabledOids = receiveDisabledOids;
+
       queryExecutor.setBinaryReceiveOids(useBinaryReceiveForOids);
       queryExecutor.setBinarySendOids(useBinarySendForOids);
 
@@ -518,6 +532,76 @@ private static Set<? extends Integer> getOidSet(String oidList) throws PSQLExcep
     return oids;
   }
 
+  /**
+   * Applies a per-direction, per-type binary override property ({@code binarySend} or
+   * {@code binaryReceive}) on top of the set already computed from the legacy
+   * {@code binaryTransfer*} properties. Every mode overrides the legacy properties for that type
+   * and direction: {@code force} adds the OID, {@code disable} removes it, and {@code auto} resets
+   * the OID to {@code defaultOids} (the driver's built-in default for the direction, which may
+   * change between driver versions). The parser itself never special-cases a type; any default
+   * such as the send-side {@code DATE} exclusion is baked into {@code defaultOids} by the caller.
+   *
+   * @param info connection properties
+   * @param property {@link PGProperty#BINARY_SEND} or {@link PGProperty#BINARY_RECEIVE}
+   * @param oids set for the given direction, mutated in place
+   * @param defaultOids the driver's built-in default set for this direction, used by {@code auto}
+   * @param isSend whether this is the send direction ({@code force} is accepted for send only)
+   * @param disabledOut collects the OIDs set to {@code disable}, so later opt-ins such as
+   *     {@code addDataType} keep honouring them, like the legacy {@code binaryTransferDisable}
+   * @throws PSQLException if an OID, a mode, or the {@code oid:mode} syntax is invalid,
+   *     or if the same OID appears more than once
+   */
+  private static void applyBinaryDirectionOverrides(Properties info, PGProperty property,
+      Set<Integer> oids, Set<Integer> defaultOids, boolean isSend, Set<Integer> disabledOut)
+      throws PSQLException {
+    String spec = property.getOrDefault(info);
+    if (spec == null || spec.isEmpty()) {
+      return;
+    }
+    Set<Integer> seen = new HashSet<>();
+    StringTokenizer tokenizer = new StringTokenizer(spec, ",");
+    while (tokenizer.hasMoreTokens()) {
+      String entry = tokenizer.nextToken().trim();
+      int colon = entry.indexOf(':');
+      if (colon < 0) {
+        throw new PSQLException(
+            GT.tr("Invalid value \"{0}\" for property {1}: expected oid:mode entries.",
+                entry, property.getName()),
+            PSQLState.INVALID_PARAMETER_VALUE);
+      }
+      int oid = Oid.valueOf(entry.substring(0, colon).trim());
+      String mode = entry.substring(colon + 1).trim();
+      if (!seen.add(oid)) {
+        throw new PSQLException(
+            GT.tr("Duplicate type \"{0}\" in property {1}.",
+                Oid.toString(oid), property.getName()),
+            PSQLState.INVALID_PARAMETER_VALUE);
+      }
+      if ("auto".equalsIgnoreCase(mode)) {
+        // Reset to the driver's built-in default for this direction, overriding any legacy
+        // binaryTransfer* setting — including binaryTransferDisable, so the type must also
+        // leave the disabled set (consulted by addDataType and other later opt-ins).
+        disabledOut.remove(oid);
+        if (defaultOids.contains(oid)) {
+          oids.add(oid);
+        } else {
+          oids.remove(oid);
+        }
+      } else if ("disable".equalsIgnoreCase(mode)) {
+        oids.remove(oid);
+        disabledOut.add(oid);
+      } else if (isSend && "force".equalsIgnoreCase(mode)) {
+        oids.add(oid);
+      } else {
+        throw new PSQLException(
+            GT.tr("Invalid mode \"{0}\" for type \"{1}\" in property {2}. Allowed modes are {3}.",
+                mode, Oid.toString(oid), property.getName(),
+                isSend ? "auto, force, disable" : "auto, disable"),
+            PSQLState.INVALID_PARAMETER_VALUE);
+      }
+    }
+  }
+
   private static String oidsToString(Set<Integer> oids) {
     StringBuilder sb = new StringBuilder();
     for (Integer oid : oids) {
@@ -840,11 +924,15 @@ public void addDataType(String type, Class<? extends PGobject> klass) throws SQL
     if (PGBinaryObject.class.isAssignableFrom(klass) && getPreferQueryMode() != PreferQueryMode.SIMPLE) {
       // try to get an oid for this type (will return 0 if the type does not exist in the database)
       int oid = typeCache.getPGType(type);
-      // check if oid is there and if it is not disabled for binary transfer
-      if (oid > 0 && !binaryDisabledOids.contains(oid)) {
-        // allow using binary transfer for receiving and sending of this type
-        queryExecutor.addBinaryReceiveOid(oid);
-        queryExecutor.addBinarySendOid(oid);
+      // check if oid is there and honour the per-direction disabled sets (which already fold in the
+      // legacy binaryTransferDisable set) so a disabled type is not silently re-enabled here
+      if (oid > 0) {
+        if (!binaryReceiveDisabledOids.contains(oid)) {
+          queryExecutor.addBinaryReceiveOid(oid);
+        }
+        if (!binarySendDisabledOids.contains(oid)) {
+          queryExecutor.addBinarySendOid(oid);
+        }
       }
     }
   }
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/BinaryDirectionPropertiesTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/BinaryDirectionPropertiesTest.java
new file mode 100644
index 0000000000..3a6c0a337b
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/BinaryDirectionPropertiesTest.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.jdbc2;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.postgresql.PGProperty;
+import org.postgresql.core.BaseConnection;
+import org.postgresql.core.Oid;
+import org.postgresql.core.QueryExecutor;
+import org.postgresql.test.TestUtil;
+import org.postgresql.util.PSQLState;
+
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Properties;
+
+/**
+ * Tests for the per-direction, per-type {@code binarySend} and {@code binaryReceive} properties,
+ * including their precedence over the legacy {@code binaryTransfer*} properties.
+ */
+class BinaryDirectionPropertiesTest {
+
+  private static QueryExecutor executorOf(Connection con) throws SQLException {
+    return con.unwrap(BaseConnection.class).getQueryExecutor();
+  }
+
+  @Test
+  void forceAddsSendBinaryForType() throws SQLException {
+    Properties props = new Properties();
+    // Start from nothing so the only send-binary type is the one we force.
+    PGProperty.BINARY_TRANSFER.set(props, false);
+    PGProperty.BINARY_SEND.set(props, "int4:force");
+    try (Connection con = TestUtil.openDB(props)) {
+      QueryExecutor executor = executorOf(con);
+      assertTrue(executor.useBinaryForSend(Oid.INT4), "int4:force should enable binary send");
+      assertFalse(executor.useBinaryForReceive(Oid.INT4),
+          "binaryReceive untouched and binaryTransfer=false, so receive stays text");
+    }
+  }
+
+  @Test
+  void forceAcceptsNumericOid() throws SQLException {
+    Properties props = new Properties();
+    PGProperty.BINARY_TRANSFER.set(props, false);
+    // 23 is the OID of int4.
+    PGProperty.BINARY_SEND.set(props, "23:force");
+    try (Connection con = TestUtil.openDB(props)) {
+      assertTrue(executorOf(con).useBinaryForSend(Oid.INT4),
+          "numeric OID 23:force should enable binary send for int4");
+    }
+  }
+
+  @Test
+  void disableRemovesReceiveBinaryFromDefault() throws SQLException {
+    Properties props = new Properties();
+    // int4 is a default binary type; disable it on receive only.
+    PGProperty.BINARY_RECEIVE.set(props, "int4:disable");
+    try (Connection con = TestUtil.openDB(props)) {
+      QueryExecutor executor = executorOf(con);
+      assertFalse(executor.useBinaryForReceive(Oid.INT4),
+          "int4:disable should turn off binary receive");
+      assertTrue(executor.useBinaryForSend(Oid.INT4),
+          "send direction is untouched and stays at the default");
+    }
+  }
+
+  @Test
+  void autoKeepsDriverDefault() throws SQLException {
+    Properties props = new Properties();
+    PGProperty.BINARY_SEND.set(props, "int4:auto");
+    PGProperty.BINARY_RECEIVE.set(props, "int4:auto");
+    try (Connection con = TestUtil.openDB(props)) {
+      QueryExecutor executor = executorOf(con);
+      assertTrue(executor.useBinaryForSend(Oid.INT4), "auto keeps the default (binary) for send");
+      assertTrue(executor.useBinaryForReceive(Oid.INT4),
+          "auto keeps the default (binary) for receive");
+    }
+  }
+
+  @Test
+  void autoResetsLegacyEnableToDefault() throws SQLException {
+    Properties props = new Properties();
+    // bool is not a default binary type, so the legacy enable is what turns send on.
+    PGProperty.BINARY_TRANSFER_ENABLE.set(props, "bool");
+    PGProperty.BINARY_SEND.set(props, "bool:auto");
+    try (Connection con = TestUtil.openDB(props)) {
+      QueryExecutor executor = executorOf(con);
+      assertFalse(executor.useBinaryForSend(Oid.BOOL),
+          "auto should override binaryTransferEnable and reset bool to its default (text) for send");
+      assertTrue(executor.useBinaryForReceive(Oid.BOOL),
+          "receive has no override, so binaryTransferEnable=bool still applies");
+    }
+  }
+
+  @Test
+  void autoResetsLegacyDisableToDefault() throws SQLException {
+    Properties props = new Properties();
+    // int4 is a default binary type; the legacy disable turns it off.
+    PGProperty.BINARY_TRANSFER_DISABLE.set(props, "int4");
+    PGProperty.BINARY_RECEIVE.set(props, "int4:auto");
+    try (Connection con = TestUtil.openDB(props)) {
+      QueryExecutor executor = executorOf(con);
+      assertTrue(executor.useBinaryForReceive(Oid.INT4),
+          "auto should override binaryTransferDisable and reset int4 to its default (binary)");
+      assertFalse(executor.useBinaryForSend(Oid.INT4),
+          "send has no override, so binaryTransferDisable=int4 still applies");
+    }
+  }
+
+  @Test
+  void perDirectionForceWinsOverLegacyDisable() throws SQLException {
+    Properties props = new Properties();
+    PGProperty.BINARY_TRANSFER_DISABLE.set(props, "int4");
+    PGProperty.BINARY_SEND.set(props, "int4:force");
+    try (Connection con = TestUtil.openDB(props)) {
+      QueryExecutor executor = executorOf(con);
+      assertTrue(executor.useBinaryForSend(Oid.INT4),
+          "binarySend=int4:force should override binaryTransferDisable=int4 for send");
+      assertFalse(executor.useBinaryForReceive(Oid.INT4),
+          "binaryTransferDisable still applies to receive, which has no override");
+    }
+  }
+
+  @Test
+  void perDirectionDisableWinsOverLegacyEnable() throws SQLException {
+    Properties props = new Properties();
+    // bool is not a default binary type, so binaryTransferEnable is what turns it on.
+    PGProperty.BINARY_TRANSFER_ENABLE.set(props, "bool");
+    PGProperty.BINARY_RECEIVE.set(props, "bool:disable");
+    try (Connection con = TestUtil.openDB(props)) {
+      QueryExecutor executor = executorOf(con);
+      assertFalse(executor.useBinaryForReceive(Oid.BOOL),
+          "binaryReceive=bool:disable should override binaryTransferEnable=bool for receive");
+      assertTrue(executor.useBinaryForSend(Oid.BOOL),
+          "send direction keeps binaryTransferEnable=bool");
+    }
+  }
+
+  @Test
+  void forceIsRejectedForReceive() {
+    assertInvalidParameterValue("binaryReceive", "int4:force",
+        "force is not a valid mode for binaryReceive");
+  }
+
+  @Test
+  void unknownModeIsRejected() {
+    assertInvalidParameterValue("binarySend", "int4:bogus", "an unknown mode should be rejected");
+  }
+
+  @Test
+  void missingColonIsRejected() {
+    assertInvalidParameterValue("binarySend", "int4",
+        "an entry without oid:mode syntax should be rejected");
+  }
+
+  @Test
+  void duplicateTypeIsRejected() {
+    assertInvalidParameterValue("binarySend", "int4:force,int4:disable",
+        "the same type listed twice in one direction should be rejected");
+  }
+
+  /**
+   * Asserts that connecting with the given binary direction property fails with
+   * {@link PSQLState#INVALID_PARAMETER_VALUE}, so the test cannot pass on an unrelated
+   * connection error.
+   */
+  private static void assertInvalidParameterValue(String property, String value, String message) {
+    Properties props = new Properties();
+    props.setProperty(property, value);
+    SQLException e = assertThrows(SQLException.class, () -> TestUtil.openDB(props).close(), message);
+    assertEquals(PSQLState.INVALID_PARAMETER_VALUE.getState(), e.getSQLState(), message);
+  }
+}


reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: github://pgjdbc/pgjdbc
  Cc: [email protected], [email protected]
  Subject: Re: [pgjdbc/pgjdbc] PR #4195: feat(jdbc): add direction-aware binarySend/binaryReceive properties
  In-Reply-To: <<[email protected]>>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox