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 4d0782bc92 Issue #8280 : Harden Neo4j execution information for Neo4j
5 (#8282)
4d0782bc92 is described below
commit 4d0782bc92487ca516b8273e64ee21ab35b7835b
Author: Matt Casters <[email protected]>
AuthorDate: Tue Sep 8 16:39:55 2026 +0200
Issue #8280 : Harden Neo4j execution information for Neo4j 5 (#8282)
* Issue #8280 : Harden Neo4j execution information for Neo4j 5
Log the real exception when sample-row registration fails, keep state
updates independent of data writes, coerce Neo4j-unsafe property types,
and fix lineage/error Cypher for Neo4j 5. Also paint nested pipeline
errors from stored metrics and fail index/constraint actions on Cypher
errors. See discussion #8204.
* Issue #8280 : Index ExecutionMetric and close Neo4j drivers on error
Point idx_execution_metric_id at :ExecutionMetric including metricKey.
Stop writeHierarchies from throwing out of finished listeners, and close
the logging session/driver in a finally block.
---
.../engines/local/LocalPipelineEngine.java | 34 ++---
.../engines/local/LocalWorkflowEngine.java | 8 +-
.../hop/spark/engines/SparkPipelineEngine.java | 6 +-
.../neo4j/actions/constraint/Neo4jConstraint.java | 9 +-
.../apache/hop/neo4j/actions/index/Neo4jIndex.java | 32 ++---
.../hop/neo4j/core/data/GraphPropertyDataType.java | 2 +-
.../neo4j/execution/NeoExecutionInfoLocation.java | 125 ++++++++++++-----
.../neo4j/execution/builder/BaseCypherBuilder.java | 151 +++++++++++++++++----
.../execution/builder/CypherCreateBuilder.java | 6 +-
.../execution/builder/CypherMatchBuilder.java | 2 +-
.../execution/builder/CypherMergeBuilder.java | 6 +-
.../execution/builder/CypherQueryBuilder.java | 6 +-
.../neo4j/execution/cache/NeoLocationCache.java | 90 +++++-------
.../path/base/NeoExecutionViewerErrorTab.java | 56 ++++----
.../path/base/NeoExecutionViewerLineageTab.java | 56 ++++----
.../path/base/NeoExecutionViewerTabBase.java | 51 ++++---
.../apache/hop/neo4j/logging/util/LoggingCore.java | 1 -
.../logging/xp/PipelineLoggingExtensionPoint.java | 38 +++---
.../logging/xp/WorkflowLoggingExtensionPoint.java | 36 ++---
.../apache/hop/neo4j/shared/DriverSingleton.java | 31 ++---
.../org/apache/hop/neo4j/shared/NeoConnection.java | 11 ++
.../hop/neo4j/transforms/importer/Importer.java | 5 +-
.../neo4j/transforms/importer/ImporterDialog.java | 6 +-
.../neo4j/actions/index/Neo4jIndexCypherTest.java | 55 ++++++++
.../neo4j/core/data/GraphPropertyDataTypeTest.java | 30 ++++
.../execution/builder/BaseCypherBuilderTest.java | 76 +++++++++++
.../execution/cache/NeoLocationCacheTest.java | 64 +++++++++
.../path/NeoExecutionViewerTabBaseTest.java | 56 ++++++++
.../hop/neo4j/perspective/ErrorPathCypherIT.java | 55 ++++++++
.../execution/PipelineExecutionViewer.java | 27 +++-
30 files changed, 822 insertions(+), 309 deletions(-)
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 fc24bcb602..8359f8c884 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
@@ -486,27 +486,29 @@ public class LocalPipelineEngine extends Pipeline
implements IPipelineEngine<Pip
new TimerTask() {
@Override
public void run() {
- try {
- // Collect data from all the sampler stores.
- //
- if (dataProfile != null) {
+ // Sample rows and execution state are written independently so a
conversion error
+ // on sampled data cannot skip the state update (and hide the real
exception).
+ //
+ if (dataProfile != null) {
+ try {
ExecutionDataBuilder dataBuilder =
ExecutionDataBuilder.fromAllTransformData(
LocalPipelineEngine.this, samplerStoresMap, false);
-
- // Send it to the location once
- //
iLocation.registerData(dataBuilder.build());
+ } catch (Exception e) {
+ log.logError(
+ "Warning: unable to register execution data at location "
+ + executionInfoLocation.getName()
+ + " (non-fatal)",
+ e);
}
+ }
- // Update the pipeline execution state regularly
- //
+ try {
ExecutionState pipelineState =
ExecutionStateBuilder.fromExecutor(LocalPipelineEngine.this,
-1).build();
iLocation.updateExecutionState(pipelineState);
- // Update the state of all the transforms
- //
for (IEngineComponent component : getComponents()) {
ExecutionState transformState =
ExecutionStateBuilder.fromTransform(LocalPipelineEngine.this, component)
@@ -514,13 +516,11 @@ public class LocalPipelineEngine extends Pipeline
implements IPipelineEngine<Pip
iLocation.updateExecutionState(transformState);
}
} catch (Exception e) {
- // This is probably cause by a race condition triggering this
code after the pipeline
- // finished. We're just going to log this as a warning.
- //
- log.logBasic(
- "Warning: unable to register execution info (data and state)
at location "
+ log.logError(
+ "Warning: unable to register execution state at location "
+ executionInfoLocation.getName()
- + "(non-fatal)");
+ + " (non-fatal)",
+ e);
}
}
};
diff --git
a/engine/src/main/java/org/apache/hop/workflow/engines/local/LocalWorkflowEngine.java
b/engine/src/main/java/org/apache/hop/workflow/engines/local/LocalWorkflowEngine.java
index 916ebd609b..632437e763 100644
---
a/engine/src/main/java/org/apache/hop/workflow/engines/local/LocalWorkflowEngine.java
+++
b/engine/src/main/java/org/apache/hop/workflow/engines/local/LocalWorkflowEngine.java
@@ -31,7 +31,6 @@ import org.apache.hop.core.database.Database;
import org.apache.hop.core.database.map.DatabaseConnectionMap;
import org.apache.hop.core.exception.HopDatabaseException;
import org.apache.hop.core.exception.HopException;
-import org.apache.hop.core.exception.HopRuntimeException;
import org.apache.hop.core.logging.ILogChannel;
import org.apache.hop.core.logging.ILoggingObject;
import org.apache.hop.core.util.ExecutorUtil;
@@ -401,9 +400,10 @@ public class LocalWorkflowEngine extends Workflow
implements IWorkflowEngine<Wor
lastLogLineNr.set(executionState.getLastLogLineNr());
}
} catch (Exception e) {
- throw new HopRuntimeException(
- "Error registering execution info data from transforms at
location "
- + executionInfoLocation.getName(),
+ log.logError(
+ "Warning: unable to register execution state at location "
+ + executionInfoLocation.getName()
+ + " (non-fatal)",
e);
}
}
diff --git
a/plugins/engines/spark/src/main/java/org/apache/hop/spark/engines/SparkPipelineEngine.java
b/plugins/engines/spark/src/main/java/org/apache/hop/spark/engines/SparkPipelineEngine.java
index f654f9dc9c..8c32a8024c 100644
---
a/plugins/engines/spark/src/main/java/org/apache/hop/spark/engines/SparkPipelineEngine.java
+++
b/plugins/engines/spark/src/main/java/org/apache/hop/spark/engines/SparkPipelineEngine.java
@@ -604,11 +604,11 @@ public class SparkPipelineEngine extends Variables
implements IPipelineEngine<Pi
updatePipelineState(iLocation);
} catch (Exception e) {
if (logChannel != null) {
- logChannel.logBasic(
+ logChannel.logError(
"Warning: unable to register execution info at location "
+ executionInfoLocation.getName()
- + " (non-fatal): "
- + e.getMessage());
+ + " (non-fatal)",
+ e);
}
}
}
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/constraint/Neo4jConstraint.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/constraint/Neo4jConstraint.java
index 6447928df8..d7ab56ae47 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/constraint/Neo4jConstraint.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/constraint/Neo4jConstraint.java
@@ -23,6 +23,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.hop.core.Result;
import org.apache.hop.core.annotations.Action;
import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopRuntimeException;
import org.apache.hop.metadata.api.HopMetadataProperty;
import org.apache.hop.neo4j.shared.NeoConnection;
import org.apache.hop.workflow.action.ActionBase;
@@ -138,8 +139,8 @@ public class Neo4jConstraint extends ActionBase implements
IAction {
result.consume();
return true;
} catch (Throwable e) {
- logError("Error dropping constraint with cypher [" + _cypher +
"]", e);
- return false;
+ throw new HopRuntimeException(
+ "Error dropping constraint with cypher [" + _cypher + "]",
e);
}
});
}
@@ -249,8 +250,8 @@ public class Neo4jConstraint extends ActionBase implements
IAction {
result.consume();
return true;
} catch (Throwable e) {
- logError("Error creating constraint with cypher [" + _cypher +
"]", e);
- return false;
+ throw new HopRuntimeException(
+ "Error creating constraint with cypher [" + _cypher + "]",
e);
}
});
}
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/index/Neo4jIndex.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/index/Neo4jIndex.java
index 75cff40926..5a48e78f0a 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/index/Neo4jIndex.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/index/Neo4jIndex.java
@@ -24,6 +24,7 @@ import org.apache.hop.core.Const;
import org.apache.hop.core.Result;
import org.apache.hop.core.annotations.Action;
import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopRuntimeException;
import org.apache.hop.metadata.api.HopMetadataProperty;
import org.apache.hop.neo4j.shared.NeoConnection;
import org.apache.hop.workflow.action.ActionBase;
@@ -109,23 +110,14 @@ public class Neo4jIndex extends ActionBase implements
IAction {
public static String generateDropIndexCypher(IndexUpdate indexUpdate) throws
HopException {
String cypher = "DROP INDEX ";
- if (StringUtils.isNotEmpty(indexUpdate.getIndexName())) {
- cypher += indexUpdate.getIndexName();
- } else {
- cypher += " FOR ";
- switch (indexUpdate.getObjectType()) {
- case NODE:
- cypher +=
- ":" + indexUpdate.getObjectName() + "(" +
indexUpdate.getObjectProperties() + ")";
- break;
- case RELATIONSHIP:
- throw new HopException(
- "Please drop indexes on relationship properties with their name.
Relationship label: "
- + indexUpdate.getObjectName()
- + ", properties: "
- + indexUpdate.getObjectProperties());
- }
+ if (StringUtils.isEmpty(indexUpdate.getIndexName())) {
+ throw new HopException(
+ "Please drop indexes with the name of the index. Object: "
+ + indexUpdate.getObjectName()
+ + ", properties: "
+ + indexUpdate.getObjectProperties());
}
+ cypher += indexUpdate.getIndexName();
cypher += " IF EXISTS";
return cypher;
}
@@ -148,8 +140,8 @@ public class Neo4jIndex extends ActionBase implements
IAction {
result.consume();
return true;
} catch (Throwable e) {
- logError("Error dropping index with cypher [" + _cypher + "]",
e);
- return false;
+ throw new HopRuntimeException(
+ "Error dropping index with cypher [" + _cypher + "]", e);
}
});
}
@@ -216,8 +208,8 @@ public class Neo4jIndex extends ActionBase implements
IAction {
result.consume();
return true;
} catch (Throwable e) {
- logError("Error creating index with cypher [" + _cypher + "]",
e);
- return false;
+ throw new HopRuntimeException(
+ "Error creating index with cypher [" + _cypher + "]", e);
}
});
}
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/core/data/GraphPropertyDataType.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/core/data/GraphPropertyDataType.java
index 817625870a..92616bb6bf 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/core/data/GraphPropertyDataType.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/core/data/GraphPropertyDataType.java
@@ -29,7 +29,7 @@ public enum GraphPropertyDataType {
String("string"),
Integer("long"),
Float("double"),
- Number("doubler"),
+ Number("double"),
Boolean("boolean"),
Date("date"),
LocalDateTime("localdatetime"),
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/NeoExecutionInfoLocation.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/NeoExecutionInfoLocation.java
index b0445b34eb..ddc6516d2a 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/NeoExecutionInfoLocation.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/NeoExecutionInfoLocation.java
@@ -232,11 +232,31 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
@Override
public void close() throws HopException {
+ Exception first = null;
try {
- session.close();
- driver.close();
+ if (session != null) {
+ session.close();
+ }
+ } catch (Exception e) {
+ first = e;
+ } finally {
+ session = null;
+ }
+ try {
+ if (driver != null) {
+ driver.close();
+ }
} catch (Exception e) {
- throw new HopException("Error closing Neo4j execution information
location", e);
+ if (first == null) {
+ first = e;
+ } else {
+ first.addSuppressed(e);
+ }
+ } finally {
+ driver = null;
+ }
+ if (first != null) {
+ throw new HopException("Error closing Neo4j execution information
location", first);
}
}
@@ -247,7 +267,7 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
@Override
public void unBuffer(String executionId) {
- // There is nothing to remove from a buffer or cache.
+ NeoLocationCache.remove(executionId);
}
/** Simply show the DDL to create the indexes */
@@ -265,7 +285,14 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
addIndex(cypher, "idx_execution_start_date", EL_EXECUTION,
EP_EXECUTION_START_DATE);
addIndex(cypher, "idx_execution_failed", EL_EXECUTION, EP_FAILED);
addIndex(cypher, "idx_execution_parent_id", EL_EXECUTION, EP_PARENT_ID);
- addIndex(cypher, "idx_execution_metric_id", EL_EXECUTION, EP_ID, EP_NAME,
EP_COPY_NR);
+ addIndex(
+ cypher,
+ "idx_execution_metric_id",
+ CL_EXECUTION_METRIC,
+ CP_ID,
+ CP_NAME,
+ CP_COPY_NR,
+ CP_METRIC_KEY);
addIndex(cypher, "idx_execution_data_id", DL_EXECUTION_DATA, DP_PARENT_ID,
DP_OWNER_ID);
addIndex(
cypher,
@@ -348,9 +375,17 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
}
addIndex(neo4jIndex, "idx_execution_id", EL_EXECUTION, EP_ID);
+ addIndex(neo4jIndex, "idx_execution_start_date", EL_EXECUTION,
EP_EXECUTION_START_DATE);
addIndex(neo4jIndex, "idx_execution_failed", EL_EXECUTION, EP_FAILED);
addIndex(neo4jIndex, "idx_execution_parent_id", EL_EXECUTION,
EP_PARENT_ID);
- addIndex(neo4jIndex, "idx_execution_metric_id", EL_EXECUTION, EP_ID,
EP_NAME, EP_COPY_NR);
+ addIndex(
+ neo4jIndex,
+ "idx_execution_metric_id",
+ CL_EXECUTION_METRIC,
+ CP_ID,
+ CP_NAME,
+ CP_COPY_NR,
+ CP_METRIC_KEY);
addIndex(neo4jIndex, "idx_execution_data_id", DL_EXECUTION_DATA,
DP_PARENT_ID, DP_OWNER_ID);
addIndex(
neo4jIndex,
@@ -423,9 +458,12 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
public void registerExecution(Execution execution) throws HopException {
synchronized (this) {
try {
- assert execution.getName() != null : "Please register executions with
a name";
- assert execution.getExecutionType() != null
- : "Please register executions with an execution type";
+ if (StringUtils.isEmpty(execution.getName())) {
+ throw new HopException("Please register executions with a name");
+ }
+ if (execution.getExecutionType() == null) {
+ throw new HopException("Please register executions with an execution
type");
+ }
session.executeWrite(transaction ->
registerNeo4jExecution(transaction, execution));
} catch (Exception e) {
@@ -762,9 +800,12 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
public void updateExecutionState(ExecutionState executionState) throws
HopException {
synchronized (this) {
try {
- assert executionState.getName() != null : "Please update execution
states with a name";
- assert executionState.getExecutionType() != null
- : "Please update execution states with an execution type";
+ if (StringUtils.isEmpty(executionState.getName())) {
+ throw new HopException("Please update execution states with a name");
+ }
+ if (executionState.getExecutionType() == null) {
+ throw new HopException("Please update execution states with an
execution type");
+ }
session.executeWrite(transaction ->
updateNeo4jExecutionState(transaction, executionState));
} catch (Exception e) {
@@ -798,14 +839,13 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
// Save all the metrics in this map in there...
//
for (String metricKey : metric.getMetrics().keySet()) {
- Map<String, Object> metricKeys =
- Map.of(
- CP_ID, state.getId(),
- CP_NAME, metric.getComponentName(),
- CP_COPY_NR, metric.getComponentCopy(),
- CP_METRIC_KEY, metricKey);
- CypherCreateBuilder metricBuilder =
- CypherCreateBuilder.of()
+ Map<String, Object> metricKeys = new HashMap<>();
+ metricKeys.put(CP_ID, state.getId());
+ metricKeys.put(CP_NAME, Const.NVL(metric.getComponentName(), ""));
+ metricKeys.put(CP_COPY_NR, Const.NVL(metric.getComponentCopy(),
"0"));
+ metricKeys.put(CP_METRIC_KEY, metricKey);
+ CypherMergeBuilder metricBuilder =
+ CypherMergeBuilder.of()
.withLabelAndKeys(CL_EXECUTION_METRIC, metricKeys)
.withValue(CP_METRIC_VALUE,
metric.getMetrics().get(metricKey));
execute(transaction, metricBuilder);
@@ -813,20 +853,16 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
CypherRelationshipBuilder.of()
.withMatch(EL_EXECUTION, "e", EP_ID, state.getId())
.withMatch(CL_EXECUTION_METRIC, "m", metricKeys)
- .withCreate("e", "m", R_HAS_METRIC);
+ .withMerge("e", "m", R_HAS_METRIC);
execute(transaction, relationshipBuilder);
}
}
}
- // Transaction is automatically committed by executeWrite
+ NeoLocationCache.store(state);
return true;
} catch (Exception e) {
- // Transaction is automatically rolled back by executeWrite on exception
throw new HopRuntimeException("Error updating the state of an execution
in Neo4j", e);
- } finally {
- // Update the cache
- NeoLocationCache.store(state);
}
}
@@ -1113,8 +1149,20 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
private boolean registerNeo4jData(TransactionContext transaction,
ExecutionData data) {
try {
- assert data != null : "no execution data provided";
- assert data.getExecutionType() != null : "execution data has no type";
+ if (data == null) {
+ throw new HopRuntimeException("no execution data provided");
+ }
+ if (data.getExecutionType() == null) {
+ throw new HopRuntimeException("execution data has no type");
+ }
+ if (StringUtils.isEmpty(data.getParentId()) ||
StringUtils.isEmpty(data.getOwnerId())) {
+ throw new HopRuntimeException(
+ "Execution data is missing parentId or ownerId (parentId="
+ + data.getParentId()
+ + ", ownerId="
+ + data.getOwnerId()
+ + ")");
+ }
// We'll not cache this data as it can be a lot.
@@ -1175,6 +1223,10 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
for (String setKey : data.getDataSets().keySet()) {
RowBuffer rowBuffer = data.getDataSets().get(setKey);
ExecutionDataSetMeta setMeta = data.getSetMetaData().get(setKey);
+ if (setMeta == null) {
+ log.logError("Skipping execution data set '" + setKey + "': no
metadata");
+ continue;
+ }
saveNeo4jRowsAndMeta(
transaction, data.getParentId(), data.getOwnerId(), rowBuffer,
setMeta);
}
@@ -1195,8 +1247,14 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
ExecutionDataSetMeta setMeta) {
try {
+ if (rowBuffer == null || rowBuffer.getRowMeta() == null) {
+ log.logError(
+ "Skipping execution data set without row metadata: "
+ + (setMeta == null ? "?" : setMeta.getSetKey()));
+ return;
+ }
IRowMeta rowMeta = rowBuffer.getRowMeta();
- String rowMetaJson = rowMeta == null ? null :
JsonRowMeta.toJson(rowMeta);
+ String rowMetaJson = JsonRowMeta.toJson(rowMeta);
// Save the Execution Data Set node
//
@@ -1730,12 +1788,9 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
@Override
public String findParentId(String childId) throws HopException {
try {
- for (String id : getExecutionIds(true, 100)) {
- ExecutionState executionState = getExecutionState(id);
- List<String> childIds = executionState.getChildIds();
- if (childIds != null && childIds.contains(childId)) {
- return id;
- }
+ Execution child = getExecution(childId);
+ if (child != null) {
+ return child.getParentId();
}
return null;
} catch (Exception e) {
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/BaseCypherBuilder.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/BaseCypherBuilder.java
index 345630e7ee..6db8f1836c 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/BaseCypherBuilder.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/BaseCypherBuilder.java
@@ -20,18 +20,31 @@ package org.apache.hop.neo4j.execution.builder;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
+import java.lang.reflect.Array;
import java.math.BigDecimal;
+import java.math.BigInteger;
import java.sql.Timestamp;
-import java.text.SimpleDateFormat;
+import java.time.Duration;
+import java.time.LocalDate;
import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.OffsetDateTime;
+import java.time.OffsetTime;
import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
-import org.apache.hop.core.exception.HopRuntimeException;
+import java.util.UUID;
import org.apache.hop.core.json.HopJson;
public abstract class BaseCypherBuilder implements ICypherBuilder {
+ private static final DateTimeFormatter TIMESTAMP_FORMAT =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSSSSS");
+
protected StringBuilder cypher;
protected Map<String, Object> parameters;
@@ -46,16 +59,7 @@ public abstract class BaseCypherBuilder implements
ICypherBuilder {
}
protected void addParameter(String property, Object value) {
- if (value != null) {
- if (value instanceof Date date) {
- // Convert to LocalDateTime
- parameters.put(property, LocalDateTime.ofInstant(date.toInstant(),
ZoneId.systemDefault()));
- } else {
- parameters.put(property, value);
- }
- } else {
- parameters.put(property, null);
- }
+ parameters.put(property, mapTypes(value));
}
public void withExtraClause(String clause) {
@@ -70,27 +74,114 @@ public abstract class BaseCypherBuilder implements
ICypherBuilder {
return parameters;
}
- public static SimpleDateFormat timestampFormat =
- new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSSSSSSS");
-
+ /**
+ * Convert a Hop / Java value into a type the Neo4j driver accepts as a node
property.
+ *
+ * <p>Unsupported values are coerced to String or JSON so a single sampled
field cannot abort an
+ * entire execution-data transaction.
+ */
protected Object mapTypes(Object value) {
- Object result = value;
- if (value instanceof BigDecimal) {
- result = value.toString();
- }
- if (value instanceof Timestamp) {
- result = timestampFormat.format((Timestamp) value);
- }
- if (value instanceof Map) {
- try {
- result = HopJson.newMapper().writeValueAsString(value);
- } catch (JsonProcessingException e) {
- throw new HopRuntimeException("Error converting Map to a JSON String",
e);
- }
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof Timestamp timestamp) {
+ return TIMESTAMP_FORMAT.format(timestamp.toLocalDateTime());
+ }
+ if (value instanceof java.sql.Date sqlDate) {
+ return sqlDate.toLocalDate();
+ }
+ if (value instanceof java.sql.Time sqlTime) {
+ return sqlTime.toLocalTime();
+ }
+ if (value instanceof Date date) {
+ return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
+ }
+ if (value instanceof BigDecimal || value instanceof BigInteger) {
+ return value.toString();
+ }
+ if (value instanceof Float number) {
+ return number.doubleValue();
+ }
+ if (value instanceof Integer number) {
+ return number.longValue();
+ }
+ if (value instanceof Short number) {
+ return number.longValue();
+ }
+ if (value instanceof Byte number) {
+ return number.longValue();
+ }
+ if (value instanceof Character || value instanceof UUID) {
+ return value.toString();
}
if (value instanceof JsonNode node) {
- result = node.toPrettyString();
+ return node.toPrettyString();
+ }
+ if (value instanceof Map<?, ?> map) {
+ return toJsonString(map);
}
- return result;
+ if (value instanceof byte[] bytes) {
+ return bytes;
+ }
+ if (value instanceof List<?> list) {
+ return mapList(list);
+ }
+ if (value.getClass().isArray()) {
+ int length = Array.getLength(value);
+ List<Object> list = new ArrayList<>(length);
+ for (int i = 0; i < length; i++) {
+ list.add(Array.get(value, i));
+ }
+ return mapList(list);
+ }
+ if (isNeo4jPropertyValue(value)) {
+ return value;
+ }
+ return String.valueOf(value);
+ }
+
+ private Object mapList(List<?> list) {
+ List<Object> mapped = new ArrayList<>(list.size());
+ Class<?> elementType = null;
+ for (Object element : list) {
+ Object mappedElement = mapTypes(element);
+ if (mappedElement == null) {
+ mapped.add(null);
+ continue;
+ }
+ if (!isNeo4jPropertyValue(mappedElement)) {
+ return toJsonString(list);
+ }
+ if (elementType == null) {
+ elementType = mappedElement.getClass();
+ } else if (!elementType.equals(mappedElement.getClass())) {
+ return toJsonString(list);
+ }
+ mapped.add(mappedElement);
+ }
+ return mapped;
+ }
+
+ private String toJsonString(Object value) {
+ try {
+ return HopJson.newMapper().writeValueAsString(value);
+ } catch (JsonProcessingException e) {
+ return String.valueOf(value);
+ }
+ }
+
+ private boolean isNeo4jPropertyValue(Object value) {
+ return value instanceof Boolean
+ || value instanceof Long
+ || value instanceof Double
+ || value instanceof String
+ || value instanceof byte[]
+ || value instanceof LocalDate
+ || value instanceof LocalDateTime
+ || value instanceof LocalTime
+ || value instanceof OffsetTime
+ || value instanceof OffsetDateTime
+ || value instanceof ZonedDateTime
+ || value instanceof Duration;
}
}
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/CypherCreateBuilder.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/CypherCreateBuilder.java
index 18f86492e8..de72e59644 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/CypherCreateBuilder.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/CypherCreateBuilder.java
@@ -38,7 +38,7 @@ public class CypherCreateBuilder extends BaseCypherBuilder {
.append(" : $")
.append(key)
.append("}) ");
- parameters.put(key, value);
+ addParameter(key, value);
return this;
}
@@ -53,7 +53,7 @@ public class CypherCreateBuilder extends BaseCypherBuilder {
cypher.append(", ");
}
cypher.append(key).append(" : $").append(key);
- parameters.put(key, value);
+ addParameter(key, value);
}
cypher.append(" }) ");
return this;
@@ -68,8 +68,6 @@ public class CypherCreateBuilder extends BaseCypherBuilder {
}
cypher.append("n.").append(property).append("=$").append(property).append(" ");
- value = mapTypes(value);
-
addParameter(property, value);
return this;
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/CypherMatchBuilder.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/CypherMatchBuilder.java
index 4040300bd3..c283b5e3ea 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/CypherMatchBuilder.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/CypherMatchBuilder.java
@@ -60,7 +60,7 @@ public class CypherMatchBuilder extends BaseCypherBuilder {
cypher.append(", ");
}
cypher.append(key).append(" : $").append(param);
- parameters.put(param, value);
+ addParameter(param, value);
}
cypher.append(" }) ");
return this;
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/CypherMergeBuilder.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/CypherMergeBuilder.java
index 2c0c36da86..fef5a0de91 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/CypherMergeBuilder.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/CypherMergeBuilder.java
@@ -38,7 +38,7 @@ public class CypherMergeBuilder extends BaseCypherBuilder {
.append(" : $")
.append(key)
.append("}) ");
- parameters.put(key, value);
+ addParameter(key, value);
return this;
}
@@ -53,7 +53,7 @@ public class CypherMergeBuilder extends BaseCypherBuilder {
cypher.append(", ");
}
cypher.append(key).append(" : $").append(key);
- parameters.put(key, value);
+ addParameter(key, value);
}
cypher.append(" }) ");
@@ -69,7 +69,7 @@ public class CypherMergeBuilder extends BaseCypherBuilder {
}
cypher.append("n.").append(property).append("=$").append(property).append(" ");
- addParameter(property, mapTypes(value));
+ addParameter(property, value);
return this;
}
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/CypherQueryBuilder.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/CypherQueryBuilder.java
index 16c90c01fa..4f9eb56531 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/CypherQueryBuilder.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/builder/CypherQueryBuilder.java
@@ -44,7 +44,7 @@ public class CypherQueryBuilder extends BaseCypherBuilder {
.append(" : $")
.append(key)
.append(" }) ");
- parameters.put(key, keyValue);
+ addParameter(key, keyValue);
return this;
}
@@ -62,7 +62,7 @@ public class CypherQueryBuilder extends BaseCypherBuilder {
cypher.append(", ");
}
cypher.append(key).append(" : $").append(param);
- parameters.put(param, value);
+ addParameter(param, value);
}
cypher.append(" }) ");
@@ -175,7 +175,7 @@ public class CypherQueryBuilder extends BaseCypherBuilder {
String parameter = nodeAlias + "_" + otherKey;
Object value = nodeKeys.get(otherKey);
cypher.append(otherKey).append(" : $").append(parameter);
- parameters.put(parameter, value);
+ addParameter(parameter, value);
}
cypher.append(" }) ");
return this;
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/cache/NeoLocationCache.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/cache/NeoLocationCache.java
index 46201689fd..3795324f44 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/cache/NeoLocationCache.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/cache/NeoLocationCache.java
@@ -6,7 +6,7 @@
* (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
+ * 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,
@@ -23,7 +23,6 @@ import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
import lombok.Getter;
import lombok.Setter;
import org.apache.hop.core.Const;
@@ -38,19 +37,18 @@ import org.apache.hop.execution.caching.DatedId;
public class NeoLocationCache {
private static NeoLocationCache instance;
- private Map<String, CacheEntry> cache;
-
- private final AtomicBoolean locked;
+ private final Map<String, CacheEntry> cache;
private int maximumSize;
+ private int evictionBatchSize;
private NeoLocationCache() {
- this.locked = new AtomicBoolean(false);
this.cache = new HashMap<>();
this.maximumSize = 1000;
+ this.evictionBatchSize = 50;
}
- public static NeoLocationCache getInstance() {
+ public static synchronized NeoLocationCache getInstance() {
if (instance == null) {
instance = new NeoLocationCache();
}
@@ -58,17 +56,14 @@ public class NeoLocationCache {
}
public static void add(CacheEntry entry) {
- synchronized (getInstance().locked) {
- getInstance().locked.set(true);
- getInstance().cache.put(entry.getId(), entry);
- getInstance().locked.set(false);
+ NeoLocationCache lc = getInstance();
+ synchronized (lc.cache) {
+ lc.cache.put(entry.getId(), entry);
+ manageCacheSize();
}
- manageCacheSize();
}
public static void store(Execution execution) {
- // Add a new Cache Entry
- //
CacheEntry cacheEntry = new CacheEntry();
cacheEntry.setId(execution.getId());
cacheEntry.setExecution(execution);
@@ -77,8 +72,6 @@ public class NeoLocationCache {
}
public static void store(ExecutionState executionState) {
- // Update the cache entry
- //
CacheEntry cacheEntry = get(executionState.getId());
if (cacheEntry != null) {
cacheEntry.setExecutionState(executionState);
@@ -87,8 +80,6 @@ public class NeoLocationCache {
}
public static void store(String executionId, ExecutionData executionData) {
- // Update the cache entry
- //
CacheEntry cacheEntry = get(executionId);
if (cacheEntry != null) {
cacheEntry.addExecutionData(executionData);
@@ -97,8 +88,9 @@ public class NeoLocationCache {
}
public static CacheEntry get(String id) {
- synchronized (getInstance().locked) {
- CacheEntry cacheEntry = getInstance().cache.get(id);
+ NeoLocationCache lc = getInstance();
+ synchronized (lc.cache) {
+ CacheEntry cacheEntry = lc.cache.get(id);
if (cacheEntry != null) {
cacheEntry.setLastRead(new Date());
}
@@ -123,53 +115,37 @@ public class NeoLocationCache {
}
public static void remove(String executionId) {
- synchronized (getInstance().locked) {
- getInstance().cache.remove(executionId);
+ NeoLocationCache lc = getInstance();
+ synchronized (lc.cache) {
+ lc.cache.remove(executionId);
}
}
- private static synchronized void manageCacheSize() {
+ private static void manageCacheSize() {
NeoLocationCache lc = getInstance();
Map<String, CacheEntry> c = lc.cache;
- try {
- if (lc.locked.get()) {
- // Let the buffer overrun happen for a bit
- // We'll sweep it clean on the next one.
- return;
- }
- lc.locked.set(true);
- // The maximum size is by default 1000 entries
- if (c.size() >= lc.maximumSize + 50) {
- // Remove the last 50
- //
- List<DatedId> datedIds = new ArrayList<>();
- for (CacheEntry ce : c.values()) {
- Date date = ce.getLastRead();
- if (date == null) {
- // Never read? Perhaps it's time to get rid of it.
- //
- date = Const.MIN_DATE;
- }
- datedIds.add(new DatedId(ce.getId(), date));
- }
- // reverse sort by creation date of the cache entry
- //
- datedIds.sort(Comparator.comparing(DatedId::getDate).reversed());
-
- // Now delete the first 50 records in the cache
- //
- for (DatedId datedId : datedIds) {
- c.remove(datedId.getId());
- }
+ if (c.size() < lc.maximumSize + lc.evictionBatchSize) {
+ return;
+ }
+ List<DatedId> datedIds = new ArrayList<>();
+ for (CacheEntry ce : c.values()) {
+ Date date = ce.getLastRead();
+ if (date == null) {
+ date = Const.MIN_DATE;
}
- } finally {
- instance.locked.set(false);
+ datedIds.add(new DatedId(ce.getId(), date));
+ }
+ datedIds.sort(Comparator.comparing(DatedId::getDate));
+ int toRemove = Math.min(lc.evictionBatchSize, datedIds.size());
+ for (int i = 0; i < toRemove; i++) {
+ c.remove(datedIds.get(i).getId());
}
}
public static void clear() {
- synchronized (getInstance().locked) {
- getInstance().cache.clear();
+ NeoLocationCache lc = getInstance();
+ synchronized (lc.cache) {
+ lc.cache.clear();
}
}
}
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/path/base/NeoExecutionViewerErrorTab.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/path/base/NeoExecutionViewerErrorTab.java
index 02200b384e..39dddd89d7 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/path/base/NeoExecutionViewerErrorTab.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/path/base/NeoExecutionViewerErrorTab.java
@@ -30,6 +30,7 @@ import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.neo4j.execution.path.PathResult;
import org.apache.hop.neo4j.logging.util.LoggingCore;
import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
import org.apache.hop.ui.core.gui.GuiResource;
import org.apache.hop.ui.core.widget.TreeMemory;
import org.apache.hop.ui.core.widget.TreeUtil;
@@ -206,33 +207,36 @@ public class NeoExecutionViewerErrorTab extends
NeoExecutionViewerTabBase {
pathParams.put("executionId", executionId);
String pathCypher = getPathToFailedCypher();
- getSession()
- .executeRead(
- tx -> {
- Result pathResult = tx.run(pathCypher, pathParams);
-
- while (pathResult.hasNext()) {
- Record pathRecord = pathResult.next();
- Value pathValue = pathRecord.get(0);
- Path path = pathValue.asPath();
- List<PathResult> shortestPath = new ArrayList<>();
- for (Node node : path.nodes()) {
- PathResult nodeResult = new PathResult();
- nodeResult.setId(LoggingCore.getStringValue(node, "id"));
- nodeResult.setName(LoggingCore.getStringValue(node, "name"));
- nodeResult.setType(LoggingCore.getStringValue(node,
"executionType"));
- nodeResult.setFailed(LoggingCore.getBooleanValue(node,
"failed"));
- nodeResult.setRegistrationDate(
- LoggingCore.getDateValue(node, "registrationDate"));
- nodeResult.setCopy(LoggingCore.getStringValue(node,
"copyNr"));
-
- shortestPath.add(0, nodeResult);
+ try {
+ getSession()
+ .executeRead(
+ tx -> {
+ Result pathResult = tx.run(pathCypher, pathParams);
+
+ while (pathResult.hasNext()) {
+ Record pathRecord = pathResult.next();
+ Value pathValue = pathRecord.get(0);
+ Path path = pathValue.asPath();
+ List<PathResult> shortestPath = new ArrayList<>();
+ for (Node node : path.nodes()) {
+ PathResult nodeResult = new PathResult();
+ nodeResult.setId(LoggingCore.getStringValue(node, "id"));
+ nodeResult.setName(LoggingCore.getStringValue(node,
"name"));
+ nodeResult.setType(LoggingCore.getStringValue(node,
"executionType"));
+ nodeResult.setFailed(LoggingCore.getBooleanValue(node,
"failed"));
+ nodeResult.setRegistrationDate(
+ LoggingCore.getDateValue(node, "registrationDate"));
+ nodeResult.setCopy(LoggingCore.getStringValue(node,
"copyNr"));
+
+ shortestPath.add(0, nodeResult);
+ }
+ shortestPaths.add(shortestPath);
}
- shortestPaths.add(shortestPath);
- }
- //
- return null;
- });
+ return null;
+ });
+ } catch (Exception e) {
+ new ErrorDialog(viewer.getShell(), "Error", "Error loading the error
lineage from Neo4j", e);
+ }
return shortestPaths;
}
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/path/base/NeoExecutionViewerLineageTab.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/path/base/NeoExecutionViewerLineageTab.java
index d61c6ddcad..85341e4af6 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/path/base/NeoExecutionViewerLineageTab.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/path/base/NeoExecutionViewerLineageTab.java
@@ -31,6 +31,7 @@ import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.neo4j.execution.path.PathResult;
import org.apache.hop.neo4j.logging.util.LoggingCore;
import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
import org.apache.hop.ui.core.gui.GuiResource;
import org.apache.hop.ui.core.widget.TreeMemory;
import org.apache.hop.ui.core.widget.TreeUtil;
@@ -211,33 +212,36 @@ public class NeoExecutionViewerLineageTab extends
NeoExecutionViewerTabBase {
pathParams.put("executionId", executionId);
String pathCypher = getPathToRootCypher();
- getSession()
- .executeRead(
- tx -> {
- Result pathResult = tx.run(pathCypher, pathParams);
-
- while (pathResult.hasNext()) {
- Record pathRecord = pathResult.next();
- Value pathValue = pathRecord.get(0);
- Path path = pathValue.asPath();
- List<PathResult> shortestPath = new ArrayList<>();
- for (Node node : path.nodes()) {
- PathResult nodeResult = new PathResult();
- nodeResult.setId(LoggingCore.getStringValue(node, "id"));
- nodeResult.setName(LoggingCore.getStringValue(node, "name"));
- nodeResult.setType(LoggingCore.getStringValue(node,
"executionType"));
- nodeResult.setFailed(LoggingCore.getBooleanValue(node,
"failed"));
- nodeResult.setRegistrationDate(
- LoggingCore.getDateValue(node, "registrationDate"));
- nodeResult.setCopy(LoggingCore.getStringValue(node,
"copyNr"));
-
- shortestPath.add(0, nodeResult);
+ try {
+ getSession()
+ .executeRead(
+ tx -> {
+ Result pathResult = tx.run(pathCypher, pathParams);
+
+ while (pathResult.hasNext()) {
+ Record pathRecord = pathResult.next();
+ Value pathValue = pathRecord.get(0);
+ Path path = pathValue.asPath();
+ List<PathResult> shortestPath = new ArrayList<>();
+ for (Node node : path.nodes()) {
+ PathResult nodeResult = new PathResult();
+ nodeResult.setId(LoggingCore.getStringValue(node, "id"));
+ nodeResult.setName(LoggingCore.getStringValue(node,
"name"));
+ nodeResult.setType(LoggingCore.getStringValue(node,
"executionType"));
+ nodeResult.setFailed(LoggingCore.getBooleanValue(node,
"failed"));
+ nodeResult.setRegistrationDate(
+ LoggingCore.getDateValue(node, "registrationDate"));
+ nodeResult.setCopy(LoggingCore.getStringValue(node,
"copyNr"));
+
+ shortestPath.add(0, nodeResult);
+ }
+ shortestPaths.add(shortestPath);
}
- shortestPaths.add(shortestPath);
- }
- //
- return null;
- });
+ return null;
+ });
+ } catch (Exception e) {
+ new ErrorDialog(viewer.getShell(), "Error", "Error loading execution
lineage from Neo4j", e);
+ }
return shortestPaths;
}
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/path/base/NeoExecutionViewerTabBase.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/path/base/NeoExecutionViewerTabBase.java
index 5a0d819f96..a2ff9d85bf 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/path/base/NeoExecutionViewerTabBase.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/path/base/NeoExecutionViewerTabBase.java
@@ -67,32 +67,51 @@ public abstract class NeoExecutionViewerTabBase {
}
protected String getPathToRootCypher() {
- // Do we have a parent? If not we can just return the cypher to the
current execution node
- //
- if (StringUtils.isEmpty(viewer.getExecution().getParentId())) {
+ return
buildPathToRootCypher(StringUtils.isNotEmpty(viewer.getExecution().getParentId()));
+ }
+
+ /**
+ * Cypher that walks from a child execution to the root parent using
directed EXECUTES
+ * relationships. The cartesian {@code MATCH (top:Execution)} form is
avoided because it does not
+ * scale on a busy logging graph and is a common timeout on Neo4j 5.
+ */
+ public static String buildPathToRootCypher(boolean hasParent) {
+ if (!hasParent) {
return "MATCH(e:Execution {id: $executionId }) " + Const.CR + "RETURN e
" + Const.CR;
- } else {
- return "MATCH(top:Execution), (child:Execution {id: $executionId }),
p=shortestPath((top)-[:EXECUTES*]-(child)) "
- + Const.CR
- + "WHERE top.parentId IS NULL "
- + Const.CR
- + "RETURN p "
- + Const.CR
- + "ORDER BY size(RELATIONSHIPS(p)) DESC "
- + Const.CR
- + "LIMIT 10 "
- + Const.CR;
}
+ return "MATCH (child:Execution {id: $executionId }) "
+ + Const.CR
+ + "MATCH p = shortestPath((top:Execution)-[:EXECUTES*]->(child)) "
+ + Const.CR
+ + "WHERE top.parentId IS NULL "
+ + Const.CR
+ + "RETURN p "
+ + Const.CR
+ + "ORDER BY size(RELATIONSHIPS(p)) DESC "
+ + Const.CR
+ + "LIMIT 10 "
+ + Const.CR;
}
protected String getPathToFailedCypher() {
+ return buildPathToFailedCypher();
+ }
- return "MATCH(top:Execution {id: $executionId }), (child:Execution),
p=shortestPath((top)-[:EXECUTES*]-(child)) "
+ /**
+ * Cypher that walks from the current execution to failed leaf executions.
The leaf predicate uses
+ * a pattern predicate rather than {@code size((n)-[:EXECUTES]->())}, which
Neo4j 5 removed.
+ */
+ public static String buildPathToFailedCypher() {
+ return "MATCH (top:Execution {id: $executionId }) "
+ Const.CR
- + "WHERE child.failed "
+ + "MATCH p = shortestPath((top)-[:EXECUTES*]->(child:Execution)) "
+ + Const.CR
+ + "WHERE child.failed = true "
+ Const.CR
+ "AND child.id <> $executionId "
+ Const.CR
+ + "AND NOT (child)-[:EXECUTES]->() "
+ + Const.CR
+ "RETURN p "
+ Const.CR
+ "ORDER BY size(RELATIONSHIPS(p)) "
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/logging/util/LoggingCore.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/logging/util/LoggingCore.java
index b1f0825315..2091eb82e5 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/logging/util/LoggingCore.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/logging/util/LoggingCore.java
@@ -131,7 +131,6 @@ public class LoggingCore {
// Transaction is automatically committed by executeWrite
} catch (Exception e) {
log.logError("Error logging hierarchies", e);
- // Transaction is automatically rolled back by executeWrite on exception
}
}
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/logging/xp/PipelineLoggingExtensionPoint.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/logging/xp/PipelineLoggingExtensionPoint.java
index 09cddab8e7..42ad268d5b 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/logging/xp/PipelineLoggingExtensionPoint.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/logging/xp/PipelineLoggingExtensionPoint.java
@@ -95,25 +95,25 @@ public class PipelineLoggingExtensionPoint
pipeline.addExecutionFinishedListener(
pipelineEngine -> {
- logEndOfPipeline(log, session, connection, pipelineEngine);
-
- // If there are no other parents, we now have the complete log
channel hierarchy
- //
- if (pipelineEngine.getParentWorkflow() == null
- && pipelineEngine.getParentPipeline() == null) {
- String logChannelId = pipelineEngine.getLogChannelId();
- List<LoggingHierarchy> loggingHierarchy =
- LoggingCore.getLoggingHierarchy(logChannelId);
- logHierarchy(log, session, connection, loggingHierarchy,
logChannelId);
- }
-
- // Let's not forget to close the session and driver...
- //
- if (session != null) {
- session.close();
- }
- if (driver != null) {
- driver.close();
+ try {
+ logEndOfPipeline(log, session, connection, pipelineEngine);
+
+ // If there are no other parents, we now have the complete log
channel hierarchy
+ //
+ if (pipelineEngine.getParentWorkflow() == null
+ && pipelineEngine.getParentPipeline() == null) {
+ String logChannelId = pipelineEngine.getLogChannelId();
+ List<LoggingHierarchy> loggingHierarchy =
+ LoggingCore.getLoggingHierarchy(logChannelId);
+ logHierarchy(log, session, connection, loggingHierarchy,
logChannelId);
+ }
+ } finally {
+ if (session != null) {
+ session.close();
+ }
+ if (driver != null) {
+ driver.close();
+ }
}
});
} catch (Exception e) {
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/logging/xp/WorkflowLoggingExtensionPoint.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/logging/xp/WorkflowLoggingExtensionPoint.java
index 4d9b36e225..fa9f8baac0 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/logging/xp/WorkflowLoggingExtensionPoint.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/logging/xp/WorkflowLoggingExtensionPoint.java
@@ -90,24 +90,24 @@ public class WorkflowLoggingExtensionPoint
workflow.addWorkflowFinishedListener(
workflowMetaIWorkflowEngine -> {
- logEndOfWorkflow(log, session, connection, workflow);
-
- // If there are no other parents, we now have the complete log
channel hierarchy
- //
- if (workflow.getParentWorkflow() == null &&
workflow.getParentPipeline() == null) {
- String logChannelId = workflow.getLogChannelId();
- List<LoggingHierarchy> loggingHierarchy =
- LoggingCore.getLoggingHierarchy(logChannelId);
- logHierarchy(log, session, connection, loggingHierarchy,
logChannelId);
- }
-
- // Let's not forget to close the session and driver...
- //
- if (session != null) {
- session.close();
- }
- if (driver != null) {
- driver.close();
+ try {
+ logEndOfWorkflow(log, session, connection, workflow);
+
+ // If there are no other parents, we now have the complete log
channel hierarchy
+ //
+ if (workflow.getParentWorkflow() == null &&
workflow.getParentPipeline() == null) {
+ String logChannelId = workflow.getLogChannelId();
+ List<LoggingHierarchy> loggingHierarchy =
+ LoggingCore.getLoggingHierarchy(logChannelId);
+ logHierarchy(log, session, connection, loggingHierarchy,
logChannelId);
+ }
+ } finally {
+ if (session != null) {
+ session.close();
+ }
+ if (driver != null) {
+ driver.close();
+ }
}
});
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/shared/DriverSingleton.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/shared/DriverSingleton.java
index 99d96c746e..bdfe307a22 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/shared/DriverSingleton.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/shared/DriverSingleton.java
@@ -37,7 +37,7 @@ public class DriverSingleton {
driverMap = new HashMap<>();
}
- public static DriverSingleton getInstance() {
+ public static synchronized DriverSingleton getInstance() {
if (singleton == null) {
singleton = new DriverSingleton();
}
@@ -47,27 +47,26 @@ public class DriverSingleton {
public static Driver getDriver(ILogChannel log, IVariables variables,
NeoConnection connection)
throws HopConfigException {
DriverSingleton ds = getInstance();
-
String key = getDriverKey(connection, variables);
-
- Driver driver = ds.driverMap.get(key);
- if (driver == null) {
- driver = connection.getDriver(log, variables);
- ds.driverMap.put(key, driver);
+ synchronized (ds.driverMap) {
+ Driver driver = ds.driverMap.get(key);
+ if (driver == null) {
+ driver = connection.getDriver(log, variables);
+ ds.driverMap.put(key, driver);
+ }
+ return driver;
}
-
- return driver;
}
public static void closeAll() {
DriverSingleton ds = getInstance();
-
- List<String> keys = new ArrayList<>(ds.getDriverMap().keySet());
- for (String key : keys) {
- synchronized (ds.getDriverMap()) {
- Driver driver = ds.driverMap.get(key);
- driver.close();
- ds.driverMap.remove(key);
+ synchronized (ds.driverMap) {
+ List<String> keys = new ArrayList<>(ds.driverMap.keySet());
+ for (String key : keys) {
+ Driver driver = ds.driverMap.remove(key);
+ if (driver != null) {
+ driver.close();
+ }
}
}
}
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/shared/NeoConnection.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/shared/NeoConnection.java
index f0be18b4d4..4de7a1a0bd 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/shared/NeoConnection.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/shared/NeoConnection.java
@@ -412,6 +412,17 @@ public class NeoConnection extends HopMetadataBase
implements IHopMetadata {
Config config = configBuilder.build();
+ if (uris.isEmpty()) {
+ throw new HopConfigException("No Neo4j URIs configured for connection
" + name);
+ }
+ if (uris.size() > 1) {
+ log.logDetailed(
+ "Neo4j Java Driver 6 accepts a single URI; using "
+ + uris.get(0)
+ + " and ignoring "
+ + (uris.size() - 1)
+ + " additional server(s). Use the neo4j:// scheme for cluster
routing.");
+ }
Driver driver;
// In Neo4j 5.x, routingDriver() was removed. Use driver() with neo4j://
URI scheme for
// routing
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/transforms/importer/Importer.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/transforms/importer/Importer.java
index 8e6297b312..001986163a 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/transforms/importer/Importer.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/transforms/importer/Importer.java
@@ -149,7 +149,10 @@ public class Importer extends BaseTransform<ImporterMeta,
ImporterData> {
// Determine Neo4j version (default to 4.x for backward compatibility)
String neo4jVersion =
StringUtils.isNotEmpty(meta.getNeo4jVersion()) ?
meta.getNeo4jVersion() : "4.x";
- boolean isNeo4j5 = "5.x".equals(neo4jVersion) ||
neo4jVersion.startsWith("5.");
+ boolean isNeo4j5 =
+ neo4jVersion.startsWith("5")
+ || neo4jVersion.startsWith("2025")
+ || neo4jVersion.startsWith("2026");
// Build command based on Neo4j version
if (isNeo4j5) {
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/transforms/importer/ImporterDialog.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/transforms/importer/ImporterDialog.java
index b553a86705..43c091b8bb 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/transforms/importer/ImporterDialog.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/transforms/importer/ImporterDialog.java
@@ -211,7 +211,7 @@ public class ImporterDialog extends BaseTransformDialog {
fdlNeo4jVersion.top = new FormAttachment(lastControl, margin);
wlNeo4jVersion.setLayoutData(fdlNeo4jVersion);
wNeo4jVersion = new CCombo(wComposite, SWT.BORDER | SWT.READ_ONLY);
- wNeo4jVersion.setItems(new String[] {"4.x", "5.x"});
+ wNeo4jVersion.setItems(new String[] {"4.x", "5.x", "2025.x"});
PropsUi.setLook(wNeo4jVersion);
wNeo4jVersion.addModifyListener(lsMod);
FormData fdNeo4jVersion = new FormData();
@@ -621,7 +621,9 @@ public class ImporterDialog extends BaseTransformDialog {
wBaseFolder.setText(Const.NVL(input.getBaseFolder(), ""));
String neo4jVersion = Const.NVL(input.getNeo4jVersion(), "4.x");
- if (neo4jVersion.equals("5.x") || neo4jVersion.startsWith("5.")) {
+ if (neo4jVersion.startsWith("2025") || neo4jVersion.startsWith("2026")) {
+ wNeo4jVersion.setText("2025.x");
+ } else if (neo4jVersion.equals("5.x") || neo4jVersion.startsWith("5.")) {
wNeo4jVersion.setText("5.x");
} else {
wNeo4jVersion.setText("4.x");
diff --git
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/actions/index/Neo4jIndexCypherTest.java
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/actions/index/Neo4jIndexCypherTest.java
new file mode 100644
index 0000000000..f8c14eadb8
--- /dev/null
+++
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/actions/index/Neo4jIndexCypherTest.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.neo4j.actions.index;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hop.core.exception.HopException;
+import org.junit.jupiter.api.Test;
+
+class Neo4jIndexCypherTest {
+
+ @Test
+ void dropRequiresAnIndexName() {
+ IndexUpdate update = new IndexUpdate(UpdateType.DROP, ObjectType.NODE, "",
"Person", "id");
+
+ HopException exception =
+ assertThrows(HopException.class, () ->
Neo4jIndex.generateDropIndexCypher(update));
+ assertTrue(exception.getMessage().contains("Please drop indexes with the
name of the index"));
+ }
+
+ @Test
+ void dropUsesIfExists() throws HopException {
+ IndexUpdate update =
+ new IndexUpdate(UpdateType.DROP, ObjectType.NODE, "idx_person_id",
"Person", "id");
+
+ assertEquals("DROP INDEX idx_person_id IF EXISTS",
Neo4jIndex.generateDropIndexCypher(update));
+ }
+
+ @Test
+ void createUsesForOnSyntax() {
+ IndexUpdate update =
+ new IndexUpdate(UpdateType.CREATE, ObjectType.NODE, "idx_person_id",
"Person", "id,name");
+
+ String cypher = Neo4jIndex.generateCreateIndexCypher(update);
+ assertTrue(cypher.contains("CREATE INDEX idx_person_id IF NOT EXISTS FOR
(n:Person)"));
+ assertTrue(cypher.contains("ON (n.id, n.name)"));
+ }
+}
diff --git
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/core/data/GraphPropertyDataTypeTest.java
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/core/data/GraphPropertyDataTypeTest.java
new file mode 100644
index 0000000000..5abf7d004d
--- /dev/null
+++
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/core/data/GraphPropertyDataTypeTest.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.neo4j.core.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+class GraphPropertyDataTypeTest {
+
+ @Test
+ void numberImportTypeIsDouble() {
+ assertEquals("double", GraphPropertyDataType.Number.getImportType());
+ }
+}
diff --git
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/execution/builder/BaseCypherBuilderTest.java
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/execution/builder/BaseCypherBuilderTest.java
new file mode 100644
index 0000000000..8d2a59d2ee
--- /dev/null
+++
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/execution/builder/BaseCypherBuilderTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.neo4j.execution.builder;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class BaseCypherBuilderTest {
+
+ private static Object mapped(Object value) {
+ return CypherMergeBuilder.of().withValue("x", value).parameters().get("x");
+ }
+
+ @Test
+ void integerBecomesLong() {
+ assertEquals(5L, mapped(5));
+ }
+
+ @Test
+ void bigDecimalBecomesString() {
+ assertEquals("1.25", mapped(new BigDecimal("1.25")));
+ }
+
+ @Test
+ void dateBecomesLocalDateTime() {
+ Date date = new Date(1_700_000_000_000L);
+ assertInstanceOf(LocalDateTime.class, mapped(date));
+ }
+
+ @Test
+ void mapBecomesJsonString() {
+ Object value = mapped(Map.of("a", "b"));
+ assertInstanceOf(String.class, value);
+ assertTrue(value.toString().contains("\"a\""));
+ }
+
+ @Test
+ void mixedListBecomesJsonString() {
+ Object value = mapped(List.of("a", 1));
+ assertInstanceOf(String.class, value);
+ }
+
+ @Test
+ void homogeneousStringListIsKept() {
+ Object value = mapped(List.of("a", "b"));
+ assertEquals(List.of("a", "b"), value);
+ }
+
+ @Test
+ void unknownObjectBecomesString() {
+ assertEquals("hello", mapped(new StringBuilder("hello")));
+ }
+}
diff --git
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/execution/cache/NeoLocationCacheTest.java
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/execution/cache/NeoLocationCacheTest.java
new file mode 100644
index 0000000000..064f2fb787
--- /dev/null
+++
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/execution/cache/NeoLocationCacheTest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.neo4j.execution.cache;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.apache.hop.execution.caching.CacheEntry;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class NeoLocationCacheTest {
+
+ @BeforeEach
+ @AfterEach
+ void reset() {
+ NeoLocationCache.clear();
+ NeoLocationCache cache = NeoLocationCache.getInstance();
+ cache.setMaximumSize(1000);
+ cache.setEvictionBatchSize(50);
+ }
+
+ @Test
+ void evictionRemovesABatchNotTheWholeCache() {
+ NeoLocationCache cache = NeoLocationCache.getInstance();
+ cache.setMaximumSize(5);
+ cache.setEvictionBatchSize(50);
+
+ for (int i = 0; i < 55; i++) {
+ CacheEntry entry = new CacheEntry();
+ entry.setId("id-" + i);
+ NeoLocationCache.add(entry);
+ }
+
+ assertEquals(5, cache.getCache().size());
+ }
+
+ @Test
+ void removeDropsASingleEntry() {
+ CacheEntry entry = new CacheEntry();
+ entry.setId("keep-me");
+ NeoLocationCache.add(entry);
+
+ NeoLocationCache.remove("keep-me");
+
+ assertNull(NeoLocationCache.get("keep-me"));
+ }
+}
diff --git
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/execution/path/NeoExecutionViewerTabBaseTest.java
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/execution/path/NeoExecutionViewerTabBaseTest.java
new file mode 100644
index 0000000000..2672cd3509
--- /dev/null
+++
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/execution/path/NeoExecutionViewerTabBaseTest.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.neo4j.execution.path;
+
+import static
org.apache.hop.neo4j.CypherAssertions.assertNoSizeOfPatternExpression;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hop.neo4j.execution.path.base.NeoExecutionViewerTabBase;
+import org.junit.jupiter.api.Test;
+
+class NeoExecutionViewerTabBaseTest {
+
+ @Test
+ void lineageCypherIsDirectedAndBoundByChildId() {
+ String cypher = NeoExecutionViewerTabBase.buildPathToRootCypher(true);
+
+ assertTrue(cypher.contains("MATCH (child:Execution {id: $executionId })"));
+ assertTrue(cypher.contains("[:EXECUTES*]->(child)"));
+ assertFalse(cypher.contains("MATCH(top:Execution), (child:Execution"));
+ assertNoSizeOfPatternExpression(cypher);
+ }
+
+ @Test
+ void errorPathCypherUsesALeafPredicateAndBooleanFailed() {
+ String cypher = NeoExecutionViewerTabBase.buildPathToFailedCypher();
+
+ assertTrue(cypher.contains("child.failed = true"));
+ assertTrue(cypher.contains("AND NOT (child)-[:EXECUTES]->()"));
+ assertTrue(cypher.contains("[:EXECUTES*]->(child:Execution)"));
+ assertNoSizeOfPatternExpression(cypher);
+ }
+
+ @Test
+ void rootExecutionLineageDoesNotWalkAPath() {
+ String cypher = NeoExecutionViewerTabBase.buildPathToRootCypher(false);
+
+ assertTrue(cypher.contains("RETURN e"));
+ assertFalse(cypher.contains("shortestPath"));
+ }
+}
diff --git
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/perspective/ErrorPathCypherIT.java
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/perspective/ErrorPathCypherIT.java
index d02014638d..6296cefc5e 100644
---
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/perspective/ErrorPathCypherIT.java
+++
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/perspective/ErrorPathCypherIT.java
@@ -27,6 +27,7 @@ import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import org.apache.hop.neo4j.execution.path.base.NeoExecutionViewerTabBase;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -100,6 +101,17 @@ class ErrorPathCypherIT {
CREATE (workflow)-[:EXECUTES]->(pipeline)
CREATE (pipeline)-[:EXECUTES]->(failed)
CREATE (pipeline)-[:EXECUTES]->(ok)
+ CREATE (eiWorkflow:Execution { id : 'ei-workflow', name :
'main',\
+ executionType : 'Workflow', failed : true })
+ CREATE (eiPipeline:Execution { id : 'ei-pipeline', name :
'load',\
+ executionType : 'Pipeline', parentId : 'ei-workflow', failed
: true })
+ CREATE (eiFailed:Execution { id : 'ei-failed', name : 'Table
output',\
+ executionType : 'Transform', parentId : 'ei-pipeline', failed
: true })
+ CREATE (eiOk:Execution { id : 'ei-ok', name : 'Dummy',\
+ executionType : 'Transform', parentId : 'ei-pipeline', failed
: false })
+ CREATE (eiWorkflow)-[:EXECUTES]->(eiPipeline)
+ CREATE (eiPipeline)-[:EXECUTES]->(eiFailed)
+ CREATE (eiPipeline)-[:EXECUTES]->(eiOk)
""");
return null;
});
@@ -171,4 +183,47 @@ class ErrorPathCypherIT {
assertThrows(ClientException.class, () ->
runErrorPathCypher(removedSyntax));
}
+
+ @Test
+ void executionInfoErrorPathReturnsTheDeepestFailedLeaf() {
+ List<Record> records = runExecutionInfoErrorPath();
+
+ assertEquals(1, records.size());
+ assertEquals(List.of("ei-workflow", "ei-pipeline", "ei-failed"),
executionIds(records.get(0)));
+ }
+
+ @Test
+ void executionInfoErrorPathSkipsNonLeavesAndSuccessfulTransforms() {
+ List<String> ids = executionIds(runExecutionInfoErrorPath().get(0));
+
+ assertEquals("ei-failed", ids.get(ids.size() - 1));
+ assertFalse(ids.contains("ei-ok"));
+ }
+
+ @Test
+ void executionInfoLineageWalksDirectedExecutesToTheRoot() {
+ try (Session session = driver.session()) {
+ List<Record> records =
+ session.executeRead(
+ tx ->
+ tx.run(
+
NeoExecutionViewerTabBase.buildPathToRootCypher(true),
+ Map.of("executionId", "ei-failed"))
+ .list());
+ assertEquals(1, records.size());
+ assertEquals(
+ List.of("ei-workflow", "ei-pipeline", "ei-failed"),
executionIds(records.get(0)));
+ }
+ }
+
+ private List<Record> runExecutionInfoErrorPath() {
+ try (Session session = driver.session()) {
+ return session.executeRead(
+ tx ->
+ tx.run(
+ NeoExecutionViewerTabBase.buildPathToFailedCypher(),
+ Map.of("executionId", "ei-workflow"))
+ .list());
+ }
+ }
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/PipelineExecutionViewer.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/PipelineExecutionViewer.java
index 4ccb36ff59..1480226183 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/PipelineExecutionViewer.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/PipelineExecutionViewer.java
@@ -791,6 +791,7 @@ public class PipelineExecutionViewer extends
BaseExecutionViewer
pipelinePainter.setMaximum(maximum);
pipelinePainter.setShowingNavigationView(true);
pipelinePainter.setScreenMagnification(magnification);
+ pipelinePainter.setTransformLogMap(buildTransformErrorMap());
try {
pipelinePainter.drawPipelineImage();
@@ -808,6 +809,28 @@ public class PipelineExecutionViewer extends
BaseExecutionViewer
CanvasFacade.setData(canvas, magnification, offset, pipelineMeta);
}
+ /**
+ * Populate the painter error map from stored component metrics so failed
transforms get a red
+ * border even though this viewer has no live {@code IPipelineEngine}.
+ */
+ private Map<String, String> buildTransformErrorMap() {
+ Map<String, String> transformErrorMap = new HashMap<>();
+ if (executionState == null || executionState.getMetrics() == null) {
+ return transformErrorMap;
+ }
+ String errorHeader = Pipeline.METRIC_ERROR.getHeader();
+ for (ExecutionStateComponentMetrics metrics : executionState.getMetrics())
{
+ if (metrics.getMetrics() == null) {
+ continue;
+ }
+ Long errors = metrics.getMetrics().get(errorHeader);
+ if (errors != null && errors > 0 &&
StringUtils.isNotEmpty(metrics.getComponentName())) {
+ transformErrorMap.put(metrics.getComponentName(), errors + "
error(s)");
+ }
+ }
+ return transformErrorMap;
+ }
+
@Override
public Control getControl() {
return this;
@@ -1107,7 +1130,7 @@ public class PipelineExecutionViewer extends
BaseExecutionViewer
// Don't load logging text as that can be a lot of data.
// Lazily load that when the logging text comes into focus.
//
- ExecutionState executionState =
iLocation.getExecutionState(execution.getId(), false);
+ ExecutionState executionState =
iLocation.getExecutionState(child.getId(), false);
perspective.createExecutionViewer(locationName, child,
executionState);
return;
}
@@ -1158,7 +1181,7 @@ public class PipelineExecutionViewer extends
BaseExecutionViewer
}
// Don't load execution logging text to prevent memory issues.
//
- ExecutionState executionState =
iLocation.getExecutionState(execution.getId(), false);
+ ExecutionState executionState =
iLocation.getExecutionState(childExecution.getId(), false);
perspective.createExecutionViewer(locationName, childExecution,
executionState);
} catch (Exception e) {