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 2e7dfb0759 Issue #6078 (Repeat Fields) (#6139)
2e7dfb0759 is described below

commit 2e7dfb0759aca44b2a428a34411bce4404039998
Author: Matt Casters <[email protected]>
AuthorDate: Tue Dec 9 16:02:11 2025 +0100

    Issue #6078 (Repeat Fields) (#6139)
    
    * Issue #6078 (Repeat Fields)
    
    * Issue #6078 (Repeat Fields)
    
    * Issue #6078 (Repeat Fields - test renamed to avoid collision with others)
    
    * Issue #6078 (Repeat Fields - test renamed to avoid collision with others)
    
    * Issue #6078 (Repeat Fields - Typo fix in test)
    
    * rebase + add doc page to nav.adoc
    
    ---------
    
    Co-authored-by: Matt Casters <[email protected]>
    Co-authored-by: Hans Van Akelyen <[email protected]>
---
 .../images/transforms/icons/repeatfields.svg       |   4 +
 docs/hop-user-manual/modules/ROOT/nav.adoc         |   1 +
 .../pages/pipeline/transforms/repeatfields.adoc    | 126 +++++++
 .../0082-repeat-fields-current-when-indicated.hpl  | 333 ++++++++++++++++++
 .../transforms/0082-repeat-fields-mdi-main.hpl     | 319 +++++++++++++++++
 .../transforms/0082-repeat-fields-mdi.hpl          | 327 +++++++++++++++++
 .../0082-repeat-fields-previous-when-null.hpl      | 192 ++++++++++
 .../transforms/0082-repeat-fields-previous.hpl     | 192 ++++++++++
 ...golden-repeat-fields-current-when-indicated.csv |   7 +
 .../datasets/golden-repeat-fields-previous.csv     |  10 +
 .../datasets/golden-repeat-fields-when-null.csv    |  10 +
 .../transforms/main-0082-repeat-fields.hwf         |  89 +++++
 ...olden-repeat-fields-current-when-indicated.json |  72 ++++
 .../dataset/golden-repeat-fields-previous.json     |  48 +++
 .../dataset/golden-repeat-fields-when-null.json    |  48 +++
 ...-repeat-fields-current-when-indicated UNIT.json |  63 ++++
 .../0082-repeat-fields-mdi-main UNIT.json          |  58 ++++
 .../0082-repeat-fields-previous UNIT.json          |  48 +++
 ...0082-repeat-fields-previous-when-null UNIT.json |  48 +++
 plugins/transforms/pom.xml                         |   1 +
 plugins/transforms/repeatfields/pom.xml            |  45 +++
 .../repeatfields/src/assembly/assembly.xml         |  50 +++
 .../pipeline/transforms/repeatfields/Repeat.java   |  53 +++
 .../transforms/repeatfields/RepeatFields.java      | 186 ++++++++++
 .../transforms/repeatfields/RepeatFieldsData.java  |  32 ++
 .../repeatfields/RepeatFieldsDialog.java           | 385 +++++++++++++++++++++
 .../transforms/repeatfields/RepeatFieldsMeta.java  | 157 +++++++++
 .../messages/messages_en_US.properties             |  44 +++
 .../src/main/resources/repeatfields.svg            |   4 +
 .../repeatfields/src/main/resources/version.xml    |  20 ++
 30 files changed, 2972 insertions(+)

diff --git 
a/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/repeatfields.svg
 
b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/repeatfields.svg
new file mode 100644
index 0000000000..daa970dc75
--- /dev/null
+++ 
b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/repeatfields.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg"; width="24" height="24">
+    <path fill="#0e3a5a" d="M11,17H4A2,2 0 0,1 2,15V3A2,2 0 0,1 
4,1H16V3H4V15H11V13L15,16L11,19V17M19,21V7H8V13H6V7A2,2 0 0,1 8,5H19A2,2 0 0,1 
21,7V21A2,2 0 0,1 19,23H8A2,2 0 0,1 6,21V19H8V21H19Z"/>
+</svg>
diff --git a/docs/hop-user-manual/modules/ROOT/nav.adoc 
b/docs/hop-user-manual/modules/ROOT/nav.adoc
index a622235839..b0de982604 100644
--- a/docs/hop-user-manual/modules/ROOT/nav.adoc
+++ b/docs/hop-user-manual/modules/ROOT/nav.adoc
@@ -217,6 +217,7 @@ under the License.
 *** xref:pipeline/transforms/propertyoutput.adoc[Properties file Output]
 *** xref:pipeline/transforms/redshift-bulkloader.adoc[Redshift Bulk Loader]
 *** xref:pipeline/transforms/regexeval.adoc[Regex Evaluation]
