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; }
