Message-ID: From: "vlsi (@vlsi)" To: "pgjdbc/pgjdbc" Date: Wed, 17 Jun 2026 20:24:44 +0000 Subject: [pgjdbc/pgjdbc] PR #4195: feat(jdbc): add direction-aware binarySend/binaryReceive properties List-Id: X-GitHub-Additions: 379 X-GitHub-Author-Id: 213894 X-GitHub-Author-Login: vlsi X-GitHub-Base: master X-GitHub-Changed-Files: 6 X-GitHub-Commits: 1 X-GitHub-Deletions: 24 X-GitHub-Draft: true X-GitHub-Head-Branch: claude/focused-easley-c622a5 X-GitHub-Head-SHA: 0b6570ad1eed3500074e1d95df3e544246f2f8ff X-GitHub-Issue: 4195 X-GitHub-Labels: enhancement X-GitHub-Merge-SHA: 01cbc2bfb9bbff8b16d4a654d52905987fb4d2c8 X-GitHub-Repo: pgjdbc/pgjdbc X-GitHub-State: open X-GitHub-Type: pull_request X-GitHub-Url: https://github.com/pgjdbc/pgjdbc/pull/4195 Content-Type: text/plain; charset=utf-8 ## 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 binaryDisabledOids; + private final Set binarySendDisabledOids; + private final Set 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 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 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 legacyBinaryOids = getBinaryEnabledOids(info); + Set legacyDisabledOids = getBinaryDisabledOids(info); + legacyBinaryOids.removeAll(legacyDisabledOids); + Set useBinarySendForOids = new HashSet<>(legacyBinaryOids); + Set useBinaryReceiveForOids = new HashSet<>(legacyBinaryOids); - Set 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 sendDefaultOids = new HashSet<>(SUPPORTED_BINARY_OIDS); + sendDefaultOids.remove(Oid.DATE); + Set 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 sendDisabledOids = new HashSet<>(legacyDisabledOids); + Set 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 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 oids, Set defaultOids, boolean isSend, Set disabledOut) + throws PSQLException { + String spec = property.getOrDefault(info); + if (spec == null || spec.isEmpty()) { + return; + } + Set 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 oids) { StringBuilder sb = new StringBuilder(); for (Integer oid : oids) { @@ -840,11 +924,15 @@ public void addDataType(String type, Class 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); + } +}