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 51fd4b68d7 Fixes #8531 : Two integration tests assert behaviour the
product never had (#8535)
51fd4b68d7 is described below
commit 51fd4b68d7b937f389f74449c412984179dbaf82
Author: Bart Maertens <[email protected]>
AuthorDate: Tue Sep 22 20:43:24 2026 +0200
Fixes #8531 : Two integration tests assert behaviour the product never had
(#8535)
---
.../database/validation/ColumnValueValidator.java | 32 +++++----
.../validation/ColumnValueValidatorTest.java | 26 +++++++
.../lint/main-0001-lint-check-db-connections.hwf | 83 ++++++++++++++--------
.../main/java/org/apache/hop/lint/HopLinter.java | 62 ++++++++++++----
.../hop/lint/PipelineVerifyLintExtension.java | 29 +++++---
.../hop/lint/LintSuppressionInEditorTest.java | 47 ++++++++++++
.../hop/uuid/UuidColumnValueValidationTest.java | 71 ++++++++++++++++++
7 files changed, 284 insertions(+), 66 deletions(-)
diff --git
a/core/src/main/java/org/apache/hop/core/database/validation/ColumnValueValidator.java
b/core/src/main/java/org/apache/hop/core/database/validation/ColumnValueValidator.java
index aac89ff95d..88e2e36d2b 100644
---
a/core/src/main/java/org/apache/hop/core/database/validation/ColumnValueValidator.java
+++
b/core/src/main/java/org/apache/hop/core/database/validation/ColumnValueValidator.java
@@ -87,18 +87,26 @@ public final class ColumnValueValidator {
converted = spec.getTargetValueMeta().convertData(streamMeta, value);
workingMeta = spec.getTargetValueMeta();
} catch (HopValueException e) {
- errors.add(
- error(
- field,
- spec,
- ColumnValueErrorCode.CONVERSION,
- BaseMessages.getString(
- PKG,
- "ColumnValueValidator.Conversion",
- spec.getColumnName(),
- typeLabel(spec),
- Const.NVL(e.getMessage(), ""),
- preview(streamMeta, value, omitValues))));
+ // A uuid or json column is read as its own value type, so a value
that is not one fails
+ // to convert before the check that knows what is wrong with it can
run. Those checks go
+ // first here: they say what is wrong with the value, where the
converter can only report
+ // that the target type refused it, in whatever words the driver or
parser used.
+ checkUuid(spec, field, streamMeta, value, omitValues, errors);
+ checkJson(spec, field, streamMeta, value, omitValues, errors);
+ if (errors.isEmpty()) {
+ errors.add(
+ error(
+ field,
+ spec,
+ ColumnValueErrorCode.CONVERSION,
+ BaseMessages.getString(
+ PKG,
+ "ColumnValueValidator.Conversion",
+ spec.getColumnName(),
+ typeLabel(spec),
+ Const.NVL(e.getMessage(), ""),
+ preview(streamMeta, value, omitValues))));
+ }
return errors;
}
}
diff --git
a/core/src/test/java/org/apache/hop/core/database/validation/ColumnValueValidatorTest.java
b/core/src/test/java/org/apache/hop/core/database/validation/ColumnValueValidatorTest.java
index 3254b21b60..83834e02a7 100644
---
a/core/src/test/java/org/apache/hop/core/database/validation/ColumnValueValidatorTest.java
+++
b/core/src/test/java/org/apache/hop/core/database/validation/ColumnValueValidatorTest.java
@@ -27,6 +27,7 @@ import org.apache.hop.core.HopClientEnvironment;
import org.apache.hop.core.row.IValueMeta;
import org.apache.hop.core.row.value.ValueMetaBigNumber;
import org.apache.hop.core.row.value.ValueMetaInteger;
+import org.apache.hop.core.row.value.ValueMetaJson;
import org.apache.hop.core.row.value.ValueMetaString;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -166,6 +167,31 @@ class ColumnValueValidatorTest {
ColumnValueValidator.validate(json, "payload", meta, "{",
false).get(0).code());
}
+ /**
+ * What PostgreSQL actually hands the validator. A jsonb column is read as
{@link ValueMetaJson},
+ * not as a string, so an invalid document fails to convert to the target
type before the JSON
+ * check ever sees it. Reported as a conversion failure it carried the
parser's wording instead of
+ * naming the fault, and INVALID_JSON was unreachable on every database that
has the type.
+ */
+ @Test
+ void invalidJsonOnAColumnReadAsJson() {
+ ColumnValueConstraints json = new ColumnValueConstraints();
+ json.setColumnName("payload");
+ json.setJson(true);
+ json.setHopType(IValueMeta.TYPE_JSON);
+ json.setTargetValueMeta(new ValueMetaJson("payload"));
+ ValueMetaString streamMeta = new ValueMetaString("payload");
+
+ assertTrue(
+ ColumnValueValidator.validate(json, "payload", streamMeta,
"{\"a\":1}", false).isEmpty());
+
+ List<ColumnValueError> errors =
+ ColumnValueValidator.validate(json, "payload", streamMeta, "{", false);
+ assertEquals(1, errors.size());
+ assertEquals(ColumnValueErrorCode.INVALID_JSON, errors.get(0).code());
+ assertEquals("column 'payload': invalid JSON; value='{'",
errors.get(0).message());
+ }
+
@Test
void omitValuesHidesFailedData() {
ColumnValueConstraints spec = varchar("name", 1);
diff --git a/integration-tests/lint/main-0001-lint-check-db-connections.hwf
b/integration-tests/lint/main-0001-lint-check-db-connections.hwf
index 9dd425f8d2..ceb9973eae 100644
--- a/integration-tests/lint/main-0001-lint-check-db-connections.hwf
+++ b/integration-tests/lint/main-0001-lint-check-db-connections.hwf
@@ -20,7 +20,7 @@ limitations under the License.
<workflow>
<name>main-0001-lint-check-db-connections</name>
<name_sync_with_filename>Y</name_sync_with_filename>
- <description>Issue #8295: the linter must not report
CONNECTION_DOES_NOT_EXIST for connections that are in the project
metadata.</description>
+ <description>Issue #8295: the linter must not report a connection that is in
the project metadata as missing.</description>
<extended_description/>
<workflow_version/>
<created_user>-</created_user>
@@ -50,7 +50,7 @@ limitations under the License.
</action>
<action>
<name>Lint the Check DB connections workflows</name>
- <description>Runs hop lint over two subject workflows and checks the
CONNECTION_DOES_NOT_EXIST findings.</description>
+ <description>Runs hop lint over two subject workflows and checks the
missing-connection findings.</description>
<type>SHELL</type>
<attributes/>
<filename/>
@@ -67,10 +67,15 @@ limitations under the License.
<script>#!/bin/bash
# Issue #8295: "Invalid linter warnings in action Check DB Connections".
#
-# The linter reported CONNECTION_DOES_NOT_EXIST for database connections that
are present in the
-# project metadata. The checker behind that rule
(ReferencedDatabaseConnectionChecker) asks the
-# metadata serializer whether the connection exists, and used to report "does
not exist" when that
-# lookup *failed* as well as when it answered no. A failed lookup is not
evidence of absence.
+# The linter reported "does not exist" for database connections that are
present in the project
+# metadata. The checker behind it (ReferencedDatabaseConnectionChecker) asks
the metadata
+# serializer whether the connection exists, and used to report "does not
exist" when that lookup
+# *failed* as well as when it answered no. A failed lookup is not evidence of
absence.
+#
+# The assertions read the messages in the report, not the checker's error
codes. A remark from
+# Hop's own verify is reported under the rule that covers it - HOP-CHECK in
the core pack - so
+# CONNECTION_DOES_NOT_EXIST never reaches the JSON. The messages are what the
three outcomes the
+# checker distinguishes look like from outside: absent, present, and never
looked up at all.
#
# Note on ${} below: Hop resolves its own variables in this script before
running it, so only Hop
# variables are written with braces. Shell variables are written without them.
@@ -79,19 +84,13 @@ set -u
PROJECT_DIR="${PROJECT_HOME}"
-# Finding the hop CLI: run-tests.sh sets HOP_LOCATION but does not export it,
so it does not reach
-# this shell - printenv rather than $HOP_LOCATION, which would abort under
"set -u". The test image
-# puts the client on the PATH, which is what normally answers here.
-HOP_CLI="$(printenv HOP_LOCATION 2>/dev/null || true)"
-if [ -n "$HOP_CLI" ] && [ -x "$HOP_CLI/hop" ]; then
- HOP_CLI="$HOP_CLI/hop"
-elif command -v hop > /dev/null 2>/dev/null; then
- HOP_CLI="$(command -v hop)"
-elif [ -x /opt/hop/hop ]; then
- HOP_CLI=/opt/hop/hop
-else
- echo "FAIL: no hop CLI found (HOP_LOCATION is not set, hop is not on the
PATH, and there is"
- echo " nothing at /opt/hop/hop). The linter cannot be tested without
it."
+# Finding the hop CLI: hop-run.sh and hop-gui.sh both start the JVM from the
Hop installation, so
+# ${user.dir} is the install folder - /opt/hop in the test image, the client
folder when this
+# workflow is run from Hop GUI. Every system property is a Hop variable
(Variables.initializeFrom),
+# so Hop substitutes it before this script runs.
+HOP_CLI="${user.dir}/hop"
+if [ ! -x "$HOP_CLI" ]; then
+ echo "FAIL: no hop CLI at $HOP_CLI. The linter cannot be tested without it."
exit 1
fi
echo "Using hop CLI: $HOP_CLI"
@@ -100,7 +99,10 @@ echo "Using hop CLI: $HOP_CLI"
# points at, exactly like the rest of this test suite.
export HOP_CONFIG_FOLDER="$PROJECT_DIR"
-RULE=CONNECTION_DOES_NOT_EXIST
+# Fragments of the checker's messages, as
engine/src/main/resources/org/apache/hop/metadata/
+# validation/messages/messages_en_US.properties writes them.
+MISSING_MSG="does not exist"
+UNVERIFIED_MSG="could not be checked"
REPORT_DIR="$PROJECT_DIR/output"
mkdir -p "$REPORT_DIR"
@@ -126,23 +128,34 @@ run_lint() {
return 0
}
-count_rule() {
- grep -c "$RULE" "$1" 2>/dev/null || true
+# How many findings say $1. grep -c exits 1 on no match, which "set -u" would
not mind but the
+# caller would read as a failed run, so a count of zero is printed rather than
raised.
+count_message() {
+ grep -c "$1" "$2" 2>/dev/null || true
}
echo "=== Subject 1: every referenced connection exists (OPS, Vault, CRM) ==="
EXISTING_REPORT="$REPORT_DIR/lint-existing-connections.json"
if run_lint "$PROJECT_DIR/subject/existing-connections.hwf"
"$EXISTING_REPORT"; then
- found=$(count_rule "$EXISTING_REPORT")
+ found=$(count_message "$MISSING_MSG" "$EXISTING_REPORT")
+ unverified=$(count_message "$UNVERIFIED_MSG" "$EXISTING_REPORT")
if [ "$found" -ne 0 ]; then
- echo "FAIL: expected no $RULE findings, got $found."
+ echo "FAIL: expected no missing-connection findings, got $found."
echo " OPS, Vault and CRM are all present in
$PROJECT_DIR/metadata/rdbms."
echo "----- report -----"
cat "$EXISTING_REPORT"
echo "------------------"
failures=$((failures + 1))
+ elif [ "$unverified" -ne 0 ]; then
+ # A clean report proves nothing if the lookup never answered. That is the
state #8295 turned
+ # into a warning on every connection in the project, so it is worth
failing on separately.
+ echo "FAIL: the connections could not be looked up, so this subject proves
nothing."
+ echo "----- report -----"
+ cat "$EXISTING_REPORT"
+ echo "------------------"
+ failures=$((failures + 1))
else
- echo "OK: no $RULE findings for connections that exist."
+ echo "OK: connections that exist are not reported as missing."
fi
else
failures=$((failures + 1))
@@ -151,15 +164,25 @@ fi
echo "=== Subject 2: the referenced connection really is absent
(NoSuchConnection) ==="
MISSING_REPORT="$REPORT_DIR/lint-missing-connection.json"
if run_lint "$PROJECT_DIR/subject/missing-connection.hwf" "$MISSING_REPORT";
then
- found=$(count_rule "$MISSING_REPORT")
+ found=$(count_message "$MISSING_MSG" "$MISSING_REPORT")
if [ "$found" -ne 1 ]; then
- echo "FAIL: expected exactly one $RULE finding, got $found."
+ echo "FAIL: expected exactly one missing-connection finding, got $found."
+ echo "----- report -----"
+ cat "$MISSING_REPORT"
+ echo "------------------"
+ failures=$((failures + 1))
+ elif ! grep -q "'NoSuchConnection'.*$MISSING_MSG" "$MISSING_REPORT"; then
+ echo "FAIL: the missing-connection finding does not name NoSuchConnection."
echo "----- report -----"
cat "$MISSING_REPORT"
echo "------------------"
failures=$((failures + 1))
- elif ! grep -q "NoSuchConnection" "$MISSING_REPORT"; then
- echo "FAIL: the $RULE finding does not name NoSuchConnection."
+ elif ! grep -q '"info" : 0' "$MISSING_REPORT"; then
+ # An absent connection is a warning; only a lookup that failed reports at
info. This subject
+ # holds one action, so an info finding here is that finding downgraded -
and a finding below
+ # warning would not reach a build that fails on warnings. Reading the
severity off the summary
+ # rather than off the finding keeps this from breaking when the report's
field order changes.
+ echo "FAIL: the missing connection is reported below warning level."
echo "----- report -----"
cat "$MISSING_REPORT"
echo "------------------"
@@ -192,7 +215,7 @@ exit 0
<type>ABORT</type>
<attributes/>
<always_log_rows>N</always_log_rows>
- <message>The Hop linter reported incorrect CONNECTION_DOES_NOT_EXIST
findings. See issue #8295.</message>
+ <message>The Hop linter reported incorrect missing-connection findings.
See issue #8295.</message>
<parallel>N</parallel>
<xloc>560</xloc>
<yloc>208</yloc>
diff --git a/plugins/misc/lint/src/main/java/org/apache/hop/lint/HopLinter.java
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/HopLinter.java
index 3c85a2bc7b..f1e9d5098a 100644
--- a/plugins/misc/lint/src/main/java/org/apache/hop/lint/HopLinter.java
+++ b/plugins/misc/lint/src/main/java/org/apache/hop/lint/HopLinter.java
@@ -130,6 +130,31 @@ public class HopLinter {
}
}
+ /**
+ * The rules in force for Hop's own verify remarks.
+ *
+ * <p>Every path that reports a native remark classifies it through this. A
remark reported under
+ * one rule id from the CLI and another from the canvas cannot be suppressed
once for both: the
+ * project writes the id it was shown, and {@link LintPolicy#isSuppressed}
matches on it.
+ *
+ * <p>It is for native remarks alone. The blanket rule in the core pack
names no plugin and no
+ * message, so it matches every {@link ICheckResult} put in front of it —
including a lint finding
+ * that {@code toCheckResults} has turned into one, whose own rule id it
would overwrite.
+ */
+ private NativeCheckClassifier nativeClassifier() {
+ ensureEffectiveRuleSet();
+ return new NativeCheckClassifier(effectiveRuleSet.getNativeVerifyRules());
+ }
+
+ /**
+ * Hop's own verify remarks as lint findings, reported under the rules in
force.
+ *
+ * @param remarks Hop's own remarks, and only those
+ */
+ public List<LintResult> fromNativeRemarks(List<ICheckResult> remarks, String
fileName) {
+ return LintCheckResultAdapter.fromCheckResults(remarks, fileName,
nativeClassifier());
+ }
+
/**
* Run the linter on a Hop project
*
@@ -483,13 +508,17 @@ public class HopLinter {
pipelineMeta.checkTransforms(
remarks, false, new NullProgressMonitor(), variables,
metadataProvider));
+ List<LintResult> results = new ArrayList<>(fromNativeRemarks(remarks,
fileName));
+
if (shouldIncludeLintInPipelineVerify()) {
- remarks.addAll(
- LintCheckResultAdapter.toCheckResults(
- runPolicyRules(pipelineMeta, fileName), pipelineMeta));
+ results.addAll(
+ LintCheckResultAdapter.fromCheckResults(
+ LintCheckResultAdapter.toCheckResults(
+ runPolicyRules(pipelineMeta, fileName), pipelineMeta),
+ fileName));
}
- return applyPolicy(LintCheckResultAdapter.fromCheckResults(remarks,
fileName), fileName);
+ return applyPolicy(results, fileName);
}
/** Compute workflow lint results the same way as workflow verify plus
optional policy rules. */
@@ -508,13 +537,17 @@ public class HopLinter {
workflowMeta.checkActions(
remarks, false, new NullProgressMonitor(), variables,
metadataProvider));
+ List<LintResult> results = new ArrayList<>(fromNativeRemarks(remarks,
fileName));
+
if (shouldIncludeLintInWorkflowVerify()) {
- remarks.addAll(
- WorkflowCheckResultAdapter.toCheckResults(
- runPolicyRules(workflowMeta, fileName), workflowMeta));
+ results.addAll(
+ LintCheckResultAdapter.fromCheckResults(
+ WorkflowCheckResultAdapter.toCheckResults(
+ runPolicyRules(workflowMeta, fileName), workflowMeta),
+ fileName));
}
- return applyPolicy(LintCheckResultAdapter.fromCheckResults(remarks,
fileName), fileName);
+ return applyPolicy(results, fileName);
}
private boolean shouldIncludeLintInWorkflowVerify() {
@@ -584,14 +617,9 @@ public class HopLinter {
throws HopException {
List<LintResult> results = new ArrayList<>(policyResults);
if (shouldIncludeNativeChecks() && hopObject != null) {
- ensureEffectiveRuleSet();
results.addAll(
HopNativeCheckRunner.runNativeChecks(
- hopObject,
- fileName,
- variables,
- metadataProvider,
- new
NativeCheckClassifier(effectiveRuleSet.getNativeVerifyRules())));
+ hopObject, fileName, variables, metadataProvider,
nativeClassifier()));
}
// Suppressions are applied last, so they cover Hop's native remarks as
well as policy
// findings — a team accepting something should not have to care which
produced it.
@@ -668,9 +696,13 @@ public class HopLinter {
// Resolved once: the project root is the same for every remark, and
finding it walks the
// filesystem.
Path projectRoot = projectRootFor(fileName);
+ // Classified like anywhere else: a suppression names the rule id the
person was shown, so
+ // reading the raw error code here would leave the finding on the canvas
after they accepted
+ // it in the report, or the other way round.
+ NativeCheckClassifier classifier = nativeClassifier();
remarks.removeIf(
remark -> {
- LintResult result = LintCheckResultAdapter.fromCheckResult(remark,
fileName);
+ LintResult result = LintCheckResultAdapter.fromCheckResult(remark,
fileName, classifier);
return result != null && policy.isSuppressed(result, projectRoot);
});
}
diff --git
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/PipelineVerifyLintExtension.java
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/PipelineVerifyLintExtension.java
index 71effe68b1..35ad6e23ef 100644
---
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/PipelineVerifyLintExtension.java
+++
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/PipelineVerifyLintExtension.java
@@ -16,7 +16,9 @@
*/
package org.apache.hop.lint;
+import java.util.ArrayList;
import java.util.List;
+import org.apache.hop.core.ICheckResult;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.extension.ExtensionPoint;
import org.apache.hop.core.extension.IExtensionPoint;
@@ -63,18 +65,27 @@ public class PipelineVerifyLintExtension implements
IExtensionPoint<CheckTransfo
return;
}
- List<LintResult> policyResults =
- linter.applyPolicy(linter.runPolicyRules(pipelineMeta, fileName),
fileName);
- extension
- .getRemarks()
- .addAll(LintCheckResultAdapter.toCheckResults(policyResults,
pipelineMeta));
-
// Hop collected its own remarks before this point, so they have passed
no suppression yet.
+ // The policy findings added below have: applyPolicy suppresses them as
it builds them.
linter.removeSuppressed(extension.getRemarks(), fileName);
- List<LintResult> verifyViewResults =
- LintResultDeduplicator.deduplicate(
- LintCheckResultAdapter.fromCheckResults(extension.getRemarks(),
fileName));
+ // Read while the list still holds Hop's own remarks alone. The blanket
native rule names no
+ // plugin and no message, so it matches anything put in front of it -
including a policy
+ // finding turned into a remark, whose own rule id it would overwrite.
+ List<LintResult> results =
+ new ArrayList<>(linter.fromNativeRemarks(extension.getRemarks(),
fileName));
+
+ List<LintResult> policyResults =
+ linter.applyPolicy(linter.runPolicyRules(pipelineMeta, fileName),
fileName);
+ List<ICheckResult> policyRemarks =
+ LintCheckResultAdapter.toCheckResults(policyResults, pipelineMeta);
+ extension.getRemarks().addAll(policyRemarks);
+
+ // Through the same conversion as before. This view has always reported
a policy finding as
+ // Hop's own verify output renders it, and reporting it differently here
would leave the
+ // Problems bar disagreeing with the background lint about the same file.
+ results.addAll(LintCheckResultAdapter.fromCheckResults(policyRemarks,
fileName));
+ List<LintResult> verifyViewResults =
LintResultDeduplicator.deduplicate(results);
LintResultsManager.getInstance().updateResultsForFile(fileName,
verifyViewResults);
LintProblemsBarManager.getInstance().updateProblemsBar(fileName);
diff --git
a/plugins/misc/lint/src/test/java/org/apache/hop/lint/LintSuppressionInEditorTest.java
b/plugins/misc/lint/src/test/java/org/apache/hop/lint/LintSuppressionInEditorTest.java
index b7e5629d0d..c04a348a3a 100644
---
a/plugins/misc/lint/src/test/java/org/apache/hop/lint/LintSuppressionInEditorTest.java
+++
b/plugins/misc/lint/src/test/java/org/apache/hop/lint/LintSuppressionInEditorTest.java
@@ -144,6 +144,53 @@ public class LintSuppressionInEditorTest {
"only the named rule should have been silenced: " + after);
}
+ /**
+ * The rules the core pack states about Hop's own remarks have to reach the
canvas.
+ *
+ * <p>They did not: only the command line classified native remarks, so a
project that switched
+ * {@code HOP-CHECK} off saw its build go quiet while every badge stayed on
the canvas, and a
+ * severity the pack had capped was still reported as the transform wrote it.
+ */
+ @Test
+ public void disablingTheNativeRuleSilencesTheEditorToo() throws Exception {
+ assertTrue(countOfRule(lintAsEditor(), "HOP-CHECK") > 0, "no native
findings to switch off");
+
+ writeProjectConfig(
+ """
+ rules:
+ HOP-CHECK:
+ enabled: false
+ """);
+
+ List<LintResult> results = lintAsEditor();
+
+ assertEquals(
+ 0, countOfRule(results, "HOP-CHECK"), "Hop's own remarks should be
gone: " + results);
+ assertTrue(
+ results.stream().anyMatch(r -> "TRANS-002".equals(r.getRuleId())),
+ "switching off the native rule must leave the linter's own rules
alone: " + results);
+ }
+
+ /**
+ * The blanket native rule names no plugin and no message, so it matches
every check result put in
+ * front of it. The linter's own findings travel through {@code
ICheckResult} on this path, and
+ * classifying them along with Hop's remarks would rename every one of them
to {@code HOP-CHECK} -
+ * silently collapsing the rule ids a project writes its suppressions
against.
+ */
+ @Test
+ public void policyFindingsKeepTheirOwnRuleIdInTheEditor() throws Exception {
+ List<LintResult> results = lintAsEditor();
+
+ assertTrue(
+ results.stream()
+ .anyMatch(r -> "TRANS-002".equals(r.getRuleId()) && "Fonte
Sql".equals(sourceName(r))),
+ "the orphaned-transform finding on Fonte Sql lost its rule id: " +
results);
+ assertTrue(
+ results.stream()
+ .anyMatch(r -> "TRANS-002".equals(r.getRuleId()) && "Salva
S3".equals(sourceName(r))),
+ "the orphaned-transform finding on Salva S3 lost its rule id: " +
results);
+ }
+
private long countOfRule(List<LintResult> results, String ruleId) {
return results.stream().filter(r -> ruleId.equals(r.getRuleId())).count();
}
diff --git
a/plugins/valuetypes/uuid/src/test/java/org/apache/hop/uuid/UuidColumnValueValidationTest.java
b/plugins/valuetypes/uuid/src/test/java/org/apache/hop/uuid/UuidColumnValueValidationTest.java
new file mode 100644
index 0000000000..4da404cec3
--- /dev/null
+++
b/plugins/valuetypes/uuid/src/test/java/org/apache/hop/uuid/UuidColumnValueValidationTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.uuid;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.database.validation.ColumnValueConstraints;
+import org.apache.hop.core.database.validation.ColumnValueError;
+import org.apache.hop.core.database.validation.ColumnValueErrorCode;
+import org.apache.hop.core.database.validation.ColumnValueValidator;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Database value validation against a column that Hop reads as {@link
ValueMetaUuid}, which is what
+ * a PostgreSQL uuid column gives it. The validator's own tests can only use a
string target, since
+ * the UUID value type lives here.
+ */
+class UuidColumnValueValidationTest {
+
+ @BeforeAll
+ static void init() throws Exception {
+ HopClientEnvironment.init();
+ }
+
+ /**
+ * A value that is not a UUID fails to convert to the target type before the
UUID check sees it.
+ * Reported as a conversion failure it carried the converter's wording
rather than naming the
+ * fault, and INVALID_UUID was unreachable on every database that has the
type.
+ */
+ @Test
+ void invalidUuidOnAColumnReadAsUuid() {
+ ColumnValueConstraints spec = new ColumnValueConstraints();
+ spec.setColumnName("uid");
+ spec.setUuid(true);
+ spec.setHopType(IValueMeta.TYPE_UUID);
+ spec.setTargetValueMeta(new ValueMetaUuid("uid"));
+ ValueMetaString streamMeta = new ValueMetaString("uid");
+
+ assertTrue(
+ ColumnValueValidator.validate(
+ spec, "uid", streamMeta,
"550e8400-e29b-41d4-a716-446655440000", false)
+ .isEmpty());
+
+ List<ColumnValueError> errors =
+ ColumnValueValidator.validate(spec, "uid", streamMeta, "not-a-uuid",
false);
+ assertEquals(1, errors.size());
+ assertEquals(ColumnValueErrorCode.INVALID_UUID, errors.get(0).code());
+ assertEquals("column 'uid': invalid UUID; value='not-a-uuid'",
errors.get(0).message());
+ }
+}