+*** xref:pipeline/transforms/repeatfields.adoc[Repeat Fields]
 *** xref:pipeline/transforms/replacestring.adoc[Replace in String]
 *** xref:pipeline/transforms/reservoirsampling.adoc[Reservoir Sampling]
 *** xref:pipeline/transforms/rest.adoc[REST Client]
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/repeatfields.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/repeatfields.adoc
new file mode 100644
index 0000000000..eb7b634890
--- /dev/null
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/repeatfields.adoc
@@ -0,0 +1,126 @@
+////
+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.
+////
+:documentationPath: /pipeline/transforms/
+:language: en_US
+:description: Repeat a field value from a previous row
+
+:openvar: {
+:closevar: }
+
+= image:transforms/icons/repeatfields.svg[Repeat Fields Icon, 
role="image-doc-icon"] Repeat Fields
+
+[%noheader,cols="3a,1a", role="table-no-borders" ]
+|===
+|
+== Description
+
+The Repeat Fields transform is solving a few common problems:
+
+* Repeat a field value from the previous row, if the value is null on the 
current row: this commonly happens when
+reading data from merged cells in a spreadsheet.  In that scenario only the 
first row is filled in for the merged cell
+and we need to repeat the data from the first row of the cell.
+* Take a field value from the current row when an indicator matches, otherwise 
take the value from the previous row.
+This can be used to combine data coming from multiple sources when you have a 
common key to group on.
+
+IMPORTANT: If the incoming data is not sorted on the specified group field(s),
+the output results may not be correct. We recommend sorting the incoming data 
within the pipeline,
+or in a source database.
+
+CAUTION: This transform isn't supported in data processing engines that can't 
guarantee a row order in the given group.
+Even on the native Hop engine you need to make sure to use partitioning on the 
group field(s) to process data in
+parallel.
+
+|
+== Supported Engines
+[%noheader,cols="2,1a",frame=none, role="table-supported-engines"]
+!===
+!Hop Engine! image:check_mark.svg[Supported, 24]
+!Spark! image:cross.svg[Not Supported, 24]
+!Flink! image:cross.svg[Not Supported, 24]
+!Dataflow! image:cross.svg[Not Supported, 24]
+!===
+|===
+
+== Group fields
+
+In the Group fields section you can specify the group in which to operate.
+Every new group starts without a previous row.
+
+== The fields to repeat
+
+[options="header"]
+|===
+|Option|Description
+|Repeat type
+|
+**Previous row**: repeat the field value from the previous row, regardless of 
its current value.
+If there is no previous row, take the value from the current row. (ETL 
Metadata Injection type = `previous`)
+
+**Previous when field is null**: copy the field value from the previous row if 
the current source value is null. (ETL Metadata Injection type = 
`previous_when_null`)
+
+**Current when indicated**: take the value from the current source field if 
the given indicator matches the value in the indicator field. (ETL Metadata 
Injection type = `current_when_indicated`)
+
+|Source field
+|The source field to repeat
+
+|Target field
+|Specify the name of the target field (mandatory)
+
+|Indicator field name
+|When using the "Current when indicated" type, this is the name of the field 
that contains the indicator value.
+
+|Indicator value
+|When using the "Current when indicated" type, this is the indicator value to 
match with.
+
+|===
+
+== Current when indicated
+
+When we're dealing with source data coming from multiple sources, for example 
information about a customer,
+we want to assemble a record containing all the different fields from the 
different sources.
+In the example of our customer, source system A might contain personal data 
like name, first name, birthdate, and so on.
+Another source system B might contain the status of the customer.
+Finally, source system C contains a flag indicating financial status.
+Each of the sources get changed at a different point in time.  In a slowly 
changing dimension (see: 
xref:pipeline/transforms/dimensionlookup.adoc[Dimension Lookup/Update] ) we 
want to see the changes when they occur in the different sources.
+We also want to process the data from the sources as they arrive.
+
+[options="header"]
+|===
+|source|customer_id|timestamp|lastname|firstname|birthdate|status|indicator
+|A|1|2025/01/01 12:00:00|Mouse|Mickey|1928/11/18|null|null
+|B|1|2025/01/01 13:00:00|null|null|null|active|null
+|C|1|2025/01/01 14:00:00|null|null|null|null|positive
+|===
+
+What we want to do with this transform is take the appropriate fields from the 
source data taking into account the
+source field as an indicator. We want to end up with this result:
+
+[options="header"]
+|===
+|source|customer_id|timestamp|lastname|firstname|birthdate|status|indicator
+|A|1|2025/01/01 12:00:00|Mouse|Mickey|1928/11/18|null|null
+|B|1|2025/01/01 13:00:00|Mouse|Mickey|1928/11/18|active|null
+|C|1|2025/01/01 14:00:00|Mouse|Mickey|1928/11/18|active|positive
+|===
+
+These 3 rows can then be used to create a detailed and correct timeline using 
the xref:pipeline/transforms/dimensionlookup.adoc[Dimension Lookup/Update] 
transform.
+
+TIP: Add the last record from your target slowly changing dimension to modify 
existing records.
+
+IMPORTANT: As mentioned above, but worth repeating, make sure your data is 
sorted on the group key
+(the customer ID in this example) and **ALSO** on the timestamp to get a 
correct timeline result.
+
diff --git 
a/integration-tests/transforms/0082-repeat-fields-current-when-indicated.hpl 
b/integration-tests/transforms/0082-repeat-fields-current-when-indicated.hpl
new file mode 100644
index 0000000000..1444cacb99
--- /dev/null
+++ b/integration-tests/transforms/0082-repeat-fields-current-when-indicated.hpl
@@ -0,0 +1,333 @@
+<?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.
+
+-->
+<pipeline>
+  <info>
+    <name>0082-repeat-fields-current-when-indicated</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2025/12/07 14:20:41.310</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2025/12/07 14:20:41.310</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>Data grid</from>
+      <to>Repeat Fields</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Repeat Fields</from>
+      <to>grab results</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>grab results</from>
+      <to>result</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Data grid</name>
+    <type>DataGrid</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <data>
+      <line>
+        <item>A</item>
+        <item>1</item>
+        <item>2025/01/01 12:00:00</item>
+        <item>Mouse</item>
+        <item>Mickey</item>
+        <item>1928/11/18</item>
+        <item/>
+        <item/>
+      </line>
+      <line>
+        <item>B</item>
+        <item>1</item>
+        <item>2025/01/01 13:00:00</item>
+        <item/>
+        <item/>
+        <item/>
+        <item>active</item>
+        <item/>
+      </line>
+      <line>
+        <item>C</item>
+        <item>1</item>
+        <item>2025/01/01 14:00:00</item>
+        <item/>
+        <item/>
+        <item/>
+        <item/>
+        <item>positive</item>
+      </line>
+      <line>
+        <item>A</item>
+        <item>2</item>
+        <item>2025/01/01 08:00:00</item>
+        <item>Duck</item>
+        <item>Daffy</item>
+        <item>1937/04/17</item>
+        <item/>
+        <item/>
+      </line>
+      <line>
+        <item>B</item>
+        <item>2</item>
+        <item>2025/01/01 09:00:00</item>
+        <item/>
+        <item/>
+        <item/>
+        <item>inactive</item>
+        <item/>
+      </line>
+      <line>
+        <item>C</item>
+        <item>2</item>
+        <item>2025/01/01 10:00:00</item>
+        <item/>
+        <item/>
+        <item/>
+        <item/>
+        <item>negative</item>
+      </line>
+    </data>
+    <fields>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>source</name>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>id</name>
+        <type>Integer</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>timestamp</name>
+        <format>yyyy/MM/dd HH:mm:ss</format>
+        <type>Date</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>lastname</name>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>firstname</name>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>birthdate</name>
+        <format>yyyy/MM/dd</format>
+        <type>Date</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>status</name>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>indicator</name>
+        <type>String</type>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Repeat Fields</name>
+    <type>RepeatFields</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <group_fields>
+      <group_field>id</group_field>
+    </group_fields>
+    <repeats>
+      <field>
+        <indicator_field_name>source</indicator_field_name>
+        <indicator_value>A</indicator_value>
+        <source_field>lastname</source_field>
+        <target_field>rLast</target_field>
+        <type>current_when_indicated</type>
+      </field>
+      <field>
+        <indicator_field_name>source</indicator_field_name>
+        <indicator_value>A</indicator_value>
+        <source_field>firstname</source_field>
+        <target_field>rFirst</target_field>
+        <type>current_when_indicated</type>
+      </field>
+      <field>
+        <indicator_field_name>source</indicator_field_name>
+        <indicator_value>A</indicator_value>
+        <source_field>birthdate</source_field>
+        <target_field>rBirthdate</target_field>
+        <type>current_when_indicated</type>
+      </field>
+      <field>
+        <indicator_field_name>source</indicator_field_name>
+        <indicator_value>B</indicator_value>
+        <source_field>status</source_field>
+        <target_field>rStatus</target_field>
+        <type>current_when_indicated</type>
+      </field>
+      <field>
+        <indicator_field_name>source</indicator_field_name>
+        <indicator_value>C</indicator_value>
+        <source_field>indicator</source_field>
+        <target_field>rIndicator</target_field>
+        <type>current_when_indicated</type>
+      </field>
+    </repeats>
+    <attributes/>
+    <GUI>
+      <xloc>208</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>grab results</name>
+    <type>SelectValues</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <length>-2</length>
+        <name>source</name>
+        <precision>-2</precision>
+      </field>
+      <field>
+        <length>-2</length>
+        <name>id</name>
+        <precision>-2</precision>
+      </field>
+      <field>
+        <length>-2</length>
+        <name>timestamp</name>
+        <precision>-2</precision>
+      </field>
+      <field>
+        <length>-2</length>
+        <name>rLast</name>
+        <precision>-2</precision>
+      </field>
+      <field>
+        <length>-2</length>
+        <name>rFirst</name>
+        <precision>-2</precision>
+      </field>
+      <field>
+        <length>-2</length>
+        <name>rBirthdate</name>
+        <precision>-2</precision>
+      </field>
+      <field>
+        <length>-2</length>
+        <name>rStatus</name>
+        <precision>-2</precision>
+      </field>
+      <field>
+        <length>-2</length>
+        <name>rIndicator</name>
+        <precision>-2</precision>
+      </field>
+      <select_unspecified>N</select_unspecified>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>320</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>result</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>432</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/transforms/0082-repeat-fields-mdi-main.hpl 
b/integration-tests/transforms/0082-repeat-fields-mdi-main.hpl
new file mode 100644
index 0000000000..72ab0ecddb
--- /dev/null
+++ b/integration-tests/transforms/0082-repeat-fields-mdi-main.hpl
@@ -0,0 +1,319 @@
+<?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.
+
+-->
+<pipeline>
+  <info>
+    <name>0082-repeat-fields-mdi-main</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2025/12/07 14:55:57.333</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2025/12/07 14:55:57.333</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>groups</from>
+      <to>ETL metadata injection</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>repeats</from>
+      <to>ETL metadata injection</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>ETL metadata injection</from>
+      <to>Results</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>ETL metadata injection</name>
+    <type>MetaInject</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <filename>${PROJECT_HOME}/0082-repeat-fields-mdi.hpl</filename>
+    <run_configuration>local</run_configuration>
+    <source_transform>result</source_transform>
+    <source_output_fields>
+      <source_output_field>
+        <source_output_field_name>source</source_output_field_name>
+        <source_output_field_type>String</source_output_field_type>
+        <source_output_field_length>-1</source_output_field_length>
+        <source_output_field_precision>-1</source_output_field_precision>
+      </source_output_field>
+      <source_output_field>
+        <source_output_field_name>id</source_output_field_name>
+        <source_output_field_type>Integer</source_output_field_type>
+        <source_output_field_length>-1</source_output_field_length>
+        <source_output_field_precision>-1</source_output_field_precision>
+      </source_output_field>
+      <source_output_field>
+        <source_output_field_name>timestamp</source_output_field_name>
+        <source_output_field_type>Date</source_output_field_type>
+        <source_output_field_length>-1</source_output_field_length>
+        <source_output_field_precision>-1</source_output_field_precision>
+      </source_output_field>
+      <source_output_field>
+        <source_output_field_name>rLast</source_output_field_name>
+        <source_output_field_type>String</source_output_field_type>
+        <source_output_field_length>-1</source_output_field_length>
+        <source_output_field_precision>-1</source_output_field_precision>
+      </source_output_field>
+      <source_output_field>
+        <source_output_field_name>rFirst</source_output_field_name>
+        <source_output_field_type>String</source_output_field_type>
+        <source_output_field_length>-1</source_output_field_length>
+        <source_output_field_precision>-1</source_output_field_precision>
+      </source_output_field>
+      <source_output_field>
+        <source_output_field_name>rBirthdate</source_output_field_name>
+        <source_output_field_type>Date</source_output_field_type>
+        <source_output_field_length>-1</source_output_field_length>
+        <source_output_field_precision>-1</source_output_field_precision>
+      </source_output_field>
+      <source_output_field>
+        <source_output_field_name>rStatus</source_output_field_name>
+        <source_output_field_type>String</source_output_field_type>
+        <source_output_field_length>-1</source_output_field_length>
+        <source_output_field_precision>-1</source_output_field_precision>
+      </source_output_field>
+      <source_output_field>
+        <source_output_field_name>rIndicator</source_output_field_name>
+        <source_output_field_type>String</source_output_field_type>
+        <source_output_field_length>-1</source_output_field_length>
+        <source_output_field_precision>-1</source_output_field_precision>
+      </source_output_field>
+    </source_output_fields>
+    <target_file/>
+    <create_parent_folder>Y</create_parent_folder>
+    <no_execution>N</no_execution>
+    <allow_empty_stream_on_execution>N</allow_empty_stream_on_execution>
+    <stream_source_transform/>
+    <stream_target_transform/>
+    <mappings>
+      <mapping>
+        <target_transform_name>Repeat Fields</target_transform_name>
+        <target_attribute_key>source_field</target_attribute_key>
+        <target_detail>Y</target_detail>
+        <source_transform>repeats</source_transform>
+        <source_field>source</source_field>
+      </mapping>
+      <mapping>
+        <target_transform_name>Repeat Fields</target_transform_name>
+        <target_attribute_key>indicator_field_name</target_attribute_key>
+        <target_detail>Y</target_detail>
+        <source_transform>repeats</source_transform>
+        <source_field>indicatorField</source_field>
+      </mapping>
+      <mapping>
+        <target_transform_name>Repeat Fields</target_transform_name>
+        <target_attribute_key>GROUP_FIELD</target_attribute_key>
+        <target_detail>Y</target_detail>
+        <source_transform>groups</source_transform>
+        <source_field>group</source_field>
+      </mapping>
+      <mapping>
+        <target_transform_name>Repeat Fields</target_transform_name>
+        <target_attribute_key>target_field</target_attribute_key>
+        <target_detail>Y</target_detail>
+        <source_transform>repeats</source_transform>
+        <source_field>target</source_field>
+      </mapping>
+      <mapping>
+        <target_transform_name>Repeat Fields</target_transform_name>
+        <target_attribute_key>type</target_attribute_key>
+        <target_detail>Y</target_detail>
+        <source_transform>repeats</source_transform>
+        <source_field>type</source_field>
+      </mapping>
+      <mapping>
+        <target_transform_name>Repeat Fields</target_transform_name>
+        <target_attribute_key>indicator_value</target_attribute_key>
+        <target_detail>Y</target_detail>
+        <source_transform>repeats</source_transform>
+        <source_field>indicatorValue</source_field>
+      </mapping>
+    </mappings>
+    <attributes/>
+    <GUI>
+      <xloc>240</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Results</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>400</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>groups</name>
+    <type>DataGrid</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <data>
+      <line>
+        <item>id</item>
+      </line>
+    </data>
+    <fields>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>group</name>
+        <type>String</type>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>repeats</name>
+    <type>DataGrid</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <data>
+      <line>
+        <item>current_when_indicated</item>
+        <item>lastname</item>
+        <item>rLast</item>
+        <item>source</item>
+        <item>A</item>
+      </line>
+      <line>
+        <item>current_when_indicated</item>
+        <item>firstname</item>
+        <item>rFirst</item>
+        <item>source</item>
+        <item>A</item>
+      </line>
+      <line>
+        <item>current_when_indicated</item>
+        <item>birthdate</item>
+        <item>rBirthdate</item>
+        <item>source</item>
+        <item>A</item>
+      </line>
+      <line>
+        <item>current_when_indicated</item>
+        <item>status</item>
+        <item>rStatus</item>
+        <item>source</item>
+        <item>B</item>
+      </line>
+      <line>
+        <item>current_when_indicated</item>
+        <item>indicator</item>
+        <item>rIndicator</item>
+        <item>source</item>
+        <item>C</item>
+      </line>
+    </data>
+    <fields>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>type</name>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>source</name>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>target</name>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>indicatorField</name>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>indicatorValue</name>
+        <type>String</type>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>208</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/transforms/0082-repeat-fields-mdi.hpl 
b/integration-tests/transforms/0082-repeat-fields-mdi.hpl
new file mode 100644
index 0000000000..d54495453c
--- /dev/null
+++ b/integration-tests/transforms/0082-repeat-fields-mdi.hpl
@@ -0,0 +1,327 @@
+<?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.
+
+-->
+<pipeline>
+  <info>
+    <name>0082-repeat-fields-mdi</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2025/12/07 14:20:41.310</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2025/12/07 14:20:41.310</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>Data grid</from>
+      <to>Repeat Fields</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Repeat Fields</from>
+      <to>grab results</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>grab results</from>
+      <to>result</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Data grid</name>
+    <type>DataGrid</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <data>
+      <line>
+        <item>A</item>
+        <item>1</item>
+        <item>2025/01/01 12:00:00</item>
+        <item>Mouse</item>
+        <item>Mickey</item>
+        <item>1928/11/18</item>
+        <item/>
+        <item/>
+      </line>
+      <line>
+        <item>B</item>
+        <item>1</item>
+        <item>2025/01/01 13:00:00</item>
+        <item/>
+        <item/>
+        <item/>
+        <item>active</item>
+        <item/>
+      </line>
+      <line>
+        <item>C</item>
+        <item>1</item>
+        <item>2025/01/01 14:00:00</item>
+        <item/>
+        <item/>
+        <item/>
+        <item/>
+        <item>positive</item>
+      </line>
+      <line>
+        <item>A</item>
+        <item>2</item>
+        <item>2025/01/01 08:00:00</item>
+        <item>Duck</item>
+        <item>Daffy</item>
+        <item>1937/04/17</item>
+        <item/>
+        <item/>
+      </line>
+      <line>
+        <item>B</item>
+        <item>2</item>
+        <item>2025/01/01 09:00:00</item>
+        <item/>
+        <item/>
+        <item/>
+        <item>inactive</item>
+        <item/>
+      </line>
+      <line>
+        <item>C</item>
+        <item>2</item>
+        <item>2025/01/01 10:00:00</item>
+        <item/>
+        <item/>
+        <item/>
+        <item/>
+        <item>negative</item>
+      </line>
+    </data>
+    <fields>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>source</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>id</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>Integer</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>timestamp</name>
+        <format>yyyy/MM/dd HH:mm:ss</format>
+        <group/>
+        <decimal/>
+        <type>Date</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>lastname</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>firstname</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>birthdate</name>
+        <format>yyyy/MM/dd</format>
+        <group/>
+        <decimal/>
+        <type>Date</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>status</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <currency/>
+        <set_empty_string>N</set_empty_string>
+        <name>indicator</name>
+        <format/>
+        <group/>
+        <decimal/>
+        <type>String</type>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Repeat Fields</name>
+    <type>RepeatFields</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <group_fields>
+</group_fields>
+    <repeats>
+</repeats>
+    <attributes/>
+    <GUI>
+      <xloc>208</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>grab results</name>
+    <type>SelectValues</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <length>-2</length>
+        <name>source</name>
+        <precision>-2</precision>
+      </field>
+      <field>
+        <length>-2</length>
+        <name>id</name>
+        <precision>-2</precision>
+      </field>
+      <field>
+        <length>-2</length>
+        <name>timestamp</name>
+        <precision>-2</precision>
+      </field>
+      <field>
+        <length>-2</length>
+        <name>rLast</name>
+        <precision>-2</precision>
+      </field>
+      <field>
+        <length>-2</length>
+        <name>rFirst</name>
+        <precision>-2</precision>
+      </field>
+      <field>
+        <length>-2</length>
+        <name>rBirthdate</name>
+        <precision>-2</precision>
+      </field>
+      <field>
+        <length>-2</length>
+        <name>rStatus</name>
+        <precision>-2</precision>
+      </field>
+      <field>
+        <length>-2</length>
+        <name>rIndicator</name>
+        <precision>-2</precision>
+      </field>
+      <select_unspecified>N</select_unspecified>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>320</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>result</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>432</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git 
a/integration-tests/transforms/0082-repeat-fields-previous-when-null.hpl 
b/integration-tests/transforms/0082-repeat-fields-previous-when-null.hpl
new file mode 100644
index 0000000000..608e004669
--- /dev/null
+++ b/integration-tests/transforms/0082-repeat-fields-previous-when-null.hpl
@@ -0,0 +1,192 @@
+<?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.
+
+-->
+<pipeline>
+  <info>
+    <name>0081-repeat-fields-previous-when-null</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2025/12/07 14:17:55.746</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2025/12/07 14:17:55.746</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>Repeat Fields</from>
+      <to>Result</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>source</from>
+      <to>Repeat Fields</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Repeat Fields</name>
+    <type>RepeatFields</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <group_fields>
+      <group_field>id</group_field>
+    </group_fields>
+    <repeats>
+      <field>
+        <source_field>value1</source_field>
+        <target_field>v1</target_field>
+        <type>previous_when_null</type>
+      </field>
+      <field>
+        <source_field>value2</source_field>
+        <target_field>v2</target_field>
+        <type>previous_when_null</type>
+      </field>
+    </repeats>
+    <attributes/>
+    <GUI>
+      <xloc>208</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Result</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>336</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>source</name>
+    <type>DataGrid</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <data>
+      <line>
+        <item>1</item>
+        <item>v111</item>
+        <item>v121</item>
+      </line>
+      <line>
+        <item>1</item>
+        <item/>
+        <item>v122</item>
+      </line>
+      <line>
+        <item>1</item>
+        <item>v111</item>
+        <item/>
+      </line>
+      <line>
+        <item>2</item>
+        <item>v212</item>
+        <item>v222</item>
+      </line>
+      <line>
+        <item>2</item>
+        <item/>
+        <item/>
+      </line>
+      <line>
+        <item>2</item>
+        <item/>
+        <item>v222</item>
+      </line>
+      <line>
+        <item>3</item>
+        <item>v313</item>
+        <item>v323</item>
+      </line>
+      <line>
+        <item>3</item>
+        <item/>
+        <item/>
+      </line>
+      <line>
+        <item>3</item>
+        <item>v313</item>
+        <item/>
+      </line>
+    </data>
+    <fields>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>id</name>
+        <format>0</format>
+        <type>Integer</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>value1</name>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>value2</name>
+        <type>String</type>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/transforms/0082-repeat-fields-previous.hpl 
b/integration-tests/transforms/0082-repeat-fields-previous.hpl
new file mode 100644
index 0000000000..e13c115053
--- /dev/null
+++ b/integration-tests/transforms/0082-repeat-fields-previous.hpl
@@ -0,0 +1,192 @@
+<?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.
+
+-->
+<pipeline>
+  <info>
+    <name>0081-repeat-fields-previous</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2025/12/07 14:10:54.470</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2025/12/07 14:10:54.470</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>source</from>
+      <to>Repeat Fields</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Repeat Fields</from>
+      <to>Result</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Repeat Fields</name>
+    <type>RepeatFields</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <group_fields>
+      <group_field>id</group_field>
+    </group_fields>
+    <repeats>
+      <field>
+        <source_field>value1</source_field>
+        <target_field>v1</target_field>
+        <type>previous</type>
+      </field>
+      <field>
+        <source_field>value2</source_field>
+        <target_field>v2</target_field>
+        <type>previous</type>
+      </field>
+    </repeats>
+    <attributes/>
+    <GUI>
+      <xloc>208</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Result</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>336</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>source</name>
+    <type>DataGrid</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <data>
+      <line>
+        <item>1</item>
+        <item>v111</item>
+        <item>v121</item>
+      </line>
+      <line>
+        <item>1</item>
+        <item>v112</item>
+        <item>v122</item>
+      </line>
+      <line>
+        <item>1</item>
+        <item>v111</item>
+        <item>v121</item>
+      </line>
+      <line>
+        <item>2</item>
+        <item>v212</item>
+        <item>v222</item>
+      </line>
+      <line>
+        <item>2</item>
+        <item>v213</item>
+        <item>v223</item>
+      </line>
+      <line>
+        <item>2</item>
+        <item>v212</item>
+        <item>v222</item>
+      </line>
+      <line>
+        <item>3</item>
+        <item>v313</item>
+        <item>v323</item>
+      </line>
+      <line>
+        <item>3</item>
+        <item>v313</item>
+        <item>v323</item>
+      </line>
+      <line>
+        <item>3</item>
+        <item>v313</item>
+        <item>v323</item>
+      </line>
+    </data>
+    <fields>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>id</name>
+        <format>0</format>
+        <type>Integer</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>value1</name>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>value2</name>
+        <type>String</type>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git 
a/integration-tests/transforms/datasets/golden-repeat-fields-current-when-indicated.csv
 
