Repository: dbt-labs/dbt-core

Issue:
No error thrown on circular ref
I have a case where dbt is not raising any kind of error on a circular ref. The circular ref includes ephemeral models, so that could be the culprit.


Discussion:
@cmcarthur we just fixed something like this in development (soon to be 091). Let's test against that and see if it still doesn't raise

Resolving patch:
diff --git a/dbt/linker.py b/dbt/linker.py
--- a/dbt/linker.py
+++ b/dbt/linker.py
@@ -32,13 +32,16 @@ def get_node(self, node):
         return self.graph.node[node]
 
     def find_cycles(self):
-        try:
-            cycles = nx.algorithms.find_cycle(self.graph)
-        except nx.exception.NetworkXNoCycle:
-            return None
-
-        if cycles:
-            return " --> ".join([".".join(node) for node in cycles])
+        # There's a networkx find_cycle function, but there's a bug in the
+        # nx 1.11 release that prevents us from using it. We should use that
+        # function when we upgrade to 2.X. More info:
+        #     https://github.com/networkx/networkx/pull/2473
+        cycles = list(nx.simple_cycles(self.graph))
+
+        if len(cycles) > 0:
+            cycle_nodes = cycles[0]
+            cycle_nodes.append(cycle_nodes[0])
+            return " --> ".join(cycle_nodes)
 
         return None
 


Test patch:
diff --git a/test/integration/027_cycle_test/complex_cycle_models/model_a.sql b/test/integration/027_cycle_test/complex_cycle_models/model_a.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/027_cycle_test/complex_cycle_models/model_a.sql
@@ -0,0 +1,2 @@
+
+select 1 as id
diff --git a/test/integration/027_cycle_test/complex_cycle_models/model_b.sql b/test/integration/027_cycle_test/complex_cycle_models/model_b.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/027_cycle_test/complex_cycle_models/model_b.sql
@@ -0,0 +1,4 @@
+
+select * from {{ ref('model_a') }}
+union all
+select * from {{ ref('model_e') }}
diff --git a/test/integration/027_cycle_test/complex_cycle_models/model_c.sql b/test/integration/027_cycle_test/complex_cycle_models/model_c.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/027_cycle_test/complex_cycle_models/model_c.sql
@@ -0,0 +1,2 @@
+
+select * from {{ ref('model_b') }}
diff --git a/test/integration/027_cycle_test/complex_cycle_models/model_d.sql b/test/integration/027_cycle_test/complex_cycle_models/model_d.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/027_cycle_test/complex_cycle_models/model_d.sql
@@ -0,0 +1,2 @@
+
+select * from {{ ref('model_c') }}
diff --git a/test/integration/027_cycle_test/complex_cycle_models/model_e.sql b/test/integration/027_cycle_test/complex_cycle_models/model_e.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/027_cycle_test/complex_cycle_models/model_e.sql
@@ -0,0 +1,2 @@
+
+select * from {{ ref('model_e') }}
diff --git a/test/integration/027_cycle_test/complex_cycle_models/readme b/test/integration/027_cycle_test/complex_cycle_models/readme
new file mode 100644
--- /dev/null
+++ b/test/integration/027_cycle_test/complex_cycle_models/readme
@@ -0,0 +1,7 @@
+
+The cycle in this graph looks like:
+
+A -> B -> C -> D
+     ^         |
+     |         |
+     +--- E <--+
diff --git a/test/integration/027_cycle_test/simple_cycle_models/model_a.sql b/test/integration/027_cycle_test/simple_cycle_models/model_a.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/027_cycle_test/simple_cycle_models/model_a.sql
@@ -0,0 +1,2 @@
+
+select * from {{ ref('model_b') }}
diff --git a/test/integration/027_cycle_test/simple_cycle_models/model_b.sql b/test/integration/027_cycle_test/simple_cycle_models/model_b.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/027_cycle_test/simple_cycle_models/model_b.sql
@@ -0,0 +1,2 @@
+
+select * from {{ ref('model_a') }}
diff --git a/test/integration/027_cycle_test/test_cycles.py b/test/integration/027_cycle_test/test_cycles.py
new file mode 100644
--- /dev/null
+++ b/test/integration/027_cycle_test/test_cycles.py
@@ -0,0 +1,44 @@
+from freezegun import freeze_time
+from nose.plugins.attrib import attr
+from test.integration.base import DBTIntegrationTest
+
+
+class TestSimpleCycle(DBTIntegrationTest):
+
+    def setUp(self):
+        DBTIntegrationTest.setUp(self)
+
+    @property
+    def schema(self):
+        return "cycles_simple_025"
+
+    @property
+    def models(self):
+        return "test/integration/027_cycle_test/simple_cycle_models"
+
+    @property
+    @attr(type='postgres')
+    def test_simple_cycle(self):
+        message = "Found a cycle.*"
+        with self.assertRaisesRegexp(Exception, message):
+            self.run_dbt(["run"])
+
+class TestComplexCycle(DBTIntegrationTest):
+
+    def setUp(self):
+        DBTIntegrationTest.setUp(self)
+
+    @property
+    def schema(self):
+        return "cycles_complex_025"
+
+    @property
+    def models(self):
+        return "test/integration/027_cycle_test/complex_cycle_models"
+
+    @property
+    @attr(type='postgres')
+    def test_simple_cycle(self):
+        message = "Found a cycle.*"
+        with self.assertRaisesRegexp(Exception, message):
+            self.run_dbt(["run"])