Repository: pgjdbc/pgjdbc
Pull request #1560 (2019-09-05 07:34:29)

New "escapeSyntaxCallMode" connection property
The new "escapeSyntaxCallMode" connection property allows you to specify how the driver transforms JDBC escape call syntax into underlying SQL, for invoking stored procedures or functions. (backend >= 11).
The possible values of this property are: select, callIfNoReturn, or call
In escapeSyntaxCallMode=select mode (the default), the JDBC driver always uses a SELECT statement (allowing function invocation only).
In escapeSyntaxCallMode=callIfNoReturn mode, the JDBC driver uses a CALL statement (allowing procedure invocation) if there is no return parameter specified.
In escapeSyntaxCallMode=call mode, the JDBC driver always uses a CALL statement (allowing procedure invocation only).

Without the addition of this new connection property, PGJDBC will always use a SELECT statement for JDBC escape call syntax, which results in the following error if an attempt is made to invoke a procedure:
    ERROR: xxxx is a procedure  Hint: To call a procedure, use CALL


AppVeyorBot: :x: [Build pgjdbc 1.0.430 failed](https://ci.appveyor.com/project/davecramer/pgjdbc/builds/27197518) (commit https://github.com/pgjdbc/pgjdbc/commit/924c675d76 by @gregn123)
davecramer: Thanks for this. I think it would be useful to do an end to end test to test procedures.
Also there are 2 places where connection strings are documented; the README.md and https://jdbc.postgresql.org/documentation/head/connect.html#connection-parameters. These should reflect the new feature as well.
gregn123: Thanks, I'm working on those additional updates (and some corrections for Javadoc).
AppVeyorBot: :white_check_mark: [Build pgjdbc 1.0.438 completed](https://ci.appveyor.com/project/davecramer/pgjdbc/builds/27257503) (commit https://github.com/pgjdbc/pgjdbc/commit/92b67323db by @gregn123)
davecramer: @gregn123 sorry this has taken so long can you fix the conflict and I will push this.
AppVeyorBot: :white_check_mark: [Build pgjdbc 1.0.544 completed](https://ci.appveyor.com/project/davecramer/pgjdbc/builds/29123556) (commit https://github.com/pgjdbc/pgjdbc/commit/dae3f59cd4 by @gregn123)

Patch:
diff --git a/README.md b/README.md
index 4fcf6a0f..f2b9a159 100644
--- a/README.md
+++ b/README.md
@@ -151,6 +151,7 @@ In addition to the standard connection parameters the driver supports a number o
 | cleanupSavepoints             | Boolean | false   | In Autosave mode the driver sets a SAVEPOINT for every query. It is possible to exhaust the server shared buffers. Setting this to true will release each SAVEPOINT at the cost of an additional round trip. |
 | 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 |
 
 ## 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 e6df9ab2..4d163030 100644
--- a/docs/documentation/head/connect.md
+++ b/docs/documentation/head/connect.md
@@ -472,6 +472,16 @@ Connection conn = DriverManager.getConnection(url);
    for logical replication from that database. <p>Parameter should be use together with 
    `assumeMinServerVersion` with parameter >= 9.4 (backend >= 9.4)</p>
     
+* **escapeSyntaxCallMode** = String
+
+	Specifies how the driver transforms JDBC escape call syntax into underlying SQL, for invoking procedures or functions.
+	In `escapeSyntaxCallMode=select` mode (the default), the driver always uses a SELECT statement (allowing function invocation only).
+	In `escapeSyntaxCallMode=callIfNoReturn` mode, the driver uses a CALL statement (allowing procedure invocation) if there is no 
+	return parameter specified, otherwise the driver uses a SELECT statement.
+	In `escapeSyntaxCallMode=call` mode, the driver always uses a CALL statement (allowing procedure invocation only).
+
+	The default is `select` 
+
     
 <a name="unix sockets"></a>
 ## Unix sockets
diff --git a/pgjdbc/src/main/java/org/postgresql/PGProperty.java b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
index 87705c3a..42729c95 100644
--- a/pgjdbc/src/main/java/org/postgresql/PGProperty.java
+++ b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
@@ -446,6 +446,19 @@ public enum PGProperty {
           + "from that database. "
           + "(backend >= 9.4)"),
 
+  /**
+   * Specifies how the driver transforms JDBC escape call syntax into underlying SQL, for invoking procedures or functions. (backend &gt;= 11)
+   * In {@code escapeSyntaxCallMode=select} mode (the default), the driver always uses a SELECT statement (allowing function invocation only).
+   * In {@code escapeSyntaxCallMode=callIfNoReturn} mode, the driver uses a CALL statement (allowing procedure invocation) if there is no return parameter specified, otherwise the driver uses a SELECT statement.
+   * In {@code escapeSyntaxCallMode=call} mode, the driver always uses a CALL statement (allowing procedure invocation only).
+   */
+  ESCAPE_SYNTAX_CALL_MODE("escapeSyntaxCallMode", "select",
+      "Specifies how the driver transforms JDBC escape call syntax into underlying SQL, for invoking procedures or functions. (backend >= 11)"
+          + "In escapeSyntaxCallMode=select mode (the default), the driver always uses a SELECT statement (allowing function invocation only)."
+          + "In escapeSyntaxCallMode=callIfNoReturn mode, the driver uses a CALL statement (allowing procedure invocation) if there is no return parameter specified, otherwise the driver uses a SELECT statement."
+          + "In escapeSyntaxCallMode=call mode, the driver always uses a CALL statement (allowing procedure invocation only).",
+      false, "select", "callIfNoReturn", "call"),
+
   /**
    * Connection parameter to control behavior when
    * {@link Connection#setReadOnly(boolean)} is set to {@code true}.
diff --git a/pgjdbc/src/main/java/org/postgresql/core/CachedQueryCreateAction.java b/pgjdbc/src/main/java/org/postgresql/core/CachedQueryCreateAction.java
index df74aa2b..d5ad5c70 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/CachedQueryCreateAction.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/CachedQueryCreateAction.java
@@ -44,7 +44,7 @@ class CachedQueryCreateAction implements LruCache.CreateAction<Object, CachedQue
     if (key instanceof CallableQueryKey) {
       JdbcCallParseInfo callInfo =
           Parser.modifyJdbcCall(parsedSql, queryExecutor.getStandardConformingStrings(),
-              queryExecutor.getServerVersionNum(), queryExecutor.getProtocolVersion());
+              queryExecutor.getServerVersionNum(), queryExecutor.getProtocolVersion(), queryExecutor.getEscapeSyntaxCallMode());
       parsedSql = callInfo.getSql();
       isFunction = callInfo.isFunction();
     } else {
diff --git a/pgjdbc/src/main/java/org/postgresql/core/JdbcCallParseInfo.java b/pgjdbc/src/main/java/org/postgresql/core/JdbcCallParseInfo.java
index db9f8423..d7f70284 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/JdbcCallParseInfo.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/JdbcCallParseInfo.java
@@ -6,7 +6,7 @@
 package org.postgresql.core;
 
 /**
- * Contains parse flags from {@link Parser#modifyJdbcCall(String, boolean, int, int)}.
+ * Contains parse flags from {@link Parser#modifyJdbcCall(String, boolean, int, int, EscapeSyntaxCallMode)}.
  */
 public class JdbcCallParseInfo {
   private final String sql;
diff --git a/pgjdbc/src/main/java/org/postgresql/core/Parser.java b/pgjdbc/src/main/java/org/postgresql/core/Parser.java
index 265cd114..a05950e7 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/Parser.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/Parser.java
@@ -5,6 +5,7 @@
 
 package org.postgresql.core;
 
+import org.postgresql.jdbc.EscapeSyntaxCallMode;
 import org.postgresql.jdbc.EscapedFunctions2;
 import org.postgresql.util.GT;
 import org.postgresql.util.PSQLException;
@@ -894,15 +895,16 @@ public class Parser {
    * [?,..])] }} into the PostgreSQL format which is {@code select <some_function> (?, [?, ...]) as
    * result} or {@code select * from <some_function> (?, [?, ...]) as result} (7.3)
    *
-   * @param jdbcSql         sql text with JDBC escapes
-   * @param stdStrings      if backslash in single quotes should be regular character or escape one
-   * @param serverVersion   server version
-   * @param protocolVersion protocol version
+   * @param jdbcSql              sql text with JDBC escapes
+   * @param stdStrings           if backslash in single quotes should be regular character or escape one
+   * @param serverVersion        server version
+   * @param protocolVersion      protocol version
+   * @param escapeSyntaxCallMode mode specifying whether JDBC escape call syntax is transformed into a CALL/SELECT statement
    * @return SQL in appropriate for given server format
    * @throws SQLException if given SQL is malformed
    */
   public static JdbcCallParseInfo modifyJdbcCall(String jdbcSql, boolean stdStrings,
-      int serverVersion, int protocolVersion) throws SQLException {
+      int serverVersion, int protocolVersion, EscapeSyntaxCallMode escapeSyntaxCallMode) throws SQLException {
     // Mini-parser for JDBC function-call syntax (only)
     // TODO: Merge with escape processing (and parameter parsing?) so we only parse each query once.
     // RE: frequently used statements are cached (see {@link org.postgresql.jdbc.PgConnection#borrowQuery}), so this "merge" is not that important.
@@ -1053,8 +1055,16 @@ public class Parser {
           PSQLState.STATEMENT_NOT_ALLOWED_IN_FUNCTION_CALL);
     }
 
-    String prefix = "select * from ";
-    String suffix = " as result";
+    String prefix;
+    String suffix;
+    if (escapeSyntaxCallMode == EscapeSyntaxCallMode.SELECT || serverVersion < 110000
+        || (outParamBeforeFunc && escapeSyntaxCallMode == EscapeSyntaxCallMode.CALL_IF_NO_RETURN)) {
+      prefix = "select * from ";
+      suffix = " as result";
+    } else {
+      prefix = "call ";
+      suffix = "";
+    }
 
     String s = jdbcSql.substring(startIndex, endIndex);
     int prefixLength = prefix.length();
@@ -1093,7 +1103,11 @@ public class Parser {
       }
     }
 
