This is an automated email from the ASF dual-hosted git repository.
mattcasters 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 bf59265820 issue #7427 : Dimension Lookup/Update regression and bug
fix (#7429)
bf59265820 is described below
commit bf592658200ccc7e897d8f91894d88d3f702991a
Author: Matt Casters <[email protected]>
AuthorDate: Sat Jul 4 23:57:18 2026 +0200
issue #7427 : Dimension Lookup/Update regression and bug fix (#7429)
---
.../0012-6-dimension-lookup-no-version-field.hwf | 103 ++++++++++
.../database/0012-dimension-lookup-no-version.hpl | 168 +++++++++++++++++
.../0012-dimension-lookup-zero-length-validity.hpl | 138 ++++++++++++++
.../0012-dimension-lookup-zero-length-validity.hwf | 152 +++++++++++++++
.../golden-dimension-lookup-no-version.csv | 3 +
.../database/main-0012-dimension-lookup-update.hwf | 66 +++++++
.../golden-dimension-lookup-no-version.json | 32 ++++
.../0012-dimension-lookup-no-version UNIT.json | 38 ++++
.../scripts/script-dimension-lookup-no-version.sql | 38 ++++
.../script-dimension-zero-length-validity.sql | 38 ++++
.../transforms/dimensionlookup/DimensionCache.java | 208 +++++++++++++++++++++
.../dimensionlookup/DimensionLookup.java | 36 +++-
.../dimensionlookup/DimensionLookupDialog.java | 33 +++-
.../dimensionlookup/DimensionLookupMeta.java | 20 +-
.../messages/messages_en_US.properties | 10 +-
.../dimensionlookup/DimensionCacheTest.java | 158 ++++++++++++++++
16 files changed, 1228 insertions(+), 13 deletions(-)
diff --git
a/integration-tests/database/0012-6-dimension-lookup-no-version-field.hwf
b/integration-tests/database/0012-6-dimension-lookup-no-version-field.hwf
new file mode 100644
index 0000000000..330d5932e8
--- /dev/null
+++ b/integration-tests/database/0012-6-dimension-lookup-no-version-field.hwf
@@ -0,0 +1,103 @@
+<?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>0012-6-dimension-lookup-no-version-field</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2026/07/04 12:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/07/04 12:00:00.000</modified_date>
+ <parameters>
+ </parameters>
+ <actions>
+ <action>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <DayOfMonth>1</DayOfMonth>
+ <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
+ <hour>12</hour>
+ <intervalMinutes>60</intervalMinutes>
+ <intervalSeconds>0</intervalSeconds>
+ <minutes>0</minutes>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <weekDay>1</weekDay>
+ <parallel>N</parallel>
+ <xloc>64</xloc>
+ <yloc>48</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>SQL</name>
+ <description/>
+ <type>SQL</type>
+ <attributes/>
+ <sql/>
+ <useVariableSubstitution>F</useVariableSubstitution>
+ <sqlfromfile>T</sqlfromfile>
+
<sqlfilename>${PROJECT_HOME}/scripts/script-dimension-lookup-no-version.sql</sqlfilename>
+ <sendOneStatement>F</sendOneStatement>
+ <connection>unit-test-db</connection>
+ <parallel>N</parallel>
+ <xloc>256</xloc>
+ <yloc>48</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Run Pipeline Unit Tests</name>
+ <description/>
+ <type>RunPipelineTests</type>
+ <attributes/>
+ <test_names>
+ <test_name>
+ <name>0012-dimension-lookup-no-version UNIT</name>
+ </test_name>
+ </test_names>
+ <parallel>N</parallel>
+ <xloc>480</xloc>
+ <yloc>48</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>SQL</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ <hop>
+ <from>SQL</from>
+ <to>Run Pipeline Unit Tests</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
\ No newline at end of file
diff --git a/integration-tests/database/0012-dimension-lookup-no-version.hpl
b/integration-tests/database/0012-dimension-lookup-no-version.hpl
new file mode 100644
index 0000000000..225a6b97fc
--- /dev/null
+++ b/integration-tests/database/0012-dimension-lookup-no-version.hpl
@@ -0,0 +1,168 @@
+<?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>0012-dimension-lookup-no-version</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <pipeline_status>0</pipeline_status>
+ <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/07/04 12:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/07/04 12:00:00.000</modified_date>
+ <key_for_session_key>H4sIAAAAAAAAAAMAAAAAAAAAAAA=</key_for_session_key>
+ <is_key_private>N</is_key_private>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>Data grid</from>
+ <to>Dimension lookup/update</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Dimension lookup/update</from>
+ <to>Verify</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>Data grid</name>
+ <type>DataGrid</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <currency/>
+ <decimal/>
+ <set_empty_string>N</set_empty_string>
+ <format/>
+ <group/>
+ <length>-1</length>
+ <name>key</name>
+ <precision>-1</precision>
+ <type>String</type>
+ </field>
+ </fields>
+ <data>
+ <line>
+ <item>1</item>
+ </line>
+ <line>
+ <item>2</item>
+ </line>
+ </data>
+ <attributes/>
+ <GUI>
+ <xloc>144</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Dimension lookup/update</name>
+ <type>DimensionLookup</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <schema>public</schema>
+ <table>dimension</table>
+ <connection>unit-test-db</connection>
+ <commit>100</commit>
+ <update>N</update>
+ <fields>
+ <key>
+ <name>key</name>
+ <lookup>key</lookup>
+ </key>
+ <date>
+ <name/>
+ <from>date_from</from>
+ <to>date_to</to>
+ </date>
+ <field>
+ <name>value</name>
+ <lookup>value</lookup>
+ <type>String</type>
+ </field>
+ <return>
+ <name>dimension_id</name>
+ <rename/>
+ <creation_method>tablemax</creation_method>
+ <use_autoinc>N</use_autoinc>
+ <version/>
+ </return>
+ </fields>
+ <sequence/>
+ <min_year>1900</min_year>
+ <max_year>2199</max_year>
+ <cache_size>0</cache_size>
+ <preload_cache>Y</preload_cache>
+ <ignore_zero_length_validity>N</ignore_zero_length_validity>
+ <use_start_date_alternative>N</use_start_date_alternative>
+ <start_date_alternative>none</start_date_alternative>
+ <start_date_field_name/>
+ <useBatch>N</useBatch>
+ <attributes/>
+ <GUI>
+ <xloc>364</xloc>
+ <yloc>94</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Verify</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>576</xloc>
+ <yloc>94</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
\ No newline at end of file
diff --git
a/integration-tests/database/0012-dimension-lookup-zero-length-validity.hpl
b/integration-tests/database/0012-dimension-lookup-zero-length-validity.hpl
new file mode 100644
index 0000000000..9f624d81d0
--- /dev/null
+++ b/integration-tests/database/0012-dimension-lookup-zero-length-validity.hpl
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+ <info>
+ <name>0012-dimension-lookup-zero-length-validity</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <pipeline_status>0</pipeline_status>
+ <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/07/04 12:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/07/04 12:00:00.000</modified_date>
+ <key_for_session_key>H4sIAAAAAAAAAAMAAAAAAAAAAAA=</key_for_session_key>
+ <is_key_private>N</is_key_private>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>Data grid</from>
+ <to>Dimension lookup/update</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>Data grid</name>
+ <type>DataGrid</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <currency/>
+ <decimal/>
+ <set_empty_string>N</set_empty_string>
+ <format/>
+ <group/>
+ <length>-1</length>
+ <name>key</name>
+ <precision>-1</precision>
+ <type>String</type>
+ </field>
+ </fields>
+ <data>
+ <line>
+ <item>K1</item>
+ </line>
+ </data>
+ <attributes/>
+ <GUI>
+ <xloc>144</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Dimension lookup/update</name>
+ <type>DimensionLookup</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <schema>public</schema>
+ <table>dimension</table>
+ <connection>unit-test-db</connection>
+ <commit>100</commit>
+ <update>N</update>
+ <fields>
+ <key>
+ <name>key</name>
+ <lookup>key</lookup>
+ </key>
+ <date>
+ <name/>
+ <from>date_from</from>
+ <to>date_to</to>
+ </date>
+ <return>
+ <name>dimension_id</name>
+ <rename/>
+ <creation_method>tablemax</creation_method>
+ <use_autoinc>N</use_autoinc>
+ <version/>
+ </return>
+ </fields>
+ <sequence/>
+ <min_year>1900</min_year>
+ <max_year>2199</max_year>
+ <cache_size>0</cache_size>
+ <preload_cache>Y</preload_cache>
+ <ignore_zero_length_validity>N</ignore_zero_length_validity>
+ <use_start_date_alternative>N</use_start_date_alternative>
+ <start_date_alternative>none</start_date_alternative>
+ <start_date_field_name/>
+ <useBatch>N</useBatch>
+ <attributes/>
+ <GUI>
+ <xloc>364</xloc>
+ <yloc>94</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
\ No newline at end of file
diff --git
a/integration-tests/database/0012-dimension-lookup-zero-length-validity.hwf
b/integration-tests/database/0012-dimension-lookup-zero-length-validity.hwf
new file mode 100644
index 0000000000..e59fe4d6b6
--- /dev/null
+++ b/integration-tests/database/0012-dimension-lookup-zero-length-validity.hwf
@@ -0,0 +1,152 @@
+<?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>0012-dimension-lookup-zero-length-validity</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2026/07/04 12:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/07/04 12:00:00.000</modified_date>
+ <parameters>
+ </parameters>
+ <actions>
+ <action>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <DayOfMonth>1</DayOfMonth>
+ <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
+ <hour>12</hour>
+ <intervalMinutes>60</intervalMinutes>
+ <intervalSeconds>0</intervalSeconds>
+ <minutes>0</minutes>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <weekDay>1</weekDay>
+ <parallel>N</parallel>
+ <xloc>64</xloc>
+ <yloc>48</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>SQL</name>
+ <description/>
+ <type>SQL</type>
+ <attributes/>
+ <sql/>
+ <useVariableSubstitution>F</useVariableSubstitution>
+ <sqlfromfile>T</sqlfromfile>
+
<sqlfilename>${PROJECT_HOME}/scripts/script-dimension-zero-length-validity.sql</sqlfilename>
+ <sendOneStatement>F</sendOneStatement>
+ <connection>unit-test-db</connection>
+ <parallel>N</parallel>
+ <xloc>208</xloc>
+ <yloc>48</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>0012-dimension-lookup-zero-length-validity.hpl</name>
+ <description/>
+ <type>PIPELINE</type>
+ <attributes/>
+
<filename>${PROJECT_HOME}/0012-dimension-lookup-zero-length-validity.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>
+ <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>448</xloc>
+ <yloc>48</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Success</name>
+ <description/>
+ <type>SUCCESS</type>
+ <attributes/>
+ <parallel>N</parallel>
+ <xloc>688</xloc>
+ <yloc>48</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Abort workflow</name>
+ <description/>
+ <type>ABORT</type>
+ <attributes/>
+ <always_log_rows>N</always_log_rows>
+ <message>Failure: preload cache should have failed on zero-length
validity data</message>
+ <parallel>N</parallel>
+ <xloc>448</xloc>
+ <yloc>160</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>SQL</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ <hop>
+ <from>SQL</from>
+ <to>0012-dimension-lookup-zero-length-validity.hpl</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>0012-dimension-lookup-zero-length-validity.hpl</from>
+ <to>Success</to>
+ <enabled>Y</enabled>
+ <evaluation>N</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>0012-dimension-lookup-zero-length-validity.hpl</from>
+ <to>Abort workflow</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
\ No newline at end of file
diff --git
a/integration-tests/database/datasets/golden-dimension-lookup-no-version.csv
b/integration-tests/database/datasets/golden-dimension-lookup-no-version.csv
new file mode 100644
index 0000000000..d9d1a32a52
--- /dev/null
+++ b/integration-tests/database/datasets/golden-dimension-lookup-no-version.csv
@@ -0,0 +1,3 @@
+key,dimension_id,value
+1,1,alpha
+2,2,beta
\ No newline at end of file
diff --git a/integration-tests/database/main-0012-dimension-lookup-update.hwf
b/integration-tests/database/main-0012-dimension-lookup-update.hwf
index bb669cabc7..6c7f990903 100644
--- a/integration-tests/database/main-0012-dimension-lookup-update.hwf
+++ b/integration-tests/database/main-0012-dimension-lookup-update.hwf
@@ -173,6 +173,58 @@ limitations under the License.
<yloc>368</yloc>
<attributes_hac/>
</action>
+ <action>
+ <name>0012-6-dimension-lookup-no-version-field.hwf</name>
+ <description/>
+ <type>WORKFLOW</type>
+ <attributes/>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <create_parent_folder>N</create_parent_folder>
+ <exec_per_row>N</exec_per_row>
+
<filename>${PROJECT_HOME}/0012-6-dimension-lookup-no-version-field.hwf</filename>
+ <logext/>
+ <logfile/>
+ <loglevel>Nothing</loglevel>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <params_from_previous>N</params_from_previous>
+ <run_configuration>local</run_configuration>
+ <set_append_logfile>N</set_append_logfile>
+ <set_logfile>N</set_logfile>
+ <wait_until_finished>Y</wait_until_finished>
+ <parallel>N</parallel>
+ <xloc>320</xloc>
+ <yloc>448</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>0012-dimension-lookup-zero-length-validity.hwf</name>
+ <description/>
+ <type>WORKFLOW</type>
+ <attributes/>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <create_parent_folder>N</create_parent_folder>
+ <exec_per_row>N</exec_per_row>
+
<filename>${PROJECT_HOME}/0012-dimension-lookup-zero-length-validity.hwf</filename>
+ <logext/>
+ <logfile/>
+ <loglevel>Nothing</loglevel>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <params_from_previous>N</params_from_previous>
+ <run_configuration>local</run_configuration>
+ <set_append_logfile>N</set_append_logfile>
+ <set_logfile>N</set_logfile>
+ <wait_until_finished>Y</wait_until_finished>
+ <parallel>N</parallel>
+ <xloc>320</xloc>
+ <yloc>528</yloc>
+ <attributes_hac/>
+ </action>
</actions>
<hops>
<hop>
@@ -210,6 +262,20 @@ limitations under the License.
<evaluation>Y</evaluation>
<unconditional>N</unconditional>
</hop>
+ <hop>
+ <from>0012-5-dimension-lookup-tk-uuid.hwf</from>
+ <to>0012-6-dimension-lookup-no-version-field.hwf</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>0012-6-dimension-lookup-no-version-field.hwf</from>
+ <to>0012-dimension-lookup-zero-length-validity.hwf</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
</hops>
<notepads>
</notepads>
diff --git
a/integration-tests/database/metadata/dataset/golden-dimension-lookup-no-version.json
b/integration-tests/database/metadata/dataset/golden-dimension-lookup-no-version.json
new file mode 100644
index 0000000000..da990ec9c2
--- /dev/null
+++
b/integration-tests/database/metadata/dataset/golden-dimension-lookup-no-version.json
@@ -0,0 +1,32 @@
+{
+ "base_filename": "golden-dimension-lookup-no-version.csv",
+ "name": "golden-dimension-lookup-no-version",
+ "description": "",
+ "dataset_fields": [
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "key",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": 15,
+ "field_type": 5,
+ "field_precision": 0,
+ "field_name": "dimension_id",
+ "field_format": "####0;-####0"
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "value",
+ "field_format": ""
+ }
+ ],
+ "folder_name": "${HOP_DATASETS_FOLDER}"
+}
\ No newline at end of file
diff --git
a/integration-tests/database/metadata/unit-test/0012-dimension-lookup-no-version
UNIT.json
b/integration-tests/database/metadata/unit-test/0012-dimension-lookup-no-version
UNIT.json
new file mode 100644
index 0000000000..85d7d2ad43
--- /dev/null
+++
b/integration-tests/database/metadata/unit-test/0012-dimension-lookup-no-version
UNIT.json
@@ -0,0 +1,38 @@
+{
+ "variableValues": [],
+ "database_replacements": [],
+ "autoOpening": true,
+ "basePath": "",
+ "golden_data_sets": [
+ {
+ "field_mappings": [
+ {
+ "transform_field": "key",
+ "data_set_field": "key"
+ },
+ {
+ "transform_field": "dimension_id",
+ "data_set_field": "dimension_id"
+ },
+ {
+ "transform_field": "value",
+ "data_set_field": "value"
+ }
+ ],
+ "field_order": [
+ "key",
+ "dimension_id",
+ "value"
+ ],
+ "data_set_name": "golden-dimension-lookup-no-version",
+ "transform_name": "Verify"
+ }
+ ],
+ "input_data_sets": [],
+ "name": "0012-dimension-lookup-no-version UNIT",
+ "description": "",
+ "persist_filename": "",
+ "trans_test_tweaks": [],
+ "pipeline_filename": "./0012-dimension-lookup-no-version.hpl",
+ "test_type": "UNIT_TEST"
+}
\ No newline at end of file
diff --git
a/integration-tests/database/scripts/script-dimension-lookup-no-version.sql
b/integration-tests/database/scripts/script-dimension-lookup-no-version.sql
new file mode 100644
index 0000000000..afe60aa6a6
--- /dev/null
+++ b/integration-tests/database/scripts/script-dimension-lookup-no-version.sql
@@ -0,0 +1,38 @@
+/*
+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.
+*/
+
+DROP TABLE IF EXISTS public.dimension;
+
+CREATE TABLE public.dimension
+(
+ dimension_id BIGSERIAL,
+ version INTEGER,
+ date_from TIMESTAMP,
+ date_to TIMESTAMP,
+ "key" TEXT,
+ "value" TEXT
+)
+;
+CREATE INDEX idx_dimension_lookup ON "public".dimension ("key")
+;
+CREATE INDEX idx_dimension_tk ON "public".dimension (dimension_id)
+;
+
+INSERT INTO public.dimension (version, date_from, date_to, "key", "value")
+VALUES (1, '1900-01-01', '2199-01-01', '1', 'alpha'),
+ (1, '1900-01-01', '2199-01-01', '2', 'beta')
+;
\ No newline at end of file
diff --git
a/integration-tests/database/scripts/script-dimension-zero-length-validity.sql
b/integration-tests/database/scripts/script-dimension-zero-length-validity.sql
new file mode 100644
index 0000000000..50e74c57b9
--- /dev/null
+++
b/integration-tests/database/scripts/script-dimension-zero-length-validity.sql
@@ -0,0 +1,38 @@
+/*
+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.
+*/
+
+DROP TABLE IF EXISTS public.dimension;
+
+CREATE TABLE public.dimension
+(
+ dimension_id BIGSERIAL,
+ version INTEGER,
+ date_from TIMESTAMP,
+ date_to TIMESTAMP,
+ "key" TEXT,
+ "value" TEXT
+)
+;
+CREATE INDEX idx_dimension_lookup ON "public".dimension ("key")
+;
+CREATE INDEX idx_dimension_tk ON "public".dimension (dimension_id)
+;
+
+INSERT INTO public.dimension (version, date_from, date_to, "key", "value")
+VALUES (1, '2020-01-01', '2020-06-01', 'K1', 'wide-range'),
+ (1, '2020-03-01', '2020-03-01', 'K1', 'zero-length')
+;
\ No newline at end of file
diff --git
a/plugins/transforms/dimensionlookup/src/main/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionCache.java
b/plugins/transforms/dimensionlookup/src/main/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionCache.java
index 3c929e2d92..fd0b980ff3 100644
---
a/plugins/transforms/dimensionlookup/src/main/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionCache.java
+++
b/plugins/transforms/dimensionlookup/src/main/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionCache.java
@@ -17,14 +17,18 @@
package org.apache.hop.pipeline.transforms.dimensionlookup;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
+import org.apache.hop.core.Const;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.exception.HopRuntimeException;
+import org.apache.hop.core.exception.HopTransformException;
import org.apache.hop.core.row.IRowMeta;
import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.i18n.BaseMessages;
/**
* This class will act as a special purpose dimension Cache. The idea here is
to not only cache the
@@ -35,6 +39,9 @@ import org.apache.hop.core.row.IValueMeta;
* conversion errors as possible, we'll use the same row as we get from the
database.
*/
public class DimensionCache implements Comparator<Object[]> {
+ private static final Class<?> PKG = DimensionLookupMeta.class;
+ private static final int MAX_VALIDATION_ERRORS = 3;
+
private IRowMeta rowMeta;
private List<Object[]> rowCache;
private int[] keyIndexes;
@@ -56,6 +63,43 @@ public class DimensionCache implements Comparator<Object[]> {
this.toDateIndex = toDateIndex;
}
+ /**
+ * Returns true when both dates are non-null and equal, representing a
zero-length [from, to)
+ * validity interval.
+ */
+ static boolean isZeroLengthValidity(Date fromDate, Date toDate) {
+ return fromDate != null
+ && toDate != null
+ && !fromDate.before(toDate)
+ && !toDate.before(fromDate);
+ }
+
+ /**
+ * Returns a new list that excludes rows with zero-length validity intervals.
+ *
+ * @param rowMeta the row layout of the cache rows
+ * @param rows the rows loaded from the dimension table
+ * @param fromDateIndex the index of the date-from field
+ * @param toDateIndex the index of the date-to field
+ * @return the filtered rows
+ */
+ public static List<Object[]> excludeZeroLengthValidityRows(
+ IRowMeta rowMeta, List<Object[]> rows, int fromDateIndex, int
toDateIndex) {
+ List<Object[]> filteredRows = new ArrayList<>(rows.size());
+ for (Object[] row : rows) {
+ try {
+ Date fromDate =
rowMeta.getValueMeta(fromDateIndex).getDate(row[fromDateIndex]);
+ Date toDate =
rowMeta.getValueMeta(toDateIndex).getDate(row[toDateIndex]);
+ if (!isZeroLengthValidity(fromDate, toDate)) {
+ filteredRows.add(row);
+ }
+ } catch (Exception e) {
+ throw new HopRuntimeException(e);
+ }
+ }
+ return filteredRows;
+ }
+
/**
* Add a row to the back of the list
*
@@ -174,10 +218,174 @@ public class DimensionCache implements
Comparator<Object[]> {
}
}
+ /**
+ * Validate dimension rows before sorting the cache. Overlapping or invalid
date ranges for the
+ * same natural key cause the lookup comparator to violate its contract
during {@link
+ * #sortRows()}.
+ *
+ * @param schemaTable the quoted schema/table combination for error reporting
+ * @throws HopTransformException when the cache data cannot be sorted safely
+ */
+ public void validateRowsForSort(String schemaTable) throws
HopTransformException {
+ try {
+ if (rowCache == null || rowCache.isEmpty()) {
+ return;
+ }
+
+ List<String> errors = new ArrayList<>();
+
+ for (Object[] row : rowCache) {
+ if (errors.size() >= MAX_VALIDATION_ERRORS) {
+ break;
+ }
+ Date fromDate = getDateFromRow(row, fromDateIndex);
+ Date toDate = getDateFromRow(row, toDateIndex);
+ if (isZeroLengthValidity(fromDate, toDate)
+ || (fromDate != null && toDate != null && fromDate.after(toDate)))
{
+ errors.add(
+ BaseMessages.getString(
+ PKG,
+ "DimensionLookup.Exception.CacheInvalidDateRange",
+ formatNaturalKey(row),
+ formatDateRange(fromDate, toDate)));
+ }
+ }
+
+ List<Object[]> sorted = new ArrayList<>(rowCache);
+ sorted.sort(
+ (o1, o2) -> {
+ int cmp = compareNaturalKeys(o1, o2);
+ if (cmp != 0) {
+ return cmp;
+ }
+ return compareFromDates(o1, o2);
+ });
+
+ for (int i = 1; i < sorted.size() && errors.size() <
MAX_VALIDATION_ERRORS; i++) {
+ Object[] previousRow = sorted.get(i - 1);
+ Object[] currentRow = sorted.get(i);
+ if (compareNaturalKeys(previousRow, currentRow) != 0) {
+ continue;
+ }
+
+ Date previousFrom = getDateFromRow(previousRow, fromDateIndex);
+ Date previousTo = getDateFromRow(previousRow, toDateIndex);
+ Date currentFrom = getDateFromRow(currentRow, fromDateIndex);
+ Date currentTo = getDateFromRow(currentRow, toDateIndex);
+ if (rangesOverlap(previousFrom, previousTo, currentFrom, currentTo)) {
+ errors.add(
+ BaseMessages.getString(
+ PKG,
+ "DimensionLookup.Exception.CacheOverlappingDateRanges",
+ formatNaturalKey(previousRow),
+ formatDateRange(previousFrom, previousTo),
+ formatDateRange(currentFrom, currentTo)));
+ }
+ }
+
+ if (!errors.isEmpty()) {
+ StringBuilder message = new StringBuilder();
+ message.append(
+ BaseMessages.getString(
+ PKG, "DimensionLookup.Exception.CacheSortValidationFailed",
schemaTable));
+ for (String error : errors) {
+ message.append(Const.CR).append(error);
+ }
+ if (errors.size() >= MAX_VALIDATION_ERRORS) {
+ message.append(Const.CR).append("...");
+ }
+ throw new HopTransformException(message.toString());
+ }
+ } catch (HopTransformException e) {
+ throw e;
+ } catch (HopRuntimeException e) {
+ throw new HopTransformException(
+ BaseMessages.getString(
+ PKG, "DimensionLookup.Exception.CacheSortValidationFailed",
schemaTable),
+ e);
+ }
+ }
+
public void sortRows() {
Collections.sort(rowCache, this);
}
+ private int compareNaturalKeys(Object[] row1, Object[] row2) {
+ try {
+ return rowMeta.compare(row1, row2, keyIndexes);
+ } catch (Exception e) {
+ throw new HopRuntimeException(e);
+ }
+ }
+
+ private Date getDateFromRow(Object[] row, int dateIndex) {
+ try {
+ return rowMeta.getValueMeta(dateIndex).getDate(row[dateIndex]);
+ } catch (Exception e) {
+ throw new HopRuntimeException(e);
+ }
+ }
+
+ private int compareFromDates(Object[] row1, Object[] row2) {
+ Date from1 = getDateFromRow(row1, fromDateIndex);
+ Date from2 = getDateFromRow(row2, fromDateIndex);
+ if (from1 == null && from2 == null) {
+ return 0;
+ }
+ if (from1 == null) {
+ return -1;
+ }
+ if (from2 == null) {
+ return 1;
+ }
+ return from1.compareTo(from2);
+ }
+
+ /**
+ * Returns true when the half-open intervals [from1, to1) and [from2, to2)
overlap. A null from
+ * date means -infinity and a null to date means +infinity.
+ */
+ private boolean rangesOverlap(Date from1, Date to1, Date from2, Date to2) {
+ boolean from1BeforeTo2 = (to2 == null) || (from1 == null) ||
from1.before(to2);
+ boolean from2BeforeTo1 = (to1 == null) || (from2 == null) ||
from2.before(to1);
+ return from1BeforeTo2 && from2BeforeTo1;
+ }
+
+ private String formatNaturalKey(Object[] row) {
+ try {
+ if (keyIndexes == null || keyIndexes.length == 0) {
+ return "(no natural key)";
+ }
+
+ StringBuilder naturalKey = new StringBuilder();
+ for (int i = 0; i < keyIndexes.length; i++) {
+ if (i > 0) {
+ naturalKey.append(", ");
+ }
+ naturalKey.append(rowMeta.getString(row, keyIndexes[i]));
+ }
+ return naturalKey.toString();
+ } catch (Exception e) {
+ throw new HopRuntimeException(e);
+ }
+ }
+
+ private String formatDateRange(Date fromDate, Date toDate) {
+ try {
+ String fromDateText = "-infinity";
+ String toDateText = "+infinity";
+ if (fromDate != null) {
+ fromDateText = rowMeta.getValueMeta(fromDateIndex).getString(fromDate);
+ }
+ if (toDate != null) {
+ toDateText = rowMeta.getValueMeta(toDateIndex).getString(toDate);
+ }
+ return "[" + fromDateText + ", " + toDateText + ")";
+ } catch (Exception e) {
+ throw new HopRuntimeException(e);
+ }
+ }
+
/**
* Compare 2 rows of data using the natural keys and indexes specified.
*
diff --git
a/plugins/transforms/dimensionlookup/src/main/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionLookup.java
b/plugins/transforms/dimensionlookup/src/main/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionLookup.java
index 0284466f33..2f3b38e3b4 100644
---
a/plugins/transforms/dimensionlookup/src/main/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionLookup.java
+++
b/plugins/transforms/dimensionlookup/src/main/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionLookup.java
@@ -216,7 +216,8 @@ public class DimensionLookup extends
BaseTransform<DimensionLookupMeta, Dimensio
} catch (HopException e) {
logError(
BaseMessages.getString(
- PKG, "DimensionLookup.Log.TransformCanNotContinueForErrors",
e.getMessage()));
+ PKG, "DimensionLookup.Log.TransformCanNotContinueForErrors",
e.getMessage()),
+ e);
setErrors(1);
stopAll();
setOutputDone();
@@ -415,10 +416,27 @@ public class DimensionLookup extends
BaseTransform<DimensionLookupMeta, Dimensio
data.schemaTable));
}
+ if (meta.isIgnoreZeroLengthValidity()) {
+ int originalSize = rows.size();
+ rows =
+ DimensionCache.excludeZeroLengthValidityRows(
+ rowMeta, rows, data.preloadFromDateIndex,
data.preloadToDateIndex);
+ int skipped = originalSize - rows.size();
+ if (skipped > 0 && isDetailed()) {
+ logDetailed(
+ BaseMessages.getString(
+ PKG, "DimensionLookup.Log.SkippedZeroLengthValidityRows",
skipped));
+ }
+ if (rows.isEmpty()) {
+ logBasic(BaseMessages.getString(PKG,
"DimensionLookup.Log.EmptyPreloadCacheAfterFilter"));
+ }
+ }
+
data.preloadCache =
new DimensionCache(
rowMeta, data.preloadKeyIndexes, data.preloadFromDateIndex,
data.preloadToDateIndex);
data.preloadCache.setRowCache(rows);
+ data.preloadCache.validateRowsForSort(data.schemaTable);
if (isDetailed()) {
logDetailed("Sorting the cache rows...");
@@ -446,6 +464,8 @@ public class DimensionLookup extends
BaseTransform<DimensionLookupMeta, Dimensio
}
// This is all for now...
+ } catch (HopTransformException e) {
+ throw e;
} catch (Exception e) {
throw new HopException("Error encountered during cache pre-load", e);
}
@@ -503,9 +523,13 @@ public class DimensionLookup extends
BaseTransform<DimensionLookupMeta, Dimensio
lookupRow[to] = toData;
}
- // Also set the lookup date on the "end of date range" (toDate) position
+ // Also set the lookup date for the date-range comparison in the cache
//
- lookupRow[data.preloadFromDateIndex] = valueDate;
+ IValueMeta preloadFromDateMeta =
+
data.preloadCache.getRowMeta().getValueMeta(data.preloadFromDateIndex);
+ IValueMeta valueDateMeta = new ValueMetaDate("lookupDate");
+ lookupRow[data.preloadFromDateIndex] =
+ preloadFromDateMeta.convertData(valueDateMeta, valueDate);
// Look up the row in the pre-load cache...
//
@@ -1981,8 +2005,10 @@ public class DimensionLookup extends
BaseTransform<DimensionLookupMeta, Dimensio
data.schemaTable));
}
} else {
- throw new HopTransformException(
- BaseMessages.getString(PKG,
"DimensionLookupMeta.CheckResult.VersionKeyRequired"));
+ if (meta.isUpdate()) {
+ throw new HopTransformException(
+ BaseMessages.getString(PKG,
"DimensionLookupMeta.CheckResult.VersionKeyRequired"));
+ }
}
String fromField = f.getDate().getFrom();
diff --git
a/plugins/transforms/dimensionlookup/src/main/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionLookupDialog.java
b/plugins/transforms/dimensionlookup/src/main/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionLookupDialog.java
index 5f9da8e43e..8292e6cfd3 100644
---
a/plugins/transforms/dimensionlookup/src/main/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionLookupDialog.java
+++
b/plugins/transforms/dimensionlookup/src/main/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionLookupDialog.java
@@ -104,6 +104,9 @@ public class DimensionLookupDialog extends
BaseTransformDialog {
private Label wlPreloadCache;
private Button wPreloadCache;
+ private Label wlIgnoreZeroLengthValidity;
+ private Button wIgnoreZeroLengthValidity;
+
private Label wlCacheSize;
private Text wCacheSize;
@@ -399,6 +402,26 @@ public class DimensionLookupDialog extends
BaseTransformDialog {
fdPreloadCache.right = new FormAttachment(100, 0);
wPreloadCache.setLayoutData(fdPreloadCache);
+ wlIgnoreZeroLengthValidity = new Label(wPhysicalComp, SWT.RIGHT);
+ wlIgnoreZeroLengthValidity.setText(
+ BaseMessages.getString(PKG,
"DimensionLookupDialog.IgnoreZeroLengthValidity.Label"));
+ PropsUi.setLook(wlIgnoreZeroLengthValidity);
+ FormData fdlIgnoreZeroLengthValidity = new FormData();
+ fdlIgnoreZeroLengthValidity.left = new FormAttachment(0, 0);
+ fdlIgnoreZeroLengthValidity.right = new FormAttachment(middle, -margin);
+ fdlIgnoreZeroLengthValidity.top = new FormAttachment(wPreloadCache,
margin);
+ wlIgnoreZeroLengthValidity.setLayoutData(fdlIgnoreZeroLengthValidity);
+ wIgnoreZeroLengthValidity = new Button(wPhysicalComp, SWT.CHECK);
+ PropsUi.setLook(wIgnoreZeroLengthValidity);
+ wIgnoreZeroLengthValidity.setToolTipText(
+ BaseMessages.getString(PKG,
"DimensionLookupDialog.IgnoreZeroLengthValidity.Tooltip"));
+ wIgnoreZeroLengthValidity.addListener(SWT.Selection, e -> setFlags());
+ FormData fdIgnoreZeroLengthValidity = new FormData();
+ fdIgnoreZeroLengthValidity.left = new FormAttachment(middle, 0);
+ fdIgnoreZeroLengthValidity.top = new
FormAttachment(wlIgnoreZeroLengthValidity, 0, SWT.CENTER);
+ fdIgnoreZeroLengthValidity.right = new FormAttachment(100, 0);
+ wIgnoreZeroLengthValidity.setLayoutData(fdIgnoreZeroLengthValidity);
+
// Cache size ...
wlCacheSize = new Label(wPhysicalComp, SWT.RIGHT);
wlCacheSize.setText(BaseMessages.getString(PKG,
"DimensionLookupDialog.CacheSize.Label"));
@@ -406,13 +429,13 @@ public class DimensionLookupDialog extends
BaseTransformDialog {
FormData fdlCacheSize = new FormData();
fdlCacheSize.left = new FormAttachment(0, 0);
fdlCacheSize.right = new FormAttachment(middle, -margin);
- fdlCacheSize.top = new FormAttachment(wPreloadCache, margin);
+ fdlCacheSize.top = new FormAttachment(wIgnoreZeroLengthValidity, margin);
wlCacheSize.setLayoutData(fdlCacheSize);
wCacheSize = new Text(wPhysicalComp, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
PropsUi.setLook(wCacheSize);
FormData fdCacheSize = new FormData();
fdCacheSize.left = new FormAttachment(middle, 0);
- fdCacheSize.top = new FormAttachment(wPreloadCache, margin);
+ fdCacheSize.top = new FormAttachment(wlCacheSize, 0, SWT.CENTER);
fdCacheSize.right = new FormAttachment(100, 0);
wCacheSize.setLayoutData(fdCacheSize);
@@ -1174,6 +1197,10 @@ public class DimensionLookupDialog extends
BaseTransformDialog {
wlPreloadCache.setEnabled(wUseCache.getSelection() &&
!wUpdate.getSelection());
wPreloadCache.setEnabled(wUseCache.getSelection() &&
!wUpdate.getSelection());
+ boolean preloadCacheEnabled = wUseCache.getSelection() &&
!wUpdate.getSelection();
+ wlIgnoreZeroLengthValidity.setEnabled(preloadCacheEnabled &&
wPreloadCache.getSelection());
+ wIgnoreZeroLengthValidity.setEnabled(preloadCacheEnabled &&
wPreloadCache.getSelection());
+
wlCacheSize.setEnabled(wUseCache.getSelection() &&
!wPreloadCache.getSelection());
wCacheSize.setEnabled(wUseCache.getSelection() &&
!wPreloadCache.getSelection());
@@ -1271,6 +1298,7 @@ public class DimensionLookupDialog extends
BaseTransformDialog {
wUseCache.setSelection(input.getCacheSize() >= 0);
wPreloadCache.setSelection(input.isPreloadingCache());
+ wIgnoreZeroLengthValidity.setSelection(input.isIgnoreZeroLengthValidity());
wCacheSize.setText("" + input.getCacheSize());
wMinYear.setText("" + input.getMinYear());
@@ -1394,6 +1422,7 @@ public class DimensionLookupDialog extends
BaseTransformDialog {
in.setCacheSize(-1);
}
in.setPreloadingCache(wPreloadCache.getSelection());
+ in.setIgnoreZeroLengthValidity(wIgnoreZeroLengthValidity.getSelection());
if (wPreloadCache.getSelection()) {
in.setCacheSize(0);
}
diff --git
a/plugins/transforms/dimensionlookup/src/main/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionLookupMeta.java
b/plugins/transforms/dimensionlookup/src/main/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionLookupMeta.java
index b394858ae3..e2432c9106 100644
---
a/plugins/transforms/dimensionlookup/src/main/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionLookupMeta.java
+++
b/plugins/transforms/dimensionlookup/src/main/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionLookupMeta.java
@@ -182,6 +182,12 @@ public class DimensionLookupMeta extends
BaseTransformMeta<DimensionLookup, Dime
injectionKeyDescription = "DimensionLookup.Injection.PRELOAD_CACHE")
private boolean preloadingCache;
+ @HopMetadataProperty(
+ key = "ignore_zero_length_validity",
+ injectionKey = "IGNORE_ZERO_LENGTH_VALIDITY",
+ injectionKeyDescription =
"DimensionLookup.Injection.IGNORE_ZERO_LENGTH_VALIDITY")
+ private boolean ignoreZeroLengthValidity;
+
@HopMetadataProperty(
key = "unknown_row_check_disabled",
injectionKey = "UNKNOWN_ROW_CHECK_DISABLED",
@@ -223,6 +229,7 @@ public class DimensionLookupMeta extends
BaseTransformMeta<DimensionLookup, Dime
this.startDateAlternative = m.startDateAlternative;
this.startDateFieldName = m.startDateFieldName;
this.preloadingCache = m.preloadingCache;
+ this.ignoreZeroLengthValidity = m.ignoreZeroLengthValidity;
this.unknownRowCheckDisabled = m.unknownRowCheckDisabled;
}
@@ -249,6 +256,7 @@ public class DimensionLookupMeta extends
BaseTransformMeta<DimensionLookup, Dime
cacheSize = 5000;
preloadingCache = false;
+ ignoreZeroLengthValidity = false;
unknownRowCheckDisabled = false;
}
@@ -667,11 +675,13 @@ public class DimensionLookupMeta extends
BaseTransformMeta<DimensionLookup, Dime
transformMeta));
}
} else {
- remarks.add(
- new CheckResult(
- ICheckResult.TYPE_RESULT_ERROR,
- BaseMessages.getString(PKG,
"DimensionLookupMeta.CheckResult.VersionKeyRequired"),
- transformMeta));
+ if (isUpdate()) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_ERROR,
+ BaseMessages.getString(PKG,
"DimensionLookupMeta.CheckResult.VersionKeyRequired"),
+ transformMeta));
+ }
}
TechnicalKeyCreationMethod method = fields.returns.creationMethod;
diff --git
a/plugins/transforms/dimensionlookup/src/main/resources/org/apache/hop/pipeline/transforms/dimensionlookup/messages/messages_en_US.properties
b/plugins/transforms/dimensionlookup/src/main/resources/org/apache/hop/pipeline/transforms/dimensionlookup/messages/messages_en_US.properties
index 5a08374572..2939c00228 100644
---
a/plugins/transforms/dimensionlookup/src/main/resources/org/apache/hop/pipeline/transforms/dimensionlookup/messages/messages_en_US.properties
+++
b/plugins/transforms/dimensionlookup/src/main/resources/org/apache/hop/pipeline/transforms/dimensionlookup/messages/messages_en_US.properties
@@ -236,4 +236,12 @@ DimensionLookup.Exception.LookupKeyFieldNotFound=Lookup
key column [{0}] not fou
DimensionLookup.Exception.LookupOrUpdateFieldNotFound=Lookup/update column
[{0}] not found in table [{1}].
DimensionLookup.Exception.DateFieldNameNotFound=Date field [{0}] could not be
found in the input stream.
DimensionLookup.Exception.TkSourceFieldNotFound=Technical key source field
[{0}] could not be found in the input stream.
-DimensionLookup.Exception.UpdateSourceFieldNotFound=Update source field [{0}]
could not be found in the input stream.
\ No newline at end of file
+DimensionLookup.Exception.UpdateSourceFieldNotFound=Update source field [{0}]
could not be found in the input stream.
+DimensionLookup.Exception.CacheSortValidationFailed=Cannot sort pre-loaded
dimension cache for table [{0}]. The dimension data contains date range
problems:
+DimensionLookup.Exception.CacheInvalidDateRange=Natural key [{0}] has an
invalid date range {1} (date-from must be before date-to).
+DimensionLookup.Exception.CacheOverlappingDateRanges=Natural key [{0}] has
overlapping validity periods {1} and {2}.
+DimensionLookupDialog.IgnoreZeroLengthValidity.Label=Ignore zero-length
validity records
+DimensionLookupDialog.IgnoreZeroLengthValidity.Tooltip=Skip dimension rows
where date-from equals date-to when pre-loading the cache. Useful when upstream
flattening produces point-in-time records that cannot be looked up.
+DimensionLookup.Injection.IGNORE_ZERO_LENGTH_VALIDITY=Skip rows where
date-from equals date-to when pre-loading the cache.
+DimensionLookup.Log.SkippedZeroLengthValidityRows=Skipped {0} zero-length
validity row(s) from preload cache.
+DimensionLookup.Log.EmptyPreloadCacheAfterFilter=Preload cache is empty after
ignoring zero-length validity records.
\ No newline at end of file
diff --git
a/plugins/transforms/dimensionlookup/src/test/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionCacheTest.java
b/plugins/transforms/dimensionlookup/src/test/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionCacheTest.java
index c34a1c682f..f68aab9769 100644
---
a/plugins/transforms/dimensionlookup/src/test/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionCacheTest.java
+++
b/plugins/transforms/dimensionlookup/src/test/java/org/apache/hop/pipeline/transforms/dimensionlookup/DimensionCacheTest.java
@@ -17,10 +17,20 @@
package org.apache.hop.pipeline.transforms.dimensionlookup;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
import java.sql.Timestamp;
+import java.util.ArrayList;
import java.util.Date;
+import java.util.List;
+import org.apache.hop.core.exception.HopTransformException;
import org.apache.hop.core.row.IRowMeta;
import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaString;
import org.apache.hop.core.row.value.ValueMetaTimestamp;
import org.apache.hop.core.util.Assert;
import org.junit.jupiter.api.Test;
@@ -68,6 +78,154 @@ class DimensionCacheTest {
assertCompareDateInterval(dc, null, D4, D5, null, -1); // NPE
}
+ @Test
+ void testValidateRowsForSortAcceptsValidAdjacentRanges() {
+ DimensionCache cache = createKeyedCache();
+ Date d1 = timestamp(0);
+ Date d2 = timestamp(1);
+ Date d3 = timestamp(2);
+
+ List<Object[]> rows = new ArrayList<>();
+ rows.add(new Object[] {"K1", d1, d2});
+ rows.add(new Object[] {"K1", d2, d3});
+ cache.setRowCache(rows);
+
+ assertDoesNotThrow(() -> cache.validateRowsForSort("dim_customer"));
+ assertDoesNotThrow(cache::sortRows);
+ }
+
+ @Test
+ void testValidateRowsForSortRejectsOverlappingRanges() {
+ DimensionCache cache = createKeyedCache();
+ Date d1 = timestamp(0);
+ Date d2 = timestamp(1);
+ Date d3 = timestamp(2);
+ Date d4 = timestamp(3);
+
+ List<Object[]> rows = new ArrayList<>();
+ rows.add(new Object[] {"K1", d1, d4});
+ rows.add(new Object[] {"K1", d2, d3});
+ cache.setRowCache(rows);
+
+ HopTransformException exception =
+ assertThrows(HopTransformException.class, () ->
cache.validateRowsForSort("dim_customer"));
+ assertTrue(exception.getMessage().contains("overlapping validity
periods"));
+ assertTrue(exception.getMessage().contains("K1"));
+ }
+
+ @Test
+ void testValidateRowsForSortRejectsInvalidDateRange() {
+ DimensionCache cache = createKeyedCache();
+ Date d1 = timestamp(0);
+ Date d2 = timestamp(1);
+
+ List<Object[]> rows = new ArrayList<>();
+ rows.add(new Object[] {"K1", d2, d1});
+ cache.setRowCache(rows);
+
+ HopTransformException exception =
+ assertThrows(HopTransformException.class, () ->
cache.validateRowsForSort("dim_customer"));
+ assertTrue(exception.getMessage().contains("invalid date range"));
+ assertTrue(exception.getMessage().contains("K1"));
+ }
+
+ @Test
+ void testValidateRowsForSortRejectsNullOpenEndedOverlap() {
+ DimensionCache cache = createKeyedCache();
+ Date d2 = timestamp(1);
+ Date d4 = timestamp(3);
+
+ List<Object[]> rows = new ArrayList<>();
+ rows.add(new Object[] {"K1", null, d4});
+ rows.add(new Object[] {"K1", d2, null});
+ cache.setRowCache(rows);
+
+ HopTransformException exception =
+ assertThrows(HopTransformException.class, () ->
cache.validateRowsForSort("dim_customer"));
+ assertTrue(exception.getMessage().contains("overlapping validity
periods"));
+ }
+
+ @Test
+ void testIsZeroLengthValidityDetectsEqualDatesOnly() {
+ Date d1 = timestamp(0);
+ Date d2 = timestamp(1);
+
+ assertTrue(DimensionCache.isZeroLengthValidity(d1, d1));
+ assertFalse(DimensionCache.isZeroLengthValidity(d1, d2));
+ assertFalse(DimensionCache.isZeroLengthValidity(d2, d1));
+ assertFalse(DimensionCache.isZeroLengthValidity(null, d1));
+ assertFalse(DimensionCache.isZeroLengthValidity(d1, null));
+ }
+
+ @Test
+ void testExcludeZeroLengthValidityRowsRemovesOnlyEqualDateRows() {
+ IRowMeta rowMeta = createKeyedCache().getRowMeta();
+ Date d1 = timestamp(0);
+ Date d2 = timestamp(1);
+ Date d3 = timestamp(2);
+
+ List<Object[]> rows = new ArrayList<>();
+ rows.add(new Object[] {"K1", d1, d2});
+ rows.add(new Object[] {"K1", d2, d2});
+ rows.add(new Object[] {"K2", d3, d3});
+
+ List<Object[]> filtered =
DimensionCache.excludeZeroLengthValidityRows(rowMeta, rows, 1, 2);
+
+ assertEquals(1, filtered.size());
+ assertEquals("K1", filtered.get(0)[0]);
+ assertEquals(d1, filtered.get(0)[1]);
+ assertEquals(d2, filtered.get(0)[2]);
+ }
+
+ @Test
+ void testFilterZeroLengthRowsAllowsSortAfterRemovingPointRecords() {
+ DimensionCache cache = createKeyedCache();
+ IRowMeta rowMeta = cache.getRowMeta();
+ Date d1 = timestamp(0);
+ Date d2 = timestamp(1);
+ Date d3 = timestamp(2);
+ Date d4 = timestamp(3);
+
+ List<Object[]> rows = new ArrayList<>();
+ rows.add(new Object[] {"K1", d1, d4});
+ rows.add(new Object[] {"K1", d2, d2});
+
+ List<Object[]> filtered =
DimensionCache.excludeZeroLengthValidityRows(rowMeta, rows, 1, 2);
+ cache.setRowCache(filtered);
+
+ assertDoesNotThrow(() -> cache.validateRowsForSort("dim_customer"));
+ assertDoesNotThrow(cache::sortRows);
+ }
+
+ @Test
+ void testValidateRowsForSortAllowsDifferentNaturalKeys() {
+ DimensionCache cache = createKeyedCache();
+ Date d1 = timestamp(0);
+ Date d2 = timestamp(1);
+ Date d3 = timestamp(2);
+ Date d4 = timestamp(3);
+
+ List<Object[]> rows = new ArrayList<>();
+ rows.add(new Object[] {"K1", d1, d4});
+ rows.add(new Object[] {"K2", d2, d3});
+ cache.setRowCache(rows);
+
+ assertDoesNotThrow(() -> cache.validateRowsForSort("dim_customer"));
+ }
+
+ private static DimensionCache createKeyedCache() {
+ IRowMeta rowMeta = new RowMeta();
+ rowMeta.addValueMeta(new ValueMetaString("KEY"));
+ rowMeta.addValueMeta(new ValueMetaTimestamp("DATE_FROM"));
+ rowMeta.addValueMeta(new ValueMetaTimestamp("DATE_TO"));
+ return new DimensionCache(rowMeta, new int[] {0}, 1, 2);
+ }
+
+ private static Date timestamp(int hourOffset) {
+ long t0 = 1425300000000L;
+ return new Timestamp(t0 + 3600000L * hourOffset);
+ }
+
private static void assertCompareDateInterval(
DimensionCache dc, Object from1, Object to1, Object from2, Object to2,
int expectedValue) {