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 e08691b901026e050f7089e7288525554f54e6f0
Author: Andrus Adamchik <[email protected]>
AuthorDate: Fri Jul 3 11:43:08 2026 -0400

    CAY-2969 Extender API for "soft" delete
    
    implementing soft delete in the stack instead of intercepting
---
 RELEASE-NOTES.txt                                  |   1 +
 .../dbsync/reverse/configuration/ToolsModule.java  |   3 +
 .../cayenne/access/flush/DbRowOpFactory.java       |  41 ++---
 .../access/flush/DefaultDataDomainFlushAction.java |   8 +-
 .../flush/DefaultDataDomainFlushActionFactory.java |   6 +-
 .../cayenne/access/flush/QueryCreatorVisitor.java  |  45 ++++--
 .../ConditionalSoftDeleteDbRowOpFactory.java       |  53 +++++++
 .../access/flush/operation/DbRowOpVisitor.java     |  10 ++
 ...RowOpVisitor.java => DeleteDbRowOpFactory.java} |  30 ++--
 .../flush/operation/DeleteInsertDbRowOp.java       |   5 +-
 ...pVisitor.java => HardDeleteDbRowOpFactory.java} |  24 +--
 .../access/flush/operation/SoftDeleteDbRowOp.java  |  69 ++++++++
 .../batch/SoftDeleteBatchTranslator.java           | 104 -------------
 .../cayenne/configuration/runtime/CoreModule.java  |   3 +
 .../configuration/runtime/CoreModuleExtender.java  |   9 +-
 .../access/DefaultDataRowStoreFactoryIT.java       |   3 +
 .../org/apache/cayenne/access/SoftDeleteIT.java    |  23 +++
 .../batch/SoftDeleteBatchTranslatorIT.java         | 173 ---------------------
 .../runtime/DataContextFactoryTest.java            |   4 +
 19 files changed, 267 insertions(+), 347 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 40cb8de23..fa434092c 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -18,6 +18,7 @@ CAY-2956 Get rid of a dedicated adapter for Oracle 8
 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 
 
 Bug Fixes:
 
diff --git 
a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/configuration/ToolsModule.java
 
b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/configuration/ToolsModule.java
index 1515f5c14..80e2e0386 100644
--- 
a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/configuration/ToolsModule.java
+++ 
b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/configuration/ToolsModule.java
@@ -23,6 +23,8 @@ import 
org.apache.cayenne.access.flush.DataDomainFlushActionFactory;
 import org.apache.cayenne.access.flush.DefaultDataDomainFlushActionFactory;
 import org.apache.cayenne.access.flush.operation.DbRowOpSorter;
 import org.apache.cayenne.access.flush.operation.DefaultDbRowOpSorter;
+import org.apache.cayenne.access.flush.operation.HardDeleteDbRowOpFactory;
+import org.apache.cayenne.access.flush.operation.DeleteDbRowOpFactory;
 import org.apache.cayenne.access.translator.batch.BatchTranslator;
 import org.apache.cayenne.access.translator.batch.DeleteBatchTranslator;
 import org.apache.cayenne.access.translator.batch.InsertBatchTranslator;
@@ -172,6 +174,7 @@ public class ToolsModule implements Module {
         binder.bind(XMLReader.class).toProviderInstance(new 
XMLReaderProvider(true)).withoutScope();
         
binder.bind(DataDomainFlushActionFactory.class).to(DefaultDataDomainFlushActionFactory.class);
         binder.bind(DbRowOpSorter.class).to(DefaultDbRowOpSorter.class);
+        
binder.bind(DeleteDbRowOpFactory.class).to(HardDeleteDbRowOpFactory.class);
     }
 
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/DbRowOpFactory.java 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/DbRowOpFactory.java
index accf3295d..fd418f8f6 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/flush/DbRowOpFactory.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/flush/DbRowOpFactory.java
@@ -19,12 +19,6 @@
 
 package org.apache.cayenne.access.flush;
 
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.Persistent;
@@ -32,15 +26,19 @@ import org.apache.cayenne.access.ObjectDiff;
 import org.apache.cayenne.access.ObjectStore;
 import org.apache.cayenne.access.flush.operation.DbRowOp;
 import org.apache.cayenne.access.flush.operation.DbRowOpType;
-import org.apache.cayenne.access.flush.operation.DeleteDbRowOp;
+import org.apache.cayenne.access.flush.operation.DeleteDbRowOpFactory;
 import org.apache.cayenne.access.flush.operation.InsertDbRowOp;
 import org.apache.cayenne.access.flush.operation.UpdateDbRowOp;