-    sql = sb.append(suffix).toString();
+    if (!suffix.isEmpty()) {
+      sql = sb.append(suffix).toString();
+    } else {
+      sql = sb.toString();
+    }
     return new JdbcCallParseInfo(sql, isFunction);
   }
 
diff --git a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java
index a2d23823..b5c97244 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java
@@ -11,6 +11,7 @@ import org.postgresql.copy.CopyOperation;
 import org.postgresql.core.v3.TypeTransferModeRegistry;
 import org.postgresql.jdbc.AutoSave;
 import org.postgresql.jdbc.BatchResultHandler;
+import org.postgresql.jdbc.EscapeSyntaxCallMode;
 import org.postgresql.jdbc.PreferQueryMode;
 import org.postgresql.util.HostSpec;
 
@@ -433,6 +434,8 @@ public interface QueryExecutor extends TypeTransferModeRegistry {
 
   boolean isColumnSanitiserDisabled();
 
+  EscapeSyntaxCallMode getEscapeSyntaxCallMode();
+
   PreferQueryMode getPreferQueryMode();
 
   AutoSave getAutoSave();
diff --git a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java
index 1a74a7f7..82d636c4 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java
@@ -8,6 +8,7 @@ package org.postgresql.core;
 import org.postgresql.PGNotification;
 import org.postgresql.PGProperty;
 import org.postgresql.jdbc.AutoSave;
+import org.postgresql.jdbc.EscapeSyntaxCallMode;
 import org.postgresql.jdbc.PreferQueryMode;
 import org.postgresql.util.HostSpec;
 import org.postgresql.util.LruCache;
@@ -42,6 +43,7 @@ public abstract class QueryExecutorBase implements QueryExecutor {
   private TransactionState transactionState;
   private final boolean reWriteBatchedInserts;
   private final boolean columnSanitiserDisabled;
+  private final EscapeSyntaxCallMode escapeSyntaxCallMode;
   private final PreferQueryMode preferQueryMode;
   private AutoSave autoSave;
   private boolean flushCacheOnDeallocate = true;
@@ -68,6 +70,8 @@ public abstract class QueryExecutorBase implements QueryExecutor {
     this.cancelSignalTimeout = cancelSignalTimeout;
     this.reWriteBatchedInserts = PGProperty.REWRITE_BATCHED_INSERTS.getBoolean(info);
     this.columnSanitiserDisabled = PGProperty.DISABLE_COLUMN_SANITISER.getBoolean(info);
+    String callMode = PGProperty.ESCAPE_SYNTAX_CALL_MODE.get(info);
+    this.escapeSyntaxCallMode = EscapeSyntaxCallMode.of(callMode);
     String preferMode = PGProperty.PREFER_QUERY_MODE.get(info);
     this.preferQueryMode = PreferQueryMode.of(preferMode);
     this.autoSave = AutoSave.of(PGProperty.AUTOSAVE.get(info));
@@ -339,6 +343,11 @@ public abstract class QueryExecutorBase implements QueryExecutor {
     return columnSanitiserDisabled;
   }
 
+  @Override
+  public EscapeSyntaxCallMode getEscapeSyntaxCallMode() {
+    return escapeSyntaxCallMode;
+  }
+
   @Override
   public PreferQueryMode getPreferQueryMode() {
     return preferQueryMode;
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 8baa1fb9..e1a6df35 100644
--- a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
+++ b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
@@ -1128,6 +1128,22 @@ public abstract class BaseDataSource implements CommonDataSource, Referenceable
     PGProperty.REPLICATION.set(properties, replication);
   }
 
+  /**
+   * @return 'select', "callIfNoReturn', or 'call'
+   * @see PGProperty#ESCAPE_SYNTAX_CALL_MODE
+   */
+  public String getEscapeSyntaxCallMode() {
+    return PGProperty.ESCAPE_SYNTAX_CALL_MODE.get(properties);
+  }
+
+  /**
+   * @param callMode the call mode to use for JDBC escape call syntax
+   * @see PGProperty#ESCAPE_SYNTAX_CALL_MODE
+   */
+  public void setEscapeSyntaxCallMode(String callMode) {
+    PGProperty.ESCAPE_SYNTAX_CALL_MODE.set(properties, callMode);
+  }
+
   /**
    * @return null, 'database', or 'true
    * @see PGProperty#REPLICATION
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/EscapeSyntaxCallMode.java b/pgjdbc/src/main/java/org/postgresql/jdbc/EscapeSyntaxCallMode.java
new file mode 100644
index 00000000..00200b14
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/EscapeSyntaxCallMode.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2019, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.jdbc;
+
+/**
+ * <p>Specifies whether a SELECT/CALL statement is used for the underlying SQL for JDBC escape call syntax: 'select' means to
+ * always use SELECT, 'callIfNoReturn' means to use CALL if there is no return parameter (otherwise use SELECT), and 'call' means
+ * to always use CALL.</p>
+ *
+ * @see org.postgresql.PGProperty#ESCAPE_SYNTAX_CALL_MODE
+ */
+public enum EscapeSyntaxCallMode {
+  SELECT("select"),
+  CALL_IF_NO_RETURN("callIfNoReturn"),
+  CALL("call");
+
+  private final String value;
+
+  EscapeSyntaxCallMode(String value) {
+    this.value = value;
+  }
+
+  public static EscapeSyntaxCallMode of(String mode) {
+    for (EscapeSyntaxCallMode escapeSyntaxCallMode : values()) {
+      if (escapeSyntaxCallMode.value.equals(mode)) {
+        return escapeSyntaxCallMode;
+      }
+    }
+    return SELECT;
+  }
+
+  public String value() {
+    return value;
+  }
+}


Test patch:
diff --git a/pgjdbc/src/test/java/org/postgresql/core/ParserTest.java b/pgjdbc/src/test/java/org/postgresql/core/ParserTest.java
index bd5aadc1..34c8ca3a 100644
--- a/pgjdbc/src/test/java/org/postgresql/core/ParserTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/core/ParserTest.java
@@ -8,6 +8,8 @@ package org.postgresql.core;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import org.postgresql.jdbc.EscapeSyntaxCallMode;
+
 import org.junit.Assert;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -136,11 +138,22 @@ public class ParserTest {
 
   @Test
   public void testModifyJdbcCall() throws SQLException {
-    assertEquals("select * from pack_getValue(?) as result", Parser.modifyJdbcCall("{ ? = call pack_getValue}", true, ServerVersion.v9_6.getVersionNum(), 3).getSql());
-    assertEquals("select * from pack_getValue(?,?)  as result", Parser.modifyJdbcCall("{ ? = call pack_getValue(?) }", true, ServerVersion.v9_6.getVersionNum(), 3).getSql());
-    assertEquals("select * from pack_getValue(?) as result", Parser.modifyJdbcCall("{ ? = call pack_getValue()}", true, ServerVersion.v9_6.getVersionNum(), 3).getSql());
-    assertEquals("select * from pack_getValue(?,?,?,?)  as result", Parser.modifyJdbcCall("{ ? = call pack_getValue(?,?,?) }", true, ServerVersion.v9_6.getVersionNum(), 3).getSql());
-    assertEquals("select * from lower(?,?) as result", Parser.modifyJdbcCall("{ ? = call lower(?)}", true, ServerVersion.v9_6.getVersionNum(), 3).getSql());
+    assertEquals("select * from pack_getValue(?) as result", Parser.modifyJdbcCall("{ ? = call pack_getValue}", true, ServerVersion.v9_6.getVersionNum(), 3, EscapeSyntaxCallMode.SELECT).getSql());
+    assertEquals("select * from pack_getValue(?,?)  as result", Parser.modifyJdbcCall("{ ? = call pack_getValue(?) }", true, ServerVersion.v9_6.getVersionNum(), 3, EscapeSyntaxCallMode.SELECT).getSql());
+    assertEquals("select * from pack_getValue(?) as result", Parser.modifyJdbcCall("{ ? = call pack_getValue()}", true, ServerVersion.v9_6.getVersionNum(), 3, EscapeSyntaxCallMode.SELECT).getSql());
+    assertEquals("select * from pack_getValue(?,?,?,?)  as result", Parser.modifyJdbcCall("{ ? = call pack_getValue(?,?,?) }", true, ServerVersion.v9_6.getVersionNum(), 3, EscapeSyntaxCallMode.SELECT).getSql());
+    assertEquals("select * from lower(?,?) as result", Parser.modifyJdbcCall("{ ? = call lower(?)}", true, ServerVersion.v9_6.getVersionNum(), 3, EscapeSyntaxCallMode.SELECT).getSql());
+    assertEquals("select * from lower(?,?) as result", Parser.modifyJdbcCall("{ ? = call lower(?)}", true, ServerVersion.v9_6.getVersionNum(), 3, EscapeSyntaxCallMode.CALL_IF_NO_RETURN).getSql());
+    assertEquals("select * from lower(?,?) as result", Parser.modifyJdbcCall("{ ? = call lower(?)}", true, ServerVersion.v9_6.getVersionNum(), 3, EscapeSyntaxCallMode.CALL).getSql());
+    assertEquals("select * from lower(?,?) as result", Parser.modifyJdbcCall("{call lower(?,?)}", true, ServerVersion.v9_6.getVersionNum(), 3, EscapeSyntaxCallMode.SELECT).getSql());
+    assertEquals("select * from lower(?,?) as result", Parser.modifyJdbcCall("{call lower(?,?)}", true, ServerVersion.v9_6.getVersionNum(), 3, EscapeSyntaxCallMode.CALL_IF_NO_RETURN).getSql());
+    assertEquals("select * from lower(?,?) as result", Parser.modifyJdbcCall("{call lower(?,?)}", true, ServerVersion.v9_6.getVersionNum(), 3, EscapeSyntaxCallMode.CALL).getSql());
+    assertEquals("select * from lower(?,?) as result", Parser.modifyJdbcCall("{ ? = call lower(?)}", true, ServerVersion.v11.getVersionNum(), 3, EscapeSyntaxCallMode.SELECT).getSql());
+    assertEquals("select * from lower(?,?) as result", Parser.modifyJdbcCall("{ ? = call lower(?)}", true, ServerVersion.v11.getVersionNum(), 3, EscapeSyntaxCallMode.CALL_IF_NO_RETURN).getSql());
+    assertEquals("call lower(?,?)", Parser.modifyJdbcCall("{ ? = call lower(?)}", true, ServerVersion.v11.getVersionNum(), 3, EscapeSyntaxCallMode.CALL).getSql());
+    assertEquals("select * from lower(?,?) as result", Parser.modifyJdbcCall("{call lower(?,?)}", true, ServerVersion.v11.getVersionNum(), 3, EscapeSyntaxCallMode.SELECT).getSql());
+    assertEquals("call lower(?,?)", Parser.modifyJdbcCall("{call lower(?,?)}", true, ServerVersion.v11.getVersionNum(), 3, EscapeSyntaxCallMode.CALL_IF_NO_RETURN).getSql());
+    assertEquals("call lower(?,?)", Parser.modifyJdbcCall("{call lower(?,?)}", true, ServerVersion.v11.getVersionNum(), 3, EscapeSyntaxCallMode.CALL).getSql());
   }
 
   @Test
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc3/EscapeSyntaxCallModeBaseTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc3/EscapeSyntaxCallModeBaseTest.java
new file mode 100644
index 00000000..ae2db304
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc3/EscapeSyntaxCallModeBaseTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2019, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.jdbc3;
+
+import org.postgresql.core.ServerVersion;
+import org.postgresql.test.TestUtil;
+import org.postgresql.test.jdbc2.BaseTest4;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class EscapeSyntaxCallModeBaseTest extends BaseTest4 {
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    Statement stmt = con.createStatement();
+    stmt.execute(
+        "CREATE OR REPLACE FUNCTION myiofunc(a INOUT int, b OUT int) AS 'BEGIN b := a; a := 1; END;' LANGUAGE plpgsql");
+    stmt.execute(
+        "CREATE OR REPLACE FUNCTION mysumfunc(a int, b int) returns int AS 'BEGIN return a + b; END;' LANGUAGE plpgsql");
+    if (TestUtil.haveMinimumServerVersion(con, ServerVersion.v11)) {
+      stmt.execute(
+          "CREATE OR REPLACE PROCEDURE myioproc(a INOUT int, b INOUT int) AS 'BEGIN b := a; a := 1; END;' LANGUAGE plpgsql");
+    }
+  }
+
+  @Override
+  public void tearDown() throws SQLException {
+    Statement stmt = con.createStatement();
+    stmt.execute("drop function myiofunc(a INOUT int, b OUT int) ");
+    stmt.execute("drop function mysumfunc(a int, b int) ");
+    if (TestUtil.haveMinimumServerVersion(con, ServerVersion.v11)) {
+      stmt.execute("drop procedure myioproc(a INOUT int, b INOUT int) ");
+    }
+    stmt.close();
+    super.tearDown();
+  }
+
+}
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc3/EscapeSyntaxCallModeCallIfNoReturnTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc3/EscapeSyntaxCallModeCallIfNoReturnTest.java
new file mode 100644
index 00000000..3e629b48
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc3/EscapeSyntaxCallModeCallIfNoReturnTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2019, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.jdbc3;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.postgresql.PGProperty;
+import org.postgresql.core.ServerVersion;
+import org.postgresql.jdbc.EscapeSyntaxCallMode;
+import org.postgresql.util.PSQLState;
+
+import org.junit.Test;
+
+import java.sql.CallableStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.Properties;
+
+public class EscapeSyntaxCallModeCallIfNoReturnTest extends EscapeSyntaxCallModeBaseTest {
+
+  @Override
+  protected void updateProperties(Properties props) {
+    super.updateProperties(props);
+    PGProperty.ESCAPE_SYNTAX_CALL_MODE.set(props, EscapeSyntaxCallMode.CALL_IF_NO_RETURN.value());
+  }
+
+  @Test
+  public void testInvokeFunction() throws Throwable {
+    // escapeSyntaxCallMode=callIfNoReturn will cause a CALL statement to be used for the JDBC escape call
+    // syntax used below (since no return parameter is specified). "myiofunc" is a function, so the
+    // attempted invocation should fail.
+    assumeCallableStatementsSupported();
+    assumeMinimumServerVersion(ServerVersion.v11);
+    CallableStatement cs = con.prepareCall("{ call myiofunc(?,?) }");
+    cs.registerOutParameter(1, Types.INTEGER);
+    cs.registerOutParameter(2, Types.INTEGER);
+    cs.setInt(1, 10);
+    try {
+      cs.execute();
+      fail("Should throw an exception");
+    } catch (SQLException ex) {
+      assertTrue(ex.getSQLState().equalsIgnoreCase(PSQLState.WRONG_OBJECT_TYPE.getState()));
+    }
+  }
+
+  @Test
+  public void testInvokeFunctionHavingReturnParameter() throws Throwable {
+    // escapeSyntaxCallMode=callIfNoReturn will cause a SELECT statement to be used for the JDBC escape call
+    // syntax used below (since a return parameter is specified). "mysumfunc" is a function, so the
+    // invocation should succeed.
+    assumeCallableStatementsSupported();
+    CallableStatement cs = con.prepareCall("{ ? = call mysumfunc(?,?) }");
+    cs.registerOutParameter(1, Types.INTEGER);
+    cs.setInt(2, 10);
+    cs.setInt(3, 20);
+    cs.execute();
+    int ret = cs.getInt(1);
+    assertTrue("Expected mysumproc(10,20) to return 30 but returned " + ret, ret == 30);
+  }
+
+  @Test
+  public void testInvokeProcedure() throws Throwable {
+    // escapeSyntaxCallMode=callIfNoReturn will cause a CALL statement to be used for the JDBC escape call
+    // syntax used below (since there is no return parameter specified). "myioproc" is a procedure, so the
+    // invocation should succeed.
+    assumeCallableStatementsSupported();
+    assumeMinimumServerVersion(ServerVersion.v11);
+    CallableStatement cs = con.prepareCall("{call myioproc(?,?)}");
+    cs.registerOutParameter(1, Types.INTEGER);
+    cs.registerOutParameter(2, Types.INTEGER);
+    cs.setInt(1, 10);
+    cs.setInt(2, 20);
+    cs.execute();
+    // Expected output: a==1 (param 1), b==10 (param 2)
+    int a = cs.getInt(1);
+    int b = cs.getInt(2);
+    assertTrue("Expected myioproc() to return output parameter values 1,10 but returned " + a + "," + b, (a == 1 && b == 10));
+  }
+
+}
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc3/EscapeSyntaxCallModeCallTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc3/EscapeSyntaxCallModeCallTest.java
new file mode 100644
index 00000000..f575506c
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc3/EscapeSyntaxCallModeCallTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2019, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.jdbc3;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.postgresql.PGProperty;
+import org.postgresql.core.ServerVersion;
+import org.postgresql.jdbc.EscapeSyntaxCallMode;
+import org.postgresql.util.PSQLState;
+
+import org.junit.Test;
+
+import java.sql.CallableStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.Properties;
+
+public class EscapeSyntaxCallModeCallTest extends EscapeSyntaxCallModeBaseTest {
+
+  @Override
+  protected void updateProperties(Properties props) {
+    super.updateProperties(props);
+    PGProperty.ESCAPE_SYNTAX_CALL_MODE.set(props, EscapeSyntaxCallMode.CALL.value());
+  }
+
+  @Test
+  public void testInvokeFunction() throws Throwable {
+    // escapeSyntaxCallMode=call will cause a CALL statement to be used for the JDBC escape call
+    // syntax used below. "myiofunc" is a function, so the attempted invocation should fail.
+    assumeCallableStatementsSupported();
+    assumeMinimumServerVersion(ServerVersion.v11);
+    CallableStatement cs = con.prepareCall("{ call myiofunc(?,?) }");
+    cs.registerOutParameter(1, Types.INTEGER);
+    cs.registerOutParameter(2, Types.INTEGER);
+    cs.setInt(1, 10);
+    try {
+      cs.execute();
+      fail("Should throw an exception");
+    } catch (SQLException ex) {
+      assertTrue(ex.getSQLState().equalsIgnoreCase(PSQLState.WRONG_OBJECT_TYPE.getState()));
+    }
+  }
+
+  @Test
+  public void testInvokeFunctionHavingReturnParameter() throws Throwable {
+    // escapeSyntaxCallMode=call will cause a CALL statement to be used for the JDBC escape call
+    // syntax used below. "mysumfunc" is a function, so the attempted invocation should fail.
+    assumeCallableStatementsSupported();
+    assumeMinimumServerVersion(ServerVersion.v11);
+    CallableStatement cs = con.prepareCall("{ ? = call mysumfunc(?,?) }");
+    cs.registerOutParameter(1, Types.INTEGER);
+    cs.setInt(2, 10);
+    cs.setInt(3, 20);
+    try {
+      cs.execute();
+      fail("Should throw an exception");
+    } catch (SQLException ex) {
+      assertTrue(ex.getSQLState().equalsIgnoreCase(PSQLState.WRONG_OBJECT_TYPE.getState()));
+    }
+  }
+
+  @Test
+  public void testInvokeProcedure() throws Throwable {
+    // escapeSyntaxCallMode=call will cause a CALL statement to be used for the JDBC escape call
+    // syntax used below. "myioproc" is a procedure, so the invocation should succeed.
+    assumeCallableStatementsSupported();
+    assumeMinimumServerVersion(ServerVersion.v11);
+    CallableStatement cs = con.prepareCall("{call myioproc(?,?)}");
+    cs.registerOutParameter(1, Types.INTEGER);
+    cs.registerOutParameter(2, Types.INTEGER);
+    cs.setInt(1, 10);
+    cs.setInt(2, 20);
+    cs.execute();
+    // Expected output: a==1 (param 1), b==10 (param 2)
+    int a = cs.getInt(1);
+    int b = cs.getInt(2);
+    assertTrue("Expected myioproc() to return output parameter values 1,10 but returned " + a + "," + b, (a == 1 && b == 10));
+  }
+
+}
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc3/EscapeSyntaxCallModeSelectTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc3/EscapeSyntaxCallModeSelectTest.java
new file mode 100644
index 00000000..b36ff4ff
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc3/EscapeSyntaxCallModeSelectTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2019, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.jdbc3;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.postgresql.PGProperty;
+import org.postgresql.core.ServerVersion;
+import org.postgresql.jdbc.EscapeSyntaxCallMode;
+import org.postgresql.util.PSQLState;
+
+import org.junit.Test;
+
+import java.sql.CallableStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.Properties;
+
+public class EscapeSyntaxCallModeSelectTest extends EscapeSyntaxCallModeBaseTest {
+
+  @Override
+  protected void updateProperties(Properties props) {
+    super.updateProperties(props);
+    PGProperty.ESCAPE_SYNTAX_CALL_MODE.set(props, EscapeSyntaxCallMode.SELECT.value());
+  }
+
+  @Test
+  public void testInvokeFunction() throws Throwable {
+    // escapeSyntaxCallMode=select will cause a SELECT statement to be used for the JDBC escape call
+    // syntax used below. "myiofunc" is a function, so the invocation should succeed.
+    assumeCallableStatementsSupported();
+    CallableStatement cs = con.prepareCall("{ call myiofunc(?,?) }");
+    cs.registerOutParameter(1, Types.INTEGER);
+    cs.registerOutParameter(2, Types.INTEGER);
+    cs.setInt(1, 10);
+    cs.execute();
+    // Expected output: a==1 (param 1), b==10 (param 2)
+    int a = cs.getInt(1);
+    int b = cs.getInt(2);
+    assertTrue("Expected myiofunc() to return output parameter values 1,10 but returned " + a + "," + b, (a == 1 && b == 10));
+  }
+
+  @Test
+  public void testInvokeFunctionHavingReturnParameter() throws Throwable {
+    // escapeSyntaxCallMode=select will cause a SELECT statement to be used for the JDBC escape call
+    // syntax used below. "mysumfunc" is a function, so the invocation should succeed.
+    assumeCallableStatementsSupported();
+    CallableStatement cs = con.prepareCall("{ ? = call mysumfunc(?,?) }");
+    cs.registerOutParameter(1, Types.INTEGER);
+    cs.setInt(2, 10);
+    cs.setInt(3, 20);
+    cs.execute();
+    int ret = cs.getInt(1);
+    assertTrue("Expected mysumfunc(10,20) to return 30 but returned " + ret, ret == 30);
+  }
+
+  @Test
+  public void testInvokeProcedure() throws Throwable {
+    // escapeSyntaxCallMode=select will cause a SELECT statement to be used for the JDBC escape call
+    // syntax used below. "myioproc" is a procedure, so the attempted invocation should fail.
+    assumeCallableStatementsSupported();
+    assumeMinimumServerVersion(ServerVersion.v11);
+    CallableStatement cs = con.prepareCall("{call myioproc(?,?)}");
+    cs.registerOutParameter(1, Types.INTEGER);
+    cs.registerOutParameter(2, Types.INTEGER);
+    cs.setInt(1, 10);
+    cs.setInt(2, 20);
+    try {
+      cs.execute();
+      fail("Should throw an exception");
+    } catch (SQLException ex) {
+      assertTrue(ex.getSQLState().equalsIgnoreCase(PSQLState.WRONG_OBJECT_TYPE.getState()));
+    }
+  }
+
+}
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc3/Jdbc3TestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc3/Jdbc3TestSuite.java
index 5e41f7bd..2693cf7f 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc3/Jdbc3TestSuite.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc3/Jdbc3TestSuite.java
@@ -16,6 +16,9 @@ import org.junit.runners.Suite;
     CompositeQueryParseTest.class,
     CompositeTest.class,
     DatabaseMetaDataTest.class,
+    EscapeSyntaxCallModeCallTest.class,
+    EscapeSyntaxCallModeCallIfNoReturnTest.class,
+    EscapeSyntaxCallModeSelectTest.class,
     GeneratedKeysTest.class,
     Jdbc3BlobTest.class,
     Jdbc3CallableStatementTest.class,