Repository: dbt-labs/dbt-core

Issue:
[CT-2325] Validate public models are not materialized as ephemeral
Ephemeral materializations shouldn't be made public because there is no database identifier that gets materialized in the data warehouse for use as a 'production pointer' by downstream dbt projects. 

This could be a parse-time error (similar to what's been proposed for https://github.com/dbt-labs/dbt-core/issues/7063#issuecomment-1446746824), or optionally could be a warning surfaced prior to writing a publication artifact that _excludes_ any public models with ephemeral materializations. Either way, public models with ephemeral materializations should _not_ be included as part of the publication artifact because there will be no way to resolve the reference from a downstream project. 




Discussion:

Resolving patch:
diff --git a/core/dbt/exceptions.py b/core/dbt/exceptions.py
--- a/core/dbt/exceptions.py
+++ b/core/dbt/exceptions.py
@@ -1254,12 +1254,15 @@ def get_message(self) -> str:
 
 
 class InvalidAccessTypeError(ParsingError):
-    def __init__(self, unique_id: str, field_value: str):
+    def __init__(self, unique_id: str, field_value: str, materialization: Optional[str] = None):
         self.unique_id = unique_id
         self.field_value = field_value
-        msg = (
-            f"Node {self.unique_id} has an invalid value ({self.field_value}) for the access field"
+        self.materialization = materialization
+
+        with_materialization = (
+            f"with '{self.materialization}' materialization " if self.materialization else ""
         )
+        msg = f"Node {self.unique_id} {with_materialization}has an invalid value ({self.field_value}) for the access field"
         super().__init__(msg=msg)
 
 
diff --git a/core/dbt/parser/manifest.py b/core/dbt/parser/manifest.py
--- a/core/dbt/parser/manifest.py
+++ b/core/dbt/parser/manifest.py
@@ -114,6 +114,7 @@
     AmbiguousAliasError,
     PublicationConfigNotFound,
     ProjectDependencyCycleError,
+    InvalidAccessTypeError,
 )
 from dbt.parser.base import Parser
 from dbt.parser.analysis import AnalysisParser
@@ -529,6 +530,7 @@ def load(self):
             self.process_docs(self.root_project)
             self.process_metrics(self.root_project)
             self.check_valid_group_config()
+            self.check_valid_access_property()
 
             # update tracking data
             self._perf_info.process_manifest_elapsed = time.perf_counter() - start_process
@@ -1293,6 +1295,19 @@ def check_valid_group_config_node(
                 node=groupable_node,
             )
 
+    def check_valid_access_property(self):
+        for node in self.manifest.nodes.values():
+            if (
+                isinstance(node, ModelNode)
+                and node.access == AccessType.Public
+                and node.get_materialization() == "ephemeral"
+            ):
+                raise InvalidAccessTypeError(
+                    unique_id=node.unique_id,
+                    field_value=node.access,
+                    materialization=node.get_materialization(),
+                )
+
     def write_perf_info(self, target_path: str):
         path = os.path.join(target_path, PERF_INFO_FILE_NAME)
         write_file(path, json.dumps(self._perf_info, cls=dbt.utils.JSONEncoder, indent=4))


Test patch:
diff --git a/tests/functional/groups/test_access.py b/tests/functional/groups/test_access.py
--- a/tests/functional/groups/test_access.py
+++ b/tests/functional/groups/test_access.py
@@ -18,6 +18,17 @@
     description: "yet another model"
 """
 
+ephemeral_schema_yml = """
+models:
+  - name: my_model
+    description: "my model"
+    access: public
+    config:
+      materialized: ephemeral
+  - name: another_model
+    description: "yet another model"
+"""
+
 v2_schema_yml = """
 models:
   - name: my_model
@@ -161,11 +172,9 @@ def models(self):
         }
 
     def test_access_attribute(self, project):
+        manifest = run_dbt(["parse"])
+        assert len(manifest.nodes) == 2
 
