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

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 081510847a Add batch delete option to Delete transform (#7195)
081510847a is described below

commit 081510847ab94a1002597ad166fd2f3ffe5b2328
Author: Lance <[email protected]>
AuthorDate: Tue Jun 2 20:55:36 2026 +0800

    Add batch delete option to Delete transform (#7195)
    
    * Add batch delete option to Delete transform
    
    Signed-off-by: lance <[email protected]>
    
    * Add batch delete option to Delete transform
    
    Signed-off-by: lance <[email protected]>
    
    * Add batch delete option to Delete transform
    
    Signed-off-by: lance <[email protected]>
    
    ---------
    
    Signed-off-by: lance <[email protected]>
---
 .../hop/pipeline/transforms/delete/Delete.java     |  44 +++--
 .../pipeline/transforms/delete/DeleteDialog.java   |  51 ++++-
 .../pipeline/transforms/delete/DeleteKeyField.java |  44 +----
 .../transforms/delete/DeleteLookupField.java       |  50 +----
 .../hop/pipeline/transforms/delete/DeleteMeta.java |  67 +++----
 .../delete/messages/messages_en_US.properties      |   4 +-
 .../delete/messages/messages_zh_CN.properties      |   4 +-
 .../pipeline/transforms/delete/DeleteDataTest.java |  48 +++++
 .../transforms/delete/DeleteKeyFieldTest.java      |  97 +++++++++
 .../transforms/delete/DeleteLookupFieldTest.java   | 102 ++++++++++
 .../transforms/delete/DeleteMetaInjectionTest.java |  34 ++++
 .../pipeline/transforms/delete/DeleteMetaTest.java |  81 ++++----
 .../pipeline/transforms/delete/DeleteSqlTest.java  | 177 +++++++++++++++++
 .../hop/pipeline/transforms/delete/DeleteTest.java | 218 +++++++++++++++++++++
 14 files changed, 849 insertions(+), 172 deletions(-)

diff --git 
a/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/Delete.java
 
b/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/Delete.java
index 87cd36d8a6..3b3a42181d 100644
--- 
a/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/Delete.java
+++ 
b/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/Delete.java
@@ -36,7 +36,6 @@ import org.apache.hop.pipeline.transform.TransformMeta;
 
 /** Delete data in a database table. */
 public class Delete extends BaseTransform<DeleteMeta, DeleteData> {
-
   private static final Class<?> PKG = DeleteMeta.class;
 
   public Delete(
@@ -75,10 +74,11 @@ public class Delete extends BaseTransform<DeleteMeta, 
DeleteData> {
               PKG,
               "Delete.Log.SetValuesForDelete",
               data.deleteParameterRowMeta.getString(deleteRow),
-              rowMeta.getString(row)));
+              rowMeta.getString(row),
+              meta.isUseBatchUpdate()));
     }
 
-    data.db.insertRow(data.prepStatementDelete);
+    data.db.insertRow(data.prepStatementDelete, meta.isUseBatchUpdate(), true);
     incrementLinesUpdated();
   }
 
