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 4c836794b5 Issue #2955 : Warn when a referenced database connection 
does not exist (#8245)
4c836794b5 is described below

commit 4c836794b5d4ccd7f04c006b7b51fbdfeadfe7f1
Author: Matt Casters <[email protected]>
AuthorDate: Thu Sep 3 15:55:29 2026 +0200

    Issue #2955 : Warn when a referenced database connection does not exist 
(#8245)
    
    Warn on save and Verify when a transform or action names a relational
    database connection that is not in project metadata. Saving is never
    blocked. Names that still contain a variable after resolving the current
    environment are skipped, and the check does not open a JDBC connection.
---
 .../java/org/apache/hop/core/util/StringUtil.java  |  18 ++
 .../metadata/util/HopMetadataPropertyWalker.java   | 159 ++++++++++++++
 .../org/apache/hop/core/util/StringUtilTest.java   |  12 ++
 .../util/HopMetadataPropertyWalkerTest.java        | 124 +++++++++++
 ...n-validate-database-connections-when-saving.png | Bin 0 -> 118099 bytes
 ...database-connections-on-save-warning-dialog.png | Bin 0 -> 62130 bytes
 .../pages/hop-gui/perspective-configuration.adoc   |   7 +
 .../pages/metadata-types/rdbms-connection.adoc     |   4 +
 .../ROOT/pages/pipeline/create-pipeline.adoc       |   4 +
 .../ROOT/pages/workflow/create-workflow.adoc       |   4 +
 .../ReferencedDatabaseConnectionChecker.java       | 208 ++++++++++++++++++
 .../java/org/apache/hop/pipeline/PipelineMeta.java |   7 +
 .../java/org/apache/hop/workflow/WorkflowMeta.java |   4 +
 .../validation/messages/messages_en_US.properties  |  20 ++
 .../ReferencedDatabaseConnectionCheckerTest.java   | 235 +++++++++++++++++++++
 .../actions/waitforsql/ActionWaitForSql.java       |  13 +-
 .../actions/mssqlbulkload/ActionMssqlBulkLoad.java |  13 +-
 .../actions/mysqlbulkload/ActionMysqlBulkLoad.java |  13 +-
 .../actions/snowflake/WarehouseManager.java        |   5 +-
 .../sqlfileoutput/SQLFileOutputMeta.java           |  11 +-
 .../file/config/FileValidationConfigPlugin.java    | 148 +++++++++++++
 .../hopgui/file/pipeline/HopGuiPipelineGraph.java  |  11 +
 .../shared/ReferencedConnectionSaveValidator.java  | 117 ++++++++++
 .../hopgui/file/workflow/HopGuiWorkflowGraph.java  |  11 +
 .../file/config/messages/messages_en_US.properties |  23 ++
 25 files changed, 1158 insertions(+), 13 deletions(-)

diff --git a/core/src/main/java/org/apache/hop/core/util/StringUtil.java 
b/core/src/main/java/org/apache/hop/core/util/StringUtil.java
index 0668ec52c5..c7d1c584a5 100644
--- a/core/src/main/java/org/apache/hop/core/util/StringUtil.java
+++ b/core/src/main/java/org/apache/hop/core/util/StringUtil.java
@@ -579,6 +579,24 @@ public class StringUtil {
         || variable.startsWith(HEX_OPEN) && variable.endsWith(HEX_CLOSE);
   }
 
+  /**
+   * Whether {@code value} still contains a Hop variable delimiter after 
substitution. Used to skip
+   * design-time checks that cannot be decided when a name or URL still holds 
{@code ${...}}, {@code
+   * %%...%%}, {@code $[...]} or {@code #{...}}.
+   *
+   * @param value the string to inspect, may be null
+   * @return true when a variable token is still present
+   */
+  public static boolean containsVariableToken(String value) {
+    if (value == null) {
+      return false;
+    }
+    return value.contains(UNIX_OPEN)
+        || value.contains(WINDOWS_OPEN)
+        || value.contains(HEX_OPEN)
+        || value.contains(RESOLVER_OPEN);
+  }
+
   /**
    * Calls the {@link String#toLowerCase()} method on the {@link String} 
returned by a call to
    * {@code obj.toString()}, guarding against {@link NullPointerException}s.
diff --git 
a/core/src/main/java/org/apache/hop/metadata/util/HopMetadataPropertyWalker.java
 
b/core/src/main/java/org/apache/hop/metadata/util/HopMetadataPropertyWalker.java
new file mode 100644
index 0000000000..699166be99
--- /dev/null
+++ 
b/core/src/main/java/org/apache/hop/metadata/util/HopMetadataPropertyWalker.java
@@ -0,0 +1,159 @@
+/*
+ * 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.metadata.util;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.metadata.api.HopMetadataPropertyType;
+
+/**
+ * Walks {@link HopMetadataProperty} fields on a metadata object, including 
nested objects and
+ * collections, and collects string values of a given {@link 
HopMetadataPropertyType}.
+ *
+ * <p>Failures to read a field are skipped. Cycles are broken. This is a 
design-time helper: it must
+ * not throw because of a broken plugin class.
+ */
+public final class HopMetadataPropertyWalker {
+
+  private static final int MAX_DEPTH = 8;
+
+  private HopMetadataPropertyWalker() {}
+
+  /**
+   * A string property found on a metadata object.
+   *
+   * @param type the annotated property type
+   * @param key the serialised key, or the field name when no key is set
+   * @param value the raw (unresolved) string value, never null
+   */
+  public record StringProperty(HopMetadataPropertyType type, String key, 
String value) {}
+
+  /**
+   * Collect every string field annotated with {@code type} under {@code root}.
+   *
+   * @param root the object to walk, may be null
+   * @param type the property type to collect
+   * @return the matching properties, possibly empty
+   */
+  public static List<StringProperty> collectStrings(Object root, 
HopMetadataPropertyType type) {
+    List<StringProperty> collected = new ArrayList<>();
+    if (root == null || type == null) {
+      return collected;
+    }
+    walk(
+        root,
+        type,
+        collected,
+        0,
+        java.util.Collections.newSetFromMap(new IdentityHashMap<Object, 
Boolean>()));
+    return collected;
+  }
+
+  private static void walk(
+      Object node,
+      HopMetadataPropertyType type,
+      List<StringProperty> collected,
+      int depth,
+      Set<Object> visited) {
+    if (node == null || depth > MAX_DEPTH || !isMetadataObject(node) || 
!visited.add(node)) {
+      return;
+    }
+    for (Field field : ReflectionUtil.findAllFields(node.getClass())) {
+      if (Modifier.isStatic(field.getModifiers())) {
+        continue;
+      }
+      HopMetadataProperty property = 
field.getAnnotation(HopMetadataProperty.class);
+      if (property == null) {
+        continue;
+      }
+      Object value = readField(field, node);
+      if (value == null) {
+        continue;
+      }
+      if (property.hopMetadataPropertyType() == type && value instanceof 
String stringValue) {
+        collected.add(new StringProperty(type, serialisedKey(property, field), 
stringValue));
+      }
+      descend(value, type, collected, depth, visited);
+    }
+  }
+
+  private static void descend(
+      Object value,
+      HopMetadataPropertyType type,
+      List<StringProperty> collected,
+      int depth,
+      Set<Object> visited) {
+    if (value instanceof Collection<?> collection) {
+      for (Object element : collection) {
+        walk(element, type, collected, depth + 1, visited);
+      }
+      return;
+    }
+    if (value instanceof Map<?, ?> map) {
+      for (Object element : map.values()) {
+        walk(element, type, collected, depth + 1, visited);
+      }
+      return;
+    }
+    if (value.getClass().isArray()) {
+      int length = Array.getLength(value);
+      for (int i = 0; i < length; i++) {
+        walk(Array.get(value, i), type, collected, depth + 1, visited);
+      }
+      return;
+    }
+    walk(value, type, collected, depth + 1, visited);
+  }
+
+  private static String serialisedKey(HopMetadataProperty property, Field 
field) {
+    if (property.key() != null && !property.key().isEmpty()) {
+      return property.key();
+    }
+    return field.getName();
+  }
+
+  /** Only descends into Hop's own metadata classes, never into JDK or 
third-party types. */
+  static boolean isMetadataObject(Object value) {
+    if (value == null) {
+      return false;
+    }
+    Class<?> type = value.getClass();
+    if (type.isPrimitive() || type.isEnum() || type.isArray()) {
+      return false;
+    }
+    Package pkg = type.getPackage();
+    return pkg != null && 
pkg.getName().toLowerCase(Locale.ROOT).startsWith("org.apache.hop");
+  }
+
+  private static Object readField(Field field, Object target) {
+    try {
+      field.setAccessible(true);
+      return field.get(target);
+    } catch (Exception e) {
+      return null;
+    }
+  }
+}
diff --git a/core/src/test/java/org/apache/hop/core/util/StringUtilTest.java 
b/core/src/test/java/org/apache/hop/core/util/StringUtilTest.java
index 41013a8a23..b030f117e7 100644
--- a/core/src/test/java/org/apache/hop/core/util/StringUtilTest.java
+++ b/core/src/test/java/org/apache/hop/core/util/StringUtilTest.java
@@ -162,6 +162,18 @@ class StringUtilTest {
     assertFalse(StringUtil.isVariable(null));
   }
 
+  @Test
+  void testContainsVariableToken() {
+    assertTrue(StringUtil.containsVariableToken("${CONNECTION}"));
+    assertTrue(StringUtil.containsVariableToken("db_${ENV}"));
+    assertTrue(StringUtil.containsVariableToken("%%WINDOWS%%"));
+    assertTrue(StringUtil.containsVariableToken("$[hex]"));
+    assertTrue(StringUtil.containsVariableToken("#{resolver}"));
+    assertFalse(StringUtil.containsVariableToken("sales-db"));
+    assertFalse(StringUtil.containsVariableToken(null));
+    assertFalse(StringUtil.containsVariableToken(""));
+  }
+
   @Test
   void testSafeToLowerCase() {
     assertNull(StringUtil.safeToLowerCase(null));
diff --git 
a/core/src/test/java/org/apache/hop/metadata/util/HopMetadataPropertyWalkerTest.java
 
b/core/src/test/java/org/apache/hop/metadata/util/HopMetadataPropertyWalkerTest.java
new file mode 100644
index 0000000000..f6a51c91a1
--- /dev/null
+++ 
b/core/src/test/java/org/apache/hop/metadata/util/HopMetadataPropertyWalkerTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.metadata.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.metadata.api.HopMetadataPropertyType;
+import org.apache.hop.metadata.util.HopMetadataPropertyWalker.StringProperty;
+import org.junit.jupiter.api.Test;
+
+class HopMetadataPropertyWalkerTest {
+
+  static class SimpleMeta {
+    @HopMetadataProperty(hopMetadataPropertyType = 
HopMetadataPropertyType.RDBMS_CONNECTION)
+    String connection = "warehouse";
+
+    @HopMetadataProperty(key = "sql")
+    String sql = "SELECT 1";
+
+    String unannotated = "ignored";
+  }
+
+  static class NestedItem {
+    @HopMetadataProperty(
+        key = "name",
+        hopMetadataPropertyType = HopMetadataPropertyType.RDBMS_CONNECTION)
+    String name;
+
+    NestedItem(String name) {
+      this.name = name;
+    }
+  }
+
+  static class NestedMeta {
+    @HopMetadataProperty(hopMetadataPropertyType = 
HopMetadataPropertyType.RDBMS_CONNECTION)
+    String connection = "primary";
+
+    @HopMetadataProperty
+    List<NestedItem> items = List.of(new NestedItem("second"), new 
NestedItem("third"));
+  }
+
+  static class TwoConnectionsMeta {
+    @HopMetadataProperty(
+        key = "referenceConnection",
+        hopMetadataPropertyType = HopMetadataPropertyType.RDBMS_CONNECTION)
+    String reference = "ref-db";
+
+    @HopMetadataProperty(
+        key = "compareConnection",
+        hopMetadataPropertyType = HopMetadataPropertyType.RDBMS_CONNECTION)
+    String compare = "cmp-db";
+  }
+
+  static class UnannotatedConnectionMeta {
+    @HopMetadataProperty(key = "connection")
+    String connection = "hidden";
+  }
+
+  @Test
+  void collectsAnnotatedConnectionStrings() {
+    List<StringProperty> found =
+        HopMetadataPropertyWalker.collectStrings(
+            new SimpleMeta(), HopMetadataPropertyType.RDBMS_CONNECTION);
+
+    assertEquals(1, found.size());
+    assertEquals("connection", found.get(0).key());
+    assertEquals("warehouse", found.get(0).value());
+  }
+
+  @Test
+  void descendsIntoNestedLists() {
+    List<StringProperty> found =
+        HopMetadataPropertyWalker.collectStrings(
+            new NestedMeta(), HopMetadataPropertyType.RDBMS_CONNECTION);
+
+    assertEquals(3, found.size());
+    assertEquals(
+        List.of("primary", "second", "third"), 
found.stream().map(StringProperty::value).toList());
+  }
+
+  @Test
+  void collectsTwoConnectionFieldsOnOneObject() {
+    List<StringProperty> found =
+        HopMetadataPropertyWalker.collectStrings(
+            new TwoConnectionsMeta(), 
HopMetadataPropertyType.RDBMS_CONNECTION);
+
+    assertEquals(2, found.size());
+    assertTrue(found.stream().anyMatch(p -> 
"referenceConnection".equals(p.key())));
+    assertTrue(found.stream().anyMatch(p -> 
"compareConnection".equals(p.key())));
+  }
+
+  @Test
+  void ignoresConnectionFieldsWithoutThePropertyType() {
+    List<StringProperty> found =
+        HopMetadataPropertyWalker.collectStrings(
+            new UnannotatedConnectionMeta(), 
HopMetadataPropertyType.RDBMS_CONNECTION);
+
+    assertTrue(found.isEmpty());
+  }
+
+  @Test
+  void nullRootYieldsNothing() {
+    assertTrue(
+        HopMetadataPropertyWalker.collectStrings(null, 
HopMetadataPropertyType.RDBMS_CONNECTION)
+            .isEmpty());
+  }
+}
diff --git 
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/configuration-perspective-file-validation-validate-database-connections-when-saving.png
 
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/configuration-perspective-file-validation-validate-database-connections-when-saving.png
new file mode 100644
index 0000000000..68426548e3
Binary files /dev/null and 
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/configuration-perspective-file-validation-validate-database-connections-when-saving.png
 differ
diff --git 
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/validate-database-connections-on-save-warning-dialog.png
 
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/validate-database-connections-on-save-warning-dialog.png
new file mode 100644
index 0000000000..5b8457d1f8
Binary files /dev/null and 
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/validate-database-connections-on-save-warning-dialog.png
 differ
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc
index b9c2504fe6..1809db1faa 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc
@@ -138,6 +138,13 @@ 
image::hop-gui/configuration-perspective-open-help-pages-in.png[Open help pages
 * xref:vfs/google-drive-vfs.adoc#_configuration[Google Drive] VFS 
configuration options.
 * xref:projects/index.adoc[Project] configuration options
 * Welcome Dialog: specify whether to show or hide the welcome dialog when Hop 
GUI starts.
+* **File validation**: when enabled (the default), saving a pipeline or 
workflow warns if a transform or action references a relational database 
connection that is not in the project metadata. You can still save. Connection 
names that still contain a variable such as `'${CONNECTION}'` after the current 
environment is applied are skipped, because the name cannot be decided at 
design time. The same check also runs when you Verify a pipeline or workflow. 
Hop does not try to open a JDBC conn [...]
++
+image::hop-gui/configuration-perspective-file-validation-validate-database-connections-when-saving.png[Validate
 database connections when saving,width="90%"]
++
+The warning lists the missing connections. *Yes* saves anyway, *No* cancels 
the save. Check *Don't run this check when saving* to turn the option off; you 
can turn it back on here.
++
+image::hop-gui/validate-database-connections-on-save-warning-dialog.png[Database
 connections save warning,width="90%"]
 
 === System Variables
 
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/rdbms-connection.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/rdbms-connection.adoc
index 65d0434959..65dc288261 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/rdbms-connection.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/rdbms-connection.adoc
@@ -30,6 +30,10 @@ Hop supports tens of relational databases out of the box. If 
your specific datab
 
 Check the list of xref:database/databases.adoc[databases] for more details.
 
+Transforms and actions store the *name* of a relational database connection. 
When you save or Verify a pipeline or workflow, Hop warns if that name is not 
in the project metadata. You can still save. If the name still contains a 
variable such as `'${CONNECTION}'` after the current environment is applied, 
the check is skipped because the name cannot be decided at design time. This 
does not open a JDBC connection; it only looks the name up in metadata. The 
warning dialog can turn the check [...]
+
+image::hop-gui/validate-database-connections-on-save-warning-dialog.png[Database
 connections save warning,width="90%"]
+
 
 == Related Plugins
 
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/create-pipeline.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/create-pipeline.adoc
index dedd4e9825..cb435efbf1 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/create-pipeline.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/create-pipeline.adoc
@@ -61,6 +61,10 @@ When you are finished with your pipeline, save it.
 This can be done via the File menu, the icons or using CTLR s or Command s.
 For new pipelines a file browser is displayed to navigate towards the location 
you want to store the file.
 
+By default Hop warns if a transform references a relational database 
connection that is not in the project metadata. You can still save. Connection 
names that still contain a variable such as `'${CONNECTION}'` are skipped. 
Check *Don't run this check when saving* on the dialog, or turn the option off 
under xref:hop-gui/perspective-configuration.adoc[Configuration] → Plugins → 
File validation.
+
+image::hop-gui/validate-database-connections-on-save-warning-dialog.png[Database
 connections save warning,width="90%"]
+
 == Add Transform to your pipelines
 
 Click anywhere in the pipeline canvas, the area where you'll see the image 
below.
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/workflow/create-workflow.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/workflow/create-workflow.adoc
index cef658ac67..65af6017fa 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/workflow/create-workflow.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/workflow/create-workflow.adoc
@@ -58,6 +58,10 @@ When you are finished with your workflow, save it.
 This can be done via the File menu, the icons or using CTLR s or Command s.
 For new workflows a file browser is displayed to navigate towards the location 
you want to store the file.
 
+By default Hop warns if an action references a relational database connection 
that is not in the project metadata. You can still save. Connection names that 
still contain a variable such as `'${CONNECTION}'` are skipped. Check *Don't 
run this check when saving* on the dialog, or turn the option off under 
xref:hop-gui/perspective-configuration.adoc[Configuration] → Plugins → File 
validation.
+
+image::hop-gui/validate-database-connections-on-save-warning-dialog.png[Database
 connections save warning,width="90%"]
+
 == Add Action to your workflow
 
 Add the following actions to your workflow and create the hops to connect them:
diff --git 
a/engine/src/main/java/org/apache/hop/metadata/validation/ReferencedDatabaseConnectionChecker.java
 
b/engine/src/main/java/org/apache/hop/metadata/validation/ReferencedDatabaseConnectionChecker.java
new file mode 100644
index 0000000000..2d451f3e01
--- /dev/null
+++ 
b/engine/src/main/java/org/apache/hop/metadata/validation/ReferencedDatabaseConnectionChecker.java
@@ -0,0 +1,208 @@
+/*
+ * 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.metadata.validation;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.CheckResult;
+import org.apache.hop.core.ICheckResult;
+import org.apache.hop.core.ICheckResultSource;
+import org.apache.hop.core.database.DatabaseMeta;
+import org.apache.hop.core.util.StringUtil;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.metadata.api.HopMetadataPropertyType;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.metadata.api.IHopMetadataSerializer;
+import org.apache.hop.metadata.util.HopMetadataPropertyWalker;
+import org.apache.hop.metadata.util.HopMetadataPropertyWalker.StringProperty;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.apache.hop.workflow.action.ActionMeta;
+
+/**
+ * Warns when a transform or action references a relational database 
connection that is not in
+ * project metadata.
+ *
+ * <p>This is an existence check only. It never opens a JDBC connection. Names 
that still contain a
+ * variable token after resolving the current {@link IVariables} are skipped, 
because the name
+ * cannot be decided at design time.
+ */
+public final class ReferencedDatabaseConnectionChecker {
+
+  public static final String ERROR_NOT_ASSIGNED = "CONNECTION_NOT_ASSIGNED";
+  public static final String ERROR_DOES_NOT_EXIST = 
"CONNECTION_DOES_NOT_EXIST";
+
+  private static final Class<?> PKG = 
ReferencedDatabaseConnectionChecker.class;
+
+  private ReferencedDatabaseConnectionChecker() {}
+
+  public static List<ICheckResult> checkPipeline(
+      PipelineMeta pipelineMeta, IVariables variables, IHopMetadataProvider 
metadataProvider) {
+    List<ICheckResult> remarks = new ArrayList<>();
+    if (pipelineMeta == null) {
+      return remarks;
+    }
+    for (TransformMeta transformMeta : pipelineMeta.getTransforms()) {
+      remarks.addAll(checkTransform(transformMeta, variables, 
metadataProvider));
+    }
+    return remarks;
+  }
+
+  public static List<ICheckResult> checkWorkflow(
+      WorkflowMeta workflowMeta, IVariables variables, IHopMetadataProvider 
metadataProvider) {
+    List<ICheckResult> remarks = new ArrayList<>();
+    if (workflowMeta == null) {
+      return remarks;
+    }
+    for (ActionMeta actionMeta : workflowMeta.getActions()) {
+      remarks.addAll(checkAction(actionMeta, variables, metadataProvider));
+    }
+    return remarks;
+  }
+
+  public static List<ICheckResult> checkTransform(
+      TransformMeta transformMeta, IVariables variables, IHopMetadataProvider 
metadataProvider) {
+    if (transformMeta == null || transformMeta.getTransform() == null) {
+      return List.of();
+    }
+    return checkObject(
+        transformMeta.getTransform(),
+        BaseMessages.getString(PKG, 
"ReferencedDatabaseConnectionChecker.Kind.Transform"),
+        transformMeta.getName(),
+        transformMeta,
+        variables,
+        metadataProvider);
+  }
+
+  public static List<ICheckResult> checkAction(
+      ActionMeta actionMeta, IVariables variables, IHopMetadataProvider 
metadataProvider) {
+    if (actionMeta == null || actionMeta.getAction() == null) {
+      return List.of();
+    }
+    return checkObject(
+        actionMeta.getAction(),
+        BaseMessages.getString(PKG, 
"ReferencedDatabaseConnectionChecker.Kind.Action"),
+        actionMeta.getName(),
+        actionMeta.getAction(),
+        variables,
+        metadataProvider);
+  }
+
+  /**
+   * Check one metadata object for {@link 
HopMetadataPropertyType#RDBMS_CONNECTION} fields.
+   *
+   * @param metadataObject the transform or action metadata, or a nested POJO 
used in tests
+   * @param ownerKind "Transform" or "Action" (already translated)
+   * @param ownerName the transform or action name
+   * @param source the check-result source, may be null
+   */
+  public static List<ICheckResult> checkObject(
+      Object metadataObject,
+      String ownerKind,
+      String ownerName,
+      ICheckResultSource source,
+      IVariables variables,
+      IHopMetadataProvider metadataProvider) {
+    List<ICheckResult> remarks = new ArrayList<>();
+    if (metadataObject == null || metadataProvider == null) {
+      return remarks;
+    }
+
+    IHopMetadataSerializer<DatabaseMeta> serializer;
+    try {
+      serializer = metadataProvider.getSerializer(DatabaseMeta.class);
+    } catch (Exception e) {
+      return remarks;
+    }
+    if (serializer == null) {
+      return remarks;
+    }
+
+    for (StringProperty property :
+        HopMetadataPropertyWalker.collectStrings(
+            metadataObject, HopMetadataPropertyType.RDBMS_CONNECTION)) {
+      ICheckResult remark =
+          checkConnectionName(
+              property.value(), ownerKind, ownerName, source, variables, 
serializer);
+      if (remark != null) {
+        remarks.add(remark);
+      }
+    }
+    return remarks;
+  }
+
+  private static ICheckResult checkConnectionName(
+      String rawName,
+      String ownerKind,
+      String ownerName,
+      ICheckResultSource source,
+      IVariables variables,
+      IHopMetadataSerializer<DatabaseMeta> serializer) {
+    if (Utils.isEmpty(rawName)) {
+      return new CheckResult(
+          ICheckResult.TYPE_RESULT_WARNING,
+          ERROR_NOT_ASSIGNED,
+          BaseMessages.getString(
+              PKG, "ReferencedDatabaseConnectionChecker.NotAssigned", 
ownerKind, ownerName),
+          source);
+    }
+
+    String resolved = variables != null ? variables.resolve(rawName) : rawName;
+    if (Utils.isEmpty(resolved)) {
+      return new CheckResult(
+          ICheckResult.TYPE_RESULT_WARNING,
+          ERROR_NOT_ASSIGNED,
+          BaseMessages.getString(
+              PKG, "ReferencedDatabaseConnectionChecker.NotAssigned", 
ownerKind, ownerName),
+          source);
+    }
+    if (StringUtil.containsVariableToken(resolved)) {
+      return null;
+    }
+
+    try {
+      if (serializer.exists(resolved)) {
+        return null;
+      }
+    } catch (Exception e) {
+      return new CheckResult(
+          ICheckResult.TYPE_RESULT_WARNING,
+          ERROR_DOES_NOT_EXIST,
+          BaseMessages.getString(
+              PKG,
+              "ReferencedDatabaseConnectionChecker.DoesNotExist",
+              resolved,
+              ownerKind,
+              ownerName),
+          source);
+    }
+
+    return new CheckResult(
+        ICheckResult.TYPE_RESULT_WARNING,
+        ERROR_DOES_NOT_EXIST,
+        BaseMessages.getString(
+            PKG,
+            "ReferencedDatabaseConnectionChecker.DoesNotExist",
+            resolved,
+            ownerKind,
+            ownerName),
+        source);
+  }
+}
diff --git a/engine/src/main/java/org/apache/hop/pipeline/PipelineMeta.java 
b/engine/src/main/java/org/apache/hop/pipeline/PipelineMeta.java
index 7db7e7d582..f0f0834355 100644
--- a/engine/src/main/java/org/apache/hop/pipeline/PipelineMeta.java
+++ b/engine/src/main/java/org/apache/hop/pipeline/PipelineMeta.java
@@ -77,6 +77,7 @@ import org.apache.hop.metadata.api.HopMetadataProperty;
 import org.apache.hop.metadata.api.IEnumHasCodeAndDescription;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.metadata.serializer.xml.XmlMetadataUtil;
+import org.apache.hop.metadata.validation.ReferencedDatabaseConnectionChecker;
 import org.apache.hop.partition.PartitionSchema;
 import org.apache.hop.pipeline.analysis.BufferDeadlockRisk;
 import org.apache.hop.pipeline.analysis.PipelineBufferDeadlockAnalyzer;
@@ -2818,6 +2819,12 @@ public class PipelineMeta extends AbstractMeta
                 risk.reconvergence()));
       }
 
+      for (TransformMeta transformMeta : transformsToCheck) {
+        remarks.addAll(
+            ReferencedDatabaseConnectionChecker.checkTransform(
+                transformMeta, variables, metadataProvider));
+      }
+
       ExtensionPointHandler.callExtensionPoint(
           LogChannel.GENERAL,
           variables,
diff --git a/engine/src/main/java/org/apache/hop/workflow/WorkflowMeta.java 
b/engine/src/main/java/org/apache/hop/workflow/WorkflowMeta.java
index e9d8ae417b..82ab7eb53c 100644
--- a/engine/src/main/java/org/apache/hop/workflow/WorkflowMeta.java
+++ b/engine/src/main/java/org/apache/hop/workflow/WorkflowMeta.java
@@ -64,6 +64,7 @@ import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.metadata.api.HopMetadataProperty;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.metadata.serializer.xml.XmlMetadataUtil;
+import org.apache.hop.metadata.validation.ReferencedDatabaseConnectionChecker;
 import org.apache.hop.resource.IResourceExport;
 import org.apache.hop.resource.IResourceNaming;
 import org.apache.hop.resource.ResourceDefinition;
@@ -1593,6 +1594,9 @@ public class WorkflowMeta extends AbstractMeta
         if (action != null) {
           checkAction(remarks, monitor, variables, metadataProvider, 
actionMeta, action);
         }
+        remarks.addAll(
+            ReferencedDatabaseConnectionChecker.checkAction(
+                actionMeta, variables, metadataProvider));
         // Progress bar...
         monitor.worked(worked++);
       }
diff --git 
a/engine/src/main/resources/org/apache/hop/metadata/validation/messages/messages_en_US.properties
 
b/engine/src/main/resources/org/apache/hop/metadata/validation/messages/messages_en_US.properties
new file mode 100644
index 0000000000..73b2d5a586
--- /dev/null
+++ 
b/engine/src/main/resources/org/apache/hop/metadata/validation/messages/messages_en_US.properties
@@ -0,0 +1,20 @@
+#
+# 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.
+#
+ReferencedDatabaseConnectionChecker.Kind.Transform=Transform
+ReferencedDatabaseConnectionChecker.Kind.Action=Action
+ReferencedDatabaseConnectionChecker.NotAssigned=No database connection is 
assigned on {0} ''{1}''
+ReferencedDatabaseConnectionChecker.DoesNotExist=Database connection ''{0}'' 
assigned on {1} ''{2}'' does not exist
diff --git 
a/engine/src/test/java/org/apache/hop/metadata/validation/ReferencedDatabaseConnectionCheckerTest.java
 
b/engine/src/test/java/org/apache/hop/metadata/validation/ReferencedDatabaseConnectionCheckerTest.java
new file mode 100644
index 0000000000..590c6d2d4b
--- /dev/null
+++ 
b/engine/src/test/java/org/apache/hop/metadata/validation/ReferencedDatabaseConnectionCheckerTest.java
@@ -0,0 +1,235 @@
+/*
+ * 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.metadata.validation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import org.apache.hop.core.ICheckResult;
+import org.apache.hop.core.database.DatabaseMeta;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.metadata.api.HopMetadataPropertyType;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.metadata.api.IHopMetadataSerializer;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transforms.dummy.DummyMeta;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.apache.hop.workflow.action.ActionBase;
+import org.apache.hop.workflow.action.ActionMeta;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class ReferencedDatabaseConnectionCheckerTest {
+
+  private IHopMetadataProvider metadataProvider;
+  private Variables variables;
+
+  @BeforeEach
+  @SuppressWarnings("unchecked")
+  void setUp() throws Exception {
+    variables = new Variables();
+    metadataProvider = mock(IHopMetadataProvider.class);
+    IHopMetadataSerializer<DatabaseMeta> serializer = 
mock(IHopMetadataSerializer.class);
+    
when(metadataProvider.getSerializer(DatabaseMeta.class)).thenReturn(serializer);
+    when(serializer.exists(anyString())).thenAnswer(inv -> 
"sales-db".equals(inv.getArgument(0)));
+  }
+
+  static class ConnMeta {
+    @HopMetadataProperty(hopMetadataPropertyType = 
HopMetadataPropertyType.RDBMS_CONNECTION)
+    String connection;
+
+    ConnMeta(String connection) {
+      this.connection = connection;
+    }
+  }
+
+  static class NestedItem {
+    @HopMetadataProperty(
+        key = "name",
+        hopMetadataPropertyType = HopMetadataPropertyType.RDBMS_CONNECTION)
+    String name;
+
+    NestedItem(String name) {
+      this.name = name;
+    }
+  }
+
+  static class NestedMeta {
+    @HopMetadataProperty List<NestedItem> items;
+
+    NestedMeta(String... names) {
+      items = java.util.Arrays.stream(names).map(NestedItem::new).toList();
+    }
+  }
+
+  static class TwoConnectionsMeta {
+    @HopMetadataProperty(hopMetadataPropertyType = 
HopMetadataPropertyType.RDBMS_CONNECTION)
+    String referenceConnection;
+
+    @HopMetadataProperty(hopMetadataPropertyType = 
HopMetadataPropertyType.RDBMS_CONNECTION)
+    String compareConnection;
+  }
+
+  static class UnannotatedMeta {
+    @HopMetadataProperty(key = "connection")
+    String connection = "missing";
+  }
+
+  static class TestAction extends ActionBase {
+    @HopMetadataProperty(hopMetadataPropertyType = 
HopMetadataPropertyType.RDBMS_CONNECTION)
+    String connection;
+
+    TestAction(String name, String connection) {
+      super(name, "");
+      this.connection = connection;
+    }
+
+    @Override
+    public org.apache.hop.core.Result execute(org.apache.hop.core.Result 
prevResult, int nr) {
+      return prevResult;
+    }
+  }
+
+  private List<ICheckResult> check(Object meta, String ownerName) {
+    return ReferencedDatabaseConnectionChecker.checkObject(
+        meta, "Transform", ownerName, null, variables, metadataProvider);
+  }
+
+  @Test
+  void missingLiteralNameIsAWarning() {
+    List<ICheckResult> remarks = check(new ConnMeta("missing-db"), "Read 
sales");
+
+    assertEquals(1, remarks.size());
+    assertEquals(
+        ReferencedDatabaseConnectionChecker.ERROR_DOES_NOT_EXIST, 
remarks.get(0).getErrorCode());
+    assertTrue(remarks.get(0).getText().contains("missing-db"));
+    assertTrue(remarks.get(0).getText().contains("Read sales"));
+  }
+
+  @Test
+  void existingLiteralNameIsSilent() {
+    assertTrue(check(new ConnMeta("sales-db"), "Read sales").isEmpty());
+  }
+
+  @Test
+  void unresolvedVariableIsSkipped() {
+    assertTrue(check(new ConnMeta("${CONNECTION}"), "Read sales").isEmpty());
+  }
+
+  @Test
+  void resolvedVariableToExistingNameIsSilent() {
+    variables.setVariable("CONNECTION", "sales-db");
+    assertTrue(check(new ConnMeta("${CONNECTION}"), "Read sales").isEmpty());
+  }
+
+  @Test
+  void resolvedVariableToMissingNameWarns() {
+    variables.setVariable("CONNECTION", "missing-db");
+    List<ICheckResult> remarks = check(new ConnMeta("${CONNECTION}"), "Read 
sales");
+
+    assertEquals(1, remarks.size());
+    assertEquals(
+        ReferencedDatabaseConnectionChecker.ERROR_DOES_NOT_EXIST, 
remarks.get(0).getErrorCode());
+    assertTrue(remarks.get(0).getText().contains("missing-db"));
+  }
+
+  @Test
+  void mixedUnresolvedVariableIsSkipped() {
+    assertTrue(check(new ConnMeta("db_${ENV}"), "Read sales").isEmpty());
+  }
+
+  @Test
+  void emptyConnectionIsAWarning() {
+    List<ICheckResult> remarks = check(new ConnMeta(""), "Read sales");
+
+    assertEquals(1, remarks.size());
+    assertEquals(
+        ReferencedDatabaseConnectionChecker.ERROR_NOT_ASSIGNED, 
remarks.get(0).getErrorCode());
+  }
+
+  @Test
+  void nestedListConnectionsAreChecked() {
+    List<ICheckResult> remarks =
+        check(new NestedMeta("sales-db", "missing-db"), "Check connections");
+
+    assertEquals(1, remarks.size());
+    assertTrue(remarks.get(0).getText().contains("missing-db"));
+  }
+
+  @Test
+  void twoConnectionFieldsOnOneObject() {
+    TwoConnectionsMeta meta = new TwoConnectionsMeta();
+    meta.referenceConnection = "sales-db";
+    meta.compareConnection = "other-db";
+
+    List<ICheckResult> remarks = check(meta, "Compare tables");
+
+    assertEquals(1, remarks.size());
+    assertTrue(remarks.get(0).getText().contains("other-db"));
+  }
+
+  @Test
+  void unannotatedConnectionFieldIsIgnored() {
+    assertTrue(check(new UnannotatedMeta(), "Legacy").isEmpty());
+  }
+
+  @Test
+  void checksAPipelineTransform() {
+    ConnTransformMeta transform = new ConnTransformMeta();
+    transform.connection = "missing-db";
+    TransformMeta transformMeta = new TransformMeta("Read sales", transform);
+
+    PipelineMeta pipelineMeta = new PipelineMeta();
+    pipelineMeta.addTransform(transformMeta);
+
+    List<ICheckResult> remarks =
+        ReferencedDatabaseConnectionChecker.checkPipeline(
+            pipelineMeta, variables, metadataProvider);
+
+    assertEquals(1, remarks.size());
+    assertEquals(transformMeta, remarks.get(0).getSourceInfo());
+  }
+
+  @Test
+  void checksAWorkflowAction() {
+    TestAction action = new TestAction("Run SQL", "missing-db");
+    ActionMeta actionMeta = new ActionMeta(action);
+
+    WorkflowMeta workflowMeta = new WorkflowMeta();
+    workflowMeta.addAction(actionMeta);
+
+    List<ICheckResult> remarks =
+        ReferencedDatabaseConnectionChecker.checkWorkflow(
+            workflowMeta, variables, metadataProvider);
+
+    assertEquals(1, remarks.size());
+    assertEquals(
+        ReferencedDatabaseConnectionChecker.ERROR_DOES_NOT_EXIST, 
remarks.get(0).getErrorCode());
+  }
+
+  /** A Dummy transform that also carries an annotated connection name, for 
pipeline-level tests. */
+  static class ConnTransformMeta extends DummyMeta {
+    @HopMetadataProperty(hopMetadataPropertyType = 
HopMetadataPropertyType.RDBMS_CONNECTION)
+    String connection;
+  }
+}
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 10a7624a08..147c22f935 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
@@ -33,6 +33,7 @@ import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.metadata.api.HopMetadataPropertyType;
 import org.apache.hop.metadata.api.IEnumHasCode;
 import org.apache.hop.metadata.api.IEnumHasCodeAndDescription;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
