Skip to content
Permalink
Browse files

feat: add new "escapeSyntaxCallMode" connection property (#1560)

The "escapeSyntaxCallMode" connection property allows you to specify how the driver transforms
JDBC escape call syntax into underlying SQL, for invoking procedures and functions.
The possible values of this property are: select, callIfNoReturn, or call
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).
Prior to the addition of this connection property, the driver always used a SELECT statement for
JDBC escape call syntax, which results in the following error when attempting to invoke a
procedure:
    ERROR: xxxx is a procedure  Hint: To call a procedure, use CALL
  • Loading branch information
gregn123 authored and davecramer committed Nov 26, 2019
1 parent 7b45435 commit d75591385538cd704a066c4ed026f767ce3784ab
@@ -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)
@@ -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
@@ -446,6 +446,19 @@
+ "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}.
@@ -44,7 +44,7 @@ public CachedQuery create(Object key) throws SQLException {
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 {
@@ -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;
@@ -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 @@ private static boolean subArraysEqual(final char[] arr,
* [?,..])] }} 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 static JdbcCallParseInfo modifyJdbcCall(String jdbcSql, boolean stdString
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 static JdbcCallParseInfo modifyJdbcCall(String jdbcSql, boolean stdString
}
}

sql = sb.append(suffix).toString();
if (!suffix.isEmpty()) {
sql = sb.append(suffix).toString();
} else {
sql = sb.toString();
}
return new JdbcCallParseInfo(sql, isFunction);
}

@@ -11,6 +11,7 @@
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 @@ Object createQueryKey(String sql, boolean escapeProcessing, boolean isParameteri

boolean isColumnSanitiserDisabled();

EscapeSyntaxCallMode getEscapeSyntaxCallMode();

PreferQueryMode getPreferQueryMode();

AutoSave getAutoSave();
@@ -8,6 +8,7 @@
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 @@
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 @@ protected QueryExecutorBase(PGStream pgStream, String user,
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 boolean isColumnSanitiserDisabled() {
return columnSanitiserDisabled;
}

@Override
public EscapeSyntaxCallMode getEscapeSyntaxCallMode() {
return escapeSyntaxCallMode;
}

@Override
public PreferQueryMode getPreferQueryMode() {
return preferQueryMode;
@@ -1128,6 +1128,22 @@ public void setReplication(String replication) {
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
@@ -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;
}
}
@@ -8,6 +8,8 @@
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 void testEscapeProcessing() throws Exception {

@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
@@ -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();
}

}

0 comments on commit d755913

Please sign in to comment.
You can’t perform that action at this time.