diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java index 40a77be15..5ad8689c8 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java @@ -1451,6 +1451,15 @@ public NClob getNClob(String parameterName) throws SQLException { if (paramNames != null) l = paramNames.size(); + // handle `@name` as well as `name`, since `@name` is what's returned + // by DatabaseMetaData#getProcedureColumns + String columnNameWithoutAtSign = null; + if (columnName.startsWith("@")) { + columnNameWithoutAtSign = columnName.substring(1, columnName.length()); + } else { + columnNameWithoutAtSign = columnName; + } + // In order to be as accurate as possible when locating parameter name // indexes, as well as be deterministic when running on various client // locales, we search for parameter names using the following scheme: @@ -1465,7 +1474,7 @@ public NClob getNClob(String parameterName) throws SQLException { for (i = 0; i < l; i++) { String sParam = paramNames.get(i); sParam = sParam.substring(1, sParam.length()); - if (sParam.equals(columnName)) { + if (sParam.equals(columnNameWithoutAtSign)) { matchPos = i; break; } @@ -1477,7 +1486,7 @@ public NClob getNClob(String parameterName) throws SQLException { for (i = 0; i < l; i++) { String sParam = paramNames.get(i); sParam = sParam.substring(1, sParam.length()); - if (sParam.equalsIgnoreCase(columnName)) { + if (sParam.equalsIgnoreCase(columnNameWithoutAtSign)) { matchPos = i; break; } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java index d0271714d..ce738ff2e 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java @@ -1,9 +1,12 @@ package com.microsoft.sqlserver.jdbc.callablestatement; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; +import java.sql.CallableStatement; import java.sql.Connection; import java.sql.DriverManager; +import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.sql.Types; @@ -17,6 +20,7 @@ import com.microsoft.sqlserver.jdbc.SQLServerCallableStatement; import com.microsoft.sqlserver.jdbc.SQLServerDataSource; +import com.microsoft.sqlserver.jdbc.SQLServerException; import com.microsoft.sqlserver.testframework.AbstractTest; import com.microsoft.sqlserver.testframework.Utils; @@ -28,6 +32,7 @@ public class CallableStatementTest extends AbstractTest { private static String tableNameGUID = "uniqueidentifier_Table"; private static String outputProcedureNameGUID = "uniqueidentifier_SP"; private static String setNullProcedureName = "CallableStatementTest_setNull_SP"; + private static String inputParamsProcedureName = "CallableStatementTest_inputParams_SP"; private static Connection connection = null; private static Statement stmt = null; @@ -45,10 +50,12 @@ public static void setupTest() throws SQLException { Utils.dropTableIfExists(tableNameGUID, stmt); Utils.dropProcedureIfExists(outputProcedureNameGUID, stmt); Utils.dropProcedureIfExists(setNullProcedureName, stmt); + Utils.dropProcedureIfExists(inputParamsProcedureName, stmt); createGUIDTable(); createGUIDStoredProcedure(); - createSetNullPreocedure(); + createSetNullProcedure(); + createInputParamsProcedure(); } /** @@ -133,6 +140,46 @@ public void getSetNullWithTypeVarchar() throws SQLException { } } + + /** + * recognize parameter names with and without leading '@' + * + * @throws SQLException + */ + @Test + public void inputParamsTest() throws SQLException { + String call = "{CALL " + inputParamsProcedureName + " (?,?)}"; + ResultSet rs = null; + + // the historical way: no leading '@', parameter names respected (not positional) + CallableStatement cs1 = connection.prepareCall(call); + cs1.setString("p2", "bar"); + cs1.setString("p1", "foo"); + rs = cs1.executeQuery(); + rs.next(); + assertEquals("foobar", rs.getString(1)); + + // the "new" way: leading '@', parameter names still respected (not positional) + CallableStatement cs2 = connection.prepareCall(call); + cs2.setString("@p2", "world!"); + cs2.setString("@p1", "Hello "); + rs = cs2.executeQuery(); + rs.next(); + assertEquals("Hello world!", rs.getString(1)); + + // sanity check: unrecognized parameter name + CallableStatement cs3 = connection.prepareCall(call); + try { + cs3.setString("@whatever", "junk"); + fail("SQLServerException should have been thrown"); + } catch (SQLServerException sse) { + if (!sse.getMessage().startsWith("Parameter @whatever was not defined")) { + fail("Unexpected content in exception message"); + } + } + + } + /** * Cleanup after test * @@ -143,6 +190,7 @@ public static void cleanup() throws SQLException { Utils.dropTableIfExists(tableNameGUID, stmt); Utils.dropProcedureIfExists(outputProcedureNameGUID, stmt); Utils.dropProcedureIfExists(setNullProcedureName, stmt); + Utils.dropProcedureIfExists(inputParamsProcedureName, stmt); if (null != stmt) { stmt.close(); @@ -162,7 +210,20 @@ private static void createGUIDTable() throws SQLException { stmt.execute(sql); } - private static void createSetNullPreocedure() throws SQLException { + private static void createSetNullProcedure() throws SQLException { stmt.execute("create procedure " + setNullProcedureName + " (@p1 nvarchar(255), @p2 nvarchar(255) output) as select @p2=@p1 return 0"); } + + private static void createInputParamsProcedure() throws SQLException { + String sql = + "CREATE PROCEDURE [dbo].[CallableStatementTest_inputParams_SP] " + + " @p1 nvarchar(max) = N'parameter1', " + + " @p2 nvarchar(max) = N'parameter2' " + + "AS " + + "BEGIN " + + " SET NOCOUNT ON; " + + " SELECT @p1 + @p2 AS result; " + + "END "; + stmt.execute(sql); + } }