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

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

commit a3ce1e77df1bbe72ab340756471fe113d618350d
Author: Andrus Adamchik <[email protected]>
AuthorDate: Fri Jul 3 14:21:12 2026 -0400

    Refactor TranslatedBatch
    
    support for deferred values resolution
---
 .../crypto/batch/CryptoBatchTranslator.java        | 17 +------
 .../crypto/transformer/BindingsTransformer.java    |  9 +++-
 .../transformer/DefaultBindingsTransformer.java    | 46 +++++++++---------
 .../transformer/DefaultTransformerFactory.java     |  5 +-
 .../crypto/transformer/TransformerFactory.java     |  5 +-
 .../access/flush/ArcValuesCreationHandler.java     |  5 +-
 .../cayenne/access/flush/ReplacementIdVisitor.java | 10 ++--
 .../apache/cayenne/access/jdbc/BatchAction.java    | 41 ++++++++++++++--
 .../cayenne/access/jdbc/PSBatchParameter.java      | 47 +++++++++++++++----
 .../translator/batch/BaseBatchTranslator.java      | 26 +++++++----
 .../access/translator/batch/BatchRowBinder.java    | 36 ---------------
 .../translator/batch/DeleteBatchTranslator.java    | 42 ++++++-----------
 .../translator/batch/InsertBatchTranslator.java    | 28 ++++-------
 .../access/translator/batch/TranslatedBatch.java   | 17 ++-----
 .../translator/batch/UpdateBatchTranslator.java    | 33 +++++--------
 .../org/apache/cayenne/query/BatchQueryRow.java    | 54 +++-------------------
 .../org/apache/cayenne/query/DeleteBatchQuery.java |  7 +--
 .../org/apache/cayenne/query/InsertBatchQuery.java |  4 +-
 18 files changed, 185 insertions(+), 247 deletions(-)

diff --git 
a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslator.java
 
b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslator.java
index 7615ea191..60d7c490f 100644
--- 
a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslator.java
+++ 
b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslator.java
@@ -18,8 +18,6 @@
  ****************************************************************/
 package org.apache.cayenne.crypto.batch;
 
-import org.apache.cayenne.access.jdbc.PSParameter;
-import org.apache.cayenne.access.jdbc.PSBatchParameter;
 import org.apache.cayenne.access.translator.batch.BatchTranslator;
 import org.apache.cayenne.access.translator.batch.TranslatedBatch;
 import org.apache.cayenne.crypto.transformer.BindingsTransformer;
@@ -27,7 +25,6 @@ import 
org.apache.cayenne.crypto.transformer.TransformerFactory;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.query.BatchQuery;
-import org.apache.cayenne.query.BatchQueryRow;
 
 /**
  * @since 5.0
@@ -48,20 +45,10 @@ public class CryptoBatchTranslator<T extends BatchQuery> 
implements BatchTransla
     @Override
     public TranslatedBatch translate(T query, DbAdapter adapter) {
         TranslatedBatch translated = delegate.translate(query, adapter);
-        BindingsTransformer encryptor = 
transformerFactory.encryptor(translated.bindings(), adapter.getExtendedTypes());
+        BindingsTransformer encryptor = 
transformerFactory.encryptor(translated.bindings());
 
         return encryptor != null
-                ? new TranslatedBatch(translated.sql(), translated.bindings(), 
(bindings, row) -> transform(bindings, row, translated, encryptor))
+                ? new TranslatedBatch(translated.sql(), 
encryptor.transform(translated.bindings()))
                 : translated;
     }
-
-    private static PSParameter[] transform(
-            PSBatchParameter[] template,
-            BatchQueryRow row,
-            TranslatedBatch translated,
-            BindingsTransformer encryptor) {
-        PSParameter<?>[] updated = translated.binder().bind(template, row);
-        encryptor.transform(updated);
-        return updated;
-    }
 }
diff --git 
a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java
 
b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java
index 2ad6a68f0..b9df1f115 100644
--- 
a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java
+++ 
b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java
@@ -18,12 +18,17 @@
  ****************************************************************/
 package org.apache.cayenne.crypto.transformer;
 
-import org.apache.cayenne.access.jdbc.PSParameter;
+import org.apache.cayenne.access.jdbc.PSBatchParameter;
 
 /**
  * @since 4.0
  */
 public interface BindingsTransformer {
 
-    void transform(PSParameter<?>[] bindings);
+    /**
+     * Returns a copy of the given batch parameters with the values of 
encrypted columns transformed.
+     *
+     * @since 5.0
+     */
+    PSBatchParameter[] transform(PSBatchParameter[] bindings);
 }
