This is an automated email from the ASF dual-hosted git repository.
bamaer 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 4d34b3713f Fix#8308 Add a Group By option to exclude rows from
aggregation based on a boolean field (#8309)
4d34b3713f is described below
commit 4d34b3713ffb9962e66df0360c57b457cc01d489
Author: Lance <[email protected]>
AuthorDate: Fri Sep 11 19:21:42 2026 +0800
Fix#8308 Add a Group By option to exclude rows from aggregation based on a
boolean field (#8309)
* Fix Add a Group By option to exclude rows from aggregation based on a
boolean field
Signed-off-by: lance <[email protected]>
* Fix Add a Group By option to exclude rows from aggregation based on a
boolean field
Signed-off-by: lance <[email protected]>
---------
Signed-off-by: lance <[email protected]>
---
.../ROOT/pages/pipeline/transforms/groupby.adoc | 5 +
.../transforms/0006-groupby-ignore-aggregate.hpl | 283 +++++++++++++++
.../datasets/golden-groupby-ignore-aggregate.csv | 3 +
integration-tests/transforms/main-0006-groupby.hwf | 9 +-
.../dataset/golden-groupby-ignore-aggregate.json | 32 ++
.../0006-groupby-ignore-aggregate UNIT.json | 36 ++
.../pipeline/transforms/groupby/Aggregation.java | 115 +-----
.../hop/pipeline/transforms/groupby/GroupBy.java | 113 +++++-
.../pipeline/transforms/groupby/GroupByData.java | 9 +
.../pipeline/transforms/groupby/GroupByDialog.java | 83 ++++-
.../pipeline/transforms/groupby/GroupByMeta.java | 191 +++-------
.../pipeline/transforms/groupby/GroupingField.java | 32 +-
.../groupby/messages/messages_en_US.properties | 12 +
.../groupby/messages/messages_zh_CN.properties | 12 +
.../transforms/groupby/AggregationTest.java | 107 ++++++
.../transforms/{ => groupby}/GroupByMetaTest.java | 126 ++++++-
.../pipeline/transforms/groupby/GroupByTest.java | 344 ++++++++++++++++++
.../transforms/groupby/GroupingFieldTest.java | 51 +++
.../transforms/groupby/IgnoreAggregateTest.java | 385 +++++++++++++++++++++
19 files changed, 1643 insertions(+), 305 deletions(-)
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/groupby.adoc
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/groupby.adoc
index b7e2e83b59..5555e9c096 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/groupby.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/groupby.adoc
@@ -67,6 +67,11 @@ You must specify the name of the flag field in that case
(the type is boolean).
|Always give back a row|If you enable this option, the Group By transform will
always give back a result row, even if there is no input row.
This can be useful if you want to count the number of rows.
Without this option you would never get a count of zero (0).
+|Ignore rows for aggregation|Enable to exclude rows from aggregation when the
selected boolean field is `true`.
+Ignored rows still participate in group detection.
+When *Include all rows* is enabled, ignored rows are still written to the
output and receive the group aggregate values, but they do not contribute to
cumulative sum, cumulative average, or moving average.
+|Ignore field name|Name of the boolean field that marks rows to exclude from
aggregation.
+This field is required when *Ignore rows for aggregation* is enabled.
|Group fields table|Specify the fields over which you want to group.
Click Get Fields to add all fields from the input stream(s).
|Aggregates table a|Specify the fields that must be aggregated, the method and
the name of the resulting new field.
diff --git a/integration-tests/transforms/0006-groupby-ignore-aggregate.hpl
b/integration-tests/transforms/0006-groupby-ignore-aggregate.hpl
new file mode 100644
index 0000000000..e2ba91e63e
--- /dev/null
+++ b/integration-tests/transforms/0006-groupby-ignore-aggregate.hpl
@@ -0,0 +1,283 @@
+<?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>0006-groupby-ignore-aggregate</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Verify Group By ignore_aggregate / field_ignore excludes rows
from aggregation</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>2026/03/25 00:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/03/25 00:00:00.000</modified_date>
+ <key_for_session_key>H4sIAAAAAAAAAAMAAAAAAAAAAAA=</key_for_session_key>
+ <is_key_private>N</is_key_private>
+ </info>
+ <notepads>
+ <notepad>
+ <note>skip=true rows are excluded from SUM/COUNT.
+Expect sum_amount=60, cnt=3 (not 160/4).</note>
+ <xloc>48</xloc>
+ <yloc>16</yloc>
+ <width>360</width>
+ <heigth>58</heigth>
+ <fontname>Segoe UI</fontname>
+ <fontsize>9</fontsize>
+ <fontbold>N</fontbold>
+ <fontitalic>N</fontitalic>
+ <fontcolorred>14</fontcolorred>
+ <fontcolorgreen>58</fontcolorgreen>
+ <fontcolorblue>90</fontcolorblue>
+ <backgroundcolorred>201</backgroundcolorred>
+ <backgroundcolorgreen>232</backgroundcolorgreen>
+ <backgroundcolorblue>251</backgroundcolorblue>
+ <bordercolorred>14</bordercolorred>
+ <bordercolorgreen>58</bordercolorgreen>
+ <bordercolorblue>90</bordercolorblue>
+ </notepad>
+ </notepads>
+ <order>
+ <hop>
+ <from>input</from>
+ <to>Group by</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Group by</from>
+ <to>numbers to string</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>numbers to string</from>
+ <to>Validate</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>input</name>
+ <type>DataGrid</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <name>grp</name>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ <field>
+ <name>amount</name>
+ <type>Number</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ <field>
+ <name>skip</name>
+ <type>Boolean</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ </fields>
+ <data>
+ <line>
+ <item>A</item>
+ <item>10</item>
+ <item>false</item>
+ </line>
+ <line>
+ <item>A</item>
+ <item>20</item>
+ <item>false</item>
+ </line>
+ <line>
+ <item>A</item>
+ <item>30</item>
+ <item>false</item>
+ </line>
+ <line>
+ <item>A</item>
+ <item>100</item>
+ <item>true</item>
+ </line>
+ <line>
+ <item>B</item>
+ <item>5</item>
+ <item>false</item>
+ </line>
+ <line>
+ <item>B</item>
+ <item>50</item>
+ <item>true</item>
+ </line>
+ </data>
+ <attributes/>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>144</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Group by</name>
+ <type>GroupBy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <all_rows>N</all_rows>
+ <ignore_aggregate>Y</ignore_aggregate>
+ <field_ignore>skip</field_ignore>
+ <directory>${java.io.tmpdir}</directory>
+ <prefix>grp</prefix>
+ <add_linenr>N</add_linenr>
+ <linenr_fieldname/>
+ <give_back_row>N</give_back_row>
+ <group>
+ <field>
+ <name>grp</name>
+ </field>
+ </group>
+ <fields>
+ <field>
+ <aggregate>sum_amount</aggregate>
+ <subject>amount</subject>
+ <type>SUM</type>
+ <valuefield/>
+ </field>
+ <field>
+ <aggregate>cnt</aggregate>
+ <subject>amount</subject>
+ <type>COUNT_ALL</type>
+ <valuefield/>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>320</xloc>
+ <yloc>144</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>numbers to string</name>
+ <type>SelectValues</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field_rename/>
+ <select_unspecified>N</select_unspecified>
+ <meta>
+ <name>sum_amount</name>
+ <rename>sum_amount</rename>
+ <type>String</type>
+ <length>-2</length>
+ <precision>-2</precision>
+ <storage_type/>
+ <conversion_mask>0.0;-0.0</conversion_mask>
+ <date_format_lenient>N</date_format_lenient>
+ <date_format_locale/>
+ <date_format_timezone/>
+ <lenient_string_to_number>N</lenient_string_to_number>
+ <decimal_symbol>.</decimal_symbol>
+ <grouping_symbol/>
+ <currency_symbol/>
+ <encoding/>
+ </meta>
+ <meta>
+ <name>cnt</name>
+ <rename>cnt</rename>
+ <type>String</type>
+ <length>-2</length>
+ <precision>-2</precision>
+ <storage_type/>
+ <conversion_mask>0;-0</conversion_mask>
+ <date_format_lenient>N</date_format_lenient>
+ <date_format_locale/>
+ <date_format_timezone/>
+ <lenient_string_to_number>N</lenient_string_to_number>
+ <decimal_symbol>.</decimal_symbol>
+ <grouping_symbol/>
+ <currency_symbol/>
+ <encoding/>
+ </meta>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>512</xloc>
+ <yloc>144</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Validate</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>704</xloc>
+ <yloc>144</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git
a/integration-tests/transforms/datasets/golden-groupby-ignore-aggregate.csv
b/integration-tests/transforms/datasets/golden-groupby-ignore-aggregate.csv
new file mode 100644
index 0000000000..e2937b95c0
--- /dev/null
+++ b/integration-tests/transforms/datasets/golden-groupby-ignore-aggregate.csv
@@ -0,0 +1,3 @@
+grp,sum_amount,cnt
+A,60.0,3
+B,5.0,1
diff --git a/integration-tests/transforms/main-0006-groupby.hwf
b/integration-tests/transforms/main-0006-groupby.hwf
index b933d71d93..138c2606e1 100644
--- a/integration-tests/transforms/main-0006-groupby.hwf
+++ b/integration-tests/transforms/main-0006-groupby.hwf
@@ -26,6 +26,8 @@ limitations under the License.
<modified_user>-</modified_user>
<created_date>2021/04/19 12:05:51.497</created_date>
<modified_date>2021/04/19 12:05:51.497</modified_date>
+ <created_hop_version/>
+ <modified_hop_version>2.20.0-SNAPSHOT</modified_hop_version>
<workflow_version/>
<parameters/>
<actions>
@@ -65,12 +67,15 @@ limitations under the License.
<test_name>
<name>0006-groupby-moving-average UNIT</name>
</test_name>
+ <test_name>
+ <name>0006-groupby-ignore-aggregate UNIT</name>
+ </test_name>
</test_names>
<name>Run Group By tests</name>
<description/>
<type>RunPipelineTests</type>
<attributes/>
- <xloc>768</xloc>
+ <xloc>864</xloc>
<yloc>80</yloc>
<parallel>N</parallel>
<attributes_hac/>
@@ -126,7 +131,7 @@ limitations under the License.
<description/>
<type>PIPELINE</type>
<attributes/>
- <xloc>544</xloc>
+ <xloc>592</xloc>
<yloc>80</yloc>
<parallel>N</parallel>
<attributes_hac/>
diff --git
a/integration-tests/transforms/metadata/dataset/golden-groupby-ignore-aggregate.json
b/integration-tests/transforms/metadata/dataset/golden-groupby-ignore-aggregate.json
new file mode 100644
index 0000000000..eede402023
--- /dev/null
+++
b/integration-tests/transforms/metadata/dataset/golden-groupby-ignore-aggregate.json
@@ -0,0 +1,32 @@
+{
+ "base_filename": "golden-groupby-ignore-aggregate.csv",
+ "name": "golden-groupby-ignore-aggregate",
+ "description": "Expected aggregates when ignore_aggregate skips
boolean-flagged rows",
+ "dataset_fields": [
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_format": "",
+ "field_name": "grp"
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_format": "",
+ "field_name": "sum_amount"
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_format": "",
+ "field_name": "cnt"
+ }
+ ],
+ "folder_name": ""
+}
diff --git
a/integration-tests/transforms/metadata/unit-test/0006-groupby-ignore-aggregate
UNIT.json
b/integration-tests/transforms/metadata/unit-test/0006-groupby-ignore-aggregate
UNIT.json
new file mode 100644
index 0000000000..33d9f309d7
--- /dev/null
+++
b/integration-tests/transforms/metadata/unit-test/0006-groupby-ignore-aggregate
UNIT.json
@@ -0,0 +1,36 @@
+{
+ "variableValues": [],
+ "database_replacements": [],
+ "autoOpening": true,
+ "basePath": "",
+ "golden_data_sets": [
+ {
+ "field_mappings": [
+ {
+ "transform_field": "grp",
+ "data_set_field": "grp"
+ },
+ {
+ "transform_field": "sum_amount",
+ "data_set_field": "sum_amount"
+ },
+ {
+ "transform_field": "cnt",
+ "data_set_field": "cnt"
+ }
+ ],
+ "field_order": [
+ "grp"
+ ],
+ "transform_name": "Validate",
+ "data_set_name": "golden-groupby-ignore-aggregate"
+ }
+ ],
+ "input_data_sets": [],
+ "name": "0006-groupby-ignore-aggregate UNIT",
+ "description": "Group By ignores rows where the configured boolean field is
true",
+ "trans_test_tweaks": [],
+ "persist_filename": "",
+ "pipeline_filename": "./0006-groupby-ignore-aggregate.hpl",
+ "test_type": "UNIT_TEST"
+}
diff --git
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/Aggregation.java
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/Aggregation.java
index ac8e7f2c0b..3e2cfc557f 100644
---
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/Aggregation.java
+++
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/Aggregation.java
@@ -13,15 +13,21 @@
* 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.groupby;
-import java.util.Objects;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.metadata.api.HopMetadataProperty;
+@Getter
+@Setter
+@NoArgsConstructor
+@EqualsAndHashCode
public class Aggregation implements Cloneable {
private static final Class<?> PKG = Aggregation.class;
@@ -160,8 +166,6 @@ public class Aggregation implements Cloneable {
injectionKeyDescription = "GroupByMeta.Injection.AGG_ORDER_FIELD")
private String orderField;
- public Aggregation() {}
-
public Aggregation(String field, String subject, String typeDesc, String
value) {
this.field = field;
this.subject = subject;
@@ -186,113 +190,12 @@ public class Aggregation implements Cloneable {
orderField);
}
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Aggregation that = (Aggregation) o;
- return type == that.type
- && Objects.equals(field, that.field)
- && Objects.equals(subject, that.subject)
- && Objects.equals(typeLabel, that.typeLabel)
- && Objects.equals(value, that.value)
- && Objects.equals(orderField, that.orderField);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(field, subject, typeLabel, type, value, orderField);
- }
-
- /**
- * Gets field
- *
- * @return value of field
- */
- public String getField() {
- return field;
- }
-
- /**
- * @param field The field to set
- */
- public void setField(String field) {
- this.field = field;
- }
-
- /**
- * Gets subject
- *
- * @return value of subject
- */
- public String getSubject() {
- return subject;
- }
-
- /**
- * @param subject The subject to set
- */
- public void setSubject(String subject) {
- this.subject = subject;
- }
-
- /**
- * Gets type
- *
- * @return value of type
- */
- public String getTypeLabel() {
- return typeLabel;
- }
-
+ /** Keep typeLabel and the numeric type code in sync. */
public void setTypeLabel(String typeCode) {
this.typeLabel = typeCode;
this.type = getTypeCodeFromLabel(typeCode);
}
- /**
- * Gets value
- *
- * @return value of value
- */
- public String getValue() {
- return value;
- }
-
- /**
- * @param value The value to set
- */
- public void setValue(String value) {
- this.value = value;
- }
-
- /**
- * Gets orderField
- *
- * @return value of orderField
- */
- public String getOrderField() {
- return orderField;
- }
-
- /**
- * @param orderField The orderField to set
- */
- public void setOrderField(String orderField) {
- this.orderField = orderField;
- }
-
- public int getType() {
- return type;
- }
-
- /**
- * @param type The type to set
- */
- public void setType(int type) {
- this.type = type;
- }
-
public static final int getTypeCodeFromLongDesc(String desc) {
for (int i = 0; i < typeGroupLongDesc.length; i++) {
if (typeGroupLongDesc[i].equalsIgnoreCase(desc)) {
diff --git
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupBy.java
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupBy.java
index 02fa2b2046..59ea72612c 100644
---
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupBy.java
+++
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupBy.java
@@ -206,6 +206,35 @@ public class GroupBy extends BaseTransform<GroupByMeta,
GroupByData> {
}
}
+ data.aggregateIgnoredFieldIndex = -1;
+ if (meta.isAggregateIgnored()) {
+ if (Utils.isEmpty(meta.getAggregateIgnoredField())) {
+ logError(BaseMessages.getString(PKG,
"GroupBy.Log.AggregateIgnoredFieldMissing"));
+ setErrors(1);
+ stopAll();
+ return false;
+ }
+ String ignoreField = resolve(meta.getAggregateIgnoredField());
+ data.aggregateIgnoredFieldIndex =
data.inputRowMeta.indexOfValue(ignoreField);
+ if (data.aggregateIgnoredFieldIndex < 0) {
+ logError(
+ BaseMessages.getString(
+ PKG, "GroupBy.Log.AggregateIgnoredFieldCouldNotFound",
ignoreField));
+ setErrors(1);
+ stopAll();
+ return false;
+ }
+ IValueMeta ignoreMeta =
data.inputRowMeta.getValueMeta(data.aggregateIgnoredFieldIndex);
+ if (!ignoreMeta.isBoolean()) {
+ logError(
+ BaseMessages.getString(
+ PKG, "GroupBy.Log.AggregateIgnoredFieldNotBoolean",
ignoreField));
+ setErrors(1);
+ stopAll();
+ return false;
+ }
+ }
+
// Create a metadata value for the counter Integers
//
data.valueMetaInteger = new ValueMetaInteger("count");
@@ -242,7 +271,9 @@ public class GroupBy extends BaseTransform<GroupByMeta,
GroupByData> {
data.previous = data.inputRowMeta.cloneRow(r); // copy the row to
previous
} else {
- calcAggregate(data.previous);
+ if (!isRowAggregateIgnored(data.previous)) {
+ calcAggregate(data.previous);
+ }
if (meta.isPassAllRows()) {
addToBuffer(data.previous);
@@ -302,7 +333,9 @@ public class GroupBy extends BaseTransform<GroupByMeta,
GroupByData> {
// ALL ROWS
if (data.previous != null) {
- calcAggregate(data.previous);
+ if (!isRowAggregateIgnored(data.previous)) {
+ calcAggregate(data.previous);
+ }
addToBuffer(data.previous);
}
data.groupResult = getAggregateResult();
@@ -334,7 +367,9 @@ public class GroupBy extends BaseTransform<GroupByMeta,
GroupByData> {
// Don't forget the last set of rows...
if (data.previous != null) {
- calcAggregate(data.previous);
+ if (!isRowAggregateIgnored(data.previous)) {
+ calcAggregate(data.previous);
+ }
}
Object[] result = buildResult(data.previous);
if (result != null) {
@@ -344,6 +379,12 @@ public class GroupBy extends BaseTransform<GroupByMeta,
GroupByData> {
}
private void addCumulativeSums(Object[] row) throws HopValueException {
+ if (isRowAggregateIgnored(row)) {
+ for (int i = 0; i < data.cumulativeSumSourceIndexes.size(); i++) {
+ row[data.cumulativeSumTargetIndexes.get(i)] = data.previousSums[i];
+ }
+ return;
+ }
// We need to adjust this row with cumulative averages?
//
@@ -377,6 +418,25 @@ public class GroupBy extends BaseTransform<GroupByMeta,
GroupByData> {
}
private void addCumulativeAverages(Object[] row) throws HopValueException {
+ if (isRowAggregateIgnored(row)) {
+ for (int i = 0; i < data.cumulativeAvgSourceIndexes.size(); i++) {
+ int targetIndex = data.cumulativeAvgTargetIndexes.get(i);
+ IValueMeta targetMeta = data.outputRowMeta.getValueMeta(targetIndex);
+ Object sum = data.previousAvgSum[i];
+ if (sum == null || data.previousAvgCount[i] == 0L) {
+ row[targetIndex] = null;
+ } else if (data.inputRowMeta
+ .getValueMeta(data.cumulativeAvgSourceIndexes.get(i))
+ .isInteger()) {
+ row[targetIndex] = ((Long) sum).doubleValue() /
data.previousAvgCount[i];
+ } else {
+ row[targetIndex] =
+ ValueDataUtil.divide(
+ targetMeta, sum, data.valueMetaInteger,
data.previousAvgCount[i]);
+ }
+ }
+ return;
+ }
// We need to adjust this row with cumulative sums
//
@@ -442,6 +502,20 @@ public class GroupBy extends BaseTransform<GroupByMeta,
GroupByData> {
int windowSize = data.movingAvgWidths.get(i);
int aggIndex = data.movingAvgIndexes.get(i);
+ if (isRowAggregateIgnored(row)) {
+ java.util.ArrayDeque<Double> window = data.movingAvgWindows[aggIndex];
+ if (window != null && window.size() == windowSize) {
+ double sum = 0.0;
+ for (double val : window) {
+ sum += val;
+ }
+ row[targetIndex] = sum / windowSize;
+ } else {
+ row[targetIndex] = null;
+ }
+ continue;
+ }
+
Object sourceValue = row[sourceIndex];
IValueMeta sourceMeta = data.inputRowMeta.getValueMeta(sourceIndex);
@@ -471,6 +545,18 @@ public class GroupBy extends BaseTransform<GroupByMeta,
GroupByData> {
return data.inputRowMeta.compare(previous, r, data.groupnrs) == 0;
}
+ /**
+ * Returns true when the row should be excluded from aggregation because
{@code ignore_aggregate}
+ * is enabled and the configured boolean field is true.
+ */
+ boolean isRowAggregateIgnored(Object[] row) throws HopValueException {
+ if (data.aggregateIgnoredFieldIndex < 0 || row == null) {
+ return false;
+ }
+ Boolean ignore = data.inputRowMeta.getBoolean(row,
data.aggregateIgnoredFieldIndex);
+ return ignore != null && ignore;
+ }
+
/**
* used for junits in GroupByAggregationNullsTest
*
@@ -593,10 +679,13 @@ public class GroupBy extends BaseTransform<GroupByMeta,
GroupByData> {
}
break;
case Aggregation.TYPE_GROUP_FIRST_INCL_NULL:
- // This is on purpose. The calculation of the
- // first field is done when setting up a new group
- // This is just the field of the first row
- // if (linesWritten==0) value.setValue(subj)
+ // First non-ignored row of the group, including a null subject
value.
+ if (data.firstInclNullSet == null || !data.firstInclNullSet[i]) {
+ data.agg[i] = subj;
+ if (data.firstInclNullSet != null) {
+ data.firstInclNullSet[i] = true;
+ }
+ }
break;
case Aggregation.TYPE_GROUP_LAST_INCL_NULL:
data.agg[i] = subj;
@@ -661,6 +750,9 @@ public class GroupBy extends BaseTransform<GroupByMeta,
GroupByData> {
data.agg = new Object[data.subjectnrs.length];
data.mean = new double[data.subjectnrs.length]; // sets all doubles to 0.0
data.aggMeta = new RowMeta();
+ data.firstInclNullSet = new boolean[data.subjectnrs.length];
+
+ boolean seedFromRow = r != null && !isRowAggregateIgnored(r);
for (int i = 0; i < data.subjectnrs.length; i++) {
Aggregation aggregation = meta.getAggregations().get(i);
@@ -708,7 +800,12 @@ public class GroupBy extends BaseTransform<GroupByMeta,
GroupByData> {
Aggregation.TYPE_GROUP_MAX:
vMeta = subjMeta.clone();
vMeta.setName(fieldName);
- v = r == null ? null : r[data.subjectnrs[i]];
+ // Do not seed from an ignored row; leave null so calcAggregate
initializes
+ // from the first non-ignored row of the group.
+ v = seedFromRow ? r[data.subjectnrs[i]] : null;
+ if (aggType == Aggregation.TYPE_GROUP_FIRST_INCL_NULL) {
+ data.firstInclNullSet[i] = seedFromRow;
+ }
break;
case Aggregation.TYPE_GROUP_CONCAT_STRING_CRLF,
Aggregation.TYPE_GROUP_CONCAT_COMMA:
vMeta = new ValueMetaString(fieldName);
diff --git
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByData.java
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByData.java
index f800d681bc..fc1461b81a 100644
---
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByData.java
+++
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByData.java
@@ -43,6 +43,15 @@ public class GroupByData extends BaseTransformData
implements ITransformData {
public IRowMeta groupAggMeta; // for speed: groupMeta+aggMeta
public int[] groupnrs;
+ /** Index of the boolean field that marks rows to exclude from aggregation,
or -1 when unused. */
+ public int aggregateIgnoredFieldIndex = -1;
+
+ /**
+ * Per-aggregation flag for {@code FIRST_INCL_NULL}: true once the first
non-ignored row of the
+ * current group has been applied.
+ */
+ public boolean[] firstInclNullSet;
+
/**
* array, length is equal to aggMeta value meta list size and metadata
subject fields length.
* Values corresponds to input values used to calculate target results.
diff --git
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByDialog.java
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByDialog.java
index 79755122ec..a66991c48f 100644
---
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByDialog.java
+++
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByDialog.java
@@ -34,6 +34,7 @@ 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.ComboVar;
import org.apache.hop.ui.core.widget.NamingSchemeTypes;
import org.apache.hop.ui.core.widget.TableView;
import org.apache.hop.ui.core.widget.TextVar;
@@ -85,6 +86,14 @@ public class GroupByDialog extends BaseTransformDialog {
private Button wAlwaysAddResult;
+ private Label wlIgnoreAggregate;
+
+ private Button wIgnoreAggregate;
+
+ private Label wlIgnoreAggregateField;
+
+ private ComboVar wIgnoreAggregateField;
+
private GroupByMeta input;
private boolean backupAllRows;
@@ -261,12 +270,64 @@ public class GroupByDialog extends BaseTransformDialog {
}
});
+ // Ignore rows for aggregation when a boolean field is true
+ //
+ wlIgnoreAggregate = new Label(shell, SWT.RIGHT);
+ wlIgnoreAggregate.setText(BaseMessages.getString(PKG,
"GroupByDialog.IgnoreAggregate.Label"));
+ wlIgnoreAggregate.setToolTipText(
+ BaseMessages.getString(PKG, "GroupByDialog.IgnoreAggregate.ToolTip"));
+ PropsUi.setLook(wlIgnoreAggregate);
+ FormData fdlIgnoreAggregate = new FormData();
+ fdlIgnoreAggregate.left = new FormAttachment(0, 0);
+ fdlIgnoreAggregate.top = new FormAttachment(wAlwaysAddResult, margin);
+ fdlIgnoreAggregate.right = new FormAttachment(middle, -margin);
+ wlIgnoreAggregate.setLayoutData(fdlIgnoreAggregate);
+ wIgnoreAggregate = new Button(shell, SWT.CHECK);
+ wIgnoreAggregate.setToolTipText(
+ BaseMessages.getString(PKG, "GroupByDialog.IgnoreAggregate.ToolTip"));
+ PropsUi.setLook(wIgnoreAggregate);
+ FormData fdIgnoreAggregate = new FormData();
+ fdIgnoreAggregate.left = new FormAttachment(middle, 0);
+ fdIgnoreAggregate.top = new FormAttachment(wlIgnoreAggregate, 0,
SWT.CENTER);
+ fdIgnoreAggregate.right = new FormAttachment(100, 0);
+ wIgnoreAggregate.setLayoutData(fdIgnoreAggregate);
+ wIgnoreAggregate.addSelectionListener(
+ new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ input.setChanged();
+ setFlags();
+ }
+ });
+
+ wlIgnoreAggregateField = new Label(shell, SWT.RIGHT);
+ wlIgnoreAggregateField.setText(
+ BaseMessages.getString(PKG,
"GroupByDialog.IgnoreAggregateField.Label"));
+ wlIgnoreAggregateField.setToolTipText(
+ BaseMessages.getString(PKG,
"GroupByDialog.IgnoreAggregateField.ToolTip"));
+ PropsUi.setLook(wlIgnoreAggregateField);
+ FormData fdlIgnoreAggregateField = new FormData();
+ fdlIgnoreAggregateField.left = new FormAttachment(0, 0);
+ fdlIgnoreAggregateField.top = new FormAttachment(wIgnoreAggregate, margin);
+ fdlIgnoreAggregateField.right = new FormAttachment(middle, -margin);
+ wlIgnoreAggregateField.setLayoutData(fdlIgnoreAggregateField);
+ wIgnoreAggregateField = new ComboVar(variables, shell, SWT.SINGLE |
SWT.LEFT | SWT.BORDER);
+ wIgnoreAggregateField.setToolTipText(
+ BaseMessages.getString(PKG,
"GroupByDialog.IgnoreAggregateField.ToolTip"));
+ PropsUi.setLook(wIgnoreAggregateField);
+ wIgnoreAggregateField.addModifyListener(lsMod);
+ FormData fdIgnoreAggregateField = new FormData();
+ fdIgnoreAggregateField.left = new FormAttachment(middle, 0);
+ fdIgnoreAggregateField.top = new FormAttachment(wIgnoreAggregate, margin);
+ fdIgnoreAggregateField.right = new FormAttachment(100, 0);
+ wIgnoreAggregateField.setLayoutData(fdIgnoreAggregateField);
+
Label wlGroup = new Label(shell, SWT.NONE);
wlGroup.setText(BaseMessages.getString(PKG, "GroupByDialog.Group.Label"));
PropsUi.setLook(wlGroup);
FormData fdlGroup = new FormData();
fdlGroup.left = new FormAttachment(0, 0);
- fdlGroup.top = new FormAttachment(wAlwaysAddResult, margin);
+ fdlGroup.top = new FormAttachment(wIgnoreAggregateField, margin);
wlGroup.setLayoutData(fdlGroup);
int nrKeyCols = 1;
@@ -387,7 +448,15 @@ public class GroupByDialog extends BaseTransformDialog {
for (int i = 0; i < row.size(); i++) {
inputFields.add(row.getValueMeta(i).getName());
}
- setComboBoxes();
+ // ComboVar.setItems must run on the UI thread
+ shell
+ .getDisplay()
+ .asyncExec(
+ () -> {
+ if (!shell.isDisposed()) {
+ setComboBoxes();
+ }
+ });
} catch (HopException e) {
logError(BaseMessages.getString(PKG,
"System.Dialog.GetFieldsFailed.Message"));
}
@@ -421,6 +490,7 @@ public class GroupByDialog extends BaseTransformDialog {
ciKey[0].setComboValues(fieldNames);
ciReturn[1].setComboValues(fieldNames);
ciReturn[4].setComboValues(fieldNames);
+ wIgnoreAggregateField.setItems(fieldNames);
}
public void setFlags() {
@@ -434,6 +504,9 @@ public class GroupByDialog extends BaseTransformDialog {
wlLineNrField.setEnabled(wAllRows.getSelection() &&
wAddLineNr.getSelection());
wLineNrField.setEnabled(wAllRows.getSelection() &&
wAddLineNr.getSelection());
+
+ wlIgnoreAggregateField.setEnabled(wIgnoreAggregate.getSelection());
+ wIgnoreAggregateField.setEnabled(wIgnoreAggregate.getSelection());
}
/** Copy information from the meta-data input to the dialog fields. */
@@ -453,6 +526,10 @@ public class GroupByDialog extends BaseTransformDialog {
wLineNrField.setText(input.getLineNrInGroupField());
}
wAlwaysAddResult.setSelection(input.isAlwaysGivingBackOneRow());
+ wIgnoreAggregate.setSelection(input.isAggregateIgnored());
+ if (input.getAggregateIgnoredField() != null) {
+ wIgnoreAggregateField.setText(input.getAggregateIgnoredField());
+ }
if (input.getGroupingFields() != null) {
for (int i = 0; i < input.getGroupingFields().size(); i++) {
@@ -502,6 +579,8 @@ public class GroupByDialog extends BaseTransformDialog {
input.setLineNrInGroupField(wLineNrField.getText());
input.setAlwaysGivingBackOneRow(wAlwaysAddResult.getSelection());
input.setPassAllRows(wAllRows.getSelection());
+ input.setAggregateIgnored(wIgnoreAggregate.getSelection());
+ input.setAggregateIgnoredField(wIgnoreAggregateField.getText());
input.getGroupingFields().clear();
for (int i = 0; i < sizegroup; i++) {
diff --git
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByMeta.java
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByMeta.java
index c337443325..81b2b8d8f2 100644
---
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByMeta.java
+++
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByMeta.java
@@ -19,6 +19,8 @@ package org.apache.hop.pipeline.transforms.groupby;
import java.util.ArrayList;
import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
import org.apache.hop.core.CheckResult;
import org.apache.hop.core.ICheckResult;
import org.apache.hop.core.annotations.Transform;
@@ -38,6 +40,8 @@ import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.pipeline.transform.BaseTransformMeta;
import org.apache.hop.pipeline.transform.TransformMeta;
+@Getter
+@Setter
@Transform(
id = "GroupBy",
image = "groupby.svg",
@@ -72,15 +76,18 @@ public class GroupByMeta extends BaseTransformMeta<GroupBy,
GroupByData> {
injectionKeyDescription = "GroupByMeta.Injection.TEMP_FILE_PREFIX")
private String prefix;
- /** Indicate that some rows don't need to be considered : TODO: make work in
GUI & worker */
- @HopMetadataProperty(key = "ignore_aggregate")
+ /** When true, rows where the ignore field is true are excluded from
aggregation. */
+ @HopMetadataProperty(
+ key = "ignore_aggregate",
+ injectionKey = "IGNORE_AGGREGATE",
+ injectionKeyDescription = "GroupByMeta.Injection.IGNORE_AGGREGATE")
private boolean aggregateIgnored;
- /**
- * name of the boolean field that indicates we need to ignore the row :
TODO: make work in GUI &
- * worker
- */
- @HopMetadataProperty(key = "field_ignore")
+ /** Boolean field that marks rows to exclude from aggregation when
ignore_aggregate is enabled. */
+ @HopMetadataProperty(
+ key = "field_ignore",
+ injectionKey = "IGNORE_AGGREGATE_FIELD",
+ injectionKeyDescription = "GroupByMeta.Injection.IGNORE_AGGREGATE_FIELD")
private String aggregateIgnoredField;
/** Fields to group over */
@@ -125,62 +132,6 @@ public class GroupByMeta extends
BaseTransformMeta<GroupBy, GroupByData> {
aggregations = new ArrayList<>();
}
- /**
- * @return Returns the aggregateIgnored.
- */
- public boolean isAggregateIgnored() {
- return aggregateIgnored;
- }
-
- /**
- * @param aggregateIgnored The aggregateIgnored to set.
- */
- public void setAggregateIgnored(boolean aggregateIgnored) {
- this.aggregateIgnored = aggregateIgnored;
- }
-
- /**
- * @return Returns the aggregateIgnoredField.
- */
- public String getAggregateIgnoredField() {
- return aggregateIgnoredField;
- }
-
- /**
- * @param aggregateIgnoredField The aggregateIgnoredField to set.
- */
- public void setAggregateIgnoredField(String aggregateIgnoredField) {
- this.aggregateIgnoredField = aggregateIgnoredField;
- }
-
- /**
- * @return Returns the groupField.
- */
- public List<GroupingField> getGroupingFields() {
- return groupingFields;
- }
-
- /**
- * @param groupingFields The groupField to set.
- */
- public void setGroupingFields(List<GroupingField> groupingFields) {
- this.groupingFields = groupingFields;
- }
-
- /**
- * @return Returns the passAllRows.
- */
- public boolean isPassAllRows() {
- return passAllRows;
- }
-
- /**
- * @param passAllRows The passAllRows to set.
- */
- public void setPassAllRows(boolean passAllRows) {
- this.passAllRows = passAllRows;
- }
-
@Override
public void setDefault() {
directory = "${java.io.tmpdir}";
@@ -189,8 +140,6 @@ public class GroupByMeta extends BaseTransformMeta<GroupBy,
GroupByData> {
passAllRows = false;
aggregateIgnored = false;
aggregateIgnoredField = null;
-
- int sizeGroup = 0;
}
@Override
@@ -354,76 +303,34 @@ public class GroupByMeta extends
BaseTransformMeta<GroupBy, GroupByData> {
transformMeta);
remarks.add(cr);
}
- }
- /**
- * @return Returns the directory.
- */
- public String getDirectory() {
- return directory;
- }
-
- /**
- * @param directory The directory to set.
- */
- public void setDirectory(String directory) {
- this.directory = directory;
- }
-
- /**
- * @return Returns the prefix.
- */
- public String getPrefix() {
- return prefix;
- }
-
- /**
- * @param prefix The prefix to set.
- */
- public void setPrefix(String prefix) {
- this.prefix = prefix;
- }
-
- /**
- * @return the addingLineNrInGroup
- */
- public boolean isAddingLineNrInGroup() {
- return addingLineNrInGroup;
- }
-
- /**
- * @param addingLineNrInGroup the addingLineNrInGroup to set
- */
- public void setAddingLineNrInGroup(boolean addingLineNrInGroup) {
- this.addingLineNrInGroup = addingLineNrInGroup;
- }
-
- /**
- * @return the lineNrInGroupField
- */
- public String getLineNrInGroupField() {
- return lineNrInGroupField;
- }
-
- /**
- * @param lineNrInGroupField the lineNrInGroupField to set
- */
- public void setLineNrInGroupField(String lineNrInGroupField) {
- this.lineNrInGroupField = lineNrInGroupField;
- }
-
- /**
- * @return the alwaysGivingBackOneRow
- */
- public boolean isAlwaysGivingBackOneRow() {
- return alwaysGivingBackOneRow;
- }
-
- /**
- * @param alwaysGivingBackOneRow the alwaysGivingBackOneRow to set
- */
- public void setAlwaysGivingBackOneRow(boolean alwaysGivingBackOneRow) {
- this.alwaysGivingBackOneRow = alwaysGivingBackOneRow;
+ if (aggregateIgnored) {
+ if (Utils.isEmpty(aggregateIgnoredField)) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_ERROR,
+ BaseMessages.getString(PKG,
"GroupByMeta.CheckResult.IgnoreAggregateFieldMissing"),
+ transformMeta));
+ } else if (prev != null && !prev.isEmpty()) {
+ String ignoreField = variables.resolve(aggregateIgnoredField);
+ int idx = prev.indexOfValue(ignoreField);
+ if (idx < 0) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_ERROR,
+ BaseMessages.getString(
+ PKG,
"GroupByMeta.CheckResult.IgnoreAggregateFieldNotFound", ignoreField),
+ transformMeta));
+ } else if (!prev.getValueMeta(idx).isBoolean()) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_ERROR,
+ BaseMessages.getString(
+ PKG,
"GroupByMeta.CheckResult.IgnoreAggregateFieldNotBoolean", ignoreField),
+ transformMeta));
+ }
+ }
+ }
}
@Override
@@ -431,22 +338,6 @@ public class GroupByMeta extends
BaseTransformMeta<GroupBy, GroupByData> {
return new PipelineMeta.PipelineType[] {PipelineMeta.PipelineType.Normal};
}
- /**
- * Gets aggregations
- *
- * @return value of aggregations
- */
- public List<Aggregation> getAggregations() {
- return aggregations;
- }
-
- /**
- * @param aggregations The aggregations to set
- */
- public void setAggregations(List<Aggregation> aggregations) {
- this.aggregations = aggregations;
- }
-
@Override
public boolean supportsMultiCopyExecution() {
return false;
diff --git
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupingField.java
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupingField.java
index e94460737d..beaa78bef9 100644
---
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupingField.java
+++
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupingField.java
@@ -17,9 +17,16 @@
package org.apache.hop.pipeline.transforms.groupby;
-import java.util.Objects;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
import org.apache.hop.metadata.api.HopMetadataProperty;
+@Getter
+@Setter
+@NoArgsConstructor
+@EqualsAndHashCode
public class GroupingField implements Cloneable {
@HopMetadataProperty(
@@ -27,33 +34,10 @@ public class GroupingField implements Cloneable {
injectionKeyDescription = "GroupByMeta.Injection.GROUP_FIELD")
private String name;
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- public GroupingField() {}
-
public GroupingField(String name) {
this.name = name;
}
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- GroupingField that = (GroupingField) o;
- return Objects.equals(name, that.name);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(name);
- }
-
@Override
public GroupingField clone() {
return new GroupingField(name);
diff --git
a/plugins/transforms/groupby/src/main/resources/org/apache/hop/pipeline/transforms/groupby/messages/messages_en_US.properties
b/plugins/transforms/groupby/src/main/resources/org/apache/hop/pipeline/transforms/groupby/messages/messages_en_US.properties
index 64ba278c6c..32c8a5032a 100644
---
a/plugins/transforms/groupby/src/main/resources/org/apache/hop/pipeline/transforms/groupby/messages/messages_en_US.properties
+++
b/plugins/transforms/groupby/src/main/resources/org/apache/hop/pipeline/transforms/groupby/messages/messages_en_US.properties
@@ -22,6 +22,9 @@ GroupBy.Exception.UnableToDeleteTemporaryFile=Unable to
delete temporary file: {
GroupBy.Exception.UnableToReadBackRowFromTemporaryFile=Unable to read back row
from temporary file\!
GroupBy.LineNumber=Linenr
GroupBy.Log.AggregateSubjectFieldCouldNotFound=Aggregate subject field [{0}]
couldn''t be found\!
+GroupBy.Log.AggregateIgnoredFieldCouldNotFound=Aggregate ignore field [{0}]
couldn''t be found\!
+GroupBy.Log.AggregateIgnoredFieldMissing=Ignore rows for aggregation is
enabled but no ignore field name was specified\!
+GroupBy.Log.AggregateIgnoredFieldNotBoolean=Aggregate ignore field [{0}] must
be a Boolean type\!
GroupBy.Log.GroupFieldCouldNotFound=Grouping field [{0}] couldn''t be found\!
GroupBy.Name=Group by
GroupByDialog.AddLineNr.Label=Add line number, restart in each group
@@ -29,6 +32,10 @@ GroupByDialog.Aggregates.Label=Aggregates\:
GroupByDialog.AllRows.Label=Include all rows
GroupByDialog.AlwaysAddResult.Label=Always give back a result row
GroupByDialog.AlwaysAddResult.ToolTip=To make sure we always output a correct
count aggregation we always output at least one row, even if there were no
input rows.\nThis makes the behavior consistent with the aggregation in an SQL
GROUP BY.
+GroupByDialog.IgnoreAggregate.Label=Ignore rows for aggregation
+GroupByDialog.IgnoreAggregate.ToolTip=When enabled, rows where the selected
boolean field is true are excluded from aggregation calculations.\nThey still
participate in group detection. With ''Include all rows'', ignored rows are
still written to the output with the group aggregates.
+GroupByDialog.IgnoreAggregateField.Label=Ignore field name
+GroupByDialog.IgnoreAggregateField.ToolTip=Boolean field that marks rows to
exclude from aggregation when the ignore option is enabled.
GroupByDialog.Browse.Button=&Browse...
GroupByDialog.ColumnInfo.GroupField=Group field
GroupByDialog.ColumnInfo.Name=Name
@@ -55,6 +62,9 @@ GroupByDialog.TempDir.Label=Temporary files directory
GroupByDialog.TransformName.Label=Transform name
GroupByMeta.CheckResult.NoInputError=No input received from other transforms\!
GroupByMeta.CheckResult.ReceivingInfoOK=Transform is receiving info from other
transforms.
+GroupByMeta.CheckResult.IgnoreAggregateFieldMissing=Ignore rows for
aggregation is enabled but no ignore field name was specified.
+GroupByMeta.CheckResult.IgnoreAggregateFieldNotFound=Aggregate ignore field
[{0}] couldn''t be found in the input stream.
+GroupByMeta.CheckResult.IgnoreAggregateFieldNotBoolean=Aggregate ignore field
[{0}] must be a Boolean type.
GroupByMeta.Exception.UnableToLoadTransformMetaFromXML=Unable to load
transform info from XML
GroupByMeta.Injection.ADD_GROUP_LINENR=Add group line number?
GroupByMeta.Injection.ADD_GROUP_LINENR_FIELD=Group line number field
@@ -67,6 +77,8 @@ GroupByMeta.Injection.AGGREGATIONS=Aggregations
GroupByMeta.Injection.ALWAYS_GIVE_ROW=Always give back row?
GroupByMeta.Injection.GROUP_FIELD=Group field
GroupByMeta.Injection.GROUPS=Groups
+GroupByMeta.Injection.IGNORE_AGGREGATE=Ignore rows for aggregation?
+GroupByMeta.Injection.IGNORE_AGGREGATE_FIELD=Ignore aggregate field
GroupByMeta.Injection.PASS_ALL_ROWS=Pass all rows?
GroupByMeta.Injection.TEMP_DIRECTORY=Temporary directory
GroupByMeta.Injection.TEMP_FILE_PREFIX=Temporary file prefix
diff --git
a/plugins/transforms/groupby/src/main/resources/org/apache/hop/pipeline/transforms/groupby/messages/messages_zh_CN.properties
b/plugins/transforms/groupby/src/main/resources/org/apache/hop/pipeline/transforms/groupby/messages/messages_zh_CN.properties
index a0b08a9c0a..41bbf7f26d 100644
---
a/plugins/transforms/groupby/src/main/resources/org/apache/hop/pipeline/transforms/groupby/messages/messages_zh_CN.properties
+++
b/plugins/transforms/groupby/src/main/resources/org/apache/hop/pipeline/transforms/groupby/messages/messages_zh_CN.properties
@@ -24,6 +24,9 @@
GroupBy.Exception.UnableToDeleteTemporaryFile=\u65E0\u6CD5\u5220\u9664\u4E34\u67
GroupBy.Exception.UnableToReadBackRowFromTemporaryFile=Unable to read back row
from temporary file\!
GroupBy.LineNumber=\u884C\u53F7
GroupBy.Log.AggregateSubjectFieldCouldNotFound=Aggregate subject field [{0}]
couldn''t be found\!
+GroupBy.Log.AggregateIgnoredFieldCouldNotFound=\u805A\u5408\u5FFD\u7565\u5B57\u6BB5
[{0}] \u672A\u627E\u5230\!
+GroupBy.Log.AggregateIgnoredFieldMissing=\u5DF2\u542F\u7528\u201C\u805A\u5408\u65F6\u5FFD\u7565\u884C\u201D\uFF0C\u4F46\u672A\u6307\u5B9A\u5FFD\u7565\u5B57\u6BB5\u540D\!
+GroupBy.Log.AggregateIgnoredFieldNotBoolean=\u805A\u5408\u5FFD\u7565\u5B57\u6BB5
[{0}] \u5FC5\u987B\u662F\u5E03\u5C14\u7C7B\u578B\!
GroupBy.Log.GroupFieldCouldNotFound=Grouping field [{0}] couldn''t be found\!
GroupBy.Name=\u5206\u7EC4
GroupByDialog.AddLineNr.Label=\u589E\u52A0\u884C\u53F7,\u6BCF\u7EC4\u91CD\u65B0\u5F00\u59CB\:
@@ -31,6 +34,10 @@ GroupByDialog.Aggregates.Label=\u805A\u5408 :
GroupByDialog.AllRows.Label=\u5305\u62EC\u6240\u6709\u7684\u884C:
GroupByDialog.AlwaysAddResult.Label=\u603B\u8FD4\u56DE\u4E00\u4E2A\u7ED3\u679C\u884C\:
GroupByDialog.AlwaysAddResult.ToolTip=\u5373\u4F7F\u5728\u6CA1\u6709\u8F93\u5165\u884C\u7684\u60C5\u51B5\u4E0B,\u4E5F\u81F3\u5C11\u751F\u6210\u4E00\u884C,\u8FD9\u6837\u5728\u8BA1\u6570\u805A\u96C6\u7684\u60C5\u51B5\u4E0B,\u53EF\u4EE5\u83B7\u5F97\u6B63\u786E\u7ED3\u679C.\r\n\u8BE5\u65B9\u5F0F\u548C
SQL \u8BED\u53E5\u7684 Group by \u7ED3\u679C\u4E00\u81F4.\r\n
+GroupByDialog.IgnoreAggregate.Label=\u805A\u5408\u65F6\u5FFD\u7565\u884C\:
+GroupByDialog.IgnoreAggregate.ToolTip=\u542F\u7528\u540E\uFF0C\u9009\u5B9A\u5E03\u5C14\u5B57\u6BB5\u4E3A
true
\u7684\u884C\u4E0D\u53C2\u4E0E\u805A\u5408\u8BA1\u7B97\u3002\u8BE5\u884C\u4ECD\u4F1A\u53C2\u4E0E\u5206\u7EC4\u5224\u65AD\u3002\u82E5\u542F\u7528\u201C\u5305\u62EC\u6240\u6709\u7684\u884C\u201D\uFF0C\u88AB\u5FFD\u7565\u7684\u884C\u4ECD\u4F1A\u8F93\u51FA\u5E76\u9644\u5E26\u7EC4\u805A\u5408\u7ED3\u679C\u3002
+GroupByDialog.IgnoreAggregateField.Label=\u5FFD\u7565\u5B57\u6BB5\u540D\:
+GroupByDialog.IgnoreAggregateField.ToolTip=\u7528\u4E8E\u6807\u8BB0\u4E0D\u53C2\u4E0E\u805A\u5408\u7684\u884C\u7684\u5E03\u5C14\u5B57\u6BB5\u3002
GroupByDialog.Browse.Button=\u6D4F\u89C8(&B)...
GroupByDialog.ColumnInfo.GroupField=\u5206\u7EC4\u5B57\u6BB5
GroupByDialog.ColumnInfo.Name=\u540D\u79F0
@@ -55,6 +62,11 @@ GroupByDialog.TempDir.Label=\u6392\u5E8F\u76EE\u5F55\:
GroupByDialog.TransformName.Label=Transform \u540D\u79F0:
GroupByMeta.CheckResult.NoInputError=No input received from other transforms\!
GroupByMeta.CheckResult.ReceivingInfoOK=Transform is receiving info from other
transforms.
+GroupByMeta.CheckResult.IgnoreAggregateFieldMissing=\u5DF2\u542F\u7528\u201C\u805A\u5408\u65F6\u5FFD\u7565\u884C\u201D\uFF0C\u4F46\u672A\u6307\u5B9A\u5FFD\u7565\u5B57\u6BB5\u540D\u3002
+GroupByMeta.CheckResult.IgnoreAggregateFieldNotFound=\u5728\u8F93\u5165\u6D41\u4E2D\u672A\u627E\u5230\u805A\u5408\u5FFD\u7565\u5B57\u6BB5
[{0}]\u3002
+GroupByMeta.CheckResult.IgnoreAggregateFieldNotBoolean=\u805A\u5408\u5FFD\u7565\u5B57\u6BB5
[{0}] \u5FC5\u987B\u662F\u5E03\u5C14\u7C7B\u578B\u3002
+GroupByMeta.Injection.IGNORE_AGGREGATE=\u805A\u5408\u65F6\u662F\u5426\u5FFD\u7565\u884C\uFF1F
+GroupByMeta.Injection.IGNORE_AGGREGATE_FIELD=\u805A\u5408\u5FFD\u7565\u5B57\u6BB5
GroupByMeta.Exception.UnableToLoadTransformMetaFromXML=Unable to load
transform info from XML
GroupByMeta.keyword=groupby
GroupByMeta.TypeGroupLongDesc.AVERAGE=\u5E73\u5747
diff --git
a/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/groupby/AggregationTest.java
b/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/groupby/AggregationTest.java
new file mode 100644
index 0000000000..a4965be68d
--- /dev/null
+++
b/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/groupby/AggregationTest.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.groupby;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+
+import org.apache.hop.core.HopEnvironment;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+class AggregationTest {
+
+ @BeforeAll
+ static void setUpClass() throws Exception {
+ HopEnvironment.init();
+ }
+
+ @Test
+ void setTypeLabelKeepsNumericTypeInSync() {
+ Aggregation aggregation = new Aggregation();
+ aggregation.setTypeLabel("SUM");
+
+ assertEquals("SUM", aggregation.getTypeLabel());
+ assertEquals(Aggregation.TYPE_GROUP_SUM, aggregation.getType());
+
+ aggregation.setTypeLabel("COUNT_ALL");
+ assertEquals("COUNT_ALL", aggregation.getTypeLabel());
+ assertEquals(Aggregation.TYPE_GROUP_COUNT_ALL, aggregation.getType());
+ }
+
+ @Test
+ void constructorResolvesTypeFromLongDescription() {
+ Aggregation aggregation =
+ new Aggregation(
+ "sum_amount",
+ "amount",
+ Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_SUM),
+ null);
+
+ assertEquals("sum_amount", aggregation.getField());
+ assertEquals("amount", aggregation.getSubject());
+ assertEquals(Aggregation.TYPE_GROUP_SUM, aggregation.getType());
+ assertEquals("SUM", aggregation.getTypeLabel());
+ }
+
+ @Test
+ void cloneCreatesIndependentCopy() {
+ Aggregation original =
+ new Aggregation(
+ "moving_avg",
+ "amount",
+
Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_MOVING_AVERAGE),
+ "3",
+ "ts");
+
+ Aggregation copy = original.clone();
+
+ assertNotSame(original, copy);
+ assertEquals(original, copy);
+
+ copy.setField("other");
+ assertNotEquals(original.getField(), copy.getField());
+ }
+
+ @Test
+ void equalsAndHashCodeConsiderAllFields() {
+ Aggregation left =
+ new Aggregation(
+ "sum_amount",
+ "amount",
+ Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_SUM),
+ null);
+ Aggregation right =
+ new Aggregation(
+ "sum_amount",
+ "amount",
+ Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_SUM),
+ null);
+ Aggregation different =
+ new Aggregation(
+ "cnt",
+ "amount",
+
Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_COUNT_ALL),
+ null);
+
+ assertEquals(left, right);
+ assertEquals(left.hashCode(), right.hashCode());
+ assertNotEquals(left, different);
+ }
+}
diff --git
a/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/GroupByMetaTest.java
b/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/groupby/GroupByMetaTest.java
similarity index 53%
rename from
plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/GroupByMetaTest.java
rename to
plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/groupby/GroupByMetaTest.java
index 5c7ef9c666..98bf9e6128 100644
---
a/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/GroupByMetaTest.java
+++
b/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/groupby/GroupByMetaTest.java
@@ -13,26 +13,40 @@
* 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;
+package org.apache.hop.pipeline.transforms.groupby;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaBoolean;
+import org.apache.hop.core.row.value.ValueMetaNumber;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.core.variables.Variables;
import org.apache.hop.core.xml.XmlHandler;
import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
import org.apache.hop.metadata.serializer.xml.XmlMetadataUtil;
-import org.apache.hop.pipeline.transforms.groupby.Aggregation;
-import org.apache.hop.pipeline.transforms.groupby.GroupByMeta;
-import org.apache.hop.pipeline.transforms.groupby.GroupingField;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
class GroupByMetaTest {
+ @BeforeAll
+ static void setUpClass() throws Exception {
+ HopEnvironment.init();
+ }
+
@Test
- void testClone() throws Exception {
+ void testClone() {
GroupByMeta meta1 = generateTestMeta();
GroupByMeta meta2 = (GroupByMeta) meta1.clone();
@@ -53,24 +67,100 @@ class GroupByMetaTest {
compareMetas(meta1, meta2);
}
- public void compareMetas(GroupByMeta meta1, GroupByMeta meta2) {
+ @Test
+ void testSetDefault() {
+ GroupByMeta meta = new GroupByMeta();
+ meta.setDefault();
+
+ assertEquals("${java.io.tmpdir}", meta.getDirectory());
+ assertEquals("grp", meta.getPrefix());
+ assertFalse(meta.isPassAllRows());
+ assertFalse(meta.isAggregateIgnored());
+ assertNull(meta.getAggregateIgnoredField());
+ assertNotNull(meta.getGroupingFields());
+ assertNotNull(meta.getAggregations());
+ }
+
+ @Test
+ void testGetFieldsWithoutPassAllRows() {
+ GroupByMeta meta = new GroupByMeta();
+ meta.setDefault();
+ meta.getGroupingFields().add(new GroupingField("grp"));
+ meta.getAggregations()
+ .add(
+ new Aggregation(
+ "sum_amount",
+ "amount",
+
Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_SUM),
+ null));
+ meta.getAggregations()
+ .add(
+ new Aggregation(
+ "cnt",
+ "amount",
+
Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_COUNT_ALL),
+ null));
+
+ IRowMeta rowMeta = inputRowMeta();
+ meta.getFields(rowMeta, "Group by", null, null, new Variables(), null);
+
+ assertEquals(3, rowMeta.size());
+ assertEquals("grp", rowMeta.getValueMeta(0).getName());
+ assertEquals("sum_amount", rowMeta.getValueMeta(1).getName());
+ assertEquals(IValueMeta.TYPE_NUMBER, rowMeta.getValueMeta(1).getType());
+ assertEquals("cnt", rowMeta.getValueMeta(2).getName());
+ assertEquals(IValueMeta.TYPE_INTEGER, rowMeta.getValueMeta(2).getType());
+ }
+
+ @Test
+ void testGetFieldsWithPassAllRowsAndLineNr() {
+ GroupByMeta meta = new GroupByMeta();
+ meta.setDefault();
+ meta.setPassAllRows(true);
+ meta.setAddingLineNrInGroup(true);
+ meta.setLineNrInGroupField("linenr");
+ meta.getGroupingFields().add(new GroupingField("grp"));
+ meta.getAggregations()
+ .add(
+ new Aggregation(
+ "sum_amount",
+ "amount",
+
Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_SUM),
+ null));
+
+ IRowMeta rowMeta = inputRowMeta();
+ meta.getFields(rowMeta, "Group by", null, null, new Variables(), null);
+
+ assertTrue(rowMeta.indexOfValue("grp") >= 0);
+ assertTrue(rowMeta.indexOfValue("amount") >= 0);
+ assertTrue(rowMeta.indexOfValue("skip") >= 0);
+ assertTrue(rowMeta.indexOfValue("sum_amount") >= 0);
+ assertTrue(rowMeta.indexOfValue("linenr") >= 0);
+ assertEquals(IValueMeta.TYPE_INTEGER,
rowMeta.searchValueMeta("linenr").getType());
+ }
+
+ @Test
+ void testSupportsMultiCopyExecution() {
+ assertFalse(new GroupByMeta().supportsMultiCopyExecution());
+ }
+
+ private void compareMetas(GroupByMeta meta1, GroupByMeta meta2) {
assertEquals(meta1.getGroupingFields().size(),
meta2.getGroupingFields().size());
for (int i = 0; i < meta1.getGroupingFields().size(); i++) {
- GroupingField field1 = meta1.getGroupingFields().get(i);
- GroupingField field2 = meta2.getGroupingFields().get(i);
- assertEquals(field1, field2);
+ assertEquals(meta1.getGroupingFields().get(i),
meta2.getGroupingFields().get(i));
}
assertEquals(meta1.getAggregations().size(),
meta2.getAggregations().size());
for (int i = 0; i < meta1.getAggregations().size(); i++) {
- Aggregation agg1 = meta1.getAggregations().get(i);
- Aggregation agg2 = meta2.getAggregations().get(i);
- assertEquals(agg1, agg2);
+ assertEquals(meta1.getAggregations().get(i),
meta2.getAggregations().get(i));
}
assertEquals(meta1.isPassAllRows(), meta2.isPassAllRows());
assertEquals(meta1.isAddingLineNrInGroup(), meta2.isAddingLineNrInGroup());
assertEquals(meta1.getLineNrInGroupField(), meta2.getLineNrInGroupField());
assertEquals(meta1.getDirectory(), meta2.getDirectory());
assertEquals(meta1.getPrefix(), meta2.getPrefix());
+ assertEquals(meta1.isAlwaysGivingBackOneRow(),
meta2.isAlwaysGivingBackOneRow());
+ assertEquals(meta1.isAggregateIgnored(), meta2.isAggregateIgnored());
+ assertEquals(meta1.getAggregateIgnoredField(),
meta2.getAggregateIgnoredField());
}
private GroupByMeta generateTestMeta() {
@@ -108,9 +198,19 @@ class GroupByMetaTest {
meta.setPrefix("prefix");
meta.setAddingLineNrInGroup(true);
meta.setLineNrInGroupField("lineNr");
+ meta.setAggregateIgnored(true);
+ meta.setAggregateIgnoredField("skip");
return meta;
}
+ private IRowMeta inputRowMeta() {
+ IRowMeta rowMeta = new RowMeta();
+ rowMeta.addValueMeta(new ValueMetaString("grp"));
+ rowMeta.addValueMeta(new ValueMetaNumber("amount"));
+ rowMeta.addValueMeta(new ValueMetaBoolean("skip"));
+ return rowMeta;
+ }
+
private String getDesc(String label) {
int type = Aggregation.getTypeCodeFromLabel(label);
return Aggregation.getTypeDescLongFromCode(type);
diff --git
a/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/groupby/GroupByTest.java
b/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/groupby/GroupByTest.java
new file mode 100644
index 0000000000..7110ce1463
--- /dev/null
+++
b/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/groupby/GroupByTest.java
@@ -0,0 +1,344 @@
+/*
+ * 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.groupby;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.QueueRowSet;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaNumber;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.pipeline.transforms.mock.TransformMockHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for the {@link GroupBy} transform worker. */
+class GroupByTest {
+
+ private TransformMockHelper<GroupByMeta, GroupByData> mockHelper;
+ private GroupByMeta meta;
+ private GroupByData data;
+ private GroupBy groupBy;
+ private IRowMeta inputRowMeta;
+
+ @BeforeAll
+ static void setUpClass() throws HopException {
+ HopEnvironment.init();
+ }
+
+ @BeforeEach
+ void setUp() throws HopException {
+ mockHelper = new TransformMockHelper<>("Group By", GroupByMeta.class,
GroupByData.class);
+ when(mockHelper.logChannelFactory.create(any(), any(ILoggingObject.class)))
+ .thenReturn(mockHelper.iLogChannel);
+ when(mockHelper.pipeline.isRunning()).thenReturn(true);
+
+ meta = new GroupByMeta();
+ meta.setDefault();
+ meta.getGroupingFields().add(new GroupingField("grp"));
+ meta.getAggregations()
+ .add(
+ new Aggregation(
+ "sum_amount",
+ "amount",
+
Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_SUM),
+ null));
+ meta.getAggregations()
+ .add(
+ new Aggregation(
+ "cnt",
+ "amount",
+
Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_COUNT_ALL),
+ null));
+ meta.getAggregations()
+ .add(
+ new Aggregation(
+ "avg_amount",
+ "amount",
+
Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_AVERAGE),
+ null));
+
+ data = new GroupByData();
+ inputRowMeta = new RowMeta();
+ inputRowMeta.addValueMeta(new ValueMetaString("grp"));
+ inputRowMeta.addValueMeta(new ValueMetaNumber("amount"));
+
+ groupBy = createGroupBy(meta, data);
+ }
+
+ @AfterEach
+ void tearDown() {
+ mockHelper.cleanUp();
+ }
+
+ @Test
+ void initPreparesBuffer() {
+ assertTrue(groupBy.init());
+ assertNotNullBuffer();
+ }
+
+ @Test
+ void processRowAggregatesSingleGroup() throws Exception {
+ List<Object[]> rows =
+ runPipeline(new Object[] {"A", 10.0}, new Object[] {"A", 20.0}, new
Object[] {"A", 30.0});
+
+ assertEquals(1, rows.size());
+ Object[] result = rows.getFirst();
+ assertEquals("A", result[0]);
+ assertEquals(60.0, ((Number) result[1]).doubleValue(), 1e-9);
+ assertEquals(3L, ((Number) result[2]).longValue());
+ assertEquals(20.0, ((Number) result[3]).doubleValue(), 1e-9);
+ }
+
+ @Test
+ void processRowAggregatesMultipleGroups() throws Exception {
+ List<Object[]> rows =
+ runPipeline(
+ new Object[] {"A", 10.0},
+ new Object[] {"A", 20.0},
+ new Object[] {"B", 5.0},
+ new Object[] {"B", 15.0},
+ new Object[] {"C", 100.0});
+
+ assertEquals(3, rows.size());
+
+ assertEquals("A", rows.getFirst()[0]);
+ assertEquals(30.0, ((Number) rows.get(0)[1]).doubleValue(), 1e-9);
+ assertEquals(2L, ((Number) rows.get(0)[2]).longValue());
+
+ assertEquals("B", rows.get(1)[0]);
+ assertEquals(20.0, ((Number) rows.get(1)[1]).doubleValue(), 1e-9);
+ assertEquals(2L, ((Number) rows.get(1)[2]).longValue());
+
+ assertEquals("C", rows.get(2)[0]);
+ assertEquals(100.0, ((Number) rows.get(2)[1]).doubleValue(), 1e-9);
+ assertEquals(1L, ((Number) rows.get(2)[2]).longValue());
+ }
+
+ @Test
+ void processRowWithNoInputAndAlwaysGiveBackRow() throws Exception {
+ meta.setAlwaysGivingBackOneRow(true);
+ meta.getGroupingFields().clear();
+ meta.getAggregations().clear();
+ meta.getAggregations()
+ .add(
+ new Aggregation(
+ "cnt",
+ "amount",
+
Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_COUNT_ANY),
+ null));
+
+ data = new GroupByData();
+ groupBy = createGroupBy(meta, data);
+
+ QueueRowSet input = new QueueRowSet();
+ input.setDone();
+ groupBy.addRowSetToInputRowSets(input);
+ groupBy.setInputRowMeta(inputRowMeta);
+
+ QueueRowSet output = new QueueRowSet();
+ groupBy.addRowSetToOutputRowSets(output);
+
+ assertFalse(groupBy.processRow());
+
+ List<Object[]> rows = drain(output);
+ assertEquals(1, rows.size());
+ assertEquals(0L, ((Number) rows.getFirst()[0]).longValue());
+ }
+
+ @Test
+ void processRowWithNoInputAndNoAlwaysGiveBack() throws Exception {
+ meta.setAlwaysGivingBackOneRow(false);
+
+ QueueRowSet input = new QueueRowSet();
+ input.setDone();
+ groupBy.addRowSetToInputRowSets(input);
+
+ QueueRowSet output = new QueueRowSet();
+ groupBy.addRowSetToOutputRowSets(output);
+
+ assertFalse(groupBy.processRow());
+ assertTrue(drain(output).isEmpty());
+ }
+
+ @Test
+ void processRowPassAllRowsKeepsInputAndAddsAggregates() throws Exception {
+ meta.setPassAllRows(true);
+ meta.getAggregations().clear();
+ meta.getAggregations()
+ .add(
+ new Aggregation(
+ "sum_amount",
+ "amount",
+
Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_SUM),
+ null));
+
+ data = new GroupByData();
+ groupBy = createGroupBy(meta, data);
+
+ List<Object[]> rows =
+ runPipeline(new Object[] {"A", 10.0}, new Object[] {"A", 20.0}, new
Object[] {"B", 5.0});
+
+ assertEquals(3, rows.size());
+
+ // Input fields are preserved; aggregate is appended.
+ assertEquals("A", rows.getFirst()[0]);
+ assertEquals(10.0, ((Number) rows.get(0)[1]).doubleValue(), 1e-9);
+ assertEquals(30.0, ((Number) rows.get(0)[2]).doubleValue(), 1e-9);
+
+ assertEquals("A", rows.get(1)[0]);
+ assertEquals(20.0, ((Number) rows.get(1)[1]).doubleValue(), 1e-9);
+ assertEquals(30.0, ((Number) rows.get(1)[2]).doubleValue(), 1e-9);
+
+ assertEquals("B", rows.get(2)[0]);
+ assertEquals(5.0, ((Number) rows.get(2)[1]).doubleValue(), 1e-9);
+ assertEquals(5.0, ((Number) rows.get(2)[2]).doubleValue(), 1e-9);
+ }
+
+ @Test
+ void sameGroupComparesConfiguredGroupFields() throws Exception {
+ data.inputRowMeta = inputRowMeta;
+ data.groupnrs = new int[] {0};
+
+ assertTrue(groupBy.sameGroup(new Object[] {"A", 1.0}, new Object[] {"A",
2.0}));
+ assertFalse(groupBy.sameGroup(new Object[] {"A", 1.0}, new Object[] {"B",
1.0}));
+ }
+
+ @Test
+ void calcAggregateUpdatesSumAndCount() throws Exception {
+ data.inputRowMeta = inputRowMeta;
+ data.subjectnrs = new int[] {1, 1};
+ data.counts = new long[2];
+ data.mean = new double[2];
+ data.previousSums = new Object[0];
+ data.previousAvgSum = new Object[0];
+ data.previousAvgCount = new long[0];
+ data.movingAvgWindows = new ArrayDeque[2];
+
+ meta.getAggregations().clear();
+ meta.getAggregations()
+ .add(
+ new Aggregation(
+ "sum_amount",
+ "amount",
+
Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_SUM),
+ null));
+ meta.getAggregations()
+ .add(
+ new Aggregation(
+ "cnt",
+ "amount",
+
Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_COUNT_ALL),
+ null));
+
+ groupBy.newAggregate(new Object[] {"A", 10.0});
+ groupBy.calcAggregate(new Object[] {"A", 10.0});
+ groupBy.calcAggregate(new Object[] {"A", 20.0});
+
+ Object[] result = groupBy.getAggregateResult();
+ assertEquals(30.0, ((Number) result[0]).doubleValue(), 1e-9);
+ assertEquals(2L, ((Number) result[1]).longValue());
+ }
+
+ @Test
+ void batchCompleteFlushesCurrentGroup() throws Exception {
+ QueueRowSet input = new QueueRowSet();
+ input.putRow(inputRowMeta, new Object[] {"A", 10.0});
+ input.putRow(inputRowMeta, new Object[] {"A", 20.0});
+ // Intentionally not done yet: batchComplete should flush the open group.
+ groupBy.addRowSetToInputRowSets(input);
+
+ QueueRowSet output = new QueueRowSet();
+ groupBy.addRowSetToOutputRowSets(output);
+
+ assertTrue(groupBy.processRow());
+ assertTrue(groupBy.processRow());
+ assertTrue(drain(output).isEmpty());
+
+ groupBy.batchComplete();
+
+ List<Object[]> rows = drain(output);
+ assertEquals(1, rows.size());
+ assertEquals("A", rows.getFirst()[0]);
+ assertEquals(30.0, ((Number) rows.getFirst()[1]).doubleValue(), 1e-9);
+ assertEquals(2L, ((Number) rows.getFirst()[2]).longValue());
+ assertTrue(data.newBatch);
+ }
+
+ private GroupBy createGroupBy(GroupByMeta transformMeta, GroupByData
transformData) {
+ GroupBy transform =
+ new GroupBy(
+ mockHelper.transformMeta,
+ transformMeta,
+ transformData,
+ 0,
+ mockHelper.pipelineMeta,
+ mockHelper.pipeline) {
+ @Override
+ public String resolve(String str) {
+ return str;
+ }
+ };
+ transform.init();
+ return transform;
+ }
+
+ private List<Object[]> runPipeline(Object[]... inputRows) throws
HopException {
+ QueueRowSet input = new QueueRowSet();
+ for (Object[] inputRow : inputRows) {
+ input.putRow(inputRowMeta, inputRow);
+ }
+ input.setDone();
+ groupBy.addRowSetToInputRowSets(input);
+
+ QueueRowSet output = new QueueRowSet();
+ groupBy.addRowSetToOutputRowSets(output);
+
+ while (groupBy.processRow()) {
+ // drain all input
+ }
+ return drain(output);
+ }
+
+ private List<Object[]> drain(QueueRowSet output) {
+ List<Object[]> rows = new ArrayList<>();
+ Object[] row;
+ while ((row = output.getRowImmediate()) != null) {
+ rows.add(row);
+ }
+ return rows;
+ }
+
+ private void assertNotNullBuffer() {
+ assertNotNull(data.bufferList);
+ }
+}
diff --git
a/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/groupby/GroupingFieldTest.java
b/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/groupby/GroupingFieldTest.java
new file mode 100644
index 0000000000..2cfc41393b
--- /dev/null
+++
b/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/groupby/GroupingFieldTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.groupby;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+
+import org.junit.jupiter.api.Test;
+
+class GroupingFieldTest {
+
+ @Test
+ void equalsAndHashCodeUseName() {
+ GroupingField left = new GroupingField("grp");
+ GroupingField right = new GroupingField("grp");
+ GroupingField other = new GroupingField("other");
+
+ assertEquals(left, right);
+ assertEquals(left.hashCode(), right.hashCode());
+ assertNotEquals(left, other);
+ }
+
+ @Test
+ void cloneCreatesIndependentCopy() {
+ GroupingField original = new GroupingField("grp");
+ GroupingField copy = original.clone();
+
+ assertNotSame(original, copy);
+ assertEquals(original, copy);
+
+ copy.setName("other");
+ assertEquals("grp", original.getName());
+ assertEquals("other", copy.getName());
+ }
+}
diff --git
a/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/groupby/IgnoreAggregateTest.java
b/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/groupby/IgnoreAggregateTest.java
new file mode 100644
index 0000000000..d8794be483
--- /dev/null
+++
b/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/groupby/IgnoreAggregateTest.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.groupby;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.QueueRowSet;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaBoolean;
+import org.apache.hop.core.row.value.ValueMetaNumber;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.pipeline.transforms.mock.TransformMockHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for Group By ignore_aggregate / field_ignore. */
+class IgnoreAggregateTest {
+
+ private TransformMockHelper<GroupByMeta, GroupByData> mockHelper;
+ private GroupByMeta meta;
+ private GroupByData data;
+ private GroupBy groupBy;
+ private IRowMeta inputRowMeta;
+
+ @BeforeAll
+ static void setUpClass() throws HopException {
+ HopEnvironment.init();
+ }
+
+ @BeforeEach
+ void setUp() throws HopException {
+ mockHelper = new TransformMockHelper<>("Group By", GroupByMeta.class,
GroupByData.class);
+ when(mockHelper.logChannelFactory.create(any(), any(ILoggingObject.class)))
+ .thenReturn(mockHelper.iLogChannel);
+ when(mockHelper.pipeline.isRunning()).thenReturn(true);
+
+ meta = new GroupByMeta();
+ meta.setDefault();
+ meta.setAggregateIgnored(true);
+ meta.setAggregateIgnoredField("skip");
+ meta.getGroupingFields().add(new GroupingField("grp"));
+ meta.getAggregations()
+ .add(
+ new Aggregation(
+ "sum_amount",
+ "amount",
+
Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_SUM),
+ null));
+ meta.getAggregations()
+ .add(
+ new Aggregation(
+ "cnt",
+ "amount",
+
Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_COUNT_ALL),
+ null));
+
+ data = new GroupByData();
+ inputRowMeta = new RowMeta();
+ inputRowMeta.addValueMeta(new ValueMetaString("grp"));
+ inputRowMeta.addValueMeta(new ValueMetaNumber("amount"));
+ inputRowMeta.addValueMeta(new ValueMetaBoolean("skip"));
+
+ groupBy =
+ new GroupBy(
+ mockHelper.transformMeta, meta, data, 0, mockHelper.pipelineMeta,
mockHelper.pipeline) {
+ @Override
+ public String resolve(String str) {
+ return str;
+ }
+ };
+ groupBy.init();
+ }
+
+ @AfterEach
+ void tearDown() {
+ mockHelper.cleanUp();
+ }
+
+ @Test
+ void isRowAggregateIgnoredRespectsBooleanField() throws Exception {
+ data.inputRowMeta = inputRowMeta;
+ data.aggregateIgnoredFieldIndex = 2;
+
+ assertFalse(groupBy.isRowAggregateIgnored(new Object[] {"A", 10.0,
Boolean.FALSE}));
+ assertTrue(groupBy.isRowAggregateIgnored(new Object[] {"A", 100.0,
Boolean.TRUE}));
+ assertFalse(groupBy.isRowAggregateIgnored(new Object[] {"A", 10.0, null}));
+ }
+
+ @Test
+ void isRowAggregateIgnoredDisabledWhenIndexUnset() throws Exception {
+ data.inputRowMeta = inputRowMeta;
+ data.aggregateIgnoredFieldIndex = -1;
+
+ assertFalse(groupBy.isRowAggregateIgnored(new Object[] {"A", 100.0,
Boolean.TRUE}));
+ }
+
+ @Test
+ void processRowSkipsIgnoredRowsInAggregation() throws Exception {
+ List<Object[]> rows =
+ runPipeline(
+ new Object[] {"A", 10.0, Boolean.FALSE},
+ new Object[] {"A", 20.0, Boolean.FALSE},
+ new Object[] {"A", 30.0, Boolean.FALSE},
+ new Object[] {"A", 100.0, Boolean.TRUE});
+
+ assertEquals(1, rows.size());
+ Object[] result = rows.getFirst();
+ assertEquals("A", result[0]);
+ assertEquals(60.0, ((Number) result[1]).doubleValue(), 1e-9);
+ assertEquals(3L, ((Number) result[2]).longValue());
+ }
+
+ @Test
+ void processRowIncludesAllRowsWhenIgnoreDisabled() throws Exception {
+ meta.setAggregateIgnored(false);
+ meta.setAggregateIgnoredField(null);
+
+ List<Object[]> rows =
+ runPipeline(
+ new Object[] {"A", 10.0, Boolean.FALSE},
+ new Object[] {"A", 20.0, Boolean.FALSE},
+ new Object[] {"A", 30.0, Boolean.FALSE},
+ new Object[] {"A", 100.0, Boolean.TRUE});
+
+ assertEquals(1, rows.size());
+ Object[] result = rows.getFirst();
+ assertEquals(160.0, ((Number) result[1]).doubleValue(), 1e-9);
+ assertEquals(4L, ((Number) result[2]).longValue());
+ }
+
+ @Test
+ void processRowSkipsIgnoredRowsPerGroup() throws Exception {
+ List<Object[]> rows =
+ runPipeline(
+ new Object[] {"A", 10.0, Boolean.FALSE},
+ new Object[] {"A", 20.0, Boolean.FALSE},
+ new Object[] {"A", 30.0, Boolean.FALSE},
+ new Object[] {"A", 100.0, Boolean.TRUE},
+ new Object[] {"B", 5.0, Boolean.FALSE},
+ new Object[] {"B", 50.0, Boolean.TRUE});
+
+ assertEquals(2, rows.size());
+
+ Object[] groupA = rows.getFirst();
+ assertEquals("A", groupA[0]);
+ assertEquals(60.0, ((Number) groupA[1]).doubleValue(), 1e-9);
+ assertEquals(3L, ((Number) groupA[2]).longValue());
+
+ Object[] groupB = rows.get(1);
+ assertEquals("B", groupB[0]);
+ assertEquals(5.0, ((Number) groupB[1]).doubleValue(), 1e-9);
+ assertEquals(1L, ((Number) groupB[2]).longValue());
+ }
+
+ @Test
+ void processRowIgnoresLastRowWhenFlagged() throws Exception {
+ List<Object[]> rows =
+ runPipeline(
+ new Object[] {"A", 10.0, Boolean.FALSE},
+ new Object[] {"A", 20.0, Boolean.FALSE},
+ new Object[] {"A", 100.0, Boolean.TRUE});
+
+ assertEquals(1, rows.size());
+ Object[] result = rows.getFirst();
+ assertEquals(30.0, ((Number) result[1]).doubleValue(), 1e-9);
+ assertEquals(2L, ((Number) result[2]).longValue());
+ }
+
+ @Test
+ void processRowWithOnlyIgnoredRowsStillEmitsGroup() throws Exception {
+ List<Object[]> rows =
+ runPipeline(
+ new Object[] {"A", 100.0, Boolean.TRUE}, new Object[] {"A", 200.0,
Boolean.TRUE});
+
+ assertEquals(1, rows.size());
+ Object[] result = rows.getFirst();
+ assertEquals("A", result[0]);
+ assertNull(result[1]); // sum stays null when every row is ignored
+ assertEquals(0L, ((Number) result[2]).longValue());
+ }
+
+ @Test
+ void processRowSkipsFirstRowWhenFlaggedForMinMaxFirstLast() throws Exception
{
+ configureAggregations(
+ Aggregation.TYPE_GROUP_MIN,
+ Aggregation.TYPE_GROUP_MAX,
+ Aggregation.TYPE_GROUP_FIRST,
+ Aggregation.TYPE_GROUP_LAST,
+ Aggregation.TYPE_GROUP_FIRST_INCL_NULL,
+ Aggregation.TYPE_GROUP_LAST_INCL_NULL);
+
+ // Group A: ignored first row holds extreme values; valid rows are 10 and
20
+ // Group B: ignored first row holds extremes; valid rows are 10 and 20
+ List<Object[]> rows =
+ runPipeline(
+ new Object[] {"A", 999.0, Boolean.TRUE},
+ new Object[] {"A", 10.0, Boolean.FALSE},
+ new Object[] {"A", 20.0, Boolean.FALSE},
+ new Object[] {"B", 1.0, Boolean.TRUE},
+ new Object[] {"B", 10.0, Boolean.FALSE},
+ new Object[] {"B", 20.0, Boolean.FALSE});
+
+ assertEquals(2, rows.size());
+
+ Object[] groupA = rows.getFirst();
+ assertEquals("A", groupA[0]);
+ assertEquals(10.0, ((Number) groupA[1]).doubleValue(), 1e-9); // min
+ assertEquals(20.0, ((Number) groupA[2]).doubleValue(), 1e-9); // max
+ assertEquals(10.0, ((Number) groupA[3]).doubleValue(), 1e-9); // first
+ assertEquals(20.0, ((Number) groupA[4]).doubleValue(), 1e-9); // last
+ assertEquals(10.0, ((Number) groupA[5]).doubleValue(), 1e-9); // first
incl null
+ assertEquals(20.0, ((Number) groupA[6]).doubleValue(), 1e-9); // last incl
null
+
+ Object[] groupB = rows.get(1);
+ assertEquals("B", groupB[0]);
+ assertEquals(10.0, ((Number) groupB[1]).doubleValue(), 1e-9); // min (not
1.0)
+ assertEquals(20.0, ((Number) groupB[2]).doubleValue(), 1e-9); // max
+ assertEquals(10.0, ((Number) groupB[3]).doubleValue(), 1e-9); // first
(not 1.0)
+ assertEquals(20.0, ((Number) groupB[4]).doubleValue(), 1e-9); // last
+ assertEquals(10.0, ((Number) groupB[5]).doubleValue(), 1e-9); // first
incl null
+ assertEquals(20.0, ((Number) groupB[6]).doubleValue(), 1e-9); // last incl
null
+ }
+
+ @Test
+ void processRowAllIgnoredLeavesMinMaxFirstNull() throws Exception {
+ configureAggregations(
+ Aggregation.TYPE_GROUP_MIN,
+ Aggregation.TYPE_GROUP_MAX,
+ Aggregation.TYPE_GROUP_FIRST,
+ Aggregation.TYPE_GROUP_FIRST_INCL_NULL,
+ Aggregation.TYPE_GROUP_COUNT_ALL);
+
+ List<Object[]> rows =
+ runPipeline(
+ new Object[] {"A", 100.0, Boolean.TRUE}, new Object[] {"A", 200.0,
Boolean.TRUE});
+
+ assertEquals(1, rows.size());
+ Object[] result = rows.getFirst();
+ assertEquals("A", result[0]);
+ assertNull(result[1]); // min
+ assertNull(result[2]); // max
+ assertNull(result[3]); // first
+ assertNull(result[4]); // first incl null
+ assertEquals(0L, ((Number) result[5]).longValue()); // count
+ }
+
+ @Test
+ void processRowPassAllRowsSkipsIgnoredInCumulativeSum() throws Exception {
+ meta.setPassAllRows(true);
+ meta.getAggregations().clear();
+ meta.getAggregations()
+ .add(
+ new Aggregation(
+ "cum_sum",
+ "amount",
+
Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_CUMULATIVE_SUM),
+ null));
+ meta.getAggregations()
+ .add(
+ new Aggregation(
+ "sum_amount",
+ "amount",
+
Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_SUM),
+ null));
+
+ List<Object[]> rows =
+ runPipeline(
+ new Object[] {"A", 10.0, Boolean.FALSE},
+ new Object[] {"A", 100.0, Boolean.TRUE},
+ new Object[] {"A", 20.0, Boolean.FALSE});
+
+ assertEquals(3, rows.size());
+
+ // output: grp, amount, skip, cum_sum, sum_amount
+ assertEquals(10.0, ((Number) rows.get(0)[3]).doubleValue(), 1e-9);
+ assertEquals(10.0, ((Number) rows.get(1)[3]).doubleValue(), 1e-9); //
ignored: carry previous
+ assertEquals(30.0, ((Number) rows.get(2)[3]).doubleValue(), 1e-9);
+
+ assertEquals(30.0, ((Number) rows.get(0)[4]).doubleValue(), 1e-9); //
group sum on every row
+ assertEquals(30.0, ((Number) rows.get(1)[4]).doubleValue(), 1e-9);
+ assertEquals(30.0, ((Number) rows.get(2)[4]).doubleValue(), 1e-9);
+ }
+
+ @Test
+ void processRowPassAllRowsSkipsIgnoredInMovingAverage() throws Exception {
+ meta.setPassAllRows(true);
+ meta.getAggregations().clear();
+ meta.getAggregations()
+ .add(
+ new Aggregation(
+ "mov_avg",
+ "amount",
+
Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_MOVING_AVERAGE),
+ "2",
+ null));
+
+ List<Object[]> rows =
+ runPipeline(
+ new Object[] {"A", 10.0, Boolean.FALSE},
+ new Object[] {"A", 100.0, Boolean.TRUE},
+ new Object[] {"A", 20.0, Boolean.FALSE});
+
+ assertEquals(3, rows.size());
+ // window size 2: after first valid row incomplete; ignored does not enter
window;
+ // after second valid row window is [10, 20] -> 15
+ assertNull(rows.get(0)[3]);
+ assertNull(rows.get(1)[3]); // ignored: still incomplete window
+ assertEquals(15.0, ((Number) rows.get(2)[3]).doubleValue(), 1e-9);
+ }
+
+ @Test
+ void processRowFailsWhenIgnoreEnabledButFieldBlank() throws Exception {
+ meta.setAggregateIgnoredField("");
+
+ QueueRowSet input = new QueueRowSet();
+ input.putRow(inputRowMeta, new Object[] {"A", 10.0, Boolean.FALSE});
+ input.setDone();
+ groupBy.addRowSetToInputRowSets(input);
+ groupBy.addRowSetToOutputRowSets(new QueueRowSet());
+
+ assertFalse(groupBy.processRow());
+ assertTrue(groupBy.getErrors() > 0);
+ }
+
+ private void configureAggregations(int... types) {
+ meta.getAggregations().clear();
+ for (int i = 0; i < types.length; i++) {
+ meta.getAggregations()
+ .add(
+ new Aggregation(
+ "agg" + i, "amount",
Aggregation.getTypeDescLongFromCode(types[i]), null));
+ }
+ }
+
+ private List<Object[]> runPipeline(Object[]... inputRows) throws
HopException {
+ QueueRowSet input = new QueueRowSet();
+ for (Object[] inputRow : inputRows) {
+ input.putRow(inputRowMeta, inputRow);
+ }
+ input.setDone();
+ groupBy.addRowSetToInputRowSets(input);
+
+ QueueRowSet output = new QueueRowSet();
+ groupBy.addRowSetToOutputRowSets(output);
+
+ while (groupBy.processRow()) {
+ // drain all input
+ }
+
+ List<Object[]> rows = new ArrayList<>();
+ Object[] row;
+ while ((row = output.getRowImmediate()) != null) {
+ rows.add(row);
+ }
+ return rows;
+ }
+}