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 18df7242ee Issue #5193 : Accept sequence start, end, and increment
from one info row (#8696)
18df7242ee is described below
commit 18df7242ee4d5211c81a38c3bdbdced2e46071b6
Author: Matt Casters <[email protected]>
AuthorDate: Thu Oct 1 15:38:10 2026 +0200
Issue #5193 : Accept sequence start, end, and increment from one info row
(#8696)
* Issue #5193 : Accept sequence start, end, and increment from one info row
* Issue #5193 : Report a distributed configuration row when Add Sequence
runs in multiple copies
---------
Co-authored-by: Bart Maertens <[email protected]>
---
.../pages/pipeline/transforms/addsequence.adoc | 12 +-
.../transforms/addsequence/AddSequence.java | 152 ++++++++++++--
.../transforms/addsequence/AddSequenceDialog.java | 167 ++++++++++++++-
.../transforms/addsequence/AddSequenceMeta.java | 214 +++++++++++++++++++
.../addsequence/messages/messages_en_US.properties | 29 +++
.../addsequence/AddSequenceMetaTest.java | 230 +++++++++++++++++++-
.../transforms/addsequence/AddSequenceTest.java | 231 +++++++++++++++++++++
7 files changed, 1005 insertions(+), 30 deletions(-)
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addsequence.adoc
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addsequence.adoc
index 5a1e071543..fbfb1167ec 100644
---
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addsequence.adoc
+++
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addsequence.adoc
@@ -31,6 +31,8 @@ You can either use a database sequence (if supported, e.g.
Oracle, PostgreSQL) t
Hop-generated sequence values are unique when used in the same pipeline, and
return to the same starting value each time you run the pipeline. Hop sequences
are unique only when used in the same pipeline.
Also, they are not stored, so the values start back at the same value every
time the pipeline is launched.
+The start, increment, and maximum can be typed in, or read from another
transform. Connect that transform to Add Sequence and select it as the
configuration transform. It must return exactly one row. More than one row
stops the pipeline with an error. The hop is an info hop, so that row is not
part of the main stream. The end field is the maximum value: after it, the
sequence returns to the start value. The configuration transform cannot run in
multiple copies. If Add Sequence itself run [...]
+
== Options
[options="header"]
@@ -46,7 +48,11 @@ This name must be unique within a single pipeline.
|Use a pipeline counter to generate the sequence|Enable if you want the
sequence to be generated by Hop, then set these parameters: Counter name
(optional), Start at, Increment by, Maximum value.
|Counter name (optional)|If multiple transforms in a pipeline generate the
same value name, this option enables you to specify the name of the counter to
associate with.
Avoids forcing unique sequencing across multiple transforms.
-|Start at|The value to begin the sequence with.
-|Increment by|The amount by which the sequence increases or decreases.
-|Maximum value|The value after which the sequence returns to the Start At
value.
+|Start at|The value to begin the sequence with. Not used when a configuration
transform is selected.
+|Increment by|The amount by which the sequence increases or decreases. Not
used when a configuration transform is selected.
+|Maximum value|The value after which the sequence returns to the Start At
value. Not used when a configuration transform is selected.
+|Configuration transform|Optional transform that provides exactly one row with
the start, end, and increment. Leave this empty to use Start at, Increment by,
and Maximum value.
+|Start field|Field in the configuration row that contains the value to begin
the sequence with.
+|End field|Field in the configuration row that contains the maximum value.
After this value the sequence returns to the start value.
+|Increment field|Field in the configuration row that contains the amount by
which the sequence increases or decreases.
|===
\ No newline at end of file
diff --git
a/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequence.java
b/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequence.java
index 55a6c3fd49..4b98db9dc5 100644
---
a/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequence.java
+++
b/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequence.java
@@ -17,13 +17,16 @@
package org.apache.hop.pipeline.transforms.addsequence;
+import org.apache.hop.core.Const;
import org.apache.hop.core.Counter;
import org.apache.hop.core.Counters;
+import org.apache.hop.core.IRowSet;
import org.apache.hop.core.database.Database;
import org.apache.hop.core.database.DatabaseMeta;
import org.apache.hop.core.exception.HopDatabaseException;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.exception.HopValueException;
import org.apache.hop.core.row.IRowMeta;
import org.apache.hop.core.row.RowDataUtil;
import org.apache.hop.core.util.Utils;
@@ -93,6 +96,19 @@ public class AddSequence extends
BaseTransform<AddSequenceMeta, AddSequenceData>
@Override
public boolean processRow() throws HopException {
+ // Read the info row before the main input so its rowset is removed and
cannot be mixed in.
+ if (first && meta.isConfigurationFromTransform()) {
+ try {
+ readConfigurationRow();
+ } catch (HopException e) {
+ logError(BaseMessages.getString(PKG,
"AddSequence.Log.ErrorInTransform") + e.getMessage());
+ setErrors(1);
+ stopAll();
+ setOutputDone();
+ return false;
+ }
+ }
+
// Get row from input rowset & set row busy!
Object[] r = getRow();
if (r == null) {
@@ -173,6 +189,17 @@ public class AddSequence extends
BaseTransform<AddSequenceMeta, AddSequenceData>
+ dbe.getMessage());
}
} else if (meta.isCounterUsed()) {
+ if (meta.isConfigurationFromTransform()) {
+ // The counter is created from the info row on the first processRow
call.
+ if (Utils.isEmpty(Const.trim(resolve(meta.getStartField())))
+ || Utils.isEmpty(Const.trim(resolve(meta.getEndField())))
+ || Utils.isEmpty(Const.trim(resolve(meta.getIncrementField()))))
{
+ logError(BaseMessages.getString(PKG,
"AddSequence.Log.ConfigurationFieldsMissing"));
+ return false;
+ }
+ return true;
+ }
+
// Do the environment translations of the counter values.
boolean doAbort = false;
try {
@@ -221,26 +248,7 @@ public class AddSequence extends
BaseTransform<AddSequenceMeta, AddSequenceData>
return false;
}
- String realCounterName = resolve(meta.getCounterName());
- if (!Utils.isEmpty(realCounterName)) {
- data.setLookup(lookupCounterName(realCounterName));
- } else {
- data.setLookup(lookupCounterName(meta.getValueName()));
- }
-
- // We need to synchronize over the whole pipeline to make sure that we
always get the same
- // counter
- // regardless of the number of transform copies asking for it.
- //
- synchronized (getPipeline()) {
- if (isDetailed()) {
- logDetailed("init counter name: {0}", data.getLookup());
- }
- data.counter =
- Counters.getInstance()
- .getOrUpdateCounter(
- data.getLookup(), new Counter(data.start,
data.increment, data.maximum));
- }
+ createCounter(data.start, data.increment, data.maximum);
return true;
} else {
logError(
@@ -275,6 +283,110 @@ public class AddSequence extends
BaseTransform<AddSequenceMeta, AddSequenceData>
super.cleanup();
}
+ /**
+ * Read the single configuration row and create the counter from its start,
end, and increment
+ * fields. Zero rows or more than one row is an error.
+ */
+ private void readConfigurationRow() throws HopException {
+ String sourceName = meta.getConfigurationTransform();
+ IRowSet rowSet = findInputRowSet(sourceName);
+ if (rowSet == null) {
+ throw new HopTransformException(
+ BaseMessages.getString(
+ PKG, "AddSequence.Exception.ConfigurationTransformNotFound",
sourceName));
+ }
+
+ Object[] row = getRowFrom(rowSet);
+ if (row == null) {
+ // Distributed over several copies of this transform, the single row
only reaches one of them.
+ String key =
+ getTransformMeta().getCopies(this) > 1
+ ? "AddSequence.Exception.ConfigurationRowMissingInCopy"
+ : "AddSequence.Exception.ConfigurationRowMissing";
+ throw new HopTransformException(
+ BaseMessages.getString(PKG, key, sourceName,
Integer.toString(getCopy())));
+ }
+ Object[] extra = getRowFrom(rowSet);
+ if (extra != null) {
+ throw new HopTransformException(
+ BaseMessages.getString(
+ PKG, "AddSequence.Exception.ConfigurationRowNotSingle",
sourceName));
+ }
+
+ IRowMeta rowMeta = rowSet.getRowMeta();
+ if (rowMeta == null) {
+ throw new HopTransformException(
+ BaseMessages.getString(
+ PKG, "AddSequence.Exception.ConfigurationRowHasNoFields",
sourceName));
+ }
+
+ long start = readConfigurationValue(rowMeta, row,
resolve(meta.getStartField()));
+ long end = readConfigurationValue(rowMeta, row,
resolve(meta.getEndField()));
+ long increment = readConfigurationValue(rowMeta, row,
resolve(meta.getIncrementField()));
+ if (isDetailed()) {
+ logDetailed(
+ BaseMessages.getString(
+ PKG, "AddSequence.Log.ReadConfiguration", sourceName, start,
end, increment));
+ }
+ createCounter(start, increment, end);
+ }
+
+ private long readConfigurationValue(IRowMeta rowMeta, Object[] row, String
fieldName)
+ throws HopException {
+ String name = Const.trim(fieldName);
+ int index = Utils.isEmpty(name) ? -1 : rowMeta.indexOfValue(name);
+ if (index < 0) {
+ throw new HopTransformException(
+ BaseMessages.getString(PKG,
"AddSequence.Exception.ConfigurationFieldNotFound", name));
+ }
+ try {
+ Long value = rowMeta.getValueMeta(index).getInteger(row[index]);
+ if (value == null) {
+ throw new HopTransformException(
+ BaseMessages.getString(PKG,
"AddSequence.Exception.ConfigurationFieldIsNull", name));
+ }
+ return value;
+ } catch (HopTransformException e) {
+ throw e;
+ } catch (HopValueException e) {
+ throw new HopTransformException(
+ BaseMessages.getString(
+ PKG,
+ "AddSequence.Exception.ConfigurationFieldNotNumeric",
+ name,
+ String.valueOf(row[index]),
+ e.getMessage()),
+ e);
+ }
+ }
+
+ /**
+ * One counter is shared by every copy of this transform. Synchronize on the
pipeline so the first
+ * copy creates it and the others reuse it.
+ */
+ private void createCounter(long start, long increment, long maximum) {
+ data.start = start;
+ data.increment = increment;
+ data.maximum = maximum;
+
+ String realCounterName = resolve(meta.getCounterName());
+ if (!Utils.isEmpty(realCounterName)) {
+ data.setLookup(lookupCounterName(realCounterName));
+ } else {
+ data.setLookup(lookupCounterName(meta.getValueName()));
+ }
+
+ synchronized (getPipeline()) {
+ if (isDetailed()) {
+ logDetailed("init counter name: {0}", data.getLookup());
+ }
+ data.counter =
+ Counters.getInstance()
+ .getOrUpdateCounter(
+ data.getLookup(), new Counter(data.start, data.increment,
data.maximum));
+ }
+ }
+
/**
* Build a unique identifier for this pipeline run.
*
diff --git
a/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceDialog.java
b/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceDialog.java
index 22f6a971f5..2b3a57ccfd 100644
---
a/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceDialog.java
+++
b/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceDialog.java
@@ -20,6 +20,8 @@ package org.apache.hop.pipeline.transforms.addsequence;
import org.apache.hop.core.Const;
import org.apache.hop.core.database.Database;
import org.apache.hop.core.database.DatabaseMeta;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.row.IRowMeta;
import org.apache.hop.core.util.Utils;
import org.apache.hop.core.variables.IVariables;
import org.apache.hop.i18n.BaseMessages;
@@ -44,6 +46,7 @@ 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.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
@@ -83,6 +86,18 @@ public class AddSequenceDialog extends BaseTransformDialog {
private Label wlMaxVal;
private TextVar wMaxVal;
+ private Label wlConfigTransform;
+ private Combo wConfigTransform;
+
+ private Label wlStartField;
+ private Combo wStartField;
+
+ private Label wlEndField;
+ private Combo wEndField;
+
+ private Label wlIncrementField;
+ private Combo wIncrementField;
+
private final AddSequenceMeta input;
public AddSequenceDialog(
@@ -354,6 +369,95 @@ public class AddSequenceDialog extends BaseTransformDialog
{
fdMaxVal.right = new FormAttachment(100, 0);
wMaxVal.setLayoutData(fdMaxVal);
+ wlConfigTransform = new Label(gCounter, SWT.RIGHT);
+ wlConfigTransform.setText(
+ BaseMessages.getString(PKG,
"AddSequenceDialog.ConfigurationTransform.Label"));
+ PropsUi.setLook(wlConfigTransform);
+ wlConfigTransform.setToolTipText(
+ BaseMessages.getString(PKG,
"AddSequenceDialog.ConfigurationTransform.Tooltip"));
+ FormData fdlConfigTransform = new FormData();
+ fdlConfigTransform.left = new FormAttachment(0, 0);
+ fdlConfigTransform.right = new FormAttachment(middle, -margin);
+ fdlConfigTransform.top = new FormAttachment(wMaxVal, margin);
+ wlConfigTransform.setLayoutData(fdlConfigTransform);
+ wConfigTransform = new Combo(gCounter, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+ PropsUi.setLook(wConfigTransform);
+ wConfigTransform.setToolTipText(
+ BaseMessages.getString(PKG,
"AddSequenceDialog.ConfigurationTransform.Tooltip"));
+
wConfigTransform.setItems(pipelineMeta.getPrevTransformNames(transformName));
+ FormData fdConfigTransform = new FormData();
+ fdConfigTransform.left = new FormAttachment(middle, 0);
+ fdConfigTransform.top = new FormAttachment(wMaxVal, margin);
+ fdConfigTransform.right = new FormAttachment(100, 0);
+ wConfigTransform.setLayoutData(fdConfigTransform);
+
+ wlStartField = new Label(gCounter, SWT.RIGHT);
+ wlStartField.setText(BaseMessages.getString(PKG,
"AddSequenceDialog.StartField.Label"));
+ PropsUi.setLook(wlStartField);
+ wlStartField.setToolTipText(
+ BaseMessages.getString(PKG, "AddSequenceDialog.StartField.Tooltip"));
+ FormData fdlStartField = new FormData();
+ fdlStartField.left = new FormAttachment(0, 0);
+ fdlStartField.right = new FormAttachment(middle, -margin);
+ fdlStartField.top = new FormAttachment(wConfigTransform, margin);
+ wlStartField.setLayoutData(fdlStartField);
+ wStartField = new Combo(gCounter, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+ PropsUi.setLook(wStartField);
+ wStartField.setToolTipText(BaseMessages.getString(PKG,
"AddSequenceDialog.StartField.Tooltip"));
+ wStartField.addModifyListener(lsMod);
+ FormData fdStartField = new FormData();
+ fdStartField.left = new FormAttachment(middle, 0);
+ fdStartField.top = new FormAttachment(wConfigTransform, margin);
+ fdStartField.right = new FormAttachment(100, 0);
+ wStartField.setLayoutData(fdStartField);
+
+ wlEndField = new Label(gCounter, SWT.RIGHT);
+ wlEndField.setText(BaseMessages.getString(PKG,
"AddSequenceDialog.EndField.Label"));
+ PropsUi.setLook(wlEndField);
+ wlEndField.setToolTipText(BaseMessages.getString(PKG,
"AddSequenceDialog.EndField.Tooltip"));
+ FormData fdlEndField = new FormData();
+ fdlEndField.left = new FormAttachment(0, 0);
+ fdlEndField.right = new FormAttachment(middle, -margin);
+ fdlEndField.top = new FormAttachment(wStartField, margin);
+ wlEndField.setLayoutData(fdlEndField);
+ wEndField = new Combo(gCounter, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+ PropsUi.setLook(wEndField);
+ wEndField.setToolTipText(BaseMessages.getString(PKG,
"AddSequenceDialog.EndField.Tooltip"));
+ wEndField.addModifyListener(lsMod);
+ FormData fdEndField = new FormData();
+ fdEndField.left = new FormAttachment(middle, 0);
+ fdEndField.top = new FormAttachment(wStartField, margin);
+ fdEndField.right = new FormAttachment(100, 0);
+ wEndField.setLayoutData(fdEndField);
+
+ wlIncrementField = new Label(gCounter, SWT.RIGHT);
+ wlIncrementField.setText(BaseMessages.getString(PKG,
"AddSequenceDialog.IncrementField.Label"));
+ PropsUi.setLook(wlIncrementField);
+ wlIncrementField.setToolTipText(
+ BaseMessages.getString(PKG,
"AddSequenceDialog.IncrementField.Tooltip"));
+ FormData fdlIncrementField = new FormData();
+ fdlIncrementField.left = new FormAttachment(0, 0);
+ fdlIncrementField.right = new FormAttachment(middle, -margin);
+ fdlIncrementField.top = new FormAttachment(wEndField, margin);
+ wlIncrementField.setLayoutData(fdlIncrementField);
+ wIncrementField = new Combo(gCounter, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+ PropsUi.setLook(wIncrementField);
+ wIncrementField.setToolTipText(
+ BaseMessages.getString(PKG,
"AddSequenceDialog.IncrementField.Tooltip"));
+ wIncrementField.addModifyListener(lsMod);
+ FormData fdIncrementField = new FormData();
+ fdIncrementField.left = new FormAttachment(middle, 0);
+ fdIncrementField.top = new FormAttachment(wEndField, margin);
+ fdIncrementField.right = new FormAttachment(100, 0);
+ wIncrementField.setLayoutData(fdIncrementField);
+
+ wConfigTransform.addModifyListener(
+ e -> {
+ input.setChanged();
+ enableFields();
+ });
+ wConfigTransform.addListener(SWT.Selection, e ->
refreshConfigurationFields());
+
wContent.pack();
Rectangle bounds = wContent.getBounds();
sc.setContent(wContent);
@@ -380,17 +484,58 @@ public class AddSequenceDialog extends
BaseTransformDialog {
wlSeqname.setEnabled(useDatabase);
wSeqname.setEnabled(useDatabase);
+ boolean fromTransform = useCounter &&
!Utils.isEmpty(wConfigTransform.getText());
+
wlCounterName.setEnabled(useCounter);
wCounterName.setEnabled(useCounter);
- wlStartAt.setEnabled(useCounter);
- wStartAt.setEnabled(useCounter);
- wlIncrBy.setEnabled(useCounter);
- wIncrBy.setEnabled(useCounter);
- wlMaxVal.setEnabled(useCounter);
- wMaxVal.setEnabled(useCounter);
+ wlStartAt.setEnabled(useCounter && !fromTransform);
+ wStartAt.setEnabled(useCounter && !fromTransform);
+ wlIncrBy.setEnabled(useCounter && !fromTransform);
+ wIncrBy.setEnabled(useCounter && !fromTransform);
+ wlMaxVal.setEnabled(useCounter && !fromTransform);
+ wMaxVal.setEnabled(useCounter && !fromTransform);
+ wlConfigTransform.setEnabled(useCounter);
+ wConfigTransform.setEnabled(useCounter);
+ wlStartField.setEnabled(fromTransform);
+ wStartField.setEnabled(fromTransform);
+ wlEndField.setEnabled(fromTransform);
+ wEndField.setEnabled(fromTransform);
+ wlIncrementField.setEnabled(fromTransform);
+ wIncrementField.setEnabled(fromTransform);
activeSequence();
}
+ /**
+ * Fill the start, end, and increment combos from the configuration
transform. A failure to
+ * calculate those fields leaves the typed names in place.
+ */
+ private void refreshConfigurationFields() {
+ if (wStartField == null || wStartField.isDisposed()) {
+ return;
+ }
+ String start = wStartField.getText();
+ String end = wEndField.getText();
+ String increment = wIncrementField.getText();
+ String[] names = new String[0];
+ try {
+ String source = wConfigTransform.getText();
+ if (!Utils.isEmpty(source)) {
+ IRowMeta rowMeta = pipelineMeta.getTransformFields(variables, source);
+ if (rowMeta != null) {
+ names = rowMeta.getFieldNames();
+ }
+ }
+ } catch (HopException e) {
+ log.logDebug("Unable to read fields from the configuration transform: "
+ e.getMessage());
+ }
+ wStartField.setItems(names);
+ wEndField.setItems(names);
+ wIncrementField.setItems(names);
+ wStartField.setText(Const.NVL(start, ""));
+ wEndField.setText(Const.NVL(end, ""));
+ wIncrementField.setText(Const.NVL(increment, ""));
+ }
+
/** Copy information from the meta-data input to the dialog fields. */
public void getData() {
logDebug(BaseMessages.getString(PKG,
"AddSequenceDialog.Log.GettingKeyInfo"));
@@ -417,6 +562,11 @@ public class AddSequenceDialog extends BaseTransformDialog
{
wStartAt.setText(input.getStartAt());
wIncrBy.setText(input.getIncrementBy());
wMaxVal.setText(input.getMaxValue());
+ wConfigTransform.setText(Const.NVL(input.getConfigurationTransform(), ""));
+ wStartField.setText(Const.NVL(input.getStartField(), ""));
+ wEndField.setText(Const.NVL(input.getEndField(), ""));
+ wIncrementField.setText(Const.NVL(input.getIncrementField(), ""));
+ refreshConfigurationFields();
enableFields();
}
@@ -446,6 +596,11 @@ public class AddSequenceDialog extends BaseTransformDialog
{
input.setStartAt(wStartAt.getText());
input.setIncrementBy(wIncrBy.getText());
input.setMaxValue(wMaxVal.getText());
+ input.setConfigurationTransform(wConfigTransform.getText());
+ input.setStartField(wStartField.getText());
+ input.setEndField(wEndField.getText());
+ input.setIncrementField(wIncrementField.getText());
+ input.searchInfoAndTargetTransforms(pipelineMeta.getTransforms());
if (input.isDatabaseUsed()
&& pipelineMeta.findDatabase(wConnection.getText(), variables) ==
null) {
diff --git
a/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceMeta.java
b/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceMeta.java
index ade6e663d5..e9221e163a 100644
---
a/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceMeta.java
+++
b/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceMeta.java
@@ -40,7 +40,13 @@ import org.apache.hop.metadata.api.IHopMetadataProvider;
import org.apache.hop.metadata.api.IOptionalDatabaseConnection;
import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.pipeline.transform.BaseTransformMeta;
+import org.apache.hop.pipeline.transform.ITransformIOMeta;
+import org.apache.hop.pipeline.transform.TransformIOMeta;
import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transform.stream.IStream;
+import org.apache.hop.pipeline.transform.stream.IStream.StreamType;
+import org.apache.hop.pipeline.transform.stream.Stream;
+import org.apache.hop.pipeline.transform.stream.StreamIcon;
/** Meta data for the Add Sequence transform. */
@Transform(
@@ -109,6 +115,35 @@ public class AddSequenceMeta extends
BaseTransformMeta<AddSequence, AddSequenceD
injectionKeyDescription = "AddSequenceMeta.Injection.MaxValue")
private String maxValue;
+ /** Info transform that provides one row with the counter start, end, and
increment. */
+ @HopMetadataProperty(
+ key = "configuration_transform",
+ injectionKeyDescription =
"AddSequenceMeta.Injection.ConfigurationTransform")
+ private String configurationTransform;
+
+ @HopMetadataProperty(
+ key = "start_field",
+ injectionKeyDescription = "AddSequenceMeta.Injection.StartField")
+ private String startField;
+
+ @HopMetadataProperty(
+ key = "end_field",
+ injectionKeyDescription = "AddSequenceMeta.Injection.EndField")
+ private String endField;
+
+ @HopMetadataProperty(
+ key = "increment_field",
+ injectionKeyDescription = "AddSequenceMeta.Injection.IncrementField")
+ private String incrementField;
+
+ /**
+ * Counter values come from one row of {@link #configurationTransform}
instead of the typed start,
+ * increment, and maximum.
+ */
+ public boolean isConfigurationFromTransform() {
+ return counterUsed && !databaseUsed &&
!Utils.isEmpty(configurationTransform);
+ }
+
/**
* @param maxValue The maxValue to set.
*/
@@ -200,6 +235,105 @@ public class AddSequenceMeta extends
BaseTransformMeta<AddSequence, AddSequenceD
transformMeta);
remarks.add(cr);
}
+
+ checkConfigurationTransform(remarks, pipelineMeta, transformMeta, info,
variables);
+ }
+
+ /**
+ * The configuration transform is optional. When it is set, the start, end,
and increment field
+ * names have to be set as well, and the transform has to exist. Running in
several copies needs
+ * the configuration row copied to every copy: distributed, it only reaches
the first one.
+ */
+ private void checkConfigurationTransform(
+ List<ICheckResult> remarks,
+ PipelineMeta pipelineMeta,
+ TransformMeta transformMeta,
+ IRowMeta info,
+ IVariables variables) {
+ if (!isConfigurationFromTransform()) {
+ return;
+ }
+
+ boolean fieldsMissing =
+ Utils.isEmpty(startField) || Utils.isEmpty(endField) ||
Utils.isEmpty(incrementField);
+ if (fieldsMissing) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_ERROR,
+ BaseMessages.getString(PKG,
"AddSequenceMeta.CheckResult.ConfigurationFieldsMissing"),
+ transformMeta));
+ }
+
+ if (pipelineMeta != null) {
+ TransformMeta source =
pipelineMeta.findTransform(configurationTransform);
+ if (source != null
+ && source.isDistributes()
+ && transformMeta != null
+ && transformMeta.getCopies(variables) > 1) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_ERROR,
+ BaseMessages.getString(
+ PKG,
+ "AddSequenceMeta.CheckResult.ConfigurationRowDistributed",
+ configurationTransform),
+ transformMeta));
+ }
+ if (source == null) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_ERROR,
+ BaseMessages.getString(
+ PKG,
+
"AddSequenceMeta.CheckResult.ConfigurationTransformNotFound",
+ configurationTransform),
+ transformMeta));
+ } else {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_OK,
+ BaseMessages.getString(
+ PKG,
+
"AddSequenceMeta.CheckResult.ConfigurationTransformSelected",
+ configurationTransform),
+ transformMeta));
+ }
+ }
+
+ if (fieldsMissing || info == null || info.isEmpty()) {
+ return;
+ }
+
+ StringBuilder missing = new StringBuilder();
+ appendMissingConfigurationField(missing, info, startField);
+ appendMissingConfigurationField(missing, info, endField);
+ appendMissingConfigurationField(missing, info, incrementField);
+ if (!missing.isEmpty()) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_ERROR,
+ BaseMessages.getString(
+ PKG,
"AddSequenceMeta.CheckResult.ConfigurationFieldsNotFound", missing),
+ transformMeta));
+ } else {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_OK,
+ BaseMessages.getString(PKG,
"AddSequenceMeta.CheckResult.ConfigurationFieldsFound"),
+ transformMeta));
+ }
+ }
+
+ private static void appendMissingConfigurationField(
+ StringBuilder missing, IRowMeta info, String fieldName) {
+ String name = Const.trim(fieldName);
+ if (Utils.isEmpty(name) || info.indexOfValue(name) >= 0) {
+ return;
+ }
+ if (!missing.isEmpty()) {
+ missing.append(", ");
+ }
+ missing.append(name);
}
/**
@@ -300,4 +434,84 @@ public class AddSequenceMeta extends
BaseTransformMeta<AddSequence, AddSequenceD
return retval;
}
+
+ /**
+ * Keeps {@link #configurationTransform} in sync when the info hop is drawn,
split, or detached.
+ * {@link #searchInfoAndTargetTransforms} resolves the stream from that name.
+ */
+ @Override
+ public void handleStreamSelection(IStream stream) {
+ List<IStream> infoStreams = getTransformIOMeta().getInfoStreams();
+ if (infoStreams.isEmpty() || stream == null ||
!infoStreams.contains(stream)) {
+ return;
+ }
+ TransformMeta source = stream.getTransformMeta();
+ if (source == null) {
+ return;
+ }
+ setConfigurationTransform(source.getName());
+ stream.setSubject(source.getName());
+ }
+
+ @Override
+ public void searchInfoAndTargetTransforms(List<TransformMeta> transforms) {
+ List<IStream> infoStreams = getTransformIOMeta().getInfoStreams();
+ if (infoStreams.isEmpty()) {
+ return;
+ }
+ IStream stream = infoStreams.get(0);
+ if (!isConfigurationFromTransform()) {
+ stream.setTransformMeta(null);
+ return;
+ }
+ String lookupName = stream.getSubject();
+ if (!Utils.isEmpty(configurationTransform)) {
+ lookupName = configurationTransform;
+ stream.setSubject(configurationTransform);
+ }
+ stream.setTransformMeta(TransformMeta.findTransform(transforms,
Const.trim(lookupName)));
+ }
+
+ @Override
+ public void convertIOMetaToTransformNames() {
+ List<IStream> infoStreams = getTransformIOMeta().getInfoStreams();
+ if (infoStreams.isEmpty()) {
+ return;
+ }
+ String name = infoStreams.get(0).getTransformName();
+ if (!Utils.isEmpty(name)) {
+ configurationTransform = name;
+ }
+ }
+
+ @Override
+ public ITransformIOMeta getTransformIOMeta() {
+ ITransformIOMeta ioMeta = super.getTransformIOMeta(false);
+ if (ioMeta == null) {
+ ioMeta = new TransformIOMeta(true, true, false, false, false, false);
+ ioMeta.addStream(
+ new Stream(
+ StreamType.INFO,
+ null,
+ BaseMessages.getString(PKG,
"AddSequenceMeta.InfoStream.Description"),
+ StreamIcon.INFO,
+ configurationTransform));
+ setTransformIOMeta(ioMeta);
+ }
+ return ioMeta;
+ }
+
+ @Override
+ public void resetTransformIoMeta() {
+ // Keep the configuration info stream. Recreating it here drops the
transform it points at.
+ }
+
+ /**
+ * The configuration row does not have the same layout as the main input.
Skip the safe-mode row
+ * mixing check while that info hop is in use.
+ */
+ @Override
+ public boolean excludeFromRowLayoutVerification() {
+ return isConfigurationFromTransform();
+ }
}
diff --git
a/plugins/transforms/addsequence/src/main/resources/org/apache/hop/pipeline/transforms/addsequence/messages/messages_en_US.properties
b/plugins/transforms/addsequence/src/main/resources/org/apache/hop/pipeline/transforms/addsequence/messages/messages_en_US.properties
index 3d84741d40..1efb891dde 100644
---
a/plugins/transforms/addsequence/src/main/resources/org/apache/hop/pipeline/transforms/addsequence/messages/messages_en_US.properties
+++
b/plugins/transforms/addsequence/src/main/resources/org/apache/hop/pipeline/transforms/addsequence/messages/messages_en_US.properties
@@ -15,9 +15,18 @@
# limitations under the License.
#
+AddSequence.Exception.ConfigurationFieldIsNull=Sequence configuration field
[{0}] is null.
+AddSequence.Exception.ConfigurationFieldNotFound=Sequence configuration field
[{0}] could not be found.
+AddSequence.Exception.ConfigurationFieldNotNumeric=Sequence configuration
field [{0}] could not be read as an integer. Value\: [{1}]. {2}
+AddSequence.Exception.ConfigurationRowHasNoFields=Sequence configuration
transform [{0}] returned a row without fields.
+AddSequence.Exception.ConfigurationRowMissing=Sequence configuration transform
[{0}] did not return a row. It must return exactly one row.
+AddSequence.Exception.ConfigurationRowMissingInCopy=Sequence configuration
transform [{0}] did not send a row to copy {1} of this transform. When Add
Sequence runs in multiple copies, set the data movement of [{0}] to copy, so
every copy receives the configuration row.
+AddSequence.Exception.ConfigurationRowNotSingle=Sequence configuration
transform [{0}] returned more than one row. It must return exactly one row.
+AddSequence.Exception.ConfigurationTransformNotFound=Unable to read sequence
configuration from transform [{0}]. Connect that transform to this one.
AddSequence.Exception.CouldNotFindNextValueForSequence=Couldn''t find next
value for sequence \:
AddSequence.Exception.ErrorReadingSequence=Error reading next value of
sequence [{0}] from database
AddSequence.Exception.NoSpecifiedMethod=No method is specified in this
transform\!
+AddSequence.Log.ConfigurationFieldsMissing=Specify the start, end, and
increment fields to read from the configuration transform.
AddSequence.Log.ConnectedDB=Connected to database...
AddSequence.Log.CouldNotConnectToDB=transform couldn''t connect to the
database\:
AddSequence.Log.CouldNotParseCounterValue=Counter value for {0} could not be
parsed, original value is [{1}] which becomes [{2}]. Error\: {3}.
@@ -25,14 +34,21 @@ AddSequence.Log.ErrorInTransform=Because of an error, this
transform can''t cont
AddSequence.Log.LineNumber=linenr
AddSequence.Log.NeedToSelectSequence=You need to select to create a sequence
either using a database or an internal counter.
AddSequence.Log.PipelineCountersHashtableNotAllocated=Sorry, Pipeline counters
hash table not allocated\! (internal error)
+AddSequence.Log.ReadConfiguration=Sequence configuration from [{0}]\:
start={1}, end={2}, increment={3}
AddSequence.Log.ReadRow=Read row \#
AddSequence.Log.WriteRow=Wrote row \#
+AddSequenceDialog.ConfigurationTransform.Label=Configuration transform
+AddSequenceDialog.ConfigurationTransform.Tooltip=Optional transform that
provides exactly one row with the start, end, and increment. More than one row
stops the pipeline. When this is set, Start at, Increment by, and Maximum value
are not used.
AddSequenceDialog.CounterName.Label=Counter name (optional)
+AddSequenceDialog.EndField.Label=End field
+AddSequenceDialog.EndField.Tooltip=Field in the configuration row that
contains the maximum value. After this value the sequence returns to the start
value.
AddSequenceDialog.ErrorGettingSchemas=Error getting schemas!
AddSequenceDialog.ErrorGettingSequences=Error getting sequences!
AddSequenceDialog.GetSchemas.Label=Schemas...
AddSequenceDialog.GetSequences.Label=Sequences...
AddSequenceDialog.IncrBy.Label=Increment by
+AddSequenceDialog.IncrementField.Label=Increment field
+AddSequenceDialog.IncrementField.Tooltip=Field in the configuration row that
contains the amount by which the sequence increases or decreases.
AddSequenceDialog.Log.GettingKeyInfo=getting key info...
AddSequenceDialog.MaxVal.Label=Maximum value
AddSequenceDialog.NoSchema.Message=No schema is available!
@@ -46,6 +62,8 @@ AddSequenceDialog.SelectSequence.Title=Available sequences on
[{0}]
AddSequenceDialog.Seqname.Label=Sequence name
AddSequenceDialog.Shell.Title=Add sequence
AddSequenceDialog.StartAt.Label=Start at value
+AddSequenceDialog.StartField.Label=Start field
+AddSequenceDialog.StartField.Tooltip=Field in the configuration row that
contains the value to begin the sequence with.
AddSequenceDialog.TargetSchema.Label=Schema name
AddSequenceDialog.TransformName.Label=Transform name
AddSequenceDialog.UseCounter.Label=Use counter to calculate sequence
@@ -55,6 +73,12 @@ AddSequenceDialog.UseDatabase.Label=Use DB to get sequence
AddSequenceDialog.UseDatabase.Tooltip=Check this to use a database (probably
Oracle) to get the value of a sequence.
AddSequenceDialog.UseDatabaseGroup.Label=Use a database to generate the
sequence
AddSequenceDialog.Valuename.Label=Name of value
+AddSequenceMeta.CheckResult.ConfigurationFieldsFound=Start, end, and increment
fields were found in the configuration transform.
+AddSequenceMeta.CheckResult.ConfigurationFieldsMissing=The configuration
transform needs a start field, an end field, and an increment field.
+AddSequenceMeta.CheckResult.ConfigurationFieldsNotFound=These fields could not
be found in the configuration transform\: {0}
+AddSequenceMeta.CheckResult.ConfigurationTransformNotFound=Configuration
transform [{0}] could not be found.
+AddSequenceMeta.CheckResult.ConfigurationTransformSelected=Configuration
transform [{0}] is selected.
+AddSequenceMeta.CheckResult.ConfigurationRowDistributed=Add Sequence runs in
multiple copies, but configuration transform [{0}] distributes its rows. Only
the first copy receives the configuration row. Set the data movement of [{0}]
to copy.
AddSequenceMeta.CheckResult.NoInputReceived.Title=No input received from other
transforms\!
AddSequenceMeta.CheckResult.SequenceCouldNotBeFound.Title=The sequence [{0}]
couldn''t be found
AddSequenceMeta.CheckResult.SequenceExists.Title=Sequence exits.
@@ -62,13 +86,18 @@
AddSequenceMeta.CheckResult.TransformIsReceving.Title=Transform is receiving inf
AddSequenceMeta.CheckResult.UnableToConnectDB.Title=Unable to connect to
database to verify sequence because of an error\:
AddSequenceMeta.ErrorMessage.NoConnectionDefined=There is no connection
defined in this transform.
AddSequenceMeta.ErrorMessage.UnableToConnectDB=I was unable to connect to the
database to verify the status of the table.
+AddSequenceMeta.InfoStream.Description=One row with the sequence start, end,
and increment
+AddSequenceMeta.Injection.ConfigurationTransform=Configuration transform
AddSequenceMeta.Injection.Connection=Database connection
AddSequenceMeta.Injection.CounterName=Counter name
+AddSequenceMeta.Injection.EndField=End field
AddSequenceMeta.Injection.IncrementBy=Increment by
+AddSequenceMeta.Injection.IncrementField=Increment field
AddSequenceMeta.Injection.MaxValue=Maximum value
AddSequenceMeta.Injection.SchemaName=Schema name
AddSequenceMeta.Injection.SequenceName=Sequence name
AddSequenceMeta.Injection.StartAt=Start at
+AddSequenceMeta.Injection.StartField=Start field
AddSequenceMeta.Injection.UseCounter=Use counter?
AddSequenceMeta.keyword=sequence,counter,increment,autonumber,nextval
BaseTransform.TypeLongDesc.AddSequence=Add sequence
diff --git
a/plugins/transforms/addsequence/src/test/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceMetaTest.java
b/plugins/transforms/addsequence/src/test/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceMetaTest.java
index ff3e87cf19..0a7b81775a 100644
---
a/plugins/transforms/addsequence/src/test/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceMetaTest.java
+++
b/plugins/transforms/addsequence/src/test/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceMetaTest.java
@@ -35,13 +35,17 @@ import org.apache.hop.core.ICheckResult;
import org.apache.hop.core.SqlStatement;
import org.apache.hop.core.database.DatabaseMeta;
import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaInteger;
import org.apache.hop.core.variables.Variables;
import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
import org.apache.hop.metadata.api.IHopMetadataProvider;
import org.apache.hop.metadata.api.IHopMetadataSerializer;
import org.apache.hop.metadata.validation.ReferencedDatabaseConnectionChecker;
+import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transform.stream.IStream;
import org.apache.hop.pipeline.transforms.loadsave.LoadSaveTester;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -70,7 +74,11 @@ class AddSequenceMetaTest {
"counterName",
"startAt",
"incrementBy",
- "maxValue");
+ "maxValue",
+ "configurationTransform",
+ "startField",
+ "endField",
+ "incrementField");
LoadSaveTester<AddSequenceMeta> loadSaveTester =
new LoadSaveTester<>(
@@ -143,6 +151,15 @@ class AddSequenceMetaTest {
meta.setMaxValueByValue(10000L);
assertEquals("10000", meta.getMaxValue());
+
+ meta.setConfigurationTransform("Max query");
+ assertEquals("Max query", meta.getConfigurationTransform());
+ meta.setStartField("start_value");
+ assertEquals("start_value", meta.getStartField());
+ meta.setEndField("end_value");
+ assertEquals("end_value", meta.getEndField());
+ meta.setIncrementField("increment_value");
+ assertEquals("increment_value", meta.getIncrementField());
}
@Test
@@ -160,6 +177,217 @@ class AddSequenceMetaTest {
assertEquals(meta.isDatabaseUsed(), cloned.isDatabaseUsed());
assertEquals(meta.getConnection(), cloned.getConnection());
assertEquals(meta.getStartAt(), cloned.getStartAt());
+
+ meta.setConfigurationTransform("Max query");
+ meta.setStartField("start_value");
+ meta.setEndField("end_value");
+ meta.setIncrementField("increment_value");
+ cloned = (AddSequenceMeta) meta.clone();
+ assertEquals("Max query", cloned.getConfigurationTransform());
+ assertEquals("start_value", cloned.getStartField());
+ assertEquals("end_value", cloned.getEndField());
+ assertEquals("increment_value", cloned.getIncrementField());
+ }
+
+ @Test
+ void configurationTransformIsAnInfoStream() {
+ AddSequenceMeta meta = new AddSequenceMeta();
+ meta.setDefault();
+ meta.setConfigurationTransform("Max query");
+ meta.setStartField("start_value");
+ meta.setEndField("end_value");
+ meta.setIncrementField("increment_value");
+
+ TransformMeta source = new TransformMeta();
+ source.setName("Max query");
+ meta.searchInfoAndTargetTransforms(List.of(source));
+
+ assertEquals(1, meta.getTransformIOMeta().getInfoStreams().size());
+ IStream stream = meta.getTransformIOMeta().getInfoStreams().get(0);
+ assertEquals(source, stream.getTransformMeta());
+ assertEquals("Max query", stream.getSubject());
+ assertTrue(meta.isConfigurationFromTransform());
+ assertTrue(meta.excludeFromRowLayoutVerification());
+
+ meta.handleStreamSelection(stream);
+ assertEquals("Max query", meta.getConfigurationTransform());
+
+ meta.resetTransformIoMeta();
+ assertEquals(1, meta.getTransformIOMeta().getInfoStreams().size());
+ assertEquals(source,
meta.getTransformIOMeta().getInfoStreams().get(0).getTransformMeta());
+
+ meta.setDatabaseUsed(true);
+ meta.setCounterUsed(false);
+ meta.searchInfoAndTargetTransforms(List.of(source));
+
assertNull(meta.getTransformIOMeta().getInfoStreams().get(0).getTransformMeta());
+ assertFalse(meta.isConfigurationFromTransform());
+ assertFalse(meta.excludeFromRowLayoutVerification());
+ }
+
+ @Test
+ void checkReportsConfigurationTransformProblems() throws Exception {
+ AddSequenceMeta meta = new AddSequenceMeta();
+ meta.setDefault();
+ meta.setConfigurationTransform("Max query");
+ TransformMeta transformMeta = new TransformMeta("Add sequence", meta);
+
+ List<ICheckResult> missingFields = new ArrayList<>();
+ meta.check(
+ missingFields,
+ null,
+ transformMeta,
+ null,
+ new String[] {"Generate rows"},
+ new String[0],
+ null,
+ new Variables(),
+ metadataProviderThatCannotLoad());
+ assertTrue(
+ missingFields.stream()
+ .anyMatch(
+ remark ->
+ remark.getType() == ICheckResult.TYPE_RESULT_ERROR
+ && remark
+ .getText()
+ .equals(
+ BaseMessages.getString(
+ AddSequenceMeta.class,
+
"AddSequenceMeta.CheckResult.ConfigurationFieldsMissing"))));
+
+ meta.setStartField("start_value");
+ meta.setEndField("end_value");
+ meta.setIncrementField("increment_value");
+ PipelineMeta pipelineMeta = mock(PipelineMeta.class);
+ when(pipelineMeta.findTransform("Max query")).thenReturn(null);
+ List<ICheckResult> missingTransform = new ArrayList<>();
+ meta.check(
+ missingTransform,
+ pipelineMeta,
+ transformMeta,
+ null,
+ new String[] {"Generate rows"},
+ new String[0],
+ null,
+ new Variables(),
+ metadataProviderThatCannotLoad());
+ assertTrue(
+ missingTransform.stream()
+ .anyMatch(
+ remark ->
+ remark.getType() == ICheckResult.TYPE_RESULT_ERROR
+ && remark.getText().contains("Max query")));
+
+ TransformMeta source = new TransformMeta();
+ source.setName("Max query");
+ when(pipelineMeta.findTransform("Max query")).thenReturn(source);
+ RowMeta info = new RowMeta();
+ info.addValueMeta(new ValueMetaInteger("start_value"));
+ info.addValueMeta(new ValueMetaInteger("other"));
+ List<ICheckResult> missingInfoFields = new ArrayList<>();
+ meta.check(
+ missingInfoFields,
+ pipelineMeta,
+ transformMeta,
+ null,
+ new String[] {"Generate rows"},
+ new String[0],
+ info,
+ new Variables(),
+ metadataProviderThatCannotLoad());
+ assertTrue(
+ missingInfoFields.stream()
+ .anyMatch(
+ remark ->
+ remark.getType() == ICheckResult.TYPE_RESULT_ERROR
+ && remark.getText().contains("end_value")
+ && remark.getText().contains("increment_value")));
+
+ info.addValueMeta(new ValueMetaInteger("end_value"));
+ info.addValueMeta(new ValueMetaInteger("increment_value"));
+ List<ICheckResult> ok = new ArrayList<>();
+ meta.check(
+ ok,
+ pipelineMeta,
+ transformMeta,
+ null,
+ new String[] {"Generate rows"},
+ new String[0],
+ info,
+ new Variables(),
+ metadataProviderThatCannotLoad());
+ assertTrue(
+ ok.stream()
+ .anyMatch(
+ remark ->
+ remark.getType() == ICheckResult.TYPE_RESULT_OK
+ && remark
+ .getText()
+ .equals(
+ BaseMessages.getString(
+ AddSequenceMeta.class,
+
"AddSequenceMeta.CheckResult.ConfigurationFieldsFound"))));
+ assertTrue(ok.stream().noneMatch(remark -> remark.getType() ==
ICheckResult.TYPE_RESULT_ERROR));
+ }
+
+ /**
+ * A distributing configuration transform only sends its single row to the
first copy of Add
+ * Sequence. Verify has to say so before the other copies fail at runtime.
+ */
+ @Test
+ void checkReportsADistributedConfigurationRowWithMultipleCopies() throws
Exception {
+ AddSequenceMeta meta = new AddSequenceMeta();
+ meta.setDefault();
+ meta.setConfigurationTransform("Max query");
+ meta.setStartField("start_value");
+ meta.setEndField("end_value");
+ meta.setIncrementField("increment_value");
+ TransformMeta transformMeta = new TransformMeta("Add sequence", meta);
+ TransformMeta source = new TransformMeta();
+ source.setName("Max query");
+ source.setDistributes(true);
+ PipelineMeta pipelineMeta = mock(PipelineMeta.class);
+ when(pipelineMeta.findTransform("Max query")).thenReturn(source);
+ // Copies are only resolved against variables for a transform in a
pipeline.
+ transformMeta.setParentPipelineMeta(pipelineMeta);
+ String distributed =
+ BaseMessages.getString(
+ AddSequenceMeta.class,
+ "AddSequenceMeta.CheckResult.ConfigurationRowDistributed",
+ "Max query");
+
+ // One copy: distributing is fine.
+ assertFalse(hasError(meta, pipelineMeta, transformMeta, distributed));
+
+ // Several copies, also from a variable, and the source distributes: an
error.
+ transformMeta.setCopiesString("${COPIES}");
+ assertTrue(hasError(meta, pipelineMeta, transformMeta, distributed));
+
+ // Several copies, and the source copies its rows to every copy: fine.
+ source.setDistributes(false);
+ assertFalse(hasError(meta, pipelineMeta, transformMeta, distributed));
+ }
+
+ private boolean hasError(
+ AddSequenceMeta meta, PipelineMeta pipelineMeta, TransformMeta
transformMeta, String text)
+ throws Exception {
+ Variables variables = new Variables();
+ variables.setVariable("COPIES", "3");
+ List<ICheckResult> remarks = new ArrayList<>();
+ meta.check(
+ remarks,
+ pipelineMeta,
+ transformMeta,
+ null,
+ new String[] {"Generate rows"},
+ new String[0],
+ null,
+ variables,
+ metadataProviderThatCannotLoad());
+ return remarks.stream()
+ .anyMatch(
+ remark ->
+ remark.getType() == ICheckResult.TYPE_RESULT_ERROR
+ && remark.getText().equals(text));
}
/**
diff --git
a/plugins/transforms/addsequence/src/test/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceTest.java
b/plugins/transforms/addsequence/src/test/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceTest.java
index c0b5fe806a..d74875c30c 100644
---
a/plugins/transforms/addsequence/src/test/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceTest.java
+++
b/plugins/transforms/addsequence/src/test/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceTest.java
@@ -23,22 +23,28 @@ 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 static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Stream;
import org.apache.hop.core.Counters;
import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.IRowSet;
import org.apache.hop.core.database.Database;
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.ValueMetaBigNumber;
import org.apache.hop.core.row.value.ValueMetaInteger;
import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
import org.apache.hop.pipeline.PipelineTestingUtil;
import org.apache.hop.pipeline.transforms.mock.TransformMockHelper;
@@ -430,4 +436,229 @@ class AddSequenceTest {
addSequence.dispose();
}
+
+ /** Start, end, and increment come from one info row. Typed counter values
are ignored. */
+ @Test
+ void testProcessRowReadsSingleConfigurationRow() throws Exception {
+ AddSequenceMeta meta = configurationMeta();
+ RowMeta configMeta = new RowMeta();
+ configMeta.addValueMeta(new ValueMetaBigNumber("start_value"));
+ configMeta.addValueMeta(new ValueMetaBigNumber("end_value"));
+ configMeta.addValueMeta(new ValueMetaInteger("increment_value"));
+ IRowSet rowSet = mock(IRowSet.class);
+ when(rowSet.getRowMeta()).thenReturn(configMeta);
+
+ AddSequence addSequence = spyConfiguration(meta, rowSet);
+ doReturn(new Object[] {new BigDecimal("100"), new BigDecimal("102"), 2L})
+ .doReturn(null)
+ .when(addSequence)
+ .getRowFrom(rowSet);
+ doReturn(new Object[] {"A"})
+ .doReturn(new Object[] {"B"})
+ .doReturn(new Object[] {"C"})
+ .doReturn(null)
+ .when(addSequence)
+ .getRow();
+
+ List<Object[]> result = PipelineTestingUtil.execute(addSequence, 3, false);
+
+ assertEquals(100L, result.get(0)[1]);
+ assertEquals(102L, result.get(1)[1]);
+ // End is the maximum: the next value wraps back to the start.
+ assertEquals(100L, result.get(2)[1]);
+ assertNotNull(addSequence.getData().counter);
+ addSequence.dispose();
+ }
+
+ @Test
+ void testProcessRowReadsStringConfigurationValues() throws Exception {
+ AddSequenceMeta meta = configurationMeta();
+ RowMeta configMeta = new RowMeta();
+ configMeta.addValueMeta(new ValueMetaString("start_value"));
+ configMeta.addValueMeta(new ValueMetaString("end_value"));
+ configMeta.addValueMeta(new ValueMetaString("increment_value"));
+ IRowSet rowSet = mock(IRowSet.class);
+ when(rowSet.getRowMeta()).thenReturn(configMeta);
+
+ AddSequence addSequence = spyConfiguration(meta, rowSet);
+ doReturn(new Object[] {"50", "1000",
"5"}).doReturn(null).when(addSequence).getRowFrom(rowSet);
+ doReturn(new Object[] {"A"})
+ .doReturn(new Object[] {"B"})
+ .doReturn(null)
+ .when(addSequence)
+ .getRow();
+
+ List<Object[]> result = PipelineTestingUtil.execute(addSequence, 2, false);
+
+ assertEquals(50L, result.get(0)[1]);
+ assertEquals(55L, result.get(1)[1]);
+ addSequence.dispose();
+ }
+
+ @Test
+ void testConfigurationTransformWithNoRowFails() throws Exception {
+ AddSequence addSequence = spyConfiguration(configurationMeta(),
rowSetWith(configRowMeta()));
+ doReturn(null).when(addSequence).getRowFrom(any(IRowSet.class));
+
+ assertFalse(addSequence.processRow());
+ assertEquals(1L, addSequence.getErrors());
+ assertNull(addSequence.getData().counter);
+ addSequence.dispose();
+ }
+
+ /**
+ * With several copies of Add Sequence, a distributed configuration row only
reaches one of them.
+ * The others have to say how to fix that, not just that the row is missing.
+ */
+ @Test
+ void testConfigurationRowMissingInACopyExplainsHowToFixIt() throws Exception
{
+ when(transformMockHelper.transformMeta.getCopies(any())).thenReturn(2);
+ AddSequenceMeta meta = configurationMeta();
+ AddSequence addSequence = spyConfiguration(meta,
rowSetWith(configRowMeta()));
+ doReturn(null).when(addSequence).getRowFrom(any(IRowSet.class));
+
+ assertFalse(addSequence.processRow());
+ assertEquals(1L, addSequence.getErrors());
+ String expected =
+ BaseMessages.getString(
+ AddSequence.class,
+ "AddSequence.Exception.ConfigurationRowMissingInCopy",
+ meta.getConfigurationTransform(),
+ "0");
+ verify(transformMockHelper.iLogChannel)
+ .logError(argThat((String message) -> message != null &&
message.contains(expected)));
+ addSequence.dispose();
+ }
+
+ @Test
+ void testConfigurationTransformWithTwoRowsFails() throws Exception {
+ AddSequence addSequence = spyConfiguration(configurationMeta(),
rowSetWith(configRowMeta()));
+ doReturn(new Object[] {1L, 10L, 1L})
+ .doReturn(new Object[] {2L, 10L, 1L})
+ .when(addSequence)
+ .getRowFrom(any(IRowSet.class));
+
+ assertFalse(addSequence.processRow());
+ assertEquals(1L, addSequence.getErrors());
+ assertNull(addSequence.getData().counter);
+ addSequence.dispose();
+ }
+
+ @Test
+ void testConfigurationTransformNotConnectedFails() throws Exception {
+ AddSequenceMeta meta = configurationMeta();
+ AddSequence addSequence =
+ new AddSequence(
+ transformMockHelper.transformMeta,
+ meta,
+ new AddSequenceData(),
+ 0,
+ transformMockHelper.pipelineMeta,
+ transformMockHelper.pipeline);
+ when(transformMockHelper.pipeline.getContainerId()).thenReturn("cfg-" +
System.nanoTime());
+ assertTrue(addSequence.init());
+ addSequence = spy(addSequence);
+
doReturn(null).when(addSequence).findInputRowSet(meta.getConfigurationTransform());
+
+ assertFalse(addSequence.processRow());
+ assertEquals(1L, addSequence.getErrors());
+ addSequence.dispose();
+ }
+
+ @Test
+ void testConfigurationFieldMissingFails() throws Exception {
+ RowMeta configMeta = new RowMeta();
+ configMeta.addValueMeta(new ValueMetaInteger("other"));
+ AddSequence addSequence = spyConfiguration(configurationMeta(),
rowSetWith(configMeta));
+ doReturn(new Object[]
{1L}).doReturn(null).when(addSequence).getRowFrom(any(IRowSet.class));
+
+ assertFalse(addSequence.processRow());
+ assertEquals(1L, addSequence.getErrors());
+ assertNull(addSequence.getData().counter);
+ addSequence.dispose();
+ }
+
+ @Test
+ void testConfigurationFieldNullFails() throws Exception {
+ AddSequence addSequence = spyConfiguration(configurationMeta(),
rowSetWith(configRowMeta()));
+ doReturn(new Object[] {null, 10L, 1L})
+ .doReturn(null)
+ .when(addSequence)
+ .getRowFrom(any(IRowSet.class));
+
+ assertFalse(addSequence.processRow());
+ assertEquals(1L, addSequence.getErrors());
+ addSequence.dispose();
+ }
+
+ @Test
+ void testInitSkipsTypedCounterValuesWhenConfigurationTransformIsSet() {
+ AddSequenceMeta meta = configurationMeta();
+ when(transformMockHelper.pipeline.getContainerId()).thenReturn("cfg-" +
System.nanoTime());
+ AddSequence addSequence =
+ new AddSequence(
+ transformMockHelper.transformMeta,
+ meta,
+ new AddSequenceData(),
+ 0,
+ transformMockHelper.pipelineMeta,
+ transformMockHelper.pipeline);
+
+ assertTrue(addSequence.init());
+ assertNull(addSequence.getData().counter);
+
+ meta.setStartField("");
+ assertFalse(addSequence.init());
+ addSequence.dispose();
+ }
+
+ private static AddSequenceMeta configurationMeta() {
+ AddSequenceMeta meta = new AddSequenceMeta();
+ meta.setDefault();
+ meta.setValueName("id");
+ meta.setCounterUsed(true);
+ meta.setDatabaseUsed(false);
+ meta.setConfigurationTransform("Max query");
+ meta.setStartField("start_value");
+ meta.setEndField("end_value");
+ meta.setIncrementField("increment_value");
+ // Proves the typed values are not parsed when the configuration transform
is set.
+ meta.setStartAt("not-a-number");
+ meta.setIncrementBy("not-a-number");
+ meta.setMaxValue("not-a-number");
+ return meta;
+ }
+
+ private static RowMeta configRowMeta() {
+ RowMeta configMeta = new RowMeta();
+ configMeta.addValueMeta(new ValueMetaInteger("start_value"));
+ configMeta.addValueMeta(new ValueMetaInteger("end_value"));
+ configMeta.addValueMeta(new ValueMetaInteger("increment_value"));
+ return configMeta;
+ }
+
+ private static IRowSet rowSetWith(RowMeta configMeta) {
+ IRowSet rowSet = mock(IRowSet.class);
+ when(rowSet.getRowMeta()).thenReturn(configMeta);
+ return rowSet;
+ }
+
+ private AddSequence spyConfiguration(AddSequenceMeta meta, IRowSet rowSet)
throws Exception {
+ when(transformMockHelper.pipeline.getContainerId()).thenReturn("cfg-" +
System.nanoTime());
+ AddSequence addSequence =
+ new AddSequence(
+ transformMockHelper.transformMeta,
+ meta,
+ new AddSequenceData(),
+ 0,
+ transformMockHelper.pipelineMeta,
+ transformMockHelper.pipeline);
+ assertTrue(addSequence.init());
+ RowMeta inputRowMeta = new RowMeta();
+ inputRowMeta.addValueMeta(new ValueMetaString("name"));
+ addSequence.setInputRowMeta(inputRowMeta);
+ addSequence = spy(addSequence);
+
doReturn(rowSet).when(addSequence).findInputRowSet(meta.getConfigurationTransform());
+ return addSequence;
+ }
}