From cf3808f2ac092904f530fd0d09716aa398d4df96 Mon Sep 17 00:00:00 2001 From: Richard Zowalla Date: Mon, 14 Sep 2026 20:03:10 +0200 Subject: [PATCH] [OPENJPA-2970] Let an explicit null named parameter win over positional lookup Look stored procedure parameters up with containsKey so a parameter bound to null is not replaced by the value registered under its position. Index the parameter array by the parameter position so IN parameters following an OUT parameter are bound instead of being silently skipped. --- .../jdbc/kernel/StoredProcedureQuery.java | 32 ++++++---- .../TestStoredProcedureQueryFixes.java | 61 +++++++++++++++++++ 2 files changed, 82 insertions(+), 11 deletions(-) diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/StoredProcedureQuery.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/StoredProcedureQuery.java index 93fb940798..dc52306710 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/StoredProcedureQuery.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/StoredProcedureQuery.java @@ -186,15 +186,17 @@ public ResultObjectProvider executeQuery(StoreQuery q, Object[] params, Range ra stmnt = conn.prepareCall(_proc.getCallSQL()); final StoredProcedureQuery spq = (StoredProcedureQuery) q; + // no parameter values at all: leave the (un)binding to the driver + final boolean bind = params != null && params.length > 0; for (Column c : spq.getProcedure().getInColumns()) { - if (params != null && c.getIndex() < params.length) { + if (bind) { dict.setUnknown(stmnt, c.getIndex() + 1, params[c.getIndex()], c); } } for (Column c : spq.getProcedure().getInOutColumns()) { final int index = c.getIndex() + 1; stmnt.registerOutParameter(index, c.getType()); - if (params != null && index - 1 < params.length) { + if (bind) { dict.setUnknown(stmnt, index, params[index - 1], c); } } @@ -265,21 +267,29 @@ public Object[] toParameterArray(StoreQuery q, Map userParams) { StoredProcedureQuery storedProcedureQuery = (StoredProcedureQuery) q; Column[] inCols = storedProcedureQuery.getProcedure().getInColumns(); Column[] inOutCols = storedProcedureQuery.getProcedure().getInOutColumns(); - int paramCount = inCols.length + inOutCols.length; - if (paramCount == 0) return NO_PARAM; + if (inCols.length + inOutCols.length == 0) return NO_PARAM; + // the values are indexed by the parameter position, OUT parameters leave a gap + int paramCount = 0; + for (final Column[] columns : asList(inCols, inOutCols)) { + for (Column c : columns) { + paramCount = Math.max(paramCount, c.getIndex() + 1); + } + } Object[] array = new Object[paramCount]; - int i = 0; for (final Column[] columns : asList(inCols, inOutCols)) { for (Column c : columns) { - // Try by name first, then by 1-based position (matching user's declaration) - Object val = userParams.get(c.getIdentifier().getName()); - if (val == null) { + // Try by name first, then by 1-based position (matching user's declaration). + // An explicit null binding wins over a lookup by position. + String name = c.getIdentifier().getName(); + Object val = null; + if (userParams.containsKey(name)) { + val = userParams.get(name); + } else if (userParams.containsKey(c.getIndex() + 1)) { val = userParams.get(c.getIndex() + 1); - } - if (val == null) { + } else if (userParams.containsKey(c.getIndex())) { val = userParams.get(c.getIndex()); } - array[i++] = val; + array[c.getIndex()] = val; } } return array; diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/procedure/TestStoredProcedureQueryFixes.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/procedure/TestStoredProcedureQueryFixes.java index 4b0c9499e4..4fab54619b 100644 --- a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/procedure/TestStoredProcedureQueryFixes.java +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/procedure/TestStoredProcedureQueryFixes.java @@ -186,6 +186,67 @@ public void testGetSingleResultNonUniqueResultException() throws Exception { } } + /** + * Test that a named parameter explicitly bound to null is not replaced by + * the value registered under the position of that parameter. + */ + public void testExplicitNullNamedParameterWinsOverPosition() throws Exception { + Procedures.inParamsInteger = -1; + Procedures.inParamsString = null; + + EntityManager em = emf.createEntityManager(); + try { + exec(em, "DROP PROCEDURE TESTINS", true); + exec(em, "CREATE PROCEDURE TESTINS(SOME_NUMBER INTEGER,SOME_STRING VARCHAR(255)) " + + "PARAMETER STYLE JAVA LANGUAGE JAVA EXTERNAL NAME " + + "'" + Procedures.class.getName() + ".inParams'", false); + + StoredProcedureQuery spq = em.createStoredProcedureQuery("TESTINS"); + spq.registerStoredProcedureParameter("SOME_NUMBER", Integer.class, ParameterMode.IN); + spq.registerStoredProcedureParameter("SOME_STRING", String.class, ParameterMode.IN); + spq.registerStoredProcedureParameter(2, String.class, ParameterMode.IN); + spq.setParameter("SOME_NUMBER", 42); + spq.setParameter(2, "positional"); + spq.setParameter("SOME_STRING", null); + assertFalse(spq.execute()); + + assertEquals(42, Procedures.inParamsInteger); + assertNull(Procedures.inParamsString); + } finally { + em.close(); + } + } + + /** + * Test that IN parameters following an OUT parameter are bound at their position. + */ + public void testInParameterAfterOutParameter() throws Exception { + EntityManager em = emf.createEntityManager(); + try { + exec(em, "DROP PROCEDURE XSUM", true); + exec(em, "CREATE PROCEDURE XSUM(IN A INTEGER,OUT S INTEGER,IN B INTEGER) " + + "PARAMETER STYLE JAVA LANGUAGE JAVA EXTERNAL NAME " + + "'" + TestStoredProcedureQueryFixes.class.getName() + ".sum'", false); + + StoredProcedureQuery spq = em.createStoredProcedureQuery("XSUM"); + spq.registerStoredProcedureParameter(1, Integer.class, ParameterMode.IN); + spq.registerStoredProcedureParameter(2, Integer.class, ParameterMode.OUT); + spq.registerStoredProcedureParameter(3, Integer.class, ParameterMode.IN); + spq.setParameter(1, 3); + spq.setParameter(3, 4); + spq.execute(); + + assertEquals(7, spq.getOutputParameterValue(2)); + } finally { + em.close(); + } + } + + // Derby stored procedure: s = a + b + public static void sum(int a, int[] s, int b) { + s[0] = a + b; + } + // Derby stored procedure: returns empty result set public static void emptyResultSet(java.sql.ResultSet[] rs) throws Exception { java.sql.Connection c = java.sql.DriverManager.getConnection("jdbc:default:connection");