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 acb9ed4548b4ed49351a3eceb26e54b3fb01470d
Author: Andrus Adamchik <[email protected]>
AuthorDate: Fri Jul 3 15:32:02 2026 -0400

    CAY-2970 Tighten deferred value resolution contract on commit
---
 RELEASE-NOTES.txt                                  |  1 +
 UPGRADE.md                                         | 14 ++++--
 .../transformer/DefaultBindingsTransformer.java    |  6 +--
 .../ObjectIdPropagatedValueFactory.java            |  5 +-
 .../org/apache/cayenne/access/DeferredValue.java   | 57 ++++++++++++++++++++++
 .../apache/cayenne/access/flush/EffectiveOpId.java | 30 +++---------
 .../access/flush/ObjectIdValueSupplier.java        |  4 +-
 .../cayenne/access/flush/ReplacementIdVisitor.java | 14 ++----
 .../cayenne/access/jdbc/PSBatchParameter.java      | 30 ++----------
 9 files changed, 91 insertions(+), 70 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index fa434092c..285df0dea 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -19,6 +19,7 @@ CAY-2957 Get rid of adapter for legacy HSQLDB <= 1.8
 CAY-2962 Allow unconstrained VARCHAR
 CAY-2963 Replace TypesHandler / types.xml with hardcoded map
 CAY-2969 Extender API for "soft" delete 
+CAY-2970 Tighten deferred value resolution contract on commit
 
 Bug Fixes:
 
