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 3bad898630 add header row when creating text file, fixes #7316 (#7317)
3bad898630 is described below

commit 3bad8986300d140105a24ab9dac47839f79ceeca
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Fri Jun 19 11:53:35 2026 +0200

    add header row when creating text file, fixes #7316 (#7317)
---
 .../transforms/textfileoutput/TextFileOutput.java  | 19 ++++++
 .../textfileoutput/TextFileOutputTest.java         | 76 ++++++++++++++++++++++
 2 files changed, 95 insertions(+)

diff --git 
a/plugins/transforms/textfile/src/main/java/org/apache/hop/pipeline/transforms/textfileoutput/TextFileOutput.java
 
b/plugins/transforms/textfile/src/main/java/org/apache/hop/pipeline/transforms/textfileoutput/TextFileOutput.java
index bbd440d785..82cb6587c0 100644
--- 
a/plugins/transforms/textfile/src/main/java/org/apache/hop/pipeline/transforms/textfileoutput/TextFileOutput.java
+++ 
b/plugins/transforms/textfile/src/main/java/org/apache/hop/pipeline/transforms/textfileoutput/TextFileOutput.java
@@ -404,6 +404,25 @@ public class TextFileOutput extends 
BaseTransform<TextFileOutputMeta, TextFileOu
       if (data.writer != null) {
         if (data.outputRowMeta != null && meta.isFooterEnabled()) {
           writeHeader();
+        } else if (first && meta.isHeaderEnabled()) {
+          // The file was created at pipeline start (init() -> initOutput()) 
but no rows were
+          // received, so the header was never written. Honor the enabled 
header for the empty
+          // result set.
+          String filename = getOutputFileName(null);
+          if (isWriteHeader(filename)) {
+            if (data.outputRowMeta == null) {
+              data.outputRowMeta =
+                  getPipelineMeta().getPrevTransformFields(variables, 
getTransformMeta());
+              if (data.outputRowMeta != null) {
+                meta.getFields(
+                    data.outputRowMeta, getTransformName(), null, null, this, 
metadataProvider);
+              }
+            }
+            // Without column metadata there is nothing to write a header from.
+            if (data.outputRowMeta != null) {
+              writeHeader();
+            }
+          }
         }
       } else if (!Utils.isEmpty(resolve(meta.getEndedLine())) && 
!meta.isFileNameInField()) {
         String filename = getOutputFileName(null);
diff --git 
a/plugins/transforms/textfile/src/test/java/org/apache/hop/pipeline/transforms/textfileoutput/TextFileOutputTest.java
 
b/plugins/transforms/textfile/src/test/java/org/apache/hop/pipeline/transforms/textfileoutput/TextFileOutputTest.java
index 2e897f5548..9cdb2cbac5 100644
--- 
a/plugins/transforms/textfile/src/test/java/org/apache/hop/pipeline/transforms/textfileoutput/TextFileOutputTest.java
+++ 
b/plugins/transforms/textfile/src/test/java/org/apache/hop/pipeline/transforms/textfileoutput/TextFileOutputTest.java
@@ -753,6 +753,82 @@ class TextFileOutputTest {
     assertEquals(1, textFileOutputSpy.getResultFiles().size());
   }
 
+  /**
+   * When the file is created at pipeline start ('Do not create file at start' 
unchecked) and the
+   * header is enabled, the header must still be written even when no rows are 
received. Without the
+   * fix this produced an empty file with no header. With the transform's 
fields configured, the
+   * header columns come from those fields - exactly as they would for a 
regular row.
+   */
+  @Test
+  void testHeaderWrittenForEmptyFileCreatedAtStart_configuredFields() throws 
Exception {
+    assertEquals("Name Surname\n", 
writeHeaderOnlyFileWithoutRows(textFileFields));
+  }
+
+  /**
+   * Same as above but with no fields configured on the transform: the header 
columns are taken from
+   * the incoming stream metadata (resolved from the previous transform when 
no rows flow), again
+   * mirroring the regular-row code path.
+   */
+  @Test
+  void testHeaderWrittenForEmptyFileCreatedAtStart_allStreamFields() throws 
Exception {
+    assertEquals("Name Surname\n", writeHeaderOnlyFileWithoutRows(new 
ArrayList<>()));
+  }
+
+  /**
+   * Runs a TextFileOutput with the header enabled, the file created at 
pipeline start and zero
+   * rows, then returns the resulting file content. The incoming stream always 
carries the
+   * Name/Surname fields; {@code outputFields} controls whether those columns 
are explicitly
+   * configured on the transform or left empty (write all stream fields).
+   */
+  private String writeHeaderOnlyFileWithoutRows(List<TextFileField> 
outputFields) throws Exception {
+    FileObject file = createTemplateFile(null); // file does not exist yet
+    String pathToFile = file.getName().getURI();
+
+    TextFileOutputData textFileOutputData = new TextFileOutputData();
+    TextFileOutputTestHandler handler =
+        new TextFileOutputTestHandler(
+            transformMockHelper.transformMeta,
+            transformMockHelper.iTransformMeta,
+            textFileOutputData,
+            0,
+            transformMockHelper.pipelineMeta,
+            transformMockHelper.pipeline);
+
+    TextFileOutputMeta.FileSettings fileSettings =
+        Mockito.mock(TextFileOutputMeta.FileSettings.class);
+    Mockito.when(fileSettings.isDoNotOpenNewFileInit()).thenReturn(false); // 
create file at start
+    Mockito.when(fileSettings.isFileAppended()).thenReturn(false);
+    Mockito.when(fileSettings.getFileName()).thenReturn(pathToFile);
+    
Mockito.when(transformMockHelper.iTransformMeta.getFileSettings()).thenReturn(fileSettings);
+    
Mockito.when(transformMockHelper.iTransformMeta.isHeaderEnabled()).thenReturn(true);
+    
Mockito.when(transformMockHelper.iTransformMeta.getEndedLine()).thenReturn(null);
+    
Mockito.when(transformMockHelper.iTransformMeta.getOutputFields()).thenReturn(outputFields);
+
+    // With zero rows the output row metadata is resolved from the previous 
transform's fields,
+    // just like getInputRowMeta() would supply it once the first row arrives.
+    IRowMeta prevFields = Mockito.mock(IRowMeta.class);
+    for (int i = 0; i < textFileFields.size(); i++) {
+      String name = textFileFields.get(i).getName();
+      Mockito.when(prevFields.searchValueMeta(name)).thenReturn(new 
ValueMetaString(name));
+      Mockito.when(prevFields.getValueMeta(i)).thenReturn(new 
ValueMetaString(name));
+      Mockito.when(prevFields.indexOfValue(name)).thenReturn(i);
+    }
+    Mockito.when(prevFields.size()).thenReturn(textFileFields.size());
+    Mockito.when(
+            transformMockHelper.pipelineMeta.getPrevTransformFields(
+                Mockito.any(IVariables.class), 
Mockito.any(TransformMeta.class)))
+        .thenReturn(prevFields);
+
+    handler.init();
+    handler.setRow(null); // no rows received
+    handler.processRow();
+    handler.dispose();
+
+    assertTrue(handler.errors.isEmpty(), handler.errors.toString());
+    assertTrue(file.exists());
+    return IOUtils.toString(file.getContent().getInputStream(), 
StandardCharsets.UTF_8);
+  }
+
   @Test
   void testFastDumpDisableStreamEncodeTest() throws Exception {
 

Reply via email to