b/integration-tests/transforms/datasets/golden-repeat-fields-current-when-indicated.csv
new file mode 100644
index 0000000000..b4c85d78ab
--- /dev/null
+++ 
b/integration-tests/transforms/datasets/golden-repeat-fields-current-when-indicated.csv
@@ -0,0 +1,7 @@
+source,id,timestamp,rLast,rFirst,rBirthdate,rStatus,rIndicator
+A,1,2025/01/01 12:00:00,Mouse,Mickey,1928/11/18,,
+B,1,2025/01/01 13:00:00,Mouse,Mickey,1928/11/18,active,
+C,1,2025/01/01 14:00:00,Mouse,Mickey,1928/11/18,active,positive
+A,2,2025/01/01 08:00:00,Duck,Daffy,1937/04/17,,
+B,2,2025/01/01 09:00:00,Duck,Daffy,1937/04/17,inactive,
+C,2,2025/01/01 10:00:00,Duck,Daffy,1937/04/17,inactive,negative
diff --git 
a/integration-tests/transforms/datasets/golden-repeat-fields-previous.csv 
b/integration-tests/transforms/datasets/golden-repeat-fields-previous.csv
new file mode 100644
index 0000000000..775e1fed96
--- /dev/null
+++ b/integration-tests/transforms/datasets/golden-repeat-fields-previous.csv
@@ -0,0 +1,10 @@
+id,value1,value2,v1,v2
+1,v111,v121,v111,v121
+1,v112,v122,v111,v121
+1,v111,v121,v111,v121
+2,v212,v222,v212,v222
+2,v213,v223,v212,v222
+2,v212,v222,v212,v222
+3,v313,v323,v313,v323
+3,v313,v323,v313,v323
+3,v313,v323,v313,v323
diff --git 
a/integration-tests/transforms/datasets/golden-repeat-fields-when-null.csv 
b/integration-tests/transforms/datasets/golden-repeat-fields-when-null.csv
new file mode 100644
index 0000000000..075d533020
--- /dev/null
+++ b/integration-tests/transforms/datasets/golden-repeat-fields-when-null.csv
@@ -0,0 +1,10 @@
+id,value1,value2,v1,v2
+1,v111,v121,v111,v121
+1,,v122,v111,v122
+1,v111,,v111,v122
+2,v212,v222,v212,v222
+2,,,v212,v222
+2,,v222,v212,v222
+3,v313,v323,v313,v323
+3,,,v313,v323
+3,v313,,v313,v323
diff --git a/integration-tests/transforms/main-0082-repeat-fields.hwf 
b/integration-tests/transforms/main-0082-repeat-fields.hwf
new file mode 100644
index 0000000000..7a1006d2dc
--- /dev/null
+++ b/integration-tests/transforms/main-0082-repeat-fields.hwf
@@ -0,0 +1,89 @@
+<?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.
+
+-->
+<workflow>
+  <name>main-0082-repeat-fields</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description/>
+  <extended_description/>
+  <workflow_version/>
+  <created_user>-</created_user>
+  <created_date>2025/12/07 14:10:31.342</created_date>
+  <modified_user>-</modified_user>
+  <modified_date>2025/12/07 14:10:31.342</modified_date>
+  <parameters>
+    </parameters>
+  <actions>
+    <action>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <DayOfMonth>1</DayOfMonth>
+      <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
+      <hour>12</hour>
+      <intervalMinutes>60</intervalMinutes>
+      <intervalSeconds>0</intervalSeconds>
+      <minutes>0</minutes>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <weekDay>1</weekDay>
+      <parallel>N</parallel>
+      <xloc>80</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>Run 0082 tests</name>
+      <description/>
+      <type>RunPipelineTests</type>
+      <attributes/>
+      <test_names>
+        <test_name>
+          <name>0082-repeat-fields-current-when-indicated UNIT</name>
+        </test_name>
+        <test_name>
+          <name>0082-repeat-fields-previous UNIT</name>
+        </test_name>
+        <test_name>
+          <name>0082-repeat-fields-previous-when-null UNIT</name>
+        </test_name>
+        <test_name>
+          <name>0082-repeat-fields-mdi-main UNIT</name>
+        </test_name>
+      </test_names>
+      <parallel>N</parallel>
+      <xloc>256</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>Run 0082 tests</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+  </hops>
+  <notepads>
+  </notepads>
+  <attributes/>
+</workflow>
diff --git 
a/integration-tests/transforms/metadata/dataset/golden-repeat-fields-current-when-indicated.json
 
