Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-added support for stored procedure 'exec' escape syntax in CallableStatements #2325

Merged
merged 12 commits into from
Feb 15, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.util.Map.Entry;
import java.util.Vector;
import java.util.logging.Level;
import java.util.regex.Pattern;

import com.microsoft.sqlserver.jdbc.SQLServerConnection.CityHash128Key;
import com.microsoft.sqlserver.jdbc.SQLServerConnection.PreparedStatementHandle;
Expand Down Expand Up @@ -70,6 +71,10 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS
/** Processed SQL statement text, may not be same as what user initially passed. */
final String userSQL;

private boolean isExecEscapeSyntax;

private boolean isCallEscapeSyntax;

/** Parameter positions in processed SQL statement text. */
final int[] userSQLParamPositions;

Expand Down Expand Up @@ -128,6 +133,16 @@ private void setPreparedStatementHandle(int handle) {
*/
private boolean useBulkCopyForBatchInsert;

/**
* Regex for JDBC 'call' escape syntax
*/
private static final Pattern callEscapePattern = Pattern.compile("^\\s*(?i)\\{(\\s*\\??\\s*=?\\s*)call (.+)\\s*\\(?\\?*,?\\)?\\s*}\\s*$");

/**
* Regex for 'exec' escape syntax
*/
private static final Pattern execEscapePattern = Pattern.compile("^\\s*(?i)(?:exec|execute)\\b");

/** Returns the prepared statement SQL */
@Override
public String toString() {
Expand Down Expand Up @@ -253,6 +268,8 @@ private boolean resetPrepStmtHandle(boolean discardCurrentCacheItem) {
procedureName = parsedSQL.procedureName;
bReturnValueSyntax = parsedSQL.bReturnValueSyntax;
userSQL = parsedSQL.processedSQL;
isExecEscapeSyntax = isExecEscapeSyntax(sql);
isCallEscapeSyntax = isCallEscapeSyntax(sql);
userSQLParamPositions = parsedSQL.parameterPositions;
initParams(userSQLParamPositions.length);
useBulkCopyForBatchInsert = conn.getUseBulkCopyForBatchInsert();
Expand Down Expand Up @@ -1210,7 +1227,15 @@ else if (needsPrepare && !connection.getEnablePrepareOnFirstPreparedStatementCal
*/
boolean callRPCDirectly(Parameter[] params) throws SQLServerException {
int paramCount = SQLServerConnection.countParams(userSQL);
return (null != procedureName && paramCount != 0 && !isTVPType(params));

// In order to execute sprocs directly the following must be true:
// 1. There must be a sproc name
// 2. There must be parameters
// 3. Parameters must not be a TVP type
// 4. Compliant CALL escape syntax
// If isExecEscapeSyntax is true, EXEC escape syntax is used then use prior behaviour to
// execute the procedure
return (null != procedureName && paramCount != 0 && !isTVPType(params) && isCallEscapeSyntax && !isExecEscapeSyntax);
}

/**
Expand All @@ -1230,6 +1255,14 @@ private boolean isTVPType(Parameter[] params) throws SQLServerException {
return false;
}

private boolean isExecEscapeSyntax(String sql) {
return execEscapePattern.matcher(sql).find();
}

private boolean isCallEscapeSyntax(String sql) {
return callEscapePattern.matcher(sql).find();
}

/**
* Executes sp_prepare to prepare a parameterized statement and sets the prepared statement handle
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public class CallableStatementTest extends AbstractTest {

/**
* Setup before test
*
*
* @throws SQLException
*/
@BeforeAll
Expand Down Expand Up @@ -201,7 +201,7 @@ public void testCallableStatementSpPrepare() throws SQLException {

/**
* Tests CallableStatement.getString() with uniqueidentifier parameter
*
*
* @throws SQLException
*/
@Test
Expand All @@ -226,7 +226,7 @@ public void getStringGUIDTest() throws SQLException {

/**
* test for setNull(index, varchar) to behave as setNull(index, nvarchar) when SendStringParametersAsUnicode is true
*
*
* @throws SQLException
*/
@Test
Expand Down Expand Up @@ -302,7 +302,7 @@ public void testGetObjectAsLocalDateTime() throws SQLException {

/**
* Tests getObject(n, java.time.OffsetDateTime.class) and getObject(n, java.time.OffsetTime.class).
*
*
* @throws SQLException
*/
@Test
Expand Down Expand Up @@ -332,7 +332,7 @@ public void testGetObjectAsOffsetDateTime() throws SQLException {

/**
* recognize parameter names with and without leading '@'
*
*
* @throws SQLException
*/
@Test
Expand Down Expand Up @@ -1067,9 +1067,95 @@ public void testRegisteringOutputByIndexandAcquiringOutputParamByName() throws S
}
}

@Test
public void testExecuteSystemStoredProcedureNamedParametersAndIndexedParameterNoResultset() throws SQLException {
String call0 = "EXEC sp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'";
String call1 = "\rEXEC\r\rsp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'";
String call2 = " EXEC sp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'";
String call3 = "\tEXEC\t\t\tsp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'";

try (CallableStatement cstmt0 = connection.prepareCall(call0);
CallableStatement cstmt1 = connection.prepareCall(call1);
CallableStatement cstmt2 = connection.prepareCall(call2);
CallableStatement cstmt3 = connection.prepareCall(call3);) {
cstmt0.setString(1, "Resource-" + UUID.randomUUID());
cstmt0.execute();

cstmt1.setString(1, "Resource-" + UUID.randomUUID());
cstmt1.execute();

cstmt2.setString(1, "Resource-" + UUID.randomUUID());
cstmt2.execute();

cstmt3.setString(1, "Resource-" + UUID.randomUUID());
cstmt3.execute();
}
}

@Test
public void testExecSystemStoredProcedureNamedParametersAndIndexedParameterResultSet() throws SQLException {
String call = "exec sp_sproc_columns_100 ?, @ODBCVer=3, @fUsePattern=0";

try (CallableStatement cstmt = connection.prepareCall(call)) {
cstmt.setString(1, "sp_getapplock");

try (ResultSet rs = cstmt.executeQuery()) {
while (rs.next()) {
assertTrue(TestResource.getResource("R_resultSetEmpty"), !rs.getString(4).isEmpty());
}
}
}
}

@Test
public void testExecSystemStoredProcedureNoIndexedParametersResultSet() throws SQLException {
String call = "execute sp_sproc_columns_100 sp_getapplock, @ODBCVer=3, @fUsePattern=0";

try (CallableStatement cstmt = connection.prepareCall(call);
ResultSet rs = cstmt.executeQuery()) {
while (rs.next()) {
assertTrue(TestResource.getResource("R_resultSetEmpty"), !rs.getString(4).isEmpty());
}
}
}

@Test
public void testExecDocumentedSystemStoredProceduresIndexedParameters() throws SQLException {
String serverName;
String testTableName = "testTable";
Integer integer = new Integer(1);

try (Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT @@SERVERNAME")) {
rs.next();
serverName = rs.getString(1);
}

String[] sprocs = {"EXEC sp_column_privileges ?",
"exec sp_catalogs ?", "execute sp_column_privileges ?", "EXEC sp_column_privileges_ex ?",
"EXECUTE sp_columns ?", "execute sp_datatype_info ?",
"EXEC sp_sproc_columns ?", "EXECUTE sp_server_info ?", "exec sp_special_columns ?",
"execute sp_statistics ?", "EXEC sp_table_privileges ?", "exec sp_tables ?"};

Object[] params = {testTableName, serverName, testTableName, serverName,
testTableName, integer, "sp_column_privileges", integer, testTableName,
testTableName, testTableName, testTableName};

int paramIndex = 0;

for (String sproc : sprocs) {
try (CallableStatement cstmt = connection.prepareCall(sproc)) {
cstmt.setObject(1, params[paramIndex]);
cstmt.execute();
paramIndex++;
} catch (Exception e) {
fail("Failed executing '" + sproc + "' with indexed parameter '" + params[paramIndex]);
}
}
}

/**
* Cleanup after test
*
*
* @throws SQLException
*/
@AfterAll
Expand Down
Loading