diff --git a/UPGRADE.md b/UPGRADE.md
index 5e9a679b3..d6515f4f1 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -53,7 +53,7 @@ Expression caseWhenExp = caseWhen(
 ```
 
 
-## Upgrading to 5.0.M3
+## Upgrading to 5.0-M3
 
 
 * Per [CAY-2954](https://issues.apache.org/jira/browse/CAY-2954) selecting 
queries are no longer wrapped in
@@ -73,7 +73,15 @@ solution may be changing to "joint" prefetches.
   `org.apache.cayenne.dba.hsqldb.HSQLDBNoSchemaAdapter` no longer exists, and 
the `HSQLDBSniffer` now maps all. If you
   happen to be on those older HSQL versions, update to the latest one.
 
-## Upgrading to 5.0.M2
+* Per [CAY-2970](https://issues.apache.org/jira/browse/CAY-2970) deferred 
batch parameter values (e.g. a generated PK
+  propagated to a dependent PK or FK within the same transaction) are now 
represented by the dedicated
+  `org.apache.cayenne.access.DeferredValue` type instead of a bare 
`java.util.function.Supplier`. Cayenne now resolves
+  only its own `DeferredValue` instances, leaving user-supplied `Supplier` 
attribute values untouched. If you have
+  custom code that fed deferred values into batch bindings or `ObjectId` 
snapshots via `Supplier`, implement
+  `DeferredValue` instead — it is a `@FunctionalInterface`, so an existing 
lambda or `Supplier` implementation can
+  usually be adapted with a minimal change.
+
+## Upgrading to 5.0-M2
 
 * Per [CAY-2947](https://issues.apache.org/jira/browse/CAY-2947) the 
`cayenne-commitlog` artifact has been removed. Commit log support is now part 
of the
   core `cayenne` artifact — no extra dependency needed. Migrate as follows:
@@ -129,7 +137,7 @@ solution may be changing to "joint" prefetches.
   `DataNode` is now used directly wherever `QueryEngine` was previously 
referenced. So you must subclass `DataNode` 
   and override `performQueries()` if you previously implemented a custom 
`QueryEngine`.
 
-## Upgrading to 5.0.M1
+## Upgrading to 5.0-M1
 
 * Per [CAY-2737](https://issues.apache.org/jira/browse/CAY-2737) All code 
deprecated in Cayenne 4.1 and 4.2 was deleted — please review your code before
   upgrading. Most notable removals are `SelectQuery` and these Cayenne modules:
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 bbbbe7009..abd18eff6 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
@@ -19,9 +19,9 @@
 package org.apache.cayenne.crypto.transformer;
 
 import java.util.Arrays;
-import java.util.function.Supplier;
 
 import org.apache.cayenne.access.jdbc.PSBatchParameter;
+import org.apache.cayenne.access.DeferredValue;
 import org.apache.cayenne.crypto.transformer.bytes.BytesEncryptor;
 import org.apache.cayenne.crypto.transformer.value.ValueEncryptor;
 
@@ -52,9 +52,9 @@ public class DefaultBindingsTransformer implements 
BindingsTransformer {
             Object[] encrypted = new Object[values.length];
             for (int r = 0; r < values.length; r++) {
                 Object value = values[r];
-                if (value instanceof Supplier<?> deferred) {
+                if (value instanceof DeferredValue deferred) {
                     // a deferred value can only be encrypted after it is 
resolved at bind time
-                    encrypted[r] = (Supplier<Object>) () -> 
transformer.encrypt(encryptor, deferred.get());
+                    encrypted[r] = (DeferredValue) () -> 
transformer.encrypt(encryptor, deferred.get());
                 } else {
                     encrypted[r] = transformer.encrypt(encryptor, value);
                 }
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/relationship/ObjectIdPropagatedValueFactory.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/relationship/ObjectIdPropagatedValueFactory.java
index 0c865030a..d5066a09f 100644
--- 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/relationship/ObjectIdPropagatedValueFactory.java
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/relationship/ObjectIdPropagatedValueFactory.java
@@ -18,15 +18,14 @@
  ****************************************************************/
 package org.apache.cayenne.lifecycle.relationship;
 
-import java.util.function.Supplier;
-
 import org.apache.cayenne.Persistent;
+import org.apache.cayenne.access.DeferredValue;
 import org.apache.cayenne.lifecycle.id.IdCoder;
 
 /**
  * @since 3.1
  */
-class ObjectIdPropagatedValueFactory implements Supplier {
+class ObjectIdPropagatedValueFactory implements DeferredValue {
 
     private IdCoder referenceableHandler;
     private Persistent to;
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DeferredValue.java 
b/cayenne/src/main/java/org/apache/cayenne/access/DeferredValue.java
new file mode 100644
index 000000000..36cd446cb
--- /dev/null
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DeferredValue.java
@@ -0,0 +1,57 @@
+/*****************************************************************
+ *   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;
+
+import org.apache.cayenne.CayenneRuntimeException;
+
+/**
+ * A value that is not known until a preceding row of the same transaction has 
been executed, e.g. a generated PK
+ * propagated to a dependent PK or to a foreign key. Such a value is captured 
when a batch is planned and resolved
+ * lazily via {@link #resolve(Object)} when the owning row is bound, after the 
rows it depends on have been executed.
+ *
+ * @since 5.0
+ */
+@FunctionalInterface
+public interface DeferredValue {
+
+    int MAX_NESTED_LEVEL = 1000;
+
+    /**
+     * Returns the current value, which may itself be another {@link 
DeferredValue}. Use {@link #resolve(Object)} to
+     * collapse a full chain.
+     */
+    Object get();
+
+    /**
+     * Fully resolves a value that may be a {@link DeferredValue}, following 
nested chains all the way down. A value
+     * that is not a {@code DeferredValue} is returned as is. Throws if the 
chain does not terminate, guarding against
+     * a value that (incorrectly) resolves to itself.
+     */
+    static Object resolve(Object value) {
+        int safeguard = 0;
+        while (value instanceof DeferredValue deferred) {
+            if (++safeguard > MAX_NESTED_LEVEL) {
+                throw new CayenneRuntimeException("Possible recursive deferred 
value chain: %s", value);
+            }
+            value = deferred.get();
+        }
+        return value;
+    }
+}
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/EffectiveOpId.java 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/EffectiveOpId.java
index 458125d3b..6f4c8fa52 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/flush/EffectiveOpId.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/flush/EffectiveOpId.java
@@ -19,12 +19,11 @@
 
 package org.apache.cayenne.access.flush;
 
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.access.DeferredValue;
+
 import java.util.HashMap;
 import java.util.Map;
-import java.util.function.Supplier;
-
-import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.ObjectId;
 
 /**
  * Helper value-object class that used to compare operations by "effective" id 
(i.e. by id snapshot,
@@ -33,7 +32,6 @@ import org.apache.cayenne.ObjectId;
  * @since 4.2
  */
 public class EffectiveOpId {
-    private static final int MAX_NESTED_SUPPLIER_LEVEL = 1000;
 
     private final String entityName;
     private final Map<String, Object> snapshot;
@@ -53,28 +51,14 @@ public class EffectiveOpId {
 
     private EffectiveOpId(ObjectId id, String entityName, Map<String, Object> 
idSnapshot) {
         this.entityName = entityName;
-        if(idSnapshot.size() == 1 && !(idSnapshot.values().iterator().next() 
instanceof Supplier)) {
+        if(idSnapshot.size() == 1 && !(idSnapshot.values().iterator().next() 
instanceof DeferredValue)) {
             this.snapshot = idSnapshot;
         } else {
             this.snapshot = new HashMap<>(idSnapshot.size());
             idSnapshot.forEach((key, value) -> {
-                Object initial = value;
-                int safeguard = 0;
-                while (value instanceof Supplier && safeguard < 
MAX_NESTED_SUPPLIER_LEVEL) {
-                    value = ((Supplier<?>) value).get();
-                    safeguard++;
-                }
-
-                // simple guard from recursive Suppliers
-                if (safeguard == MAX_NESTED_SUPPLIER_LEVEL) {
-                    throw new CayenneRuntimeException("Possible recursive 
supplier chain for PK value: key '%s'", key);
-                }
-
-                if (value != null) {
-                    this.snapshot.put(key, value);
-                } else {
-                    this.snapshot.put(key, initial);
-                }
+                Object resolved = DeferredValue.resolve(value);
+                // keep the unresolved value if it resolves to null, 
preserving the original snapshot entry
+                this.snapshot.put(key, resolved != null ? resolved : value);
             });
         }
         this.id = id;
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/ObjectIdValueSupplier.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/ObjectIdValueSupplier.java
index eac62efcc..7eda79d63 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/ObjectIdValueSupplier.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/ObjectIdValueSupplier.java
@@ -20,9 +20,9 @@
 package org.apache.cayenne.access.flush;
 
 import java.util.Objects;
-import java.util.function.Supplier;
 
 import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.access.DeferredValue;
 import org.apache.cayenne.access.types.InternalUnsupportedTypeFactory;
 
 /**
@@ -30,7 +30,7 @@ import 
org.apache.cayenne.access.types.InternalUnsupportedTypeFactory;
  *
  * @since 4.2
  */
-class ObjectIdValueSupplier implements Supplier<Object>, 
InternalUnsupportedTypeFactory.Marker {
+class ObjectIdValueSupplier implements DeferredValue, 
InternalUnsupportedTypeFactory.Marker {
 
     private final ObjectId id;
     private final String attribute;
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 29043e600..164ffa711 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
@@ -20,12 +20,12 @@
 package org.apache.cayenne.access.flush;
 
 import java.util.Map;
-import java.util.function.Supplier;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.Persistent;
 import org.apache.cayenne.access.ObjectStore;
+import org.apache.cayenne.access.DeferredValue;
 import org.apache.cayenne.access.flush.operation.DbRowOp;
 import org.apache.cayenne.access.flush.operation.DbRowOpVisitor;
 import org.apache.cayenne.access.flush.operation.InsertDbRowOp;
@@ -56,11 +56,9 @@ class ReplacementIdVisitor implements DbRowOpVisitor<Void> {
         updateId(dbRow);
         dbRow.getValues().getFlattenedIds().forEach((path, id) -> {
             if(id.isTemporary() && id.isReplacementIdAttached()) {
-                // resolve lazy suppliers
+                // resolve lazy deferred values
                 for (Map.Entry<String, Object> next : 
id.getReplacementIdMap().entrySet()) {
-                    if (next.getValue() instanceof Supplier) {
-                        next.setValue(((Supplier<?>) next.getValue()).get());
-                    }
+                    next.setValue(DeferredValue.resolve(next.getValue()));
                 }
                 store.markFlattenedPath(dbRow.getChangeId(), path, 
id.createReplacementId());
             } else {
@@ -93,10 +91,8 @@ class ReplacementIdVisitor implements DbRowOpVisitor<Void> {
             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());
-            }
+            // resolve lazy deferred values, as by now the DB values they 
refer to are known
+            next.setValue(DeferredValue.resolve(next.getValue()));
         }
 
         ObjectId replacementId = id.createReplacementId();
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 3dec7bd18..d45fe58cc 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,17 +19,15 @@
 
 package org.apache.cayenne.access.jdbc;
 
-import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.access.DeferredValue;
 import org.apache.cayenne.map.DbAttribute;
 
-import java.util.function.Supplier;
-
 /**
  * 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
+ * <p>A value may be a {@link DeferredValue}, 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.
  *
@@ -37,30 +35,8 @@ import java.util.function.Supplier;
  */
 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());
-        }
-
+        Object value = DeferredValue.resolve(values[row]);
         values[row] = value;
         return value;
     }

Reply via email to