b/integration-tests/transforms/metadata/dataset/golden-repeat-fields-current-when-indicated.json
new file mode 100644
index 0000000000..d11028a787
--- /dev/null
+++ 
b/integration-tests/transforms/metadata/dataset/golden-repeat-fields-current-when-indicated.json
@@ -0,0 +1,72 @@
+{
+  "base_filename": "golden-repeat-fields-current-when-indicated.csv",
+  "name": "golden-repeat-fields-current-when-indicated",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "source",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 5,
+      "field_precision": 0,
+      "field_name": "id",
+      "field_format": "####0;-####0"
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 3,
+      "field_precision": -1,
+      "field_name": "timestamp",
+      "field_format": "yyyy/MM/dd HH:mm:ss"
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "rLast",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "rFirst",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 3,
+      "field_precision": -1,
+      "field_name": "rBirthdate",
+      "field_format": "yyyy/MM/dd"
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "rStatus",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "rIndicator",
+      "field_format": ""
+    }
+  ],
+  "folder_name": ""
+}
\ No newline at end of file
diff --git 
a/integration-tests/transforms/metadata/dataset/golden-repeat-fields-previous.json
 
b/integration-tests/transforms/metadata/dataset/golden-repeat-fields-previous.json
new file mode 100644
index 0000000000..b73ace9b32
--- /dev/null
+++ 
b/integration-tests/transforms/metadata/dataset/golden-repeat-fields-previous.json
@@ -0,0 +1,48 @@
+{
+  "base_filename": "golden-repeat-fields-previous.csv",
+  "name": "golden-repeat-fields-previous",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 5,
+      "field_precision": 0,
+      "field_name": "id",
+      "field_format": "0"
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "value1",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "value2",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "v1",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "v2",
+      "field_format": ""
+    }
+  ],
+  "folder_name": ""
+}
\ No newline at end of file
diff --git 
a/integration-tests/transforms/metadata/dataset/golden-repeat-fields-when-null.json
 
b/integration-tests/transforms/metadata/dataset/golden-repeat-fields-when-null.json
new file mode 100644
index 0000000000..070f91877f
--- /dev/null
+++ 
b/integration-tests/transforms/metadata/dataset/golden-repeat-fields-when-null.json
@@ -0,0 +1,48 @@
+{
+  "base_filename": "golden-repeat-fields-when-null.csv",
+  "name": "golden-repeat-fields-when-null",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 5,
+      "field_precision": 0,
+      "field_name": "id",
+      "field_format": "0"
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "value1",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "value2",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "v1",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "v2",
+      "field_format": ""
+    }
+  ],
+  "folder_name": ""
+}
\ No newline at end of file
diff --git 
a/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-current-when-indicated
 UNIT.json 
b/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-current-when-indicated
 UNIT.json
new file mode 100644
index 0000000000..06b991f497
--- /dev/null
+++ 
b/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-current-when-indicated
 UNIT.json       
