Repository: dbt-labs/dbt-core

Issue:
In `build`, tests should always block downstream, even when parent sets are equal
Last one, I promise. I should've caught this a while ago, but better late than never.
```sql
-- models/model_a.sql
select null as id
```
```sql
-- models/model_b.sql
select * from {{ ref('model_b') }}
```
```yml
-- models/schema.yml
version: 2
models:
  - name: model_a
    columns:
      - name: id
        tests:
          - not_null
```

I'd expect:
```
21:32:06 | 1 of 3 START view model dbt_jcohen.model_a........................... [RUN]
21:32:06 | 1 of 3 OK created view model dbt_jcohen.model_a...................... [CREATE VIEW in 0.06s]
21:32:06 | 2 of 3 START test not_null_model_a_id................................ [RUN]
21:32:06 | 2 of 3 FAIL 1 not_null_model_a_id.................................... [FAIL 1 in 0.03s]
21:32:06 | 3 of 3 SKIP relation dbt_jcohen.model_b.............................. [SKIP]
```

But in v0.21.0, it's:
```
21:31:38 | 1 of 3 START view model dbt_jcohen.model_a........................... [RUN]
21:31:38 | 1 of 3 OK created view model dbt_jcohen.model_a...................... [CREATE VIEW in 0.06s]
21:31:38 | 2 of 3 START view model dbt_jcohen.model_b........................... [RUN]
21:31:38 | 3 of 3 START test not_null_model_a_id................................ [RUN]
21:31:38 | 3 of 3 FAIL 1 not_null_model_a_id.................................... [FAIL 1 in 0.04s]
21:31:38 | 2 of 3 OK created view model dbt_jcohen.model_b...................... [CREATE VIEW in 0.05s]
```

The issue comes up because we require a strict/proper subset when adding dependency links. Because `model_b` and the test `not_null_model_a_id` have the same set of parents (`model_a`), the test's dependencies aren't a strict subset of the model's dependencies, and we don't add a link between `not_null_model_a_id --> model_b`.

There's a reason we opted for strict/proper subset, but I'm not exactly sure what it is, and I've managed to get all our tests passing when I change the logic to use a non-strict/improper subset.

If I'm on the right track (never guaranteed!), I'd love to fix this for v0.21. It really threw me for a loop earlier today, and it goes against the documented/expected behavior of `build`.


Discussion:

Resolving patch:
diff --git a/core/dbt/compilation.py b/core/dbt/compilation.py
--- a/core/dbt/compilation.py
+++ b/core/dbt/compilation.py
@@ -438,22 +438,22 @@ def link_graph(self, linker: Linker, manifest: Manifest, add_test_edges: bool =
     def add_test_edges(self, linker: Linker, manifest: Manifest) -> None:
         """ This method adds additional edges to the DAG. For a given non-test
         executable node, add an edge from an upstream test to the given node if
-        the set of nodes the test depends on is a proper/strict subset of the
-        upstream nodes for the given node. """
+        the set of nodes the test depends on is a subset of the upstream nodes
+        for the given node. """
 
         # Given a graph:
         # model1 --> model2 --> model3
-        #   |         |
-        #   |        \/
-        #  \/      test 2
+        #   |             |
+        #   |            \/
+        #  \/          test 2
         # test1
         #
         # Produce the following graph:
         # model1 --> model2 --> model3
-        #   |         |         /\ /\
-        #   |        \/         |  |
-        #  \/      test2 -------   |
-        # test1 -------------------
+        #   |       /\    |      /\ /\
+        #   |       |    \/      |  |
+        #  \/       |  test2 ----|  |
+        # test1 ----|---------------|
 
         for node_id in linker.graph:
             # If node is executable (in manifest.nodes) and does _not_
@@ -491,11 +491,9 @@ def add_test_edges(self, linker: Linker, manifest: Manifest) -> None:
                     )
 
                     # If the set of nodes that an upstream test depends on
-                    # is a proper (or strict) subset of all upstream nodes of
-                    # the current node, add an edge from the upstream test
-                    # to the current node. Must be a proper/strict subset to
-                    # avoid adding a circular dependency to the graph.
-                    if (test_depends_on < upstream_nodes):
+                    # is a subset of all upstream nodes of the current node,
+                    # add an edge from the upstream test to the current node.
+                    if (test_depends_on.issubset(upstream_nodes)):
                         linker.graph.add_edge(
                             upstream_test,
                             node_id


Test patch:
diff --git a/test/integration/069_build_test/models-simple-blocking/model_a.sql b/test/integration/069_build_test/models-simple-blocking/model_a.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/069_build_test/models-simple-blocking/model_a.sql
@@ -0,0 +1 @@
+select null as id
diff --git a/test/integration/069_build_test/models-simple-blocking/model_b.sql b/test/integration/069_build_test/models-simple-blocking/model_b.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/069_build_test/models-simple-blocking/model_b.sql
@@ -0,0 +1 @@
+select * from {{ ref('model_a') }}
diff --git a/test/integration/069_build_test/models-simple-blocking/schema.yml b/test/integration/069_build_test/models-simple-blocking/schema.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/069_build_test/models-simple-blocking/schema.yml
@@ -0,0 +1,8 @@
+version: 2
+
+models:
+  - name: model_a
+    columns:
+      - name: id
+        tests:
+          - not_null
diff --git a/test/integration/069_build_test/test_build.py b/test/integration/069_build_test/test_build.py
--- a/test/integration/069_build_test/test_build.py
+++ b/test/integration/069_build_test/test_build.py
@@ -82,6 +82,29 @@ def test__postgres_circular_relationship_test_success(self):
         expected = ['success']*7 + ['pass']*2
         self.assertEqual(sorted(actual), sorted(expected))
 
+
+class TestSimpleBlockingTest(TestBuildBase):
+    @property
+    def models(self):
+        return "models-simple-blocking"
+        
+    @property
+    def project_config(self):
+        return {
+            "config-version": 2,
+            "snapshot-paths": ["does-not-exist"],
+            "seed-paths": ["does-not-exist"],
+        }
+
+    @use_profile("postgres")
+    def test__postgres_simple_blocking_test(self):
+        """ Ensure that a failed test on model_a always blocks model_b """
+        results = self.build(expect_pass=False)
+        actual = [r.status for r in results]
+        expected = ['success', 'fail', 'skipped']
+        self.assertEqual(sorted(actual), sorted(expected))
+
+
 class TestInterdependentModels(TestBuildBase):
 
     @property