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 baa3ad4cbe Cleanup XML of transform MonetDB Bulk Loader, fixes #1954 
(#6772)
baa3ad4cbe is described below

commit baa3ad4cbeab491dcf7274aaf1d3e9b72f0666d1
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Thu Mar 12 11:08:13 2026 +0100

    Cleanup XML of transform MonetDB Bulk Loader, fixes #1954 (#6772)
---
 .../monetdbbulkloader/MonetDbBulkLoader.java       |  70 ++-
 .../monetdbbulkloader/MonetDbBulkLoaderDialog.java |  45 +-
 .../monetdbbulkloader/MonetDbBulkLoaderMeta.java   | 467 +++++----------------
 .../MonetDbBulkLoaderMetaTest.java                 | 153 +++++++
 .../monetdbbulkloader/MonetDbBulkLoaderTest.java   | 107 +++++
 .../monetdbbulkloader/MonetDbVersionTest.java      | 108 +++++
 .../test/resources/monetdbbulkloader-transform.xml |  57 +++
 7 files changed, 570 insertions(+), 437 deletions(-)

diff --git 
a/plugins/transforms/monetdbbulkloader/src/main/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbBulkLoader.java
 
b/plugins/transforms/monetdbbulkloader/src/main/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbBulkLoader.java
index 14d48d3d3c..fd54cb56cb 100644
--- 
a/plugins/transforms/monetdbbulkloader/src/main/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbBulkLoader.java
+++ 
b/plugins/transforms/monetdbbulkloader/src/main/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbBulkLoader.java
@@ -50,6 +50,7 @@ public class MonetDbBulkLoader extends 
BaseTransform<MonetDbBulkLoaderMeta, Mone
 
   private IRowMeta physicalTableRowMeta;
   private static final String ERROR_LOADING_DATA = "Error loading data: ";
+  DatabaseMeta databaseMeta;
 
   public MonetDbBulkLoader(
       TransformMeta transformMeta,
@@ -88,10 +89,8 @@ public class MonetDbBulkLoader extends 
BaseTransform<MonetDbBulkLoaderMeta, Mone
         logDetailed("Auto String Length flag: " + meta.isAutoStringWidths());
       }
 
-      DatabaseMeta dm = meta.getDatabaseMeta();
-
-      String user = resolve(Const.NVL(dm.getUsername(), ""));
-      String password = Utils.resolvePassword(variables, 
Const.NVL(dm.getPassword(), ""));
+      String user = resolve(Const.NVL(databaseMeta.getUsername(), ""));
+      String password = Utils.resolvePassword(variables, 
Const.NVL(databaseMeta.getPassword(), ""));
 
       MapiSocket mserver = getMonetDBConnection();
       data.mserver = mserver;
@@ -115,7 +114,7 @@ public class MonetDbBulkLoader extends 
BaseTransform<MonetDbBulkLoaderMeta, Mone
       // get table metadata, will be used later for date type identification 
(DATE, TIMESTAMP, ...)
       try {
 
-        db = new Database(meta.getParent(), variables, dm);
+        db = new Database(meta.getParent(), variables, databaseMeta);
         db.connect();
         physicalTableRowMeta = db.getTableFields(data.schemaTable);
       } catch (Exception e) {
@@ -149,7 +148,7 @@ public class MonetDbBulkLoader extends 
BaseTransform<MonetDbBulkLoaderMeta, Mone
         setOutputDone();
         if (!first) {
           try {
-            writeBufferToMonetDB(meta.getDatabaseMeta());
+            writeBufferToMonetDB(databaseMeta);
             data.out.flush();
           } finally {
             data.mserver.close();
@@ -163,9 +162,9 @@ public class MonetDbBulkLoader extends 
BaseTransform<MonetDbBulkLoaderMeta, Mone
 
         // Cache field indexes.
         //
-        data.keynrs = new int[meta.getFieldStream().length];
+        data.keynrs = new int[meta.getFields().size()];
         for (int i = 0; i < data.keynrs.length; i++) {
-          data.keynrs[i] = 
getInputRowMeta().indexOfValue(meta.getFieldStream()[i]);
+          data.keynrs[i] = 
getInputRowMeta().indexOfValue(meta.getFields().get(i).getFieldStream());
         }
 
         // execute the psql statement...
@@ -173,7 +172,7 @@ public class MonetDbBulkLoader extends 
BaseTransform<MonetDbBulkLoaderMeta, Mone
         execute(meta);
       }
 
-      writeRowToMonetDB(getInputRowMeta(), r, meta.getDatabaseMeta());
+      writeRowToMonetDB(getInputRowMeta(), r, databaseMeta);
       putRow(getInputRowMeta(), r);
       incrementLinesOutput();
 
@@ -240,7 +239,7 @@ public class MonetDbBulkLoader extends 
BaseTransform<MonetDbBulkLoaderMeta, Mone
               line.append(data.quote);
               break;
             case IValueMeta.TYPE_INTEGER:
-              if (valueMeta.isStorageBinaryString() && 
meta.getFieldFormatOk()[i]) {
+              if (valueMeta.isStorageBinaryString() && 
meta.getFields().get(i).isFieldFormatOk()) {
                 line.append(valueMeta.getString(valueData));
               } else {
                 Long value = valueMeta.getInteger(valueData);
@@ -257,7 +256,7 @@ public class MonetDbBulkLoader extends 
BaseTransform<MonetDbBulkLoaderMeta, Mone
               //
             case IValueMeta.TYPE_TIMESTAMP, IValueMeta.TYPE_DATE:
               // Keep the data format as indicated.
-              if (valueMeta.isStorageBinaryString() && 
meta.getFieldFormatOk()[i]) {
+              if (valueMeta.isStorageBinaryString() && 
meta.getFields().get(i).isFieldFormatOk()) {
                 line.append(valueMeta.getString(valueData));
               } else {
 
@@ -301,7 +300,7 @@ public class MonetDbBulkLoader extends 
BaseTransform<MonetDbBulkLoaderMeta, Mone
               break;
 
             case IValueMeta.TYPE_NUMBER:
-              if (valueMeta.isStorageBinaryString() && 
meta.getFieldFormatOk()[i]) {
+              if (valueMeta.isStorageBinaryString() && 
meta.getFields().get(i).isFieldFormatOk()) {
                 line.append(valueMeta.getString(valueData));
               } else {
                 Double dbl = valueMeta.getNumber(valueData);
@@ -326,7 +325,7 @@ public class MonetDbBulkLoader extends 
BaseTransform<MonetDbBulkLoaderMeta, Mone
               break;
 
             case IValueMeta.TYPE_BIGNUMBER:
-              if (valueMeta.isStorageBinaryString() && 
meta.getFieldFormatOk()[i]) {
+              if (valueMeta.isStorageBinaryString() && 
meta.getFields().get(i).isFieldFormatOk()) {
                 line.append(valueMeta.getString(valueData));
               } else {
                 BigDecimal bd = valueMeta.getBigNumber(valueData);
@@ -374,8 +373,8 @@ public class MonetDbBulkLoader extends 
BaseTransform<MonetDbBulkLoaderMeta, Mone
     String cmd;
     String table = data.schemaTable;
     String truncateStatement =
-        meta.getDatabaseMeta()
-            .getTruncateTableStatement(variables, meta.getSchemaName(), 
meta.getTableName());
+        databaseMeta.getTruncateTableStatement(
+            variables, meta.getSchemaName(), meta.getTableName());
     if (truncateStatement == null) {
       throw new HopException("Truncate table is not supported!");
     }
@@ -535,7 +534,7 @@ public class MonetDbBulkLoader extends 
BaseTransform<MonetDbBulkLoaderMeta, Mone
 
   protected void verifyDatabaseConnection() throws HopException {
     // Confirming Database Connection is defined.
-    if (meta.getDatabaseMeta() == null) {
+    if (databaseMeta == null) {
       throw new HopException(
           BaseMessages.getString(PKG, 
"MonetDbBulkLoaderMeta.GetSQL.NoConnectionDefined"));
     }
@@ -545,6 +544,8 @@ public class MonetDbBulkLoader extends 
BaseTransform<MonetDbBulkLoaderMeta, Mone
   public boolean init() {
     if (super.init()) {
 
+      databaseMeta = 
getPipelineMeta().findDatabase(meta.getDbConnectionName(), variables);
+
       try {
         verifyDatabaseConnection();
       } catch (HopException ex) {
@@ -592,21 +593,12 @@ public class MonetDbBulkLoader extends 
BaseTransform<MonetDbBulkLoaderMeta, Mone
 
       // Make sure our database connection settings are consistent with our 
dialog settings by
       // altering the connection with an updated answer depending on the 
dialog setting.
-      meta.getDatabaseMeta().setQuoteAllFields(meta.isFullyQuoteSQL());
-
-      // Support parameterized database connection names
-      String connectionName = meta.getDbConnectionName();
-      if (!Utils.isEmpty(connectionName)
-          && connectionName.startsWith("${")
-          && connectionName.endsWith("}")) {
-        
meta.setDatabaseMeta(getPipelineMeta().findDatabase(resolve(connectionName), 
variables));
-      }
+      databaseMeta.setQuoteAllFields(meta.isFullyQuoteSQL());
 
       // Schema-table combination...
       data.schemaTable =
-          meta.getDatabaseMeta(this)
-              .getQuotedSchemaTableCombination(
-                  variables, meta.getSchemaName(), meta.getTableName());
+          databaseMeta.getQuotedSchemaTableCombination(
+              variables, meta.getSchemaName(), meta.getTableName());
 
       return true;
     }
@@ -622,12 +614,11 @@ public class MonetDbBulkLoader extends 
BaseTransform<MonetDbBulkLoaderMeta, Mone
     if (this.meta == null) {
       throw new HopException("No metadata available to determine connection 
information from.");
     }
-    DatabaseMeta dm = meta.getDatabaseMeta();
-    String hostname = resolve(Const.NVL(dm.getHostname(), ""));
-    String portnum = resolve(Const.NVL(dm.getPort(), ""));
-    String user = resolve(Const.NVL(dm.getUsername(), ""));
-    String password = Utils.resolvePassword(variables, 
Const.NVL(dm.getPassword(), ""));
-    String db = resolve(Const.NVL(dm.getDatabaseName(), ""));
+    String hostname = resolve(Const.NVL(databaseMeta.getHostname(), ""));
+    String portnum = resolve(Const.NVL(databaseMeta.getPort(), ""));
+    String user = resolve(Const.NVL(databaseMeta.getUsername(), ""));
+    String password = Utils.resolvePassword(variables, 
Const.NVL(databaseMeta.getPassword(), ""));
+    String db = resolve(Const.NVL(databaseMeta.getDatabaseName(), ""));
 
     return getMonetDBConnection(
         hostname, Integer.parseInt(portnum), user, password, db, 
getLogChannel());
@@ -664,12 +655,11 @@ public class MonetDbBulkLoader extends 
BaseTransform<MonetDbBulkLoaderMeta, Mone
     if (this.meta == null) {
       throw new HopException("No metadata available to determine connection 
information from.");
     }
-    DatabaseMeta dm = meta.getDatabaseMeta();
-    String hostname = resolve(Const.NVL(dm.getHostname(), ""));
-    String portnum = resolve(Const.NVL(dm.getPort(), ""));
-    String user = resolve(Const.NVL(dm.getUsername(), ""));
-    String password = Utils.resolvePassword(variables, 
Const.NVL(dm.getPassword(), ""));
-    String db = resolve(Const.NVL(dm.getDatabaseName(), ""));
+    String hostname = resolve(Const.NVL(databaseMeta.getHostname(), ""));
+    String portnum = resolve(Const.NVL(databaseMeta.getPort(), ""));
+    String user = resolve(Const.NVL(databaseMeta.getUsername(), ""));
+    String password = Utils.resolvePassword(variables, 
Const.NVL(databaseMeta.getPassword(), ""));
+    String db = resolve(Const.NVL(databaseMeta.getDatabaseName(), ""));
 
     executeSql(query, hostname, Integer.parseInt(portnum), user, password, db);
   }
diff --git 
a/plugins/transforms/monetdbbulkloader/src/main/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbBulkLoaderDialog.java
 
b/plugins/transforms/monetdbbulkloader/src/main/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbBulkLoaderDialog.java
index 4ca17e8be3..7b4e1b6bc2 100644
--- 
a/plugins/transforms/monetdbbulkloader/src/main/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbBulkLoaderDialog.java
+++ 
b/plugins/transforms/monetdbbulkloader/src/main/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbBulkLoaderDialog.java
@@ -163,7 +163,7 @@ public class MonetDbBulkLoaderDialog extends 
BaseTransformDialog {
     // Connection line
     //
     // Connection line
-    wConnection = addConnectionLine(shell, wSpacer, input.getDatabaseMeta(), 
lsMod);
+    wConnection = addConnectionLine(shell, wSpacer, 
input.getDbConnectionName(), lsMod);
 
     // //////////////////////////////////////////////
     // Prepare the Folder that will contain tabs. //
@@ -328,12 +328,13 @@ public class MonetDbBulkLoaderDialog extends 
BaseTransformDialog {
 
     wFullyQuoteSQL = new Button(wGeneralSettingsComp, SWT.CHECK);
     PropsUi.setLook(wFullyQuoteSQL);
+    DatabaseMeta databaseMeta = 
pipelineMeta.findDatabase(wConnection.getText(), variables);
     SelectionAdapter lsFullyQuoteSQL =
         new SelectionAdapter() {
           @Override
           public void widgetSelected(SelectionEvent arg0) {
             input.setChanged();
-            
input.getDatabaseMeta().setQuoteAllFields(wFullyQuoteSQL.getSelection());
+            databaseMeta.setQuoteAllFields(wFullyQuoteSQL.getSelection());
           }
         };
     wFullyQuoteSQL.addSelectionListener(lsFullyQuoteSQL);
@@ -408,7 +409,7 @@ public class MonetDbBulkLoaderDialog extends 
BaseTransformDialog {
     PropsUi.setLook(wlReturn);
 
     int upInsCols = 3;
-    int upInsRows = (input.getFieldTable() != null ? 
input.getFieldTable().length : 1);
+    int upInsRows = (input.getFields() != null ? input.getFields().size() : 1);
 
     ciReturn = new ColumnInfo[upInsCols];
     ciReturn[0] =
@@ -784,21 +785,21 @@ public class MonetDbBulkLoaderDialog extends 
BaseTransformDialog {
       logDebug(BaseMessages.getString(PKG, 
"MonetDBBulkLoaderDialog.Log.GettingKeyInfo"));
     }
 
-    if (input.getFieldTable() != null) {
-      for (int i = 0; i < input.getFieldTable().length; i++) {
+    if (!input.getFields().isEmpty()) {
+      for (int i = 0; i < input.getFields().size(); i++) {
         TableItem item = wReturn.table.getItem(i);
-        if (input.getFieldTable()[i] != null) {
-          item.setText(1, input.getFieldTable()[i]);
+        if (input.getFields().get(i).getFieldTable() != null) {
+          item.setText(1, input.getFields().get(i).getFieldTable());
         }
-        if (input.getFieldStream()[i] != null) {
-          item.setText(2, input.getFieldStream()[i]);
+        if (input.getFields().get(i).getFieldStream() != null) {
+          item.setText(2, input.getFields().get(i).getFieldStream());
         }
-        item.setText(3, input.getFieldFormatOk()[i] ? "Y" : "N");
+        item.setText(3, input.getFields().get(i).isFieldFormatOk() ? "Y" : 
"N");
       }
     }
 
-    if (input.getDatabaseMeta() != null) {
-      wConnection.setText(input.getDatabaseMeta().getName());
+    if (input.getDbConnectionName() != null) {
+      wConnection.setText(input.getDbConnectionName());
     }
     // General Settings Tab values from transform meta-data configuration.
     if (input.getSchemaName() != null) {
@@ -855,24 +856,23 @@ public class MonetDbBulkLoaderDialog extends 
BaseTransformDialog {
   protected void getInfo(MonetDbBulkLoaderMeta inf) {
     int nrfields = wReturn.nrNonEmpty();
 
-    inf.allocate(nrfields);
-
     if (log.isDebug()) {
       logDebug(
           BaseMessages.getString(PKG, 
"MonetDBBulkLoaderDialog.Log.FoundFields", "" + nrfields));
     }
+    inf.getFields().clear();
 
     for (int i = 0; i < nrfields; i++) {
       TableItem item = wReturn.getNonEmpty(i);
-      inf.getFieldTable()[i] = item.getText(1);
-      inf.getFieldStream()[i] = item.getText(2);
-      inf.getFieldFormatOk()[i] = "Y".equalsIgnoreCase(item.getText(3));
+      MonetDbBulkLoaderMeta.MonetDbField mf =
+          new MonetDbBulkLoaderMeta.MonetDbField(
+              item.getText(1), item.getText(2), 
"Y".equalsIgnoreCase(item.getText(3)));
+      inf.getFields().add(mf);
     }
     // General Settings Tab values from transform meta-data configuration.
     inf.setDbConnectionName(wConnection.getText());
     inf.setSchemaName(wSchema.getText());
     inf.setTableName(wTable.getText());
-    inf.setDatabaseMeta(pipelineMeta.findDatabase(wConnection.getText(), 
variables));
     inf.setBufferSize(wBufferSize.getText());
     inf.setLogFile(wLogFile.getText());
     inf.setTruncate(wTruncate.getSelection());
@@ -954,7 +954,6 @@ public class MonetDbBulkLoaderDialog extends 
BaseTransformDialog {
       return;
     }
     // refresh data
-    input.setDatabaseMeta(pipelineMeta.findDatabase(wConnection.getText(), 
variables));
     input.setTableName(variables.resolve(wTable.getText()));
     ITransformMeta transformMetaInterface = transformMeta.getTransform();
     try {
@@ -1118,17 +1117,13 @@ public class MonetDbBulkLoaderDialog extends 
BaseTransformDialog {
 
       String name = transformName; // new name might not yet be linked to 
other transforms!
 
+      DatabaseMeta databaseMeta = 
pipelineMeta.findDatabase(wConnection.getText(), variables);
       SqlStatement sql = info.getTableDdl(variables, pipelineMeta, name, 
false, null, false);
       if (!sql.hasError()) {
         if (sql.hasSql()) {
           SqlEditor sqledit =
               new SqlEditor(
-                  shell,
-                  SWT.NONE,
-                  variables,
-                  info.getDatabaseMeta(),
-                  DbCache.getInstance(),
-                  sql.getSql());
+                  shell, SWT.NONE, variables, databaseMeta, 
DbCache.getInstance(), sql.getSql());
           sqledit.open();
         } else {
           MessageBox mb = new MessageBox(shell, SWT.OK | SWT.ICON_INFORMATION);
diff --git 
a/plugins/transforms/monetdbbulkloader/src/main/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbBulkLoaderMeta.java
 
b/plugins/transforms/monetdbbulkloader/src/main/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbBulkLoaderMeta.java
index 0190feddcd..da14c5142f 100644
--- 
a/plugins/transforms/monetdbbulkloader/src/main/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbBulkLoaderMeta.java
+++ 
b/plugins/transforms/monetdbbulkloader/src/main/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbBulkLoaderMeta.java
@@ -16,7 +16,10 @@
  */
 package org.apache.hop.pipeline.transforms.monetdbbulkloader;
 
+import java.util.ArrayList;
 import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
 import org.apache.hop.core.CheckResult;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.ICheckResult;
@@ -27,23 +30,20 @@ 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.exception.HopTransformException;
-import org.apache.hop.core.exception.HopXmlException;
-import org.apache.hop.core.injection.Injection;
-import org.apache.hop.core.injection.InjectionSupported;
 import org.apache.hop.core.row.IRowMeta;
 import org.apache.hop.core.row.IValueMeta;
 import org.apache.hop.core.row.RowMeta;
 import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.IVariables;
-import org.apache.hop.core.xml.XmlHandler;
 import org.apache.hop.databases.monetdb.MonetDBDatabaseMeta;
 import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.metadata.api.HopMetadataPropertyType;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.pipeline.DatabaseImpact;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.pipeline.transform.BaseTransformMeta;
 import org.apache.hop.pipeline.transform.TransformMeta;
-import org.w3c.dom.Node;
 
 @Transform(
     id = "MonetDBBulkLoader",
@@ -56,53 +56,69 @@ import org.w3c.dom.Node;
     isIncludeJdbcDrivers = true,
     classLoaderGroup = "monetdb",
     actionTransformTypes = {ActionTransformType.RDBMS, 
ActionTransformType.OUTPUT})
-@InjectionSupported(localizationPrefix = "MonetDBBulkLoaderDialog.Injection.")
+@Getter
+@Setter
 public class MonetDbBulkLoaderMeta
     extends BaseTransformMeta<MonetDbBulkLoader, MonetDbBulkLoaderData> {
   private static final Class<?> PKG =
       MonetDbBulkLoaderMeta.class; // for i18n purposes, needed by 
Translator2!!
   public static final String CONST_SPACES = "        ";
 
+  /** Inline class representing a single field mapping from stream to table 
column. */
+  @Getter
+  @Setter
+  public static class MonetDbField {
+    @HopMetadataProperty(key = "stream_name", injectionKey = "TARGETFIELDS")
+    private String fieldTable;
+
+    @HopMetadataProperty(key = "field_name", injectionKey = "SOURCEFIELDS")
+    private String fieldStream;
+
+    @HopMetadataProperty(key = "field_format_ok", injectionKey = 
"FIELDFORMATOK")
+    private boolean fieldFormatOk;
+
+    public MonetDbField() {}
+
+    public MonetDbField(String fieldTable, String fieldStream, boolean 
fieldFormatOk) {
+      this.fieldTable = fieldTable;
+      this.fieldStream = fieldStream;
+      this.fieldFormatOk = fieldFormatOk;
+    }
+  }
+
   /** The database connection name * */
-  @Injection(name = "CONNECTIONNAME")
+  @HopMetadataProperty(
+      key = "connection",
+      injectionKey = "CONNECTIONNAME",
+      hopMetadataPropertyType = HopMetadataPropertyType.RDBMS_CONNECTION)
   private String dbConnectionName;
 
   /** what's the schema for the target? */
-  @Injection(name = "SCHEMANAME")
+  @HopMetadataProperty(key = "schema", injectionKey = "SCHEMANAME")
   private String schemaName;
 
   /** what's the table for the target? */
-  @Injection(name = "TABLENAME")
+  @HopMetadataProperty(key = "table", injectionKey = "SCHEMANAME")
   private String tableName;
 
   /** Path to the log file */
-  @Injection(name = "LOGFILE")
+  @HopMetadataProperty(key = "log_file", injectionKey = "LOGFILE")
   private String logFile;
 
-  /** database connection */
-  private DatabaseMeta databaseMeta;
-
-  /** Field name of the target table */
-  @Injection(name = "TARGETFIELDS")
-  private String[] fieldTable;
-
-  /** Field name in the stream */
-  @Injection(name = "SOURCEFIELDS")
-  private String[] fieldStream;
-
-  /** flag to indicate that the format is OK for MonetDB */
-  @Injection(name = "FIELDFORMATOK")
-  private boolean[] fieldFormatOk;
+  @HopMetadataProperty(
+      key = "mapping",
+      inlineListTags = {"stream_name", "field_name", "field_format_ok"})
+  private List<MonetDbField> fields;
 
   /** Field separator character or string used to delimit fields */
-  @Injection(name = "SEPARATOR")
+  @HopMetadataProperty(key = "field_separator", injectionKey = "SEPARATOR")
   private String fieldSeparator;
 
   /**
    * Specifies which character surrounds each field's data. i.e. double 
quotes, single quotes or
    * something else
    */
-  @Injection(name = "FIELDENCLOSURE")
+  @HopMetadataProperty(key = "field_enclosure", injectionKey = 
"FIELDENCLOSURE")
   private String fieldEnclosure;
 
   /**
@@ -110,19 +126,19 @@ public class MonetDbBulkLoaderMeta
    * something else the value is written out as text to the API and MonetDB is 
able to interpret it
    * to the correct representation of NULL in the database for the given 
column type.
    */
-  @Injection(name = "NULLVALUE")
+  @HopMetadataProperty(key = "null_representation", injectionKey = "NULLVALUE")
   private String nullRepresentation;
 
   /** Encoding to use */
-  @Injection(name = "ENCODING")
+  @HopMetadataProperty(key = "encoding", injectionKey = "ENCODING")
   private String encoding;
 
   /** Truncate table? */
-  @Injection(name = "TRUNCATE")
+  @HopMetadataProperty(key = "truncate", injectionKey = "TRUNCATE")
   private boolean truncate = false;
 
   /** Fully Quote SQL used in the transform? */
-  @Injection(name = "QUOTEFIELDS")
+  @HopMetadataProperty(key = "fully_quote_sql", injectionKey = "QUOTEFIELDS")
   private boolean fullyQuoteSQL;
 
   /** Auto adjust the table structure? */
@@ -131,38 +147,11 @@ public class MonetDbBulkLoaderMeta
   /** Auto adjust strings that are too long? */
   private boolean autoStringWidths = false;
 
-  public boolean isAutoStringWidths() {
-    return autoStringWidths;
-  }
-
-  public boolean isTruncate() {
-    return truncate;
-  }
-
-  public void setTruncate(boolean truncate) {
-    this.truncate = truncate;
-  }
-
-  public boolean isFullyQuoteSQL() {
-    return fullyQuoteSQL;
-  }
-
-  public void setFullyQuoteSQL(boolean fullyQuoteSQLbool) {
-    this.fullyQuoteSQL = fullyQuoteSQLbool;
-  }
-
-  public boolean isAutoSchema() {
-    return autoSchema;
-  }
-
-  public void setAutoSchema(boolean autoSchema) {
-    this.autoSchema = autoSchema;
-  }
-
   /**
    * The number of rows to buffer before passing them over to MonetDB. This 
number should be
    * non-zero since we need to specify the number of rows we pass.
    */
+  @HopMetadataProperty(key = "buffer_size")
   private String bufferSize;
 
   /**
@@ -172,215 +161,19 @@ public class MonetDbBulkLoaderMeta
    */
   private boolean compatibilityDbVersionMode = false;
 
-  /**
-   * @return Returns the database.
-   */
-  public DatabaseMeta getDatabaseMeta() {
-    return databaseMeta;
-  }
-
-  /**
-   * @return Returns the database.
-   */
-  public DatabaseMeta getDatabaseMeta(MonetDbBulkLoader loader) {
-    return databaseMeta;
-  }
-
-  /**
-   * @param database The database to set.
-   */
-  public void setDatabaseMeta(DatabaseMeta database) {
-    this.databaseMeta = database;
-  }
-
-  /**
-   * @return Returns the tableName.
-   */
-  public String getTableName() {
-    return tableName;
-  }
-
-  /**
-   * @param tableName The tableName to set.
-   */
-  public void setTableName(String tableName) {
-    this.tableName = tableName;
-  }
-
-  /**
-   * @return Returns the fieldTable.
-   */
-  public String[] getFieldTable() {
-    return fieldTable;
-  }
-
-  /**
-   * @param fieldTable The fieldTable to set.
-   */
-  public void setFieldTable(String[] fieldTable) {
-    this.fieldTable = fieldTable;
-  }
-
-  /**
-   * @return Returns the fieldStream.
-   */
-  public String[] getFieldStream() {
-    return fieldStream;
-  }
-
-  /**
-   * @param fieldStream The fieldStream to set.
-   */
-  public void setFieldStream(String[] fieldStream) {
-    this.fieldStream = fieldStream;
-  }
-
-  /**
-   * @deprecated
-   * @param transformNode xml for the transform
-   * @param metadataProvider containing variables
-   * @throws HopXmlException when unable to parse xml
-   */
-  @Override
-  @Deprecated(since = "2.14")
-  public void loadXml(Node transformNode, IHopMetadataProvider 
metadataProvider)
-      throws HopXmlException {
-    readData(transformNode, metadataProvider);
-  }
-
-  public void allocate(int nrvalues) {
-    fieldTable = new String[nrvalues];
-    fieldStream = new String[nrvalues];
-    fieldFormatOk = new boolean[nrvalues];
-  }
-
-  @Override
-  public Object clone() {
-    MonetDbBulkLoaderMeta retval = (MonetDbBulkLoaderMeta) super.clone();
-    int nrvalues = fieldTable.length;
-
-    retval.allocate(nrvalues);
-
-    System.arraycopy(fieldTable, 0, retval.fieldTable, 0, nrvalues);
-    System.arraycopy(fieldStream, 0, retval.fieldStream, 0, nrvalues);
-    System.arraycopy(fieldFormatOk, 0, retval.fieldFormatOk, 0, nrvalues);
-    return retval;
-  }
-
-  private void readData(Node transformNode, IHopMetadataProvider 
metadataProvider)
-      throws HopXmlException {
-    try {
-      dbConnectionName = XmlHandler.getTagValue(transformNode, "connection");
-      databaseMeta = DatabaseMeta.loadDatabase(metadataProvider, 
dbConnectionName);
-
-      schemaName = XmlHandler.getTagValue(transformNode, "schema");
-      tableName = XmlHandler.getTagValue(transformNode, "table");
-      bufferSize = XmlHandler.getTagValue(transformNode, "buffer_size");
-      logFile = XmlHandler.getTagValue(transformNode, "log_file");
-      truncate = "Y".equals(XmlHandler.getTagValue(transformNode, "truncate"));
-
-      // New in January 2013 Updates - For compatibility we set default values 
according to the old
-      // version of the transform.
-      //
-
-      // This expression will only be true if a yes answer was previously 
recorded.
-      fullyQuoteSQL = "Y".equals(XmlHandler.getTagValue(transformNode, 
"fully_quote_sql"));
-
-      fieldSeparator = XmlHandler.getTagValue(transformNode, 
"field_separator");
-      if (fieldSeparator == null) {
-        fieldSeparator = "|";
-      }
-      fieldEnclosure = XmlHandler.getTagValue(transformNode, 
"field_enclosure");
-      if (fieldEnclosure == null) {
-        fieldEnclosure = "\"";
-      }
-      nullRepresentation = XmlHandler.getTagValue(transformNode, 
"null_representation");
-      if (nullRepresentation == null) {
-        nullRepresentation = "null";
-      }
-      encoding = XmlHandler.getTagValue(transformNode, "encoding");
-      if (encoding == null) {
-        encoding = "UTF-8";
-      }
-
-      int nrvalues = XmlHandler.countNodes(transformNode, "mapping");
-      allocate(nrvalues);
-
-      for (int i = 0; i < nrvalues; i++) {
-        Node vnode = XmlHandler.getSubNodeByNr(transformNode, "mapping", i);
-
-        fieldTable[i] = XmlHandler.getTagValue(vnode, "stream_name");
-        fieldStream[i] = XmlHandler.getTagValue(vnode, "field_name");
-        if (fieldStream[i] == null) {
-          fieldStream[i] = fieldTable[i]; // default: the same name!
-        }
-        fieldFormatOk[i] = "Y".equalsIgnoreCase(XmlHandler.getTagValue(vnode, 
"field_format_ok"));
-      }
-    } catch (Exception e) {
-      throw new HopXmlException(
-          BaseMessages.getString(
-              PKG, 
"MonetDBBulkLoaderMeta.Exception.UnableToReadTransformInfoFromXML"),
-          e);
-    }
-  }
-
   @Override
   public void setDefault() {
-    fieldTable = null;
-    databaseMeta = null;
+    fields = new ArrayList<>();
     schemaName = "";
     tableName = BaseMessages.getString(PKG, 
"MonetDBBulkLoaderMeta.DefaultTableName");
     bufferSize = "100000";
     logFile = "";
     truncate = false;
     fullyQuoteSQL = true;
-
-    // MonetDB safe defaults.
     fieldSeparator = "|";
     fieldEnclosure = "\"";
     nullRepresentation = "";
     encoding = "UTF-8";
-    allocate(0);
-  }
-
-  /**
-   * @deprecated
-   * @return the XML to store the transform
-   */
-  @Override
-  @Deprecated(since = "2.14")
-  public String getXml() {
-    StringBuilder retval = new StringBuilder(300);
-
-    // General Settings Tab
-    retval.append("    ").append(XmlHandler.addTagValue("connection", 
dbConnectionName));
-    retval.append("    ").append(XmlHandler.addTagValue("buffer_size", 
bufferSize));
-    retval.append("    ").append(XmlHandler.addTagValue("schema", schemaName));
-    retval.append("    ").append(XmlHandler.addTagValue("table", tableName));
-    retval.append("    ").append(XmlHandler.addTagValue("log_file", logFile));
-    retval.append("    ").append(XmlHandler.addTagValue("truncate", truncate));
-    retval.append("    ").append(XmlHandler.addTagValue("fully_quote_sql", 
fullyQuoteSQL));
-
-    // MonetDB Settings Tab
-    retval.append("    ").append(XmlHandler.addTagValue("field_separator", 
fieldSeparator));
-    retval.append("    ").append(XmlHandler.addTagValue("field_enclosure", 
fieldEnclosure));
-    retval.append("    ").append(XmlHandler.addTagValue("null_representation", 
nullRepresentation));
-    retval.append("    ").append(XmlHandler.addTagValue("encoding", encoding));
-
-    // Output Fields Tab
-    for (int i = 0; i < fieldTable.length; i++) {
-      Boolean fieldFormat = false;
-      if (fieldFormatOk.length == fieldTable.length) {
-        fieldFormat = fieldFormatOk[i];
-      }
-      retval.append("      <mapping>").append(Const.CR);
-      retval.append(CONST_SPACES).append(XmlHandler.addTagValue("stream_name", 
fieldTable[i]));
-      retval.append(CONST_SPACES).append(XmlHandler.addTagValue("field_name", 
fieldStream[i]));
-      
retval.append(CONST_SPACES).append(XmlHandler.addTagValue("field_format_ok", 
fieldFormat));
-      retval.append("      </mapping>").append(Const.CR);
-    }
-
-    return retval.toString();
   }
 
   @Override
@@ -409,6 +202,25 @@ public class MonetDbBulkLoaderMeta
     CheckResult cr;
     StringBuilder erroMessage = new StringBuilder();
 
+    DatabaseMeta databaseMeta = null;
+
+    try {
+      databaseMeta =
+          metadataProvider
+              .getSerializer(DatabaseMeta.class)
+              .load(variables.resolve(dbConnectionName));
+    } catch (HopException e) {
+      cr =
+          new CheckResult(
+              ICheckResult.TYPE_RESULT_ERROR,
+              BaseMessages.getString(
+                  PKG,
+                  "TableInputMeta.CheckResult.DatabaseMetaError",
+                  variables.resolve(dbConnectionName)),
+              transformMeta);
+      remarks.add(cr);
+    }
+
     if (databaseMeta != null) {
       Database db = new Database(loggingObject, variables, databaseMeta);
       try {
@@ -441,8 +253,8 @@ public class MonetDbBulkLoaderMeta
             first = true;
             errorFound = false;
 
-            for (String field : fieldTable) {
-              IValueMeta v = r.searchValueMeta(field);
+            for (MonetDbField field : fields) {
+              IValueMeta v = r.searchValueMeta(field.fieldTable);
               if (v == null) {
                 if (first) {
                   first = false;
@@ -496,8 +308,8 @@ public class MonetDbBulkLoaderMeta
           erroMessage.setLength(0);
           boolean errorFound = false;
 
-          for (String s : fieldStream) {
-            IValueMeta v = prev.searchValueMeta(s);
+          for (MonetDbField s : fields) {
+            IValueMeta v = prev.searchValueMeta(s.fieldStream);
             if (v == null) {
               if (first) {
                 first = false;
@@ -608,21 +420,16 @@ public class MonetDbBulkLoaderMeta
   public IRowMeta updateFields(IRowMeta prev, MonetDbBulkLoaderData data) {
     // update the field table from the fields coming from the previous 
transforms
     IRowMeta tableFields = new RowMeta();
-    List<IValueMeta> fields = prev.getValueMetaList();
-    fieldTable = new String[fields.size()];
-    fieldStream = new String[fields.size()];
-    fieldFormatOk = new boolean[fields.size()];
+    List<IValueMeta> prevFields = prev.getValueMetaList();
     int idx = 0;
-    for (IValueMeta field : fields) {
+    for (IValueMeta field : prevFields) {
       IValueMeta tableField = field.clone();
       tableFields.addValueMeta(tableField);
-      fieldTable[idx] = field.getName();
-      fieldStream[idx] = field.getName();
-      fieldFormatOk[idx] = true;
+      fields.add(new MonetDbField(field.getName(), field.getName(), true));
       idx++;
     }
 
-    data.keynrs = new int[getFieldStream().length];
+    data.keynrs = new int[fields.size()];
     for (int i = 0; i < data.keynrs.length; i++) {
       data.keynrs[i] = i;
     }
@@ -636,6 +443,9 @@ public class MonetDbBulkLoaderMeta
       boolean autoSchema,
       MonetDbBulkLoaderData data,
       boolean safeMode) {
+
+    DatabaseMeta databaseMeta =
+        
getParentTransformMeta().getParentPipelineMeta().findDatabase(dbConnectionName, 
variables);
     SqlStatement retval =
         new SqlStatement(transformMeta.getName(), databaseMeta, null); // 
default: nothing to do!
 
@@ -649,11 +459,11 @@ public class MonetDbBulkLoaderMeta
         } else {
           tableFields = new RowMeta();
           // Now change the field names
-          for (int i = 0; i < fieldTable.length; i++) {
-            IValueMeta v = prev.searchValueMeta(fieldStream[i]);
+          for (int i = 0; i < fields.size(); i++) {
+            IValueMeta v = prev.searchValueMeta(fields.get(i).fieldStream);
             if (v != null) {
               IValueMeta tableField = v.clone();
-              tableField.setName(fieldTable[i]);
+              tableField.setName(fields.get(i).fieldTable);
               tableFields.addValueMeta(tableField);
             }
           }
@@ -712,11 +522,14 @@ public class MonetDbBulkLoaderMeta
       IRowMeta info,
       IHopMetadataProvider metadataProvider)
       throws HopTransformException {
+    DatabaseMeta databaseMeta =
+        
getParentTransformMeta().getParentPipelineMeta().findDatabase(dbConnectionName, 
variables);
+
     if (prev != null) {
       /* DEBUG CHECK THIS */
       // Insert dateMask fields : read/write
-      for (int i = 0; i < fieldTable.length; i++) {
-        IValueMeta v = prev.searchValueMeta(fieldStream[i]);
+      for (MonetDbField field : fields) {
+        IValueMeta v = prev.searchValueMeta(field.fieldStream);
 
         DatabaseImpact ii =
             new DatabaseImpact(
@@ -725,8 +538,8 @@ public class MonetDbBulkLoaderMeta
                 transformMeta.getName(),
                 databaseMeta.getDatabaseName(),
                 variables.resolve(tableName),
-                fieldTable[i],
-                fieldStream[i],
+                field.fieldTable,
+                field.fieldStream,
                 v != null ? v.getOrigin() : "?",
                 "",
                 "Type = " + v.toStringMeta());
@@ -740,6 +553,9 @@ public class MonetDbBulkLoaderMeta
     String realTableName = variables.resolve(tableName);
     String realSchemaName = variables.resolve(schemaName);
 
+    DatabaseMeta databaseMeta =
+        
getParentTransformMeta().getParentPipelineMeta().findDatabase(dbConnectionName, 
variables);
+
     if (databaseMeta != null) {
       Database db = new Database(loggingObject, variables, databaseMeta);
       try {
@@ -773,102 +589,6 @@ public class MonetDbBulkLoaderMeta
     }
   }
 
-  /**
-   * @return the schemaName
-   */
-  public String getSchemaName() {
-    return schemaName;
-  }
-
-  /**
-   * @param schemaName the schemaName to set
-   */
-  public void setSchemaName(String schemaName) {
-    this.schemaName = schemaName;
-  }
-
-  public String getLogFile() {
-    return logFile;
-  }
-
-  public void setLogFile(String logFile) {
-    this.logFile = logFile;
-  }
-
-  public String getFieldSeparator() {
-    return fieldSeparator;
-  }
-
-  public void setFieldSeparator(String fieldSeparatorStr) {
-    this.fieldSeparator = fieldSeparatorStr;
-  }
-
-  public String getFieldEnclosure() {
-    return fieldEnclosure;
-  }
-
-  public void setFieldEnclosure(String fieldEnclosureStr) {
-    this.fieldEnclosure = fieldEnclosureStr;
-  }
-
-  public String getNullRepresentation() {
-    return nullRepresentation;
-  }
-
-  public void setNullRepresentation(String nullRepresentationString) {
-    this.nullRepresentation = nullRepresentationString;
-  }
-
-  public String getEncoding() {
-    return encoding;
-  }
-
-  public void setEncoding(String encoding) {
-    this.encoding = encoding;
-  }
-
-  /**
-   * @return the bufferSize
-   */
-  public String getBufferSize() {
-    return bufferSize;
-  }
-
-  /**
-   * @param bufferSize the bufferSize to set
-   */
-  public void setBufferSize(String bufferSize) {
-    this.bufferSize = bufferSize;
-  }
-
-  /**
-   * @return the fieldFormatOk
-   */
-  public boolean[] getFieldFormatOk() {
-    return fieldFormatOk;
-  }
-
-  /**
-   * @param fieldFormatOk the fieldFormatOk to set
-   */
-  public void setFieldFormatOk(boolean[] fieldFormatOk) {
-    this.fieldFormatOk = fieldFormatOk;
-  }
-
-  /**
-   * @param dbConnectionName connection name to set
-   */
-  public void setDbConnectionName(String dbConnectionName) {
-    this.dbConnectionName = dbConnectionName;
-  }
-
-  /**
-   * @return the database connection name
-   */
-  public String getDbConnectionName() {
-    return this.dbConnectionName;
-  }
-
   /**
    * Returns the version of MonetDB that is used.
    *
@@ -876,7 +596,10 @@ public class MonetDbBulkLoaderMeta
    * @throws HopException if an error occurs
    */
   private MonetDbVersion getMonetDBVersion(IVariables variables) throws 
HopException {
-    Database db = null;
+    Database db;
+
+    DatabaseMeta databaseMeta =
+        
getParentTransformMeta().getParentPipelineMeta().findDatabase(dbConnectionName, 
variables);
 
     db = new Database(loggingObject, variables, databaseMeta);
     try {
diff --git 
a/plugins/transforms/monetdbbulkloader/src/test/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbBulkLoaderMetaTest.java
 
b/plugins/transforms/monetdbbulkloader/src/test/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbBulkLoaderMetaTest.java
new file mode 100644
index 0000000000..f36c118bd6
--- /dev/null
+++ 
b/plugins/transforms/monetdbbulkloader/src/test/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbBulkLoaderMetaTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.monetdbbulkloader;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.database.DatabaseMeta;
+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.variables.Variables;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+class MonetDbBulkLoaderMetaTest {
+
+  @BeforeEach
+  void setUp() throws Exception {
+    HopEnvironment.init();
+    PluginRegistry.init();
+  }
+
+  @Test
+  void testSerialization() throws Exception {
+    IHopMetadataProvider metadataProvider = new MemoryMetadataProvider();
+    DatabaseMeta dbMeta = new DatabaseMeta();
+    dbMeta.setName("unit-test-db");
+    metadataProvider.getSerializer(DatabaseMeta.class).save(dbMeta);
+
+    Document document =
+        XmlHandler.loadXmlFile(
+            
MonetDbBulkLoaderMeta.class.getResourceAsStream("/monetdbbulkloader-transform.xml"));
+    Node node = XmlHandler.getSubNode(document, TransformMeta.XML_TAG);
+
+    MonetDbBulkLoaderMeta meta = new MonetDbBulkLoaderMeta();
+    meta.loadXml(node, metadataProvider);
+
+    String xml =
+        XmlHandler.openTag(TransformMeta.XML_TAG)
+            + meta.getXml()
+            + XmlHandler.closeTag(TransformMeta.XML_TAG);
+
+    Document copyDocument = XmlHandler.loadXmlString(xml);
+    Node copyNode = XmlHandler.getSubNode(copyDocument, TransformMeta.XML_TAG);
+    MonetDbBulkLoaderMeta copy = new MonetDbBulkLoaderMeta();
+    copy.loadXml(copyNode, metadataProvider);
+
+    assertEquals(meta.getXml(), copy.getXml());
+
+    assertEquals("unit-test-db", meta.getDbConnectionName());
+    assertEquals("myschema", meta.getSchemaName());
+    assertEquals("mytable", meta.getTableName());
+    assertEquals("100000", meta.getBufferSize());
+    assertEquals("/tmp/monetdb.log", meta.getLogFile());
+    assertTrue(meta.isTruncate());
+    assertTrue(meta.isFullyQuoteSQL());
+    assertEquals("|", meta.getFieldSeparator());
+    assertEquals("\"", meta.getFieldEnclosure());
+    assertEquals("null", meta.getNullRepresentation());
+    assertEquals("UTF-8", meta.getEncoding());
+
+    // Assert field mappings list loaded correctly from XML
+    assertNotNull(meta.getFields());
+    assertEquals(2, meta.getFields().size(), "Expected 2 mapping elements");
+
+    MonetDbBulkLoaderMeta.MonetDbField first = meta.getFields().get(0);
+    assertEquals("col1", first.getFieldTable(), "first mapping stream_name -> 
fieldTable");
+    assertEquals("field1", first.getFieldStream(), "first mapping field_name 
-> fieldStream");
+    assertTrue(first.isFieldFormatOk(), "first mapping field_format_ok Y -> 
true");
+
+    MonetDbBulkLoaderMeta.MonetDbField second = meta.getFields().get(1);
+    assertEquals("col2", second.getFieldTable(), "second mapping stream_name 
-> fieldTable");
+    assertEquals("field2", second.getFieldStream(), "second mapping field_name 
-> fieldStream");
+    assertFalse(second.isFieldFormatOk(), "second mapping field_format_ok N -> 
false");
+
+    // Round-trip: copy should have same field mappings
+    assertNotNull(copy.getFields());
+    assertEquals(meta.getFields().size(), copy.getFields().size());
+    for (int i = 0; i < meta.getFields().size(); i++) {
+      MonetDbBulkLoaderMeta.MonetDbField orig = meta.getFields().get(i);
+      MonetDbBulkLoaderMeta.MonetDbField copied = copy.getFields().get(i);
+      assertEquals(orig.getFieldTable(), copied.getFieldTable(), "field " + i 
+ " table");
+      assertEquals(orig.getFieldStream(), copied.getFieldStream(), "field " + 
i + " stream");
+      assertEquals(orig.isFieldFormatOk(), copied.isFieldFormatOk(), "field " 
+ i + " formatOk");
+    }
+  }
+
+  @Test
+  void testSetDefault() {
+    MonetDbBulkLoaderMeta meta = new MonetDbBulkLoaderMeta();
+    meta.setDefault();
+
+    assertEquals("", meta.getSchemaName());
+    assertNotNull(meta.getTableName());
+    assertEquals("100000", meta.getBufferSize());
+    assertEquals("", meta.getLogFile());
+    assertTrue(meta.isFullyQuoteSQL());
+    assertEquals("|", meta.getFieldSeparator());
+    assertEquals("\"", meta.getFieldEnclosure());
+    assertEquals("", meta.getNullRepresentation());
+    assertEquals("UTF-8", meta.getEncoding());
+  }
+
+  @Test
+  void testClone() {
+    MonetDbBulkLoaderMeta meta = new MonetDbBulkLoaderMeta();
+    meta.setSchemaName("s");
+    meta.setTableName("t");
+
+    MonetDbBulkLoaderMeta clone = (MonetDbBulkLoaderMeta) meta.clone();
+
+    assertNotNull(clone);
+    assertEquals(meta.getSchemaName(), clone.getSchemaName());
+    assertEquals(meta.getTableName(), clone.getTableName());
+  }
+
+  @Test
+  void testGetFieldsDoesNotModifyInputRowMeta() throws Exception {
+    MonetDbBulkLoaderMeta meta = new MonetDbBulkLoaderMeta();
+    IRowMeta inputRowMeta = new RowMeta();
+    int originalSize = inputRowMeta.size();
+
+    meta.getFields(
+        inputRowMeta, "origin", null, null, new Variables(), 
(IHopMetadataProvider) null);
+
+    assertEquals(originalSize, inputRowMeta.size());
+  }
+}
diff --git 
a/plugins/transforms/monetdbbulkloader/src/test/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbBulkLoaderTest.java
 
b/plugins/transforms/monetdbbulkloader/src/test/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbBulkLoaderTest.java
new file mode 100644
index 0000000000..a49fb217b6
--- /dev/null
+++ 
b/plugins/transforms/monetdbbulkloader/src/test/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbBulkLoaderTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.monetdbbulkloader;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.plugins.PluginRegistry;
+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;
+
+/** Test for MonetDbBulkLoader (excluding dialog). */
+class MonetDbBulkLoaderTest {
+
+  @BeforeEach
+  void setUp() throws Exception {
+    HopEnvironment.init();
+    PluginRegistry.init();
+  }
+
+  @Test
+  void testInitFailsWhenNoConnection() {
+    PipelineMeta pipelineMeta = new PipelineMeta();
+    TransformMeta transformMeta = new TransformMeta("test", new 
MonetDbBulkLoaderMeta());
+    pipelineMeta.addTransform(transformMeta);
+
+    MonetDbBulkLoaderMeta meta = new MonetDbBulkLoaderMeta();
+    MonetDbBulkLoaderData data = new MonetDbBulkLoaderData();
+    Pipeline pipeline = new LocalPipelineEngine(pipelineMeta);
+
+    MonetDbBulkLoader transform =
+        new MonetDbBulkLoader(transformMeta, meta, data, 0, pipelineMeta, 
pipeline);
+
+    assertFalse(transform.init());
+  }
+
+  @Test
+  void testEscapeOsPathSpacesOnUnix() {
+    PipelineMeta pipelineMeta = new PipelineMeta();
+    TransformMeta transformMeta = new TransformMeta("test", new 
MonetDbBulkLoaderMeta());
+    pipelineMeta.addTransform(transformMeta);
+
+    MonetDbBulkLoaderMeta meta = new MonetDbBulkLoaderMeta();
+    MonetDbBulkLoaderData data = new MonetDbBulkLoaderData();
+    Pipeline pipeline = new LocalPipelineEngine(pipelineMeta);
+
+    MonetDbBulkLoader transform =
+        new MonetDbBulkLoader(transformMeta, meta, data, 0, pipelineMeta, 
pipeline);
+
+    String result = transform.escapeOsPath("/path with spaces/file", false);
+    assertEquals("/path\\ with\\ spaces/file", result);
+  }
+
+  @Test
+  void testEscapeOsPathSpacesOnWindows() {
+    PipelineMeta pipelineMeta = new PipelineMeta();
+    TransformMeta transformMeta = new TransformMeta("test", new 
MonetDbBulkLoaderMeta());
+    pipelineMeta.addTransform(transformMeta);
+
+    MonetDbBulkLoaderMeta meta = new MonetDbBulkLoaderMeta();
+    MonetDbBulkLoaderData data = new MonetDbBulkLoaderData();
+    Pipeline pipeline = new LocalPipelineEngine(pipelineMeta);
+
+    MonetDbBulkLoader transform =
+        new MonetDbBulkLoader(transformMeta, meta, data, 0, pipelineMeta, 
pipeline);
+
+    String result = transform.escapeOsPath("C:\\path with spaces\\file", true);
+    assertEquals("C:\\path^ with^ spaces\\file", result);
+  }
+
+  @Test
+  void testEscapeOsPathNoSpaces() {
+    PipelineMeta pipelineMeta = new PipelineMeta();
+    TransformMeta transformMeta = new TransformMeta("test", new 
MonetDbBulkLoaderMeta());
+    pipelineMeta.addTransform(transformMeta);
+
+    MonetDbBulkLoaderMeta meta = new MonetDbBulkLoaderMeta();
+    MonetDbBulkLoaderData data = new MonetDbBulkLoaderData();
+    Pipeline pipeline = new LocalPipelineEngine(pipelineMeta);
+
+    MonetDbBulkLoader transform =
+        new MonetDbBulkLoader(transformMeta, meta, data, 0, pipelineMeta, 
pipeline);
+
+    String result = transform.escapeOsPath("/path/nospaces", false);
+    assertEquals("/path/nospaces", result);
+  }
+}
diff --git 
a/plugins/transforms/monetdbbulkloader/src/test/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbVersionTest.java
 
b/plugins/transforms/monetdbbulkloader/src/test/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbVersionTest.java
new file mode 100644
index 0000000000..b112b58a71
--- /dev/null
+++ 
b/plugins/transforms/monetdbbulkloader/src/test/java/org/apache/hop/pipeline/transforms/monetdbbulkloader/MonetDbVersionTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.monetdbbulkloader;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class MonetDbVersionTest {
+
+  @Test
+  void testConstructorWithInts() {
+    MonetDbVersion v = new MonetDbVersion(11, 17, 17);
+    assertEquals(11, v.getMajorVersion());
+    assertEquals(17, v.getMinorVersion());
+    assertEquals(17, v.getPatchVersion());
+  }
+
+  @Test
+  void testParseFullVersion() throws MonetDbVersionException {
+    MonetDbVersion v = new MonetDbVersion("11.17.17");
+    assertEquals(11, v.getMajorVersion());
+    assertEquals(17, v.getMinorVersion());
+    assertEquals(17, v.getPatchVersion());
+  }
+
+  @Test
+  void testParseMajorMinorOnly() throws MonetDbVersionException {
+    MonetDbVersion v = new MonetDbVersion("11.0");
+    assertEquals(11, v.getMajorVersion());
+    assertEquals(0, v.getMinorVersion());
+    assertNull(v.getPatchVersion());
+  }
+
+  @Test
+  void testParseWithExtraParts() throws MonetDbVersionException {
+    MonetDbVersion v = new MonetDbVersion("11.5.17.1");
+    assertEquals(11, v.getMajorVersion());
+    assertEquals(5, v.getMinorVersion());
+    assertEquals(17, v.getPatchVersion());
+  }
+
+  @Test
+  void testParseThrowsWhenNull() {
+    MonetDbVersionException e =
+        assertThrows(MonetDbVersionException.class, () -> new 
MonetDbVersion(null));
+    assertTrue(e.getMessage() != null && e.getMessage().contains("null"));
+  }
+
+  @Test
+  void testParseThrowsWhenInvalidFormat() {
+    assertThrows(MonetDbVersionException.class, () -> new MonetDbVersion(""));
+    assertThrows(MonetDbVersionException.class, () -> new 
MonetDbVersion("abc"));
+    assertThrows(MonetDbVersionException.class, () -> new 
MonetDbVersion("11.17.a"));
+    assertThrows(MonetDbVersionException.class, () -> new 
MonetDbVersion("11-17-17"));
+  }
+
+  @Test
+  void testCompareTo() throws MonetDbVersionException {
+    MonetDbVersion v111717 = new MonetDbVersion("11.17.17");
+    MonetDbVersion v111717b = new MonetDbVersion("11.17.17");
+    MonetDbVersion v111800 = new MonetDbVersion("11.18.0");
+    MonetDbVersion v120000 = new MonetDbVersion("12.0.0");
+
+    assertEquals(0, v111717.compareTo(v111717b));
+    assertTrue(v111717.compareTo(v111800) < 0);
+    assertTrue(v111800.compareTo(v111717) > 0);
+    assertTrue(v111717.compareTo(v120000) < 0);
+    assertTrue(v120000.compareTo(v111717) > 0);
+  }
+
+  @Test
+  void testCompareToJan2014Sp2() throws MonetDbVersionException {
+    MonetDbVersion older = new MonetDbVersion("11.17.16");
+    MonetDbVersion same = new MonetDbVersion("11.17.17");
+    MonetDbVersion newer = new MonetDbVersion("11.17.18");
+
+    assertTrue(older.compareTo(MonetDbVersion.JAN_2014_SP2_DB_VERSION) < 0);
+    assertEquals(0, same.compareTo(MonetDbVersion.JAN_2014_SP2_DB_VERSION));
+    assertTrue(newer.compareTo(MonetDbVersion.JAN_2014_SP2_DB_VERSION) > 0);
+  }
+
+  @Test
+  void testToString() throws MonetDbVersionException {
+    MonetDbVersion v = new MonetDbVersion("11.17.17");
+    String s = v.toString();
+    assertTrue(s.contains("11"));
+    assertTrue(s.contains("17"));
+  }
+}
diff --git 
a/plugins/transforms/monetdbbulkloader/src/test/resources/monetdbbulkloader-transform.xml
 
b/plugins/transforms/monetdbbulkloader/src/test/resources/monetdbbulkloader-transform.xml
new file mode 100644
index 0000000000..3ca8117e2a
--- /dev/null
+++ 
b/plugins/transforms/monetdbbulkloader/src/test/resources/monetdbbulkloader-transform.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  ~
+  -->
+
+<transform>
+    <name>MonetDB Bulk Loader</name>
+    <type>MonetDBBulkLoader</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+        <method>none</method>
+        <schema_name/>
+    </partitioning>
+    <connection>unit-test-db</connection>
+    <buffer_size>100000</buffer_size>
+    <schema>myschema</schema>
+    <table>mytable</table>
+    <log_file>/tmp/monetdb.log</log_file>
+    <truncate>Y</truncate>
+    <fully_quote_sql>Y</fully_quote_sql>
+    <field_separator>|</field_separator>
+    <field_enclosure>"</field_enclosure>
+    <null_representation>null</null_representation>
+    <encoding>UTF-8</encoding>
+    <mapping>
+        <stream_name>col1</stream_name>
+        <field_name>field1</field_name>
+        <field_format_ok>Y</field_format_ok>
+    </mapping>
+    <mapping>
+        <stream_name>col2</stream_name>
+        <field_name>field2</field_name>
+        <field_format_ok>N</field_format_ok>
+    </mapping>
+    <attributes/>
+    <GUI>
+        <xloc>368</xloc>
+        <yloc>96</yloc>
+    </GUI>
+</transform>

Reply via email to