diff --git a/src/docs/asciidoc/release_notes.adoc b/src/docs/asciidoc/release_notes.adoc index afb5ee9e1..a60da07b0 100644 --- a/src/docs/asciidoc/release_notes.adoc +++ b/src/docs/asciidoc/release_notes.adoc @@ -911,6 +911,9 @@ The JDBC API specifies _"This `byte` array contains up to `length` consecutive b + The JDBC API does not specify what should happen if the requested position is beyond the end-of-blob. The modified implementation returns an empty array, but given this is unspecified behaviour, we reserve the option to change this in the future to throw an exception instead. +* Fixed: `CallableStatement.getXXX(String)` could return value from wrong column (https://github.com/FirebirdSQL/jaybird/issues/771[#771]) ++ +This change was also backported to Jaybird 4.0.10 and Jaybird 5.0.3. * `FBResultSetNotUpdatableException` now extends `SQLNonTransientException` instead of `FBSQLException`. * Jaybird no longer throws any instances of `FBSQLException`. @@ -1098,6 +1101,10 @@ there is no replacement there is no replacement ** `getLoggerImplementation()`; there is no replacement +* `FBCallableStatement` +** `findOutParameter(String)` (protected); +use `getAndAssertSingletonResultSet().findColumn(paramName)`; +carefully check if that is the correct usage (the method was removed because the old usage within Jaybird resulted in mapping the wrong column) The following methods had their visibility reduced: diff --git a/src/main/org/firebirdsql/jdbc/FBCallableStatement.java b/src/main/org/firebirdsql/jdbc/FBCallableStatement.java index 994f3a6ec..b3007a14f 100644 --- a/src/main/org/firebirdsql/jdbc/FBCallableStatement.java +++ b/src/main/org/firebirdsql/jdbc/FBCallableStatement.java @@ -508,7 +508,7 @@ public Object getObject(int parameterIndex) throws SQLException { @Override public Object getObject(String colName) throws SQLException { - return getObject(findOutParameter(colName)); + return getAndAssertSingletonResultSet().getObject(colName); } /** @@ -530,7 +530,7 @@ public Object getObject(int parameterIndex, Map> map) throws SQ */ @Override public Object getObject(String colName, Map> map) throws SQLException { - return getObject(findOutParameter(colName), map); + return getAndAssertSingletonResultSet().getObject(colName, map); } @Override @@ -540,7 +540,7 @@ public T getObject(int parameterIndex, Class type) throws SQLException { @Override public T getObject(String parameterName, Class type) throws SQLException { - return getObject(findOutParameter(parameterName), type); + return getAndAssertSingletonResultSet().getObject(parameterName, type); } @Override @@ -590,107 +590,107 @@ public URL getURL(int parameterIndex) throws SQLException { @Override public String getString(String colName) throws SQLException { - return getString(findOutParameter(colName)); + return getAndAssertSingletonResultSet().getString(colName); } @Override public boolean getBoolean(String colName) throws SQLException { - return getBoolean(findOutParameter(colName)); + return getAndAssertSingletonResultSet().getBoolean(colName); } @Override public byte getByte(String colName) throws SQLException { - return getByte(findOutParameter(colName)); + return getAndAssertSingletonResultSet().getByte(colName); } @Override public short getShort(String colName) throws SQLException { - return getShort(findOutParameter(colName)); + return getAndAssertSingletonResultSet().getShort(colName); } @Override public int getInt(String colName) throws SQLException { - return getInt(findOutParameter(colName)); + return getAndAssertSingletonResultSet().getInt(colName); } @Override public long getLong(String colName) throws SQLException { - return getLong(findOutParameter(colName)); + return getAndAssertSingletonResultSet().getLong(colName); } @Override public float getFloat(String colName) throws SQLException { - return getFloat(findOutParameter(colName)); + return getAndAssertSingletonResultSet().getFloat(colName); } @Override public double getDouble(String colName) throws SQLException { - return getDouble(findOutParameter(colName)); + return getAndAssertSingletonResultSet().getDouble(colName); } @Override public byte[] getBytes(String colName) throws SQLException { - return getBytes(findOutParameter(colName)); + return getAndAssertSingletonResultSet().getBytes(colName); } @Override public Date getDate(String colName) throws SQLException { - return getDate(findOutParameter(colName)); + return getAndAssertSingletonResultSet().getDate(colName); } @Override public Time getTime(String colName) throws SQLException { - return getTime(findOutParameter(colName)); + return getAndAssertSingletonResultSet().getTime(colName); } @Override public Timestamp getTimestamp(String colName) throws SQLException { - return getTimestamp(findOutParameter(colName)); + return getAndAssertSingletonResultSet().getTimestamp(colName); } @Override public BigDecimal getBigDecimal(String colName) throws SQLException { - return getBigDecimal(findOutParameter(colName)); + return getAndAssertSingletonResultSet().getBigDecimal(colName); } @Override public Ref getRef(String colName) throws SQLException { - return getRef(findOutParameter(colName)); + return getAndAssertSingletonResultSet().getRef(colName); } @Override public Blob getBlob(String colName) throws SQLException { - return getBlob(findOutParameter(colName)); + return getAndAssertSingletonResultSet().getBlob(colName); } @Override public Clob getClob(String colName) throws SQLException { - return getClob(findOutParameter(colName)); + return getAndAssertSingletonResultSet().getClob(colName); } @Override public Array getArray(String colName) throws SQLException { - return getArray(findOutParameter(colName)); + return getAndAssertSingletonResultSet().getArray(colName); } @Override public Date getDate(String colName, Calendar cal) throws SQLException { - return getDate(findOutParameter(colName), cal); + return getAndAssertSingletonResultSet().getDate(colName, cal); } @Override public Time getTime(String colName, Calendar cal) throws SQLException { - return getTime(findOutParameter(colName), cal); + return getAndAssertSingletonResultSet().getTime(colName, cal); } @Override public Timestamp getTimestamp(String colName, Calendar cal) throws SQLException { - return getTimestamp(findOutParameter(colName), cal); + return getAndAssertSingletonResultSet().getTimestamp(colName, cal); } @Override public URL getURL(String colName) throws SQLException { - return getURL(findOutParameter(colName)); + return getAndAssertSingletonResultSet().getURL(colName); } @Override @@ -700,7 +700,7 @@ public Reader getCharacterStream(int parameterIndex) throws SQLException { @Override public Reader getCharacterStream(String parameterName) throws SQLException { - return getCharacterStream(findOutParameter(parameterName)); + return getAndAssertSingletonResultSet().getCharacterStream(parameterName); } /** @@ -722,7 +722,7 @@ public Reader getNCharacterStream(int parameterIndex) throws SQLException { */ @Override public Reader getNCharacterStream(String parameterName) throws SQLException { - return getNCharacterStream(findOutParameter(parameterName)); + return getAndAssertSingletonResultSet().getNCharacterStream(parameterName); } /** @@ -744,7 +744,7 @@ public String getNString(int parameterIndex) throws SQLException { */ @Override public String getNString(String parameterName) throws SQLException { - return getNString(findOutParameter(parameterName)); + return getAndAssertSingletonResultSet().getNString(parameterName); } @Override @@ -1268,16 +1268,6 @@ private void setSelectabilityAutomatically(StoredProcedureMetaData storedProcMet selectableProcedure = storedProcMetaData.isSelectable(procedureCall.getName()); } - /** - * Helper method to identify the right result set column for the give OUT parameter name. - * - * @param paramName - * Name of the OUT parameter - */ - protected int findOutParameter(String paramName) throws SQLException { - return getAndAssertSingletonResultSet().findColumn(paramName); - } - /** * {@inheritDoc} *

