Repository: pgjdbc/pgjdbc
Pull request #1617 (2019-11-25 20:26:23)

fix: remove currentTimeMillis from code, tests are OK

AppVeyorBot: :white_check_mark: [Build pgjdbc 1.0.541 completed](https://ci.appveyor.com/project/davecramer/pgjdbc/builds/29111696) (commit https://github.com/pgjdbc/pgjdbc/commit/db468e6314 by @davecramer)
codecov-io: # [Codecov](https://codecov.io/gh/pgjdbc/pgjdbc/pull/1617?src=pr&el=h1) Report
> Merging [#1617](https://codecov.io/gh/pgjdbc/pgjdbc/pull/1617?src=pr&el=desc) into [master](https://codecov.io/gh/pgjdbc/pgjdbc/commit/7b454355939aebd995b1b79598a1e945c168eb68?src=pr&el=desc) will **increase** coverage by `0.13%`.
> The diff coverage is `83.33%`.



```diff
@@             Coverage Diff              @@
##             master    #1617      +/-   ##
============================================
+ Coverage     68.94%   69.08%   +0.13%     
- Complexity     4032     4042      +10     
============================================
  Files           179      179              
  Lines         16750    16750              
  Branches       2740     2740              
============================================
+ Hits          11549    11571      +22     
+ Misses         3931     3915      -16     
+ Partials       1270     1264       -6
```


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/Driver.java b/pgjdbc/src/main/java/org/postgresql/Driver.java
index bd456b4d..3f6fbba9 100644
--- a/pgjdbc/src/main/java/org/postgresql/Driver.java
+++ b/pgjdbc/src/main/java/org/postgresql/Driver.java
@@ -61,7 +61,7 @@ public class Driver implements java.sql.Driver {
   private static final Logger LOGGER = Logger.getLogger("org.postgresql.Driver");
   private static SharedTimer sharedTimer = new SharedTimer();
   private static final String DEFAULT_PORT =
-      /*$"\""+mvn.project.property.template.default.pg.port+"\";"$*//*-*/"5431";
+      /*$"\""+mvn.project.property.template.default.pg.port+"\";"$*//*-*/"5432";
 
   static {
     try {
@@ -397,7 +397,7 @@ public class Driver implements java.sql.Driver {
      * @throws SQLException if a connection error occurs or the timeout is reached
      */
     public Connection getResult(long timeout) throws SQLException {
-      long expiry = System.currentTimeMillis() + timeout;
+      long expiry = System.nanoTime() / 1000 + timeout;
       synchronized (this) {
         while (true) {
           if (result != null) {
@@ -416,7 +416,7 @@ public class Driver implements java.sql.Driver {
             }
           }
 
-          long delay = expiry - System.currentTimeMillis();
+          long delay = expiry - System.nanoTime() / 1000;
           if (delay <= 0) {
             abandoned = true;
             throw new PSQLException(GT.tr("Connection attempt timed out."),
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/core/v3/QueryExecutorImpl.java b/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java
index 4e85f02a..c944ca03 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java
@@ -717,7 +717,7 @@ public class QueryExecutorImpl extends QueryExecutorBase {
     long startTime = 0;
     int oldTimeout = 0;
     if (useTimeout) {
-      startTime = System.currentTimeMillis();
+      startTime = System.nanoTime() / 1000;
       try {
         oldTimeout = pgStream.getSocket().getSoTimeout();
       } catch (SocketException e) {
@@ -747,7 +747,7 @@ public class QueryExecutorImpl extends QueryExecutorBase {
             SQLWarning warning = receiveNoticeResponse();
             addWarning(warning);
             if (useTimeout) {
-              long newTimeMillis = System.currentTimeMillis();
+              long newTimeMillis = System.nanoTime() / 1000;
               timeoutMillis += startTime - newTimeMillis; // Overflows after 49 days, ignore that
               startTime = newTimeMillis;
               if (timeoutMillis == 0) {
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/hostchooser/GlobalHostStatusTracker.java b/pgjdbc/src/main/java/org/postgresql/hostchooser/GlobalHostStatusTracker.java
index bd53ab93..0ba547bc 100644
--- a/pgjdbc/src/main/java/org/postgresql/hostchooser/GlobalHostStatusTracker.java
+++ b/pgjdbc/src/main/java/org/postgresql/hostchooser/GlobalHostStatusTracker.java
@@ -5,8 +5,6 @@
 
 package org.postgresql.hostchooser;
 
-import static java.lang.System.currentTimeMillis;
-
 import org.postgresql.util.HostSpec;
 
 import java.util.ArrayList;
@@ -28,7 +26,7 @@ public class GlobalHostStatusTracker {
    * @param hostStatus Latest known status for the host.
    */
   public static void reportHostStatus(HostSpec hostSpec, HostStatus hostStatus) {
-    long now = currentTimeMillis();
+    long now = System.nanoTime() / 1000;
     synchronized (hostStatusMap) {
       HostSpecStatus hostSpecStatus = hostStatusMap.get(hostSpec);
       if (hostSpecStatus == null) {
@@ -51,7 +49,7 @@ public class GlobalHostStatusTracker {
   static List<HostSpec> getCandidateHosts(HostSpec[] hostSpecs,
       HostRequirement targetServerType, long hostRecheckMillis) {
     List<HostSpec> candidates = new ArrayList<HostSpec>(hostSpecs.length);
-    long latestAllowedUpdate = currentTimeMillis() - hostRecheckMillis;
+    long latestAllowedUpdate = System.nanoTime() / 1000 - hostRecheckMillis;
     synchronized (hostStatusMap) {
       for (HostSpec hostSpec : hostSpecs) {
         HostSpecStatus hostInfo = hostStatusMap.get(hostSpec);
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,