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 496044bda2 Fixes #8670: Fix execution details and parent links in
execution information (#8850)
496044bda2 is described below
commit 496044bda20e0b32381439bc5576c6507c10eee2
Author: Matt Casters <[email protected]>
AuthorDate: Sun Oct 11 07:58:27 2026 +0200
Fixes #8670: Fix execution details and parent links in execution
information (#8850)
* Fixes #8670: Fix execution information parent-child links and transform
row sampling
- Fix row sampling clearing issue: create defensive copies in RowBuffer and
data sampler stores so that clearing store samples does not wipe out row
buffers in ExecutionData before persisting.
- Close execution info location before releasing published samples in
LocalPipelineEngine.
- Fix parent/child link resolution in caching and file execution info
locations (BaseCachingExecutionInfoLocation, CachingFileExecutionInfoLocation,
CachingDatabaseExecutionInfoLocation, FileExecutionInfoLocation) when resolving
child actions and pipelines.
- Fix drill down in WorkflowExecutionViewer when selectedExecutionData is
null.
- Add integration test suite 002 in integration-tests/exec-info to
reproduce and validate the fix.
* Address review feedback on #8850: fix database child matching and bound
file scans
---
.../java/org/apache/hop/core/row/RowBuffer.java | 8 +-
.../caching/BaseCachingExecutionInfoLocation.java | 64 ++-
.../apache/hop/execution/caching/CacheEntry.java | 31 +-
.../caching/CachingFileExecutionInfoLocation.java | 62 ++-
.../execution/local/FileExecutionInfoLocation.java | 8 +-
.../first/FirstRowsExecutionDataSamplerStore.java | 10 +-
.../last/LastRowsExecutionDataSamplerStore.java | 10 +-
.../RandomRowsExecutionDataSamplerStore.java | 10 +-
.../engines/local/LocalPipelineEngine.java | 4 +-
.../BaseCachingExecutionInfoLocationTest.java | 90 +++
.../CachingFileExecutionInfoLocationTest.java | 63 +++
integration-tests/exec-info/002-mapping-sub.hpl | 134 +++++
.../exec-info/002-pipeline-with-mapping.hpl | 219 ++++++++
integration-tests/exec-info/002-sub-workflow.hwf | 86 +++
integration-tests/exec-info/002-validation.hpl | 615 +++++++++++++++++++++
.../main-002-workflow-pipeline-mapping.hwf | 138 +++++
.../CachingDatabaseExecutionInfoLocation.java | 83 +++
.../CachingDatabaseExecutionInfoLocationTest.java | 66 +++
.../execution/WorkflowExecutionViewer.java | 25 +-
19 files changed, 1707 insertions(+), 19 deletions(-)
diff --git a/core/src/main/java/org/apache/hop/core/row/RowBuffer.java
b/core/src/main/java/org/apache/hop/core/row/RowBuffer.java
index 9427230ad8..9f63324767 100644
--- a/core/src/main/java/org/apache/hop/core/row/RowBuffer.java
+++ b/core/src/main/java/org/apache/hop/core/row/RowBuffer.java
@@ -48,7 +48,13 @@ public class RowBuffer {
*/
public RowBuffer(IRowMeta rowMeta, List<Object[]> buffer) {
this.rowMeta = rowMeta;
- this.buffer = buffer;
+ if (buffer == null) {
+ this.buffer = Collections.synchronizedList(new ArrayList<>());
+ } else {
+ synchronized (buffer) {
+ this.buffer = Collections.synchronizedList(new ArrayList<>(buffer));
+ }
+ }
}
/**
diff --git
a/engine/src/main/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocation.java
b/engine/src/main/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocation.java
index 4704a1c010..21425f8cce 100644
---
a/engine/src/main/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocation.java
+++
b/engine/src/main/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocation.java
@@ -798,9 +798,15 @@ public abstract class BaseCachingExecutionInfoLocation
implements IExecutionInfo
@Override
public synchronized Execution getExecution(String executionId) throws
HopException {
CacheEntry entry = findCacheEntry(executionId);
+ if (entry == null) {
+ entry = findCacheEntryWithChild(executionId);
+ }
if (entry == null) {
return null;
}
+ if (!entry.getId().equals(executionId)) {
+ return entry.getChildExecution(executionId);
+ }
Execution execution = entry.getExecution();
if (execution != null
&& entry.isHeavyDocumentStored()
@@ -829,9 +835,25 @@ public abstract class BaseCachingExecutionInfoLocation
implements IExecutionInfo
try {
Set<Execution> executions = new HashSet<>();
- for (String id : getExecutionIds(true, 10000)) {
+ if (StringUtils.isNotEmpty(parentExecutionId)) {
+ CacheEntry parentEntry = findCacheEntry(parentExecutionId);
+ if (parentEntry == null) {
+ parentEntry = findCacheEntryWithChild(parentExecutionId);
+ }
+ if (parentEntry != null && parentEntry.getChildExecutions() != null) {
+ for (Execution childExecution :
parentEntry.getChildExecutions().values()) {
+ if (childExecution != null &&
parentExecutionId.equals(childExecution.getParentId())) {
+ executions.add(childExecution);
+ }
+ }
+ }
+ }
+
+ for (String id : getExecutionIds(false, 10000)) {
Execution execution = getExecution(id);
- if (execution != null &&
parentExecutionId.equals(execution.getParentId())) {
+ if (execution != null
+ && parentExecutionId != null
+ && parentExecutionId.equals(execution.getParentId())) {
executions.add(execution);
}
}
@@ -974,10 +996,46 @@ public abstract class BaseCachingExecutionInfoLocation
implements IExecutionInfo
@Override
public String findParentId(String childId) throws HopException {
+ if (StringUtils.isEmpty(childId)) {
+ return null;
+ }
CacheEntry cacheEntry = findCacheEntry(childId);
+ if (cacheEntry == null) {
+ cacheEntry = findCacheEntryWithChild(childId);
+ }
if (cacheEntry == null) {
return null;
}
- return cacheEntry.getId();
+ if (cacheEntry.getId().equals(childId)) {
+ return cacheEntry.getExecution() != null ?
cacheEntry.getExecution().getParentId() : null;
+ }
+ Execution child = cacheEntry.getChildExecution(childId);
+ if (child != null && child.getParentId() != null) {
+ return child.getParentId();
+ }
+ if (child != null
+ || (cacheEntry.getChildIds() != null &&
cacheEntry.getChildIds().contains(childId))) {
+ return cacheEntry.getId();
+ }
+ return null;
+ }
+
+ protected synchronized CacheEntry findCacheEntryWithChild(String childId)
throws HopException {
+ if (StringUtils.isEmpty(childId)) {
+ return null;
+ }
+ for (CacheEntry cacheEntry : cache.values()) {
+ if (cacheEntry.peekChildExecution(childId) != null
+ || (cacheEntry.getChildIds() != null &&
cacheEntry.getChildIds().contains(childId))) {
+ cache.get(cacheEntry.getId());
+ cacheEntry.markRead();
+ return cacheEntry;
+ }
+ }
+ return loadCacheEntryWithChild(childId);
+ }
+
+ protected CacheEntry loadCacheEntryWithChild(String childId) throws
HopException {
+ return null;
}
}
diff --git
a/engine/src/main/java/org/apache/hop/execution/caching/CacheEntry.java
b/engine/src/main/java/org/apache/hop/execution/caching/CacheEntry.java
index 697e84c925..f5caf13fa9 100644
--- a/engine/src/main/java/org/apache/hop/execution/caching/CacheEntry.java
+++ b/engine/src/main/java/org/apache/hop/execution/caching/CacheEntry.java
@@ -35,6 +35,8 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.vfs2.FileObject;
import org.apache.hop.core.Const;
import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.json.HopJson;
+import org.apache.hop.core.row.RowBuffer;
import org.apache.hop.core.variables.IVariables;
import org.apache.hop.core.vfs.HopVfs;
import org.apache.hop.execution.Execution;
@@ -168,7 +170,7 @@ public class CacheEntry {
// was created successfully via VFS. Named schemes (db-volume://) need
variables so providers
// load from project metadata.
try (OutputStream os = HopVfs.getOutputStream(filename, false, variables))
{
- ObjectMapper objectMapper = new ObjectMapper();
+ ObjectMapper objectMapper = HopJson.newMapper();
objectMapper.writeValue(os, this);
} catch (Exception e) {
throw new HopException("Error writing cache entry to file '" +
targetFilename + "'", e);
@@ -257,7 +259,30 @@ public class CacheEntry {
}
public void addExecutionData(ExecutionData executionData) {
- childExecutionData.put(executionData.getOwnerId(), executionData);
+ ExecutionData existing =
childExecutionData.get(executionData.getOwnerId());
+ if (existing == null) {
+ childExecutionData.put(executionData.getOwnerId(), executionData);
+ } else {
+ existing.setFinished(executionData.isFinished() ||
existing.isFinished());
+ existing.setCollectionDate(executionData.getCollectionDate());
+ if (executionData.getDataSetMeta() != null) {
+ existing.setDataSetMeta(executionData.getDataSetMeta());
+ }
+ if (executionData.getSetMetaData() != null) {
+ existing.getSetMetaData().putAll(executionData.getSetMetaData());
+ }
+ if (executionData.getDataSets() != null) {
+ for (Map.Entry<String, RowBuffer> entry :
executionData.getDataSets().entrySet()) {
+ RowBuffer newBuffer = entry.getValue();
+ RowBuffer existingBuffer =
existing.getDataSets().get(entry.getKey());
+ if (newBuffer != null && !newBuffer.isEmpty()) {
+ existing.getDataSets().put(entry.getKey(), newBuffer);
+ } else if (existingBuffer == null) {
+ existing.getDataSets().put(entry.getKey(), newBuffer);
+ }
+ }
+ }
+ }
flagDirty();
}
@@ -287,7 +312,7 @@ public class CacheEntry {
}
/** Read a child without counting as a cache hit. */
- Execution peekChildExecution(String id) {
+ public Execution peekChildExecution(String id) {
if (childExecutions == null) {
return null;
}
diff --git
a/engine/src/main/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocation.java
b/engine/src/main/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocation.java
index e3dfd0fecf..cd5728c324 100644
---
a/engine/src/main/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocation.java
+++
b/engine/src/main/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocation.java
@@ -23,11 +23,14 @@ import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
+import java.util.ArrayList;
import java.util.Date;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.Getter;
@@ -410,7 +413,7 @@ public class CachingFileExecutionInfoLocation extends
BaseCachingExecutionInfoLo
if (!HopVfs.fileExists(filename, variables)) {
return null;
}
- ObjectMapper objectMapper = new ObjectMapper(HopJson.newFactory());
+ ObjectMapper objectMapper = HopJson.newMapper();
return objectMapper.readValue(HopVfs.getInputStream(filename,
variables), CacheEntry.class);
} catch (Exception e) {
throw new HopException(
@@ -419,6 +422,63 @@ public class CachingFileExecutionInfoLocation extends
BaseCachingExecutionInfoLo
}
}
+ public static final int MAX_CHILD_SEARCH_SCAN = 100;
+
+ @Override
+ protected synchronized CacheEntry loadCacheEntryWithChild(String childId)
throws HopException {
+ if (StringUtils.isEmpty(childId)) {
+ return null;
+ }
+ try {
+ FileObject[] files = getAllFileObjects(actualRootFolder);
+ if (files == null || files.length == 0) {
+ return null;
+ }
+
+ try {
+ record FileWithTimestamp(FileObject file, long lastModified) {}
+ List<FileWithTimestamp> fileList = new ArrayList<>(files.length);
+ for (FileObject file : files) {
+ long lastModified = 0L;
+ try {
+ lastModified = file.getContent().getLastModifiedTime();
+ } catch (Exception ignored) {
+ }
+ fileList.add(new FileWithTimestamp(file, lastModified));
+ }
+ fileList.sort((a, b) -> Long.compare(b.lastModified, a.lastModified));
+
+ int maxScan = Math.min(fileList.size(),
Math.max(MAX_CHILD_SEARCH_SCAN, maxSize * 2));
+ for (int i = 0; i < maxScan; i++) {
+ FileObject file = fileList.get(i).file();
+ String id = getIdFromFileName(file);
+ if (id.equals(childId)) {
+ continue;
+ }
+ String content =
+ HopVfs.getTextFileContent(file.getName().getURI(),
StandardCharsets.UTF_8);
+ if (content != null && content.contains(childId)) {
+ CacheEntry entry = HopJson.newMapper().readValue(content,
CacheEntry.class);
+ if (entry != null
+ && (entry.peekChildExecution(childId) != null
+ || (entry.getChildIds() != null &&
entry.getChildIds().contains(childId)))) {
+ cache.put(entry.getId(), entry);
+ enforceMaxCacheSize();
+ return entry;
+ }
+ }
+ }
+ } finally {
+ for (FileObject file : files) {
+ closeQuietly(file);
+ }
+ }
+ } catch (Exception e) {
+ LogChannel.GENERAL.logError("Error searching for cache entry containing
child " + childId, e);
+ }
+ return null;
+ }
+
@Override
protected void retrieveIds(
boolean includeChildren, Set<DatedId> ids, int limit, final
IExecutionSelector selector)
diff --git
a/engine/src/main/java/org/apache/hop/execution/local/FileExecutionInfoLocation.java
b/engine/src/main/java/org/apache/hop/execution/local/FileExecutionInfoLocation.java
index b3add061df..beecae6a42 100644
---
a/engine/src/main/java/org/apache/hop/execution/local/FileExecutionInfoLocation.java
+++
b/engine/src/main/java/org/apache/hop/execution/local/FileExecutionInfoLocation.java
@@ -784,9 +784,15 @@ public class FileExecutionInfoLocation implements
IExecutionInfoLocation {
@Override
public synchronized String findParentId(String childId) throws HopException {
try {
+ Execution execution = getExecution(childId);
+ if (execution != null &&
StringUtils.isNotEmpty(execution.getParentId())) {
+ return execution.getParentId();
+ }
for (String id : getExecutionIds(true, 100)) {
ExecutionState executionState = getExecutionState(id);
- if (executionState.getChildIds().contains(childId)) {
+ if (executionState != null
+ && executionState.getChildIds() != null
+ && executionState.getChildIds().contains(childId)) {
return id;
}
}
diff --git
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/first/FirstRowsExecutionDataSamplerStore.java
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/first/FirstRowsExecutionDataSamplerStore.java
index c1502e9703..fb360ed220 100644
---
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/first/FirstRowsExecutionDataSamplerStore.java
+++
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/first/FirstRowsExecutionDataSamplerStore.java
@@ -69,9 +69,17 @@ public class FirstRowsExecutionDataSamplerStore
@Override
public Map<String, RowBuffer> getSamples() {
+ List<Object[]> copy;
+ if (rows != null) {
+ synchronized (rows) {
+ copy = new java.util.ArrayList<>(rows);
+ }
+ } else {
+ copy = new java.util.ArrayList<>();
+ }
return Map.of(
getKeyForStore(EXECUTION_DATA_SAMPLE_FIRST_OUTPUT, samplerMeta),
- new RowBuffer(rowMeta, rows));
+ new RowBuffer(rowMeta, copy));
}
@Override
diff --git
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/last/LastRowsExecutionDataSamplerStore.java
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/last/LastRowsExecutionDataSamplerStore.java
index dfc95a61d9..df5899f41b 100644
---
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/last/LastRowsExecutionDataSamplerStore.java
+++
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/last/LastRowsExecutionDataSamplerStore.java
@@ -67,9 +67,17 @@ public class LastRowsExecutionDataSamplerStore
@Override
public Map<String, RowBuffer> getSamples() {
+ List<Object[]> copy;
+ if (rows != null) {
+ synchronized (rows) {
+ copy = new java.util.ArrayList<>(rows);
+ }
+ } else {
+ copy = new java.util.ArrayList<>();
+ }
return Map.of(
getKeyForStore(EXECUTION_DATA_SAMPLE_LAST_OUTPUT, samplerMeta),
- new RowBuffer(rowMeta, rows));
+ new RowBuffer(rowMeta, copy));
}
@Override
diff --git
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/random/RandomRowsExecutionDataSamplerStore.java
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/random/RandomRowsExecutionDataSamplerStore.java
index dee29d4940..8c0a3470cb 100644
---
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/random/RandomRowsExecutionDataSamplerStore.java
+++
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/random/RandomRowsExecutionDataSamplerStore.java
@@ -65,9 +65,17 @@ public class RandomRowsExecutionDataSamplerStore
@Override
public Map<String, RowBuffer> getSamples() {
+ List<Object[]> copy;
+ if (rows != null) {
+ synchronized (rows) {
+ copy = new java.util.ArrayList<>(rows);
+ }
+ } else {
+ copy = new java.util.ArrayList<>();
+ }
return Map.of(
getKeyForStore(EXECUTION_DATA_SAMPLE_RANDOM_OUTPUT, samplerMeta),
- new RowBuffer(rowMeta, rows));
+ new RowBuffer(rowMeta, copy));
}
@Override
diff --git
a/engine/src/main/java/org/apache/hop/pipeline/engines/local/LocalPipelineEngine.java
b/engine/src/main/java/org/apache/hop/pipeline/engines/local/LocalPipelineEngine.java
index 617c27ca92..add8d54751 100644
---
a/engine/src/main/java/org/apache/hop/pipeline/engines/local/LocalPipelineEngine.java
+++
b/engine/src/main/java/org/apache/hop/pipeline/engines/local/LocalPipelineEngine.java
@@ -565,7 +565,6 @@ public class LocalPipelineEngine extends Pipeline
implements IPipelineEngine<Pip
try {
writeExecutionInfoState(iLocation);
- releasePublishedSamples();
} catch (Exception e) {
log.logError(
"Warning: unable to register execution state at location "
@@ -711,7 +710,6 @@ public class LocalPipelineEngine extends Pipeline
implements IPipelineEngine<Pip
ExecutionDataBuilder.fromAllTransformData(
LocalPipelineEngine.this, samplerStoresMap, true);
iLocation.registerData(dataBuilder.build());
- releasePublishedSamples();
}
} catch (Throwable e) {
log.logError("Error handling writing final pipeline state to location
(non-fatal)", e);
@@ -722,6 +720,8 @@ public class LocalPipelineEngine extends Pipeline
implements IPipelineEngine<Pip
iLocation.close();
} catch (Exception e) {
log.logError("Error closing execution information location: " +
location.getName(), e);
+ } finally {
+ releasePublishedSamples();
}
}
}
diff --git
a/engine/src/test/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocationTest.java
b/engine/src/test/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocationTest.java
index b6c62b4e41..0c7d9962ef 100644
---
a/engine/src/test/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocationTest.java
+++
b/engine/src/test/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocationTest.java
@@ -29,8 +29,13 @@ import java.util.Set;
import java.util.UUID;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.logging.HopLogStore;
+import org.apache.hop.core.row.RowBuffer;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaString;
import org.apache.hop.core.variables.Variables;
import org.apache.hop.execution.Execution;
+import org.apache.hop.execution.ExecutionData;
+import org.apache.hop.execution.ExecutionDataSetMeta;
import org.apache.hop.execution.ExecutionState;
import org.apache.hop.execution.ExecutionType;
import org.apache.hop.execution.IExecutionSelector;
@@ -152,6 +157,91 @@ class BaseCachingExecutionInfoLocationTest {
assertTrue(location.persisted.contains(id));
}
+ @Test
+ void testParentChildLinkResolution() throws Exception {
+ FakeLocation location = new FakeLocation();
+ String workflowId = "wf-1";
+ String actionId = "act-1";
+ String pipelineId = "pipe-1";
+
+ Execution wf = new Execution();
+ wf.setId(workflowId);
+ wf.setName("Workflow");
+ wf.setExecutionType(ExecutionType.Workflow);
+ wf.setRegistrationDate(new Date());
+
+ Execution action = new Execution();
+ action.setId(actionId);
+ action.setName("ActionPipeline");
+ action.setParentId(workflowId);
+ action.setExecutionType(ExecutionType.Action);
+ action.setRegistrationDate(new Date());
+
+ Execution pipe = new Execution();
+ pipe.setId(pipelineId);
+ pipe.setName("Pipeline");
+ pipe.setParentId(actionId);
+ pipe.setExecutionType(ExecutionType.Pipeline);
+ pipe.setRegistrationDate(new Date());
+
+ location.registerExecution(wf);
+ location.registerExecution(action);
+ location.registerExecution(pipe);
+
+ assertEquals(actionId, location.findParentId(pipelineId));
+ assertEquals(workflowId, location.findParentId(actionId));
+ assertNull(location.findParentId(workflowId));
+
+ Execution retrievedAction = location.getExecution(actionId);
+ assertNotNull(retrievedAction);
+ assertEquals(actionId, retrievedAction.getId());
+ assertEquals(ExecutionType.Action, retrievedAction.getExecutionType());
+
+ java.util.List<Execution> actionChildren =
location.findExecutions(actionId);
+ assertEquals(1, actionChildren.size());
+ assertEquals(pipelineId, actionChildren.get(0).getId());
+
+ java.util.List<Execution> wfChildren = location.findExecutions(workflowId);
+ assertTrue(wfChildren.stream().anyMatch(e -> e.getId().equals(actionId)));
+ }
+
+ @Test
+ void testAddExecutionDataRetainsNonEmptySamples() {
+ CacheEntry entry = new CacheEntry();
+ entry.setId("pipe-1");
+
+ RowMeta rowMeta = new RowMeta();
+ rowMeta.addValueMeta(new ValueMetaString("col1"));
+
+ RowBuffer bufferWithRows = new RowBuffer(rowMeta);
+ bufferWithRows.addRow(new Object[] {"value1"});
+
+ ExecutionData data1 = new ExecutionData();
+ data1.setOwnerId("all-transforms");
+ data1.getDataSets().put("set1", bufferWithRows);
+ data1
+ .getSetMetaData()
+ .put("set1", new ExecutionDataSetMeta("set1", "log1", "transform1",
"0", "desc1"));
+
+ entry.addExecutionData(data1);
+
+ assertEquals(1,
entry.getExecutionData("all-transforms").getDataSets().get("set1").size());
+
+ // Subsequent tick with empty rows
+ RowBuffer emptyBuffer = new RowBuffer(rowMeta);
+ ExecutionData data2 = new ExecutionData();
+ data2.setOwnerId("all-transforms");
+ data2.getDataSets().put("set1", emptyBuffer);
+ data2
+ .getSetMetaData()
+ .put("set1", new ExecutionDataSetMeta("set1", "log1", "transform1",
"0", "desc1"));
+
+ entry.addExecutionData(data2);
+
+ // Verify samples were preserved
+ assertEquals(1,
entry.getExecutionData("all-transforms").getDataSets().get("set1").size());
+ }
+
private static Execution pipeline(String id, String name) {
Execution execution = new Execution();
execution.setId(id);
diff --git
a/engine/src/test/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocationTest.java
b/engine/src/test/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocationTest.java
index c05b3863b9..252c2f39ba 100644
---
a/engine/src/test/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocationTest.java
+++
b/engine/src/test/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocationTest.java
@@ -522,6 +522,69 @@ class CachingFileExecutionInfoLocationTest {
}
}
+ @Test
+ void testUncachedParentChildLinkResolution() throws Exception {
+ Path root = tempDir.resolve("uncached-parent-child");
+ CachingFileExecutionInfoLocation location = new
CachingFileExecutionInfoLocation();
+ location.setRootFolder(root.toAbsolutePath().toString());
+ location.initialize(new Variables(), null);
+ try {
+ String workflowId = "wf-1";
+ String actionId = "act-1";
+ String pipelineId = "pipe-1";
+
+ Execution wf = new Execution();
+ wf.setId(workflowId);
+ wf.setName("Workflow");
+ wf.setExecutionType(ExecutionType.Workflow);
+ wf.setRegistrationDate(new Date());
+
+ Execution action = new Execution();
+ action.setId(actionId);
+ action.setName("ActionPipeline");
+ action.setParentId(workflowId);
+ action.setExecutionType(ExecutionType.Action);
+ action.setRegistrationDate(new Date());
+
+ Execution pipe = new Execution();
+ pipe.setId(pipelineId);
+ pipe.setName("Pipeline");
+ pipe.setParentId(actionId);
+ pipe.setExecutionType(ExecutionType.Pipeline);
+ pipe.setRegistrationDate(new Date());
+
+ location.registerExecution(wf);
+ location.registerExecution(action);
+ location.registerExecution(pipe);
+
+ // Persist to disk and clear cache so nothing is in memory
+ for (CacheEntry entry : location.getCache().values()) {
+ location.persistCacheEntry(entry);
+ }
+ location.clearCaches();
+
+ assertEquals(actionId, location.findParentId(pipelineId));
+ assertEquals(workflowId, location.findParentId(actionId));
+ assertNull(location.findParentId(workflowId));
+
+ Execution retrievedAction = location.getExecution(actionId);
+ assertNotNull(retrievedAction);
+ assertEquals(actionId, retrievedAction.getId());
+ assertEquals(ExecutionType.Action, retrievedAction.getExecutionType());
+
+ assertNull(location.getExecution("non-existent-id"));
+
+ List<Execution> actionChildren = location.findExecutions(actionId);
+ assertEquals(1, actionChildren.size());
+ assertEquals(pipelineId, actionChildren.get(0).getId());
+
+ List<Execution> wfChildren = location.findExecutions(workflowId);
+ assertTrue(wfChildren.stream().anyMatch(e ->
e.getId().equals(actionId)));
+ } finally {
+ location.close();
+ }
+ }
+
private static Execution executionAt(String id, Date start) {
Execution execution = execution(id, null);
execution.setExecutionStartDate(start);
diff --git a/integration-tests/exec-info/002-mapping-sub.hpl
b/integration-tests/exec-info/002-mapping-sub.hpl
new file mode 100644
index 0000000000..a1cb79512d
--- /dev/null
+++ b/integration-tests/exec-info/002-mapping-sub.hpl
@@ -0,0 +1,134 @@
+<?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>002-mapping-sub</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/10/10 14:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/10/10 14:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>input</from>
+ <to>add_desc</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>add_desc</from>
+ <to>output</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>input</name>
+ <type>MappingInput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <name>id</name>
+ <type>Integer</type>
+ <length>-1</length>
+ <precision>-1</precision>
+ </field>
+ <field>
+ <name>name</name>
+ <type>String</type>
+ <length>-1</length>
+ <precision>-1</precision>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>112</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>add_desc</name>
+ <type>Constant</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <name>desc</name>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif>mapped_description</nullif>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>288</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>output</name>
+ <type>MappingOutput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>464</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/exec-info/002-pipeline-with-mapping.hpl
b/integration-tests/exec-info/002-pipeline-with-mapping.hpl
new file mode 100644
index 0000000000..d29b0d4d4d
--- /dev/null
+++ b/integration-tests/exec-info/002-pipeline-with-mapping.hpl
@@ -0,0 +1,219 @@
+<?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>002-pipeline-with-mapping</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/10/10 14:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/10/10 14:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>10-rows</from>
+ <to>id</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>id</from>
+ <to>name</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>name</from>
+ <to>simple-mapping</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>simple-mapping</from>
+ <to>output</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>10-rows</name>
+ <type>RowGenerator</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields/>
+ <interval_in_ms>5000</interval_in_ms>
+ <last_time_field/>
+ <never_ending>N</never_ending>
+ <limit>10</limit>
+ <row_time_field/>
+ <attributes/>
+ <GUI>
+ <xloc>80</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>id</name>
+ <type>Sequence</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <connection/>
+ <counter_name/>
+ <increment_by>1</increment_by>
+ <max_value>999999</max_value>
+ <schema/>
+ <seqname>SEQ_</seqname>
+ <start_at>1</start_at>
+ <use_counter>Y</use_counter>
+ <use_database>N</use_database>
+ <valuename>id</valuename>
+ <attributes/>
+ <GUI>
+ <xloc>208</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>name</name>
+ <type>Constant</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <name>name</name>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif>test_name</nullif>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>336</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>simple-mapping</name>
+ <type>SimpleMapping</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <filename>${PROJECT_HOME}/002-mapping-sub.hpl</filename>
+ <mappings>
+ <input>
+ <mapping>
+ <connector>
+ <child>id</child>
+ <parent>id</parent>
+ </connector>
+ <connector>
+ <child>name</child>
+ <parent>name</parent>
+ </connector>
+ <main_path>Y</main_path>
+ <rename_on_output>Y</rename_on_output>
+ </mapping>
+ </input>
+ <output>
+ <mapping>
+ <connector>
+ <child>id</child>
+ <parent>id</parent>
+ </connector>
+ <connector>
+ <child>name</child>
+ <parent>name</parent>
+ </connector>
+ <connector>
+ <child>desc</child>
+ <parent>desc</parent>
+ </connector>
+ <main_path>Y</main_path>
+ <rename_on_output>N</rename_on_output>
+ </mapping>
+ </output>
+ <parameters>
+ <inherit_all_vars>Y</inherit_all_vars>
+ </parameters>
+ </mappings>
+ <runConfiguration>local</runConfiguration>
+ <attributes/>
+ <GUI>
+ <xloc>480</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>output</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>640</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/exec-info/002-sub-workflow.hwf
b/integration-tests/exec-info/002-sub-workflow.hwf
new file mode 100644
index 0000000000..7c5d501847
--- /dev/null
+++ b/integration-tests/exec-info/002-sub-workflow.hwf
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<workflow>
+ <name>002-sub-workflow</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <created_user>-</created_user>
+ <modified_user>-</modified_user>
+ <created_date>2026/10/10 14:00:00.000</created_date>
+ <modified_date>2026/10/10 14:00:00.000</modified_date>
+ <parameters/>
+ <actions>
+ <action>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <intervalSeconds>0</intervalSeconds>
+ <intervalMinutes>60</intervalMinutes>
+ <DayOfMonth>1</DayOfMonth>
+ <weekDay>1</weekDay>
+ <minutes>0</minutes>
+ <hour>12</hour>
+ <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <xloc>80</xloc>
+ <yloc>80</yloc>
+ <parallel>N</parallel>
+ <attributes_hac/>
+ </action>
+ <action>
+ <filename>${PROJECT_HOME}/002-pipeline-with-mapping.hpl</filename>
+ <params_from_previous>N</params_from_previous>
+ <exec_per_row>N</exec_per_row>
+ <clear_rows>N</clear_rows>
+ <clear_files>N</clear_files>
+ <create_parent_folder>N</create_parent_folder>
+ <set_logfile>N</set_logfile>
+ <set_append_logfile>N</set_append_logfile>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <loglevel>Basic</loglevel>
+ <wait_until_finished>Y</wait_until_finished>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <run_configuration>local-caching-file</run_configuration>
+ <name>run-pipeline-with-mapping</name>
+ <description/>
+ <type>PIPELINE</type>
+ <attributes/>
+ <xloc>288</xloc>
+ <yloc>80</yloc>
+ <parallel>N</parallel>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>run-pipeline-with-mapping</to>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ <enabled>Y</enabled>
+ </hop>
+ </hops>
+ <notepads/>
+ <attributes/>
+</workflow>
diff --git a/integration-tests/exec-info/002-validation.hpl
b/integration-tests/exec-info/002-validation.hpl
new file mode 100644
index 0000000000..1ad604f812
--- /dev/null
+++ b/integration-tests/exec-info/002-validation.hpl
@@ -0,0 +1,615 @@
+<?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>002-validation</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/10/10 14:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/10/10 14:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>params</from>
+ <to>find-pipeline-execution</to>
+ <enabled>Y</enabled>
+ </hop>
+ <!-- Branch A: Parent-child links -->
+ <hop>
+ <from>find-pipeline-execution</from>
+ <to>verify-pipeline-has-parent</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>verify-pipeline-has-parent</from>
+ <to>find-pipeline-parent-id</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>find-pipeline-parent-id</from>
+ <to>verify-pipeline-parent-id</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>verify-pipeline-parent-id</from>
+ <to>select-before-action-parent</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>select-before-action-parent</from>
+ <to>find-action-parent-id</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>find-action-parent-id</from>
+ <to>verify-action-parent-id</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>verify-action-parent-id</from>
+ <to>select-before-find-executions</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>select-before-find-executions</from>
+ <to>find-executions-of-action</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>find-executions-of-action</from>
+ <to>verify-child-pipeline-execution</to>
+ <enabled>Y</enabled>
+ </hop>
+ <!-- Branch B: Mapping output rows in execution data -->
+ <hop>
+ <from>find-pipeline-execution</from>
+ <to>select-for-get-execution-data</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>select-for-get-execution-data</from>
+ <to>get-execution-data</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>get-execution-data</from>
+ <to>filter-simple-mapping</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>filter-simple-mapping</from>
+ <to>count-mapping-rows</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>count-mapping-rows</from>
+ <to>verify-mapping-rows-exist</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>params</name>
+ <type>DataGrid</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <currency/>
+ <decimal/>
+ <group/>
+ <name>name</name>
+ <type>String</type>
+ <format/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ <field>
+ <currency/>
+ <decimal/>
+ <group/>
+ <name>type</name>
+ <type>String</type>
+ <format/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ <field>
+ <currency/>
+ <decimal/>
+ <group/>
+ <name>ownerId</name>
+ <type>String</type>
+ <format/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ </fields>
+ <data>
+ <line>
+ <item>002-pipeline-with-mapping</item>
+ <item>Pipeline</item>
+ <item>all-transforms</item>
+ </line>
+ </data>
+ <attributes/>
+ <GUI>
+ <xloc>80</xloc>
+ <yloc>160</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>find-pipeline-execution</name>
+ <type>ExecInfo</type>
+ <description/>
+ <distribute>N</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <location>caching-file</location>
+ <operationType>FindLastExecution</operationType>
+ <idFieldName/>
+ <parentIdFieldName/>
+ <typeFieldName>type</typeFieldName>
+ <nameFieldName>name</nameFieldName>
+ <includeChildrenFieldName/>
+ <limitFieldName/>
+ <attributes/>
+ <GUI>
+ <xloc>240</xloc>
+ <yloc>160</yloc>
+ </GUI>
+ </transform>
+ <!-- Branch A Transforms -->
+ <transform>
+ <name>verify-pipeline-has-parent</name>
+ <type>ScriptValueMod</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <optimizationLevel>9</optimizationLevel>
+ <jsScripts>
+ <jsScript>
+ <jsScript_type>0</jsScript_type>
+ <jsScript_name>Script 1</jsScript_name>
+ <jsScript_script>
+if (executionId == null || parentId == null) {
+ throw "Pipeline execution ID or parent action ID is null!";
+}
+</jsScript_script>
+ </jsScript>
+ </jsScripts>
+ <fields/>
+ <attributes/>
+ <GUI>
+ <xloc>400</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>find-pipeline-parent-id</name>
+ <type>ExecInfo</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <location>caching-file</location>
+ <operationType>FindParentId</operationType>
+ <idFieldName>executionId</idFieldName>
+ <parentIdFieldName/>
+ <typeFieldName/>
+ <nameFieldName/>
+ <includeChildrenFieldName/>
+ <limitFieldName/>
+ <attributes/>
+ <GUI>
+ <xloc>560</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>verify-pipeline-parent-id</name>
+ <type>ScriptValueMod</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <optimizationLevel>9</optimizationLevel>
+ <jsScripts>
+ <jsScript>
+ <jsScript_type>0</jsScript_type>
+ <jsScript_name>Script 1</jsScript_name>
+ <jsScript_script>
+if (id == null || !id.equals(parentId)) {
+ throw "findParentId(pipelineId) [" + id + "] did not match parent action ID
[" + parentId + "]!";
+}
+</jsScript_script>
+ </jsScript>
+ </jsScripts>
+ <fields/>
+ <attributes/>
+ <GUI>
+ <xloc>720</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>select-before-action-parent</name>
+ <type>SelectValues</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <select_unspecified>Y</select_unspecified>
+ <meta>
+ <name>id</name>
+ <rename>resolvedPipelineParentId</rename>
+ <type>String</type>
+ <length>-1</length>
+ <precision>-1</precision>
+ <conversion_mask/>
+ <date_format_lenient>false</date_format_lenient>
+ <date_format_locale/>
+ <date_format_timezone/>
+ <lenient_string_to_number>false</lenient_string_to_number>
+ <encoding/>
+ <decimal_symbol/>
+ <grouping_symbol/>
+ <currency_symbol/>
+ <storage_type/>
+ </meta>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>880</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>find-action-parent-id</name>
+ <type>ExecInfo</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <location>caching-file</location>
+ <operationType>FindParentId</operationType>
+ <idFieldName>parentId</idFieldName>
+ <parentIdFieldName/>
+ <typeFieldName/>
+ <nameFieldName/>
+ <includeChildrenFieldName/>
+ <limitFieldName/>
+ <attributes/>
+ <GUI>
+ <xloc>1040</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>verify-action-parent-id</name>
+ <type>ScriptValueMod</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <optimizationLevel>9</optimizationLevel>
+ <jsScripts>
+ <jsScript>
+ <jsScript_type>0</jsScript_type>
+ <jsScript_name>Script 1</jsScript_name>
+ <jsScript_script>
+if (id == null || id.equals("") || id.equals(parentId)) {
+ throw "findParentId(actionId) [" + id + "] is null or equal to actionId
itself!";
+}
+</jsScript_script>
+ </jsScript>
+ </jsScripts>
+ <fields/>
+ <attributes/>
+ <GUI>
+ <xloc>1200</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>select-before-find-executions</name>
+ <type>SelectValues</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <name>parentId</name>
+ </field>
+ <field>
+ <name>executionId</name>
+ <rename>origPipelineId</rename>
+ </field>
+ <select_unspecified>N</select_unspecified>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>1360</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>find-executions-of-action</name>
+ <type>ExecInfo</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <location>caching-file</location>
+ <operationType>FindExecutions</operationType>
+ <idFieldName/>
+ <parentIdFieldName>parentId</parentIdFieldName>
+ <typeFieldName/>
+ <nameFieldName/>
+ <includeChildrenFieldName/>
+ <limitFieldName/>
+ <attributes/>
+ <GUI>
+ <xloc>1520</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>verify-child-pipeline-execution</name>
+ <type>ScriptValueMod</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <optimizationLevel>9</optimizationLevel>
+ <jsScripts>
+ <jsScript>
+ <jsScript_type>0</jsScript_type>
+ <jsScript_name>Script 1</jsScript_name>
+ <jsScript_script>
+if (executionId == null || !executionId.equals(origPipelineId)) {
+ throw "findExecutions(actionId) did not return expected pipeline execution
ID [" + origPipelineId + "]! Got: [" + executionId + "]";
+}
+</jsScript_script>
+ </jsScript>
+ </jsScripts>
+ <fields/>
+ <attributes/>
+ <GUI>
+ <xloc>1680</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+
+ <!-- Branch B Transforms -->
+ <transform>
+ <name>select-for-get-execution-data</name>
+ <type>SelectValues</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <name>executionId</name>
+ <rename>pipelineExecId</rename>
+ </field>
+ <field>
+ <name>ownerId</name>
+ </field>
+ <select_unspecified>N</select_unspecified>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>400</xloc>
+ <yloc>240</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>get-execution-data</name>
+ <type>ExecInfo</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <location>caching-file</location>
+ <operationType>GetExecutionData</operationType>
+ <idFieldName>ownerId</idFieldName>
+ <parentIdFieldName>pipelineExecId</parentIdFieldName>
+ <typeFieldName/>
+ <nameFieldName/>
+ <includeChildrenFieldName/>
+ <limitFieldName/>
+ <attributes/>
+ <GUI>
+ <xloc>560</xloc>
+ <yloc>240</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>filter-simple-mapping</name>
+ <type>FilterRows</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <compare>
+ <condition>
+ <negated>N</negated>
+ <operator>-</operator>
+ <leftvalue>name</leftvalue>
+ <function>=</function>
+ <rightvalue/>
+ <value>
+ <name>constant</name>
+ <type>String</type>
+ <text>simple-mapping</text>
+ <length>-1</length>
+ <precision>-1</precision>
+ <isnull>N</isnull>
+ <mask/>
+ </value>
+ </condition>
+ </compare>
+ <attributes/>
+ <GUI>
+ <xloc>720</xloc>
+ <yloc>240</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>count-mapping-rows</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>N</ignore_aggregate>
+ <fields>
+ <field>
+ <aggregate>mappingRowCount</aggregate>
+ <subject>name</subject>
+ <type>COUNT_ANY</type>
+ </field>
+ </fields>
+ <give_back_row>Y</give_back_row>
+ <directory>${java.io.tmpdir}</directory>
+ <prefix>grp</prefix>
+ <add_linenr>N</add_linenr>
+ <attributes/>
+ <GUI>
+ <xloc>880</xloc>
+ <yloc>240</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>verify-mapping-rows-exist</name>
+ <type>ScriptValueMod</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <optimizationLevel>9</optimizationLevel>
+ <jsScripts>
+ <jsScript>
+ <jsScript_type>0</jsScript_type>
+ <jsScript_name>Script 1</jsScript_name>
+ <jsScript_script>
+if (mappingRowCount == null || mappingRowCount < 10) {
+ throw "Mapping transform output rows missing or insufficient in execution
data! Expected >= 10, got: " + mappingRowCount;
+}
+</jsScript_script>
+ </jsScript>
+ </jsScripts>
+ <fields/>
+ <attributes/>
+ <GUI>
+ <xloc>1040</xloc>
+ <yloc>240</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/exec-info/main-002-workflow-pipeline-mapping.hwf
b/integration-tests/exec-info/main-002-workflow-pipeline-mapping.hwf
new file mode 100644
index 0000000000..75c945f736
--- /dev/null
+++ b/integration-tests/exec-info/main-002-workflow-pipeline-mapping.hwf
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<workflow>
+ <name>main-002-workflow-pipeline-mapping</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <created_user>-</created_user>
+ <modified_user>-</modified_user>
+ <created_date>2026/10/10 14:00:00.000</created_date>
+ <modified_date>2026/10/10 14:00:00.000</modified_date>
+ <parameters/>
+ <actions>
+ <action>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <intervalSeconds>0</intervalSeconds>
+ <intervalMinutes>60</intervalMinutes>
+ <DayOfMonth>1</DayOfMonth>
+ <weekDay>1</weekDay>
+ <minutes>0</minutes>
+ <hour>12</hour>
+ <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <xloc>80</xloc>
+ <yloc>80</yloc>
+ <parallel>N</parallel>
+ <attributes_hac/>
+ </action>
+ <action>
+ <foldername>${java.io.tmpdir}/caching-file-location/</foldername>
+ <fail_of_folder_exists>N</fail_of_folder_exists>
+ <name>create /tmp/caching-file-location/</name>
+ <description/>
+ <type>CREATE_FOLDER</type>
+ <attributes/>
+ <xloc>272</xloc>
+ <yloc>80</yloc>
+ <parallel>N</parallel>
+ <attributes_hac/>
+ </action>
+ <action>
+ <filename>${PROJECT_HOME}/002-sub-workflow.hwf</filename>
+ <params_from_previous>N</params_from_previous>
+ <exec_per_row>N</exec_per_row>
+ <clear_rows>N</clear_rows>
+ <clear_files>N</clear_files>
+ <create_parent_folder>N</create_parent_folder>
+ <set_logfile>N</set_logfile>
+ <set_append_logfile>N</set_append_logfile>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <loglevel>Basic</loglevel>
+ <wait_until_finished>Y</wait_until_finished>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <run_configuration>local-caching-file</run_configuration>
+ <name>run-002-sub-workflow</name>
+ <description/>
+ <type>WORKFLOW</type>
+ <attributes/>
+ <xloc>512</xloc>
+ <yloc>80</yloc>
+ <parallel>N</parallel>
+ <attributes_hac/>
+ </action>
+ <action>
+ <filename>${PROJECT_HOME}/002-validation.hpl</filename>
+ <params_from_previous>N</params_from_previous>
+ <exec_per_row>N</exec_per_row>
+ <clear_rows>N</clear_rows>
+ <clear_files>N</clear_files>
+ <create_parent_folder>N</create_parent_folder>
+ <set_logfile>N</set_logfile>
+ <set_append_logfile>N</set_append_logfile>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <loglevel>Basic</loglevel>
+ <wait_until_finished>Y</wait_until_finished>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <run_configuration>local</run_configuration>
+ <name>run-002-validation</name>
+ <description/>
+ <type>PIPELINE</type>
+ <attributes/>
+ <xloc>784</xloc>
+ <yloc>80</yloc>
+ <parallel>N</parallel>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>create /tmp/caching-file-location/</to>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>create /tmp/caching-file-location/</from>
+ <to>run-002-sub-workflow</to>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>run-002-sub-workflow</from>
+ <to>run-002-validation</to>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ <enabled>Y</enabled>
+ </hop>
+ </hops>
+ <notepads/>
+ <attributes/>
+</workflow>
diff --git
a/plugins/misc/execution-database/src/main/java/org/apache/hop/execution/database/CachingDatabaseExecutionInfoLocation.java
b/plugins/misc/execution-database/src/main/java/org/apache/hop/execution/database/CachingDatabaseExecutionInfoLocation.java
index 5135e42601..12dc7a1649 100644
---
a/plugins/misc/execution-database/src/main/java/org/apache/hop/execution/database/CachingDatabaseExecutionInfoLocation.java
+++
b/plugins/misc/execution-database/src/main/java/org/apache/hop/execution/database/CachingDatabaseExecutionInfoLocation.java
@@ -719,6 +719,89 @@ public class CachingDatabaseExecutionInfoLocation extends
BaseCachingExecutionIn
}
}
+ @Override
+ protected CacheEntry loadCacheEntryWithChild(String childId) throws
HopException {
+ if (StringUtils.isEmpty(childId)) {
+ return null;
+ }
+ try {
+ String columns = databaseMeta.quoteField(COL_JSON);
+ if (stateJsonColumnAvailable) {
+ columns += ", " + databaseMeta.quoteField(COL_STATE_JSON);
+ }
+ StringBuilder sql = new StringBuilder();
+ sql.append("SELECT ")
+ .append(columns)
+ .append(" FROM ")
+ .append(getQuotedSchemaTable())
+ .append(" WHERE (")
+ .append(databaseMeta.quoteField(COL_JSON))
+ .append(" LIKE ?");
+ if (stateJsonColumnAvailable) {
+ sql.append(" OR
").append(databaseMeta.quoteField(COL_STATE_JSON)).append(" LIKE ?");
+ }
+ sql.append(") AND ")
+ .append(databaseMeta.quoteField(COL_ID))
+ .append(" != ?")
+ .append(" ORDER BY ")
+ .append(databaseMeta.quoteField(COL_EXECUTION_START_DATE))
+ .append(" DESC")
+ .append(databaseMeta.getLimitClause(20));
+
+ IRowMeta paramMeta = new RowMeta();
+ paramMeta.addValueMeta(new ValueMetaString("p1", 100, -1));
+ List<Object> params = new ArrayList<>();
+ params.add("%" + childId + "%");
+ if (stateJsonColumnAvailable) {
+ paramMeta.addValueMeta(new ValueMetaString("p2", 100, -1));
+ params.add("%" + childId + "%");
+ }
+ paramMeta.addValueMeta(new ValueMetaString("p_not_id", 100, -1));
+ params.add(childId);
+
+ return callWithDatabase(
+ () -> {
+ ResultSet rs = database.openQuery(sql.toString(), paramMeta,
params.toArray());
+ try {
+ Object[] row = database.getRow(rs);
+ while (row != null) {
+ if (row[0] != null) {
+ try {
+ CacheEntry entry =
JSON_MAPPER.readValue(row[0].toString(), CacheEntry.class);
+ if (entry != null && !entry.getId().equals(childId)) {
+ if (stateJsonColumnAvailable
+ && row.length > 1
+ && row[1] != null
+ && StringUtils.isNotEmpty(row[1].toString())) {
+ applyStateOverlay(
+ entry, JSON_MAPPER.readValue(row[1].toString(),
CacheEntry.class));
+ }
+ if (entry.peekChildExecution(childId) != null
+ || (entry.getChildIds() != null
+ && entry.getChildIds().contains(childId))) {
+ cache.put(entry.getId(), entry);
+ enforceMaxCacheSize();
+ return entry;
+ }
+ }
+ } catch (Exception e) {
+ // Ignore corrupted candidate and continue checking other
rows
+ }
+ }
+ row = database.getRow(rs);
+ }
+ return null;
+ } finally {
+ database.closeQuery(rs);
+ }
+ });
+ } catch (Exception e) {
+ LogChannel.GENERAL.logError(
+ "Error finding cache entry containing child " + childId + " from
database", e);
+ return null;
+ }
+ }
+
@Override
public void deleteCacheEntry(CacheEntry cacheEntry) throws HopException {
if (cacheEntry == null || StringUtils.isEmpty(cacheEntry.getId())) {
diff --git
a/plugins/misc/execution-database/src/test/java/org/apache/hop/execution/database/CachingDatabaseExecutionInfoLocationTest.java
b/plugins/misc/execution-database/src/test/java/org/apache/hop/execution/database/CachingDatabaseExecutionInfoLocationTest.java
index bcbfd0732c..34b4ee794f 100644
---
a/plugins/misc/execution-database/src/test/java/org/apache/hop/execution/database/CachingDatabaseExecutionInfoLocationTest.java
+++
b/plugins/misc/execution-database/src/test/java/org/apache/hop/execution/database/CachingDatabaseExecutionInfoLocationTest.java
@@ -752,6 +752,72 @@ class CachingDatabaseExecutionInfoLocationTest {
}
}
+ @Test
+ void testUncachedParentChildLinkResolution() throws Exception {
+ CachingDatabaseExecutionInfoLocation location = new
CachingDatabaseExecutionInfoLocation();
+ location.setConnectionName("h2-exec");
+
location.setTableName(CachingDatabaseExecutionInfoLocation.DEFAULT_TABLE_NAME);
+ location.setPersistenceDelay("0");
+ location.setMaxCacheAge("86400000");
+ location.setDatabaseMeta(databaseMeta);
+ location.initialize(variables, metadataProvider);
+ try {
+ String workflowId = UUID.randomUUID().toString();
+ String actionId = UUID.randomUUID().toString();
+ String pipelineId = UUID.randomUUID().toString();
+
+ Execution wf = new Execution();
+ wf.setId(workflowId);
+ wf.setName("Workflow");
+ wf.setExecutionType(ExecutionType.Workflow);
+ wf.setRegistrationDate(new Date());
+
+ Execution action = new Execution();
+ action.setId(actionId);
+ action.setName("ActionPipeline");
+ action.setParentId(workflowId);
+ action.setExecutionType(ExecutionType.Action);
+ action.setRegistrationDate(new Date());
+
+ Execution pipe = new Execution();
+ pipe.setId(pipelineId);
+ pipe.setName("Pipeline");
+ pipe.setParentId(actionId);
+ pipe.setExecutionType(ExecutionType.Pipeline);
+ pipe.setRegistrationDate(new Date());
+
+ location.registerExecution(wf);
+ location.registerExecution(action);
+ location.registerExecution(pipe);
+
+ // Persist to database and clear in-memory cache so nothing is in memory
+ for (CacheEntry entry : location.getCache().values()) {
+ location.persistCacheEntry(entry);
+ }
+ location.clearCaches();
+
+ assertEquals(actionId, location.findParentId(pipelineId));
+ assertEquals(workflowId, location.findParentId(actionId));
+ assertNull(location.findParentId(workflowId));
+
+ Execution retrievedAction = location.getExecution(actionId);
+ assertNotNull(retrievedAction);
+ assertEquals(actionId, retrievedAction.getId());
+ assertEquals(ExecutionType.Action, retrievedAction.getExecutionType());
+
+ assertNull(location.getExecution("non-existent-id"));
+
+ List<Execution> actionChildren = location.findExecutions(actionId);
+ assertEquals(1, actionChildren.size());
+ assertEquals(pipelineId, actionChildren.get(0).getId());
+
+ List<Execution> wfChildren = location.findExecutions(workflowId);
+ assertTrue(wfChildren.stream().anyMatch(e ->
e.getId().equals(actionId)));
+ } finally {
+ location.close();
+ }
+ }
+
private static final class FailingFlushLocation extends
CachingDatabaseExecutionInfoLocation {
private boolean failPersist;
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
index 124a3618f2..f5f0a48885 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
@@ -954,10 +954,6 @@ public class WorkflowExecutionViewer extends
BaseExecutionViewer
toolTip =
"i18n::WorkflowExecutionViewer.ToolbarElement.DrillDown.Tooltip",
image = "ui/images/down.svg")
public void drillDown() {
- if (selectedExecutionData == null) {
- return;
- }
-
ExecutionInfoLocation location =
perspective.getLocationMap().get(locationName);
if (location == null) {
return;
@@ -967,7 +963,26 @@ public class WorkflowExecutionViewer extends
BaseExecutionViewer
// We need to look up a pipeline or workflow execution where the parent is
the ID of the action
//
try {
- String id = selectedExecutionData.getOwnerId();
+ String id = null;
+ if (selectedExecutionData != null) {
+ id = selectedExecutionData.getOwnerId();
+ } else if (selectedAction != null) {
+ List<String> childIds = iLocation.findChildIds(ExecutionType.Workflow,
execution.getId());
+ if (childIds != null) {
+ for (String childId : childIds) {
+ Execution childExec = iLocation.getExecution(childId);
+ if (childExec != null &&
selectedAction.getName().equals(childExec.getName())) {
+ id = childId;
+ break;
+ }
+ }
+ }
+ }
+
+ if (id == null) {
+ return;
+ }
+
List<Execution> childExecutions = iLocation.findExecutions(id);
if (childExecutions.isEmpty()) {
return;