@@ -1297,7 +1287,7 @@ public NClob getNClob(int parameterIndex) throws SQLException { */ @Override public NClob getNClob(String parameterName) throws SQLException { - return getNClob(findOutParameter(parameterName)); + return getAndAssertSingletonResultSet().getNClob(parameterName); } @Override @@ -1307,7 +1297,7 @@ public RowId getRowId(int parameterIndex) throws SQLException { @Override public RowId getRowId(String parameterName) throws SQLException { - return getRowId(findOutParameter(parameterName)); + return getAndAssertSingletonResultSet().getRowId(parameterName); } @Override @@ -1317,7 +1307,7 @@ public SQLXML getSQLXML(int parameterIndex) throws SQLException { @Override public SQLXML getSQLXML(String parameterName) throws SQLException { - return getSQLXML(findOutParameter(parameterName)); + return getAndAssertSingletonResultSet().getSQLXML(parameterName); } /** diff --git a/src/test/org/firebirdsql/jdbc/FBCallableStatementTest.java b/src/test/org/firebirdsql/jdbc/FBCallableStatementTest.java index 00273efef..e25681687 100644 --- a/src/test/org/firebirdsql/jdbc/FBCallableStatementTest.java +++ b/src/test/org/firebirdsql/jdbc/FBCallableStatementTest.java @@ -57,82 +57,82 @@ class FBCallableStatementTest { @RegisterExtension final UsesDatabaseExtension.UsesDatabaseForEach usesDatabase = UsesDatabaseExtension.usesDatabase(); - //@formatter:off - private static final String CREATE_PROCEDURE = - "CREATE PROCEDURE factorial( " - + " max_rows INTEGER, " - + " mode INTEGER " - + ") RETURNS ( " - + " row_num INTEGER, " - + " factorial INTEGER " - + ") AS " - + " DECLARE VARIABLE temp INTEGER; " - + " DECLARE VARIABLE counter INTEGER; " - + "BEGIN " - + " counter = 0; " - + " temp = 1; " - + " WHILE (counter <= max_rows) DO BEGIN " - + " row_num = counter; " - + " IF (row_num = 0) THEN " - + " temp = 1; " - + " ELSE " - + " temp = temp * row_num; " - + " factorial = temp; " - + " counter = counter + 1; " - + " IF (mode = 1) THEN " - + " SUSPEND; " - + " END " - + " IF (mode = 2) THEN " - + " SUSPEND; " - + "END "; + private static final String CREATE_PROCEDURE = """ + CREATE PROCEDURE factorial( + max_rows INTEGER, + mode INTEGER + ) RETURNS ( + row_num INTEGER, + factorial INTEGER + ) AS + DECLARE VARIABLE temp INTEGER; + DECLARE VARIABLE counter INTEGER; + BEGIN + counter = 0; + temp = 1; + WHILE (counter <= max_rows) DO BEGIN + row_num = counter; + IF (row_num = 0) THEN + temp = 1; + ELSE + temp = temp * row_num; + factorial = temp; + counter = counter + 1; + IF (mode = 1) THEN + SUSPEND; + END + IF (mode = 2) THEN + SUSPEND; + END"""; private static final String SELECT_PROCEDURE = "SELECT * FROM factorial(?, 2)"; private static final String CALL_SELECT_PROCEDURE = "{call factorial(?, 1, ?, ?)}"; private static final String EXECUTE_PROCEDURE = "{call factorial(?, ?, ?, ?)}"; private static final String EXECUTE_PROCEDURE_AS_STMT = "{call factorial(?, 0)}"; - private static final String CREATE_PROCEDURE_EMP_SELECT = - "CREATE PROCEDURE get_emp_proj(emp_no SMALLINT) " - + " RETURNS (proj_id VARCHAR(25)) AS " - + " BEGIN " - + " FOR SELECT PROJ_ID " - + " FROM employee_project " - + " WHERE emp_no = :emp_no ORDER BY proj_id " - + " INTO :proj_id " - + " DO " - + " SUSPEND; " - + "END"; + private static final String CREATE_PROCEDURE_EMP_SELECT = """ + CREATE PROCEDURE get_emp_proj(emp_no SMALLINT) + RETURNS (proj_id VARCHAR(25)) AS + BEGIN + FOR SELECT PROJ_ID + FROM employee_project + WHERE emp_no = :emp_no ORDER BY proj_id + INTO :proj_id + DO + SUSPEND; + END"""; private static final String SELECT_PROCEDURE_EMP_SELECT = "SELECT * FROM get_emp_proj(?)"; private static final String EXECUTE_PROCEDURE_EMP_SELECT = "{call get_emp_proj(?)}"; - private static final String CREATE_PROCEDURE_EMP_INSERT = - "CREATE PROCEDURE set_emp_proj(emp_no SMALLINT, proj_id VARCHAR(10)" - + " , last_name VARCHAR(10), proj_name VARCHAR(25)) " - + " AS " - + " BEGIN " - + " INSERT INTO employee_project (emp_no, proj_id, last_name, proj_name) " - + " VALUES (:emp_no, :proj_id, :last_name, :proj_name); " - + "END"; + private static final String CREATE_PROCEDURE_EMP_INSERT = """ + CREATE PROCEDURE set_emp_proj(emp_no SMALLINT, proj_id VARCHAR(10) + , last_name VARCHAR(10), proj_name VARCHAR(25)) + AS + BEGIN + INSERT INTO employee_project (emp_no, proj_id, last_name, proj_name) + VALUES (:emp_no, :proj_id, :last_name, :proj_name); + END"""; private static final String EXECUTE_PROCEDURE_EMP_INSERT = "{call set_emp_proj (?,?,?,?)}"; private static final String EXECUTE_PROCEDURE_EMP_INSERT_1 = "EXECUTE PROCEDURE set_emp_proj (?,?,?,?)"; private static final String EXECUTE_PROCEDURE_EMP_INSERT_SPACES = "EXECUTE PROCEDURE \nset_emp_proj\t ( ?,?\t,?\n ,?)"; - private static final String CREATE_EMPLOYEE_PROJECT = - "CREATE TABLE employee_project( " - + " emp_no INTEGER NOT NULL, " - + " proj_id VARCHAR(10) NOT NULL, " - + " last_name VARCHAR(10) NOT NULL, " - + " proj_name VARCHAR(25) NOT NULL, " - + " proj_desc BLOB SUB_TYPE 1, " - + " product VARCHAR(25) )"; - - private static final String CREATE_SIMPLE_OUT_PROC = - "CREATE PROCEDURE test_out (inParam VARCHAR(10)) RETURNS (outParam VARCHAR(10)) " - + "AS BEGIN " - + " outParam = inParam; " - + "END"; + private static final String CREATE_EMPLOYEE_PROJECT = """ + CREATE TABLE employee_project( + emp_no INTEGER NOT NULL, + proj_id VARCHAR(10) NOT NULL, + last_name VARCHAR(10) NOT NULL, + proj_name VARCHAR(25) NOT NULL, + proj_desc BLOB SUB_TYPE 1, + product VARCHAR(25) + )"""; + + private static final String CREATE_SIMPLE_OUT_PROC = """ + CREATE PROCEDURE test_out (inParam VARCHAR(10)) RETURNS (outParam VARCHAR(10)) + AS BEGIN + outParam = inParam; + END"""; private static final String EXECUTE_SIMPLE_OUT_PROCEDURE = "{call test_out ?, ? }"; private static final String EXECUTE_SIMPLE_OUT_PROCEDURE_1 = "{?=CALL test_out(?)}"; @@ -141,59 +141,58 @@ class FBCallableStatementTest { private static final String EXECUTE_SIMPLE_OUT_PROCEDURE_CONST_WITH_QUESTION = "EXECUTE PROCEDURE test_out 'test?'"; private static final String EXECUTE_SIMPLE_OUT_WITH_OUT_PARAM = "EXECUTE PROCEDURE test_out(?, ?)"; - private static final String CREATE_PROCEDURE_WITHOUT_PARAMS = - "CREATE PROCEDURE test_no_params " - + "AS BEGIN " - + " exit;" - + "END"; + private static final String CREATE_PROCEDURE_WITHOUT_PARAMS = """ + CREATE PROCEDURE test_no_params + AS BEGIN + exit; + END"""; private static final String EXECUTE_PROCEDURE_WITHOUT_PARAMS = "{call test_no_params}"; private static final String EXECUTE_PROCEDURE_WITHOUT_PARAMS_1 = "{call test_no_params()}"; private static final String EXECUTE_PROCEDURE_WITHOUT_PARAMS_2 = "{call test_no_params () }"; private static final String EXECUTE_PROCEDURE_WITHOUT_PARAMS_3 = "EXECUTE PROCEDURE test_no_params ()"; - private static final String CREATE_PROCEDURE_SELECT_WITHOUT_PARAMS = - "CREATE PROCEDURE select_no_params " - + " RETURNS (proj_id VARCHAR(25)) " - + "AS BEGIN " - + " proj_id = 'abc'; " - + " SUSPEND;" - + "END"; + private static final String CREATE_PROCEDURE_SELECT_WITHOUT_PARAMS = """ + CREATE PROCEDURE select_no_params + RETURNS (proj_id VARCHAR(25)) + AS BEGIN + proj_id = 'abc'; + SUSPEND; + END"""; private static final String EXECUTE_PROCEDURE_SELECT_WITHOUT_PARAMS = "{call select_no_params}"; private static final String EXECUTE_PROCEDURE_SELECT_WITHOUT_PARAMS_1 = "{call select_no_params()}"; private static final String EXECUTE_PROCEDURE_SELECT_WITHOUT_PARAMS_2 = "{call select_no_params () }"; - private static final String CREATE_PROCEDURE_BLOB_RESULT = - "CREATE PROCEDURE blob_result\n" + - "RETURNS (\n" + - " SQL_SELECT BLOB SUB_TYPE 1 )\n" + - "AS\n" + - "BEGIN\n" + - " sql_select = '\n" + - " EXECUTE BLOCK\n" + - " RETURNS(\n" + - " column_info_column_name VARCHAR(30),\n" + - " column_value VARCHAR(100),\n" + - " column_alias VARCHAR(20))\n" + - " AS\n" + - " DECLARE VARIABLE i INTEGER;\n" + - " BEGIN\n" + - " i = 0;\n" + - " WHILE (i < 10)\n" + - " DO BEGIN\n" + - " column_info_column_name = ''FK_TETEL__DYN'';\n" + - " column_value = ascii_char(ascii_val(''a'') + i);\n" + - " column_alias = UPPER(column_value);\n" + - " SUSPEND;\n" + - "\n" + - " i = i + 1;\n" + - " END\n" + - " END';\n" + - "END"; + private static final String CREATE_PROCEDURE_BLOB_RESULT = """ + CREATE PROCEDURE blob_result + RETURNS ( + SQL_SELECT BLOB SUB_TYPE 1 ) + AS + BEGIN + sql_select = ' + EXECUTE BLOCK + RETURNS( + column_info_column_name VARCHAR(30), + column_value VARCHAR(100), + column_alias VARCHAR(20)) + AS + DECLARE VARIABLE i INTEGER; + BEGIN + i = 0; + WHILE (i < 10) + DO BEGIN + column_info_column_name = ''FK_TETEL__DYN''; + column_value = ascii_char(ascii_val(''a'') + i); + column_alias = UPPER(column_value); + SUSPEND; + + i = i + 1; + END + END'; + END"""; private static final String EXECUTE_PROCEDURE_BLOB_RESULT = "EXECUTE PROCEDURE blob_result"; - //@formatter:on private Connection con; @@ -1122,6 +1121,33 @@ void executeProcedureOnSelectable_ignoreProcedureType_null_636() throws Exceptio } } + /** + * Rationale: an implementation bug looked up the result set index by name, then requested the value by index on + * the callable statement. If that index corresponded to the index of a registered OUT parameter, it would remap + * that to a different result set column and return the value of the wrong column. + */ + @Test + void namedRetrievalMapping() throws Exception { + executeDDL(con, """ + create procedure one_in_two_out(in1 varchar(5)) returns (out1 varchar(8), out2 varchar(8)) + as + begin + out1 = 'out1' || in1; + out2 = 'out2' || in1; + end"""); + + try (var cstmt = con.prepareCall("{call one_in_two_out(?, ?, ?)}")) { + cstmt.setString(1, "test"); + cstmt.registerOutParameter(2, Types.VARCHAR); + cstmt.registerOutParameter(3, Types.VARCHAR); + + cstmt.execute(); + + assertEquals("out1test", cstmt.getString("OUT1"), "Unexpected value for column OUT1"); + assertEquals("out2test", cstmt.getString("OUT2"), "Unexpected value for column OUT2"); + } + } + static Stream scrollableCursorPropertyValues() { // We are unconditionally emitting SERVER, to check if the value behaves appropriately on versions that do // not support server-side scrollable cursors