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 17d7c320ee fix wait for sql result type, fixes #2483 (#8086)
17d7c320ee is described below

commit 17d7c320ee3f270048d52005b3c14c243660e425
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Tue Aug 25 15:22:01 2026 +0200

    fix wait for sql result type, fixes #2483 (#8086)
---
 plugins/actions/waitforsql/pom.xml                 |  26 +++
 .../actions/waitforsql/ActionWaitForSql.java       |   2 +-
 .../waitforsql/ActionWaitForSqlResultRowsTest.java | 194 +++++++++++++++++++++
 3 files changed, 221 insertions(+), 1 deletion(-)

diff --git a/plugins/actions/waitforsql/pom.xml 
b/plugins/actions/waitforsql/pom.xml
index 2f430c8e18..93edeaa0fc 100644
--- a/plugins/actions/waitforsql/pom.xml
+++ b/plugins/actions/waitforsql/pom.xml
@@ -29,4 +29,30 @@
     <packaging>jar</packaging>
     <name>Hop Plugins Actions Wait for SQL</name>
 
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.apache.hop</groupId>
+                <artifactId>hop-libs-jdbc</artifactId>
+                <version>${project.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.hop</groupId>
+            <artifactId>hop-databases-h2</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
 </project>
diff --git 
a/plugins/actions/waitforsql/src/main/java/org/apache/hop/workflow/actions/waitforsql/ActionWaitForSql.java
 
b/plugins/actions/waitforsql/src/main/java/org/apache/hop/workflow/actions/waitforsql/ActionWaitForSql.java
index e24a42a67a..10a7624a08 100644
--- 
a/plugins/actions/waitforsql/src/main/java/org/apache/hop/workflow/actions/waitforsql/ActionWaitForSql.java
+++ 
b/plugins/actions/waitforsql/src/main/java/org/apache/hop/workflow/actions/waitforsql/ActionWaitForSql.java
@@ -460,7 +460,7 @@ public class ActionWaitForSql extends ActionBase implements 
Cloneable, IAction {
       } // end if countStatement!=null
 
       if (addRowsResult && customSqlEnabled && ar != null) {
-        rowMeta = db.getQueryFields(countStatement, false);
+        rowMeta = db.getReturnRowMeta();
       }
     } catch (HopDatabaseException dbe) {
       logError(
diff --git 
a/plugins/actions/waitforsql/src/test/java/org/apache/hop/workflow/actions/waitforsql/ActionWaitForSqlResultRowsTest.java
 
b/plugins/actions/waitforsql/src/test/java/org/apache/hop/workflow/actions/waitforsql/ActionWaitForSqlResultRowsTest.java
new file mode 100644
index 0000000000..bd22112c2b
--- /dev/null
+++ 
b/plugins/actions/waitforsql/src/test/java/org/apache/hop/workflow/actions/waitforsql/ActionWaitForSqlResultRowsTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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.workflow.actions.waitforsql;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.util.UUID;
+import org.apache.hop.core.DbCache;
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.Result;
+import org.apache.hop.core.RowMetaAndData;
+import org.apache.hop.core.database.Database;
+import org.apache.hop.core.database.DatabaseMeta;
+import org.apache.hop.core.database.DatabasePluginType;
+import org.apache.hop.core.logging.LoggingObject;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.databases.h2.H2DatabaseMeta;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.workflow.WorkflowMeta;
+import 
org.apache.hop.workflow.actions.waitforsql.ActionWaitForSql.SuccessCondition;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Reproduction for issue #2483 - "Wait for SQL: Passing date row results to 
pipeline".
+ *
+ * <p>With "Add rows to result" the action reads its values with the metadata 
of the live result set
+ * but describes them with a second, independent derivation ({@code 
Database.getQueryFields}). The
+ * two are married in {@code new RowMetaAndData(rowMeta, objects)} without any 
check that they
+ * agree. When they don't, the action hands downstream a row whose {@link 
IRowMeta} does not
+ * describe its own values. Nothing notices at that point; it surfaces much 
later, when the
+ * execution information platform serializes the row, as
+ *
+ * <pre>
+ * SAMPLEDATE Timestamp : There was a data type error: the data type of 
java.lang.String
+ * object ['2023-02-27T13:00:00.000'] does not correspond to value meta 
[Timestamp]
+ * (through reference chain: 
org.apache.hop.execution.ExecutionData["rowsBinaryGzipBase64Encoded"])
+ * </pre>
+ *
+ * <p>In the reporter's case the two derivations diverge inside the SQL Server 
driver, which is not
+ * something a unit test can force. This test drives the divergence through 
the other way in, which
+ * is driver independent: {@code getQueryFields} is served from the process 
wide {@link DbCache},
+ * which is keyed on nothing but the connection name and the SQL text. Two Hop 
projects or
+ * environments that both define a connection called "warehouse" share that 
key, so the second run
+ * gets the first run's column types. Hop only invalidates that cache when it 
runs the DDL itself
+ * (see {@code Database.execStatement}), which does not help across databases.
+ */
+class ActionWaitForSqlResultRowsTest {
+
+  private static final String CONNECTION_NAME = "issue-2483";
+  private static final String CUSTOM_SQL = "SELECT ID, SAMPLEDATE FROM SAMPLE";
+
+  private Variables variables;
+  private MemoryMetadataProvider metadataProvider;
+  private WorkflowMeta workflowMeta;
+
+  @BeforeAll
+  static void initHop() throws Exception {
+    HopClientEnvironment.init();
+    
DatabasePluginType.getInstance().registerClassPathPlugin(H2DatabaseMeta.class);
+  }
+
+  @BeforeEach
+  void setUp() {
+    DbCache.clearAll();
+    variables = new Variables();
+    metadataProvider = new MemoryMetadataProvider();
+    workflowMeta = new WorkflowMeta();
+    workflowMeta.setMetadataProvider(metadataProvider);
+  }
+
+  @AfterEach
+  void tearDown() {
+    DbCache.clearAll();
+  }
+
+  @Test
+  void resultRowMetadataDescribesTheValuesThatWereActuallyRead() throws 
Exception {
+    // Two databases, one connection name. Think "dev" and "prod", or two 
projects that both call
+    // their connection "warehouse", running in the same Hop GUI or Hop Server 
process.
+    //
+    DatabaseMeta timestampDb = connectionTo("mem:issue2483_ts_" + 
UUID.randomUUID());
+    DatabaseMeta varcharDb = connectionTo("mem:issue2483_vc_" + 
UUID.randomUUID());
+
+    // All DDL up front: Database.execStatement clears the DbCache for the 
connection name on
+    // CREATE/ALTER/DROP TABLE, so doing this later would hide the effect we 
are after.
+    //
+    execute(
+        timestampDb,
+        "CREATE TABLE SAMPLE(ID INT, SAMPLEDATE TIMESTAMP)",
+        "INSERT INTO SAMPLE VALUES(1, TIMESTAMP '2023-02-27 13:00:00')");
+    execute(
+        varcharDb,
+        "CREATE TABLE SAMPLE(ID INT, SAMPLEDATE VARCHAR(30))",
+        "INSERT INTO SAMPLE VALUES(1, '2023-02-27T13:00:00.000')");
+
+    // Run against the database where SAMPLEDATE is a TIMESTAMP. This is the 
run that fills the
+    // DbCache entry for (connection name, SQL).
+    //
+    RowMetaAndData timestampRow = runActionAgainst(timestampDb);
+    assertEquals(
+        "Timestamp",
+        timestampRow.getRowMeta().getValueMeta(1).getTypeDesc(),
+        "sanity check: an H2 TIMESTAMP column is read as a Hop Timestamp");
+
+    // Same action, same connection name, same SQL, other database.
+    //
+    RowMetaAndData varcharRow = runActionAgainst(varcharDb);
+
+    assertEquals(
+        String.class,
+        varcharRow.getData()[1].getClass(),
+        "sanity check: the value the action read really is a String");
+    assertEquals(
+        "String",
+        varcharRow.getRowMeta().getValueMeta(1).getTypeDesc(),
+        "the result row metadata must describe the values the action actually 
read");
+
+    // What the execution information platform does with every result row of 
an action:
+    // ExecutionData.getRowsBinaryGzipBase64Encoded() -> IRowMeta.writeData(). 
A row whose metadata
+    // lies about its own values blows up here, far away from where it was 
built.
+    //
+    assertDoesNotThrow(() -> writeTheWayExecutionDataDoes(varcharRow));
+  }
+
+  private DatabaseMeta connectionTo(String database) {
+    // DB_CLOSE_DELAY=-1 keeps the in-memory database alive between connections
+    DatabaseMeta databaseMeta =
+        new DatabaseMeta(
+            CONNECTION_NAME, "H2", "Native", "", database + 
";DB_CLOSE_DELAY=-1", "", "", "");
+    databaseMeta.setSupportsTimestampDataType(true);
+    return databaseMeta;
+  }
+
+  private RowMetaAndData runActionAgainst(DatabaseMeta databaseMeta) throws 
Exception {
+    metadataProvider.getSerializer(DatabaseMeta.class).save(databaseMeta);
+
+    ActionWaitForSql action = new ActionWaitForSql("wait for sql");
+    action.setParentWorkflowMeta(workflowMeta);
+    action.setConnection(CONNECTION_NAME);
+    action.setCustomSqlEnabled(true);
+    action.setCustomSql(CUSTOM_SQL);
+    action.setAddRowsResult(true);
+    action.setSuccessCondition(SuccessCondition.ROWS_COUNT_GREATER);
+
+    Result result = new Result();
+    assertTrue(
+        action.sqlDataOK(result, 0, null, null, CUSTOM_SQL),
+        "the action should see the single row we inserted");
+    assertEquals(1, result.getRows().size());
+    return result.getRows().get(0);
+  }
+
+  private void writeTheWayExecutionDataDoes(RowMetaAndData row) throws 
Exception {
+    try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        DataOutputStream dos = new DataOutputStream(baos)) {
+      row.getRowMeta().writeMeta(dos);
+      row.getRowMeta().writeData(dos, row.getData());
+    }
+  }
+
+  private void execute(DatabaseMeta databaseMeta, String... statements) throws 
Exception {
+    try (Database db =
+        new Database(
+            new LoggingObject("ActionWaitForSqlResultRowsTest"), variables, 
databaseMeta)) {
+      db.connect();
+      for (String statement : statements) {
+        db.execStatement(statement);
+      }
+    }
+  }
+}

Reply via email to