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 9e94fa7411 Issue #9039 : Read Neo4j execution dates stored by graph
logging (#9044)
9e94fa7411 is described below
commit 9e94fa74117db5d7e664f5b7f4bb14c20dc666b3
Author: Matt Casters <[email protected]>
AuthorDate: Tue Oct 20 20:34:19 2026 +0200
Issue #9039 : Read Neo4j execution dates stored by graph logging (#9044)
Graph logging writes registration dates as yyyy/MM/dd'T'HH:mm:ss. The
execution information location only parsed ISO local date-times, so one such
value aborted the whole execution list. Parse the logging format, ISO strings,
and offset values, skip a node that still cannot be read, and clear the static
location cache when the location closes.
An integration test writes a workflow and pipeline with logging enabled and
reads the dates, parent link, and sample rows back through the location.
---
.../main-neo4j-execution-info-roundtrip.hwf | 217 ++++
.../roundtrip-check.hpl | 1190 ++++++++++++++++++++
.../subject-pipeline.hpl | 114 ++
.../subject-workflow.hwf | 93 ++
.../neo4j/execution/NeoExecutionInfoLocation.java | 128 ++-
.../apache/hop/neo4j/logging/util/LoggingCore.java | 63 +-
.../NeoExecutionInfoLocationReadTest.java | 112 ++
.../hop/neo4j/logging/util/LoggingCoreTest.java | 24 +-
8 files changed, 1908 insertions(+), 33 deletions(-)
diff --git
a/integration-tests/neo4j/tests/neo4j-execution-info-roundtrip/main-neo4j-execution-info-roundtrip.hwf
b/integration-tests/neo4j/tests/neo4j-execution-info-roundtrip/main-neo4j-execution-info-roundtrip.hwf
new file mode 100644
index 0000000000..47afb8da3e
--- /dev/null
+++
b/integration-tests/neo4j/tests/neo4j-execution-info-roundtrip/main-neo4j-execution-info-roundtrip.hwf
@@ -0,0 +1,217 @@
+<?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-neo4j-execution-info-roundtrip</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Writes a workflow and a pipeline to the Neo4j execution
information location while graph logging is on, then reads them back. See issue
#9039.</description>
+ <extended_description>Graph logging stores executionStart as
yyyy/MM/dd'T'HH:mm:ss. The check copies that text onto the pipeline
registrationDate and the same instant as an ISO string onto the workflow, then
reads both back through the execution information
location.</extended_description>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2026/10/20 14:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/10/20 14:00:00.000</modified_date>
+ <parameters>
+ </parameters>
+ <actions>
+ <action>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <intervalSeconds>0</intervalSeconds>
+ <intervalMinutes>60</intervalMinutes>
+ <hour>12</hour>
+ <minutes>0</minutes>
+ <weekDay>1</weekDay>
+ <DayOfMonth>1</DayOfMonth>
+ <parallel>N</parallel>
+ <xloc>80</xloc>
+ <yloc>128</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>cleanup-database.hwf</name>
+ <description/>
+ <type>WORKFLOW</type>
+ <attributes/>
+ <filename>${PROJECT_HOME}/tests/shared/cleanup-database.hwf</filename>
+ <params_from_previous>N</params_from_previous>
+ <exec_per_row>N</exec_per_row>
+ <set_logfile>N</set_logfile>
+ <logfile/>
+ <logext/>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <loglevel>Nothing</loglevel>
+ <wait_until_finished>Y</wait_until_finished>
+ <follow_abort_remote>N</follow_abort_remote>
+ <create_parent_folder>N</create_parent_folder>
+ <pass_export>N</pass_export>
+ <run_configuration>local</run_configuration>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <set_append_logfile>N</set_append_logfile>
+ <parallel>N</parallel>
+ <xloc>240</xloc>
+ <yloc>128</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>enable logging</name>
+ <description/>
+ <type>SET_VARIABLES</type>
+ <attributes/>
+ <replacevars>Y</replacevars>
+ <filename/>
+ <file_variable_type>CURRENT_WORKFLOW</file_variable_type>
+ <fields>
+ <field>
+ <variable_name>HOP_GRAPH_LOGGING_CONNECTION</variable_name>
+ <variable_value>Test</variable_value>
+ <variable_type>CURRENT_WORKFLOW</variable_type>
+ </field>
+ </fields>
+ <parallel>N</parallel>
+ <xloc>416</xloc>
+ <yloc>128</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>subject-workflow.hwf</name>
+ <description/>
+ <type>WORKFLOW</type>
+ <attributes/>
+
<filename>${PROJECT_HOME}/tests/neo4j-execution-info-roundtrip/subject-workflow.hwf</filename>
+ <params_from_previous>N</params_from_previous>
+ <exec_per_row>N</exec_per_row>
+ <set_logfile>N</set_logfile>
+ <logfile/>
+ <logext/>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <loglevel>Basic</loglevel>
+ <wait_until_finished>Y</wait_until_finished>
+ <follow_abort_remote>N</follow_abort_remote>
+ <create_parent_folder>N</create_parent_folder>
+ <pass_export>N</pass_export>
+ <run_configuration>local-neo4j</run_configuration>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <set_append_logfile>N</set_append_logfile>
+ <parallel>N</parallel>
+ <xloc>608</xloc>
+ <yloc>128</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>disable logging</name>
+ <description/>
+ <type>SET_VARIABLES</type>
+ <attributes/>
+ <replacevars>Y</replacevars>
+ <filename/>
+ <file_variable_type>CURRENT_WORKFLOW</file_variable_type>
+ <fields>
+ <field>
+ <variable_name>HOP_GRAPH_LOGGING_CONNECTION</variable_name>
+ <variable_value>-</variable_value>
+ <variable_type>CURRENT_WORKFLOW</variable_type>
+ </field>
+ </fields>
+ <parallel>N</parallel>
+ <xloc>800</xloc>
+ <yloc>128</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>roundtrip-check.hpl</name>
+ <description/>
+ <type>PIPELINE</type>
+ <attributes/>
+
<filename>${PROJECT_HOME}/tests/neo4j-execution-info-roundtrip/roundtrip-check.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>
+ <set_logfile>N</set_logfile>
+ <logfile/>
+ <logext/>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <loglevel>Basic</loglevel>
+ <set_append_logfile>N</set_append_logfile>
+ <wait_until_finished>Y</wait_until_finished>
+ <follow_abort_remote>N</follow_abort_remote>
+ <create_parent_folder>N</create_parent_folder>
+ <run_configuration>local</run_configuration>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <parallel>N</parallel>
+ <xloc>992</xloc>
+ <yloc>128</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>cleanup-database.hwf</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ <hop>
+ <from>cleanup-database.hwf</from>
+ <to>enable logging</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>enable logging</from>
+ <to>subject-workflow.hwf</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>subject-workflow.hwf</from>
+ <to>disable logging</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>disable logging</from>
+ <to>roundtrip-check.hpl</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git
a/integration-tests/neo4j/tests/neo4j-execution-info-roundtrip/roundtrip-check.hpl
b/integration-tests/neo4j/tests/neo4j-execution-info-roundtrip/roundtrip-check.hpl
new file mode 100644
index 0000000000..3b5af4dd48
--- /dev/null
+++
b/integration-tests/neo4j/tests/neo4j-execution-info-roundtrip/roundtrip-check.hpl
@@ -0,0 +1,1190 @@
+<?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>roundtrip-check</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Reads the subject workflow and pipeline back from the Neo4j
execution information location, including a logging-format registration date.
See issue #9039.</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/20 14:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/10/20 14:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>names</from>
+ <to>count logging dates</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>count logging dates</from>
+ <to>logging dates present</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>logging dates present</from>
+ <to>set pipeline date</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>logging dates present</from>
+ <to>Abort no logging date</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>set pipeline date</from>
+ <to>pipeline updated</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>pipeline updated</from>
+ <to>set workflow date</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>pipeline updated</from>
+ <to>Abort pipeline not stored</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>set workflow date</from>
+ <to>workflow updated</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>workflow updated</from>
+ <to>stringify other dates</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>workflow updated</from>
+ <to>Abort workflow not stored</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>stringify other dates</from>
+ <to>remove typeless</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>remove typeless</from>
+ <to>find pipeline</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>remove typeless</from>
+ <to>find workflow</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>find pipeline</from>
+ <to>missing pipeline</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>find pipeline</from>
+ <to>check pipeline</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>missing pipeline</from>
+ <to>Abort missing pipeline</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>check pipeline</from>
+ <to>find parent</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>check pipeline</from>
+ <to>execution data</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>find parent</from>
+ <to>missing parent</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>find parent</from>
+ <to>check parent</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>missing parent</from>
+ <to>Abort missing parent</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>check parent</from>
+ <to>keep child lookup fields</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>keep child lookup fields</from>
+ <to>find children</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>find children</from>
+ <to>child is pipeline</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>child is pipeline</from>
+ <to>child ok</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>child ok</from>
+ <to>no child</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>no child</from>
+ <to>Abort no child</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>execution data</from>
+ <to>rows with marker</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>rows with marker</from>
+ <to>samples ok</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>samples ok</from>
+ <to>no samples</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>no samples</from>
+ <to>Abort no samples</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>find workflow</from>
+ <to>missing workflow</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>find workflow</from>
+ <to>check workflow</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>missing workflow</from>
+ <to>Abort missing workflow</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>names</name>
+ <type>DataGrid</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <data>
+ <line>
+ <item>subject-pipeline</item>
+ <item>Pipeline</item>
+ <item>subject-workflow</item>
+ <item>Workflow</item>
+ <item>all-transforms</item>
+ </line>
+ </data>
+ <fields>
+ <field>
+ <name>pipelineName</name>
+ <type>String</type>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ <field>
+ <name>pipelineType</name>
+ <type>String</type>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ <field>
+ <name>workflowName</name>
+ <type>String</type>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ <field>
+ <name>workflowType</name>
+ <type>String</type>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ <field>
+ <name>owner</name>
+ <type>String</type>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>80</xloc>
+ <yloc>160</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>count logging dates</name>
+ <type>Neo4jCypherOutput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <connection>Test</connection>
+ <cypher>MATCH (e:Execution) WHERE e.executionStart IS NOT NULL AND
valueType(e.executionStart) STARTS WITH 'STRING' RETURN count(e) AS
loggingDates</cypher>
+ <cypher_from_field>N</cypher_from_field>
+ <read_only>N</read_only>
+ <retry>Y</retry>
+ <returning_graph>N</returning_graph>
+ <unwind>N</unwind>
+ <returns>
+ <return>
+ <name>loggingDates</name>
+ <source_type>Integer</source_type>
+ <type>Integer</type>
+ </return>
+ </returns>
+ <attributes/>
+ <GUI>
+ <xloc>256</xloc>
+ <yloc>160</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>logging dates present</name>
+ <type>FilterRows</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <send_true_to>set pipeline date</send_true_to>
+ <send_false_to>Abort no logging date</send_false_to>
+ <compare>
+ <condition>
+ <negated>N</negated>
+ <leftvalue>loggingDates</leftvalue>
+ <function>></function>
+ <rightvalue/>
+ <value>
+ <name>constant</name>
+ <type>Integer</type>
+ <text>0</text>
+ <length>-1</length>
+ <precision>-1</precision>
+ <isnull>N</isnull>
+ <mask/>
+ </value>
+ </condition>
+ </compare>
+ <attributes/>
+ <GUI>
+ <xloc>432</xloc>
+ <yloc>160</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Abort no logging date</name>
+ <type>Abort</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <abort_option>ABORT_WITH_ERROR</abort_option>
+ <always_log_rows>Y</always_log_rows>
+ <message>Graph logging did not store an executionStart string</message>
+ <row_threshold>0</row_threshold>
+ <attributes/>
+ <GUI>
+ <xloc>432</xloc>
+ <yloc>304</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>set pipeline date</name>
+ <type>Neo4jCypherOutput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <connection>Test</connection>
+ <cypher>MATCH (src:Execution {type: 'PIPELINE', name: 'subject-pipeline'})
WHERE src.executionStart IS NOT NULL AND valueType(src.executionStart) STARTS
WITH 'STRING' MATCH (e:Execution {executionType: 'Pipeline', name:
'subject-pipeline'}) SET e.registrationDate = src.executionStart RETURN
src.executionStart AS loggedPipelineDate, count(e) AS pipelineNodes</cypher>
+ <cypher_from_field>N</cypher_from_field>
+ <read_only>N</read_only>
+ <retry>Y</retry>
+ <returning_graph>N</returning_graph>
+ <unwind>N</unwind>
+ <returns>
+ <return>
+ <name>loggedPipelineDate</name>
+ <source_type>String</source_type>
+ <type>String</type>
+ </return>
+ <return>
+ <name>pipelineNodes</name>
+ <source_type>Integer</source_type>
+ <type>Integer</type>
+ </return>
+ </returns>
+ <attributes/>
+ <GUI>
+ <xloc>608</xloc>
+ <yloc>160</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>pipeline updated</name>
+ <type>FilterRows</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <send_true_to>set workflow date</send_true_to>
+ <send_false_to>Abort pipeline not stored</send_false_to>
+ <compare>
+ <condition>
+ <negated>N</negated>
+ <leftvalue>pipelineNodes</leftvalue>
+ <function>></function>
+ <rightvalue/>
+ <value>
+ <name>constant</name>
+ <type>Integer</type>
+ <text>0</text>
+ <length>-1</length>
+ <precision>-1</precision>
+ <isnull>N</isnull>
+ <mask/>
+ </value>
+ </condition>
+ </compare>
+ <attributes/>
+ <GUI>
+ <xloc>784</xloc>
+ <yloc>160</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Abort pipeline not stored</name>
+ <type>Abort</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <abort_option>ABORT_WITH_ERROR</abort_option>
+ <always_log_rows>Y</always_log_rows>
+ <message>The subject pipeline execution was not stored</message>
+ <row_threshold>0</row_threshold>
+ <attributes/>
+ <GUI>
+ <xloc>784</xloc>
+ <yloc>304</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>set workflow date</name>
+ <type>Neo4jCypherOutput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <connection>Test</connection>
+ <cypher>MATCH (src:Execution {type: 'WORKFLOW', name: 'subject-workflow'})
WHERE src.executionStart IS NOT NULL AND valueType(src.executionStart) STARTS
WITH 'STRING' MATCH (e:Execution {executionType: 'Workflow', name:
'subject-workflow'}) SET e.registrationDate = replace(src.executionStart, '/',
'-') RETURN replace(src.executionStart, '/', '-') AS loggedWorkflowDate,
count(e) AS workflowNodes</cypher>
+ <cypher_from_field>N</cypher_from_field>
+ <read_only>N</read_only>
+ <retry>Y</retry>
+ <returning_graph>N</returning_graph>
+ <unwind>N</unwind>
+ <returns>
+ <return>
+ <name>loggedWorkflowDate</name>
+ <source_type>String</source_type>
+ <type>String</type>
+ </return>
+ <return>
+ <name>workflowNodes</name>
+ <source_type>Integer</source_type>
+ <type>Integer</type>
+ </return>
+ </returns>
+ <attributes/>
+ <GUI>
+ <xloc>960</xloc>
+ <yloc>160</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>workflow updated</name>
+ <type>FilterRows</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <send_true_to>stringify other dates</send_true_to>
+ <send_false_to>Abort workflow not stored</send_false_to>
+ <compare>
+ <condition>
+ <negated>N</negated>
+ <leftvalue>workflowNodes</leftvalue>
+ <function>></function>
+ <rightvalue/>
+ <value>
+ <name>constant</name>
+ <type>Integer</type>
+ <text>0</text>
+ <length>-1</length>
+ <precision>-1</precision>
+ <isnull>N</isnull>
+ <mask/>
+ </value>
+ </condition>
+ </compare>
+ <attributes/>
+ <GUI>
+ <xloc>1136</xloc>
+ <yloc>160</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Abort workflow not stored</name>
+ <type>Abort</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <abort_option>ABORT_WITH_ERROR</abort_option>
+ <always_log_rows>Y</always_log_rows>
+ <message>The subject workflow execution was not stored</message>
+ <row_threshold>0</row_threshold>
+ <attributes/>
+ <GUI>
+ <xloc>1136</xloc>
+ <yloc>304</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>stringify other dates</name>
+ <type>Neo4jCypherOutput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <connection>Test</connection>
+ <cypher>MATCH (e:Execution) WHERE e.executionType IS NOT NULL AND
e.registrationDate IS NOT NULL AND NOT valueType(e.registrationDate) STARTS
WITH 'STRING' SET e.registrationDate = '2026/10/07T16:08:52' RETURN count(e) AS
converted</cypher>
+ <cypher_from_field>N</cypher_from_field>
+ <read_only>N</read_only>
+ <retry>Y</retry>
+ <returning_graph>N</returning_graph>
+ <unwind>N</unwind>
+ <returns>
+ <return>
+ <name>converted</name>
+ <source_type>Integer</source_type>
+ <type>Integer</type>
+ </return>
+ </returns>
+ <attributes/>
+ <GUI>
+ <xloc>1312</xloc>
+ <yloc>160</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>remove typeless</name>
+ <type>Neo4jCypherOutput</type>
+ <description/>
+ <distribute>N</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <connection>Test</connection>
+ <cypher>MATCH (e:Execution) WHERE e.executionType IS NULL WITH collect(e)
AS doomed FOREACH (n IN doomed | DETACH DELETE n) RETURN size(doomed) AS
removed</cypher>
+ <cypher_from_field>N</cypher_from_field>
+ <read_only>N</read_only>
+ <retry>Y</retry>
+ <returning_graph>N</returning_graph>
+ <unwind>N</unwind>
+ <returns>
+ <return>
+ <name>removed</name>
+ <source_type>Integer</source_type>
+ <type>Integer</type>
+ </return>
+ </returns>
+ <attributes/>
+ <GUI>
+ <xloc>1488</xloc>
+ <yloc>160</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>find pipeline</name>
+ <type>ExecInfo</type>
+ <description/>
+ <distribute>N</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <location>neo4j</location>
+ <operationType>FindLastExecution</operationType>
+ <idFieldName/>
+ <parentIdFieldName/>
+ <typeFieldName>pipelineType</typeFieldName>
+ <nameFieldName>pipelineName</nameFieldName>
+ <includeChildrenFieldName/>
+ <limitFieldName/>
+ <attributes/>
+ <GUI>
+ <xloc>1680</xloc>
+ <yloc>64</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>missing pipeline</name>
+ <type>DetectEmptyStream</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>1856</xloc>
+ <yloc>208</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Abort missing pipeline</name>
+ <type>Abort</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <abort_option>ABORT_WITH_ERROR</abort_option>
+ <always_log_rows>Y</always_log_rows>
+ <message>The location did not return the subject pipeline</message>
+ <row_threshold>0</row_threshold>
+ <attributes/>
+ <GUI>
+ <xloc>2032</xloc>
+ <yloc>208</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>check pipeline</name>
+ <type>ScriptValueMod</type>
+ <description/>
+ <distribute>N</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><![CDATA[if (registrationDate == null ||
executionStartDate == null || executionEndDate == null || updateTime == null) {
+ throw "Pipeline execution is missing a date";
+}
+if (parentId == null || parentId == "") {
+ throw "Pipeline execution has no parent";
+}
+if (failed == true) {
+ throw "Pipeline execution failed";
+}
+if (statusDescription == null || statusDescription.indexOf("Finished") < 0) {
+ throw "Pipeline status is not finished: " + statusDescription;
+}
+var DateFormat = Packages.java.text.SimpleDateFormat;
+var text = "" + new
DateFormat("yyyy/MM/dd'T'HH:mm:ss").format(registrationDate);
+var expected = "" + loggedPipelineDate;
+if (text != expected) {
+ throw "Pipeline registrationDate was [" + text + "] expected [" + expected +
"]";
+}
+]]></jsScript_script>
+ </jsScript>
+ </jsScripts>
+ <fields/>
+ <attributes/>
+ <GUI>
+ <xloc>1856</xloc>
+ <yloc>64</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>find parent</name>
+ <type>ExecInfo</type>
+ <description/>
+ <distribute>N</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <location>neo4j</location>
+ <operationType>FindParentId</operationType>
+ <idFieldName>executionId</idFieldName>
+ <parentIdFieldName/>
+ <typeFieldName/>
+ <nameFieldName/>
+ <includeChildrenFieldName/>
+ <limitFieldName/>
+ <attributes/>
+ <GUI>
+ <xloc>2032</xloc>
+ <yloc>64</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>missing parent</name>
+ <type>DetectEmptyStream</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>2208</xloc>
+ <yloc>208</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Abort missing parent</name>
+ <type>Abort</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <abort_option>ABORT_WITH_ERROR</abort_option>
+ <always_log_rows>Y</always_log_rows>
+ <message>findParentId returned nothing for the subject pipeline</message>
+ <row_threshold>0</row_threshold>
+ <attributes/>
+ <GUI>
+ <xloc>2384</xloc>
+ <yloc>208</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>check 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><![CDATA[if (id == null || !id.equals(parentId)) {
+ throw "findParentId [" + id + "] did not match parentId [" + parentId + "]";
+}
+]]></jsScript_script>
+ </jsScript>
+ </jsScripts>
+ <fields/>
+ <attributes/>
+ <GUI>
+ <xloc>2208</xloc>
+ <yloc>64</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>keep child lookup fields</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>pipelineExecutionId</rename>
+ </field>
+ <select_unspecified>N</select_unspecified>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>2384</xloc>
+ <yloc>64</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>find children</name>
+ <type>ExecInfo</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <location>neo4j</location>
+ <operationType>FindExecutions</operationType>
+ <idFieldName/>
+ <parentIdFieldName>parentId</parentIdFieldName>
+ <typeFieldName/>
+ <nameFieldName/>
+ <includeChildrenFieldName/>
+ <limitFieldName/>
+ <attributes/>
+ <GUI>
+ <xloc>2560</xloc>
+ <yloc>64</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>child is pipeline</name>
+ <type>FilterRows</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <send_true_to>child ok</send_true_to>
+ <send_false_to/>
+ <compare>
+ <condition>
+ <negated>N</negated>
+ <leftvalue>name</leftvalue>
+ <function>=</function>
+ <rightvalue/>
+ <value>
+ <name>constant</name>
+ <type>String</type>
+ <text>subject-pipeline</text>
+ <length>-1</length>
+ <precision>-1</precision>
+ <isnull>N</isnull>
+ <mask/>
+ </value>
+ </condition>
+ </compare>
+ <attributes/>
+ <GUI>
+ <xloc>2736</xloc>
+ <yloc>64</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>child ok</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>2912</xloc>
+ <yloc>64</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>no child</name>
+ <type>DetectEmptyStream</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>2912</xloc>
+ <yloc>208</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Abort no child</name>
+ <type>Abort</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <abort_option>ABORT_WITH_ERROR</abort_option>
+ <always_log_rows>Y</always_log_rows>
+ <message>findExecutions did not return the subject pipeline</message>
+ <row_threshold>0</row_threshold>
+ <attributes/>
+ <GUI>
+ <xloc>3088</xloc>
+ <yloc>208</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>execution data</name>
+ <type>ExecInfo</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <location>neo4j</location>
+ <operationType>GetExecutionData</operationType>
+ <idFieldName>owner</idFieldName>
+ <parentIdFieldName>executionId</parentIdFieldName>
+ <typeFieldName/>
+ <nameFieldName/>
+ <includeChildrenFieldName/>
+ <limitFieldName/>
+ <attributes/>
+ <GUI>
+ <xloc>2032</xloc>
+ <yloc>400</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>rows with marker</name>
+ <type>FilterRows</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <send_true_to>samples ok</send_true_to>
+ <send_false_to/>
+ <compare>
+ <condition>
+ <negated>N</negated>
+ <conditions>
+ <condition>
+ <negated>N</negated>
+ <leftvalue>setRowDataJson</leftvalue>
+ <function>CONTAINS</function>
+ <operator>AND</operator>
+ <value>
+ <name>constant</name>
+ <type>String</type>
+ <text>hop-graph-execution-info</text>
+ <length>-1</length>
+ <precision>-1</precision>
+ <isnull>N</isnull>
+ <mask/>
+ </value>
+ </condition>
+ <condition>
+ <negated>N</negated>
+ <leftvalue>collectionDate</leftvalue>
+ <function>IS NOT NULL</function>
+ <operator>-</operator>
+ <value>
+ <name>constant</name>
+ <type>String</type>
+ <text/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <isnull>N</isnull>
+ <mask/>
+ </value>
+ </condition>
+ </conditions>
+ </condition>
+ </compare>
+ <attributes/>
+ <GUI>
+ <xloc>2208</xloc>
+ <yloc>400</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>samples ok</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>2384</xloc>
+ <yloc>400</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>no samples</name>
+ <type>DetectEmptyStream</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>2384</xloc>
+ <yloc>528</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Abort no samples</name>
+ <type>Abort</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <abort_option>ABORT_WITH_ERROR</abort_option>
+ <always_log_rows>Y</always_log_rows>
+ <message>Execution samples did not contain the marker</message>
+ <row_threshold>0</row_threshold>
+ <attributes/>
+ <GUI>
+ <xloc>2560</xloc>
+ <yloc>528</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>find workflow</name>
+ <type>ExecInfo</type>
+ <description/>
+ <distribute>N</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <location>neo4j</location>
+ <operationType>FindLastExecution</operationType>
+ <idFieldName/>
+ <parentIdFieldName/>
+ <typeFieldName>workflowType</typeFieldName>
+ <nameFieldName>workflowName</nameFieldName>
+ <includeChildrenFieldName/>
+ <limitFieldName/>
+ <attributes/>
+ <GUI>
+ <xloc>1680</xloc>
+ <yloc>640</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>missing workflow</name>
+ <type>DetectEmptyStream</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>1856</xloc>
+ <yloc>768</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Abort missing workflow</name>
+ <type>Abort</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <abort_option>ABORT_WITH_ERROR</abort_option>
+ <always_log_rows>Y</always_log_rows>
+ <message>The location did not return the subject workflow</message>
+ <row_threshold>0</row_threshold>
+ <attributes/>
+ <GUI>
+ <xloc>2032</xloc>
+ <yloc>768</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>check workflow</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><![CDATA[if (registrationDate == null) {
+ throw "Workflow execution is missing its registration date";
+}
+var DateFormat = Packages.java.text.SimpleDateFormat;
+var text = "" + new
DateFormat("yyyy-MM-dd'T'HH:mm:ss").format(registrationDate);
+var expected = "" + loggedWorkflowDate;
+if (text != expected) {
+ throw "Workflow registrationDate was [" + text + "] expected [" + expected +
"]";
+}
+]]></jsScript_script>
+ </jsScript>
+ </jsScripts>
+ <fields/>
+ <attributes/>
+ <GUI>
+ <xloc>1856</xloc>
+ <yloc>640</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git
a/integration-tests/neo4j/tests/neo4j-execution-info-roundtrip/subject-pipeline.hpl
b/integration-tests/neo4j/tests/neo4j-execution-info-roundtrip/subject-pipeline.hpl
new file mode 100644
index 0000000000..fc8c89a0d0
--- /dev/null
+++
b/integration-tests/neo4j/tests/neo4j-execution-info-roundtrip/subject-pipeline.hpl
@@ -0,0 +1,114 @@
+<?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>subject-pipeline</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Three rows stored as execution samples by the Neo4j execution
information location.</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/20 14:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/10/20 14:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>value</from>
+ <to>done</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>value</name>
+ <type>DataGrid</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <data>
+ <line>
+ <item>hop-graph-execution-info</item>
+ <item>1</item>
+ </line>
+ <line>
+ <item>hop-graph-execution-info</item>
+ <item>2</item>
+ </line>
+ <line>
+ <item>hop-graph-execution-info</item>
+ <item>3</item>
+ </line>
+ </data>
+ <fields>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>marker</name>
+ <type>String</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>nr</name>
+ <type>Integer</type>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>128</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>done</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>320</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git
a/integration-tests/neo4j/tests/neo4j-execution-info-roundtrip/subject-workflow.hwf
b/integration-tests/neo4j/tests/neo4j-execution-info-roundtrip/subject-workflow.hwf
new file mode 100644
index 0000000000..d06f7ba05c
--- /dev/null
+++
b/integration-tests/neo4j/tests/neo4j-execution-info-roundtrip/subject-workflow.hwf
@@ -0,0 +1,93 @@
+<?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>subject-workflow</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Runs the subject pipeline with execution samples stored in
Neo4j.</description>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2026/10/20 14:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/10/20 14:00:00.000</modified_date>
+ <parameters>
+ </parameters>
+ <actions>
+ <action>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <intervalSeconds>0</intervalSeconds>
+ <intervalMinutes>60</intervalMinutes>
+ <hour>12</hour>
+ <minutes>0</minutes>
+ <weekDay>1</weekDay>
+ <DayOfMonth>1</DayOfMonth>
+ <parallel>N</parallel>
+ <xloc>176</xloc>
+ <yloc>128</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>subject-pipeline.hpl</name>
+ <description/>
+ <type>PIPELINE</type>
+ <attributes/>
+
<filename>${PROJECT_HOME}/tests/neo4j-execution-info-roundtrip/subject-pipeline.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>
+ <set_logfile>N</set_logfile>
+ <logfile/>
+ <logext/>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <loglevel>Basic</loglevel>
+ <set_append_logfile>N</set_append_logfile>
+ <wait_until_finished>Y</wait_until_finished>
+ <follow_abort_remote>N</follow_abort_remote>
+ <create_parent_folder>N</create_parent_folder>
+ <run_configuration>local-graph-location</run_configuration>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <parallel>N</parallel>
+ <xloc>400</xloc>
+ <yloc>128</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>subject-pipeline.hpl</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
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 abf952a2d0..fbc713ebfd 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
@@ -27,7 +27,6 @@ import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
-import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Base64;
@@ -75,6 +74,7 @@ import org.apache.hop.execution.IExecutionInfoLocation;
import org.apache.hop.execution.IExecutionMatcher;
import org.apache.hop.execution.IExecutionSelector;
import org.apache.hop.execution.LastPeriod;
+import org.apache.hop.execution.ListedExecution;
import org.apache.hop.execution.plugin.ExecutionInfoLocationPlugin;
import org.apache.hop.metadata.api.HopMetadataProperty;
import org.apache.hop.metadata.api.IHopMetadataProvider;
@@ -90,6 +90,7 @@ import
org.apache.hop.neo4j.execution.builder.CypherQueryBuilder;
import org.apache.hop.neo4j.execution.builder.CypherRelationshipBuilder;
import org.apache.hop.neo4j.execution.builder.ICypherBuilder;
import org.apache.hop.neo4j.execution.cache.NeoLocationCache;
+import org.apache.hop.neo4j.logging.util.LoggingCore;
import org.apache.hop.neo4j.shared.CypherConnectionSelectionLine;
import org.apache.hop.neo4j.shared.NamedGraphConnection;
import org.apache.hop.neo4j.shared.NeoConnectionUtils;
@@ -253,6 +254,9 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
throw new HopException("Error closing graph database execution
information location", e);
} finally {
connection = null;
+ // The cache is static and shared by every location in this JVM. A
finished engine must
+ // not keep serving its copy to the next reader.
+ NeoLocationCache.clear();
}
}
@@ -912,8 +916,6 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
public List<String> findNeo4jExecutionIDs(
IGraphTransaction transaction, IExecutionSelector selector) {
- List<String> ids = new ArrayList<>();
-
CypherQueryBuilder builder =
CypherQueryBuilder.of().withLabelAndKeys("n", EL_EXECUTION, Map.of());
@@ -992,18 +994,78 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
builder.withLimit(50);
List<Map<String, Object>> result = run(transaction, builder.cypher(),
builder.parameters());
- for (Map<String, Object> record : result) {
+ return idsOfReadableExecutions(result, selector);
+ }
+
+ /**
+ * Ids of the executions in one list result that can be read. A node that
cannot be read is
+ * skipped, so the other executions still show (issue #9039).
+ */
+ List<String> idsOfReadableExecutions(
+ List<Map<String, Object>> records, IExecutionSelector selector) {
+ List<String> ids = new ArrayList<>();
+ for (Map<String, Object> record : records) {
String executionId = getString(record, EP_ID);
- Execution execution = buildExecution(executionId, record);
- ExecutionState state = buildExecutionState(executionId, record, "");
- if (selector.isSelected(execution) && selector.isSelected(state)) {
- ids.add(executionId);
+ try {
+ Execution execution = buildExecution(executionId, record);
+ ExecutionState state = buildExecutionState(executionId, record, "");
+ if (selector.isSelected(execution) && selector.isSelected(state)) {
+ ids.add(executionId);
+ }
+ } catch (Exception e) {
+ logReadError(executionId, e);
}
}
-
return ids;
}
+ /**
+ * One execution that cannot be read is skipped. A failure reading the
location itself still fails
+ * the list (issue #9039).
+ */
+ @Override
+ public List<ListedExecution> listExecutions(IExecutionSelector selector)
throws HopException {
+ IExecutionSelector active = selector == null ? IExecutionSelector.ALL :
selector;
+ List<String> ids = findExecutionIDs(active);
+ List<ListedExecution> listed = new ArrayList<>();
+ Exception firstFailure = null;
+ int failures = 0;
+ for (String id : ids) {
+ try {
+ Execution execution = getExecution(id);
+ if (execution == null || !active.isSelected(execution)) {
+ continue;
+ }
+ ExecutionState state = getExecutionState(id);
+ if (!active.isSelected(state)) {
+ continue;
+ }
+ listed.add(new ListedExecution(execution, state));
+ } catch (Exception e) {
+ failures++;
+ if (firstFailure == null) {
+ firstFailure = e;
+ }
+ logReadError(id, e);
+ }
+ }
+ // Every id failed, for example because the connection dropped. Keep the
location error.
+ //
+ if (listed.isEmpty() && failures > 0) {
+ if (firstFailure instanceof HopException hopException) {
+ throw hopException;
+ }
+ throw new HopException(CONST_ERROR_GETTING_EXECUTION_FROM_NEO_4_J,
firstFailure);
+ }
+ return listed;
+ }
+
+ private void logReadError(String executionId, Exception e) {
+ if (log != null) {
+ log.logError("Error reading execution " + executionId + " from Neo4j
(non-fatal)", e);
+ }
+ }
+
@Override
public void updateExecutionState(ExecutionState executionState) throws
HopException {
synchronized (this) {
@@ -1310,8 +1372,7 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
executions.add(execution);
}
} catch (Exception e) {
- log.logError(
- "Error loading execution with id : " + executionId + " from Neo4j
(non-fatal)", e);
+ logReadError(executionId, e);
}
}
return executions;
@@ -1335,9 +1396,13 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
// Get all
List<String> ids = getNeo4jExecutionIds(transaction, true, 0);
for (String id : ids) {
- Execution execution = getNeo4jExecution(transaction, id);
- if (execution != null && matcher.matches(execution)) {
- executions.add(execution);
+ try {
+ Execution execution = getNeo4jExecution(transaction, id);
+ if (execution != null && matcher.matches(execution)) {
+ executions.add(execution);
+ }
+ } catch (Exception e) {
+ logReadError(id, e);
}
}
return executions;
@@ -1831,7 +1896,7 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
case IValueMeta.TYPE_STRING -> value.toString();
case IValueMeta.TYPE_INTEGER ->
value instanceof Number number ? number.longValue() :
Long.valueOf(value.toString());
- case IValueMeta.TYPE_DATE -> toDate(value);
+ case IValueMeta.TYPE_DATE -> dateValue(value);
case IValueMeta.TYPE_BOOLEAN ->
value instanceof Boolean bool ? bool :
Boolean.valueOf(value.toString());
case IValueMeta.TYPE_NUMBER ->
@@ -2043,12 +2108,19 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
}
private Date getDate(Map<String, Object> record, String key) {
- return toDate(record.get("n." + key));
+ Object value = record.get("n." + key);
+ Date date = toDate(value);
+ if (date == null && value != null && !value.toString().isBlank() && log !=
null) {
+ log.logError("Unable to parse execution " + key + " '" + value + "'
(left empty)");
+ }
+ return date;
}
/**
- * A date and time as the graph databases return them: a LocalDateTime from
Neo4j and Memgraph, an
- * ISO string from FalkorDB and Apache AGE.
+ * A date and time as the graph databases return them: a {@link
LocalDateTime} from Neo4j and
+ * Memgraph, an ISO-8601 string from FalkorDB and Apache AGE, or the {@code
yyyy/MM/dd'T'HH:mm:ss}
+ * string Neo4j logging stores. A value that is not a date is null, so one
bad property does not
+ * fail the read of every execution (issue #9039).
*/
public static Date toDate(Object value) {
if (value == null) {
@@ -2058,7 +2130,7 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
return date;
}
if (value instanceof LocalDateTime localDateTime) {
- return
Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
+ return LoggingCore.toDate(localDateTime);
}
if (value instanceof ZonedDateTime zonedDateTime) {
return Date.from(zonedDateTime.toInstant());
@@ -2067,13 +2139,21 @@ public class NeoExecutionInfoLocation implements
IExecutionInfoLocation {
return Date.from(offsetDateTime.toInstant());
}
if (value instanceof LocalDate localDate) {
- return
Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
+ return LoggingCore.toDate(localDate.atStartOfDay());
}
- String string = value.toString();
- if (string.isEmpty()) {
- return null;
+ return LoggingCore.parseDate(value.toString());
+ }
+
+ /**
+ * A sampled date. An unparsable value is reported on that cell, unlike
{@link #toDate}, which
+ * leaves an execution date empty so the execution can still be listed.
+ */
+ private static Date dateValue(Object value) {
+ Date date = toDate(value);
+ if (date == null && value != null && !value.toString().isBlank()) {
+ throw new HopRuntimeException("Unable to parse date value '" + value +
"'");
}
- return
Date.from(LocalDateTime.parse(string).atZone(ZoneId.systemDefault()).toInstant());
+ return date;
}
private String getString(Map<String, Object> record, String key) {
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 08381ae686..53ff8c5731 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
@@ -24,7 +24,9 @@ import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoField;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
@@ -55,6 +57,27 @@ public class LoggingCore {
/** The format Neo4j logging stores the registration date of an execution
in. */
public static final String REGISTRATION_DATE_FORMAT =
"yyyy/MM/dd'T'HH:mm:ss";
+ /** Logging dates, with an optional fraction of a second. */
+ private static final DateTimeFormatter REGISTRATION_DATE_TIME =
+ new DateTimeFormatterBuilder()
+ .appendPattern(REGISTRATION_DATE_FORMAT)
+ .optionalStart()
+ .appendFraction(ChronoField.NANO_OF_SECOND, 1, 9, true)
+ .optionalEnd()
+ .toFormatter();
+
+ /** Timestamp strings written for a graph property, with an optional
fraction of a second. */
+ private static final DateTimeFormatter LOCAL_DATE_TIME_SPACE =
+ new DateTimeFormatterBuilder()
+ .appendPattern("yyyy-MM-dd HH:mm:ss")
+ .optionalStart()
+ .appendFraction(ChronoField.NANO_OF_SECOND, 1, 9, true)
+ .optionalEnd()
+ .toFormatter();
+
+ private static final List<DateTimeFormatter> LOCAL_DATE_TIMES =
+ List.of(REGISTRATION_DATE_TIME, DateTimeFormatter.ISO_LOCAL_DATE_TIME,
LOCAL_DATE_TIME_SPACE);
+
/**
* The name of the connection to log to: {@link
Defaults#HOP_GRAPH_LOGGING_CONNECTION} or else
* {@link Defaults#NEO4J_LOGGING_CONNECTION}. Null if logging is disabled.
@@ -284,21 +307,45 @@ public class LoggingCore {
return null;
}
- private static Date parseDate(String string) {
- for (DateTimeFormatter formatter :
- List.of(
- DateTimeFormatter.ofPattern(REGISTRATION_DATE_FORMAT),
- DateTimeFormatter.ISO_LOCAL_DATE_TIME)) {
+ /**
+ * A date stored as text on an execution. Neo4j logging writes {@link
#REGISTRATION_DATE_FORMAT}.
+ * FalkorDB and Apache AGE write an ISO-8601 local date time, and a
timestamp property can be a
+ * local date time separated with a space. An offset or a zoned value is
read as that instant.
+ * Anything else is null, so one bad value does not fail the read (issue
#9039).
+ */
+ public static Date parseDate(String string) {
+ if (string == null) {
+ return null;
+ }
+ String trimmed = string.trim();
+ if (trimmed.isEmpty()) {
+ return null;
+ }
+ for (DateTimeFormatter formatter : LOCAL_DATE_TIMES) {
try {
- return toDate(LocalDateTime.parse(string, formatter));
+ return toDate(LocalDateTime.parse(trimmed, formatter));
} catch (DateTimeParseException e) {
// Try the next format
}
}
- return null;
+ try {
+ return Date.from(OffsetDateTime.parse(trimmed).toInstant());
+ } catch (DateTimeParseException e) {
+ // Try the next format
+ }
+ try {
+ return Date.from(ZonedDateTime.parse(trimmed).toInstant());
+ } catch (DateTimeParseException e) {
+ // Try the next format
+ }
+ try {
+ return toDate(LocalDate.parse(trimmed).atStartOfDay());
+ } catch (DateTimeParseException e) {
+ return null;
+ }
}
- private static Date toDate(LocalDateTime localDateTime) {
+ public static Date toDate(LocalDateTime localDateTime) {
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
}
diff --git
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/execution/NeoExecutionInfoLocationReadTest.java
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/execution/NeoExecutionInfoLocationReadTest.java
new file mode 100644
index 0000000000..dda775eb6b
--- /dev/null
+++
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/execution/NeoExecutionInfoLocationReadTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hop.core.exception.HopRuntimeException;
+import org.apache.hop.core.row.value.ValueMetaDate;
+import org.apache.hop.execution.IExecutionSelector;
+import org.junit.jupiter.api.Test;
+
+/** Reading execution dates must survive the text Neo4j logging stores, see
issue #9039. */
+class NeoExecutionInfoLocationReadTest {
+
+ private static Date at(int year, int month, int day, int hour, int minute,
int second) {
+ return Date.from(
+ LocalDateTime.of(year, month, day, hour, minute, second)
+ .atZone(ZoneId.systemDefault())
+ .toInstant());
+ }
+
+ @Test
+ void loggingRegistrationDateIsRead() {
+ Date registered = at(2026, 10, 7, 16, 8, 52);
+
+ assertEquals(registered,
NeoExecutionInfoLocation.toDate("2026/10/07T16:08:52"));
+ assertEquals(registered,
NeoExecutionInfoLocation.toDate("2026-10-07T16:08:52"));
+ assertEquals(registered, NeoExecutionInfoLocation.toDate("2026-10-07
16:08:52"));
+ assertEquals(
+ registered, NeoExecutionInfoLocation.toDate(LocalDateTime.of(2026, 10,
7, 16, 8, 52)));
+ assertEquals(
+ Date.from(OffsetDateTime.parse("2026-10-07T14:08:52Z").toInstant()),
+ NeoExecutionInfoLocation.toDate("2026-10-07T14:08:52Z"));
+ }
+
+ @Test
+ void unparsableDateIsEmpty() {
+ assertNull(NeoExecutionInfoLocation.toDate(null));
+ assertNull(NeoExecutionInfoLocation.toDate(""));
+ assertNull(NeoExecutionInfoLocation.toDate(" "));
+ assertNull(NeoExecutionInfoLocation.toDate("not a date"));
+ assertNull(NeoExecutionInfoLocation.toDate(42));
+ }
+
+ @Test
+ void sampledDateReportsABadValueOnThatCell() {
+ ValueMetaDate when = new ValueMetaDate("when");
+
+ assertEquals(
+ at(2026, 10, 7, 16, 8, 52),
+ NeoExecutionInfoLocation.extractHopValue(when, "2026/10/07T16:08:52"));
+ assertNull(NeoExecutionInfoLocation.extractHopValue(when, ""));
+ assertThrows(
+ HopRuntimeException.class,
+ () -> NeoExecutionInfoLocation.extractHopValue(when, "not a date"));
+ }
+
+ @Test
+ void oneUnreadableExecutionDoesNotHideTheOthers() {
+ Map<String, Object> badDetails = execution("bad-details", "Workflow",
null);
+ badDetails.put("n.details", "{");
+
+ List<String> ids =
+ new NeoExecutionInfoLocation()
+ .idsOfReadableExecutions(
+ List.of(
+ execution("good", "Pipeline", LocalDateTime.of(2026, 10,
7, 16, 8, 52)),
+ execution("bad-type", "PIPELINE", "2026/10/07T16:08:52"),
+ execution("logging", "Pipeline", "2026/10/07T16:08:52"),
+ badDetails,
+ execution("later", "Workflow", "not a date")),
+ IExecutionSelector.ALL);
+
+ assertEquals(List.of("good", "logging", "later"), ids);
+ }
+
+ /** A row as {@link NeoExecutionInfoLocation} reads it back from a Cypher
result. */
+ private static Map<String, Object> execution(String id, String type, Object
registrationDate) {
+ Map<String, Object> record = new HashMap<>();
+ record.put("n.id", id);
+ record.put("n.executionType", type);
+ record.put("n.name", id);
+ if (registrationDate != null) {
+ record.put("n.registrationDate", registrationDate);
+ }
+ return record;
+ }
+}
diff --git
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/logging/util/LoggingCoreTest.java
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/logging/util/LoggingCoreTest.java
index 0138b16ee2..c46267f62e 100644
---
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/logging/util/LoggingCoreTest.java
+++
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/logging/util/LoggingCoreTest.java
@@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
@@ -80,7 +81,28 @@ class LoggingCoreTest {
}
@Test
- void unparsableStringIsNull() {
+ void loggingDateReportedByTheExecutionInformationLocation() {
+ // Issue #9039: Neo4j logging stores this text, and the execution list has
to read it.
+ assertEquals(
+ toDate(LocalDateTime.of(2026, 10, 7, 16, 8, 52)),
+ LoggingCore.parseDate("2026/10/07T16:08:52"));
+ }
+
+ @Test
+ void timestampTextAndOffset() {
+ assertEquals(
+ toDate(LocalDateTime.of(2026, 9, 30, 14, 15, 16)),
+ LoggingCore.parseDate("2026-09-30 14:15:16.000000000"));
+ assertEquals(
+ Date.from(OffsetDateTime.parse("2026-09-30T12:15:16Z").toInstant()),
+ LoggingCore.parseDate("2026-09-30T12:15:16Z"));
+ }
+
+ @Test
+ void blankOrUnparsableStringIsNull() {
+ assertNull(LoggingCore.parseDate(null));
+ assertNull(LoggingCore.parseDate(""));
+ assertNull(LoggingCore.parseDate(" "));
assertNull(LoggingCore.getDateValue(execution("not a date"),
"registrationDate"));
}