Repository: pgjdbc/pgjdbc
Pull request #1561 (2019-09-05 12:52:19)

fix: allow OUT parameter registration when using CallableStatement na…
…tive CALL

Currently, using CallableStatement, the PGJDBC driver will allow invocation of procedures using
the native "call myproc(...)" syntax, provided the procedure only has IN parameters or no
parameters.
However, if the procedure has any INOUT parameters, then an attempt to invoke the
CallableStatement's registerOutParameter() method, which is required in order to register the OUT
part of the INOUT parameter, will fail with the following error:
   This statement does not declare an OUT parameter.  Use { ?= call ... } to declare one.

This fixes issue: https://github.com/pgjdbc/pgjdbc/issues/1545

AppVeyorBot: :x: [Build pgjdbc 1.0.432 failed](https://ci.appveyor.com/project/davecramer/pgjdbc/builds/27204435) (commit https://github.com/pgjdbc/pgjdbc/commit/b9f04cbeb7 by @gregn123)
AppVeyorBot: :x: [Build pgjdbc 1.0.434 failed](https://ci.appveyor.com/project/davecramer/pgjdbc/builds/27207761) (commit https://github.com/pgjdbc/pgjdbc/commit/215fb8f29b by @gregn123)
codecov-io: # [Codecov](https://codecov.io/gh/pgjdbc/pgjdbc/pull/1561?src=pr&el=h1) Report
> Merging [#1561](https://codecov.io/gh/pgjdbc/pgjdbc/pull/1561?src=pr&el=desc) into [master](https://codecov.io/gh/pgjdbc/pgjdbc/commit/8495127fe0450f94923251f67ab8e2208319437f?src=pr&el=desc) will **decrease** coverage by `4.03%`.
> The diff coverage is `37.5%`.



```diff
@@             Coverage Diff              @@
##             master    #1561      +/-   ##
============================================
- Coverage     68.92%   64.88%   -4.04%     
+ Complexity     3971     3729     -242     
============================================
  Files           179      179              
  Lines         16545    16553       +8     
  Branches       2695     2699       +4     
============================================
- Hits          11403    10740     -663     
- Misses         3890     4555     +665     
- Partials       1252     1258       +6
```

AppVeyorBot: :white_check_mark: [Build pgjdbc 1.0.542 completed](https://ci.appveyor.com/project/davecramer/pgjdbc/builds/29116580) (commit https://github.com/pgjdbc/pgjdbc/commit/8a3bf60ef7 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..139455a5 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.
@@ -1040,6 +1042,21 @@ public class Parser {
     if (i == len && !syntaxError) {
       if (state == 1) {
         // Not an escaped syntax.
+
+        // Detect PostgreSQL native CALL.
+        // (OUT parameter registration, needed for stored procedures with INOUT arguments, will fail without this)
+        i = 0;
+        while (i < len && Character.isWhitespace(jdbcSql.charAt(i))) {
+          i++; // skip any preceding whitespace
+        }
+        if (i < len - 5) { // 5 == length of "call" + 1 whitespace
+          //Check for CALL followed by whitespace
+          char ch = jdbcSql.charAt(i);
+          if ((ch == 'c' || ch == 'C') && jdbcSql.substring(i, i + 4).equalsIgnoreCase("call")
+               && Character.isWhitespace(jdbcSql.charAt(i + 4))) {
+            isFunction = true;
+          }
+        }
         return new JdbcCallParseInfo(sql, isFunction);
       }
       if (state != 8) {
@@ -1053,8 +1070,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 +1118,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/Jdbc3CallableStatementTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc3/Jdbc3CallableStatementTest.java
index 57643034..f54e1a58 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc3/Jdbc3CallableStatementTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc3/Jdbc3CallableStatementTest.java
@@ -11,6 +11,7 @@ import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import org.postgresql.core.ServerVersion;
 import org.postgresql.test.TestUtil;
 import org.postgresql.test.jdbc2.BaseTest4;
 import org.postgresql.util.PSQLState;
@@ -84,6 +85,12 @@ public class Jdbc3CallableStatementTest extends BaseTest4 {
         + "end;'"
         + "LANGUAGE plpgsql VOLATILE;"
     );
+    if (TestUtil.haveMinimumServerVersion(con, ServerVersion.v11)) {
+      stmt.execute(
+          "CREATE OR REPLACE PROCEDURE inonlyprocedure(a IN int) AS 'BEGIN NULL; END;' LANGUAGE plpgsql");
+      stmt.execute(
+          "CREATE OR REPLACE PROCEDURE inoutprocedure(a INOUT int) AS 'BEGIN a := a + a; END;' LANGUAGE plpgsql");
+    }
   }
 
   @Override
@@ -97,6 +104,10 @@ public class Jdbc3CallableStatementTest extends BaseTest4 {
     stmt.execute("drop function myif(a INOUT int, b IN int)");
     stmt.execute("drop function mynoparams()");
     stmt.execute("drop function mynoparamsproc()");
+    if (TestUtil.haveMinimumServerVersion(con, ServerVersion.v11)) {
+      stmt.execute("drop procedure inonlyprocedure(a IN int)");
+      stmt.execute("drop procedure inoutprocedure(a INOUT int)");
+    }
     stmt.close();
     super.tearDown();
   }
@@ -1034,4 +1045,26 @@ public class Jdbc3CallableStatementTest extends BaseTest4 {
     TestUtil.closeQuietly(cs);
   }
 
+  @Test
+  public void testProcedureInOnlyNativeCall() throws SQLException {
+    assumeMinimumServerVersion(ServerVersion.v11);
+    assumeCallableStatementsSupported();
+    CallableStatement cs = con.prepareCall("call inonlyprocedure(?)");
+    cs.setInt(1, 5);
+    cs.execute();
+    TestUtil.closeQuietly(cs);
+  }
+
+  @Test
+  public void testProcedureInOutNativeCall() throws SQLException {
+    assumeMinimumServerVersion(ServerVersion.v11);
+    assumeCallableStatementsSupported();
+    // inoutprocedure(a INOUT int) returns a*2 via the INOUT parameter
+    CallableStatement cs = con.prepareCall("call inoutprocedure(?)");
+    cs.setInt(1, 5);
+    cs.registerOutParameter(1, Types.INTEGER);
+    cs.execute();
+    assertEquals("call inoutprocedure(?) should return 10 (when input param = 5) via the INOUT parameter, but did not.", 10, cs.getInt(1));
+    TestUtil.closeQuietly(cs);
+  }
 }
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,