-import org.apache.cayenne.exp.parser.ASTDbPath;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.map.ObjEntity;
 import org.apache.cayenne.reflect.ClassDescriptor;
 
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
 /**
  * Factory that produces a collection of {@link DbRowOp} from given {@link 
ObjectDiff}.
  *
@@ -53,21 +51,27 @@ class DbRowOpFactory {
     private final Set<ArcTarget> processedArcs;
     private final Map<ObjectId, DbRowOp> dbRows;
     private final RootRowOpProcessor rootRowOpProcessor;
+    private final DeleteDbRowOpFactory deleteDbRowOpFactory;
 
     private ClassDescriptor descriptor;
     private Persistent object;
     private ObjectDiff diff;
 
-    DbRowOpFactory(EntityResolver resolver, ObjectStore store, Set<ArcTarget> 
processedArcs) {
+    DbRowOpFactory(EntityResolver resolver,
+                   ObjectStore store,
+                   Set<ArcTarget> processedArcs,
+                   DeleteDbRowOpFactory deleteDbRowOpFactory) {
+
         this.resolver = resolver;
         this.store = store;
         this.dbRows = new HashMap<>(4);
         this.processedArcs = processedArcs;
         this.rootRowOpProcessor = new RootRowOpProcessor(this);
+        this.deleteDbRowOpFactory = deleteDbRowOpFactory;
     }
 
     private void updateDiff(ObjectDiff diff) {
-        ObjectId id = (ObjectId)diff.getNodeId();
+        ObjectId id = (ObjectId) diff.getNodeId();
         this.diff = diff;
         this.descriptor = resolver.getClassDescriptor(id.getEntityName());
         this.object = (Persistent) store.getNode(id);
@@ -95,7 +99,7 @@ class DbRowOpFactory {
 
     private DbRowOp createRow(DbEntity entity, ObjectId id, DbRowOpType type) {
         // skip phantom nodes, this could be a created and immediately deleted 
relationship
-        if(store.getNode(id) == null && !id.getEntityName().startsWith("db:")) 
{
+        if (store.getNode(id) == null && 
!id.getEntityName().startsWith("db:")) {
             return null;
         }
         switch (type) {
@@ -104,7 +108,7 @@ class DbRowOpFactory {
             case UPDATE:
                 return new UpdateDbRowOp(object, entity, id);
             case DELETE:
-                return new DeleteDbRowOp(object, entity, id);
+                return deleteDbRowOpFactory.createOp(object, entity, id);
         }
         throw new CayenneRuntimeException("Unknown DbRowType '%s'", type);
     }
@@ -125,17 +129,6 @@ class DbRowOpFactory {
         return diff;
     }
 
-    DbEntity getDbEntity(ObjectId id) {
-        String entityName = id.getEntityName();
-        if(entityName.startsWith(ASTDbPath.DB_PREFIX)) {
-            entityName = entityName.substring(ASTDbPath.DB_PREFIX.length());
-            return resolver.getDbEntity(entityName);
-        } else {
-            ObjEntity objEntity = resolver.getObjEntity(entityName);
-            return objEntity.getDbEntity();
-        }
-    }
-
     Set<ArcTarget> getProcessedArcs() {
         return processedArcs;
     }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java
index d67ed20be..d4c3de940 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java
@@ -42,6 +42,7 @@ import 
org.apache.cayenne.access.flush.operation.DbRowOpSorter;
 import org.apache.cayenne.access.flush.operation.DbRowOp;
 import org.apache.cayenne.access.flush.operation.DbRowOpVisitor;
 import org.apache.cayenne.access.flush.operation.DeleteDbRowOp;
+import org.apache.cayenne.access.flush.operation.DeleteDbRowOpFactory;
 import org.apache.cayenne.access.flush.operation.InsertDbRowOp;
 import org.apache.cayenne.access.flush.operation.OpIdFactory;
 import org.apache.cayenne.access.flush.operation.UpdateDbRowOp;
@@ -62,12 +63,15 @@ public class DefaultDataDomainFlushAction implements 
DataDomainFlushAction {
     protected final DbRowOpSorter dbRowOpSorter;
     protected final JdbcEventLogger jdbcEventLogger;
     protected final OperationObserver observer;
+    protected final DeleteDbRowOpFactory deleteDbRowOpFactory;
 
-    protected DefaultDataDomainFlushAction(DataDomain dataDomain, 
DbRowOpSorter dbRowOpSorter, JdbcEventLogger jdbcEventLogger) {
+    protected DefaultDataDomainFlushAction(DataDomain dataDomain, 
DbRowOpSorter dbRowOpSorter,
+                                           JdbcEventLogger jdbcEventLogger, 
DeleteDbRowOpFactory deleteDbRowOpFactory) {
         this.dataDomain = dataDomain;
         this.dbRowOpSorter = dbRowOpSorter;
         this.jdbcEventLogger = jdbcEventLogger;
         this.observer = new FlushObserver(jdbcEventLogger);
+        this.deleteDbRowOpFactory = deleteDbRowOpFactory;
     }
 
     @Override
@@ -111,7 +115,7 @@ public class DefaultDataDomainFlushAction implements 
DataDomainFlushAction {
         List<DbRowOp> ops = new ArrayList<>(changesByObjectId.size());
         Set<ArcTarget> processedArcs = new HashSet<>();
 
-        DbRowOpFactory factory = new DbRowOpFactory(resolver, objectStore, 
processedArcs);
+        DbRowOpFactory factory = new DbRowOpFactory(resolver, objectStore, 
processedArcs, deleteDbRowOpFactory);
         // ops.addAll() method is slower in this case as it will allocate new 
array for all values
         //noinspection UseBulkOperation
         changesByObjectId.forEach((obj, diff) -> 
factory.createRows(diff).forEach(ops::add));
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushActionFactory.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushActionFactory.java
index ad260b1cd..299f79875 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushActionFactory.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushActionFactory.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.access.flush;
 
 import org.apache.cayenne.access.DataDomain;
 import org.apache.cayenne.access.flush.operation.DbRowOpSorter;
+import org.apache.cayenne.access.flush.operation.DeleteDbRowOpFactory;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.log.JdbcEventLogger;
 
@@ -37,8 +38,11 @@ public class DefaultDataDomainFlushActionFactory implements 
DataDomainFlushActio
     @Inject
     private JdbcEventLogger jdbcEventLogger;
 
+    @Inject
+    private DeleteDbRowOpFactory deleteDbRowOpFactory;
+
     @Override
     public DataDomainFlushAction createFlushAction(DataDomain dataDomain) {
-        return new DefaultDataDomainFlushAction(dataDomain, operationSorter, 
jdbcEventLogger);
+        return new DefaultDataDomainFlushAction(dataDomain, operationSorter, 
jdbcEventLogger, deleteDbRowOpFactory);
     }
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/QueryCreatorVisitor.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/QueryCreatorVisitor.java
index 3f7a55f2c..1ac359b77 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/QueryCreatorVisitor.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/QueryCreatorVisitor.java
@@ -19,19 +19,20 @@
 
 package org.apache.cayenne.access.flush;
 
-import java.util.ArrayList;
-import java.util.List;
-
 import org.apache.cayenne.access.flush.operation.DbRowOp;
 import org.apache.cayenne.access.flush.operation.DbRowOpVisitor;
 import org.apache.cayenne.access.flush.operation.DeleteDbRowOp;
 import org.apache.cayenne.access.flush.operation.InsertDbRowOp;
+import org.apache.cayenne.access.flush.operation.SoftDeleteDbRowOp;
 import org.apache.cayenne.access.flush.operation.UpdateDbRowOp;
 import org.apache.cayenne.query.BatchQuery;
 import org.apache.cayenne.query.DeleteBatchQuery;
 import org.apache.cayenne.query.InsertBatchQuery;
 import org.apache.cayenne.query.UpdateBatchQuery;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Visitor that creates batch queries.
  * It relies on correct sorting of {@link DbRowOp} to just linearly scan of 
rows and put them in batches.
@@ -59,12 +60,12 @@ class QueryCreatorVisitor implements DbRowOpVisitor<Void> {
     @Override
     public Void visitInsert(InsertDbRowOp dbRow) {
         InsertBatchQuery query;
-        if(lastRow == null || !lastRow.isSameBatch(dbRow)) {
+        if (lastRow == null || !lastRow.isSameBatch(dbRow)) {
             query = new InsertBatchQuery(dbRow.getEntity(), batchSize);
             queryList.add(query);
             lastBatch = query;
         } else {
-            query = (InsertBatchQuery)lastBatch;
+            query = (InsertBatchQuery) lastBatch;
         }
         query.add(dbRow.getValues().getSnapshot(), dbRow.getChangeId());
         lastRow = dbRow;
@@ -74,12 +75,35 @@ class QueryCreatorVisitor implements DbRowOpVisitor<Void> {
     @Override
     public Void visitUpdate(UpdateDbRowOp dbRow) {
         // skip empty update..
-        if(dbRow.getValues().isEmpty()) {
+        if (dbRow.getValues().isEmpty()) {
             return null;
         }
 
         UpdateBatchQuery query;
-        if(lastRow == null || !lastRow.isSameBatch(dbRow)) {
+        if (lastRow == null || !lastRow.isSameBatch(dbRow)) {
+            query = new UpdateBatchQuery(
+                    dbRow.getEntity(),
+                    dbRow.getQualifier().getQualifierAttributes(),
+                    dbRow.getValues().getUpdatedAttributes(),
+                    dbRow.getQualifier().getNullQualifierNames(),
+                    batchSize
+            );
+            
query.setUsingOptimisticLocking(dbRow.getQualifier().isUsingOptimisticLocking());
+            queryList.add(query);
+            lastBatch = query;
+        } else {
+            query = (UpdateBatchQuery) lastBatch;
+        }
+        query.add(dbRow.getQualifier().getSnapshot(), 
dbRow.getValues().getSnapshot(), dbRow.getChangeId());
+        lastRow = dbRow;
+        return null;
+    }
+
+    @Override
+    public Void visitSoftDelete(SoftDeleteDbRowOp dbRow) {
+        // a soft delete is executed as an update of the soft-delete flag 
column
+        UpdateBatchQuery query;
+        if (lastRow == null || !lastRow.isSameBatch(dbRow)) {
             query = new UpdateBatchQuery(
                     dbRow.getEntity(),
                     dbRow.getQualifier().getQualifierAttributes(),
@@ -91,7 +115,8 @@ class QueryCreatorVisitor implements DbRowOpVisitor<Void> {
             queryList.add(query);
             lastBatch = query;
         } else {
-            query = (UpdateBatchQuery)lastBatch;
+            // dbRow.isSameBatch() guarantees the last batch is an update 
created by this method
+            query = (UpdateBatchQuery) lastBatch;
         }
         query.add(dbRow.getQualifier().getSnapshot(), 
dbRow.getValues().getSnapshot(), dbRow.getChangeId());
         lastRow = dbRow;
@@ -101,7 +126,7 @@ class QueryCreatorVisitor implements DbRowOpVisitor<Void> {
     @Override
     public Void visitDelete(DeleteDbRowOp dbRow) {
         DeleteBatchQuery query;
-        if(lastRow == null || !lastRow.isSameBatch(dbRow)) {
+        if (lastRow == null || !lastRow.isSameBatch(dbRow)) {
             query = new DeleteBatchQuery(
                     dbRow.getEntity(),
                     dbRow.getQualifier().getQualifierAttributes(),
@@ -112,7 +137,7 @@ class QueryCreatorVisitor implements DbRowOpVisitor<Void> {
             queryList.add(query);
             lastBatch = query;
         } else {
-            query = (DeleteBatchQuery)lastBatch;
+            query = (DeleteBatchQuery) lastBatch;
         }
         query.add(dbRow.getQualifier().getSnapshot());
         lastRow = dbRow;
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/ConditionalSoftDeleteDbRowOpFactory.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/ConditionalSoftDeleteDbRowOpFactory.java
new file mode 100644
index 000000000..90be083ad
--- /dev/null
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/ConditionalSoftDeleteDbRowOpFactory.java
@@ -0,0 +1,53 @@
+/*****************************************************************
+ *   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.flush.operation;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+
+import java.sql.Types;
+import java.util.Objects;
+
+/**
+ * A {@link DeleteDbRowOpFactory} enabling "soft delete". For tables that 
contain a BOOLEAN column with the configured
+ * name it produces {@link SoftDeleteDbRowOp}, so the row is deleted by an 
UPDATE setting that column to {@code true}.
+ * Tables without such a column (or whose column of that name is not BOOLEAN) 
get a plain {@link DeleteDbRowOp} and
+ * are deleted with a regular SQL DELETE.
+ *
+ * @since 5.0
+ */
+public class ConditionalSoftDeleteDbRowOpFactory implements 
DeleteDbRowOpFactory {
+
+    private final String columnName;
+
+    public ConditionalSoftDeleteDbRowOpFactory(String columnName) {
+        this.columnName = Objects.requireNonNull(columnName);
+    }
+
+    @Override
+    public DeleteDbRowOp createOp(Persistent object, DbEntity entity, ObjectId 
id) {
+        DbAttribute attr = entity.getAttribute(columnName);
+        return attr != null && attr.getType() == Types.BOOLEAN
+                ? new SoftDeleteDbRowOp(object, entity, id, attr)
+                : new DeleteDbRowOp(object, entity, id);
+    }
+}
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java
index 25a110396..ea432c179 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java
@@ -35,4 +35,14 @@ public interface DbRowOpVisitor<T> {
     default T visitDelete(DeleteDbRowOp dbRow) {
         return null;
     }
+
+    /**
+     * Visits a "soft" delete op. The default implementation delegates to 
{@link #visitDelete(DeleteDbRowOp)}, so
+     * visitors that don't distinguish soft deletes from regular ones 
automatically get correct delete semantics.
+     *
+     * @since 5.0
+     */
+    default T visitSoftDelete(SoftDeleteDbRowOp dbRow) {
+        return visitDelete(dbRow);
+    }
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DeleteDbRowOpFactory.java
similarity index 62%
copy from 
cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java
copy to 
cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DeleteDbRowOpFactory.java
index 25a110396..661616078 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DeleteDbRowOpFactory.java
@@ -19,20 +19,24 @@
 
 package org.apache.cayenne.access.flush.operation;
 
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.map.DbEntity;
+
 /**
- * @since 4.2
+ * A factory of delete row ops used by the commit pipeline.
+ *
+ * @since 5.0
  */
-public interface DbRowOpVisitor<T> {
-
-    default T visitInsert(InsertDbRowOp dbRow) {
-        return null;
-    }
-
-    default T visitUpdate(UpdateDbRowOp dbRow) {
-        return null;
-    }
+public interface DeleteDbRowOpFactory {
 
-    default T visitDelete(DeleteDbRowOp dbRow) {
-        return null;
-    }
+    /**
+     * Creates a delete op for a given object and target table.
+     *
+     * @param object the object being deleted
+     * @param entity the target DbEntity (may be the root table or an 
additional / flattened table)
+     * @param id     the id of the row being deleted
+     * @return a delete op
+     */
+    DeleteDbRowOp createOp(Persistent object, DbEntity entity, ObjectId id);
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DeleteInsertDbRowOp.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DeleteInsertDbRowOp.java
index da6195b96..6394d04d8 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DeleteInsertDbRowOp.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DeleteInsertDbRowOp.java
@@ -38,8 +38,9 @@ public class DeleteInsertDbRowOp extends BaseDbRowOp {
 
     @Override
     public <T> T accept(DbRowOpVisitor<T> visitor) {
-        visitor.visitDelete(delete);
-        return visitor.visitInsert(insert);
+        // double-dispatch through the wrapped ops, so their subclasses (e.g. 
SoftDeleteDbRowOp) route correctly
+        delete.accept(visitor);
+        return insert.accept(visitor);
     }
 
     @Override
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/HardDeleteDbRowOpFactory.java
similarity index 67%
copy from 
cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java
copy to 
cayenne/src/main/java/org/apache/cayenne/access/flush/operation/HardDeleteDbRowOpFactory.java
index 25a110396..a80973f91 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/HardDeleteDbRowOpFactory.java
@@ -19,20 +19,20 @@
 
 package org.apache.cayenne.access.flush.operation;
 
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.map.DbEntity;
+
 /**
- * @since 4.2
+ * Default implementation of {@link DeleteDbRowOpFactory} producing plain 
{@link DeleteDbRowOp} that translate to SQL
+ * DELETE.
+ *
+ * @since 5.0
  */
-public interface DbRowOpVisitor<T> {
-
-    default T visitInsert(InsertDbRowOp dbRow) {
-        return null;
-    }
-
-    default T visitUpdate(UpdateDbRowOp dbRow) {
-        return null;
-    }
+public class HardDeleteDbRowOpFactory implements DeleteDbRowOpFactory {
 
-    default T visitDelete(DeleteDbRowOp dbRow) {
-        return null;
+    @Override
+    public DeleteDbRowOp createOp(Persistent object, DbEntity entity, ObjectId 
id) {
+        return new DeleteDbRowOp(object, entity, id);
     }
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/SoftDeleteDbRowOp.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/SoftDeleteDbRowOp.java
new file mode 100644
index 000000000..3b281ba6f
--- /dev/null
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/SoftDeleteDbRowOp.java
@@ -0,0 +1,69 @@
+/*****************************************************************
+ *   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.flush.operation;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+
+/**
+ * A "soft" delete op that is executed as an UPDATE setting the soft-delete 
flag column to {@code true} instead of a
+ * SQL DELETE, leaving the row physically in the table. For all other purposes 
— sorting, merging, snapshot cache
+ * eviction and the rest of the flush postprocessing — it behaves exactly like 
a {@link DeleteDbRowOp}.
+ *
+ * @since 5.0
+ */
+public class SoftDeleteDbRowOp extends DeleteDbRowOp implements 
DbRowOpWithValues {
+
+    protected final Values values;
+
+    public SoftDeleteDbRowOp(Persistent object, DbEntity entity, ObjectId id, 
DbAttribute deletedAttribute) {
+        super(object, entity, id);
+        this.values = new Values(this, false);
+        values.addValue(deletedAttribute, true, false);
+    }
+
+    @Override
+    public <T> T accept(DbRowOpVisitor<T> visitor) {
+        return visitor.visitSoftDelete(this);
+    }
+
+    @Override
+    public Values getValues() {
+        return values;
+    }
+
+    @Override
+    public boolean isSameBatch(DbRowOp rowOp) {
+        if (!(rowOp instanceof SoftDeleteDbRowOp other)) {
+            return false;
+        }
+        if (!super.isSameBatch(rowOp)) {
+            return false;
+        }
+        return values.isSameBatch(other.values);
+    }
+
+    @Override
+    public String toString() {
+        return "soft " + super.toString();
+    }
+}
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java
deleted file mode 100644
index a52f597b7..000000000
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java
+++ /dev/null
@@ -1,104 +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.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.dba.TypesMapping;
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.query.BatchQueryRow;
-import org.apache.cayenne.query.DeleteBatchQuery;
-
-import java.sql.Types;
-
-import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.*;
-
-/**
- * A delete {@link BatchTranslator} that performs a 'soft' delete (an UPDATE 
setting the 'deleted' field
- * to true) for entities that have a boolean 'deleted' field, and falls back 
to the regular SQL DELETE of
- * the {@link DeleteBatchTranslator} superclass for the rest. Bind it under 
the {@link BatchTranslator#DELETE}
- * name to enable soft deletes. The preferred way to do this is
- * {@code CoreModule.extend(binder).useSoftDeleteIfColumnPresent(columnName)} 
(see
- * {@link 
org.apache.cayenne.configuration.runtime.CoreModuleExtender#useSoftDeleteIfColumnPresent(String)}).
- *
- * @since 4.2
- */
-public class SoftDeleteBatchTranslator extends DeleteBatchTranslator {
-
-    public static final String DEFAULT_DELETED_FIELD_NAME = "DELETED";
-
-    private final String deletedFieldName;
-
-    public SoftDeleteBatchTranslator() {
-        this(DEFAULT_DELETED_FIELD_NAME);
-    }
-
-    public SoftDeleteBatchTranslator(String deletedFieldName) {
-        this.deletedFieldName = deletedFieldName;
-    }
-
-    protected boolean isHardDelete(DeleteBatchQuery query) {
-        DbAttribute attr = query.getDbEntity().getAttribute(deletedFieldName);
-        return attr == null || attr.getType() != Types.BOOLEAN;
-    }
-
-    @Override
-    protected String createSql(BatchTranslatorContext<DeleteBatchQuery> 
context) {
-        DeleteBatchQuery query = context.getQuery();
-        if (isHardDelete(query)) {
-            return super.createSql(context);
-        }
-
-        DbAttribute deleteAttribute = 
query.getDbEntity().getAttribute(deletedFieldName);
-        UpdateBuilder updateBuilder = update(context.getRootDbEntity())
-                .set(column(deletedFieldName).attribute(deleteAttribute)
-                        .eq(SQLBuilder.value(true).attribute(deleteAttribute)))
-                .where(buildQualifier(context, query.getDbAttributes()));
-
-        return doTranslate(context, updateBuilder);
-    }
-
-    @Override
-    protected PSParameter[] 
updateBindings(BatchTranslatorContext<DeleteBatchQuery> context,
-                                           PSBatchParameter[] template, 
BatchQueryRow row) {
-        DeleteBatchQuery deleteBatch = context.getQuery();
-        if (isHardDelete(deleteBatch)) {
-            return super.updateBindings(context, template, row);
-        }
-
-        PSParameter[] bindings = new PSParameter[template.length];
-
-        // the 'deleted' flag is the first binding and stays constant across 
all rows of the batch
-        DbAttribute deleteAttribute = 
deleteBatch.getDbEntity().getAttribute(deletedFieldName);
-        String typeName = TypesMapping.getJavaBySqlType(deleteAttribute);
-        ExtendedType extendedType = 
context.getAdapter().getExtendedTypes().getRegisteredType(typeName);
-        bindings[0] = template[0].bind(Boolean.TRUE, 1, extendedType);
-
-        // bindings[0] holds the constant 'deleted' flag, so qualifier values 
start at position 1
-        for(int i=0, position=1; i<deleteBatch.getDbAttributes().size(); i++) {
-            position = updateBinding(context, template, bindings, 
row.getValue(i), position);
-        }
-
-        return 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 94f1519f0..4a561aba8 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
@@ -35,6 +35,8 @@ import 
org.apache.cayenne.access.flush.DataDomainFlushActionFactory;
 import org.apache.cayenne.access.flush.DefaultDataDomainFlushActionFactory;
 import org.apache.cayenne.access.flush.operation.DbRowOpSorter;
 import org.apache.cayenne.access.flush.operation.DefaultDbRowOpSorter;
+import org.apache.cayenne.access.flush.operation.HardDeleteDbRowOpFactory;
+import org.apache.cayenne.access.flush.operation.DeleteDbRowOpFactory;
 import org.apache.cayenne.access.translator.sqltemplate.SQLTemplateTranslator;
 import org.apache.cayenne.access.jdbc.reader.DefaultRowReaderFactory;
 import org.apache.cayenne.access.jdbc.reader.RowReaderFactory;
@@ -494,6 +496,7 @@ public class CoreModule implements Module {
 
         
binder.bind(DataDomainFlushActionFactory.class).to(DefaultDataDomainFlushActionFactory.class);
         binder.bind(DbRowOpSorter.class).to(DefaultDbRowOpSorter.class);
+        
binder.bind(DeleteDbRowOpFactory.class).to(HardDeleteDbRowOpFactory.class);
 
         binder.bind(CommitLogFilter.class).to(CommitLogFilter.class);
         
binder.bind(CommitLogEntityFactory.class).to(IncludeAllCommitLogEntityFactory.class);
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModuleExtender.java
 
b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModuleExtender.java
index cc64c09b1..e27758132 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModuleExtender.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModuleExtender.java
@@ -21,8 +21,8 @@ package org.apache.cayenne.configuration.runtime;
 
 import org.apache.cayenne.DataChannelQueryFilter;
 import org.apache.cayenne.DataChannelSyncFilter;
-import org.apache.cayenne.access.translator.batch.BatchTranslator;
-import org.apache.cayenne.access.translator.batch.SoftDeleteBatchTranslator;
+import org.apache.cayenne.access.flush.operation.DeleteDbRowOpFactory;
+import 
org.apache.cayenne.access.flush.operation.ConditionalSoftDeleteDbRowOpFactory;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
 import org.apache.cayenne.access.types.ValueObjectType;
@@ -34,7 +34,6 @@ import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.PkGenerator;
 import org.apache.cayenne.di.Binder;
-import org.apache.cayenne.di.Key;
 import org.apache.cayenne.di.ListBuilder;
 import org.apache.cayenne.di.MapBuilder;
 import org.apache.cayenne.graph.GraphChangeHandler;
@@ -118,12 +117,10 @@ public class CoreModuleExtender {
      * of the mapping, e.g. a DbEntity qualifier like {@code DELETED = false 
or DELETED = null}.
      *
      * @param columnName the name of the BOOLEAN column marking soft-deleted 
rows
-     * @see SoftDeleteBatchTranslator
      * @since 5.0
      */
     public CoreModuleExtender useSoftDeleteIfColumnPresent(String columnName) {
-        binder.bind(Key.get(BatchTranslator.class, BatchTranslator.DELETE))
-                .toInstance(new SoftDeleteBatchTranslator(columnName));
+        binder.bind(DeleteDbRowOpFactory.class).toInstance(new 
ConditionalSoftDeleteDbRowOpFactory(columnName));
         return this;
     }
 
diff --git 
a/cayenne/src/test/java/org/apache/cayenne/access/DefaultDataRowStoreFactoryIT.java
 
b/cayenne/src/test/java/org/apache/cayenne/access/DefaultDataRowStoreFactoryIT.java
index ad3cb0285..d18e5148a 100644
--- 
a/cayenne/src/test/java/org/apache/cayenne/access/DefaultDataRowStoreFactoryIT.java
+++ 
b/cayenne/src/test/java/org/apache/cayenne/access/DefaultDataRowStoreFactoryIT.java
@@ -23,6 +23,8 @@ import 
org.apache.cayenne.access.flush.DataDomainFlushActionFactory;
 import org.apache.cayenne.access.flush.operation.DbRowOpSorter;
 import org.apache.cayenne.access.flush.DefaultDataDomainFlushActionFactory;
 import org.apache.cayenne.access.flush.operation.DefaultDbRowOpSorter;
+import org.apache.cayenne.access.flush.operation.HardDeleteDbRowOpFactory;
+import org.apache.cayenne.access.flush.operation.DeleteDbRowOpFactory;
 import org.apache.cayenne.configuration.DefaultRuntimeProperties;
 import org.apache.cayenne.configuration.RuntimeProperties;
 import org.apache.cayenne.configuration.runtime.CoreModule;
@@ -115,6 +117,7 @@ public class DefaultDataRowStoreFactoryIT {
             
binder.bind(DataRowStoreFactory.class).to(DefaultDataRowStoreFactory.class);
             
binder.bind(DataDomainFlushActionFactory.class).to(DefaultDataDomainFlushActionFactory.class);
             binder.bind(DbRowOpSorter.class).to(DefaultDbRowOpSorter.class);
+            
binder.bind(DeleteDbRowOpFactory.class).to(HardDeleteDbRowOpFactory.class);
             
binder.bind(AdhocObjectFactory.class).to(DefaultAdhocObjectFactory.class);
             
binder.bind(ClassLoaderManager.class).to(DefaultClassLoaderManager.class);
             new TestExtender(binder).initAllExtensions();
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/SoftDeleteIT.java 
b/cayenne/src/test/java/org/apache/cayenne/access/SoftDeleteIT.java
index 863ab1462..0c681611e 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/SoftDeleteIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/SoftDeleteIT.java
@@ -140,6 +140,29 @@ public class SoftDeleteIT {
         assertEquals(0, 
ObjectSelect.query(HardDelete.class).selectCount(context));
     }
 
+    @Test
+    public void mixedSoftAndHardDeleteInOneCommit() throws Exception {
+        TableHelper tHardDelete = env.table("HARD_DELETE", "ID", "NAME");
+        DataContext context = env.context();
+
+        SoftDelete soft = context.newObject(SoftDelete.class);
+        soft.setName("soft");
+        HardDelete hard = context.newObject(HardDelete.class);
+        hard.setName("hard");
+        context.commitChanges();
+
+        context.deleteObjects(soft, hard);
+        context.commitChanges();
+
+        // the SoftDelete row survives, flagged as deleted, and is hidden by 
the qualifier
+        assertEquals(1, tSoftDelete.getRowCount());
+        assertTrue(toBoolean(tSoftDelete.selectAll().getFirst()[2]), "DELETED 
flag should be true");
+        assertEquals(0, 
ObjectSelect.query(SoftDelete.class).selectCount(context));
+
+        // the HardDelete row is physically gone
+        assertEquals(0, tHardDelete.getRowCount());
+    }
+
     private static boolean toBoolean(Object value) {
         if (value instanceof Boolean b) {
             return b;
diff --git 
a/cayenne/src/test/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslatorIT.java
 
b/cayenne/src/test/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslatorIT.java
deleted file mode 100644
index b8ecf8423..000000000
--- 
a/cayenne/src/test/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslatorIT.java
+++ /dev/null
@@ -1,173 +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.PersistenceState;
-import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.dba.JdbcAdapter;
-import org.apache.cayenne.di.AdhocObjectFactory;
-import org.apache.cayenne.exp.Expression;
-import org.apache.cayenne.exp.ExpressionFactory;
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.query.DeleteBatchQuery;
-import org.apache.cayenne.query.ObjectSelect;
-import org.apache.cayenne.query.SQLTemplate;
-import org.apache.cayenne.test.parallel.ParallelTestContainer;
-import org.apache.cayenne.testdo.soft_delete.SoftDelete;
-import org.apache.cayenne.unit.dba.TestDbAdapter;
-import org.apache.cayenne.unit.CayenneProjects;
-import org.apache.cayenne.unit.CayenneTestsEnv;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
-
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-
-public class SoftDeleteBatchTranslatorIT {
-
-    @RegisterExtension
-    static final CayenneTestsEnv env = 
CayenneTestsEnv.forProject(CayenneProjects.SOFT_DELETE_PROJECT);
-
-    private DataNode node;
-    private TestDbAdapter unitAdapter;
-    private AdhocObjectFactory objectFactory;
-
-    private String createSql(DeleteBatchQuery query) {
-        DbAdapter adapter = objectFactory.newInstance(JdbcAdapter.class, 
JdbcAdapter.class.getName());
-        return createSql(query, adapter);
-    }
-
-    private String createSql(DeleteBatchQuery query, DbAdapter adapter) {
-        return new SoftDeleteBatchTranslator().translate(query, adapter).sql();
-    }
-
-    @BeforeEach
-    public void setUp() {
-        node = env.dataNode();
-        unitAdapter = env.testDbAdapter();
-        objectFactory = env.adhocObjectFactory();
-    }
-
-    @Test
-    public void createSqlString() {
-        DbEntity entity = 
env.context().getEntityResolver().getObjEntity(SoftDelete.class).getDbEntity();
-
-        List<DbAttribute> idAttributes = 
Collections.singletonList(entity.getAttribute("ID"));
-
-        DeleteBatchQuery deleteQuery = new DeleteBatchQuery(entity, 
idAttributes, Collections.emptySet(), 1);
-        String generatedSql = createSql(deleteQuery);
-        assertNotNull(generatedSql);
-        assertEquals("UPDATE " + entity.getName() + " SET DELETED = ? WHERE ID 
= ?", generatedSql);
-    }
-
-    @Test
-    public void createSqlStringWithNulls() {
-        DbEntity entity = 
env.context().getEntityResolver().getObjEntity(SoftDelete.class).getDbEntity();
-
-        List<DbAttribute> idAttributes = 
Arrays.asList(entity.getAttribute("ID"), entity.getAttribute("NAME"));
-
-        Collection<String> nullAttributes = Collections.singleton("NAME");
-
-        DeleteBatchQuery deleteQuery = new DeleteBatchQuery(entity, 
idAttributes, nullAttributes, 1);
-        String generatedSql = createSql(deleteQuery);
-        assertNotNull(generatedSql);
-        assertEquals("UPDATE " + entity.getName() + " SET DELETED = ? WHERE ( 
ID = ? ) AND ( NAME IS NULL )", generatedSql);
-    }
-
-    @Test
-    public void createSqlStringWithIdentifiersQuote() {
-        DbEntity entity = 
env.context().getEntityResolver().getObjEntity(SoftDelete.class).getDbEntity();
-        try {
-
-            entity.getDataMap().setQuotingSQLIdentifiers(true);
-
-            List<DbAttribute> idAttributes = 
Collections.singletonList(entity.getAttribute("ID"));
-
-            DeleteBatchQuery deleteQuery = new DeleteBatchQuery(entity, 
idAttributes, Collections.emptySet(), 1);
-            DbAdapter adapter = node.getAdapter();
-            String generatedSql = createSql(deleteQuery, adapter);
-
-            String charStart = unitAdapter.getIdentifiersStartQuote();
-            String charEnd = unitAdapter.getIdentifiersEndQuote();
-
-            assertNotNull(generatedSql);
-            assertEquals("UPDATE " + charStart + entity.getName() + charEnd + 
" SET " + charStart + "DELETED" + charEnd
-                    + " = ? WHERE " + charStart + "ID" + charEnd + " = ?", 
generatedSql);
-        } finally {
-            entity.getDataMap().setQuotingSQLIdentifiers(false);
-        }
-
-    }
-
-    @Test
-    public void update() throws Exception {
-
-        DbEntity entity = 
env.context().getEntityResolver().getObjEntity(SoftDelete.class).getDbEntity();
-
-        BatchTranslator<DeleteBatchQuery> oldTranslator = 
node.getDeleteBatchTranslator();
-        try {
-            node.setDeleteBatchTranslator(new SoftDeleteBatchTranslator());
-
-            final SoftDelete test = env.context().newObject(SoftDelete.class);
-            test.setName("SoftDeleteBatchQueryBuilderTest");
-            env.context().commitChanges();
-
-            new ParallelTestContainer() {
-
-                @Override
-                protected void assertResult() {
-                    Expression exp = ExpressionFactory.matchExp("name", 
test.getName());
-                    assertEquals(1, ObjectSelect.query(SoftDelete.class, 
exp).selectCount(env.context()));
-
-                    exp = ExpressionFactory.matchDbExp("DELETED", true);
-                    assertEquals(0, ObjectSelect.query(SoftDelete.class, 
exp).selectCount(env.context()));
-                }
-            }.runTest(200);
-
-            env.context().deleteObjects(test);
-            assertEquals(test.getPersistenceState(), PersistenceState.DELETED);
-            env.context().commitChanges();
-
-            new ParallelTestContainer() {
-
-                @Override
-                protected void assertResult() {
-                    Expression exp = ExpressionFactory.matchExp("name", 
test.getName());
-                    assertEquals(0, ObjectSelect.query(SoftDelete.class, 
exp).selectCount(env.context()));
-
-                    SQLTemplate template = new SQLTemplate(entity, "SELECT * 
FROM SOFT_DELETE");
-                    template.setFetchingDataRows(true);
-                    assertEquals(1, 
env.context().performQuery(template).size());
-                }
-            }.runTest(200);
-        } finally {
-            env.context().performQuery(new SQLTemplate(entity, "DELETE FROM 
SOFT_DELETE"));
-            node.setDeleteBatchTranslator(oldTranslator);
-        }
-    }
-
-}
diff --git 
a/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataContextFactoryTest.java
 
b/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataContextFactoryTest.java
index 7242bc1c0..db1b5f26a 100644
--- 
a/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataContextFactoryTest.java
+++ 
b/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataContextFactoryTest.java
@@ -28,6 +28,8 @@ import 
org.apache.cayenne.access.flush.DataDomainFlushActionFactory;
 import org.apache.cayenne.access.flush.operation.DbRowOpSorter;
 import org.apache.cayenne.access.flush.DefaultDataDomainFlushActionFactory;
 import org.apache.cayenne.access.flush.operation.DefaultDbRowOpSorter;
+import org.apache.cayenne.access.flush.operation.HardDeleteDbRowOpFactory;
+import org.apache.cayenne.access.flush.operation.DeleteDbRowOpFactory;
 import org.apache.cayenne.ashwood.AshwoodEntitySorter;
 import org.apache.cayenne.cache.MapQueryCache;
 import org.apache.cayenne.cache.QueryCache;
@@ -86,6 +88,7 @@ public class DataContextFactoryTest {
             
binder.bind(DataRowStoreFactory.class).to(DefaultDataRowStoreFactory.class);
             
binder.bind(DataDomainFlushActionFactory.class).to(DefaultDataDomainFlushActionFactory.class);
             binder.bind(DbRowOpSorter.class).to(DefaultDbRowOpSorter.class);
+            
binder.bind(DeleteDbRowOpFactory.class).to(HardDeleteDbRowOpFactory.class);
             binder.bind(EntitySorter.class).to(AshwoodEntitySorter.class);
             
binder.bind(AdhocObjectFactory.class).to(DefaultAdhocObjectFactory.class);
             
binder.bind(ClassLoaderManager.class).to(DefaultClassLoaderManager.class);
@@ -127,6 +130,7 @@ public class DataContextFactoryTest {
             
binder.bind(DataRowStoreFactory.class).to(DefaultDataRowStoreFactory.class);
             
binder.bind(DataDomainFlushActionFactory.class).to(DefaultDataDomainFlushActionFactory.class);
             binder.bind(DbRowOpSorter.class).to(DefaultDbRowOpSorter.class);
+            
binder.bind(DeleteDbRowOpFactory.class).to(HardDeleteDbRowOpFactory.class);
             binder.bind(EntitySorter.class).to(AshwoodEntitySorter.class);
             
binder.bind(AdhocObjectFactory.class).to(DefaultAdhocObjectFactory.class);
             
binder.bind(ClassLoaderManager.class).to(DefaultClassLoaderManager.class);

Reply via email to