@@ -0,0 +1,63 @@
+{
+  "database_replacements": [],
+  "autoOpening": true,
+  "description": "",
+  "persist_filename": "",
+  "test_type": "UNIT_TEST",
+  "variableValues": [],
+  "basePath": "",
+  "golden_data_sets": [
+    {
+      "field_mappings": [
+        {
+          "transform_field": "source",
+          "data_set_field": "source"
+        },
+        {
+          "transform_field": "id",
+          "data_set_field": "id"
+        },
+        {
+          "transform_field": "timestamp",
+          "data_set_field": "timestamp"
+        },
+        {
+          "transform_field": "rLast",
+          "data_set_field": "rLast"
+        },
+        {
+          "transform_field": "rFirst",
+          "data_set_field": "rFirst"
+        },
+        {
+          "transform_field": "rBirthdate",
+          "data_set_field": "rBirthdate"
+        },
+        {
+          "transform_field": "rStatus",
+          "data_set_field": "rStatus"
+        },
+        {
+          "transform_field": "rIndicator",
+          "data_set_field": "rIndicator"
+        }
+      ],
+      "field_order": [
+        "source",
+        "id",
+        "timestamp",
+        "rLast",
+        "rFirst",
+        "rBirthdate",
+        "rStatus",
+        "rIndicator"
+      ],
+      "data_set_name": "golden-repeat-fields-current-when-indicated",
+      "transform_name": "result"
+    }
+  ],
+  "input_data_sets": [],
+  "name": "0082-repeat-fields-current-when-indicated UNIT",
+  "trans_test_tweaks": [],
+  "pipeline_filename": "./0082-repeat-fields-current-when-indicated.hpl"
+}
\ No newline at end of file
diff --git 
a/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-mdi-main 
UNIT.json 
b/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-mdi-main 
UNIT.json
new file mode 100644
index 0000000000..9df5505843
--- /dev/null
+++ 
b/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-mdi-main 
UNIT.json     
@@ -0,0 +1,58 @@
+{
+  "database_replacements": [],
+  "autoOpening": true,
+  "description": "",
+  "persist_filename": "",
+  "test_type": "UNIT_TEST",
+  "variableValues": [],
+  "basePath": "",
+  "golden_data_sets": [
+    {
+      "field_mappings": [
+        {
+          "transform_field": "source",
+          "data_set_field": "source"
+        },
+        {
+          "transform_field": "id",
+          "data_set_field": "id"
+        },
+        {
+          "transform_field": "timestamp",
+          "data_set_field": "timestamp"
+        },
+        {
+          "transform_field": "rLast",
+          "data_set_field": "rLast"
+        },
+        {
+          "transform_field": "rFirst",
+          "data_set_field": "rFirst"
+        },
+        {
+          "transform_field": "rBirthdate",
+          "data_set_field": "rBirthdate"
+        },
+        {
+          "transform_field": "rStatus",
+          "data_set_field": "rStatus"
+        },
+        {
+          "transform_field": "rIndicator",
+          "data_set_field": "rIndicator"
+        }
+      ],
+      "field_order": [
+        "id",
+        "timestamp",
+        "source"
+      ],
+      "data_set_name": "golden-repeat-fields-current-when-indicated",
+      "transform_name": "Results"
+    }
+  ],
+  "input_data_sets": [],
+  "name": "0082-repeat-fields-mdi-main UNIT",
+  "trans_test_tweaks": [],
+  "pipeline_filename": "./0082-repeat-fields-mdi-main.hpl"
+}
\ No newline at end of file
diff --git 
a/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-previous 
UNIT.json 
b/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-previous 
UNIT.json
new file mode 100644
index 0000000000..bf5dab4bb6
--- /dev/null
+++ 
b/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-previous 
UNIT.json     
@@ -0,0 +1,48 @@
+{
+  "database_replacements": [],
+  "autoOpening": true,
+  "description": "",
+  "persist_filename": "",
+  "test_type": "UNIT_TEST",
+  "variableValues": [],
+  "basePath": "",
+  "golden_data_sets": [
+    {
+      "field_mappings": [
+        {
+          "transform_field": "id",
+          "data_set_field": "id"
+        },
+        {
+          "transform_field": "value1",
+          "data_set_field": "value1"
+        },
+        {
+          "transform_field": "value2",
+          "data_set_field": "value2"
+        },
+        {
+          "transform_field": "v1",
+          "data_set_field": "v1"
+        },
+        {
+          "transform_field": "v2",
+          "data_set_field": "v2"
+        }
+      ],
+      "field_order": [
+        "id",
+        "value1",
+        "value2",
+        "v1",
+        "v2"
+      ],
+      "data_set_name": "golden-repeat-fields-previous",
+      "transform_name": "Result"
+    }
+  ],
+  "input_data_sets": [],
+  "name": "0082-repeat-fields-previous UNIT",
+  "trans_test_tweaks": [],
+  "pipeline_filename": "./0082-repeat-fields-previous.hpl"
+}
\ No newline at end of file
diff --git 
a/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-previous-when-null
 UNIT.json 
b/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-previous-when-null
 UNIT.json
new file mode 100644
index 0000000000..e074f4150e
--- /dev/null
+++ 
b/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-previous-when-null
 UNIT.json   
@@ -0,0 +1,48 @@
+{
+  "database_replacements": [],
+  "autoOpening": true,
+  "description": "",
+  "persist_filename": "",
+  "test_type": "UNIT_TEST",
+  "variableValues": [],
+  "basePath": "",
+  "golden_data_sets": [
+    {
+      "field_mappings": [
+        {
+          "transform_field": "id",
+          "data_set_field": "id"
+        },
+        {
+          "transform_field": "value1",
+          "data_set_field": "value1"
+        },
+        {
+          "transform_field": "value2",
+          "data_set_field": "value2"
+        },
+        {
+          "transform_field": "v1",
+          "data_set_field": "v1"
+        },
+        {
+          "transform_field": "v2",
+          "data_set_field": "v2"
+        }
+      ],
+      "field_order": [
+        "id",
+        "value1",
+        "value2",
+        "v1",
+        "v2"
+      ],
+      "data_set_name": "golden-repeat-fields-when-null",
+      "transform_name": "Result"
+    }
+  ],
+  "input_data_sets": [],
+  "name": "0082-repeat-fields-previous-when-null UNIT",
+  "trans_test_tweaks": [],
+  "pipeline_filename": "./0082-repeat-fields-previous-when-null.hpl"
+}
\ No newline at end of file
diff --git a/plugins/transforms/pom.xml b/plugins/transforms/pom.xml
index 6682aa5496..68cf25b8a4 100644
--- a/plugins/transforms/pom.xml
+++ b/plugins/transforms/pom.xml
@@ -124,6 +124,7 @@
         <module>propertyoutput</module>
         <module>randomvalue</module>
         <module>regexeval</module>
+        <module>repeatfields</module>
         <module>replacestring</module>
         <module>reservoirsampling</module>
         <module>rest</module>
