This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch past-M2
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit 1f96946ed3aa84fabb35fd95037a7cc75fba5b0d
Author: Andrus Adamchik <[email protected]>
AuthorDate: Sun May 31 14:49:42 2026 -0400

    Translators as stateless services producing immutable TranslatedXyz records
    
        * procedure translators
---
 .../java/org/apache/cayenne/access/DataNode.java   |  16 ++
 .../cayenne/access/jdbc/ProcedureAction.java       |  49 +++--
 .../DbAdapterDelegatedProcedureTranslator.java     |  38 ++++
 .../procedure/DefaultProcedureTranslator.java      | 129 ++++++++++++
 .../translator/procedure/ProcedureTranslator.java  | 232 +--------------------
 .../translator/procedure/TranslatedProcedure.java  |  31 +++
 .../cayenne/configuration/runtime/CoreModule.java  |   3 +
 .../runtime/DefaultDataNodeFactory.java            |   5 +
 .../java/org/apache/cayenne/dba/AutoAdapter.java   |  10 +
 .../java/org/apache/cayenne/dba/DbAdapter.java     |  10 +
 .../java/org/apache/cayenne/dba/JdbcAdapter.java   |   8 +
 .../apache/cayenne/dba/db2/DB2ProcedureAction.java |  10 +-
 .../cayenne/dba/hsqldb/HSQLActionBuilder.java      |  22 --
 .../apache/cayenne/dba/hsqldb/HSQLDBAdapter.java   |  11 +
 .../dba/hsqldb/HSQLDBProcedureTranslator.java      |  11 +-
 .../org/apache/cayenne/dba/mysql/MySQLAdapter.java |  11 +
 .../cayenne/dba/mysql/MySQLProcedureAction.java    |  42 +---
 .../dba/mysql/MySQLProcedureTranslator.java        |  45 ++++
 .../cayenne/dba/postgres/PostgresAdapter.java      |  11 +
 .../dba/postgres/PostgresProcedureAction.java      |  39 +---
 .../dba/postgres/PostgresProcedureTranslator.java  |  45 ++++
 .../dba/sqlserver/SQLServerProcedureAction.java    |  11 +-
 .../runtime/DataDomainProviderTest.java            |   3 +
 23 files changed, 449 insertions(+), 343 deletions(-)

diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataNode.java 
b/cayenne/src/main/java/org/apache/cayenne/access/DataNode.java
index f1287c41b..049e4fc36 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataNode.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataNode.java
@@ -26,6 +26,7 @@ import org.apache.cayenne.access.jdbc.RowDescriptor;
 import org.apache.cayenne.access.jdbc.reader.RowReader;
 import org.apache.cayenne.access.jdbc.reader.RowReaderFactory;
 import org.apache.cayenne.access.translator.batch.BatchTranslator;
+import org.apache.cayenne.access.translator.procedure.ProcedureTranslator;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
 import org.apache.cayenne.access.translator.sqltemplate.SQLTemplateTranslator;
 import org.apache.cayenne.dba.DbAdapter;
