Repository: dbt-labs/dbt-core

Issue:
[CT-3329] [Bug] Unit testing errors with long CTE name given reasonable test name length
### Is this a new bug in dbt-core?

- [X] I believe this is a new bug in dbt-core
- [X] I have searched the existing issues, and I could not find an existing issue for this bug

### Current Behavior

Long unit test names (in combination with long model name + input names) can lead to a "Relation name '{}' is longer than 63 characters" if the sum of lengths is > 63 (on postgres) characters.

### Expected Behavior

No error

### Steps To Reproduce

Changing the `test_my_model_incremental_yml` fixture from in `test_unit_testing.py`:
```
test_my_model_incremental_yml = """
unit_tests:
  - name: incremental_false
...
```
to: 
```
test_my_model_incremental_yml = """
unit_tests:
  - name: test_incremental_false
...
```

Results in: 
```
15:58:28  Completed with 1 error and 0 warnings:
15:58:28  
15:58:28    Runtime Error in unit_test my_incremental_model__test_incremental_false (models/test_my_incremental_model.yml)
  Relation name '__dbt__cte__my_incremental_model__test_incremental_false__events' is longer than 63 characters
15:58:28  
15:58:28  Done. PASS=1 WARN=0 ERROR=1 SKIP=0 TOTAL=2
```

### Relevant log output

_No response_

### Environment

```markdown
- OS:
- Python:
- dbt:
```


### Which database adapter are you using with dbt?

_No response_

### Additional Context

This is happening because the ephemeral fixture nodes are named based on the concatenation of: the model being tested, the name of the unit test, and the name of the input itself. This ensures they have globally unique names in the unit test manifest but is overkill for the purpose of naming the CTEs, which are local to a unit test case.

Options: 
Shorten the name of the ModelNode for input fixtures to only include the name of the original input node -- assuming that node names don't need to be unique within a DAG. - These aren't used in a DAG, so this shouldn't be a concern.
Consider hashing to add extra layer of caution to avoid name duplication.
These are ephemeral names only they are not executable.

### Testing Considerations:

- Test having 2 input models with same names and different packages.


Discussion:
We have some prior art for this. Postgres in particular has the 63-character limit; other DWHs have higher limits.

https://github.com/dbt-labs/dbt-core/blob/bb35b3eb87ab2543247fd6ea51f6043bb89a1f89/plugins/postgres/dbt/include/postgres/macros/adapters.sql#L141-L178
I ran into the same issue when testing this out, is there any way to make this CTE name shorter without requiring folks to have a shorter name for their unit test?

Resolving patch:
diff --git a/core/dbt/context/providers.py b/core/dbt/context/providers.py
--- a/core/dbt/context/providers.py
+++ b/core/dbt/context/providers.py
@@ -577,7 +577,6 @@ def resolve(
         target_package: Optional[str] = None,
         target_version: Optional[NodeVersion] = None,
     ) -> RelationProxy:
-        target_name = f"{self.model.name}__{target_name}"
         return super().resolve(target_name, target_package, target_version)
 
 


Test patch:
diff --git a/core/dbt/parser/unit_tests.py b/core/dbt/parser/unit_tests.py
--- a/core/dbt/parser/unit_tests.py
+++ b/core/dbt/parser/unit_tests.py
@@ -64,7 +64,7 @@ def parse_unit_test_case(self, test_case: UnitTestDefinition):
         # already been done, we don't have to care about fields that are necessary
         # for selection.
         # Note: no depends_on, that's added later using input nodes
-        name = f"{test_case.model}__{test_case.name}"
+        name = test_case.name
         unit_test_node = UnitTestNode(
             name=name,
             resource_type=NodeType.Unit,
@@ -134,7 +134,7 @@ def parse_unit_test_case(self, test_case: UnitTestDefinition):
                 NodeType.Seed,
                 NodeType.Snapshot,
             ):