@@ -86,10 +86,10 @@ public class Delete extends BaseTransform<DeleteMeta, 
DeleteData> {
   public boolean processRow() throws HopException {
     boolean sendToErrorRow = false;
     String errorMessage = null;
-
-    Object[] r = getRow(); // Get row from input rowset & set row busy!
-    if (r == null) { // no more input to be expected...
-
+    // Get row from input rowset & set row busy!
+    Object[] r = getRow();
+    // no more input to be expected...
+    if (r == null) {
       setOutputDone();
       return false;
     }
@@ -151,24 +151,24 @@ public class Delete extends BaseTransform<DeleteMeta, 
DeleteData> {
     }
 
     try {
-      deleteValues(getInputRowMeta(), r); // add new values to the row in 
rowset[0].
-      putRow(
-          data.outputRowMeta, r); // output the same rows of data, but with a 
copy of the metadata
+      // add new values to the row in rowset[0].
+      deleteValues(getInputRowMeta(), r);
+      // output the same rows of data, but with a copy of the metadata
+      putRow(data.outputRowMeta, r);
 
       if (checkFeedback(getLinesRead()) && isBasic()) {
         logBasic(BaseMessages.getString(PKG, "Delete.Log.LineNumber") + 
getLinesRead());
       }
     } catch (HopException e) {
-
       if (getTransformMeta().isDoingErrorHandling()) {
         sendToErrorRow = true;
         errorMessage = e.toString();
       } else {
-
         logError(BaseMessages.getString(PKG, "Delete.Log.ErrorInTransform") + 
e.getMessage());
         setErrors(1);
         stopAll();
-        setOutputDone(); // signal end to receiver(s)
+        // signal end to receiver(s)
+        setOutputDone();
         return false;
       }
 
@@ -225,7 +225,6 @@ public class Delete extends BaseTransform<DeleteMeta, 
DeleteData> {
   @Override
   public boolean init() {
     if (super.init()) {
-
       if (Utils.isEmpty(meta.getConnection())) {
         logError(BaseMessages.getString(PKG, "Delete.Init.ConnectionMissing", 
getTransformName()));
         return false;
@@ -238,16 +237,13 @@ public class Delete extends BaseTransform<DeleteMeta, 
DeleteData> {
       }
 
       data.db = new Database(this, variables, databaseMeta);
-
       try {
         data.db.connect();
-
         if (isDetailed()) {
           logDetailed(BaseMessages.getString(PKG, "Delete.Log.ConnectedToDB"));
         }
 
         data.db.setCommit(meta.getCommitSize(this));
-
         return true;
       } catch (HopException ke) {
         logError(BaseMessages.getString(PKG, "Delete.Log.ErrorOccurred") + 
ke.getMessage());
@@ -274,18 +270,26 @@ public class Delete extends BaseTransform<DeleteMeta, 
DeleteData> {
       try {
         if (!data.db.isAutoCommit()) {
           if (getErrors() == 0) {
-            data.db.commit();
+            if (dispose) {
+              data.db.emptyAndCommit(data.prepStatementDelete, 
meta.isUseBatchUpdate());
+              data.prepStatementDelete = null;
+            } else {
+              data.db.commit();
+            }
           } else {
             data.db.rollback();
           }
         }
-        if (dispose) data.db.closeUpdate();
+        if (dispose && data.prepStatementDelete != null) {
+          data.db.closePreparedStatement(data.prepStatementDelete);
+          data.prepStatementDelete = null;
+        }
       } catch (HopDatabaseException e) {
         logError(
             BaseMessages.getString(PKG, 
"Delete.Log.UnableToCommitUpdateConnection")
                 + data.db
                 + "] :"
-                + e.toString());
+                + e);
         setErrors(1);
       } finally {
         if (dispose) {
diff --git 
a/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteDialog.java
 
b/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteDialog.java
index be331621d5..cc0c442782 100644
--- 
a/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteDialog.java
+++ 
b/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteDialog.java
@@ -69,6 +69,8 @@ public class DeleteDialog extends BaseTransformDialog {
 
   private TextVar wCommit;
 
+  private Button wBatch;
+
   private final DeleteMeta input;
 
   private final List<String> inputFields = new ArrayList<>();
@@ -183,12 +185,37 @@ public class DeleteDialog extends BaseTransformDialog {
     fdCommit.right = new FormAttachment(100, 0);
     wCommit.setLayoutData(fdCommit);
 
+    // Batch delete
+    Label wlBatch = new Label(shell, SWT.RIGHT);
+    wlBatch.setText(BaseMessages.getString(PKG, "DeleteDialog.Batch.Label"));
+    PropsUi.setLook(wlBatch);
+    FormData fdlBatch = new FormData();
+    fdlBatch.left = new FormAttachment(0, 0);
+    fdlBatch.top = new FormAttachment(wCommit, margin);
+    fdlBatch.right = new FormAttachment(middle, -margin);
+    wlBatch.setLayoutData(fdlBatch);
+    wBatch = new Button(shell, SWT.CHECK);
+    PropsUi.setLook(wBatch);
+    FormData fdBatch = new FormData();
+    fdBatch.left = new FormAttachment(middle, 0);
+    fdBatch.top = new FormAttachment(wlBatch, 0, SWT.CENTER);
+    fdBatch.right = new FormAttachment(100, 0);
+    wBatch.setLayoutData(fdBatch);
+    wBatch.addSelectionListener(
+        new SelectionAdapter() {
+          @Override
+          public void widgetSelected(SelectionEvent arg0) {
+            setFlags();
+            input.setChanged();
+          }
+        });
+
     Label wlKey = new Label(shell, SWT.NONE);
     wlKey.setText(BaseMessages.getString(PKG, "DeleteDialog.Key.Label"));
     PropsUi.setLook(wlKey);
     FormData fdlKey = new FormData();
     fdlKey.left = new FormAttachment(0, 0);
-    fdlKey.top = new FormAttachment(wCommit, margin);
+    fdlKey.top = new FormAttachment(wBatch, margin);
     wlKey.setLayoutData(fdlKey);
 
     int nrKeyCols = 4;
@@ -288,6 +315,7 @@ public class DeleteDialog extends BaseTransformDialog {
 
     getData();
     setTableFieldCombo();
+    setFlags();
     focusTransformName();
     BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel());
 
@@ -309,6 +337,7 @@ public class DeleteDialog extends BaseTransformDialog {
     }
 
     wCommit.setText(input.getCommitSizeVar());
+    wBatch.setSelection(input.isUseBatchUpdate());
 
     List<DeleteKeyField> keyFields = input.getLookup().getFields();
 
@@ -352,6 +381,25 @@ public class DeleteDialog extends BaseTransformDialog {
     dispose();
   }
 
+  /**
+   * Updates dialog control states based on the current configuration.
+   *
+   * <p>Batch deletes are disabled when this transform uses error handling and 
the selected database
+   * does not support batch updates together with error handling (for example 
mysql and look-likes).
+   */
+  public void setFlags() {
+    DatabaseMeta databaseMeta = 
pipelineMeta.findDatabase(wConnection.getText(), variables);
+    boolean hasErrorHandling = 
pipelineMeta.findTransform(transformName).isDoingErrorHandling();
+
+    boolean enableBatch = wBatch.getSelection();
+    enableBatch =
+        enableBatch
+            && !(databaseMeta != null
+                && databaseMeta.supportsErrorHandlingOnBatchUpdates()
+                && hasErrorHandling);
+    wBatch.setSelection(enableBatch);
+  }
+
   private void setTableFieldCombo() {
     Runnable fieldLoader =
         () -> {
@@ -409,6 +457,7 @@ public class DeleteDialog extends BaseTransformDialog {
     int nrkeys = wKey.nrNonEmpty();
 
     inf.setCommitSize(wCommit.getText());
+    inf.setUseBatchUpdate(wBatch.getSelection());
 
     if (log.isDebug()) {
       logDebug(BaseMessages.getString(PKG, "DeleteDialog.Log.FoundKeys", 
String.valueOf(nrkeys)));
diff --git 
a/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteKeyField.java
 
b/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteKeyField.java
index ef6afd302b..cd0543b1fa 100644
--- 
a/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteKeyField.java
+++ 
b/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteKeyField.java
@@ -18,9 +18,13 @@
 package org.apache.hop.pipeline.transforms.delete;
 
 import java.util.Objects;
+import lombok.Getter;
+import lombok.Setter;
 import org.apache.hop.metadata.api.HopMetadataProperty;
 import org.apache.hop.metadata.api.HopMetadataPropertyType;
 
+@Getter
+@Setter
 public class DeleteKeyField {
 
   /** which field in input stream to compare with? */
@@ -65,42 +69,14 @@ public class DeleteKeyField {
     this.keyStream2 = f.keyStream2;
   }
 
-  public String getKeyStream() {
-    return keyStream;
-  }
-
-  public void setKeyStream(String keyStream) {
-    this.keyStream = keyStream;
-  }
-
-  public String getKeyLookup() {
-    return keyLookup;
-  }
-
-  public void setKeyLookup(String keyLookup) {
-    this.keyLookup = keyLookup;
-  }
-
-  public String getKeyCondition() {
-    return keyCondition;
-  }
-
-  public void setKeyCondition(String keyCondition) {
-    this.keyCondition = keyCondition;
-  }
-
-  public String getKeyStream2() {
-    return keyStream2;
-  }
-
-  public void setKeyStream2(String keyStream2) {
-    this.keyStream2 = keyStream2;
-  }
-
   @Override
   public boolean equals(Object o) {
-    if (this == o) return true;
-    if (o == null || getClass() != o.getClass()) return false;
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
     DeleteKeyField that = (DeleteKeyField) o;
     return keyStream.equals(that.keyStream)
         && keyLookup.equals(that.keyLookup)
diff --git 
a/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteLookupField.java
 
b/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteLookupField.java
index 52db89b145..4d1d5cec56 100644
--- 
a/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteLookupField.java
+++ 
b/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteLookupField.java
@@ -20,9 +20,13 @@ package org.apache.hop.pipeline.transforms.delete;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import lombok.Getter;
+import lombok.Setter;
 import org.apache.hop.metadata.api.HopMetadataProperty;
 import org.apache.hop.metadata.api.HopMetadataPropertyType;
 
+@Getter
+@Setter
 public class DeleteLookupField {
 
   @HopMetadataProperty(
@@ -65,48 +69,14 @@ public class DeleteLookupField {
     this.tableName = tableName;
   }
 
-  /**
-   * @return Returns the tableName.
-   */
-  public String getTableName() {
-    return tableName;
-  }
-
-  /**
-   * @param tableName The tableName to set.
-   */
-  public void setTableName(String tableName) {
-    this.tableName = tableName;
-  }
-
-  public String getSchemaName() {
-    return schemaName;
-  }
-
-  public void setSchemaName(String schemaName) {
-    this.schemaName = schemaName;
-  }
-
-  /**
-   * Gets fields
-   *
-   * @return value of fields
-   */
-  public List<DeleteKeyField> getFields() {
-    return fields;
-  }
-
-  /**
-   * @param fields The fields to set
-   */
-  public void setFields(List<DeleteKeyField> fields) {
-    this.fields = fields;
-  }
-
   @Override
   public boolean equals(Object o) {
-    if (this == o) return true;
-    if (o == null || getClass() != o.getClass()) return false;
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
     DeleteLookupField that = (DeleteLookupField) o;
     return fields.equals(that.fields)
         && Objects.equals(schemaName, that.schemaName)
diff --git 
a/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteMeta.java
 
b/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteMeta.java
index 60ffd46ddd..abeefd633a 100644
--- 
a/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteMeta.java
+++ 
b/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteMeta.java
@@ -18,6 +18,8 @@
 package org.apache.hop.pipeline.transforms.delete;
 
 import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hop.core.CheckResult;
 import org.apache.hop.core.Const;
@@ -46,6 +48,8 @@ import org.apache.hop.pipeline.transform.TransformMeta;
  * This class takes care of deleting values in a table using a certain 
condition and values for
  * input.
  */
+@Getter
+@Setter
 @Transform(
     id = "Delete",
     image = "delete.svg",
@@ -73,20 +77,19 @@ public class DeleteMeta extends BaseTransformMeta<Delete, 
DeleteData> {
   @HopMetadataProperty(key = "commit", injectionKeyDescription = 
"DeleteMeta.Injection.CommitSize")
   private String commitSize;
 
+  /** Flag to indicate the use of batch deletes, disabled by default for 
backward compatibility */
+  @HopMetadataProperty(
+      key = "use_batch",
+      injectionKeyDescription = "DeleteMeta.Injection.UseBatchUpdate",
+      injectionKey = "BATCH_UPDATE")
+  private boolean useBatchUpdate;
+
   public DeleteMeta() {
     super();
     lookup = new DeleteLookupField();
     // allocate BaseTransformMeta
   }
 
-  public String getConnection() {
-    return connection;
-  }
-
-  public void setConnection(String connection) {
-    this.connection = connection;
-  }
-
   /**
    * @return Returns the commitSize.
    */
@@ -94,18 +97,6 @@ public class DeleteMeta extends BaseTransformMeta<Delete, 
DeleteData> {
     return commitSize;
   }
 
-  public DeleteLookupField getLookup() {
-    return lookup;
-  }
-
-  public void setLookup(DeleteLookupField lookup) {
-    this.lookup = lookup;
-  }
-
-  public String getCommitSize() {
-    return commitSize;
-  }
-
   /**
    * @param vs - variable variables to be used for searching variable value 
usually "this" for a
    *     calling transform
@@ -117,17 +108,11 @@ public class DeleteMeta extends BaseTransformMeta<Delete, 
DeleteData> {
     return Integer.parseInt(vs.resolve(commitSize));
   }
 
-  /**
-   * @param commitSize The commitSize to set.
-   */
-  public void setCommitSize(String commitSize) {
-    this.commitSize = commitSize;
-  }
-
   public DeleteMeta(DeleteMeta obj) {
 
     this.connection = obj.connection;
     this.commitSize = obj.commitSize;
+    this.useBatchUpdate = obj.useBatchUpdate;
     this.lookup = new DeleteLookupField(obj.lookup);
   }
 
@@ -150,8 +135,7 @@ public class DeleteMeta extends BaseTransformMeta<Delete, 
DeleteData> {
       IRowMeta[] info,
       TransformMeta nextTransform,
       IVariables variables,
-      IHopMetadataProvider metadataProvider)
-      throws HopTransformException {
+      IHopMetadataProvider metadataProvider) {
     // Default: nothing changes to rowMeta
   }
 
@@ -350,8 +334,8 @@ public class DeleteMeta extends BaseTransformMeta<Delete, 
DeleteData> {
 
     DatabaseMeta databaseMeta = pipelineMeta.findDatabase(connection, 
variables);
 
-    SqlStatement retval =
-        new SqlStatement(transformMeta.getName(), databaseMeta, null); // 
default: nothing to do!
+    // default: nothing to do!
+    SqlStatement ret = new SqlStatement(transformMeta.getName(), databaseMeta, 
null);
 
     if (databaseMeta != null) {
       if (prev != null && !prev.isEmpty()) {
@@ -374,20 +358,19 @@ public class DeleteMeta extends BaseTransformMeta<Delete, 
DeleteData> {
                 idxFields[i] = keyFields.get(i).getKeyLookup();
               }
             } else {
-              retval.setError(
-                  BaseMessages.getString(PKG, 
"DeleteMeta.CheckResult.KeyFieldsRequired"));
+              ret.setError(BaseMessages.getString(PKG, 
"DeleteMeta.CheckResult.KeyFieldsRequired"));
             }
 
             // Key lookup dimensions...
             if (idxFields != null
                 && idxFields.length > 0
                 && !db.checkIndexExists(schemaTable, idxFields)) {
-              String indexname = "idx_" + lookup.getTableName() + "_lookup";
+              String indexName = "idx_" + lookup.getTableName() + "_lookup";
               crIndex =
                   db.getCreateIndexStatement(
                       lookup.getSchemaName(),
                       lookup.getTableName(),
-                      indexname,
+                      indexName,
                       idxFields,
                       false,
                       false,
@@ -397,27 +380,27 @@ public class DeleteMeta extends BaseTransformMeta<Delete, 
DeleteData> {
 
             String sql = crTable + crIndex;
             if (sql.isEmpty()) {
-              retval.setSql(null);
+              ret.setSql(null);
             } else {
-              retval.setSql(sql);
+              ret.setSql(sql);
             }
           } catch (HopException e) {
-            retval.setError(
+            ret.setError(
                 BaseMessages.getString(PKG, 
"DeleteMeta.Returnvalue.ErrorOccurred")
                     + e.getMessage());
           }
         } else {
-          retval.setError(
+          ret.setError(
               BaseMessages.getString(PKG, 
"DeleteMeta.Returnvalue.NoTableDefinedOnConnection"));
         }
       } else {
-        retval.setError(BaseMessages.getString(PKG, 
"DeleteMeta.Returnvalue.NoReceivingAnyFields"));
+        ret.setError(BaseMessages.getString(PKG, 
"DeleteMeta.Returnvalue.NoReceivingAnyFields"));
       }
     } else {
-      retval.setError(BaseMessages.getString(PKG, 
"DeleteMeta.Returnvalue.NoConnectionDefined"));
+      ret.setError(BaseMessages.getString(PKG, 
"DeleteMeta.Returnvalue.NoConnectionDefined"));
     }
 
-    return retval;
+    return ret;
   }
 
   @Override
diff --git 
a/plugins/transforms/delete/src/main/resources/org/apache/hop/pipeline/transforms/delete/messages/messages_en_US.properties
 
b/plugins/transforms/delete/src/main/resources/org/apache/hop/pipeline/transforms/delete/messages/messages_en_US.properties
index 284bef099d..40af50f72a 100644
--- 
a/plugins/transforms/delete/src/main/resources/org/apache/hop/pipeline/transforms/delete/messages/messages_en_US.properties
+++ 
b/plugins/transforms/delete/src/main/resources/org/apache/hop/pipeline/transforms/delete/messages/messages_en_US.properties
@@ -24,7 +24,7 @@ Delete.Log.ErrorInTransform=Error in transform, asking 
everyone to stop because
 Delete.Log.ErrorOccurred=An error occurred, processing will be stopped\: 
 Delete.Log.FieldInfo=Field [{0}] has nr. 
 Delete.Log.LineNumber=linenr 
-Delete.Log.SetValuesForDelete=Values set for delete\: {0}, input row\: {1}
+Delete.Log.SetValuesForDelete=Values set for delete\: {0}, input row\: {1}, 
use batch: {2}
 Delete.Log.UnableToCommitUpdateConnection=Unable to commit Update connection [
 Delete.Name=Delete
 DeleteDialog.AvailableSchemas.Message=Please select a schema name
@@ -76,7 +76,9 @@ DeleteMeta.Injection.Field.KeyLookup=The table field
 DeleteMeta.Injection.Field.KeyStream=The stream field 1
 DeleteMeta.Injection.Field.KeyStream2=The stream field 2
 DeleteMeta.Injection.SchemaName=The name of the schema to use
+DeleteDialog.Batch.Label=Use batch deletes
 DeleteMeta.Injection.TableName=The name of the table to use
+DeleteMeta.Injection.UseBatchUpdate=Set this flag to perform batch deletes
 DeleteMeta.Keyword=delete
 DeleteMeta.Returnvalue.ErrorOccurred=An error occurred\: 
 DeleteMeta.Returnvalue.NoConnectionDefined=There is no connection defined in 
this transform.
diff --git 
a/plugins/transforms/delete/src/main/resources/org/apache/hop/pipeline/transforms/delete/messages/messages_zh_CN.properties
 
b/plugins/transforms/delete/src/main/resources/org/apache/hop/pipeline/transforms/delete/messages/messages_zh_CN.properties
index 233cf11682..fc2f4add69 100644
--- 
a/plugins/transforms/delete/src/main/resources/org/apache/hop/pipeline/transforms/delete/messages/messages_zh_CN.properties
+++ 
b/plugins/transforms/delete/src/main/resources/org/apache/hop/pipeline/transforms/delete/messages/messages_zh_CN.properties
@@ -26,7 +26,7 @@ Delete.Log.ErrorInTransform=Error in transform, asking 
everyone to stop because
 Delete.Log.ErrorOccurred=An error occurred, processing will be stopped\: 
 Delete.Log.FieldInfo=Field [{0}] has nr. 
 Delete.Log.LineNumber=linenr 
-Delete.Log.SetValuesForDelete=Values set for delete\: {0}, input row\: {1}
+Delete.Log.SetValuesForDelete=Values set for delete\: {0}, input row\: {1}, 
use batch: {2}
 Delete.Log.UnableToCommitUpdateConnection=Unable to commit Update connection [
 Delete.Name=\u5220\u9664
 DeleteDialog.AvailableSchemas.Message=\u8BF7\u9009\u62E9\u4E00\u4E2A Schema 
\u540D\u79F0
@@ -37,6 +37,7 @@ 
DeleteDialog.ColumnInfo.StreamField1=\u6D41\u91CC\u7684\u5B57\u6BB51
 DeleteDialog.ColumnInfo.StreamField2=\u6D41\u91CC\u7684\u5B57\u6BB52
 DeleteDialog.ColumnInfo.TableField=\u8868\u5B57\u6BB5
 DeleteDialog.Commit.Label=\u63D0\u4EA4\u8BB0\u5F55\u6570\u91CF
+DeleteDialog.Batch.Label=\u4F7F\u7528\u6279\u91CF\u5220\u9664
 DeleteDialog.ErrorGettingSchemas=\u83B7\u53D6 Schema \u65F6\u51FA\u9519
 
DeleteDialog.FailedToGetFields.DialogMessage=\u65E0\u6CD5\u4ECE\u524D\u7F6E\u901A\u9053\u91CC\u83B7\u53D6\u5B57\u6BB5,
 \u56E0\u4E3A\u4E00\u4E2A\u9519\u8BEF
 DeleteDialog.FailedToGetFields.DialogTitle=\u83B7\u53D6\u5B57\u6BB5\u5931\u8D25
@@ -79,6 +80,7 @@ DeleteMeta.Injection.Field.KeyStream2=\u6D41\u5B57\u6BB5 2
 DeleteMeta.Injection.Fields=\u5B57\u6BB5
 
DeleteMeta.Injection.SchemaName=\u8981\u4F7F\u7528\u7684\u6A21\u5F0F\u540D\u79F0
 DeleteMeta.Injection.TableName=\u8981\u4F7F\u7528\u7684\u8868\u540D\u79F0
+DeleteMeta.Injection.UseBatchUpdate=\u8BBE\u7F6E\u6B64\u6807\u5FD7\u4EE5\u6267\u884C\u6279\u91CF\u5220\u9664
 DeleteMeta.Keyword=delete
 DeleteMeta.Returnvalue.ErrorOccurred=An error occurred\: 
 DeleteMeta.Returnvalue.NoConnectionDefined=There is no connection defined in 
this transform.
diff --git 
a/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteDataTest.java
 
b/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteDataTest.java
new file mode 100644
index 0000000000..0063a54bb1
--- /dev/null
+++ 
b/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteDataTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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
+ *
+ *      http://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.hop.pipeline.transforms.delete;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+/** Unit test for {@link DeleteData} */
+class DeleteDataTest {
+
+  @Test
+  void testDefaultConstructor() {
+    DeleteData data = new DeleteData();
+    assertNull(data.db);
+    assertNull(data.keynrs);
+    assertNull(data.keynrs2);
+    assertNull(data.outputRowMeta);
+    assertNull(data.schemaTable);
+    assertNull(data.deleteParameterRowMeta);
+    assertNull(data.prepStatementDelete);
+  }
+
+  @Test
+  void testFieldsCanBeAssigned() {
+    DeleteData data = new DeleteData();
+    data.schemaTable = "public.customers";
+    assertNotNull(data.schemaTable);
+    assertEquals("public.customers", data.schemaTable);
+  }
+}
diff --git 
a/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteKeyFieldTest.java
 
b/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteKeyFieldTest.java
new file mode 100644
index 0000000000..03209a830d
--- /dev/null
+++ 
b/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteKeyFieldTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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
+ *
+ *      http://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.hop.pipeline.transforms.delete;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.Test;
+
+/** Unit test for {@link DeleteKeyField} */
+class DeleteKeyFieldTest {
+
+  @Test
+  void testDefaultConstructor() {
+    DeleteKeyField field = new DeleteKeyField();
+    assertNotNull(field);
+  }
+
+  @Test
+  void testParameterizedConstructorAndGetters() {
+    DeleteKeyField field = new DeleteKeyField("id", "=", "streamId", 
"streamId2");
+    assertEquals("id", field.getKeyLookup());
+    assertEquals("=", field.getKeyCondition());
+    assertEquals("streamId", field.getKeyStream());
+    assertEquals("streamId2", field.getKeyStream2());
+  }
+
+  @Test
+  void testCopyConstructor() {
+    DeleteKeyField original = new DeleteKeyField("name", "LIKE", "streamName", 
null);
+    DeleteKeyField copy = new DeleteKeyField(original);
+    assertEquals(original, copy);
+    assertEquals(original.hashCode(), copy.hashCode());
+  }
+
+  @Test
+  void testSetters() {
+    DeleteKeyField field = new DeleteKeyField();
+    field.setKeyLookup("col");
+    field.setKeyCondition(">=");
+    field.setKeyStream("s1");
+    field.setKeyStream2("s2");
+    assertEquals("col", field.getKeyLookup());
+    assertEquals(">=", field.getKeyCondition());
+    assertEquals("s1", field.getKeyStream());
+    assertEquals("s2", field.getKeyStream2());
+  }
+
+  @Test
+  void testEqualsAndHashCodeWithNullStream2() {
+    DeleteKeyField a = new DeleteKeyField("id", "=", "streamId", null);
+    DeleteKeyField b = new DeleteKeyField("id", "=", "streamId", null);
+    DeleteKeyField c = new DeleteKeyField("id", "<>", "streamId", null);
+
+    assertEquals(a, b);
+    assertEquals(a.hashCode(), b.hashCode());
+    assertNotEquals(a, c);
+  }
+
+  @Test
+  void testEqualsAndHashCodeWithBetweenCondition() {
+    DeleteKeyField a = new DeleteKeyField("age", "BETWEEN", "minAge", 
"maxAge");
+    DeleteKeyField b = new DeleteKeyField("age", "BETWEEN", "minAge", 
"maxAge");
+    DeleteKeyField c = new DeleteKeyField("age", "BETWEEN", "minAge", 
"otherMax");
+
+    assertEquals(a, b);
+    assertNotEquals(a, c);
+  }
+
+  @Test
+  void testEqualsSameInstance() {
+    DeleteKeyField field = new DeleteKeyField("id", "=", "streamId", null);
+    assertEquals(field, field);
+  }
+
+  @Test
+  void testNotEqualsDifferentClass() {
+    DeleteKeyField field = new DeleteKeyField("id", "=", "streamId", null);
+    assertNotEquals("not a DeleteKeyField", field);
+  }
+}
diff --git 
a/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteLookupFieldTest.java
 
b/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteLookupFieldTest.java
new file mode 100644
index 0000000000..0c528eb8f5
--- /dev/null
+++ 
b/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteLookupFieldTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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
+ *
+ *      http://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.hop.pipeline.transforms.delete;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.jupiter.api.Test;
+
+/** Unit test for {@link DeleteKeyField} */
+class DeleteLookupFieldTest {
+
+  @Test
+  void testDefaultConstructorInitializesFieldsList() {
+    DeleteLookupField lookup = new DeleteLookupField();
+    assertNotNull(lookup.getFields());
+    assertTrue(lookup.getFields().isEmpty());
+  }
+
+  @Test
+  void testParameterizedConstructor() {
+    DeleteKeyField key = new DeleteKeyField("id", "=", "streamId", null);
+    DeleteLookupField lookup =
+        new DeleteLookupField("public", "customers", 
Collections.singletonList(key));
+
+    assertEquals("public", lookup.getSchemaName());
+    assertEquals("customers", lookup.getTableName());
+    assertEquals(1, lookup.getFields().size());
+    assertEquals(key, lookup.getFields().getFirst());
+  }
+
+  @Test
+  void testCopyConstructorDeepCopiesFields() {
+    DeleteKeyField key = new DeleteKeyField("id", "=", "streamId", null);
+    DeleteLookupField original =
+        new DeleteLookupField("dbo", "orders", Collections.singletonList(key));
+
+    DeleteLookupField copy = new DeleteLookupField(original);
+    assertEquals(original, copy);
+    assertEquals(original.hashCode(), copy.hashCode());
+
+    copy.getFields().getFirst().setKeyLookup("changed");
+    assertNotEquals(
+        original.getFields().getFirst().getKeyLookup(), 
copy.getFields().getFirst().getKeyLookup());
+  }
+
+  @Test
+  void testSetters() {
+    DeleteLookupField lookup = new DeleteLookupField();
+    lookup.setSchemaName("schema1");
+    lookup.setTableName("table1");
+    lookup.setFields(
+        Arrays.asList(
+            new DeleteKeyField("a", "=", "b", null), new DeleteKeyField("c", 
"<>", "d", null)));
+
+    assertEquals("schema1", lookup.getSchemaName());
+    assertEquals("table1", lookup.getTableName());
+    assertEquals(2, lookup.getFields().size());
+  }
+
+  @Test
+  void testEqualsAndHashCode() {
+    DeleteKeyField key = new DeleteKeyField("id", "=", "streamId", null);
+    DeleteLookupField a =
+        new DeleteLookupField("public", "customers", 
Collections.singletonList(key));
+    DeleteLookupField b =
+        new DeleteLookupField(
+            "public", "customers", Collections.singletonList(new 
DeleteKeyField(key)));
+    DeleteLookupField c =
+        new DeleteLookupField(
+            "public", "orders", Collections.singletonList(new 
DeleteKeyField(key)));
+
+    assertEquals(a, b);
+    assertEquals(a.hashCode(), b.hashCode());
+    assertNotEquals(a, c);
+  }
+
+  @Test
+  void testEqualsSameInstance() {
+    DeleteLookupField lookup = new DeleteLookupField();
+    assertEquals(lookup, lookup);
+  }
+}
diff --git 
a/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteMetaInjectionTest.java
 
b/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteMetaInjectionTest.java
new file mode 100644
index 0000000000..5bc8545ecf
--- /dev/null
+++ 
b/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteMetaInjectionTest.java
@@ -0,0 +1,34 @@
+/*
+ * 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
+ *
+ *      http://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.hop.pipeline.transforms.delete;
+
+import org.apache.hop.core.injection.BaseMetadataInjectionTestJunit5;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+/** DeleteMeta inject */
+class DeleteMetaInjectionTest extends 
BaseMetadataInjectionTestJunit5<DeleteMeta> {
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  @BeforeEach
+  void setup() throws Exception {
+    setup(new DeleteMeta());
+  }
+}
diff --git 
a/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteMetaTest.java
 
b/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteMetaTest.java
index b572d6b8ad..9c958f4eab 100644
--- 
a/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteMetaTest.java
+++ 
b/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteMetaTest.java
@@ -18,14 +18,14 @@
 package org.apache.hop.pipeline.transforms.delete;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Random;
 import java.util.UUID;
 import org.apache.commons.lang3.builder.EqualsBuilder;
 import org.apache.hop.core.HopEnvironment;
@@ -49,9 +49,10 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
+/** Unit test for {@link DeleteMeta} */
 class DeleteMetaTest implements IInitializer<ITransformMeta> {
-  LoadSaveTester loadSaveTester;
-  Class<DeleteMeta> testMetaClass = DeleteMeta.class;
+  private LoadSaveTester loadSaveTester;
+  private final Class<DeleteMeta> testMetaClass = DeleteMeta.class;
 
   @RegisterExtension
   static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
@@ -59,7 +60,7 @@ class DeleteMetaTest implements IInitializer<ITransformMeta> {
   @BeforeEach
   void setUpLoadSave() throws Exception {
     PluginRegistry.init();
-    List<String> attributes = Arrays.asList("commit", "connection", "lookup");
+    List<String> attributes = Arrays.asList("commit", "connection", "lookup", 
"use_batch");
 
     Map<String, String> getterMap =
         new HashMap<>() {
@@ -67,6 +68,7 @@ class DeleteMetaTest implements IInitializer<ITransformMeta> {
             put("commit", "getCommitSize");
             put("connection", "getConnection");
             put("lookup", "getLookup");
+            put("use_batch", "isUseBatchUpdate");
           }
         };
     Map<String, String> setterMap =
@@ -75,6 +77,7 @@ class DeleteMetaTest implements IInitializer<ITransformMeta> {
             put("commit", "setCommitSize");
             put("connection", "setConnection");
             put("lookup", "setLookup");
+            put("use_batch", "setUseBatchUpdate");
           }
         };
 
@@ -142,9 +145,7 @@ class DeleteMetaTest implements 
IInitializer<ITransformMeta> {
     loadSaveTester.testSerialization();
   }
 
-  private TransformMeta transformMeta;
   private Delete del;
-  private DeleteData data;
   private DeleteMeta meta;
 
   @BeforeAll
@@ -158,12 +159,12 @@ class DeleteMetaTest implements 
IInitializer<ITransformMeta> {
     pipelineMeta.setName("delete1");
 
     meta = new DeleteMeta();
-    data = new DeleteData();
+    DeleteData data = new DeleteData();
 
     PluginRegistry plugReg = PluginRegistry.getInstance();
     String deletePid = plugReg.getPluginId(TransformPluginType.class, meta);
 
-    transformMeta = new TransformMeta(deletePid, "delete", meta);
+    TransformMeta transformMeta = new TransformMeta(deletePid, "delete", meta);
     Pipeline pipeline = new LocalPipelineEngine(pipelineMeta);
 
     Map<String, String> vars = new HashMap<>();
@@ -193,36 +194,50 @@ class DeleteMetaTest implements 
IInitializer<ITransformMeta> {
       meta.getCommitSize(del);
       fail();
     } catch (Exception ex) {
+      // ignore ex
     }
   }
 
-  public class DeleteLookupKeyFieldInputFieldLoadSaveValidator
-      implements IFieldLoadSaveValidator<DeleteLookupField> {
-    final Random rand = new Random();
+  @Test
+  void testUseBatchUpdateDefaultIsFalse() {
+    assertFalse(meta.isUseBatchUpdate());
+  }
 
-    @Override
-    public DeleteLookupField getTestObject() {
-      return new DeleteLookupField(
-          UUID.randomUUID().toString(), UUID.randomUUID().toString(), new 
ArrayList<>());
-    }
+  @Test
+  void testUseBatchUpdateSetterAndGetter() {
+    meta.setUseBatchUpdate(true);
+    assertTrue(meta.isUseBatchUpdate());
+  }
 
-    @Override
-    public boolean validateTestObject(DeleteLookupField testObject, Object 
actual) {
-      if (!(actual instanceof DeleteLookupField)) {
-        return false;
-      }
-      DeleteLookupField another = (DeleteLookupField) actual;
-      return new EqualsBuilder()
-          .append(testObject.getSchemaName(), another.getSchemaName())
-          .append(testObject.getTableName(), another.getTableName())
-          .append(testObject.getFields(), another.getFields())
-          .isEquals();
-    }
+  @Test
+  void testCloneIncludesUseBatchUpdate() {
+    meta.setUseBatchUpdate(true);
+    DeleteMeta cloned = (DeleteMeta) meta.clone();
+    assertTrue(cloned.isUseBatchUpdate());
+  }
+
+  @Test
+  void testCopyConstructorIncludesUseBatchUpdate() {
+    meta.setUseBatchUpdate(true);
+    DeleteMeta copied = new DeleteMeta(meta);
+    assertTrue(copied.isUseBatchUpdate());
   }
 
-  public class DeleteKeyFieldInputFieldLoadSaveValidator
+  @Test
+  void testSetDefaultValues() {
+    DeleteMeta defaults = new DeleteMeta();
+    defaults.setDefault();
+    assertEquals("100", defaults.getCommitSize());
+    assertFalse(defaults.isUseBatchUpdate());
+  }
+
+  @Test
+  void testSupportsErrorHandling() {
+    assertTrue(meta.supportsErrorHandling());
+  }
+
+  public static class DeleteKeyFieldInputFieldLoadSaveValidator
       implements IFieldLoadSaveValidator<DeleteKeyField> {
-    final Random rand = new Random();
 
     @Override
     public DeleteKeyField getTestObject() {
@@ -235,10 +250,10 @@ class DeleteMetaTest implements 
IInitializer<ITransformMeta> {
 
     @Override
     public boolean validateTestObject(DeleteKeyField testObject, Object 
actual) {
-      if (!(actual instanceof DeleteKeyField)) {
+      if (!(actual instanceof DeleteKeyField another)) {
         return false;
       }
-      DeleteKeyField another = (DeleteKeyField) actual;
+
       return new EqualsBuilder()
           .append(testObject.getKeyLookup(), another.getKeyLookup())
           .append(testObject.getKeyCondition(), another.getKeyCondition())
diff --git 
a/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteSqlTest.java
 
b/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteSqlTest.java
new file mode 100644
index 0000000000..6fba2b08ff
--- /dev/null
+++ 
b/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteSqlTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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
+ *
+ *      http://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.hop.pipeline.transforms.delete;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import org.apache.hop.core.database.Database;
+import org.apache.hop.core.database.DatabaseMeta;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaInteger;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.pipeline.Pipeline;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.engines.local.LocalPipelineEngine;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+
+/** Unit test for {@link DatabaseMeta} */
+@ExtendWith(RestoreHopEngineEnvironmentExtension.class)
+class DeleteSqlTest {
+  private DeleteMeta meta;
+  private DeleteData data;
+  private Delete deleteTransform;
+
+  @BeforeEach
+  void setUp() {
+    DatabaseMeta databaseMeta = mock(DatabaseMeta.class);
+    when(databaseMeta.getQuotedSchemaTableCombination(any(), eq("public"), 
eq("customers")))
+        .thenReturn("\"public\".\"customers\"");
+    when(databaseMeta.quoteField(anyString()))
+        .thenAnswer(invocation -> "\"" + invocation.getArgument(0) + "\"");
+    when(databaseMeta.stripCR(anyString())).thenAnswer(invocation -> 
invocation.getArgument(0));
+
+    PipelineMeta pipelineMeta = spy(new PipelineMeta());
+    pipelineMeta.setName("delete-sql-test");
+    doReturn(databaseMeta).when(pipelineMeta).findDatabase(anyString(), any());
+
+    meta = new DeleteMeta();
+    meta.setConnection("unit-test-db");
+    meta.getLookup().setSchemaName("public");
+    meta.getLookup().setTableName("customers");
+
+    data = new DeleteData();
+    TransformMeta transformMeta = new TransformMeta("delete", meta);
+    pipelineMeta.addTransform(transformMeta);
+
+    Pipeline pipeline = new LocalPipelineEngine(pipelineMeta);
+    deleteTransform = new Delete(transformMeta, meta, data, 0, pipelineMeta, 
pipeline);
+    data.schemaTable = "\"public\".\"customers\"";
+  }
+
+  @Test
+  void testPrepareDeleteWithEqualsCondition() throws Exception {
+    meta.getLookup().getFields().add(new DeleteKeyField("id", "=", "streamId", 
null));
+
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaInteger("streamId"));
+
+    prepareDeleteAndCaptureSql(rowMeta);
+
+    assertEquals(1, data.deleteParameterRowMeta.size());
+    assertTrue(capturedSql().contains("DELETE FROM"));
+    assertTrue(capturedSql().contains("WHERE"));
+    assertTrue(capturedSql().contains("\"id\" = ?"));
+  }
+
+  @Test
+  void testPrepareDeleteWithBetweenCondition() throws Exception {
+    meta.getLookup().getFields().add(new DeleteKeyField("age", "BETWEEN", 
"minAge", "maxAge"));
+
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaInteger("minAge"));
+    rowMeta.addValueMeta(new ValueMetaInteger("maxAge"));
+
+    prepareDeleteAndCaptureSql(rowMeta);
+
+    assertEquals(2, data.deleteParameterRowMeta.size());
+    assertTrue(capturedSql().contains("\"age\" BETWEEN ? AND ?"));
+  }
+
+  @Test
+  void testPrepareDeleteWithIsNullCondition() throws Exception {
+    meta.getLookup().getFields().add(new DeleteKeyField("deleted_at", "IS 
NULL", "", null));
+
+    IRowMeta rowMeta = new RowMeta();
+
+    prepareDeleteAndCaptureSql(rowMeta);
+
+    assertEquals(0, data.deleteParameterRowMeta.size());
+    assertTrue(capturedSql().contains("\"deleted_at\" IS NULL"));
+  }
+
+  @Test
+  void testPrepareDeleteWithIsNotNullCondition() throws Exception {
+    meta.getLookup().getFields().add(new DeleteKeyField("updated_at", "IS NOT 
NULL", "", null));
+
+    IRowMeta rowMeta = new RowMeta();
+
+    prepareDeleteAndCaptureSql(rowMeta);
+
+    assertEquals(0, data.deleteParameterRowMeta.size());
+    assertTrue(capturedSql().contains("\"updated_at\" IS NOT NULL"));
+  }
+
+  @Test
+  void testPrepareDeleteWithMultipleKeyConditions() throws Exception {
+    meta.getLookup().getFields().add(new DeleteKeyField("id", "=", "streamId", 
null));
+    meta.getLookup().getFields().add(new DeleteKeyField("name", "<>", 
"streamName", null));
+    meta.getLookup().getFields().add(new DeleteKeyField("status", "IS NULL", 
"", null));
+
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaInteger("streamId"));
+    rowMeta.addValueMeta(new ValueMetaString("streamName"));
+
+    prepareDeleteAndCaptureSql(rowMeta);
+
+    assertEquals(2, data.deleteParameterRowMeta.size());
+    String sql = capturedSql();
+    assertTrue(sql.contains("\"id\" = ?"));
+    assertTrue(sql.contains("AND"));
+    assertTrue(sql.contains("\"name\" <> ?"));
+    assertTrue(sql.contains("\"status\" IS NULL"));
+  }
+
+  private String capturedSqlValue;
+
+  private void prepareDeleteAndCaptureSql(IRowMeta rowMeta) throws Exception {
+    Database db = mock(Database.class);
+    Connection connection = mock(Connection.class);
+    PreparedStatement preparedStatement = mock(PreparedStatement.class);
+
+    when(db.getConnection()).thenReturn(connection);
+    
when(connection.prepareStatement(anyString())).thenReturn(preparedStatement);
+
+    data.db = db;
+    deleteTransform.prepareDelete(rowMeta);
+
+    ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+    verify(connection).prepareStatement(sqlCaptor.capture());
+    capturedSqlValue = sqlCaptor.getValue();
+  }
+
+  private String capturedSql() {
+    return capturedSqlValue;
+  }
+}
diff --git 
a/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteTest.java
 
b/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteTest.java
new file mode 100644
index 0000000000..0400d47037
--- /dev/null
+++ 
b/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteTest.java
@@ -0,0 +1,218 @@
+/*
+ * 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
+ *
+ *      http://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.hop.pipeline.transforms.delete;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.sql.PreparedStatement;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.database.Database;
+import org.apache.hop.core.database.DatabaseMeta;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.core.plugins.PluginRegistry;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaInteger;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.pipeline.Pipeline;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.engines.local.LocalPipelineEngine;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transforms.mock.TransformMockHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+/** Unit test for {@link Delete} */
+@MockitoSettings(strictness = Strictness.LENIENT)
+class DeleteTest {
+
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  private TransformMockHelper<DeleteMeta, DeleteData> mockHelper;
+  private DeleteMeta meta;
+  private DeleteData data;
+  private PipelineMeta pipelineMeta;
+
+  @BeforeAll
+  static void initEnvironment() throws Exception {
+    HopEnvironment.init();
+    PluginRegistry.init();
+  }
+
+  @BeforeEach
+  void setUp() {
+    mockHelper = new TransformMockHelper<>("DeleteTest", DeleteMeta.class, 
DeleteData.class);
+    when(mockHelper.logChannelFactory.create(any(), any(ILoggingObject.class)))
+        .thenReturn(mockHelper.iLogChannel);
+    when(mockHelper.pipeline.isRunning()).thenReturn(true);
+    when(mockHelper.transformMeta.isPartitioned()).thenReturn(false);
+
+    pipelineMeta = spy(new PipelineMeta());
+    pipelineMeta.setName("delete-test");
+
+    DatabaseMeta databaseMeta = mock(DatabaseMeta.class);
+    when(databaseMeta.getQuotedSchemaTableCombination(any(), anyString(), 
anyString()))
+        .thenReturn("\"customers\"");
+    doReturn(databaseMeta).when(pipelineMeta).findDatabase(anyString(), any());
+
+    meta = new DeleteMeta();
+    meta.setConnection("unit-test-db");
+    meta.getLookup().setTableName("customers");
+    meta.getLookup().getFields().add(new DeleteKeyField("id", "=", "streamId", 
null));
+
+    data = new DeleteData();
+  }
+
+  @AfterEach
+  void tearDown() {
+    mockHelper.cleanUp();
+  }
+
+  private Delete newSpyTransform() {
+    TransformMeta transformMeta = new TransformMeta("delete", meta);
+    pipelineMeta.addTransform(transformMeta);
+    Pipeline pipeline = new LocalPipelineEngine(pipelineMeta);
+    Delete delete = spy(new Delete(transformMeta, meta, data, 0, pipelineMeta, 
pipeline));
+    delete.setMetadataProvider(new MemoryMetadataProvider());
+    return delete;
+  }
+
+  @Test
+  void testInitFailsWhenConnectionMissing() {
+    meta.setConnection(null);
+    Delete delete = newSpyTransform();
+    assertFalse(delete.init());
+  }
+
+  @Test
+  void testInitFailsWhenConnectionNotFound() {
+    meta.setConnection("missing-connection");
+    Delete delete = newSpyTransform();
+    assertFalse(delete.init());
+  }
+
+  @Test
+  void testProcessRowUsesBatchWhenEnabled() throws HopException {
+    meta.setUseBatchUpdate(true);
+
+    Database db = mock(Database.class);
+    PreparedStatement preparedStatement = mock(PreparedStatement.class);
+    data.db = db;
+
+    Delete delete = newSpyTransform();
+
+    IRowMeta inputRowMeta = new RowMeta();
+    inputRowMeta.addValueMeta(new ValueMetaInteger("streamId"));
+    doReturn(new Object[] {1L}).doReturn(null).when(delete).getRow();
+    doReturn(inputRowMeta).when(delete).getInputRowMeta();
+    doAnswer(
+            inv -> {
+              data.prepStatementDelete = preparedStatement;
+              data.deleteParameterRowMeta = new RowMeta();
+              data.deleteParameterRowMeta.addValueMeta(new 
ValueMetaInteger("streamId"));
+              return null;
+            })
+        .when(delete)
+        .prepareDelete(any());
+
+    assertTrue(delete.processRow());
+    verify(db).insertRow(preparedStatement, true, true);
+    assertFalse(delete.processRow());
+  }
+
+  @Test
+  void testProcessRowDoesNotUseBatchWhenDisabled() throws HopException {
+    meta.setUseBatchUpdate(false);
+
+    Database db = mock(Database.class);
+    PreparedStatement preparedStatement = mock(PreparedStatement.class);
+    data.db = db;
+
+    Delete delete = newSpyTransform();
+
+    IRowMeta inputRowMeta = new RowMeta();
+    inputRowMeta.addValueMeta(new ValueMetaInteger("streamId"));
+    doReturn(new Object[] {1L}).doReturn(null).when(delete).getRow();
+    doReturn(inputRowMeta).when(delete).getInputRowMeta();
+    doAnswer(
+            inv -> {
+              data.prepStatementDelete = preparedStatement;
+              data.deleteParameterRowMeta = new RowMeta();
+              data.deleteParameterRowMeta.addValueMeta(new 
ValueMetaInteger("streamId"));
+              return null;
+            })
+        .when(delete)
+        .prepareDelete(any());
+
+    assertTrue(delete.processRow());
+    verify(db).insertRow(preparedStatement, false, true);
+  }
+
+  @Test
+  void testDisposeCallsEmptyAndCommitWhenBatchEnabled() throws Exception {
+    meta.setUseBatchUpdate(true);
+
+    Database db = mock(Database.class);
+    PreparedStatement preparedStatement = mock(PreparedStatement.class);
+    when(db.isAutoCommit()).thenReturn(false);
+
+    data.db = db;
+    data.prepStatementDelete = preparedStatement;
+
+    Delete delete = newSpyTransform();
+    delete.dispose();
+
+    verify(db).emptyAndCommit(preparedStatement, true);
+    verify(db).disconnect();
+  }
+
+  @Test
+  void testDisposeCallsEmptyAndCommitWhenBatchDisabled() throws Exception {
+    meta.setUseBatchUpdate(false);
+
+    Database db = mock(Database.class);
+    PreparedStatement preparedStatement = mock(PreparedStatement.class);
+    when(db.isAutoCommit()).thenReturn(false);
+
+    data.db = db;
+    data.prepStatementDelete = preparedStatement;
+
+    Delete delete = newSpyTransform();
+    delete.dispose();
+
+    verify(db).emptyAndCommit(preparedStatement, false);
+    verify(db).disconnect();
+  }
+}

Reply via email to