@@ -72,13 +73,19 @@ public class ActionWaitForSql extends ActionBase implements 
Cloneable, IAction {
   @HopMetadataProperty(key = "custom_sql")
   private String customSql;
 
-  @HopMetadataProperty(key = "connection")
+  @HopMetadataProperty(
+      key = "connection",
+      hopMetadataPropertyType = HopMetadataPropertyType.RDBMS_CONNECTION)
   private String connection;
 
-  @HopMetadataProperty(key = "tablename")
+  @HopMetadataProperty(
+      key = "tablename",
+      hopMetadataPropertyType = HopMetadataPropertyType.RDBMS_TABLE)
   private String tableName;
 
-  @HopMetadataProperty(key = "schemaname")
+  @HopMetadataProperty(
+      key = "schemaname",
+      hopMetadataPropertyType = HopMetadataPropertyType.RDBMS_SCHEMA)
   private String schemaName;
 
   /** Maximum timeout in seconds */
diff --git 
a/plugins/databases/mssqlnative/src/main/java/org/apache/hop/workflow/actions/mssqlbulkload/ActionMssqlBulkLoad.java
 
b/plugins/databases/mssqlnative/src/main/java/org/apache/hop/workflow/actions/mssqlbulkload/ActionMssqlBulkLoad.java
index fb7bae774c..9d952f0252 100644
--- 
a/plugins/databases/mssqlnative/src/main/java/org/apache/hop/workflow/actions/mssqlbulkload/ActionMssqlBulkLoad.java
+++ 
b/plugins/databases/mssqlnative/src/main/java/org/apache/hop/workflow/actions/mssqlbulkload/ActionMssqlBulkLoad.java
@@ -39,6 +39,7 @@ import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.core.vfs.HopVfs;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.metadata.api.HopMetadataPropertyType;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.resource.ResourceEntry;
 import org.apache.hop.resource.ResourceEntry.ResourceType;
@@ -65,10 +66,14 @@ import 
org.apache.hop.workflow.action.validator.ValidatorContext;
 public class ActionMssqlBulkLoad extends ActionBase {
   private static final Class<?> PKG = ActionMssqlBulkLoad.class;
 
-  @HopMetadataProperty(key = "schemaname")
+  @HopMetadataProperty(
+      key = "schemaname",
+      hopMetadataPropertyType = HopMetadataPropertyType.RDBMS_SCHEMA)
   private String schemaName;
 
-  @HopMetadataProperty(key = "tablename")
+  @HopMetadataProperty(
+      key = "tablename",
+      hopMetadataPropertyType = HopMetadataPropertyType.RDBMS_TABLE)
   private String tableName;
 
   @HopMetadataProperty(key = "filename")
@@ -140,7 +145,9 @@ public class ActionMssqlBulkLoad extends ActionBase {
   @HopMetadataProperty(key = "truncate")
   private boolean truncate;
 
-  @HopMetadataProperty(key = "connection")
+  @HopMetadataProperty(
+      key = "connection",
+      hopMetadataPropertyType = HopMetadataPropertyType.RDBMS_CONNECTION)
   private String connection;
 
   public ActionMssqlBulkLoad(String n) {
diff --git 
a/plugins/databases/mysql/src/main/java/org/apache/hop/workflow/actions/mysqlbulkload/ActionMysqlBulkLoad.java
 
b/plugins/databases/mysql/src/main/java/org/apache/hop/workflow/actions/mysqlbulkload/ActionMysqlBulkLoad.java
index 513f896eed..9a7f62668e 100644
--- 
a/plugins/databases/mysql/src/main/java/org/apache/hop/workflow/actions/mysqlbulkload/ActionMysqlBulkLoad.java
+++ 
b/plugins/databases/mysql/src/main/java/org/apache/hop/workflow/actions/mysqlbulkload/ActionMysqlBulkLoad.java
@@ -38,6 +38,7 @@ import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.core.vfs.HopVfs;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.metadata.api.HopMetadataPropertyType;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.resource.ResourceEntry;
 import org.apache.hop.resource.ResourceEntry.ResourceType;
@@ -65,10 +66,14 @@ import 
org.apache.hop.workflow.action.validator.ValidatorContext;
 public class ActionMysqlBulkLoad extends ActionBase {
   private static final Class<?> PKG = ActionMysqlBulkLoad.class;
 
-  @HopMetadataProperty(key = "schemaname")
+  @HopMetadataProperty(
+      key = "schemaname",
+      hopMetadataPropertyType = HopMetadataPropertyType.RDBMS_SCHEMA)
   private String schemaName;
 
-  @HopMetadataProperty(key = "tablename")
+  @HopMetadataProperty(
+      key = "tablename",
+      hopMetadataPropertyType = HopMetadataPropertyType.RDBMS_TABLE)
   private String tableName;
 
   @HopMetadataProperty(key = "filename")
@@ -107,7 +112,9 @@ public class ActionMysqlBulkLoad extends ActionBase {
   @HopMetadataProperty(key = "addfiletoresult")
   private boolean addFileToResult;
 
-  @HopMetadataProperty(key = "connection")
+  @HopMetadataProperty(
+      key = "connection",
+      hopMetadataPropertyType = HopMetadataPropertyType.RDBMS_CONNECTION)
   private String connection;
 
   public ActionMysqlBulkLoad(String n) {
diff --git 
a/plugins/databases/snowflake/src/main/java/org/apache/hop/workflow/actions/snowflake/WarehouseManager.java
 
b/plugins/databases/snowflake/src/main/java/org/apache/hop/workflow/actions/snowflake/WarehouseManager.java
index 0442a4c64d..e0ff861075 100644
--- 
a/plugins/databases/snowflake/src/main/java/org/apache/hop/workflow/actions/snowflake/WarehouseManager.java
+++ 
b/plugins/databases/snowflake/src/main/java/org/apache/hop/workflow/actions/snowflake/WarehouseManager.java
@@ -36,6 +36,7 @@ import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.metadata.api.HopMetadataPropertyType;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.workflow.WorkflowMeta;
 import org.apache.hop.workflow.action.ActionBase;
@@ -96,7 +97,9 @@ public class WarehouseManager extends ActionBase implements 
Cloneable, IAction {
   public static final String CONST_COMMIT = ";\ncommit;";
 
   /** The database to connect to. */
-  @HopMetadataProperty(key = CONNECTION)
+  @HopMetadataProperty(
+      key = CONNECTION,
+      hopMetadataPropertyType = HopMetadataPropertyType.RDBMS_CONNECTION)
   private String connection;
 
   /** The management action to perform. */
diff --git 
a/plugins/transforms/sqlfileoutput/src/main/java/org/apache/hop/pipeline/transforms/sqlfileoutput/SQLFileOutputMeta.java
 
b/plugins/transforms/sqlfileoutput/src/main/java/org/apache/hop/pipeline/transforms/sqlfileoutput/SQLFileOutputMeta.java
index 477759cb6c..0365295844 100644
--- 
a/plugins/transforms/sqlfileoutput/src/main/java/org/apache/hop/pipeline/transforms/sqlfileoutput/SQLFileOutputMeta.java
+++ 
b/plugins/transforms/sqlfileoutput/src/main/java/org/apache/hop/pipeline/transforms/sqlfileoutput/SQLFileOutputMeta.java
@@ -42,6 +42,7 @@ import org.apache.hop.core.vfs.HopVfs;
 import org.apache.hop.core.xml.XmlHandler;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.metadata.api.HopMetadataPropertyType;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.pipeline.DatabaseImpact;
 import org.apache.hop.pipeline.PipelineMeta;
@@ -66,13 +67,17 @@ public class SQLFileOutputMeta extends 
BaseTransformMeta<SQLFileOutput, SQLFileO
   private static final String CONST_SPACE = "      ";
   private static final String CONST_SPACE_SHORT = "    ";
 
-  @HopMetadataProperty(key = "connection")
+  @HopMetadataProperty(
+      key = "connection",
+      hopMetadataPropertyType = HopMetadataPropertyType.RDBMS_CONNECTION)
   private String connection;
 
-  @HopMetadataProperty(key = "schema")
+  @HopMetadataProperty(
+      key = "schema",
+      hopMetadataPropertyType = HopMetadataPropertyType.RDBMS_SCHEMA)
   private String schemaName;
 
-  @HopMetadataProperty(key = "table")
+  @HopMetadataProperty(key = "table", hopMetadataPropertyType = 
HopMetadataPropertyType.RDBMS_TABLE)
   private String tableName;
 
   @HopMetadataProperty(key = "truncate")
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/config/FileValidationConfigPlugin.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/config/FileValidationConfigPlugin.java
new file mode 100644
index 0000000000..0ce254db6c
--- /dev/null
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/config/FileValidationConfigPlugin.java
@@ -0,0 +1,148 @@
+/*
+ * 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.ui.hopgui.file.config;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.hop.core.config.HopConfig;
+import org.apache.hop.core.config.plugin.ConfigPlugin;
+import org.apache.hop.core.config.plugin.IConfigOptions;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.gui.plugin.GuiElementType;
+import org.apache.hop.core.gui.plugin.GuiPlugin;
+import org.apache.hop.core.gui.plugin.GuiWidgetElement;
+import org.apache.hop.core.logging.ILogChannel;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.metadata.api.IHasHopMetadataProvider;
+import org.apache.hop.ui.core.gui.GuiCompositeWidgets;
+import org.apache.hop.ui.core.gui.IGuiPluginCompositeWidgetsListener;
+import 
org.apache.hop.ui.hopgui.perspective.configuration.tabs.ConfigPluginOptionsTab;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Control;
+import picocli.CommandLine;
+
+/**
+ * Options that run when a pipeline or workflow file is saved. Surfaced in the 
Configuration
+ * perspective Plugins tab.
+ */
+@ConfigPlugin(
+    id = "file-validation-config",
+    description = "Validate referenced database connections when saving a 
file",
+    category = ConfigPlugin.CATEGORY_CONFIG)
+@GuiPlugin(description = "i18n::FileValidationConfigPlugin.Description")
+public class FileValidationConfigPlugin
+    implements IConfigOptions, IGuiPluginCompositeWidgetsListener {
+
+  public static final String KEY_VALIDATE_DB_CONNECTIONS_ON_SAVE = 
"ValidateDbConnectionsOnSave";
+
+  private static final String WIDGET_VALIDATE_DB_CONNECTIONS_ON_SAVE =
+      "file-validation-validate-db-connections-on-save";
+
+  @GuiWidgetElement(
+      id = WIDGET_VALIDATE_DB_CONNECTIONS_ON_SAVE,
+      parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
+      type = GuiElementType.CHECKBOX,
+      label = 
"i18n::FileValidationConfigPlugin.ValidateDbConnectionsOnSave.Label",
+      toolTip = 
"i18n::FileValidationConfigPlugin.ValidateDbConnectionsOnSave.ToolTip")
+  @CommandLine.Option(
+      names = {"--validate-db-connections-on-save"},
+      description =
+          "Warn when saving a pipeline or workflow if a referenced database 
connection does not exist (default: true)")
+  private Boolean validateDbConnectionsOnSave;
+
+  public FileValidationConfigPlugin() {
+    loadFromHopConfig();
+  }
+
+  public static FileValidationConfigPlugin getInstance() {
+    return new FileValidationConfigPlugin();
+  }
+
+  private void loadFromHopConfig() {
+    try {
+      validateDbConnectionsOnSave =
+          HopConfig.readOptionBoolean(KEY_VALIDATE_DB_CONNECTIONS_ON_SAVE, 
true);
+    } catch (Exception e) {
+      validateDbConnectionsOnSave = true;
+    }
+  }
+
+  /**
+   * JavaBeans getter used by {@code GuiCompositeWidgets}. Null (never 
persisted) is treated as
+   * enabled, which is the default.
+   */
+  public Boolean getValidateDbConnectionsOnSave() {
+    return isValidateDbConnectionsOnSave();
+  }
+
+  /** JavaBeans setter used by {@code GuiCompositeWidgets}. */
+  public void setValidateDbConnectionsOnSave(Boolean 
validateDbConnectionsOnSave) {
+    this.validateDbConnectionsOnSave = validateDbConnectionsOnSave;
+  }
+
+  public boolean isValidateDbConnectionsOnSave() {
+    return validateDbConnectionsOnSave == null || validateDbConnectionsOnSave;
+  }
+
+  @Override
+  public void widgetsCreated(GuiCompositeWidgets compositeWidgets) {
+    // Widgets are filled from this instance, loaded in the constructor.
+  }
+
+  @Override
+  public void widgetsPopulated(GuiCompositeWidgets compositeWidgets) {
+    // Nothing to do.
+  }
+
+  @Override
+  public void widgetModified(
+      GuiCompositeWidgets compositeWidgets, Control changedWidget, String 
widgetId) {
+    persistContents(compositeWidgets);
+  }
+
+  @Override
+  public void persistContents(GuiCompositeWidgets compositeWidgets) {
+    Control control = 
compositeWidgets.getWidgetsMap().get(WIDGET_VALIDATE_DB_CONNECTIONS_ON_SAVE);
+    if (control instanceof Button button) {
+      validateDbConnectionsOnSave = button.getSelection();
+    }
+    saveToHopConfig();
+  }
+
+  public Map<String, Object> saveToHopConfig() {
+    Map<String, Object> options = new HashMap<>();
+    if (validateDbConnectionsOnSave != null) {
+      options.put(KEY_VALIDATE_DB_CONNECTIONS_ON_SAVE, 
validateDbConnectionsOnSave);
+      HopConfig.saveOptions(options);
+    }
+    return options;
+  }
+
+  @Override
+  public boolean handleOption(
+      ILogChannel log, IHasHopMetadataProvider metadataProvider, IVariables 
variables)
+      throws HopException {
+    if (validateDbConnectionsOnSave == null) {
+      return false;
+    }
+    saveToHopConfig();
+    log.logBasic(
+        "Validate database connections on save is now "
+            + (isValidateDbConnectionsOnSave() ? "enabled" : "disabled"));
+    return true;
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
index a97f718051..31bdae5841 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
@@ -109,6 +109,7 @@ import org.apache.hop.execution.IExecutionInfoLocation;
 import org.apache.hop.history.AuditManager;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.laf.BasePropertyHandler;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.metadata.api.IHopMetadataSerializer;
 import org.apache.hop.metadata.serializer.multi.MultiMetadataProvider;
 import org.apache.hop.pipeline.DatabaseImpact;
@@ -200,6 +201,7 @@ import 
org.apache.hop.ui.hopgui.file.shared.HopGuiGraphSnapshotUndo;
 import org.apache.hop.ui.hopgui.file.shared.HopGuiTooltipExtension;
 import org.apache.hop.ui.hopgui.file.shared.ISnapshotUndoSupport;
 import org.apache.hop.ui.hopgui.file.shared.PipelineRowSamplerHelper;
+import org.apache.hop.ui.hopgui.file.shared.ReferencedConnectionSaveValidator;
 import org.apache.hop.ui.hopgui.palette.GraphPalette;
 import org.apache.hop.ui.hopgui.palette.GraphPaletteTree;
 import org.apache.hop.ui.hopgui.palette.IGraphPaletteHost;
@@ -5141,6 +5143,15 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
         throw new HopException("No filename: please specify a filename for 
this pipeline");
       }
 
+      IHopMetadataProvider saveMetadataProvider = 
pipelineMeta.getMetadataProvider();
+      if (saveMetadataProvider == null) {
+        saveMetadataProvider = hopGui.getMetadataProvider();
+      }
+      if (!ReferencedConnectionSaveValidator.confirmSave(
+          hopShell(), pipelineMeta, variables, saveMetadataProvider)) {
+        return;
+      }
+
       // Keep track of save
       //
       AuditManager.registerEvent(
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/ReferencedConnectionSaveValidator.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/ReferencedConnectionSaveValidator.java
new file mode 100644
index 0000000000..1fdd52b5df
--- /dev/null
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/ReferencedConnectionSaveValidator.java
@@ -0,0 +1,117 @@
+/*
+ * 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.ui.hopgui.file.shared;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.hop.core.ICheckResult;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.metadata.validation.ReferencedDatabaseConnectionChecker;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.ui.core.dialog.MessageDialogWithToggle;
+import org.apache.hop.ui.hopgui.file.config.FileValidationConfigPlugin;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * Optional warning when saving a pipeline or workflow that references a 
missing database
+ * connection. Saving is never blocked: the user can continue or cancel.
+ */
+public final class ReferencedConnectionSaveValidator {
+
+  private static final Class<?> PKG = FileValidationConfigPlugin.class;
+  private static final int MAX_LISTED = 15;
+
+  private ReferencedConnectionSaveValidator() {}
+
+  public static boolean confirmSave(
+      Shell shell,
+      PipelineMeta pipelineMeta,
+      IVariables variables,
+      IHopMetadataProvider metadataProvider) {
+    if 
(!FileValidationConfigPlugin.getInstance().isValidateDbConnectionsOnSave()) {
+      return true;
+    }
+    return confirmRemarks(
+        shell,
+        ReferencedDatabaseConnectionChecker.checkPipeline(
+            pipelineMeta, variables, metadataProvider));
+  }
+
+  public static boolean confirmSave(
+      Shell shell,
+      WorkflowMeta workflowMeta,
+      IVariables variables,
+      IHopMetadataProvider metadataProvider) {
+    if 
(!FileValidationConfigPlugin.getInstance().isValidateDbConnectionsOnSave()) {
+      return true;
+    }
+    return confirmRemarks(
+        shell,
+        ReferencedDatabaseConnectionChecker.checkWorkflow(
+            workflowMeta, variables, metadataProvider));
+  }
+
+  static boolean confirmRemarks(Shell shell, List<ICheckResult> remarks) {
+    if (remarks == null || remarks.isEmpty()) {
+      return true;
+    }
+    if (shell == null || shell.isDisposed()) {
+      return true;
+    }
+
+    String listed =
+        remarks.stream()
+            .limit(MAX_LISTED)
+            .map(ICheckResult::getText)
+            .collect(Collectors.joining("\n"));
+    if (remarks.size() > MAX_LISTED) {
+      listed =
+          listed
+              + "\n"
+              + BaseMessages.getString(
+                  PKG,
+                  "ReferencedConnectionSaveValidator.Dialog.More",
+                  Integer.toString(remarks.size() - MAX_LISTED));
+    }
+
+    MessageDialogWithToggle dialog =
+        new MessageDialogWithToggle(
+            shell,
+            BaseMessages.getString(PKG, 
"ReferencedConnectionSaveValidator.Dialog.Title"),
+            BaseMessages.getString(PKG, 
"ReferencedConnectionSaveValidator.Dialog.Message", listed),
+            SWT.ICON_WARNING,
+            new String[] {
+              BaseMessages.getString(PKG, "System.Button.Yes"),
+              BaseMessages.getString(PKG, "System.Button.No")
+            },
+            BaseMessages.getString(PKG, 
"ReferencedConnectionSaveValidator.Dialog.Toggle"),
+            false);
+    int answer = dialog.open();
+
+    if (dialog.getToggleState()) {
+      FileValidationConfigPlugin config = 
FileValidationConfigPlugin.getInstance();
+      config.setValidateDbConnectionsOnSave(false);
+      config.saveToHopConfig();
+    }
+
+    return answer == 0;
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
index d61339984a..a35b218104 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
@@ -98,6 +98,7 @@ import org.apache.hop.execution.IExecutionInfoLocation;
 import org.apache.hop.history.AuditManager;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.laf.BasePropertyHandler;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.metadata.api.IHopMetadataSerializer;
 import org.apache.hop.metadata.serializer.multi.MultiMetadataProvider;
 import org.apache.hop.pipeline.PipelinePainter;
@@ -141,6 +142,7 @@ import 
org.apache.hop.ui.hopgui.file.shared.HopGuiAbstractGraph;
 import org.apache.hop.ui.hopgui.file.shared.HopGuiGraphSnapshotUndo;
 import org.apache.hop.ui.hopgui.file.shared.HopGuiTooltipExtension;
 import org.apache.hop.ui.hopgui.file.shared.ISnapshotUndoSupport;
+import org.apache.hop.ui.hopgui.file.shared.ReferencedConnectionSaveValidator;
 import 
org.apache.hop.ui.hopgui.file.workflow.context.HopGuiWorkflowActionContext;
 import org.apache.hop.ui.hopgui.file.workflow.context.HopGuiWorkflowContext;
 import org.apache.hop.ui.hopgui.file.workflow.context.HopGuiWorkflowHopContext;
@@ -4673,6 +4675,15 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
         throw new HopException("No filename: please specify a filename for 
this workflow");
       }
 
+      IHopMetadataProvider saveMetadataProvider = 
workflowMeta.getMetadataProvider();
+      if (saveMetadataProvider == null) {
+        saveMetadataProvider = hopGui.getMetadataProvider();
+      }
+      if (!ReferencedConnectionSaveValidator.confirmSave(
+          hopShell(), workflowMeta, variables, saveMetadataProvider)) {
+        return;
+      }
+
       // Keep track of save
       //
       AuditManager.registerEvent(
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/file/config/messages/messages_en_US.properties
 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/file/config/messages/messages_en_US.properties
new file mode 100644
index 0000000000..cbb4f206bc
--- /dev/null
+++ 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/file/config/messages/messages_en_US.properties
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+FileValidationConfigPlugin.Description=File validation
+FileValidationConfigPlugin.ValidateDbConnectionsOnSave.Label=Validate database 
connections when saving
+FileValidationConfigPlugin.ValidateDbConnectionsOnSave.ToolTip=When saving a 
pipeline or workflow, warn if a transform or action references a database 
connection that is not in the project metadata. Names that still contain a 
variable such as '${CONNECTION}' after resolving the current environment are 
skipped. Saving is never blocked.
+ReferencedConnectionSaveValidator.Dialog.Title=Database connections
+ReferencedConnectionSaveValidator.Dialog.Message=The following database 
connection problems were found. Save anyway?\n\n{0}
+ReferencedConnectionSaveValidator.Dialog.More=...and {0} more
+ReferencedConnectionSaveValidator.Dialog.Toggle=Don''t run this check when 
saving. Configure the option in the configuration perspective

Reply via email to