-                input_name = f"{unit_test_node.name}__{original_input_node.name}"
+                input_name = original_input_node.name
                 input_node = ModelNode(
                     **common_fields,
                     unique_id=f"model.{test_case.package_name}.{input_name}",
@@ -145,7 +145,7 @@ def parse_unit_test_case(self, test_case: UnitTestDefinition):
                 # We are reusing the database/schema/identifier from the original source,
                 # but that shouldn't matter since this acts as an ephemeral model which just
                 # wraps a CTE around the unit test node.
-                input_name = f"{unit_test_node.name}__{original_input_node.search_name}__{original_input_node.name}"
+                input_name = original_input_node.name
                 input_node = UnitTestSourceDefinition(
                     **common_fields,
                     unique_id=f"model.{test_case.package_name}.{input_name}",
diff --git a/core/dbt/task/test.py b/core/dbt/task/test.py
--- a/core/dbt/task/test.py
+++ b/core/dbt/task/test.py
@@ -27,11 +27,9 @@
     LogTestResult,
     LogStartLine,
 )
-from dbt.exceptions import (
-    DbtInternalError,
-    BooleanError,
-)
-from ..adapters.exceptions import MissingMaterializationError
+from dbt.exceptions import DbtInternalError, BooleanError
+from dbt_common.exceptions import DbtBaseException, DbtRuntimeError
+from dbt.adapters.exceptions import MissingMaterializationError
 from dbt.graph import (
     ResourceTypeSelector,
 )
@@ -83,15 +81,21 @@ class UnitTestResultData(dbtClassMixin):
 class TestRunner(CompileRunner):
     _ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
 
+    def describe_node_name(self):
+        if self.node.resource_type == NodeType.Unit:
+            return f"{self.node.model}::{self.node.name}"
+        else:
+            return self.node.name
+
     def describe_node(self):
-        return f"{self.node.resource_type} {self.node.name}"
+        return f"{self.node.resource_type} {self.describe_node_name()}"
 
     def print_result_line(self, result):
         model = result.node
 
         fire_event(
             LogTestResult(
-                name=model.name,
+                name=self.describe_node_name(),
                 status=str(result.status),
                 index=self.node_index,
                 num_models=self.num_nodes,
@@ -207,7 +211,13 @@ def execute_unit_test(
         # generate materialization macro
         macro_func = MacroGenerator(materialization_macro, context)
         # execute materialization macro
-        macro_func()
+        try:
+            macro_func()
+        except DbtBaseException as e:
+            raise DbtRuntimeError(
+                f"During unit test execution of {self.describe_node_name()}, dbt could not build the 'actual' result for comparison against 'expected' given the unit test definition:\n {e}"
+            )
+
         # load results from context
         # could eventually be returned directly by materialization
         result = context["load_result"]("main")
diff --git a/tests/adapter/dbt/tests/adapter/unit_testing/test_unit_testing.py b/tests/adapter/dbt/tests/adapter/unit_testing/test_unit_testing.py
new file mode 100644
--- /dev/null
+++ b/tests/adapter/dbt/tests/adapter/unit_testing/test_unit_testing.py
@@ -0,0 +1,69 @@
+import pytest
+
+from dbt.adapters.postgres.relation_configs import MAX_CHARACTERS_IN_IDENTIFIER
+from dbt.tests.util import run_dbt, write_file
+
+my_model_a_sql = """
+SELECT
+1 as a,
+1 as id,
+2 as not_testing,
+'a' as string_a,
+DATE '2020-01-02' as date_a
+"""
+
+test_model_a_long_test_name_yml = """
+unit_tests:
+  - name: {test_name}
+    model: my_model_a
+    given: []
+    expect:
+      rows:
+        - {{a: 1, id: 1, not_testing: 2, string_a: "a", date_a: "2020-01-02"}}
+"""
+
+
+class BaseUnitTestLongTestName:
+    @pytest.fixture(scope="class")
+    def models(self):
+        return {
+            "my_model_a.sql": my_model_a_sql,
+            "test_model_a.yml": test_model_a_long_test_name_yml,
+        }
+
+    @pytest.fixture
+    def max_unit_test_name_length(self) -> int:
+        return -1
+
+    def test_long_unit_test_name(self, project, max_unit_test_name_length):
+        # max test name == passing unit test
+        write_file(
+            test_model_a_long_test_name_yml.format(test_name="a" * max_unit_test_name_length),
+            "models",
+            "test_model_a.yml",
+        )
+        results = run_dbt(["run"])
+        assert len(results) == 1
+
+        results = run_dbt(["test"], expect_pass=True)
+        assert len(results) == 1
+
+        # max test name == failing command
+        write_file(
+            test_model_a_long_test_name_yml.format(
+                test_name="a" * (max_unit_test_name_length + 1)
+            ),
+            "models",
+            "test_model_a.yml",
+        )
+
+        results = run_dbt(["run"])
+        assert len(results) == 1
+
+        run_dbt(["test"], expect_pass=False)
+
+
+class TestPostgresUnitTestLongTestNames(BaseUnitTestLongTestName):
+    @pytest.fixture
+    def max_unit_test_name_length(self) -> int:
+        return MAX_CHARACTERS_IN_IDENTIFIER
diff --git a/tests/functional/unit_testing/fixtures.py b/tests/functional/unit_testing/fixtures.py
--- a/tests/functional/unit_testing/fixtures.py
+++ b/tests/functional/unit_testing/fixtures.py
@@ -598,3 +598,54 @@
       format: csv
       fixture: test_my_model_basic_fixture
 """
+
+test_model_a_b_yml = """
+unit_tests:
+  - name: my_test_name
+    model: my_model_a
+    given: []
+    expect:
+      rows:
+        - {a: 1, id: 1, not_testing: 2, string_a: "a", date_a: "2020-01-02"}
+
+  - name: my_test_name
+    model: my_model_b
+    given: []
+    expect:
+      rows:
+        - {b: 2, id: 1, c: 2, string_b: "b"}
+"""
+
+test_model_a_with_duplicate_test_name_yml = """
+unit_tests:
+  - name: my_test_name
+    model: my_model_a
+    given: []
+    expect:
+      rows:
+        - {a: 1, id: 1, not_testing: 2, string_a: "a", date_a: "2020-01-02"}
+
+  - name: my_test_name
+    model: my_model_a
+    given: []
+    expect:
+      rows:
+        - {a: 1, id: 1, not_testing: 2, string_a: "a", date_a: "2020-01-02"}
+"""
+
+test_my_model_yml_invalid = """
+unit_tests:
+  - name: test_my_model
+    model: my_model
+    given:
+      - input: ref('my_model_a')
+        rows:
+          - {id: 1, a: "a"}
+      - input: ref('my_model_b')
+        rows:
+          - {id: 1, b: 2}
+          - {id: 2, b: 2}
+    expect:
+      rows:
+        - {c: 3}
+"""
diff --git a/tests/functional/unit_testing/test_unit_testing.py b/tests/functional/unit_testing/test_unit_testing.py
--- a/tests/functional/unit_testing/test_unit_testing.py
+++ b/tests/functional/unit_testing/test_unit_testing.py
@@ -7,6 +7,7 @@
 from dbt.contracts.results import NodeStatus
 from dbt.exceptions import DuplicateResourceNameError, ParsingError
 from fixtures import (
+    my_model_sql,
     my_model_vars_sql,
     my_model_a_sql,
     my_model_b_sql,
@@ -15,6 +16,7 @@
     my_incremental_model_sql,
     event_sql,
     test_my_model_incremental_yml,
+    test_my_model_yml_invalid,
 )
 
 
@@ -237,3 +239,20 @@ def test_nonexistent_seed(self, project):
             ParsingError, match="Unable to find seed 'test.my_second_favorite_seed' for unit tests"
         ):
             run_dbt(["test", "--select", "my_new_model"], expect_pass=False)
+
+
+class TestUnitTestInvalidInputConfiguration:
+    @pytest.fixture(scope="class")
+    def models(self):
+        return {
+            "my_model.sql": my_model_sql,
+            "my_model_a.sql": my_model_a_sql,
+            "my_model_b.sql": my_model_b_sql,
+            "test_my_model.yml": test_my_model_yml_invalid,
+        }
+
+    def test_invalid_input_configuration(self, project):
+        results = run_dbt(["run"])
+        assert len(results) == 3
+
+        run_dbt(["test"], expect_pass=False)
diff --git a/tests/functional/unit_testing/test_ut_names.py b/tests/functional/unit_testing/test_ut_names.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/unit_testing/test_ut_names.py
@@ -0,0 +1,69 @@
+import pytest
+
+from dbt.tests.util import run_dbt, run_dbt_and_capture
+from dbt.exceptions import DuplicateResourceNameError
+
+from fixtures import (
+    my_model_a_sql,
+    my_model_b_sql,
+    test_model_a_b_yml,
+    test_model_a_with_duplicate_test_name_yml,
+)
+
+
+class TestUnitTestDuplicateTestNamesAcrossModels:
+    @pytest.fixture(scope="class")
+    def models(self):
+        return {
+            "my_model_a.sql": my_model_a_sql,
+            "my_model_b.sql": my_model_b_sql,
+            "test_model_a_b.yml": test_model_a_b_yml,
+        }
+
+    def test_duplicate_test_names_across_models(self, project):
+        results = run_dbt(["run"])
+        assert len(results) == 2
+
+        # Select duplicate tests
+        results, log_output = run_dbt_and_capture(["test"], expect_pass=True)
+        assert len(results) == 2
+        assert ["my_model_a", "my_model_b"] == sorted([result.node.model for result in results])
+        assert "my_model_a::my_test_name" in log_output
+        assert "my_model_b::my_test_name" in log_output
+
+        # Test select duplicates by by test name
+        results = run_dbt(["test", "--select", "test_name:my_test_name"])
+        assert len(results) == 2
+        assert ["my_model_a", "my_model_b"] == sorted([result.node.model for result in results])
+        assert "my_model_a::my_test_name" in log_output
+        assert "my_model_b::my_test_name" in log_output
+
+        results = run_dbt(["test", "--select", "my_model_a,test_name:my_test_name"])
+        assert len(results) == 1
+        assert results[0].node.model == "my_model_a"
+
+        results = run_dbt(["test", "--select", "my_model_b,test_name:my_test_name"])
+        assert len(results) == 1
+        assert results[0].node.model == "my_model_b"
+
+        # Test select by model name
+        results = run_dbt(["test", "--select", "my_model_a"])
+        assert len(results) == 1
+        assert results[0].node.model == "my_model_a"
+
+        results = run_dbt(["test", "--select", "my_model_b"])
+        assert len(results) == 1
+        assert results[0].node.model == "my_model_b"
+
+
+class TestUnitTestDuplicateTestNamesWithinModel:
+    @pytest.fixture(scope="class")
+    def models(self):
+        return {
+            "my_model_a.sql": my_model_a_sql,
+            "test_model_a.yml": test_model_a_with_duplicate_test_name_yml,
+        }
+
+    def test_duplicate_test_names_within_model(self, project):
+        with pytest.raises(DuplicateResourceNameError):
+            run_dbt(["run"])