diff --git a/plugins/transforms/repeatfields/pom.xml 
b/plugins/transforms/repeatfields/pom.xml
new file mode 100644
index 0000000000..2ee361d3d5
--- /dev/null
+++ b/plugins/transforms/repeatfields/pom.xml
@@ -0,0 +1,45 @@
+<?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.
+  ~
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.hop</groupId>
+        <artifactId>hop-plugins-transforms</artifactId>
+        <version>2.17.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>hop-transform-repeatfields</artifactId>
+    <packaging>jar</packaging>
+    <name>Hop Plugins Transforms Repeat Fields</name>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.apache.hop</groupId>
+                <artifactId>hop-libs</artifactId>
+                <version>${project.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies></dependencies>
+</project>
diff --git a/plugins/transforms/repeatfields/src/assembly/assembly.xml 
b/plugins/transforms/repeatfields/src/assembly/assembly.xml
new file mode 100644
index 0000000000..cf4445966b
--- /dev/null
+++ b/plugins/transforms/repeatfields/src/assembly/assembly.xml
@@ -0,0 +1,50 @@
+<!--
+  ~ 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.
+  ~
+  -->
+
+<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0";
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+    xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0 
http://maven.apache.org/xsd/assembly-2.2.0.xsd";>
+    <id>hop-transform-repeatfields</id>
+    <formats>
+        <format>zip</format>
+    </formats>
+    <baseDirectory>.</baseDirectory>
+    <files>
+        <file>
+            <source>${project.basedir}/src/main/resources/version.xml</source>
+            <outputDirectory>plugins/transforms/repeatfields</outputDirectory>
+            <filtered>true</filtered>
+        </file>
+    </files>
+
+    <fileSets>
+        <fileSet>
+            <directory>${project.basedir}/src/main/samples</directory>
+            <outputDirectory>config/projects/samples/</outputDirectory>
+        </fileSet>
+    </fileSets>
+
+    <dependencySets>
+        <dependencySet>
+            <includes>
+                
<include>org.apache.hop:hop-transform-repeatfields:jar</include>
+            </includes>
+            <outputDirectory>plugins/transforms/repeatfields</outputDirectory>
+        </dependencySet>
+    </dependencySets>
+</assembly>
\ No newline at end of file
diff --git 
a/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/Repeat.java
 
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/Repeat.java
new file mode 100644
index 0000000000..66d6b3f54c
--- /dev/null
+++ 
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/Repeat.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       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.repeatfields;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import 
org.apache.hop.pipeline.transforms.repeatfields.RepeatFieldsMeta.RepeatType;
+
+@Getter
+@Setter
+public class Repeat {
+  @HopMetadataProperty(storeWithCode = true)
+  private RepeatType type;
+
+  @HopMetadataProperty(key = "source_field")
+  private String sourceField;
+
+  @HopMetadataProperty(key = "target_field")
+  private String targetField;
+
+  @HopMetadataProperty(key = "indicator_field_name")
+  private String indicatorFieldName;
+
+  @HopMetadataProperty(key = "indicator_value")
+  private String indicatorValue;
+
+  public Repeat() {}
+
+  public Repeat(Repeat r) {
+    this.type = r.type;
+    this.sourceField = r.sourceField;
+    this.targetField = r.targetField;
+    this.indicatorFieldName = r.indicatorFieldName;
+    this.indicatorValue = r.indicatorValue;
+  }
+}
diff --git 
a/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFields.java
 
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFields.java
new file mode 100644
index 0000000000..a333182bd6
--- /dev/null
+++ 
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFields.java
@@ -0,0 +1,186 @@
+/*
+ * 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.repeatfields;
+
+import com.google.common.primitives.Ints;
+import java.util.ArrayList;
+import org.apache.commons.lang.StringUtils;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.RowDataUtil;
+import org.apache.hop.pipeline.Pipeline;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.BaseTransform;
+import org.apache.hop.pipeline.transform.TransformMeta;
+
+public class RepeatFields extends BaseTransform<RepeatFieldsMeta, 
RepeatFieldsData> {
+  public RepeatFields(
+      TransformMeta transformMeta,
+      RepeatFieldsMeta meta,
+      RepeatFieldsData data,
+      int copyNr,
+      PipelineMeta pipelineMeta,
+      Pipeline pipeline) {
+    super(transformMeta, meta, data, copyNr, pipelineMeta, pipeline);
+  }
+
+  @Override
+  public boolean processRow() throws HopException {
+    Object[] r = getRow();
+    if (r == null) {
+      setOutputDone();
+      return false;
+    }
+
+    if (first) {
+      first = false;
+
+      // The output row metadata
+      data.outputRowMeta = getInputRowMeta().clone();
+      meta.getFields(data.outputRowMeta, getTransformName(), null, null, this, 
metadataProvider);
+
+      data.groupIndexes = new ArrayList<>();
+      for (String groupField : meta.getGroupFields()) {
+        int index = getInputRowMeta().indexOfValue(groupField);
+        if (index < 0) {
+          throw new HopException("Unable to find group field: " + groupField);
+        }
+        data.groupIndexes.add(index);
+      }
+      data.sourceIndexes = new ArrayList<>();
+      data.indicatorIndexes = new ArrayList<>();
+      for (Repeat repeat : meta.getRepeats()) {
+        String sourceFieldName = resolve(repeat.getSourceField());
+        int index = getInputRowMeta().indexOfValue(sourceFieldName);
+        if (index < 0) {
+          throw new HopException("Unable to find source field to repeat: " + 
sourceFieldName);
+        }
+        data.sourceIndexes.add(index);
+
+        int indicatorIndex = -1;
+        String indicatorFieldName = resolve(repeat.getIndicatorFieldName());
+        if (StringUtils.isNotEmpty(indicatorFieldName)) {
+          indicatorIndex = getInputRowMeta().indexOfValue(indicatorFieldName);
+          if (indicatorIndex < 0) {
+            throw new HopException("Unable to find indicator field: " + 
indicatorFieldName);
+          }
+        }
+        data.indicatorIndexes.add(indicatorIndex);
+      }
+    }
+
+    // Compare the current row with the previous one.  If there's a change, 
start a new group
+    //
+    if (isNewGroup(r, data.previousRow)) {
+      startNewGroup();
+    }
+
+    Object[] outputRow = RowDataUtil.resizeArray(r, data.outputRowMeta.size());
+    int targetIndex = getInputRowMeta().size();
+    for (int i = 0; i < meta.getRepeats().size(); i++) {
+      Repeat repeat = meta.getRepeats().get(i);
+      int sourceIndex = data.sourceIndexes.get(i);
+      IValueMeta sourceValueMeta = getInputRowMeta().getValueMeta(sourceIndex);
+      Object sourceValue = r[sourceIndex];
+      Object targetValue = sourceValue;
+      // What do we need to do?
+      switch (repeat.getType()) {
+        case Previous -> targetValue = getPreviousValue(sourceValue, 
targetIndex);
+        case PreviousWhenNull -> {
+          // If the source value is null, take the previous value.
+          if (sourceValueMeta.isNull(sourceValue)) {
+            targetValue = getPreviousValue(null, targetIndex);
+          }
+        }
+        case CurrentWhenIndicated ->
+            targetValue = getCurrentValueWhenIndicated(i, repeat, r, 
sourceValue, targetIndex);
+      }
+      outputRow[targetIndex] = targetValue;
+      targetIndex++;
+    }
+
+    // Make a copy of the current row to prevent next transforms from 
modifying it.
+    //
+    data.previousRow = data.outputRowMeta.cloneRow(outputRow);
+
+    // Send the output to the next transforms.
+    //
+    putRow(data.outputRowMeta, outputRow);
+
+    return true;
+  }
+
+  private Object getCurrentValueWhenIndicated(
+      int i, Repeat repeat, Object[] r, Object sourceValue, int targetIndex) 
throws HopException {
+    Object targetValue;
+    int indicatorIndex = data.indicatorIndexes.get(i);
+    if (indicatorIndex < 0) {
+      throw new HopException("Unable to find indicator field: " + 
repeat.getIndicatorFieldName());
+    }
+    String indicator = getInputRowMeta().getString(r, indicatorIndex);
+    if (StringUtils.isEmpty(indicator)) {
+      throw new HopException(
+          "No indicator value was found in field " + 
repeat.getIndicatorFieldName());
+    }
+    if (indicator.equals(repeat.getIndicatorValue())) {
+      targetValue = sourceValue;
+    } else {
+      if (data.previousRow == null) {
+        // The first row in a data set is often the last version of an SCD2 
dimension.
+        // We'll simply keep this value.
+        targetValue = sourceValue;
+      } else {
+        // We take the target value from the previous row
+        targetValue = data.previousRow[targetIndex];
+      }
+    }
+    return targetValue;
+  }
+
+  /**
+   * Take the value from the previous row target field (if there is one). The 
default value (if
+   * there's no previous row) is the source value of the first row.
+   *
+   * @param sourceValue The source value (default when no previous row)
+   * @param targetIndex The target field index to read from
+   * @return The value for the previous row (or the given current source value)
+   */
+  private Object getPreviousValue(Object sourceValue, int targetIndex) {
+    Object value = sourceValue;
+    if (data.previousRow != null) {
+      value = data.previousRow[targetIndex];
+    }
+    return value;
+  }
+
+  private void startNewGroup() {
+    // There is no previous row
+    data.previousRow = null;
+  }
+
+  private boolean isNewGroup(Object[] currentRow, Object[] previousRow) throws 
HopValueException {
+    if (previousRow == null) {
+      return true;
+    }
+    int compare =
+        getInputRowMeta().compare(currentRow, previousRow, 
Ints.toArray(data.groupIndexes));
+    return compare != 0;
+  }
+}
diff --git 
a/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFieldsData.java
 
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFieldsData.java
new file mode 100644
index 0000000000..214ec8cfd9
--- /dev/null
+++ 
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFieldsData.java
@@ -0,0 +1,32 @@
+/*
+ * 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.repeatfields;
+
+import java.util.List;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.pipeline.transform.BaseTransformData;
+import org.apache.hop.pipeline.transform.ITransformData;
+
+public class RepeatFieldsData extends BaseTransformData implements 
ITransformData {
+  public List<Integer> groupIndexes;
+  public List<Integer> sourceIndexes;
+  public List<Integer> indicatorIndexes;
+  public IRowMeta outputRowMeta;
+  public Object[] previousRow;
+}
diff --git 
a/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFieldsDialog.java
 
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFieldsDialog.java
new file mode 100644
index 0000000000..71d6ab2712
--- /dev/null
+++ 
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFieldsDialog.java
@@ -0,0 +1,385 @@
+/*
+ * 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.repeatfields;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.ui.core.ConstUi;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.BaseDialog;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
+import org.apache.hop.ui.core.dialog.MessageDialogWithToggle;
+import org.apache.hop.ui.core.widget.ColumnInfo;
+import org.apache.hop.ui.core.widget.TableView;
+import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.TableItem;
+import org.eclipse.swt.widgets.Text;
+
+public class RepeatFieldsDialog extends BaseTransformDialog {
+  private static final Class<?> PKG = RepeatFields.class;
+
+  public static final String STRING_SORT_WARNING_PARAMETER = 
"RepeatFieldsWarning";
+
+  private ColumnInfo[] ciGroup;
+  private TableView wGroup;
+
+  private ColumnInfo[] ciRepeats;
+  private TableView wRepeats;
+
+  private final RepeatFieldsMeta input;
+  private final List<String> inputFields = new ArrayList<>();
+
+  public RepeatFieldsDialog(
+      Shell parent,
+      IVariables variables,
+      RepeatFieldsMeta transformMeta,
+      PipelineMeta pipelineMeta) {
+    super(parent, variables, transformMeta, pipelineMeta);
+    input = transformMeta;
+  }
+
+  @Override
+  public String open() {
+    Shell parent = getParent();
+
+    shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MAX | 
SWT.MIN);
+    PropsUi.setLook(shell);
+    setShellImage(shell, input);
+
+    FormLayout formLayout = new FormLayout();
+    formLayout.marginWidth = PropsUi.getFormMargin();
+    formLayout.marginHeight = PropsUi.getFormMargin();
+
+    shell.setLayout(formLayout);
+    shell.setText(BaseMessages.getString(PKG, 
"RepeatFieldsDialog.Shell.Title"));
+
+    int middle = props.getMiddlePct();
+    int margin = PropsUi.getMargin();
+
+    // The buttons go at the bottom
+    //
+    // THE BUTTONS
+    wOk = new Button(shell, SWT.PUSH);
+    wOk.setText(BaseMessages.getString(PKG, "System.Button.OK"));
+    wOk.addListener(SWT.Selection, e -> ok());
+    wCancel = new Button(shell, SWT.PUSH);
+    wCancel.setText(BaseMessages.getString(PKG, "System.Button.Cancel"));
+    wCancel.addListener(SWT.Selection, e -> cancel());
+    setButtonPositions(new Button[] {wOk, wCancel}, margin, null);
+
+    // TransformName line
+    wlTransformName = new Label(shell, SWT.RIGHT);
+    wlTransformName.setText(BaseMessages.getString(PKG, 
"RepeatFieldsDialog.TransformName.Label"));
+    PropsUi.setLook(wlTransformName);
+    fdlTransformName = new FormData();
+    fdlTransformName.left = new FormAttachment(0, 0);
+    fdlTransformName.right = new FormAttachment(middle, -margin);
+    fdlTransformName.top = new FormAttachment(0, margin);
+    wlTransformName.setLayoutData(fdlTransformName);
+    wTransformName = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+    wTransformName.setText(transformName);
+    PropsUi.setLook(wTransformName);
+    fdTransformName = new FormData();
+    fdTransformName.left = new FormAttachment(middle, 0);
+    fdTransformName.top = new FormAttachment(0, margin);
+    fdTransformName.right = new FormAttachment(100, 0);
+    wTransformName.setLayoutData(fdTransformName);
+
+    Label wlGroup = new Label(shell, SWT.NONE);
+    wlGroup.setText(BaseMessages.getString(PKG, 
"RepeatFieldsDialog.Group.Label"));
+    PropsUi.setLook(wlGroup);
+    FormData fdlGroup = new FormData();
+    fdlGroup.left = new FormAttachment(0, 0);
+    fdlGroup.top = new FormAttachment(wlTransformName, 2 * margin);
+    wlGroup.setLayoutData(fdlGroup);
+
+    // The group fields
+    //
+    int groupRows = input.getGroupFields().size();
+    ciGroup =
+        new ColumnInfo[] {
+          new ColumnInfo(
+              BaseMessages.getString(PKG, 
"RepeatFieldsDialog.ColumnInfo.GroupField"),
+              ColumnInfo.COLUMN_TYPE_CCOMBO,
+              new String[] {""},
+              false)
+        };
+
+    wGroup =
+        new TableView(
+            variables,
+            shell,
+            SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI | SWT.V_SCROLL | 
SWT.H_SCROLL,
+            ciGroup,
+            groupRows,
+            null,
+            props);
+
+    Button wGet = new Button(shell, SWT.PUSH);
+    wGet.setText(BaseMessages.getString(PKG, 
"RepeatFieldsDialog.GetFields.Button"));
+    FormData fdGet = new FormData();
+    fdGet.top = new FormAttachment(wlGroup, margin);
+    fdGet.right = new FormAttachment(100, 0);
+    wGet.setLayoutData(fdGet);
+    wGet.addListener(SWT.Selection, e -> getGroups());
+
+    FormData fdGroup = new FormData();
+    fdGroup.left = new FormAttachment(0, 0);
+    fdGroup.top = new FormAttachment(wlGroup, margin);
+    fdGroup.right = new FormAttachment(wGet, -margin);
+    fdGroup.bottom = new FormAttachment(30, 0); // 35% from the top
+    wGroup.setLayoutData(fdGroup);
+
+    // The repeat fields section
+    //
+    Label wlRepeats = new Label(shell, SWT.NONE);
+    wlRepeats.setText(BaseMessages.getString(PKG, 
"RepeatFieldsDialog.RepeatFields.Label"));
+    PropsUi.setLook(wlRepeats);
+    FormData fdlRepeats = new FormData();
+    fdlRepeats.left = new FormAttachment(0, 0);
+    fdlRepeats.top = new FormAttachment(wGroup, margin);
+    wlRepeats.setLayoutData(fdlRepeats);
+
+    ciRepeats =
+        new ColumnInfo[] {
+          new ColumnInfo(
+              BaseMessages.getString(PKG, 
"RepeatFieldsDialog.ColumnInfo.RepeatType"),
+              ColumnInfo.COLUMN_TYPE_CCOMBO,
+              new String[] {""},
+              false),
+          new ColumnInfo(
+              BaseMessages.getString(PKG, 
"RepeatFieldsDialog.ColumnInfo.SourceFieldName"),
+              ColumnInfo.COLUMN_TYPE_CCOMBO,
+              new String[] {""},
+              false),
+          new ColumnInfo(
+              BaseMessages.getString(PKG, 
"RepeatFieldsDialog.ColumnInfo.TargetFieldName"),
+              ColumnInfo.COLUMN_TYPE_TEXT,
+              false),
+          new ColumnInfo(
+              BaseMessages.getString(PKG, 
"RepeatFieldsDialog.ColumnInfo.IndicatorFieldName"),
+              ColumnInfo.COLUMN_TYPE_CCOMBO,
+              new String[] {""},
+              false),
+          new ColumnInfo(
+              BaseMessages.getString(PKG, 
"RepeatFieldsDialog.ColumnInfo.IndicatorValue"),
+              ColumnInfo.COLUMN_TYPE_TEXT,
+              false),
+        };
+
+    ciRepeats[0].setComboValues(RepeatFieldsMeta.RepeatType.getDescriptions());
+    ciRepeats[1].setUsingVariables(true);
+    ciRepeats[2].setUsingVariables(true);
+    ciRepeats[3].setUsingVariables(true);
+
+    wRepeats =
+        new TableView(
+            variables,
+            shell,
+            SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI | SWT.V_SCROLL | 
SWT.H_SCROLL,
+            ciRepeats,
+            input.getRepeats().size(),
+            null,
+            props);
+
+    Button wGetRepeats = new Button(shell, SWT.PUSH);
+    wGetRepeats.setText(BaseMessages.getString(PKG, 
"RepeatFieldsDialog.GetRepeatFields.Button"));
+    FormData fdGetRepeats = new FormData();
+    fdGetRepeats.top = new FormAttachment(wlRepeats, margin);
+    fdGetRepeats.right = new FormAttachment(100, 0);
+    wGetRepeats.setLayoutData(fdGetRepeats);
+    wGetRepeats.addListener(SWT.Selection, e -> getRepeats());
+
+    FormData fdRepeats = new FormData();
+    fdRepeats.left = new FormAttachment(0, 0);
+    fdRepeats.top = new FormAttachment(wlRepeats, margin);
+    fdRepeats.right = new FormAttachment(wGetRepeats, -margin);
+    fdRepeats.bottom = new FormAttachment(wOk, -2 * margin);
+    wRepeats.setLayoutData(fdRepeats);
+
+    final Runnable runnable =
+        () -> {
+          TransformMeta transformMeta = 
pipelineMeta.findTransform(transformName);
+          if (transformMeta != null) {
+            try {
+              IRowMeta row = pipelineMeta.getPrevTransformFields(variables, 
transformMeta);
+
+              // Remember these fields...
+              for (int i = 0; i < row.size(); i++) {
+                inputFields.add(row.getValueMeta(i).getName());
+              }
+              setComboBoxes();
+            } catch (HopException e) {
+              logError(BaseMessages.getString(PKG, 
"System.Dialog.GetFieldsFailed.Message"));
+            }
+          }
+        };
+    new Thread(runnable).start();
+
+    getData();
+
+    BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel());
+
+    return transformName;
+  }
+
+  protected void setComboBoxes() {
+    // Something was changed in the row.
+    //
+    String[] fieldNames = ConstUi.sortFieldNames(inputFields);
+    ciGroup[0].setComboValues(fieldNames);
+    ciRepeats[1].setComboValues(fieldNames);
+    ciRepeats[3].setComboValues(fieldNames);
+  }
+
+  /** Copy information from the meta-data input to the dialog fields. */
+  public void getData() {
+    for (int i = 0; i < input.getGroupFields().size(); i++) {
+      TableItem item = wGroup.table.getItem(i);
+      item.setText(1, Const.NVL(input.getGroupFields().get(i), ""));
+    }
+
+    for (int i = 0; i < input.getRepeats().size(); i++) {
+      Repeat repeat = input.getRepeats().get(i);
+      TableItem item = wRepeats.table.getItem(i);
+      item.setText(1, repeat.getType() != null ? 
repeat.getType().getDescription() : "");
+      item.setText(2, Const.NVL(repeat.getSourceField(), ""));
+      item.setText(3, Const.NVL(repeat.getTargetField(), ""));
+      item.setText(4, Const.NVL(repeat.getIndicatorFieldName(), ""));
+      item.setText(5, Const.NVL(repeat.getIndicatorValue(), ""));
+    }
+
+    wGroup.setRowNums();
+    wGroup.optWidth(true);
+    wRepeats.setRowNums();
+    wRepeats.optWidth(true);
+
+    wTransformName.selectAll();
+    wTransformName.setFocus();
+  }
+
+  private void cancel() {
+    transformName = null;
+    dispose();
+  }
+
+  private void ok() {
+    if (Utils.isEmpty(wTransformName.getText())) {
+      return;
+    }
+
+    transformName = wTransformName.getText();
+
+    getInfo(input);
+
+    showSortWarning();
+
+    dispose();
+  }
+
+  private void showSortWarning() {
+    if (!input.getGroupFields().isEmpty()
+        && 
"Y".equalsIgnoreCase(props.getCustomParameter(STRING_SORT_WARNING_PARAMETER, 
"Y"))) {
+      MessageDialogWithToggle md =
+          new MessageDialogWithToggle(
+              shell,
+              BaseMessages.getString(PKG, 
"RepeatFieldsDialog.SortWarningDialog.DialogTitle"),
+              BaseMessages.getString(
+                      PKG, 
"RepeatFieldsDialog.SortWarningDialog.DialogMessage", Const.CR)
+                  + Const.CR,
+              SWT.ICON_WARNING,
+              new String[] {
+                BaseMessages.getString(PKG, 
"RepeatFieldsDialog.SortWarningDialog.Option1")
+              },
+              BaseMessages.getString(PKG, 
"RepeatFieldsDialog.SortWarningDialog.Option2"),
+              
"N".equalsIgnoreCase(props.getCustomParameter(STRING_SORT_WARNING_PARAMETER, 
"Y")));
+      md.open();
+      props.setCustomParameter(STRING_SORT_WARNING_PARAMETER, 
md.getToggleState() ? "N" : "Y");
+    }
+  }
+
+  /** Copy the information in the widgets into the metadata */
+  private void getInfo(RepeatFieldsMeta meta) {
+    // The group
+    meta.getGroupFields().clear();
+    for (TableItem item : wGroup.getNonEmptyItems()) {
+      input.getGroupFields().add(item.getText(1));
+    }
+
+    // The fields to repeat
+    //
+    meta.getRepeats().clear();
+    for (TableItem item : wRepeats.getNonEmptyItems()) {
+      Repeat repeat = new Repeat();
+      
repeat.setType(RepeatFieldsMeta.RepeatType.lookupDescription(item.getText(1)));
+      repeat.setSourceField(item.getText(2));
+      repeat.setTargetField(item.getText(3));
+      repeat.setIndicatorFieldName(item.getText(4));
+      repeat.setIndicatorValue(item.getText(5));
+      meta.getRepeats().add(repeat);
+    }
+  }
+
+  private void getGroups() {
+    try {
+      IRowMeta r = pipelineMeta.getPrevTransformFields(variables, 
transformName);
+      if (r != null && !r.isEmpty()) {
+        BaseTransformDialog.getFieldsFromPrevious(
+            r, wGroup, 1, new int[] {1}, new int[] {}, -1, -1, null);
+      }
+    } catch (HopException ke) {
+      new ErrorDialog(
+          shell,
+          BaseMessages.getString(PKG, "System.Dialog.GetFieldsFailed.Title"),
+          BaseMessages.getString(PKG, "System.Dialog.GetFieldsFailed.Message"),
+          ke);
+    }
+  }
+
+  private void getRepeats() {
+    try {
+      IRowMeta r = pipelineMeta.getPrevTransformFields(variables, 
transformName);
+      if (r != null && !r.isEmpty()) {
+        BaseTransformDialog.getFieldsFromPrevious(
+            r, wRepeats, 2, new int[] {2}, new int[] {}, -1, -1, null);
+      }
+    } catch (HopException ke) {
+      new ErrorDialog(
+          shell,
+          BaseMessages.getString(PKG, "System.Dialog.GetFieldsFailed.Title"),
+          BaseMessages.getString(PKG, "System.Dialog.GetFieldsFailed.Message"),
+          ke);
+    }
+  }
+}
diff --git 
a/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFieldsMeta.java
 
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFieldsMeta.java
new file mode 100644
index 0000000000..52809d68f2
--- /dev/null
+++ 
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFieldsMeta.java
@@ -0,0 +1,157 @@
+/*
+ * 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.repeatfields;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.commons.lang.StringUtils;
+import org.apache.hop.core.annotations.Transform;
+import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.metadata.api.IEnumHasCodeAndDescription;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.pipeline.transform.BaseTransformMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+
+@Transform(
+    id = "RepeatFields",
+    image = "repeatfields.svg",
+    name = "i18n::RepeatFields.Name",
+    description = "i18n::RepeatFields.Description",
+    categoryDescription = 
"i18n:org.apache.hop.pipeline.transform:BaseTransform.Category.Utility",
+    keywords = "i18n::RepeatFields.keyword",
+    documentationUrl = "/pipeline/transforms/repeatfields.html")
+@Getter
+@Setter
+public class RepeatFieldsMeta extends BaseTransformMeta<RepeatFields, 
RepeatFieldsData> {
+  private static final Class<?> PKG = RepeatFields.class;
+
+  @HopMetadataProperty(
+      key = "group_field",
+      groupKey = "group_fields",
+      injectionGroupKey = "GROUP_FIELDS",
+      injectionGroupDescription = "RepeatFields.Injection.GroupFields",
+      injectionKey = "GROUP_FIELD",
+      injectionKeyDescription = "RepeatFields.Injection.GroupField")
+  private List<String> groupFields;
+
+  @HopMetadataProperty(
+      key = "field",
+      groupKey = "repeats",
+      injectionGroupKey = "REPEATS",
+      injectionGroupDescription = "RepeatFields.Injection.Repeats",
+      injectionKey = "REPEAT",
+      injectionKeyDescription = "RepeatFields.Injection.Repeat")
+  private List<Repeat> repeats;
+
+  public RepeatFieldsMeta() {
+    super();
+    this.groupFields = new ArrayList<>();
+    this.repeats = new ArrayList<>();
+  }
+
+  public RepeatFieldsMeta(RepeatFieldsMeta m) {
+    this();
+    this.groupFields.addAll(m.groupFields);
+    for (Repeat repeat : m.repeats) {
+      this.repeats.add(new Repeat(repeat));
+    }
+  }
+
+  @Override
+  public Object clone() {
+    return new RepeatFieldsMeta(this);
+  }
+
+  /**
+   * We only add an optional target field containing a corrected copy of the 
source field.
+   *
+   * @param inputRowMeta the input row meta that is modified in this method to 
reflect the output
+   *     row metadata of the transform
+   * @param name Name of the transform to use as input for the origin field in 
the values
+   * @param info Fields used as extra lookup information
+   * @param nextTransform the next transform that is targeted
+   * @param variables the variables The variable variables to use to replace 
variables
+   * @param metadataProvider the MetaStore to use to load additional external 
data or metadata
+   *     impacting the output fields
+   * @throws HopTransformException in case there is a missing source field
+   */
+  @Override
+  public void getFields(
+      IRowMeta inputRowMeta,
+      String name,
+      IRowMeta[] info,
+      TransformMeta nextTransform,
+      IVariables variables,
+      IHopMetadataProvider metadataProvider)
+      throws HopTransformException {
+    for (Repeat repeat : repeats) {
+      String targetFieldName = variables.resolve(repeat.getTargetField());
+      if (StringUtils.isNotEmpty(targetFieldName)) {
+        // We need to add a new field with the new name and the same data type 
as the source.
+        String sourceFieldName = variables.resolve(repeat.getSourceField());
+        IValueMeta sourceFieldValueMeta = 
inputRowMeta.searchValueMeta(sourceFieldName);
+        if (sourceFieldValueMeta == null) {
+          throw new HopTransformException(
+              "Unable to find source field "
+                  + sourceFieldName
+                  + " in the input of transform "
+                  + name);
+        }
+        IValueMeta targetFieldValueMeta = sourceFieldValueMeta.clone();
+        targetFieldValueMeta.setName(targetFieldName);
+        targetFieldValueMeta.setOrigin(name);
+        inputRowMeta.addValueMeta(targetFieldValueMeta);
+      }
+    }
+  }
+
+  @Getter
+  public enum RepeatType implements IEnumHasCodeAndDescription {
+    Previous("previous", BaseMessages.getString(PKG, 
"RepeatFields.Previous.Description")),
+    PreviousWhenNull(
+        "previous_when_null",
+        BaseMessages.getString(PKG, 
"RepeatFields.PreviousWhenNull.Description")),
+    CurrentWhenIndicated(
+        "current_when_indicated",
+        BaseMessages.getString(PKG, 
"RepeatFields.CurrentWhenIndicated.Description"));
+
+    private final String code;
+    private final String description;
+
+    RepeatType(String code, String description) {
+      this.code = code;
+      this.description = description;
+    }
+
+    public static RepeatType lookupDescription(String description) {
+      return IEnumHasCodeAndDescription.lookupDescription(RepeatType.class, 
description, null);
+    }
+
+    public static String[] getDescriptions() {
+      return IEnumHasCodeAndDescription.getDescriptions(RepeatType.class);
+    }
+  }
+}
diff --git 
a/plugins/transforms/repeatfields/src/main/resources/org/apache/hop/pipeline/transforms/repeatfields/messages/messages_en_US.properties
 