diff --git 
a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java
 
b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java
index e6a750743..bbbbe7009 100644
--- 
a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java
+++ 
b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java
@@ -18,9 +18,10 @@
  ****************************************************************/
 package org.apache.cayenne.crypto.transformer;
 
-import org.apache.cayenne.access.jdbc.PSParameter;
-import org.apache.cayenne.access.types.ExtendedType;
-import org.apache.cayenne.access.types.ExtendedTypeMap;
+import java.util.Arrays;
+import java.util.function.Supplier;
+
+import org.apache.cayenne.access.jdbc.PSBatchParameter;
 import org.apache.cayenne.crypto.transformer.bytes.BytesEncryptor;
 import org.apache.cayenne.crypto.transformer.value.ValueEncryptor;
 
@@ -32,35 +33,36 @@ public class DefaultBindingsTransformer implements 
BindingsTransformer {
     private final int[] positions;
     private final ValueEncryptor[] transformers;
     private final BytesEncryptor encryptor;
-    private final ExtendedTypeMap extendedTypeMap;
 
-    public DefaultBindingsTransformer(
-            int[] positions,
-            ValueEncryptor[] transformers,
-            BytesEncryptor encryptor,
-            ExtendedTypeMap extendedTypeMap) {
+    public DefaultBindingsTransformer(int[] positions, ValueEncryptor[] 
transformers, BytesEncryptor encryptor) {
         this.positions = positions;
         this.transformers = transformers;
         this.encryptor = encryptor;
-        this.extendedTypeMap = extendedTypeMap;
     }
 
     @Override
-    public void transform(PSParameter<?>[] bindings) {
-
-        int len = positions.length;
+    public PSBatchParameter[] transform(PSBatchParameter[] bindings) {
+        PSBatchParameter[] result = Arrays.copyOf(bindings, bindings.length);
 
-        for (int i = 0; i < len; i++) {
-            PSParameter<?> b = bindings[positions[i]];
-            Object transformed = transformers[i].encrypt(encryptor, b.value());
+        for (int i = 0; i < positions.length; i++) {
+            PSBatchParameter b = bindings[positions[i]];
+            ValueEncryptor transformer = transformers[i];
 
-            ExtendedType<?> extendedType = transformed != null
-                    ? extendedTypeMap.getRegisteredType(transformed.getClass())
-                    : extendedTypeMap.getDefaultType();
+            Object[] values = b.values();
+            Object[] encrypted = new Object[values.length];
+            for (int r = 0; r < values.length; r++) {
+                Object value = values[r];
+                if (value instanceof Supplier<?> deferred) {
+                    // a deferred value can only be encrypted after it is 
resolved at bind time
+                    encrypted[r] = (Supplier<Object>) () -> 
transformer.encrypt(encryptor, deferred.get());
+                } else {
+                    encrypted[r] = transformer.encrypt(encryptor, value);
+                }
+            }
 
-            bindings[positions[i]] = new PSParameter(transformed, 
b.psPosition(), b.psType(), b.psScale(), extendedType, b.attribute()
-            );
+            result[positions[i]] = new PSBatchParameter(encrypted, 
b.psPosition(), b.psType(), b.psScale(), b.attribute());
         }
-    }
 
+        return result;
+    }
 }
diff --git 
a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java
 
b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java
index 50d1cac5a..9d5c45564 100644
--- 
a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java
+++ 
b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java
@@ -20,7 +20,6 @@ package org.apache.cayenne.crypto.transformer;
 
 import org.apache.cayenne.access.jdbc.RSColumn;
 import org.apache.cayenne.access.jdbc.PSBatchParameter;
-import org.apache.cayenne.access.types.ExtendedTypeMap;
 import org.apache.cayenne.crypto.map.ColumnMapper;
 import org.apache.cayenne.crypto.transformer.bytes.BytesTransformerFactory;
 import org.apache.cayenne.crypto.transformer.value.ValueDecryptor;
@@ -91,7 +90,7 @@ public class DefaultTransformerFactory implements 
TransformerFactory {
     }
 
     @Override
-    public BindingsTransformer encryptor(PSBatchParameter[] bindings, 
ExtendedTypeMap extendedTypeMap) {
+    public BindingsTransformer encryptor(PSBatchParameter[] bindings) {
         int len = bindings.length;
         List<Integer> cryptoColumns = null;
 
@@ -121,7 +120,7 @@ public class DefaultTransformerFactory implements 
TransformerFactory {
                 transformers[i] = transformerFactory.encryptor(b.attribute());
             }
 
-            return new DefaultBindingsTransformer(positions, transformers, 
bytesTransformerFactory.encryptor(), extendedTypeMap);
+            return new DefaultBindingsTransformer(positions, transformers, 
bytesTransformerFactory.encryptor());
         }
 
         return null;
diff --git 
a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java
 
b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java
index 7ed5cc520..21230a968 100644
--- 
a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java
+++ 
b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java
@@ -20,17 +20,16 @@ package org.apache.cayenne.crypto.transformer;
 
 import org.apache.cayenne.access.jdbc.RSColumn;
 import org.apache.cayenne.access.jdbc.PSBatchParameter;
-import org.apache.cayenne.access.types.ExtendedTypeMap;
 
 /**
  * A factory that creates encryption transformers used for processing batch
  * bindings and decryption transformers - for result rows.
- * 
+ *
  * @since 4.0
  */
 public interface TransformerFactory {
 
-    BindingsTransformer encryptor(PSBatchParameter[] bindings, ExtendedTypeMap 
extendedTypeMap);
+    BindingsTransformer encryptor(PSBatchParameter[] bindings);
 
     MapTransformer decryptor(RSColumn[] columns, Object sampleRow);
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/ArcValuesCreationHandler.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/ArcValuesCreationHandler.java
index 6da46d468..c529f791c 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/ArcValuesCreationHandler.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/ArcValuesCreationHandler.java
@@ -234,8 +234,9 @@ class ArcValuesCreationHandler implements 
GraphChangeHandler {
                 }
             }
 
-            // propagated master -> child PK
-            if(id != null && attribute.isPrimaryKey()) {
+            // propagated master -> child PK; only an arc creation defines the 
new id value, an arc deletion
+            // must not stomp it with the old target's value
+            if(add && id != null && attribute.isPrimaryKey()) {
                 id.getReplacementIdMap().put(attribute.getName(), valueToUse);
             }
             if(rowOp != null) {
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/ReplacementIdVisitor.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/ReplacementIdVisitor.java
index 3194e7fff..29043e600 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/ReplacementIdVisitor.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/ReplacementIdVisitor.java
@@ -89,11 +89,15 @@ class ReplacementIdVisitor implements DbRowOpVisitor<Void> {
             return;
         }
         Map<String, Object> replacement = id.getReplacementIdMap();
-        replacement.forEach((attr, val) -> {
-            if(val instanceof IdGenerationMarker) {
+        for (Map.Entry<String, Object> next : replacement.entrySet()) {
+            if (next.getValue() instanceof IdGenerationMarker) {
                 throw new CayenneRuntimeException("PK for the object %s is not 
set during insert.", object);
             }
-        });
+            // resolve lazy suppliers, as by now the DB values they refer to 
are known
+            if (next.getValue() instanceof Supplier) {
+                next.setValue(((Supplier<?>) next.getValue()).get());
+            }
+        }
 
         ObjectId replacementId = id.createReplacementId();
         if (object.getObjectId() == id && 
!replacementId.getEntityName().startsWith(ASTDbPath.DB_PREFIX)) {
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
index eb32da59a..a2b2b4f14 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
@@ -27,6 +27,7 @@ import org.apache.cayenne.access.OperationObserver;
 import org.apache.cayenne.access.OptimisticLockException;
 import org.apache.cayenne.access.jdbc.reader.RowReader;
 import org.apache.cayenne.access.types.ExtendedType;
+import org.apache.cayenne.access.types.ExtendedTypeMap;
 import org.apache.cayenne.access.translator.batch.TranslatedBatch;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.log.JdbcEventLogger;
@@ -124,9 +125,9 @@ public class BatchAction extends BaseSQLAction {
                DbAdapter adapter = dataNode.getAdapter();
 
                try (PreparedStatement statement = prepareStatement(con, sql, 
adapter, generatesKeys)) {
-                       for (BatchQueryRow row : query.getRows()) {
+                       for (int row = 0; row < query.getRows().size(); row++) {
 
-                               PSParameter<?>[] bindings = 
translated.updateBindings(row);
+                               PSParameter<?>[] bindings = 
rowBindings(translated, row, adapter.getExtendedTypes());
                                logger.logQueryParameters("batch bind", 
bindings);
                                for (PSParameter<?> b : bindings) {
                                        adapter.bindParameter(statement, b);
@@ -185,9 +186,11 @@ public class BatchAction extends BaseSQLAction {
                DbAdapter adapter = dataNode.getAdapter();
 
                try (PreparedStatement statement = prepareStatement(connection, 
queryStr, adapter, generatesKeys)) {
-                       for (BatchQueryRow row : query.getRows()) {
+                       List<BatchQueryRow> rows = query.getRows();
+                       for (int i = 0; i < rows.size(); i++) {
+                               BatchQueryRow row = rows.get(i);
 
-                               PSParameter<?>[] bindings = 
translated.updateBindings(row);
+                               PSParameter<?>[] bindings = 
rowBindings(translated, i, adapter.getExtendedTypes());
                                logger.logQueryParameters("bind", bindings);
 
                                for (PSParameter<?> b : bindings) {
@@ -211,6 +214,36 @@ public class BatchAction extends BaseSQLAction {
                }
        }
 
+       /**
+        * Resolves the given batch row against the translated batch 
parameters, returning per-row bindings.
+        */
+       protected PSParameter<?>[] rowBindings(TranslatedBatch translated, int 
row, ExtendedTypeMap extendedTypes) {
+               PSBatchParameter[] template = translated.bindings();
+               PSParameter<?>[] bindings = new PSParameter[template.length];
+               for (int j = 0; j < template.length; j++) {
+                       bindings[j] = bind(template[j], row, extendedTypes);
+               }
+               return bindings;
+       }
+
+       @SuppressWarnings("unchecked")
+       private static <T> PSParameter<T> bind(PSBatchParameter parameter, int 
row, ExtendedTypeMap extendedTypes) {
+               Object value = parameter.getValue(row);
+
+               DbAttribute attribute = parameter.attribute();
+               if (value == null && attribute.isPrimaryKey()) {
+                       String entity = attribute.getEntity() != null ? 
attribute.getEntity().getName() : "<null>";
+                       throw new CayenneRuntimeException("Failed to generate 
PK: %s.%s", entity, attribute.getName());
+               }
+
+               ExtendedType<T> extendedType = value != null
+                               ? 
extendedTypes.getRegisteredType(value.getClass())
+                               : extendedTypes.getDefaultType();
+               return new PSParameter<>((T) value, parameter.psPosition(), 
parameter.psType(), parameter.psScale(),
+                               extendedType, parameter.attribute());
+       }
+
+
        protected PreparedStatement prepareStatement(Connection connection,     
String queryStr,
                                                                                
                 DbAdapter adapter,     boolean generatedKeys) throws 
SQLException {
                return (generatedKeys)
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/PSBatchParameter.java 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/PSBatchParameter.java
index 982d678f5..3dec7bd18 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/PSBatchParameter.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/PSBatchParameter.java
@@ -19,22 +19,49 @@
 
 package org.apache.cayenne.access.jdbc;
 
-import org.apache.cayenne.access.types.ExtendedType;
+import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.map.DbAttribute;
 
+import java.util.function.Supplier;
+
 /**
- * An immutable per-batch parameter binding template, carrying the static 
PreparedStatement position and column
- * metadata shared by all rows of a batch. A per-row {@link PSParameter} is 
produced by
- * {@link #bind(Object, ExtendedType)}.
+ * An immutable batch parameter corresponding to a single PreparedStatement 
placeholder, carrying the values of all
+ * batch rows for that placeholder alongside the static position and column 
metadata. The value of a given row is
+ * resolved via {@link #getValue(int)}.
+ *
+ * <p>A value may be a deferred {@link Supplier}, e.g. when it comes from a 
generated key of another row in the same
+ * transaction. Deferred values are resolved when the corresponding row is 
bound, i.e. after the rows preceding it
+ * have been executed.
  *
  * @since 5.0
  */
-public record PSBatchParameter(int psPosition, int psType, int psScale, 
DbAttribute attribute) {
+public record PSBatchParameter(Object[] values, int psPosition, int psType, 
int psScale, DbAttribute attribute) {
+
+    private static final int MAX_NESTED_SUPPLIER_LEVEL = 1000;
+
+    public Object getValue(int row) {
+        Object value = values[row];
+
+        if (!(value instanceof Supplier)) {
+            return value;
+        }
+
+        int safeguard = 0;
+
+        // Suppliers can be nested, resolve all the way down
+        while (value instanceof Supplier<?> s && safeguard < 
MAX_NESTED_SUPPLIER_LEVEL) {
+            value = s.get();
+            safeguard++;
+        }
+
+        // guard against recursive Suppliers
+        if (safeguard == MAX_NESTED_SUPPLIER_LEVEL) {
+            throw new CayenneRuntimeException(
+                    "Possible recursive supplier chain for the batch value of 
attribute %s",
+                    attribute.getName());
+        }
 
-    /**
-     * Resolves this per-batch template into a per-row {@link PSParameter}.
-     */
-    public <T> PSParameter<T> bind(T value, ExtendedType<T> extendedType) {
-        return new PSParameter<>(value, psPosition, psType, psScale, 
extendedType, attribute);
+        values[row] = value;
+        return value;
     }
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/BaseBatchTranslator.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/BaseBatchTranslator.java
index ad56b7599..0e6b84d07 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/BaseBatchTranslator.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/BaseBatchTranslator.java
@@ -47,25 +47,33 @@ public abstract class BaseBatchTranslator<T extends 
BatchQuery> implements Batch
         String sql = createSql(context);
         PSBatchParameter[] bindings = createBindings(context);
 
-        return new TranslatedBatch(sql, bindings, (template, row) -> 
updateBindings(context, template, row));
+        return new TranslatedBatch(sql, bindings);
     }
 
     protected abstract String createSql(BatchTranslatorContext<T> context);
 
     protected PSBatchParameter[] createBindings(BatchTranslatorContext<T> 
context) {
+        int[] rowValueIndexes = createRowValueIndexes(context);
+        List<BatchQueryRow> rows = context.getQuery().getRows();
+
         PSBatchParameter[] bindings = new 
PSBatchParameter[context.getBindings().size()];
-        int i = 0;
-        for (PSParameter b : context.getBindings()) {
-            bindings[i] = new PSBatchParameter(i + 1, b.psType(), b.psScale(), 
b.attribute());
-            i++;
+        int j = 0;
+        for (PSParameter<?> b : context.getBindings()) {
+            Object[] values = new Object[rows.size()];
+            for (int r = 0; r < values.length; r++) {
+                values[r] = rows.get(r).getValue(rowValueIndexes[j]);
+            }
+            bindings[j] = new PSBatchParameter(values, j + 1, b.psType(), 
b.psScale(), b.attribute());
+            j++;
         }
         return bindings;
     }
 
-    protected abstract PSParameter[] updateBindings(
-            BatchTranslatorContext<T> context,
-            PSBatchParameter[] template,
-            BatchQueryRow row);
+    /**
+     * Returns, for each PreparedStatement placeholder in the order {@link 
#createSql(BatchTranslatorContext)} emits
+     * them, the index of the {@link BatchQueryRow} value that feeds it.
+     */
+    protected abstract int[] createRowValueIndexes(BatchTranslatorContext<T> 
context);
 
     protected String doTranslate(BatchTranslatorContext<T> context, 
NodeBuilder nodeBuilder) {
         Node node = 
context.getAdapter().getSqlTreeProcessor().process(nodeBuilder.build());
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/BatchRowBinder.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/BatchRowBinder.java
deleted file mode 100644
index 99ba70d25..000000000
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/BatchRowBinder.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*****************************************************************
- *   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.batch;
-
-import org.apache.cayenne.access.jdbc.PSBatchParameter;
-import org.apache.cayenne.access.jdbc.PSParameter;
-import org.apache.cayenne.query.BatchQueryRow;
-
-/**
- * A stateless strategy that resolves a batch query's binding templates 
against the values of a single
- * {@link BatchQueryRow}, returning a fresh array of per-row {@link 
PSParameter}s.
- *
- * @since 5.0
- */
-@FunctionalInterface
-public interface BatchRowBinder {
-
-    PSParameter<?>[] bind(PSBatchParameter[] template, BatchQueryRow row);
-}
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java
index 818a3ebf6..91e5d8bed 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java
@@ -19,13 +19,11 @@
 
 package org.apache.cayenne.access.translator.batch;
 
-import org.apache.cayenne.access.jdbc.PSBatchParameter;
+import java.util.Arrays;
+
 import org.apache.cayenne.access.sqlbuilder.DeleteBuilder;
 import org.apache.cayenne.access.sqlbuilder.SQLBuilder;
-import org.apache.cayenne.access.jdbc.PSParameter;
-import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.query.BatchQueryRow;
 import org.apache.cayenne.query.DeleteBatchQuery;
 
 /**
@@ -47,31 +45,19 @@ public class DeleteBatchTranslator extends 
BaseBatchTranslator<DeleteBatchQuery>
     }
 
     @Override
-    protected PSParameter[] updateBindings(
-            BatchTranslatorContext<DeleteBatchQuery> context,
-            PSBatchParameter[] template,
-            BatchQueryRow row) {
-
+    protected int[] 
createRowValueIndexes(BatchTranslatorContext<DeleteBatchQuery> context) {
         DeleteBatchQuery deleteBatch = context.getQuery();
-        PSParameter[] bindings = new PSParameter[template.length];
-        for (int i = 0, position = 0; i < 
deleteBatch.getDbAttributes().size(); i++) {
-            position = updateBinding(context, template, bindings, 
row.getValue(i), position);
-        }
-        return bindings;
-    }
-
-    protected int updateBinding(
-            BatchTranslatorContext<DeleteBatchQuery> context,
-            PSBatchParameter[] template,
-            PSParameter[] bindings,
-            Object value, int position) {
-
-        // skip null attributes... they are translated as "IS NULL"
-        if (value != null) {
-            ExtendedType extendedType = 
context.getAdapter().getExtendedTypes().getRegisteredType(value.getClass());
-            bindings[position] = template[position].bind(value, extendedType);
-            position++;
+        int[] indexes = new int[deleteBatch.getDbAttributes().size()];
+
+        int i = 0;
+        int j = 0;
+        // attributes with null values render as "IS NULL" and have no 
placeholder
+        for (DbAttribute attribute : deleteBatch.getDbAttributes()) {
+            if (!deleteBatch.isNull(attribute)) {
+                indexes[j++] = i;
+            }
+            i++;
         }
-        return position;
+        return Arrays.copyOf(indexes, j);
     }
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java
index c4df88c23..7aa08b0e8 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java
@@ -19,13 +19,11 @@
 
 package org.apache.cayenne.access.translator.batch;
 
-import org.apache.cayenne.access.jdbc.PSBatchParameter;
+import java.util.Arrays;
+
 import org.apache.cayenne.access.sqlbuilder.InsertBuilder;
 import org.apache.cayenne.access.sqlbuilder.SQLBuilder;
-import org.apache.cayenne.access.jdbc.PSParameter;
-import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.query.BatchQueryRow;
 import org.apache.cayenne.query.InsertBatchQuery;
 
 /**
@@ -46,7 +44,7 @@ public class InsertBatchTranslator extends 
BaseBatchTranslator<InsertBatchQuery>
             insertBuilder
                     
.column(SQLBuilder.column(attribute.getName()).attribute(attribute))
                     // We can use here any non-null value, to create attribute 
binding,
-                    // actual value and ExtendedType will be set at 
updateBindings() call.
+                    // actual values and ExtendedTypes are resolved when 
binding each row.
                     .value(SQLBuilder.value(1).attribute(attribute));
         }
 
@@ -54,26 +52,18 @@ public class InsertBatchTranslator extends 
BaseBatchTranslator<InsertBatchQuery>
     }
 
     @Override
-    protected PSParameter[] 
updateBindings(BatchTranslatorContext<InsertBatchQuery> context,
-                                           PSBatchParameter[] template, 
BatchQueryRow row) {
+    protected int[] 
createRowValueIndexes(BatchTranslatorContext<InsertBatchQuery> context) {
         InsertBatchQuery query = context.getQuery();
-        PSParameter[] bindings = new PSParameter[template.length];
+        int[] indexes = new int[query.getDbAttributes().size()];
         int i = 0;
         int j = 0;
         for (DbAttribute attribute : query.getDbAttributes()) {
-            if (excludeInBatch(context, attribute)) {
-                i++;
-                continue;
+            if (!excludeInBatch(context, attribute)) {
+                indexes[j++] = i;
             }
-
-            Object value = row.getValue(i++);
-            ExtendedType extendedType = value != null
-                    ? 
context.getAdapter().getExtendedTypes().getRegisteredType(value.getClass())
-                    : context.getAdapter().getExtendedTypes().getDefaultType();
-            bindings[j] = template[j].bind(value, extendedType);
-            j++;
+            i++;
         }
-        return bindings;
+        return Arrays.copyOf(indexes, j);
     }
 
     protected boolean excludeInBatch(BatchTranslatorContext<InsertBatchQuery> 
context, DbAttribute attribute) {
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/TranslatedBatch.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/TranslatedBatch.java
index 8bc3fc1b5..02125d30b 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/TranslatedBatch.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/TranslatedBatch.java
@@ -20,22 +20,13 @@
 package org.apache.cayenne.access.translator.batch;
 
 import org.apache.cayenne.access.jdbc.PSBatchParameter;
-import org.apache.cayenne.access.jdbc.PSParameter;
-import org.apache.cayenne.query.BatchQueryRow;
 
 /**
- * An immutable result of translating a batch query: the SQL String shared by 
all rows of the batch,
- * the widest possible array of per-batch binding templates, and a stateless 
{@link BatchRowBinder} that
- * resolves a single row's state into per-row bindings.
+ * An immutable result of translating a batch query: the SQL String shared by 
all rows of the batch, and one
+ * {@link PSBatchParameter} per PreparedStatement placeholder, each carrying 
the values of all batch rows for
+ * its placeholder.
  *
  * @since 5.0
  */
-public record TranslatedBatch(String sql, PSBatchParameter[] bindings, 
BatchRowBinder binder) {
-
-    /**
-     * Resolves the given row's state against the binding templates, returning 
a fresh per-row bindings array.
-     */
-    public PSParameter<?>[] updateBindings(BatchQueryRow row) {
-        return binder.bind(bindings, row);
-    }
+public record TranslatedBatch(String sql, PSBatchParameter[] bindings) {
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java
index c0b496275..4580e90c5 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java
@@ -19,13 +19,11 @@
 
 package org.apache.cayenne.access.translator.batch;
 
-import org.apache.cayenne.access.jdbc.PSBatchParameter;
+import java.util.Arrays;
+
 import org.apache.cayenne.access.sqlbuilder.SQLBuilder;
 import org.apache.cayenne.access.sqlbuilder.UpdateBuilder;
-import org.apache.cayenne.access.jdbc.PSParameter;
-import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.query.BatchQueryRow;
 import org.apache.cayenne.query.UpdateBatchQuery;
 
 /**
@@ -55,33 +53,24 @@ public class UpdateBatchTranslator extends 
BaseBatchTranslator<UpdateBatchQuery>
     }
 
     @Override
-    protected PSParameter[] 
updateBindings(BatchTranslatorContext<UpdateBatchQuery> context,
-                                           PSBatchParameter[] template, 
BatchQueryRow row) {
+    protected int[] 
createRowValueIndexes(BatchTranslatorContext<UpdateBatchQuery> context) {
         UpdateBatchQuery updateBatch = context.getQuery();
-        PSParameter[] bindings = new PSParameter[template.length];
+        int updatedCount = updateBatch.getUpdatedAttributes().size();
+        int[] indexes = new int[updatedCount + 
updateBatch.getQualifierAttributes().size()];
 
         int i = 0;
         int j = 0;
-        for (; i < updateBatch.getUpdatedAttributes().size(); i++) {
-            Object value = row.getValue(i);
-            ExtendedType extendedType = value == null
-                    ? context.getAdapter().getExtendedTypes().getDefaultType()
-                    : 
context.getAdapter().getExtendedTypes().getRegisteredType(value.getClass());
-            bindings[j] = template[j].bind(value, extendedType);
-            j++;
+        for (; i < updatedCount; i++) {
+            indexes[j++] = i;
         }
 
+        // qualifier attributes with null values render as "IS NULL" and have 
no placeholder
         for (DbAttribute attribute : updateBatch.getQualifierAttributes()) {
-            if (updateBatch.isNull(attribute)) {
-                i++;
-                continue;
+            if (!updateBatch.isNull(attribute)) {
+                indexes[j++] = i;
             }
-            Object value = row.getValue(i);
-            ExtendedType extendedType = 
context.getAdapter().getExtendedTypes().getRegisteredType(value.getClass());
-            bindings[j] = template[j].bind(value, extendedType);
-            j++;
             i++;
         }
-        return bindings;
+        return Arrays.copyOf(indexes, j);
     }
 }
diff --git a/cayenne/src/main/java/org/apache/cayenne/query/BatchQueryRow.java 
b/cayenne/src/main/java/org/apache/cayenne/query/BatchQueryRow.java
index 0bab779c2..084e422ce 100644
--- a/cayenne/src/main/java/org/apache/cayenne/query/BatchQueryRow.java
+++ b/cayenne/src/main/java/org/apache/cayenne/query/BatchQueryRow.java
@@ -16,24 +16,24 @@
  *  specific language governing permissions and limitations
  *  under the License.
  ****************************************************************/
+
 package org.apache.cayenne.query;
 
 import java.util.Map;
 import java.util.function.Supplier;
 
-import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.map.DbAttribute;
 
 /**
- * Represents a single row of values in a BatchQuery.
- * 
+ * Represents a single row of values in a BatchQuery. A value may be a 
deferred {@link Supplier}, e.g. when it comes
+ * from a generated key of another row in the same transaction. Deferred 
values are resolved downstream, when the
+ * row is bound to a PreparedStatement.
+ *
  * @since 4.0
  */
 public abstract class BatchQueryRow {
 
-    private static final int MAX_NESTED_SUPPLIER_LEVEL = 1000;
-
     protected ObjectId objectId;
     protected Map<String, Object> qualifier;
 
@@ -52,49 +52,7 @@ public abstract class BatchQueryRow {
         return objectId;
     }
 
-    /**
-     * Used by subclasses to resolve deferred values on demand. This is useful
-     * when a certain value comes from a generated key of another master 
object.
-     */
     protected Object getValue(Map<String, Object> valueMap, DbAttribute 
attribute) {
-
-        Object value = valueMap.get(attribute.getName());
-        boolean isSupplier = false;
-        int safeguard = 0;
-
-        // Supplier can be nested, resolve all the way down
-        while(value instanceof Supplier && safeguard < 
MAX_NESTED_SUPPLIER_LEVEL) {
-            value = ((Supplier) value).get();
-            isSupplier = true;
-            safeguard++;
-        }
-
-        // simple guard from recursive Suppliers
-        if(safeguard == MAX_NESTED_SUPPLIER_LEVEL) {
-            throw new CayenneRuntimeException("Possible recursive supplier 
chain for batch row value, object %s, attribute %s"
-                    , objectId, attribute.getName());
-        }
-
-        // if a value is a Supplier, resolve it here...
-        if (isSupplier) {
-            valueMap.put(attribute.getName(), value);
-
-            // update replacement id
-            if (attribute.isPrimaryKey()) {
-                // sanity check
-                if (value == null) {
-                    String name = attribute.getEntity() != null ? 
attribute.getEntity().getName() : "<null>";
-                    throw new CayenneRuntimeException("Failed to generate PK: 
%s.%s", name, attribute.getName());
-                }
-
-                ObjectId id = getObjectId();
-                if (id != null) {
-                    // always override with fresh value as this is what's in 
the DB
-                    id.getReplacementIdMap().put(attribute.getName(), value);
-                }
-            }
-        }
-
-        return value;
+        return valueMap.get(attribute.getName());
     }
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/query/DeleteBatchQuery.java 
b/cayenne/src/main/java/org/apache/cayenne/query/DeleteBatchQuery.java
index ca857c6fc..3daea7263 100644
--- a/cayenne/src/main/java/org/apache/cayenne/query/DeleteBatchQuery.java
+++ b/cayenne/src/main/java/org/apache/cayenne/query/DeleteBatchQuery.java
@@ -25,7 +25,6 @@ import org.apache.cayenne.map.DbEntity;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
-import java.util.function.Supplier;
 
 /**
  * Batched delete query.
@@ -83,11 +82,7 @@ public class DeleteBatchQuery extends BatchQuery {
         rows.add(new BatchQueryRow(null, objectId) {
             @Override
             public Object getValue(int i) {
-                Object value = qualifier.get(dbAttributes.get(i).getName());
-                if (value instanceof Supplier) {
-                    return ((Supplier) value).get();
-                }
-                return value;
+                return getValue(qualifier, dbAttributes.get(i));
             }
         });
     }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/query/InsertBatchQuery.java 
b/cayenne/src/main/java/org/apache/cayenne/query/InsertBatchQuery.java
index c004bee95..6a08f0f6d 100644
--- a/cayenne/src/main/java/org/apache/cayenne/query/InsertBatchQuery.java
+++ b/cayenne/src/main/java/org/apache/cayenne/query/InsertBatchQuery.java
@@ -51,8 +51,8 @@ public class InsertBatchQuery extends BatchQuery {
      * Adds a snapshot to batch. Optionally stores the object id for the
      * snapshot. Note that snapshot can hold either the real values or the
      * instances of java.util.Supplier that will be resolved
-     * to the actual value on the spot, thus allowing deferred propagated keys
-     * resolution.
+     * to the actual value when the row is bound to a PreparedStatement, thus
+     * allowing deferred propagated keys resolution.
      * 
      * @since 1.2
      */


Reply via email to