This is an automated email from the ASF dual-hosted git repository.

hansva 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 b0711a5f87 Cube in/output module merge and code hardening, fixes #8380 
(#8381)
b0711a5f87 is described below

commit b0711a5f874e92c67c133a1291a0c983f260c934
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Wed Sep 16 12:31:40 2026 +0200

    Cube in/output module merge and code hardening, fixes #8380 (#8381)
---
 assemblies/plugins/pom.xml                         |   8 +-
 .../transforms/0050-cube-empty-stream-generate.hpl | 140 ++++++++++++
 .../transforms/0050-cube-empty-stream-refresh.hpl  | 226 +++++++++++++++++++
 .../transforms/0050-cube-empty-stream-verify.hpl   | 248 +++++++++++++++++++++
 .../transforms/main-0050-cube-empty-stream.hwf     | 162 ++++++++++++++
 plugins/transforms/{cubeinput => cube}/pom.xml     |   4 +-
 .../{cubeinput => cube}/src/assembly/assembly.xml  |   2 +-
 .../pipeline/transforms/cubeinput/CubeInput.java   |   0
 .../transforms/cubeinput/CubeInputData.java        |   0
 .../transforms/cubeinput/CubeInputDialog.java      |   0
 .../transforms/cubeinput/CubeInputMeta.java        |  37 ++-
 .../pipeline/transforms/cubeoutput/CubeOutput.java |  42 +++-
 .../transforms/cubeoutput/CubeOutputData.java      |   0
 .../transforms/cubeoutput/CubeOutputDialog.java    |   0
 .../transforms/cubeoutput/CubeOutputMeta.java      |   0
 .../src/main/resources/cubeinput.svg               |   0
 .../src/main/resources/cubeoutput.svg              |   0
 .../cubeinput/messages/messages_de_DE.properties   |   0
 .../cubeinput/messages/messages_en_US.properties   |   4 +-
 .../cubeinput/messages/messages_es_AR.properties   |   0
 .../cubeinput/messages/messages_es_ES.properties   |   0
 .../cubeinput/messages/messages_fr_FR.properties   |   0
 .../cubeinput/messages/messages_it_IT.properties   |   0
 .../cubeinput/messages/messages_ja_JP.properties   |   0
 .../cubeinput/messages/messages_ko_KR.properties   |   0
 .../cubeinput/messages/messages_nl_NL.properties   |   0
 .../cubeinput/messages/messages_no_NO.properties   |   0
 .../cubeinput/messages/messages_pl_PL.properties   |   0
 .../cubeinput/messages/messages_pt_BR.properties   |   0
 .../cubeinput/messages/messages_pt_PT.properties   |   0
 .../cubeinput/messages/messages_zh_CN.properties   |   0
 .../cubeoutput/messages/messages_de_DE.properties  |   0
 .../cubeoutput/messages/messages_en_US.properties  |   2 +
 .../cubeoutput/messages/messages_es_AR.properties  |   0
 .../cubeoutput/messages/messages_es_ES.properties  |   0
 .../cubeoutput/messages/messages_fr_FR.properties  |   0
 .../cubeoutput/messages/messages_it_IT.properties  |   0
 .../cubeoutput/messages/messages_ja_JP.properties  |   0
 .../cubeoutput/messages/messages_ko_KR.properties  |   0
 .../cubeoutput/messages/messages_nl_NL.properties  |   0
 .../cubeoutput/messages/messages_no_NO.properties  |   0
 .../cubeoutput/messages/messages_pl_PL.properties  |   0
 .../cubeoutput/messages/messages_pt_BR.properties  |   0
 .../cubeoutput/messages/messages_pt_PT.properties  |   0
 .../cubeoutput/messages/messages_zh_CN.properties  |   0
 .../src/main/resources/version.xml                 |   0
 .../transforms/cubeinput/CubeInputMetaTest.java    |  27 +++
 .../transforms/cubeoutput/CubeOutputMetaTest.java  |   0
 .../transforms/cubeoutput/CubeOutputTest.java      | 147 ++++++++++++
 .../transforms/cubeoutput/CubeRoundTripTest.java   | 115 ++++++++++
 .../src/test/resources/de-serialize-transform.xml  |   0
 .../src/test/resources/serialize-transform.xml     |   0
 plugins/transforms/cubeoutput/pom.xml              |  32 ---
 .../cubeoutput/src/assembly/assembly.xml           |  46 ----
 .../cubeoutput/src/main/resources/version.xml      |  20 --
 plugins/transforms/pom.xml                         |   3 +-
 56 files changed, 1125 insertions(+), 140 deletions(-)

diff --git a/assemblies/plugins/pom.xml b/assemblies/plugins/pom.xml
index 4417eb7bbb..0d3dfe9227 100644
--- a/assemblies/plugins/pom.xml
+++ b/assemblies/plugins/pom.xml
@@ -878,13 +878,7 @@
         </dependency>
         <dependency>
             <groupId>org.apache.hop</groupId>
-            <artifactId>hop-transform-cubeinput</artifactId>
-            <version>${project.version}</version>
-            <type>zip</type>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.hop</groupId>
-            <artifactId>hop-transform-cubeoutput</artifactId>
+            <artifactId>hop-transform-cube</artifactId>
             <version>${project.version}</version>
             <type>zip</type>
         </dependency>
diff --git a/integration-tests/transforms/0050-cube-empty-stream-generate.hpl 
b/integration-tests/transforms/0050-cube-empty-stream-generate.hpl
new file mode 100644
index 0000000000..bd6c411925
--- /dev/null
+++ b/integration-tests/transforms/0050-cube-empty-stream-generate.hpl
@@ -0,0 +1,140 @@
+<?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>0050-cube-empty-stream-generate</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2026/09/15 10:00:00.000</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/09/15 10:00:00.000</modified_date>
+  </info>
+  <notepads>
+    <notepad>
+      <backgroundcolorblue>251</backgroundcolorblue>
+      <backgroundcolorgreen>232</backgroundcolorgreen>
+      <backgroundcolorred>201</backgroundcolorred>
+      <bordercolorblue>90</bordercolorblue>
+      <bordercolorgreen>58</bordercolorgreen>
+      <bordercolorred>14</bordercolorred>
+      <fontbold>N</fontbold>
+      <fontcolorblue>90</fontcolorblue>
+      <fontcolorgreen>58</fontcolorgreen>
+      <fontcolorred>14</fontcolorred>
+      <fontitalic>N</fontitalic>
+      <fontname>Ubuntu</fontname>
+      <fontsize>11</fontsize>
+      <height>26</height>
+      <xloc>64</xloc>
+      <yloc>16</yloc>
+      <note>Writes a cube file with a layout (id, name) but no rows.</note>
+      <width>900</width>
+    </notepad>
+  </notepads>
+  <order>
+    <hop>
+      <from>no rows</from>
+      <to>write empty cube</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>no rows</name>
+    <type>RowGenerator</type>
+    <description></description>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <name>id</name>
+        <type>Integer</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <nullif></nullif>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+      </field>
+      <field>
+        <name>name</name>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <nullif></nullif>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+      </field>
+    </fields>
+    <interval_in_ms>5000</interval_in_ms>
+    <last_time_field>FiveSecondsAgo</last_time_field>
+    <limit>0</limit>
+    <never_ending>N</never_ending>
+    <row_time_field>now</row_time_field>
+    <attributes/>
+    <GUI>
+      <xloc>160</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>write empty cube</name>
+    <type>CubeOutput</type>
+    <description></description>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <file>
+      <name>${java.io.tmpdir}/file-0050-empty.cube</name>
+      <add_to_result_filenames>N</add_to_result_filenames>
+      <do_not_open_newfile_init>N</do_not_open_newfile_init>
+    </file>
+    <attributes/>
+    <GUI>
+      <xloc>416</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/transforms/0050-cube-empty-stream-refresh.hpl 
b/integration-tests/transforms/0050-cube-empty-stream-refresh.hpl
new file mode 100644
index 0000000000..f5599fe1c9
--- /dev/null
+++ b/integration-tests/transforms/0050-cube-empty-stream-refresh.hpl
@@ -0,0 +1,226 @@
+<?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>0050-cube-empty-stream-refresh</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2026/09/15 10:00:00.000</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/09/15 10:00:00.000</modified_date>
+  </info>
+  <notepads>
+    <notepad>
+      <backgroundcolorblue>251</backgroundcolorblue>
+      <backgroundcolorgreen>232</backgroundcolorgreen>
+      <backgroundcolorred>201</backgroundcolorred>
+      <bordercolorblue>90</bordercolorblue>
+      <bordercolorgreen>58</bordercolorgreen>
+      <bordercolorred>14</bordercolorred>
+      <fontbold>N</fontbold>
+      <fontcolorblue>90</fontcolorblue>
+      <fontcolorgreen>58</fontcolorgreen>
+      <fontcolorred>14</fontcolorred>
+      <fontitalic>N</fontitalic>
+      <fontname>Ubuntu</fontname>
+      <fontsize>11</fontsize>
+      <height>26</height>
+      <xloc>64</xloc>
+      <yloc>16</yloc>
+      <note>Cube Output receives no rows, so it has to learn the layout from 
the pipeline. That made Cube Input re-open its file, which by then can be gone: 
a cube refreshed in place is truncated by the output transform, a concurrent 
job may have replaced it, here it is simply deleted. The layout must still end 
up in the copy.</note>
+      <width>900</width>
+    </notepad>
+  </notepads>
+  <order>
+    <hop>
+      <from>read empty cube</from>
+      <to>wait until the source is gone</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>wait until the source is gone</from>
+      <to>write copy</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>one row</from>
+      <to>source file name</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>source file name</from>
+      <to>delete source cube</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>read empty cube</name>
+    <type>CubeInput</type>
+    <description></description>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <file>
+      <name>${java.io.tmpdir}/file-0050-empty.cube</name>
+    </file>
+    <limit>0</limit>
+    <addfilenameresult>N</addfilenameresult>
+    <attributes/>
+    <GUI>
+      <xloc>160</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>wait until the source is gone</name>
+    <type>BlockUntilTransformsFinish</type>
+    <description></description>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <transforms>
+      <transform>
+        <CopyNr>0</CopyNr>
+        <name>delete source cube</name>
+      </transform>
+    </transforms>
+    <attributes/>
+    <GUI>
+      <xloc>416</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>write copy</name>
+    <type>CubeOutput</type>
+    <description></description>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <file>
+      <name>${java.io.tmpdir}/file-0050-empty-copy.cube</name>
+      <add_to_result_filenames>N</add_to_result_filenames>
+      <do_not_open_newfile_init>N</do_not_open_newfile_init>
+    </file>
+    <attributes/>
+    <GUI>
+      <xloc>688</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>one row</name>
+    <type>RowGenerator</type>
+    <description></description>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+    </fields>
+    <interval_in_ms>5000</interval_in_ms>
+    <last_time_field>FiveSecondsAgo</last_time_field>
+    <limit>1</limit>
+    <never_ending>N</never_ending>
+    <row_time_field>now</row_time_field>
+    <attributes/>
+    <GUI>
+      <xloc>160</xloc>
+      <yloc>240</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>source file name</name>
+    <type>GetVariable</type>
+    <description></description>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <length>-1</length>
+        <name>filename</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>${java.io.tmpdir}/file-0050-empty.cube</variable>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>320</xloc>
+      <yloc>240</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>delete source cube</name>
+    <type>ProcessFiles</type>
+    <description></description>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <addresultfilenames>N</addresultfilenames>
+    <createparentfolder>N</createparentfolder>
+    <operation_type>delete</operation_type>
+    <overwritetargetfile>N</overwritetargetfile>
+    <simulate>N</simulate>
+    <sourcefilenamefield>filename</sourcefilenamefield>
+    <attributes/>
+    <GUI>
+      <xloc>544</xloc>
+      <yloc>240</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/transforms/0050-cube-empty-stream-verify.hpl 
b/integration-tests/transforms/0050-cube-empty-stream-verify.hpl
new file mode 100644
index 0000000000..2731baaebe
--- /dev/null
+++ b/integration-tests/transforms/0050-cube-empty-stream-verify.hpl
@@ -0,0 +1,248 @@
+<?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>0050-cube-empty-stream-verify</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2026/09/15 10:00:00.000</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/09/15 10:00:00.000</modified_date>
+  </info>
+  <notepads>
+    <notepad>
+      <backgroundcolorblue>251</backgroundcolorblue>
+      <backgroundcolorgreen>232</backgroundcolorgreen>
+      <backgroundcolorred>201</backgroundcolorred>
+      <bordercolorblue>90</bordercolorblue>
+      <bordercolorgreen>58</bordercolorgreen>
+      <bordercolorred>14</bordercolorred>
+      <fontbold>N</fontbold>
+      <fontcolorblue>90</fontcolorblue>
+      <fontcolorgreen>58</fontcolorgreen>
+      <fontcolorred>14</fontcolorred>
+      <fontitalic>N</fontitalic>
+      <fontname>Ubuntu</fontname>
+      <fontsize>11</fontsize>
+      <height>26</height>
+      <xloc>64</xloc>
+      <yloc>16</yloc>
+      <note>The copy must carry the layout of the original: id, name.</note>
+      <width>900</width>
+    </notepad>
+  </notepads>
+  <order>
+    <hop>
+      <from>read copy</from>
+      <to>one row with the layout</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>one row with the layout</from>
+      <to>layout</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>layout</from>
+      <to>concat field names</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>concat field names</from>
+      <to>not id,name</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>not id,name</from>
+      <to>Abort</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>read copy</name>
+    <type>CubeInput</type>
+    <description></description>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <file>
+      <name>${java.io.tmpdir}/file-0050-empty-copy.cube</name>
+    </file>
+    <limit>0</limit>
+    <addfilenameresult>N</addfilenameresult>
+    <attributes/>
+    <GUI>
+      <xloc>160</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>one row with the layout</name>
+    <type>DetectEmptyStream</type>
+    <description></description>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+
+    <attributes/>
+    <GUI>
+      <xloc>352</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>layout</name>
+    <type>TransformMetaStructure</type>
+    <description></description>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <includePositionField>N</includePositionField>
+    <positionFieldname>position</positionFieldname>
+    <includeFieldnameField>Y</includeFieldnameField>
+    <fieldFieldname>fieldname</fieldFieldname>
+    <includeCommentsField>N</includeCommentsField>
+    <commentsFieldname>comments</commentsFieldname>
+    <includeTypeField>N</includeTypeField>
+    <typeFieldname>type</typeFieldname>
+    <includeLengthField>N</includeLengthField>
+    <lengthFieldname>length</lengthFieldname>
+    <includePrecisionField>N</includePrecisionField>
+    <precisionFieldname>precision</precisionFieldname>
+    <includeMaskField>N</includeMaskField>
+    <maskFieldname>mask</maskFieldname>
+    <includeOriginField>N</includeOriginField>
+    <originFieldname>origin</originFieldname>
+    <outputRowcount>N</outputRowcount>
+    <rowcountField>rowcount</rowcountField>
+    <attributes/>
+    <GUI>
+      <xloc>544</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>concat field names</name>
+    <type>MemoryGroupBy</type>
+    <description></description>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <aggregate>fieldnames</aggregate>
+        <subject>fieldname</subject>
+        <type>CONCAT_STRING</type>
+        <valuefield>,</valuefield>
+      </field>
+    </fields>
+    <give_back_row>Y</give_back_row>
+    <group>
+    </group>
+    <attributes/>
+    <GUI>
+      <xloc>736</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>not id,name</name>
+    <type>FilterRows</type>
+    <description></description>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <send_true_to/>
+    <send_false_to/>
+    <compare>
+      <condition>
+        <negated>N</negated>
+        <leftvalue>fieldnames</leftvalue>
+        <function>&lt;&gt;</function>
+        <rightvalue/>
+        <value>
+          <name>constant</name>
+          <type>String</type>
+          <text>id,name</text>
+          <isnull>N</isnull>
+          <mask/>
+        </value>
+      </condition>
+    </compare>
+    <attributes/>
+    <GUI>
+      <xloc>928</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Abort</name>
+    <type>Abort</type>
+    <description></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 empty cube copy does not have the layout (id, name) of the 
original</message>
+    <row_threshold>0</row_threshold>
+    <attributes/>
+    <GUI>
+      <xloc>1120</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/transforms/main-0050-cube-empty-stream.hwf 
b/integration-tests/transforms/main-0050-cube-empty-stream.hwf
new file mode 100644
index 0000000000..273828505e
--- /dev/null
+++ b/integration-tests/transforms/main-0050-cube-empty-stream.hwf
@@ -0,0 +1,162 @@
+<?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-0050-cube-empty-stream</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description/>
+  <extended_description/>
+  <workflow_version/>
+  <created_user>-</created_user>
+  <created_date>2026/09/15 10:00:00.000</created_date>
+  <modified_user>-</modified_user>
+  <modified_date>2026/09/15 10:00:00.000</modified_date>
+  <parameters>
+    </parameters>
+  <actions>
+    <action>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <DayOfMonth>1</DayOfMonth>
+      <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>112</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>0050-cube-empty-stream-generate</name>
+      <description/>
+      <type>PIPELINE</type>
+      <attributes/>
+      <filename>${PROJECT_HOME}/0050-cube-empty-stream-generate.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>336</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>0050-cube-empty-stream-refresh</name>
+      <description/>
+      <type>PIPELINE</type>
+      <attributes/>
+      <filename>${PROJECT_HOME}/0050-cube-empty-stream-refresh.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>624</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>0050-cube-empty-stream-verify</name>
+      <description/>
+      <type>PIPELINE</type>
+      <attributes/>
+      <filename>${PROJECT_HOME}/0050-cube-empty-stream-verify.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>912</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>0050-cube-empty-stream-generate</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+    <hop>
+      <from>0050-cube-empty-stream-generate</from>
+      <to>0050-cube-empty-stream-refresh</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+    <hop>
+      <from>0050-cube-empty-stream-refresh</from>
+      <to>0050-cube-empty-stream-verify</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+  </hops>
+  <notepads>
+  </notepads>
+  <attributes/>
+</workflow>
diff --git a/plugins/transforms/cubeinput/pom.xml 
b/plugins/transforms/cube/pom.xml
similarity index 92%
rename from plugins/transforms/cubeinput/pom.xml
rename to plugins/transforms/cube/pom.xml
index 63c19e9f02..96896880f7 100644
--- a/plugins/transforms/cubeinput/pom.xml
+++ b/plugins/transforms/cube/pom.xml
@@ -25,8 +25,8 @@
         <version>2.20.0-SNAPSHOT</version>
     </parent>
 
-    <artifactId>hop-transform-cubeinput</artifactId>
+    <artifactId>hop-transform-cube</artifactId>
     <packaging>jar</packaging>
-    <name>Hop Plugins Transforms Cube Input</name>
+    <name>Hop Plugins Transforms Cube</name>
 
 </project>
diff --git a/plugins/transforms/cubeinput/src/assembly/assembly.xml 
b/plugins/transforms/cube/src/assembly/assembly.xml
similarity index 97%
rename from plugins/transforms/cubeinput/src/assembly/assembly.xml
rename to plugins/transforms/cube/src/assembly/assembly.xml
index c9b2763280..5897c0cbf2 100644
--- a/plugins/transforms/cubeinput/src/assembly/assembly.xml
+++ b/plugins/transforms/cube/src/assembly/assembly.xml
@@ -19,7 +19,7 @@
 <assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0";
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
     xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0 
http://maven.apache.org/xsd/assembly-2.2.0.xsd";>
-    <id>hop-transform-cubinput</id>
+    <id>hop-transform-cube</id>
     <formats>
         <format>zip</format>
     </formats>
diff --git 
a/plugins/transforms/cubeinput/src/main/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInput.java
 
b/plugins/transforms/cube/src/main/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInput.java
similarity index 100%
rename from 
plugins/transforms/cubeinput/src/main/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInput.java
rename to 
plugins/transforms/cube/src/main/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInput.java
diff --git 
a/plugins/transforms/cubeinput/src/main/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputData.java
 
b/plugins/transforms/cube/src/main/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputData.java
similarity index 100%
rename from 
plugins/transforms/cubeinput/src/main/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputData.java
rename to 
plugins/transforms/cube/src/main/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputData.java
diff --git 
a/plugins/transforms/cubeinput/src/main/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputDialog.java
 
b/plugins/transforms/cube/src/main/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputDialog.java
similarity index 100%
rename from 
plugins/transforms/cubeinput/src/main/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputDialog.java
rename to 
plugins/transforms/cube/src/main/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputDialog.java
diff --git 
a/plugins/transforms/cubeinput/src/main/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputMeta.java
 
b/plugins/transforms/cube/src/main/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputMeta.java
similarity index 90%
rename from 
plugins/transforms/cubeinput/src/main/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputMeta.java
rename to 
plugins/transforms/cube/src/main/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputMeta.java
index 77fcf3210c..1741bda7ed 100644
--- 
a/plugins/transforms/cubeinput/src/main/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputMeta.java
+++ 
b/plugins/transforms/cube/src/main/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputMeta.java
@@ -94,19 +94,19 @@ public class CubeInputMeta extends 
BaseTransformMeta<CubeInput, CubeInputData> {
       IVariables variables,
       IHopMetadataProvider metadataProvider)
       throws HopTransformException {
-    GZIPInputStream fis = null;
-    DataInputStream dis = null;
     if (file == null || file.getName() == null) {
       throw new HopTransformException(
           BaseMessages.getString(PKG, 
"CubeInputMeta.Exception.NoFilenameSpecified"));
     }
-    try {
-      InputStream is =
-          HopVfs.getInputStream(
-              
variables.resolve(file.getName().replace("${Internal.Transform.CopyNr}", "0")));
-      fis = new GZIPInputStream(is);
-      dis = new DataInputStream(fis);
-
+    String filename =
+        
variables.resolve(file.getName().replace("${Internal.Transform.CopyNr}", "0"));
+    // The layout of a cube lives in the file itself, so we have to open it. 
Resolve the name
+    // through the variables so that named VFS connections work here the same 
way they do at
+    // runtime in CubeInput.init(), and close the raw stream too when the GZIP 
header can't be read.
+    //
+    try (InputStream is = HopVfs.getInputStream(filename, variables);
+        GZIPInputStream fis = new GZIPInputStream(is);
+        DataInputStream dis = new DataInputStream(fis)) {
       IRowMeta add = new RowMeta(dis);
       for (int i = 0; i < add.size(); i++) {
         add.getValueMeta(i).setOrigin(name);
@@ -114,22 +114,13 @@ public class CubeInputMeta extends 
BaseTransformMeta<CubeInput, CubeInputData> {
       r.mergeRowMeta(add);
     } catch (HopFileException kfe) {
       throw new HopTransformException(
-          BaseMessages.getString(PKG, 
"CubeInputMeta.Exception.UnableToReadMetaData"), kfe);
+          BaseMessages.getString(PKG, 
"CubeInputMeta.Exception.UnableToReadMetaData", filename),
+          kfe);
     } catch (IOException e) {
       throw new HopTransformException(
-          BaseMessages.getString(PKG, 
"CubeInputMeta.Exception.ErrorOpeningOrReadingCubeFile"), e);
-    } finally {
-      try {
-        if (fis != null) {
-          fis.close();
-        }
-        if (dis != null) {
-          dis.close();
-        }
-      } catch (IOException ioe) {
-        throw new HopTransformException(
-            BaseMessages.getString(PKG, 
"CubeInputMeta.Exception.UnableToCloseCubeFile"), ioe);
-      }
+          BaseMessages.getString(
+              PKG, "CubeInputMeta.Exception.ErrorOpeningOrReadingCubeFile", 
filename),
+          e);
     }
   }
 
diff --git 
a/plugins/transforms/cubeoutput/src/main/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutput.java
 
b/plugins/transforms/cube/src/main/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutput.java
similarity index 81%
rename from 
plugins/transforms/cubeoutput/src/main/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutput.java
rename to 
plugins/transforms/cube/src/main/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutput.java
index aef08c6980..59de22d5de 100644
--- 
a/plugins/transforms/cubeoutput/src/main/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutput.java
+++ 
b/plugins/transforms/cube/src/main/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutput.java
@@ -26,6 +26,8 @@ import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.exception.HopFileException;
 import org.apache.hop.core.exception.HopTransformException;
 import org.apache.hop.core.io.CountingOutputStream;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.RowMeta;
 import org.apache.hop.core.vfs.HopVfs;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.pipeline.Pipeline;
@@ -60,11 +62,18 @@ public class CubeOutput extends 
BaseTransform<CubeOutputMeta, CubeOutputData> {
     if (first) { // Always run this code once, even if stream is empty 
(r==null)
       if (getInputRowMeta() != null) {
         data.outputMeta = getInputRowMeta().clone();
-      } else {
-        // If the stream is empty, then row metadata probably hasn't been 
received. In this case,
-        // use
-        // the design-time algorithm to calculate the output metadata.
-        data.outputMeta = getPipelineMeta().getPrevTransformFields(this, 
getTransformMeta());
+      } else if (data.outputMeta == null) {
+        // If the stream is empty, then row metadata hasn't been received. Use 
the design-time
+        // layout unless init() already resolved it before opening the file.
+        data.outputMeta = lookupDesignTimeLayout();
+        if (data.outputMeta == null) {
+          logBasic(
+              BaseMessages.getString(
+                  PKG,
+                  "CubeOutput.Log.UnableToDetermineEmptyStreamLayout",
+                  resolve(meta.getFilename())));
+          data.outputMeta = new RowMeta();
+        }
       }
 
       // If input stream is empty, but file was already opened in init(), then
@@ -122,6 +131,24 @@ public class CubeOutput extends 
BaseTransform<CubeOutputMeta, CubeOutputData> {
     return result;
   }
 
+  /**
+   * The layout of the stream as the pipeline knows it at design time. Some 
input transforms have to
+   * interrogate their source to answer (Cube Input reads the cube file, Table 
Input runs the query
+   * metadata), so this can fail; in that case the pipeline itself reports the 
problem.
+   *
+   * @return the layout, or null when it could not be determined
+   */
+  private IRowMeta lookupDesignTimeLayout() {
+    try {
+      return getPipelineMeta().getPrevTransformFields(this, 
getTransformMeta());
+    } catch (HopTransformException e) {
+      logDetailed(
+          BaseMessages.getString(
+              PKG, "CubeOutput.Log.UnableToLookupDesignTimeLayout", 
e.getMessage()));
+      return null;
+    }
+  }
+
   private synchronized boolean writeHeaderToFile() {
     try {
       data.outputMeta.writeMeta(data.dos);
@@ -151,6 +178,11 @@ public class CubeOutput extends 
BaseTransform<CubeOutputMeta, CubeOutputData> {
   public boolean init() {
     if (super.init()) {
       if (!meta.isDoNotOpenNewFileInit()) {
+        // Resolve the layout of the incoming stream before opening the output 
file. Opening
+        // truncates it, and when the pipeline refreshes a cube in place the 
Cube Input upstream can
+        // no longer read that file to tell us its layout at the end of an 
empty run.
+        //
+        data.outputMeta = lookupDesignTimeLayout();
         try {
           prepareFile();
           data.oneFileOpened = true;
diff --git 
a/plugins/transforms/cubeoutput/src/main/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutputData.java
 
b/plugins/transforms/cube/src/main/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutputData.java
similarity index 100%
rename from 
plugins/transforms/cubeoutput/src/main/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutputData.java
rename to 
plugins/transforms/cube/src/main/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutputData.java
diff --git 
a/plugins/transforms/cubeoutput/src/main/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutputDialog.java
 
b/plugins/transforms/cube/src/main/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutputDialog.java
similarity index 100%
rename from 
plugins/transforms/cubeoutput/src/main/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutputDialog.java
rename to 
plugins/transforms/cube/src/main/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutputDialog.java
diff --git 
a/plugins/transforms/cubeoutput/src/main/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutputMeta.java
 
b/plugins/transforms/cube/src/main/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutputMeta.java
similarity index 100%
rename from 
plugins/transforms/cubeoutput/src/main/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutputMeta.java
rename to 
plugins/transforms/cube/src/main/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutputMeta.java
diff --git a/plugins/transforms/cubeinput/src/main/resources/cubeinput.svg 
b/plugins/transforms/cube/src/main/resources/cubeinput.svg
similarity index 100%
rename from plugins/transforms/cubeinput/src/main/resources/cubeinput.svg
rename to plugins/transforms/cube/src/main/resources/cubeinput.svg
diff --git a/plugins/transforms/cubeoutput/src/main/resources/cubeoutput.svg 
b/plugins/transforms/cube/src/main/resources/cubeoutput.svg
similarity index 100%
rename from plugins/transforms/cubeoutput/src/main/resources/cubeoutput.svg
rename to plugins/transforms/cube/src/main/resources/cubeoutput.svg
diff --git 
a/plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_de_DE.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_de_DE.properties
similarity index 100%
rename from 
plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_de_DE.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_de_DE.properties
diff --git 
a/plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_en_US.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_en_US.properties
similarity index 97%
rename from 
plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_en_US.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_en_US.properties
index acb694ea61..d6b9d056d8 100644
--- 
a/plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_en_US.properties
+++ 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_en_US.properties
@@ -31,9 +31,9 @@ CubeInputDialog.Limit.Label=Limit size
 CubeInputDialog.Shell.Title=De-serialize from file
 CubeInputDialog.TransformName.Label=Transform name
 CubeInputMeta.CheckResult.FileSpecificationsNotChecked=File specifications are 
not checked.
-CubeInputMeta.Exception.ErrorOpeningOrReadingCubeFile=Error opening/reading 
cube file
+CubeInputMeta.Exception.ErrorOpeningOrReadingCubeFile=Error opening/reading 
cube file [{0}]
 CubeInputMeta.Exception.NoFilenameSpecified=No cube file name has been 
specified for this transform
 CubeInputMeta.Exception.UnableToCloseCubeFile=Unable to close cube file
 CubeInputMeta.Exception.UnableToLoadTransformMeta=Unable to load transform 
info from XML
-CubeInputMeta.Exception.UnableToReadMetaData=Unable to read metadata from cube 
file
+CubeInputMeta.Exception.UnableToReadMetaData=Unable to read metadata from cube 
file [{0}]
 CubeInputMeta.keyword=cube,deserialize,read,binary,serialized
diff --git 
a/plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_es_AR.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_es_AR.properties
similarity index 100%
rename from 
plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_es_AR.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_es_AR.properties
diff --git 
a/plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_es_ES.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_es_ES.properties
similarity index 100%
rename from 
plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_es_ES.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_es_ES.properties
diff --git 
a/plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_fr_FR.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_fr_FR.properties
similarity index 100%
rename from 
plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_fr_FR.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_fr_FR.properties
diff --git 
a/plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_it_IT.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_it_IT.properties
similarity index 100%
rename from 
plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_it_IT.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_it_IT.properties
diff --git 
a/plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_ja_JP.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_ja_JP.properties
similarity index 100%
rename from 
plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_ja_JP.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_ja_JP.properties
diff --git 
a/plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_ko_KR.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_ko_KR.properties
similarity index 100%
rename from 
plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_ko_KR.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_ko_KR.properties
diff --git 
a/plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_nl_NL.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_nl_NL.properties
similarity index 100%
rename from 
plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_nl_NL.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_nl_NL.properties
diff --git 
a/plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_no_NO.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_no_NO.properties
similarity index 100%
rename from 
plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_no_NO.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_no_NO.properties
diff --git 
a/plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_pl_PL.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_pl_PL.properties
similarity index 100%
rename from 
plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_pl_PL.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_pl_PL.properties
diff --git 
a/plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_pt_BR.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_pt_BR.properties
similarity index 100%
rename from 
plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_pt_BR.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_pt_BR.properties
diff --git 
a/plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_pt_PT.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_pt_PT.properties
similarity index 100%
rename from 
plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_pt_PT.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_pt_PT.properties
diff --git 
a/plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_zh_CN.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_zh_CN.properties
similarity index 100%
rename from 
plugins/transforms/cubeinput/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_zh_CN.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeinput/messages/messages_zh_CN.properties
diff --git 
a/plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_de_DE.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_de_DE.properties
similarity index 100%
rename from 
plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_de_DE.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_de_DE.properties
diff --git 
a/plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_en_US.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_en_US.properties
similarity index 88%
rename from 
plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_en_US.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_en_US.properties
index 3c4eb1e45f..7fdc7fe97b 100644
--- 
a/plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_en_US.properties
+++ 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_en_US.properties
@@ -40,3 +40,5 @@ CubeOutputMeta.CheckResult.FileSpecificationsNotChecked=File 
specifications are
 CubeOutputMeta.CheckResult.ReceivingFields=Transform is connected to previous 
one, receiving {0} fields
 CubeOutputMeta.Exception.UnableToLoadTransformMeta=Unable to load transform 
info from XML
 CubeOutputMeta.keyword=serialize,cube,binary,write,file
+CubeOutput.Log.UnableToDetermineEmptyStreamLayout=WARNING\: no rows were 
received and the layout of the input stream could not be determined, writing an 
empty cube file without fields to [{0}]
+CubeOutput.Log.UnableToLookupDesignTimeLayout=Could not determine the layout 
of the input stream from the pipeline\: {0}
diff --git 
a/plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_es_AR.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_es_AR.properties
similarity index 100%
rename from 
plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_es_AR.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_es_AR.properties
diff --git 
a/plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_es_ES.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_es_ES.properties
similarity index 100%
rename from 
plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_es_ES.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_es_ES.properties
diff --git 
a/plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_fr_FR.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_fr_FR.properties
similarity index 100%
rename from 
plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_fr_FR.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_fr_FR.properties
diff --git 
a/plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_it_IT.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_it_IT.properties
similarity index 100%
rename from 
plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_it_IT.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_it_IT.properties
diff --git 
a/plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_ja_JP.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_ja_JP.properties
similarity index 100%
rename from 
plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_ja_JP.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_ja_JP.properties
diff --git 
a/plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_ko_KR.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_ko_KR.properties
similarity index 100%
rename from 
plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_ko_KR.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_ko_KR.properties
diff --git 
a/plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_nl_NL.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_nl_NL.properties
similarity index 100%
rename from 
plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_nl_NL.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_nl_NL.properties
diff --git 
a/plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_no_NO.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_no_NO.properties
similarity index 100%
rename from 
plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_no_NO.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_no_NO.properties
diff --git 
a/plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_pl_PL.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_pl_PL.properties
similarity index 100%
rename from 
plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_pl_PL.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_pl_PL.properties
diff --git 
a/plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_pt_BR.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_pt_BR.properties
similarity index 100%
rename from 
plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_pt_BR.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_pt_BR.properties
diff --git 
a/plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_pt_PT.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_pt_PT.properties
similarity index 100%
rename from 
plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_pt_PT.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_pt_PT.properties
diff --git 
a/plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_zh_CN.properties
 
b/plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_zh_CN.properties
similarity index 100%
rename from 
plugins/transforms/cubeoutput/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_zh_CN.properties
rename to 
plugins/transforms/cube/src/main/resources/org/apache/hop/pipeline/transforms/cubeoutput/messages/messages_zh_CN.properties
diff --git a/plugins/transforms/cubeinput/src/main/resources/version.xml 
b/plugins/transforms/cube/src/main/resources/version.xml
similarity index 100%
rename from plugins/transforms/cubeinput/src/main/resources/version.xml
rename to plugins/transforms/cube/src/main/resources/version.xml
diff --git 
a/plugins/transforms/cubeinput/src/test/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputMetaTest.java
 
b/plugins/transforms/cube/src/test/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputMetaTest.java
similarity index 65%
rename from 
plugins/transforms/cubeinput/src/test/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputMetaTest.java
rename to 
plugins/transforms/cube/src/test/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputMetaTest.java
index 361da84d8e..405f04d87d 100644
--- 
a/plugins/transforms/cubeinput/src/test/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputMetaTest.java
+++ 
b/plugins/transforms/cube/src/test/java/org/apache/hop/pipeline/transforms/cubeinput/CubeInputMetaTest.java
@@ -21,14 +21,20 @@ import static 
org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.io.EOFException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import org.apache.hop.core.exception.HopTransformException;
 import org.apache.hop.core.row.RowMeta;
 import org.apache.hop.core.variables.Variables;
 import org.apache.hop.pipeline.transform.TransformSerializationTestUtil;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
 class CubeInputMetaTest {
 
+  @TempDir Path tempDir;
+
   @Test
   void testRoundTrip() throws Exception {
     CubeInputMeta meta =
@@ -52,4 +58,25 @@ class CubeInputMetaTest {
             () -> meta.getFields(new RowMeta(), "cube input", null, null, new 
Variables(), null));
     assertTrue(e.getMessage().contains("cube file name"), e.getMessage());
   }
+
+  /**
+   * The layout of a cube lives in the file, so getFields() has to open it. 
That also happens at
+   * runtime, when a downstream transform that received no rows falls back to 
the design-time
+   * layout: if the file is empty or half-written by then (a cube refreshed in 
place, a concurrent
+   * writer), we fail with a message that names the file instead of a bare 
EOFException.
+   */
+  @Test
+  void getFieldsOnTruncatedCubeFileReportsTheFile() throws Exception {
+    Path cubeFile = Files.createFile(tempDir.resolve("truncated.cube"));
+    CubeInputMeta meta = new CubeInputMeta();
+    meta.setDefault();
+    meta.getFile().setName(cubeFile.toString());
+
+    HopTransformException e =
+        assertThrows(
+            HopTransformException.class,
+            () -> meta.getFields(new RowMeta(), "cube input", null, null, new 
Variables(), null));
+    assertTrue(e.getMessage().contains(cubeFile.toString()), e.getMessage());
+    assertTrue(e.getCause() instanceof EOFException, 
String.valueOf(e.getCause()));
+  }
 }
diff --git 
a/plugins/transforms/cubeoutput/src/test/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutputMetaTest.java
 
b/plugins/transforms/cube/src/test/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutputMetaTest.java
similarity index 100%
rename from 
plugins/transforms/cubeoutput/src/test/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutputMetaTest.java
rename to 
plugins/transforms/cube/src/test/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutputMetaTest.java
diff --git 
a/plugins/transforms/cube/src/test/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutputTest.java
 
b/plugins/transforms/cube/src/test/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutputTest.java
new file mode 100644
index 0000000000..b0eea80777
--- /dev/null
+++ 
b/plugins/transforms/cube/src/test/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeOutputTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.pipeline.transforms.cubeoutput;
+
+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 static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import java.io.DataInputStream;
+import java.io.EOFException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.zip.GZIPInputStream;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.exception.HopEofException;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transforms.mock.TransformMockHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
+
+class CubeOutputTest {
+
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  @TempDir Path tempDir;
+
+  private TransformMockHelper<CubeOutputMeta, CubeOutputData> helper;
+
+  @BeforeAll
+  static void setUpBeforeClass() throws HopException {
+    HopEnvironment.init();
+  }
+
+  @BeforeEach
+  void setUp() {
+    helper = new TransformMockHelper<>("Cube output", CubeOutputMeta.class, 
CubeOutputData.class);
+    when(helper.logChannelFactory.create(any(), any(ILoggingObject.class)))
+        .thenReturn(helper.iLogChannel);
+    when(helper.pipeline.isRunning()).thenReturn(true);
+  }
+
+  @AfterEach
+  void tearDown() {
+    helper.cleanUp();
+  }
+
+  /**
+   * When no rows arrive, the output layout is derived from the pipeline 
graph. That walk can fail
+   * because an upstream input transform has to re-open its source to answer 
(a Cube Input whose
+   * file is being rewritten in place by this very transform, for example). 
The run used to end in
+   * an error with a cube file that had a GZIP header but no layout; now it 
writes a valid, empty
+   * cube without fields.
+   */
+  @Test
+  void emptyStreamWithUnknownLayoutWritesAnEmptyCube() throws Exception {
+    Path cubeFile = tempDir.resolve("empty.cube");
+    CubeOutput cubeOutput = createCubeOutput(cubeFile);
+    when(helper.pipelineMeta.getPrevTransformFields(any(), 
any(TransformMeta.class)))
+        .thenThrow(
+            new HopTransformException("Error opening/reading cube file", new 
EOFException()));
+    doReturn(null).when(cubeOutput).getRow();
+
+    assertTrue(cubeOutput.init());
+    assertFalse(cubeOutput.processRow());
+    cubeOutput.dispose();
+
+    assertEquals(0, cubeOutput.getErrors());
+    assertEquals(0, readCubeLayout(cubeFile).size());
+  }
+
+  @Test
+  void emptyStreamWithKnownLayoutWritesTheLayout() throws Exception {
+    Path cubeFile = tempDir.resolve("empty.cube");
+    CubeOutput cubeOutput = createCubeOutput(cubeFile);
+    IRowMeta layout = new RowMeta();
+    layout.addValueMeta(new 
org.apache.hop.core.row.value.ValueMetaString("name"));
+    when(helper.pipelineMeta.getPrevTransformFields(any(), 
any(TransformMeta.class)))
+        .thenReturn(layout);
+    doReturn(null).when(cubeOutput).getRow();
+
+    assertTrue(cubeOutput.init());
+    assertFalse(cubeOutput.processRow());
+    cubeOutput.dispose();
+
+    assertEquals(0, cubeOutput.getErrors());
+    IRowMeta written = readCubeLayout(cubeFile);
+    assertEquals(1, written.size());
+    assertEquals("name", written.getValueMeta(0).getName());
+  }
+
+  private CubeOutput createCubeOutput(Path cubeFile) {
+    CubeOutputMeta meta = new CubeOutputMeta();
+    meta.setDefault();
+    meta.setFilename(cubeFile.toString());
+    when(helper.transformMeta.getTransform()).thenReturn(meta);
+    CubeOutput cubeOutput =
+        new CubeOutput(
+            helper.transformMeta,
+            meta,
+            new CubeOutputData(),
+            0,
+            helper.pipelineMeta,
+            helper.pipeline);
+    return spy(cubeOutput);
+  }
+
+  /** Reads the file the way Cube Input does: header, then rows until the 
end-of-file marker. */
+  private static IRowMeta readCubeLayout(Path cubeFile) throws Exception {
+    try (InputStream is = Files.newInputStream(cubeFile);
+        DataInputStream dis = new DataInputStream(new GZIPInputStream(is))) {
+      IRowMeta rowMeta = new RowMeta(dis);
+      assertThrows(HopEofException.class, () -> rowMeta.readData(dis));
+      return rowMeta;
+    }
+  }
+}
diff --git 
a/plugins/transforms/cube/src/test/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeRoundTripTest.java
 
b/plugins/transforms/cube/src/test/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeRoundTripTest.java
new file mode 100644
index 0000000000..298e9b0e89
--- /dev/null
+++ 
b/plugins/transforms/cube/src/test/java/org/apache/hop/pipeline/transforms/cubeoutput/CubeRoundTripTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.pipeline.transforms.cubeoutput;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.logging.LoggingObject;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaInteger;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.pipeline.PipelineHopMeta;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.engines.local.LocalPipelineEngine;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transforms.cubeinput.CubeInputMeta;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/** Runs a real Cube Input → Cube Output pipeline. */
+class CubeRoundTripTest {
+
+  @TempDir Path tempDir;
+
+  @BeforeAll
+  static void setUpBeforeClass() throws Exception {
+    HopEnvironment.init();
+  }
+
+  /**
+   * An empty cube still has a layout. Reading it and writing it back (here to 
another file, but
+   * refreshing the same file in place is the case that used to fail) must 
produce a cube with that
+   * same layout and no rows, without the output transform having to re-open 
the input file at the
+   * end of the run to find out what the layout was.
+   */
+  @Test
+  void emptyCubeKeepsItsLayoutThroughAPipeline() throws Exception {
+    IRowMeta layout = new RowMeta();
+    layout.addValueMeta(new ValueMetaString("name"));
+    layout.addValueMeta(new ValueMetaInteger("id"));
+    Path source = tempDir.resolve("source.cube");
+    writeEmptyCube(source, layout);
+    Path target = tempDir.resolve("target.cube");
+
+    CubeInputMeta inputMeta = new CubeInputMeta();
+    inputMeta.setDefault();
+    inputMeta.getFile().setName(source.toString());
+    CubeOutputMeta outputMeta = new CubeOutputMeta();
+    outputMeta.setDefault();
+    outputMeta.setFilename(target.toString());
+
+    PipelineMeta pipelineMeta = new PipelineMeta();
+    pipelineMeta.setName("cube round trip");
+    TransformMeta input = new TransformMeta("Cube input", inputMeta);
+    TransformMeta output = new TransformMeta("Cube output", outputMeta);
+    pipelineMeta.addTransform(input);
+    pipelineMeta.addTransform(output);
+    pipelineMeta.addPipelineHop(new PipelineHopMeta(input, output));
+
+    // Truncate the source before the output transform gets a chance to ask 
for its layout again:
+    // this is what an in-place refresh (same file for input and output) does 
in init().
+    //
+    LocalPipelineEngine pipeline =
+        new LocalPipelineEngine(pipelineMeta, new Variables(), new 
LoggingObject("test"));
+    pipeline.prepareExecution();
+    Files.write(source, new byte[0]);
+    pipeline.startThreads();
+    pipeline.waitUntilFinished();
+
+    assertEquals(0, pipeline.getErrors(), "the run reported errors");
+    IRowMeta written = readCubeLayout(target);
+    assertEquals(layout.getFieldNames().length, written.size());
+    assertEquals("name", written.getValueMeta(0).getName());
+    assertEquals("id", written.getValueMeta(1).getName());
+  }
+
+  private static void writeEmptyCube(Path file, IRowMeta layout) throws 
Exception {
+    try (OutputStream os = Files.newOutputStream(file);
+        DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))) 
{
+      layout.writeMeta(dos);
+    }
+  }
+
+  private static IRowMeta readCubeLayout(Path file) throws Exception {
+    try (InputStream is = Files.newInputStream(file);
+        DataInputStream dis = new DataInputStream(new GZIPInputStream(is))) {
+      return new RowMeta(dis);
+    }
+  }
+}
diff --git 
a/plugins/transforms/cubeinput/src/test/resources/de-serialize-transform.xml 
b/plugins/transforms/cube/src/test/resources/de-serialize-transform.xml
similarity index 100%
rename from 
plugins/transforms/cubeinput/src/test/resources/de-serialize-transform.xml
rename to plugins/transforms/cube/src/test/resources/de-serialize-transform.xml
diff --git 
a/plugins/transforms/cubeoutput/src/test/resources/serialize-transform.xml 
b/plugins/transforms/cube/src/test/resources/serialize-transform.xml
similarity index 100%
rename from 
plugins/transforms/cubeoutput/src/test/resources/serialize-transform.xml
rename to plugins/transforms/cube/src/test/resources/serialize-transform.xml
diff --git a/plugins/transforms/cubeoutput/pom.xml 
b/plugins/transforms/cubeoutput/pom.xml
deleted file mode 100644
index cbf79e25f0..0000000000
--- a/plugins/transforms/cubeoutput/pom.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?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.
-  ~
-  -->
-<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
-    <modelVersion>4.0.0</modelVersion>
-
-    <parent>
-        <groupId>org.apache.hop</groupId>
-        <artifactId>hop-plugins-transforms</artifactId>
-        <version>2.20.0-SNAPSHOT</version>
-    </parent>
-
-    <artifactId>hop-transform-cubeoutput</artifactId>
-    <packaging>jar</packaging>
-    <name>Hop Plugins Transforms Cube Output</name>
-
-</project>
diff --git a/plugins/transforms/cubeoutput/src/assembly/assembly.xml 
b/plugins/transforms/cubeoutput/src/assembly/assembly.xml
deleted file mode 100644
index 86c00c3080..0000000000
--- a/plugins/transforms/cubeoutput/src/assembly/assembly.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-<!--
-  ~ 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.
-  ~
-  -->
-
-<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0";
-    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
-    xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0 
http://maven.apache.org/xsd/assembly-2.2.0.xsd";>
-    <id>hop-transform-cubeoutput</id>
-    <formats>
-        <format>zip</format>
-    </formats>
-    <baseDirectory>.</baseDirectory>
-    <files>
-        <file>
-            <source>${project.basedir}/src/main/resources/version.xml</source>
-            <outputDirectory>${hop.plugin.libdir}</outputDirectory>
-            <filtered>true</filtered>
-        </file>
-    </files>
-
-    <fileSets>
-        <fileSet>
-            <directory>${project.basedir}/src/main/samples</directory>
-            <outputDirectory>config/projects/samples/</outputDirectory>
-        </fileSet>
-    </fileSets>
-
-
-    <componentDescriptors>
-        
<componentDescriptor>${maven.multiModuleProjectDirectory}/assemblies/shared/hop-plugin-libs.xml</componentDescriptor>
-    </componentDescriptors>
-</assembly>
\ No newline at end of file
diff --git a/plugins/transforms/cubeoutput/src/main/resources/version.xml 
b/plugins/transforms/cubeoutput/src/main/resources/version.xml
deleted file mode 100644
index 6be576acae..0000000000
--- a/plugins/transforms/cubeoutput/src/main/resources/version.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?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.
-  ~
-  -->
-
-<version>${project.version}</version>
\ No newline at end of file
diff --git a/plugins/transforms/pom.xml b/plugins/transforms/pom.xml
index ebb3fa9eab..240b5e594d 100644
--- a/plugins/transforms/pom.xml
+++ b/plugins/transforms/pom.xml
@@ -47,8 +47,7 @@
         <module>concatfields</module>
         <module>constant</module>
         <module>creditcardvalidator</module>
-        <module>cubeinput</module>
-        <module>cubeoutput</module>
+        <module>cube</module>
         <module>databasejoin</module>
         <module>databaselookup</module>
         <module>databasevaluevalidation</module>

Reply via email to