Repository: dbt-labs/dbt-core

Issue:
[CT-1992] Support `group:` selection method
Groups can be selected (`group:`), similar to the `tag:` or `fqn:` selection methods.


Discussion:

Resolving patch:
diff --git a/core/dbt/graph/selector_methods.py b/core/dbt/graph/selector_methods.py
--- a/core/dbt/graph/selector_methods.py
+++ b/core/dbt/graph/selector_methods.py
@@ -32,6 +32,7 @@
 class MethodName(StrEnum):
     FQN = "fqn"
     Tag = "tag"
+    Group = "group"
     Source = "source"
     Path = "path"
     File = "file"
@@ -143,6 +144,15 @@ def non_source_nodes(
             self.metric_nodes(included_nodes),
         )
 
+    def groupable_nodes(
+        self,
+        included_nodes: Set[UniqueId],
+    ) -> Iterator[Tuple[UniqueId, Union[ManifestNode, Metric]]]:
+        yield from chain(
+            self.parsed_nodes(included_nodes),
+            self.metric_nodes(included_nodes),
+        )
+
     @abc.abstractmethod
     def search(
         self,
@@ -189,6 +199,14 @@ def search(self, included_nodes: Set[UniqueId], selector: str) -> Iterator[Uniqu
                 yield node
 
 
+class GroupSelectorMethod(SelectorMethod):
+    def search(self, included_nodes: Set[UniqueId], selector: str) -> Iterator[UniqueId]:
+        """yields nodes from included in the specified group"""
+        for node, real_node in self.groupable_nodes(included_nodes):
+            if selector == real_node.config.get("group"):
+                yield node
+
+
 class SourceSelectorMethod(SelectorMethod):
     def search(self, included_nodes: Set[UniqueId], selector: str) -> Iterator[UniqueId]:
         """yields nodes from included are the specified source."""
@@ -605,6 +623,7 @@ class MethodManager:
     SELECTOR_METHODS: Dict[MethodName, Type[SelectorMethod]] = {
         MethodName.FQN: QualifiedNameSelectorMethod,
         MethodName.Tag: TagSelectorMethod,
+        MethodName.Group: GroupSelectorMethod,
         MethodName.Source: SourceSelectorMethod,
         MethodName.Path: PathSelectorMethod,
         MethodName.File: FileSelectorMethod,


Test patch:
diff --git a/test/unit/test_graph_selector_methods.py b/test/unit/test_graph_selector_methods.py
--- a/test/unit/test_graph_selector_methods.py
+++ b/test/unit/test_graph_selector_methods.py
@@ -14,6 +14,7 @@
     ModelNode,
     Exposure,
     Metric,
+    Group,
     SeedNode,
     SingularTestNode,
     GenericTestNode,
@@ -30,6 +31,7 @@
     MethodManager,
     QualifiedNameSelectorMethod,
     TagSelectorMethod,
+    GroupSelectorMethod,
     SourceSelectorMethod,
     PathSelectorMethod,
     FileSelectorMethod,
@@ -353,7 +355,7 @@ def make_metric(pkg, name, path=None):
     return Metric(
         name=name,
         resource_type=NodeType.Metric,
-        path='schema.yml',
+        path=path,
         package_name=pkg,
         original_file_path=path,
         unique_id=f'metric.{pkg}.{name}',
@@ -377,6 +379,20 @@ def make_metric(pkg, name, path=None):
     )
 
 
+def make_group(pkg, name, path=None):
+    if path is None:
+        path = 'schema.yml'
+
+    return Group(
+        name=name,
+        resource_type=NodeType.Group,
+        path=path,
+        package_name=pkg,
+        original_file_path=path,
+        unique_id=f'group.{pkg}.{name}',
+        owner='email@gmail.com',
+    )
+
 @pytest.fixture
 def macro_test_unique():
     return make_macro(
@@ -638,6 +654,7 @@ def manifest(seed, source, ephemeral_model, view_model, table_model, table_model
         metrics={},
         disabled=[],
         selectors={},
+        groups={},
     )
     return manifest
 
@@ -677,6 +694,21 @@ def test_select_tag(manifest):
     assert not search_manifest_using_method(manifest, method, 'missing')
 
 
+def test_select_group(manifest, view_model):
+    group_name = 'my_group'
+    group = make_group('test', group_name)
+    manifest.groups[group.unique_id] = group
+    change_node(manifest, view_model.replace(config={'materialized': 'view', 'group': group_name},))
+    methods = MethodManager(manifest, None)
+    method = methods.get_method('group', [])
+    assert isinstance(method, GroupSelectorMethod)
+    assert method.arguments == []
+
+    assert search_manifest_using_method(manifest, method, group_name) == {
+        'view_model'}
+    assert not search_manifest_using_method(manifest, method, 'not_my_group')
+
+
 def test_select_source(manifest):
     methods = MethodManager(manifest, None)
     method = methods.get_method('source', [])
diff --git a/tests/functional/graph_selection/fixtures.py b/tests/functional/graph_selection/fixtures.py
--- a/tests/functional/graph_selection/fixtures.py
+++ b/tests/functional/graph_selection/fixtures.py
@@ -4,19 +4,46 @@
 
 schema_yml = """
 version: 2
+
+groups:
+  - name: emails_group
+    owner:
+      name: Jeremy
+      email: data@jer.co
+      slack: talk-jerco-memes
+      github: jtcohen6
+      whatever: you want
+  - name: users_group
+    owner:
+      name: Jeremy
+      email: data@jer.co
+      slack: talk-jerco-memes
+      github: jtcohen6
+      whatever: you want
+  - name: users_rollup_group
+    owner:
+      name: Jeremy
+      email: data@jer.co
+      slack: talk-jerco-memes
+      github: jtcohen6
+      whatever: you want
+
 models:
   - name: emails
+    group: emails_group
     columns:
     - name: email
       tests:
       - not_null:
           severity: warn
   - name: users
+    group: users_group
     columns:
     - name: id
       tests:
       - unique
   - name: users_rollup
+    group: users_rollup_group
     columns:
     - name: gender
       tests:
diff --git a/tests/functional/graph_selection/test_graph_selection.py b/tests/functional/graph_selection/test_graph_selection.py
--- a/tests/functional/graph_selection/test_graph_selection.py
+++ b/tests/functional/graph_selection/test_graph_selection.py
@@ -75,6 +75,11 @@ def test_tags_and_children_limited(self, project):
         )
         assert_correct_schemas(project)
 
+    def test_group(self, project):
+        expected = ["test.unique_users_id", "test.users"]
+        results = run_dbt(["ls", "--select", "group:users_group"])
+        assert sorted(results) == expected
+
     def test_specific_model_and_children(self, project):
         results = run_dbt(["run", "--select", "users+"], expect_pass=False)
         check_result_nodes_by_name(
diff --git a/tests/functional/graph_selection/test_group_selection.py b/tests/functional/graph_selection/test_group_selection.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/graph_selection/test_group_selection.py
@@ -0,0 +1,123 @@
+import pytest
+
+from dbt.tests.util import run_dbt, read_file
+from tests.functional.graph_selection.fixtures import (
+    schema_yml,
+    base_users_sql,
+    users_sql,
+    users_rollup_sql,
+    users_rollup_dependency_sql,
+    emails_sql,
+    emails_alt_sql,
+    alternative_users_sql,
+    never_selected_sql,
+    subdir_sql,
+    nested_users_sql,
+    properties_yml,
+)
+
+
+selectors_yml = """
+            selectors:
+              - name: group_specified_as_string_str
+                definition: group:users_group
+              - name: group_specified_as_string_dict
+                definition:
+                  method: group
+                  value: users_group
+              - name: users_grouped_childrens_parents
+                definition:
+                  method: group
+                  value: users_group
+                  childrens_parents: true
+"""
+
+
+class TestGroupSelection:
+    @pytest.fixture(scope="class")
+    def models(self):
+        return {
+            "schema.yml": schema_yml,
+            "base_users.sql": base_users_sql,
+            "users.sql": users_sql,
+            "users_rollup.sql": users_rollup_sql,
+            "users_rollup_dependency.sql": users_rollup_dependency_sql,
+            "emails.sql": emails_sql,
+            "emails_alt.sql": emails_alt_sql,
+            "alternative.users.sql": alternative_users_sql,
+            "never_selected.sql": never_selected_sql,
+            "test": {
+                "subdir.sql": subdir_sql,
+                "subdir": {"nested_users.sql": nested_users_sql},
+            },
+        }
+
+    @pytest.fixture(scope="class")
+    def seeds(self, test_data_dir):
+        # Read seed file and return
+        seeds = {"properties.yml": properties_yml}
+        for seed_file in ["seed.csv", "summary_expected.csv"]:
+            seeds[seed_file] = read_file(test_data_dir, seed_file)
+        return seeds
+
+    @pytest.fixture(scope="class")
+    def selectors(self):
+        return selectors_yml
+
+    def test_select_models_by_group(self, project):
+        results = run_dbt(["ls", "--model", "group:users_group"])
+        assert sorted(results) == ["test.users"]
+
+    def test_select_group_selector_str(self, project):
+        results = run_dbt(["ls", "--selector", "group_specified_as_string_str"])
+        assert sorted(results) == ["test.unique_users_id", "test.users"]
+
+    def test_select_group_selector_dict(self, project):
+        results = run_dbt(["ls", "--selector", "group_specified_as_string_dict"])
+        assert sorted(results) == ["test.unique_users_id", "test.users"]
+
+    def test_select_models_by_group_and_children(self, project):  # noqa
+        results = run_dbt(["ls", "--models", "+group:users_group+"])
+        assert sorted(results) == [
+            "test.base_users",
+            "test.emails_alt",
+            "test.users",
+            "test.users_rollup",
+            "test.users_rollup_dependency",
+        ]
+
+    def test_select_group_and_children(self, project):  # noqa
+        expected = [
+            "exposure:test.user_exposure",
+            "source:test.raw.seed",
+            "test.base_users",
+            "test.emails_alt",
+            "test.unique_users_id",
+            "test.unique_users_rollup_gender",
+            "test.users",
+            "test.users_rollup",
+            "test.users_rollup_dependency",
+        ]
+        results = run_dbt(["ls", "--select", "+group:users_group+"])
+        assert sorted(results) == expected
+
+    def test_select_group_and_children_selector_str(self, project):  # noqa
+        expected = [
+            "exposure:test.user_exposure",
+            "source:test.raw.seed",
+            "test.base_users",
+            "test.emails_alt",
+            "test.unique_users_id",
+            "test.unique_users_rollup_gender",
+            "test.users",
+            "test.users_rollup",
+            "test.users_rollup_dependency",
+        ]
+        results = run_dbt(["ls", "--selector", "users_grouped_childrens_parents"])
+        assert sorted(results) == expected
+
+    # 2 groups
+    def test_select_models_two_groups(self, project):
+        expected = ["test.base_users", "test.emails", "test.users"]
+        results = run_dbt(["ls", "--models", "@group:emails_group group:users_group"])
+        assert sorted(results) == expected