@@ -75,6 +76,7 @@ public class DataNode {
        private BatchTranslator<UpdateBatchQuery> updateBatchTranslator;
        private BatchTranslator<DeleteBatchQuery> deleteBatchTranslator;
        private SelectTranslator selectTranslator;
+       private ProcedureTranslator procedureTranslator;
        private SQLTemplateTranslator sqlTemplateTranslator;
 
        TransactionDataSource readThroughDataSource;
@@ -425,6 +427,20 @@ public class DataNode {
                this.selectTranslator = selectTranslator;
        }
 
+       /**
+        * @since 5.0
+        */
+       public ProcedureTranslator getProcedureTranslator() {
+               return procedureTranslator;
+       }
+
+       /**
+        * @since 5.0
+        */
+       public void setProcedureTranslator(ProcedureTranslator 
procedureTranslator) {
+               this.procedureTranslator = procedureTranslator;
+       }
+
     // a read-through DataSource that ensures returning the same connection
     // within
     // transaction.
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/ProcedureAction.java 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/ProcedureAction.java
index 31e661090..9fd59caed 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/ProcedureAction.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/ProcedureAction.java
@@ -23,8 +23,10 @@ import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.DataRow;
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.OperationObserver;
-import org.apache.cayenne.access.translator.procedure.ProcedureTranslator;
+import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.procedure.TranslatedProcedure;
 import org.apache.cayenne.access.types.ExtendedType;
+import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.map.Procedure;
 import org.apache.cayenne.map.ProcedureParameter;
 import org.apache.cayenne.query.ProcedureQuery;
@@ -66,10 +68,14 @@ public class ProcedureAction extends BaseSQLAction {
 
                processedResultSets = 0;
 
-               ProcedureTranslator transl = createTranslator(connection);
+               TranslatedProcedure translated = 
dataNode.getProcedureTranslator()
+                               .translate(query, dataNode.getAdapter(), 
dataNode.getEntityResolver());
 
-               try (CallableStatement statement = (CallableStatement) 
transl.createStatement();) {
+               dataNode.getJdbcEventLogger().logQuery(translated.sql(), 
translated.bindings());
+
+               try (CallableStatement statement = 
connection.prepareCall(translated.sql());) {
                        initStatement(statement);
+                       bindParameters(statement, translated);
 
                        // stored procedure may contain a mixture of update 
counts and
                        // result sets,
@@ -114,19 +120,32 @@ public class ProcedureAction extends BaseSQLAction {
        }
 
        /**
-        * Returns the ProcedureTranslator to use for this ProcedureAction.
-        * 
-        * @param connection
-        *            JDBC connection
+        * Applies the translated bindings to the CallableStatement: registers 
OUT parameters and binds IN parameters.
+        * A stored procedure parameter can be both IN and OUT at the same time.
+        *
+        * @since 5.0
         */
-       protected ProcedureTranslator createTranslator(Connection connection) {
-               ProcedureTranslator translator = new ProcedureTranslator();
-               translator.setAdapter(dataNode.getAdapter());
-               translator.setQuery(query);
-               translator.setEntityResolver(dataNode.getEntityResolver());
-               translator.setConnection(connection);
-               translator.setJdbcEventLogger(dataNode.getJdbcEventLogger());
-               return translator;
+       protected void bindParameters(CallableStatement statement, 
TranslatedProcedure translated) throws Exception {
+               DbAdapter adapter = dataNode.getAdapter();
+               ProcedureParameter[] callParams = translated.callParams();
+               ParameterBinding[] bindings = translated.bindings();
+
+               for (int i = 0; i < callParams.length; i++) {
+                       ProcedureParameter param = callParams[i];
+
+                       if (param.isOutParam()) {
+                               int precision = param.getPrecision();
+                               if (precision >= 0) {
+                                       statement.registerOutParameter(i + 1, 
param.getType(), precision);
+                               } else {
+                                       statement.registerOutParameter(i + 1, 
param.getType());
+                               }
+                       }
+
+                       if (param.isInParameter()) {
+                               adapter.bindParameter(statement, bindings[i]);
+                       }
+               }
        }
 
        /**
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/procedure/DbAdapterDelegatedProcedureTranslator.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/procedure/DbAdapterDelegatedProcedureTranslator.java
new file mode 100644
index 000000000..ca32a8603
--- /dev/null
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/procedure/DbAdapterDelegatedProcedureTranslator.java
@@ -0,0 +1,38 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.translator.procedure;
+
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.ProcedureQuery;
+
+/**
+ * A {@link ProcedureTranslator} that resolves the actual translator from 
{@link DbAdapter#getProcedureTranslator}
+ * allowing adapters to customize translation.
+ *
+ * @since 5.0
+ */
+public class DbAdapterDelegatedProcedureTranslator implements 
ProcedureTranslator {
+
+    @Override
+    public TranslatedProcedure translate(ProcedureQuery query, DbAdapter 
adapter, EntityResolver resolver) {
+        return adapter.getProcedureTranslator(query, 
resolver).translate(query, adapter, resolver);
+    }
+}
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/procedure/DefaultProcedureTranslator.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/procedure/DefaultProcedureTranslator.java
new file mode 100644
index 000000000..d4ebc13c3
--- /dev/null
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/procedure/DefaultProcedureTranslator.java
@@ -0,0 +1,129 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.translator.procedure;
+
+import java.util.Map;
+
+import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.types.ExtendedType;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.Procedure;
+import org.apache.cayenne.map.ProcedureParameter;
+import org.apache.cayenne.query.ProcedureQuery;
+
+/**
+ * A {@link ProcedureTranslator} returned by the base {@link 
org.apache.cayenne.dba.JdbcAdapter}. Adapters may subclass
+ * it to customize the generated call string.
+ *
+ * @since 5.0
+ */
+public class DefaultProcedureTranslator implements ProcedureTranslator {
+
+    /**
+     * Helper class to make OUT and VOID parameters logger-friendly.
+     */
+    static class NotInParam {
+
+        protected String type;
+
+        public NotInParam(String type) {
+            this.type = type;
+        }
+
+        @Override
+        public String toString() {
+            return type;
+        }
+    }
+
+    private static final NotInParam OUT_PARAM = new NotInParam("[OUT]");
+
+    @Override
+    public TranslatedProcedure translate(ProcedureQuery query, DbAdapter 
adapter, EntityResolver resolver) {
+
+        Procedure procedure = query.getMetaData(resolver).getProcedure();
+        ProcedureParameter[] callParams = 
procedure.getCallParameters().toArray(new ProcedureParameter[0]);
+        Map<String, ?> queryValues = query.getParameters();
+
+        ParameterBinding[] bindings = new ParameterBinding[callParams.length];
+        for (int i = 0; i < callParams.length; i++) {
+            bindings[i] = createBinding(adapter, callParams[i], queryValues, i 
+ 1);
+        }
+
+        String sql = createSqlString(procedure, callParams.length);
+        return new TranslatedProcedure(sql, callParams, bindings);
+    }
+
+    /**
+     * Builds a {@link ParameterBinding} for a single call parameter. IN (and 
INOUT) parameters carry the actual value
+     * and its {@link ExtendedType}; pure OUT parameters carry an "[OUT]" 
marker value so that logging renders nicely.
+     */
+    protected ParameterBinding createBinding(DbAdapter adapter, 
ProcedureParameter param,
+                                             Map<String, ?> queryValues, int 
position) {
+
+        // match values with parameters in the correct order, assuming a 
missing value is NULL
+        if (param.getDirection() == ProcedureParameter.OUT_PARAMETER) {
+            return new ParameterBinding(param.getType(), 
param.getPrecision()).reset(position, OUT_PARAM, null);
+        }
+
+        Object value = queryValues.get(param.getName());
+        ExtendedType extendedType = value != null
+                ? 
adapter.getExtendedTypes().getRegisteredType(value.getClass())
+                : adapter.getExtendedTypes().getDefaultType();
+
+        return new 
ParameterBinding(adapter.preferredBindingType(param.getType()), 
param.getPrecision())
+                .reset(position, value, extendedType);
+    }
+
+    /**
+     * Creates an SQL String for the stored procedure call.
+     */
+    protected String createSqlString(Procedure procedure, int callParamsSize) {
+
+        StringBuilder buf = new StringBuilder();
+
+        int totalParams = callParamsSize;
+
+        // check if procedure returns values
+        if (procedure.isReturningValue()) {
+            totalParams--;
+            buf.append("{? = call ");
+        } else {
+            buf.append("{call ");
+        }
+
+        buf.append(procedure.getFullyQualifiedName());
+
+        if (totalParams > 0) {
+            // unroll the loop
+            buf.append("(?");
+
+            for (int i = 1; i < totalParams; i++) {
+                buf.append(", ?");
+            }
+
+            buf.append(")");
+        }
+
+        buf.append("}");
+        return buf.toString();
+    }
+}
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/procedure/ProcedureTranslator.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/procedure/ProcedureTranslator.java
index b04929dd2..9d7df9fd2 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/procedure/ProcedureTranslator.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/procedure/ProcedureTranslator.java
@@ -19,233 +19,19 @@
 
 package org.apache.cayenne.access.translator.procedure;
 
-import java.sql.CallableStatement;
-import java.sql.Connection;
-import java.sql.PreparedStatement;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import org.apache.cayenne.access.translator.ParameterBinding;
-import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.log.JdbcEventLogger;
-import org.apache.cayenne.log.NoopJdbcEventLogger;
 import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.map.Procedure;
-import org.apache.cayenne.map.ProcedureParameter;
 import org.apache.cayenne.query.ProcedureQuery;
 
 /**
- * Stored procedure query translator.
+ * A stateless service that translates a {@link ProcedureQuery} into an 
immutable {@link TranslatedProcedure}.
  */
-public class ProcedureTranslator {
-
-       /**
-        * Helper class to make OUT and VOID parameters logger-friendly.
-        */
-       static class NotInParam {
-
-               protected String type;
-
-               public NotInParam(String type) {
-                       this.type = type;
-               }
-
-               @Override
-               public String toString() {
-                       return type;
-               }
-       }
-
-       private static NotInParam OUT_PARAM = new NotInParam("[OUT]");
-
-       protected ProcedureQuery query;
-       protected Connection connection;
-       protected DbAdapter adapter;
-       protected EntityResolver entityResolver;
-       protected List<ProcedureParameter> callParams;
-       protected List<Object> values;
-       protected JdbcEventLogger logger;
-
-       public ProcedureTranslator() {
-               this.logger = NoopJdbcEventLogger.getInstance();
-       }
-
-       public void setQuery(ProcedureQuery query) {
-               this.query = query;
-       }
-
-       public void setConnection(Connection connection) {
-               this.connection = connection;
-       }
-
-       public void setAdapter(DbAdapter adapter) {
-               this.adapter = adapter;
-       }
-
-       /**
-        * @since 3.1
-        */
-       public void setJdbcEventLogger(JdbcEventLogger logger) {
-               this.logger = logger;
-       }
-
-       /**
-        * @since 3.1
-        */
-       public JdbcEventLogger getJdbcEventLogger() {
-               return logger;
-       }
-
-       /**
-        * @since 1.2
-        */
-       public void setEntityResolver(EntityResolver entityResolver) {
-               this.entityResolver = entityResolver;
-       }
-
-       /**
-        * Creates an SQL String for the stored procedure call.
-        */
-       protected String createSqlString() {
-               Procedure procedure = getProcedure();
-
-               StringBuilder buf = new StringBuilder();
-
-               int totalParams = callParams.size();
-
-               // check if procedure returns values
-               if (procedure.isReturningValue()) {
-                       totalParams--;
-                       buf.append("{? = call ");
-               } else {
-                       buf.append("{call ");
-               }
-
-               buf.append(procedure.getFullyQualifiedName());
-
-               if (totalParams > 0) {
-                       // unroll the loop
-                       buf.append("(?");
-
-                       for (int i = 1; i < totalParams; i++) {
-                               buf.append(", ?");
-                       }
-
-                       buf.append(")");
-               }
-
-               buf.append("}");
-               return buf.toString();
-       }
-
-       /**
-        * Creates and binds a PreparedStatement to execute query SQL via JDBC.
-        */
-       public PreparedStatement createStatement() throws Exception {
-               this.callParams = getProcedure().getCallParameters();
-               this.values = new ArrayList<>(callParams.size());
-
-               initValues();
-               String sqlStr = createSqlString();
-
-               if (logger.isLoggable()) {
-                       // need to convert OUT/VOID parameters to loggable 
strings
-                       ParameterBinding[] parameterBindings = new 
ParameterBinding[values.size()];
-                       for (int i=0; i<values.size(); i++) {
-                               ProcedureParameter procedureParameter = 
callParams.get(i);
-                               Object value = values.get(i);
-                               if(value instanceof NotInParam) {
-                                       value = value.toString();
-                               }
-                               parameterBindings[i] = new 
ParameterBinding(procedureParameter.getType(), 
procedureParameter.getPrecision())
-                                               .reset(i + 1, value, null);
-                       }
-                       logger.logQuery(sqlStr, parameterBindings);
-               }
-               CallableStatement stmt = connection.prepareCall(sqlStr);
-               initStatement(stmt);
-               return stmt;
-       }
-
-       public Procedure getProcedure() {
-               return query.getMetaData(entityResolver).getProcedure();
-       }
-
-       public ProcedureQuery getProcedureQuery() {
-               return query;
-       }
-
-       /**
-        * Set IN and OUT parameters.
-        */
-       protected void initStatement(CallableStatement stmt) throws Exception {
-               if (values != null && values.size() > 0) {
-                       List<ProcedureParameter> params = 
getProcedure().getCallParameters();
-
-                       int len = values.size();
-                       for (int i = 0; i < len; i++) {
-                               ProcedureParameter param = params.get(i);
-
-                               // !Stored procedure parameter can be both in 
and out
-                               // at the same time
-                               if (param.isOutParam()) {
-                                       setOutParam(stmt, param, i + 1);
-                               }
-
-                               if (param.isInParameter()) {
-                                       setInParam(stmt, param, values.get(i), 
i + 1);
-                               }
-                       }
-               }
-       }
-
-       protected void initValues() {
-               Map<String, ?> queryValues = 
getProcedureQuery().getParameters();
-
-               // match values with parameters in the correct order.
-               // make an assumption that a missing value is NULL
-               // Any reason why this is bad?
-
-               for (ProcedureParameter param : callParams) {
-
-                       if (param.getDirection() == 
ProcedureParameter.OUT_PARAMETER) {
-                               values.add(OUT_PARAM);
-                       } else {
-                               values.add(queryValues.get(param.getName()));
-                       }
-               }
-       }
-
-       /**
-        * Sets a single IN parameter of the CallableStatement.
-        */
-       protected void setInParam(
-                       CallableStatement stmt,
-                       ProcedureParameter param,
-                       Object val,
-                       int pos) throws Exception {
-               ExtendedType extendedType = val != null
-                               ? 
adapter.getExtendedTypes().getRegisteredType(val.getClass())
-                               : adapter.getExtendedTypes().getDefaultType();
-
-               ParameterBinding binding = new 
ParameterBinding(adapter.preferredBindingType(param.getType()), 
param.getPrecision())
-                               .reset(pos, val, extendedType);
-               adapter.bindParameter(stmt, binding);
-       }
-
-       /**
-        * Sets a single OUT parameter of the CallableStatement.
-        */
-       protected void setOutParam(CallableStatement stmt, ProcedureParameter 
param, int pos)
-                       throws Exception {
-
-               int precision = param.getPrecision();
-               if (precision >= 0) {
-                       stmt.registerOutParameter(pos, param.getType(), 
precision);
-               } else {
-                       stmt.registerOutParameter(pos, param.getType());
-               }
-       }
+public interface ProcedureTranslator {
+
+    /**
+     * Translates the provided query into an immutable {@link 
TranslatedProcedure}.
+     *
+     * @since 5.0
+     */
+    TranslatedProcedure translate(ProcedureQuery query, DbAdapter adapter, 
EntityResolver resolver);
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/procedure/TranslatedProcedure.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/procedure/TranslatedProcedure.java
new file mode 100644
index 000000000..5810ad97c
--- /dev/null
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/procedure/TranslatedProcedure.java
@@ -0,0 +1,31 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.translator.procedure;
+
+import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.map.ProcedureParameter;
+
+/**
+ * An immutable result of translating a {@link 
org.apache.cayenne.query.ProcedureQuery} to a stored procedure call.
+ *
+ * @since 5.0
+ */
+public record TranslatedProcedure(String sql, ProcedureParameter[] callParams, 
ParameterBinding[] bindings) {
+}
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModule.java
 
b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModule.java
index 562ba503c..d71c9d892 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModule.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModule.java
@@ -42,6 +42,8 @@ import 
org.apache.cayenne.access.translator.batch.BatchTranslator;
 import org.apache.cayenne.access.translator.batch.DeleteBatchTranslator;
 import org.apache.cayenne.access.translator.batch.InsertBatchTranslator;
 import org.apache.cayenne.access.translator.batch.UpdateBatchTranslator;
+import 
org.apache.cayenne.access.translator.procedure.DbAdapterDelegatedProcedureTranslator;
+import org.apache.cayenne.access.translator.procedure.ProcedureTranslator;
 import 
org.apache.cayenne.access.translator.select.DbAdapterDelegatedSelectTranslator;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
 import org.apache.cayenne.access.types.*;
@@ -467,6 +469,7 @@ public class CoreModule implements Module {
         binder.bind(Key.get(BatchTranslator.class, 
BatchTranslator.UPDATE)).to(UpdateBatchTranslator.class);
         binder.bind(Key.get(BatchTranslator.class, 
BatchTranslator.DELETE)).to(DeleteBatchTranslator.class);
         
binder.bind(SelectTranslator.class).to(DbAdapterDelegatedSelectTranslator.class);
+        
binder.bind(ProcedureTranslator.class).to(DbAdapterDelegatedProcedureTranslator.class);
 
         // a default ObjectMapRetainStrategy used to create objects map for
         // ObjectStore
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/DefaultDataNodeFactory.java
 
b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/DefaultDataNodeFactory.java
index fa818cd2e..23349873d 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/DefaultDataNodeFactory.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/DefaultDataNodeFactory.java
@@ -23,6 +23,7 @@ import 
org.apache.cayenne.access.dbsync.SchemaUpdateStrategyFactory;
 import org.apache.cayenne.access.translator.sqltemplate.SQLTemplateTranslator;
 import org.apache.cayenne.access.jdbc.reader.RowReaderFactory;
 import org.apache.cayenne.access.translator.batch.BatchTranslator;
+import org.apache.cayenne.access.translator.procedure.ProcedureTranslator;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
 import org.apache.cayenne.query.DeleteBatchQuery;
 import org.apache.cayenne.query.InsertBatchQuery;
@@ -60,6 +61,9 @@ public class DefaultDataNodeFactory implements 
DataNodeFactory {
     @Inject
     protected SelectTranslator selectTranslator;
 
+    @Inject
+    protected ProcedureTranslator procedureTranslator;
+
     @Inject
     protected DbAdapterFactory adapterFactory;
 
@@ -83,6 +87,7 @@ public class DefaultDataNodeFactory implements 
DataNodeFactory {
         dataNode.setUpdateBatchTranslator(updateBatchTranslator);
         dataNode.setDeleteBatchTranslator(deleteBatchTranslator);
         dataNode.setSelectTranslator(selectTranslator);
+        dataNode.setProcedureTranslator(procedureTranslator);
         dataNode.setSqlTemplateTranslator(sqlTemplateTranslator);
 
         DataSource dataSource = 
dataSourceFactory.getDataSource(nodeDescriptor);
diff --git a/cayenne/src/main/java/org/apache/cayenne/dba/AutoAdapter.java 
b/cayenne/src/main/java/org/apache/cayenne/dba/AutoAdapter.java
index 8c2c7980b..255512782 100644
--- a/cayenne/src/main/java/org/apache/cayenne/dba/AutoAdapter.java
+++ b/cayenne/src/main/java/org/apache/cayenne/dba/AutoAdapter.java
@@ -24,6 +24,7 @@ import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
+import org.apache.cayenne.access.translator.procedure.ProcedureTranslator;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
 import org.apache.cayenne.access.types.ExtendedTypeMap;
 import org.apache.cayenne.di.Provider;
@@ -32,6 +33,7 @@ import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbRelationship;
 import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.ProcedureQuery;
 import org.apache.cayenne.query.Select;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.SQLAction;
@@ -104,6 +106,14 @@ public class AutoAdapter implements DbAdapter {
                return getAdapter().getSelectTranslator(query, entityResolver);
        }
 
+       /**
+        * @since 5.0
+        */
+       @Override
+       public ProcedureTranslator getProcedureTranslator(ProcedureQuery query, 
EntityResolver entityResolver) {
+               return getAdapter().getProcedureTranslator(query, 
entityResolver);
+       }
+
        @Override
        public String getBatchTerminator() {
                return getAdapter().getBatchTerminator();
diff --git a/cayenne/src/main/java/org/apache/cayenne/dba/DbAdapter.java 
b/cayenne/src/main/java/org/apache/cayenne/dba/DbAdapter.java
index 7d3e399de..ac54919fa 100644
--- a/cayenne/src/main/java/org/apache/cayenne/dba/DbAdapter.java
+++ b/cayenne/src/main/java/org/apache/cayenne/dba/DbAdapter.java
@@ -22,12 +22,14 @@ import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
+import org.apache.cayenne.access.translator.procedure.ProcedureTranslator;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
 import org.apache.cayenne.access.types.ExtendedTypeMap;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbRelationship;
 import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.ProcedureQuery;
 import org.apache.cayenne.query.Select;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.SQLAction;
@@ -60,6 +62,14 @@ public interface DbAdapter {
         */
        SelectTranslator getSelectTranslator(Select<?> query, EntityResolver 
entityResolver);
 
+       /**
+        * Returns the {@link ProcedureTranslator} for the given query. The 
default implementation ignores the
+        * query, but the parameter is retained so adapters can pick a 
translator based on it.
+        *
+        * @since 5.0
+        */
+       ProcedureTranslator getProcedureTranslator(ProcedureQuery query, 
EntityResolver entityResolver);
+
        /**
         * @since 4.2
         * @return {@link SQLTreeProcessor} that can adjust SQL tree to 
specific database flavour
diff --git a/cayenne/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java 
b/cayenne/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java
index ee15761da..f23027b16 100644
--- a/cayenne/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java
+++ b/cayenne/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java
@@ -25,6 +25,8 @@ import 
org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.translator.ejbql.JdbcEJBQLTranslatorFactory;
+import 
org.apache.cayenne.access.translator.procedure.DefaultProcedureTranslator;
+import org.apache.cayenne.access.translator.procedure.ProcedureTranslator;
 import org.apache.cayenne.access.translator.select.DefaultSelectTranslator;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
 import org.apache.cayenne.access.types.ExtendedType;
@@ -41,6 +43,7 @@ import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbJoin;
 import org.apache.cayenne.map.DbRelationship;
 import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.ProcedureQuery;
 import org.apache.cayenne.query.Select;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.SQLAction;
@@ -526,6 +529,11 @@ public class JdbcAdapter implements DbAdapter {
         return new DefaultSelectTranslator();
     }
 
+    @Override
+    public ProcedureTranslator getProcedureTranslator(ProcedureQuery query, 
EntityResolver entityResolver) {
+        return new DefaultProcedureTranslator();
+    }
+
     @Override
     public SQLTreeProcessor getSqlTreeProcessor() {
         return node -> node;
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/dba/db2/DB2ProcedureAction.java 
b/cayenne/src/main/java/org/apache/cayenne/dba/db2/DB2ProcedureAction.java
index 79f304be4..95848f97e 100644
--- a/cayenne/src/main/java/org/apache/cayenne/dba/db2/DB2ProcedureAction.java
+++ b/cayenne/src/main/java/org/apache/cayenne/dba/db2/DB2ProcedureAction.java
@@ -22,7 +22,7 @@ import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.OperationObserver;
 import org.apache.cayenne.access.jdbc.ProcedureAction;
 import org.apache.cayenne.access.jdbc.RowDescriptor;
-import org.apache.cayenne.access.translator.procedure.ProcedureTranslator;
+import org.apache.cayenne.access.translator.procedure.TranslatedProcedure;
 import org.apache.cayenne.query.ProcedureQuery;
 
 import java.sql.CallableStatement;
@@ -46,10 +46,14 @@ class DB2ProcedureAction extends ProcedureAction {
 
                processedResultSets = 0;
 
-               ProcedureTranslator transl = createTranslator(connection);
+               TranslatedProcedure translated = 
dataNode.getProcedureTranslator()
+                               .translate(query, dataNode.getAdapter(), 
dataNode.getEntityResolver());
 
-               try (CallableStatement statement = (CallableStatement) 
transl.createStatement();) {
+               dataNode.getJdbcEventLogger().logQuery(translated.sql(), 
translated.bindings());
+
+               try (CallableStatement statement = 
connection.prepareCall(translated.sql());) {
                        initStatement(statement);
+                       bindParameters(statement, translated);
                        boolean hasResultSet = statement.execute();
 
                        // read out parameters
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLActionBuilder.java 
b/cayenne/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLActionBuilder.java
index 6a83b0b30..1d82e8c4b 100644
--- a/cayenne/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLActionBuilder.java
+++ b/cayenne/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLActionBuilder.java
@@ -19,14 +19,9 @@
 
 package org.apache.cayenne.dba.hsqldb;
 
-import java.sql.Connection;
-
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.jdbc.ProcedureAction;
-import org.apache.cayenne.access.translator.procedure.ProcedureTranslator;
 import org.apache.cayenne.dba.JdbcActionBuilder;
 import org.apache.cayenne.query.FluentSelect;
-import org.apache.cayenne.query.ProcedureQuery;
 import org.apache.cayenne.query.SQLAction;
 
 class HSQLActionBuilder extends JdbcActionBuilder {
@@ -43,21 +38,4 @@ class HSQLActionBuilder extends JdbcActionBuilder {
         return new HSQLSelectAction(query, dataNode);
     }
 
-    @Override
-    public SQLAction procedureAction(ProcedureQuery query) {
-        return new ProcedureAction(query, dataNode) {
-
-            @Override
-            protected ProcedureTranslator createTranslator(Connection 
connection) {
-                ProcedureTranslator transl = new HSQLDBProcedureTranslator();
-                transl.setAdapter(dataNode.getAdapter());
-                transl.setQuery(query);
-                transl.setEntityResolver(dataNode.getEntityResolver());
-                transl.setConnection(connection);
-                transl.setJdbcEventLogger(dataNode.getJdbcEventLogger());
-                return transl;
-            }
-        };
-    }
-
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java 
b/cayenne/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java
index ad92da895..2ab9b619f 100644
--- a/cayenne/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java
+++ b/cayenne/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java
@@ -24,6 +24,7 @@ import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.translator.ejbql.JdbcEJBQLTranslatorFactory;
+import org.apache.cayenne.access.translator.procedure.ProcedureTranslator;
 import org.apache.cayenne.access.types.CharType;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
@@ -38,6 +39,8 @@ import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbJoin;
 import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.ProcedureQuery;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.resource.ResourceLocator;
@@ -95,6 +98,14 @@ public class HSQLDBAdapter extends JdbcAdapter {
                return new HSQLTreeProcessor();
        }
 
+       /**
+        * @since 5.0
+        */
+       @Override
+       public ProcedureTranslator getProcedureTranslator(ProcedureQuery query, 
EntityResolver entityResolver) {
+               return new HSQLDBProcedureTranslator();
+       }
+
        /**
         * @since 4.0
         */
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBProcedureTranslator.java
 
b/cayenne/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBProcedureTranslator.java
index a238df78c..68ba31e11 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBProcedureTranslator.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBProcedureTranslator.java
@@ -19,26 +19,25 @@
 
 package org.apache.cayenne.dba.hsqldb;
 
-import org.apache.cayenne.access.translator.procedure.ProcedureTranslator;
+import 
org.apache.cayenne.access.translator.procedure.DefaultProcedureTranslator;
 import org.apache.cayenne.map.Procedure;
 
 /**
  * Works around HSQLDB's pickiness about stored procedure syntax.
- * 
+ *
  * @since 1.2
  */
-public class HSQLDBProcedureTranslator extends ProcedureTranslator {
+public class HSQLDBProcedureTranslator extends DefaultProcedureTranslator {
 
     /**
      * Creates HSQLDB-compliant SQL to execute a stored procedure.
      */
     @Override
-    protected String createSqlString() {
-        Procedure procedure = getProcedure();
+    protected String createSqlString(Procedure procedure, int callParamsSize) {
 
         StringBuilder buf = new StringBuilder();
 
-        int totalParams = callParams.size();
+        int totalParams = callParamsSize;
 
         // check if procedure returns values
         if (procedure.isReturningValue()) {
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java 
b/cayenne/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java
index 907e94e6b..14c537586 100644
--- a/cayenne/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java
+++ b/cayenne/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java
@@ -22,6 +22,7 @@ package org.apache.cayenne.dba.mysql;
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.procedure.ProcedureTranslator;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.translator.ejbql.JdbcEJBQLTranslatorFactory;
 import org.apache.cayenne.access.types.ByteArrayType;
@@ -44,6 +45,8 @@ import org.apache.cayenne.dba.TypesMapping;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.ProcedureQuery;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.resource.ResourceLocator;
@@ -122,6 +125,14 @@ public class MySQLAdapter extends JdbcAdapter {
                return query.createSQLAction(new MySQLActionBuilder(node));
        }
 
+       /**
+        * @since 5.0
+        */
+       @Override
+       public ProcedureTranslator getProcedureTranslator(ProcedureQuery query, 
EntityResolver entityResolver) {
+               return new MySQLProcedureTranslator();
+       }
+
        /**
         * @since 3.0
         */
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/dba/mysql/MySQLProcedureAction.java 
b/cayenne/src/main/java/org/apache/cayenne/dba/mysql/MySQLProcedureAction.java
index 694d3728b..10982a2f5 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/dba/mysql/MySQLProcedureAction.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/dba/mysql/MySQLProcedureAction.java
@@ -22,7 +22,7 @@ import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.OperationObserver;
 import org.apache.cayenne.access.jdbc.ProcedureAction;
 import org.apache.cayenne.access.jdbc.RowDescriptor;
-import org.apache.cayenne.access.translator.procedure.ProcedureTranslator;
+import org.apache.cayenne.access.translator.procedure.TranslatedProcedure;
 import org.apache.cayenne.query.ProcedureQuery;
 
 import java.sql.CallableStatement;
@@ -45,9 +45,13 @@ class MySQLProcedureAction extends ProcedureAction {
 
                processedResultSets = 0;
 
-               ProcedureTranslator transl = createTranslator(connection);
+               TranslatedProcedure translated = 
dataNode.getProcedureTranslator()
+                               .translate(query, dataNode.getAdapter(), 
dataNode.getEntityResolver());
 
-               try (CallableStatement statement = (CallableStatement) 
transl.createStatement();) {
+               dataNode.getJdbcEventLogger().logQuery(translated.sql(), 
translated.bindings());
+
+               try (CallableStatement statement = 
connection.prepareCall(translated.sql());) {
+                       bindParameters(statement, translated);
 
                        // this is one difference with super - we need to read 
the first
                        // result set
@@ -102,36 +106,4 @@ class MySQLProcedureAction extends ProcedureAction {
 
                return true;
        }
-
-       /**
-        * Creates a translator that adds parenthesis to no-param queries.
-        */
-       // see CAY-750 for the problem description
-       @Override
-       protected ProcedureTranslator createTranslator(Connection connection) {
-               ProcedureTranslator translator = new MySQLProcedureTranslator();
-               translator.setAdapter(dataNode.getAdapter());
-               translator.setQuery(query);
-               translator.setEntityResolver(dataNode.getEntityResolver());
-               translator.setConnection(connection);
-               translator.setJdbcEventLogger(dataNode.getJdbcEventLogger());
-               return translator;
-       }
-
-       // same as postgres translator - should we make this the default?
-       static class MySQLProcedureTranslator extends ProcedureTranslator {
-
-               @Override
-               protected String createSqlString() {
-
-                       String sql = super.createSqlString();
-
-                       // add empty parameter parenthesis
-                       if (sql.endsWith("}") && !sql.endsWith(")}")) {
-                               sql = sql.substring(0, sql.length() - 1) + 
"()}";
-                       }
-
-                       return sql;
-               }
-       }
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/dba/mysql/MySQLProcedureTranslator.java
 
b/cayenne/src/main/java/org/apache/cayenne/dba/mysql/MySQLProcedureTranslator.java
new file mode 100644
index 000000000..6927e36f1
--- /dev/null
+++ 
b/cayenne/src/main/java/org/apache/cayenne/dba/mysql/MySQLProcedureTranslator.java
@@ -0,0 +1,45 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.dba.mysql;
+
+import 
org.apache.cayenne.access.translator.procedure.DefaultProcedureTranslator;
+import org.apache.cayenne.map.Procedure;
+
+/**
+ * A translator that adds parenthesis to no-param queries.
+ *
+ * @since 5.0
+ */
+// see CAY-750 for the problem description
+public class MySQLProcedureTranslator extends DefaultProcedureTranslator {
+
+    @Override
+    protected String createSqlString(Procedure procedure, int callParamsSize) {
+
+        String sql = super.createSqlString(procedure, callParamsSize);
+
+        // add empty parameter parenthesis
+        if (sql.endsWith("}") && !sql.endsWith(")}")) {
+            sql = sql.substring(0, sql.length() - 1) + "()}";
+        }
+
+        return sql;
+    }
+}
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java 
b/cayenne/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java
index a18f5d68b..7420887f8 100644
--- a/cayenne/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java
+++ b/cayenne/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java
@@ -23,6 +23,7 @@ import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.procedure.ProcedureTranslator;
 import org.apache.cayenne.access.types.CharType;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
@@ -37,6 +38,8 @@ import org.apache.cayenne.dba.TypesMapping;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.ProcedureQuery;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.resource.ResourceLocator;
@@ -95,6 +98,14 @@ public class PostgresAdapter extends JdbcAdapter {
         return query.createSQLAction(new PostgresActionBuilder(node));
     }
 
+    /**
+     * @since 5.0
+     */
+    @Override
+    public ProcedureTranslator getProcedureTranslator(ProcedureQuery query, 
EntityResolver entityResolver) {
+        return new PostgresProcedureTranslator();
+    }
+
     /**
      * Installs appropriate ExtendedTypes as converters for passing values
      * between JDBC and Java layers.
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/dba/postgres/PostgresProcedureAction.java
 
b/cayenne/src/main/java/org/apache/cayenne/dba/postgres/PostgresProcedureAction.java
index a2a4d8ce1..76dd10352 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/dba/postgres/PostgresProcedureAction.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/dba/postgres/PostgresProcedureAction.java
@@ -19,10 +19,7 @@
 
 package org.apache.cayenne.dba.postgres;
 
-import java.sql.Connection;
-
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.translator.procedure.ProcedureTranslator;
 import org.apache.cayenne.dba.sqlserver.SQLServerProcedureAction;
 import org.apache.cayenne.query.ProcedureQuery;
 
@@ -30,8 +27,9 @@ import org.apache.cayenne.query.ProcedureQuery;
  * Current implementation simply relies on SQLServerProcedureAction superclass 
behavior.
  * Namely that CallableStatement.execute() rewinds result set pointer so
  * CallableStatement.getMoreResults() shouldn't be invoked until the first 
result set is
- * processed.
- * 
+ * processed. The no-param parenthesis quirk (CAY-750) is handled by
+ * {@link PostgresProcedureTranslator}.
+ *
  * @since 1.2
  */
 class PostgresProcedureAction extends SQLServerProcedureAction {
@@ -39,35 +37,4 @@ class PostgresProcedureAction extends 
SQLServerProcedureAction {
     PostgresProcedureAction(ProcedureQuery query, DataNode dataNode) {
         super(query, dataNode);
     }
-
-    /**
-     * Creates a translator that adds parenthesis to no-param queries.
-     */
-    // see CAY-750 for the problem description
-    @Override
-    protected ProcedureTranslator createTranslator(Connection connection) {
-        ProcedureTranslator translator = new PostgresProcedureTranslator();
-        translator.setAdapter(dataNode.getAdapter());
-        translator.setQuery(query);
-        translator.setEntityResolver(dataNode.getEntityResolver());
-        translator.setConnection(connection);
-        translator.setJdbcEventLogger(dataNode.getJdbcEventLogger());
-        return translator;
-    }
-
-    static class PostgresProcedureTranslator extends ProcedureTranslator {
-
-        @Override
-        protected String createSqlString() {
-
-            String sql = super.createSqlString();
-
-            // add empty parameter parenthesis
-            if (sql.endsWith("}") && !sql.endsWith(")}")) {
-                sql = sql.substring(0, sql.length() - 1) + "()}";
-            }
-
-            return sql;
-        }
-    }
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/dba/postgres/PostgresProcedureTranslator.java
 
b/cayenne/src/main/java/org/apache/cayenne/dba/postgres/PostgresProcedureTranslator.java
new file mode 100644
index 000000000..eb807e18b
--- /dev/null
+++ 
b/cayenne/src/main/java/org/apache/cayenne/dba/postgres/PostgresProcedureTranslator.java
@@ -0,0 +1,45 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.dba.postgres;
+
+import 
org.apache.cayenne.access.translator.procedure.DefaultProcedureTranslator;
+import org.apache.cayenne.map.Procedure;
+
+/**
+ * A translator that adds parenthesis to no-param queries.
+ *
+ * @since 5.0
+ */
+// see CAY-750 for the problem description
+public class PostgresProcedureTranslator extends DefaultProcedureTranslator {
+
+    @Override
+    protected String createSqlString(Procedure procedure, int callParamsSize) {
+
+        String sql = super.createSqlString(procedure, callParamsSize);
+
+        // add empty parameter parenthesis
+        if (sql.endsWith("}") && !sql.endsWith(")}")) {
+            sql = sql.substring(0, sql.length() - 1) + "()}";
+        }
+
+        return sql;
+    }
+}
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerProcedureAction.java
 
b/cayenne/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerProcedureAction.java
index 0ea3e0cd4..705df6a3d 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerProcedureAction.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerProcedureAction.java
@@ -25,7 +25,7 @@ import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.OperationObserver;
 import org.apache.cayenne.access.jdbc.ProcedureAction;
 import org.apache.cayenne.access.jdbc.RowDescriptor;
-import org.apache.cayenne.access.translator.procedure.ProcedureTranslator;
+import org.apache.cayenne.access.translator.procedure.TranslatedProcedure;
 import org.apache.cayenne.query.ProcedureQuery;
 import org.apache.cayenne.query.Query;
 
@@ -57,9 +57,14 @@ public class SQLServerProcedureAction extends 
ProcedureAction {
        @Override
        public void performAction(Connection connection, OperationObserver 
observer) throws Exception {
 
-               ProcedureTranslator transl = createTranslator(connection);
+               TranslatedProcedure translated = 
dataNode.getProcedureTranslator()
+                               .translate(query, dataNode.getAdapter(), 
dataNode.getEntityResolver());
+
+               dataNode.getJdbcEventLogger().logQuery(translated.sql(), 
translated.bindings());
+
+               try (CallableStatement statement = 
connection.prepareCall(translated.sql());) {
+                       bindParameters(statement, translated);
 
-               try (CallableStatement statement = (CallableStatement) 
transl.createStatement();) {
                        // stored procedure may contain a mixture of update 
counts and
                        // result sets,
                        // and out parameters. Read out parameters first, then
diff --git 
a/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataDomainProviderTest.java
 
b/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataDomainProviderTest.java
index cf8ea1cd6..1dfd33bd3 100644
--- 
a/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataDomainProviderTest.java
+++ 
b/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataDomainProviderTest.java
@@ -36,6 +36,8 @@ import 
org.apache.cayenne.access.translator.batch.BatchTranslator;
 import org.apache.cayenne.access.translator.batch.DeleteBatchTranslator;
 import org.apache.cayenne.access.translator.batch.InsertBatchTranslator;
 import org.apache.cayenne.access.translator.batch.UpdateBatchTranslator;
+import 
org.apache.cayenne.access.translator.procedure.DbAdapterDelegatedProcedureTranslator;
+import org.apache.cayenne.access.translator.procedure.ProcedureTranslator;
 import 
org.apache.cayenne.access.translator.select.DbAdapterDelegatedSelectTranslator;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
 import org.apache.cayenne.access.types.DefaultValueObjectTypeRegistry;
@@ -228,6 +230,7 @@ public class DataDomainProviderTest {
             b.bind(Key.get(BatchTranslator.class, 
BatchTranslator.UPDATE)).to(UpdateBatchTranslator.class);
             b.bind(Key.get(BatchTranslator.class, 
BatchTranslator.DELETE)).to(DeleteBatchTranslator.class);
             
b.bind(SelectTranslator.class).to(DbAdapterDelegatedSelectTranslator.class);
+            
b.bind(ProcedureTranslator.class).to(DbAdapterDelegatedProcedureTranslator.class);
 
             b.bind(DataSourceFactory.class).toInstance(new 
MockDataSourceFactory());
             b.bind(JdbcEventLogger.class).to(Slf4jJdbcEventLogger.class);

Reply via email to