b/plugins/transforms/repeatfields/src/main/resources/org/apache/hop/pipeline/transforms/repeatfields/messages/messages_en_US.properties
new file mode 100644
index 0000000000..84643c30cf
--- /dev/null
+++ 
b/plugins/transforms/repeatfields/src/main/resources/org/apache/hop/pipeline/transforms/repeatfields/messages/messages_en_US.properties
@@ -0,0 +1,44 @@
+#
+# 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.
+#
+RepeatFields.Name=Repeat Fields
+RepeatFields.Description=Repeat field values from the previous row
+RepeatFields.keyword=Repeat,MDM
+RepeatFields.Injection.GroupFields=Group fields
+RepeatFields.Injection.GroupField=Group field
+RepeatFields.Injection.Repeats=Repeats
+RepeatFields.Injection.Repeat=Repeat
+RepeatFields.Previous.Description=Previous row
+RepeatFields.PreviousWhenNull.Description=Previous when field is null
+RepeatFields.CurrentWhenIndicated.Description=Current when indicated
+RepeatFieldsDialog.Shell.Title=Repeat Fields
+RepeatFieldsDialog.TransformName.Label=Transform name
+RepeatFieldsDialog.Group.Label=Group fields
+RepeatFieldsDialog.ColumnInfo.GroupField=Group field
+RepeatFieldsDialog.GetFields.Button=Get group fields
+RepeatFieldsDialog.RepeatFields.Label=The fields to repeat
+RepeatFieldsDialog.ColumnInfo.RepeatType=Repeat type
+RepeatFieldsDialog.ColumnInfo.SourceFieldName=Source field
+RepeatFieldsDialog.ColumnInfo.TargetFieldName=Target field
+RepeatFieldsDialog.ColumnInfo.IndicatorFieldName=Indicator field name
+RepeatFieldsDialog.ColumnInfo.IndicatorValue=Indicator value
+RepeatFieldsDialog.GetRepeatFields.Button=Get repeat fields
+RepeatFieldsDialog.SortWarningDialog.DialogMessage=If the incoming data is not 
sorted on the specified group field(s), \
+  the output results may not be correct.\nWe recommend sorting the incoming 
data within the pipeline, \
+  or in a source database.
+RepeatFieldsDialog.SortWarningDialog.DialogTitle=Notice
+RepeatFieldsDialog.SortWarningDialog.Option1=Close
+RepeatFieldsDialog.SortWarningDialog.Option2=Don''t show this message again.
diff --git 
a/plugins/transforms/repeatfields/src/main/resources/repeatfields.svg 
b/plugins/transforms/repeatfields/src/main/resources/repeatfields.svg
new file mode 100644
index 0000000000..daa970dc75
--- /dev/null
+++ b/plugins/transforms/repeatfields/src/main/resources/repeatfields.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg"; width="24" height="24">
+    <path fill="#0e3a5a" d="M11,17H4A2,2 0 0,1 2,15V3A2,2 0 0,1 
4,1H16V3H4V15H11V13L15,16L11,19V17M19,21V7H8V13H6V7A2,2 0 0,1 8,5H19A2,2 0 0,1 
21,7V21A2,2 0 0,1 19,23H8A2,2 0 0,1 6,21V19H8V21H19Z"/>
+</svg>
diff --git a/plugins/transforms/repeatfields/src/main/resources/version.xml 
b/plugins/transforms/repeatfields/src/main/resources/version.xml
new file mode 100644
index 0000000000..6be576acae
--- /dev/null
+++ b/plugins/transforms/repeatfields/src/main/resources/version.xml
@@ -0,0 +1,20 @@
+<?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.
+  ~
+  -->
+
+<version>${project.version}</version>
\ No newline at end of file

Reply via email to