-        results = run_dbt(["run"])
-        assert len(results) == 2
-
-        manifest = get_manifest(project.project_root)
         my_model_id = "model.test.my_model"
         another_model_id = "model.test.another_model"
         assert my_model_id in manifest.nodes
@@ -174,25 +183,34 @@ def test_access_attribute(self, project):
         assert manifest.nodes[my_model_id].access == AccessType.Public
         assert manifest.nodes[another_model_id].access == AccessType.Protected
 
+        # write a file with invalid materialization for public access value
+        write_file(ephemeral_schema_yml, project.project_root, "models", "schema.yml")
+        with pytest.raises(InvalidAccessTypeError):
+            run_dbt(["parse"])
+
         # write a file with an invalid access value
         write_file(yet_another_model_sql, project.project_root, "models", "yet_another_model.sql")
         write_file(v2_schema_yml, project.project_root, "models", "schema.yml")
 
         with pytest.raises(InvalidAccessTypeError):
-            run_dbt(["run"])
+            run_dbt(["parse"])
+
+        write_file(v2_schema_yml, project.project_root, "models", "schema.yml")
+        with pytest.raises(InvalidAccessTypeError):
+            run_dbt(["parse"])
 
         # Remove invalid access files and write out model that refs my_model
         rm_file(project.project_root, "models", "yet_another_model.sql")
         write_file(schema_yml, project.project_root, "models", "schema.yml")
         write_file(ref_my_model_sql, project.project_root, "models", "ref_my_model.sql")
-        results = run_dbt(["run"])
-        assert len(results) == 3
+        manifest = run_dbt(["parse"])
+        assert len(manifest.nodes) == 3
 
         # make my_model private, set same group on my_model and ref_my_model
         write_file(groups_yml, project.project_root, "models", "groups.yml")
         write_file(v3_schema_yml, project.project_root, "models", "schema.yml")
-        results = run_dbt(["run"])
-        assert len(results) == 3
+        manifest = run_dbt(["parse"])
+        assert len(manifest.nodes) == 3
         manifest = get_manifest(project.project_root)
         ref_my_model_id = "model.test.ref_my_model"
         assert manifest.nodes[my_model_id].group == "analytics"
@@ -201,18 +219,18 @@ def test_access_attribute(self, project):
         # Change group on ref_my_model and it should raise
         write_file(v4_schema_yml, project.project_root, "models", "schema.yml")
         with pytest.raises(DbtReferenceError):
-            run_dbt(["run"])
+            run_dbt(["parse"])
 
         # put back group on ref_my_model, add exposure with ref to private model
         write_file(v3_schema_yml, project.project_root, "models", "schema.yml")
         # verify it works again
-        results = run_dbt(["run"])
-        assert len(results) == 3
+        manifest = run_dbt(["parse"])
+        assert len(manifest.nodes) == 3
         # Write out exposure refing private my_model
         write_file(simple_exposure_yml, project.project_root, "models", "simple_exposure.yml")
         # Fails with reference error
         with pytest.raises(DbtReferenceError):
-            run_dbt(["run"])
+            run_dbt(["parse"])
 
         # Remove exposure and add people model and metric file
         write_file(v5_schema_yml, project.project_root, "models", "schema.yml")
@@ -220,8 +238,8 @@ def test_access_attribute(self, project):
         write_file(people_model_sql, "models", "people_model.sql")
         write_file(people_metric_yml, "models", "people_metric.yml")
         # Should succeed
-        results = run_dbt(["run"])
-        assert len(results) == 4
+        manifest = run_dbt(["parse"])
+        assert len(manifest.nodes) == 4
         manifest = get_manifest(project.project_root)
         metric_id = "metric.test.number_of_people"
         assert manifest.metrics[metric_id].group == "analytics"
@@ -230,4 +248,4 @@ def test_access_attribute(self, project):
         write_file(v2_people_metric_yml, "models", "people_metric.yml")
         # Should raise a reference error
         with pytest.raises(DbtReferenceError):
-            run_dbt(["run"])
+            run_dbt(["parse"])