Repository: dbt-labs/dbt-core

Issue:
[CT-3041] [Bug] Semantic Model nested element descriptions do not support doc blocks
### 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

The semantic model has entities, measures and dimensions. Each of these can have name, description etc. 
Writing a manual description works, but re-using a predefined description does not work. Compilation fails with 
```
12:51:01  Running with dbt=1.6.0
12:51:02  Registered adapter: databricks=1.6.1
12:51:02  Encountered an error:
Compilation Error
  Could not render {{ doc('orders_created') }}: 'doc' is undefined
```

Yes, I am absolutely sure the doc block referenced does exist as it is used in the metric description as well. 

### Expected Behavior

As everywhere else in dbt, I would expect DRY and I would expect to be able to reuse descriptions.

### Steps To Reproduce

Semantic model that compiles:
```
semantic_models:
  - name: orders
    defaults:
      agg_time_dimension: created_date_local
    description: Some description
    model: ref('orders')
    # Entities. These usually corespond to keys in the table.
    entities:
      - name: order_key
        description: Manual description
        type: primary
      - name: city
        type: foreign
        expr: city_id
    # Measures. These are the aggregations on the columns in the table.
    measures:
      - name: orders_created
        description: Manual description
        expr: 1
        agg: sum
    # Dimensions. Either categorical or time.
    dimensions:
      - name: created_date_local
        description: Manual description
        type: time
        type_params:
          time_granularity: day 
      - name: city_id
        type: categorical
```
and this does not compile
```
semantic_models:
  - name: orders
    defaults:
      agg_time_dimension: created_date_local
    description: Some description
    model: ref('orders')
    # Entities. These usually corespond to keys in the table.
    entities:
      - name: order_key
        description: "{{ doc('orders_created') }}"
        type: primary
      - name: city
        type: foreign
        expr: city_id
    # Measures. These are the aggregations on the columns in the table.
    measures:
      - name: orders_created
        description: "{{ doc('orders_created') }}"
        expr: 1
        agg: sum
    # Dimensions. Either categorical or time.
    dimensions:
      - name: created_date_local
        description: "{{ doc('orders_created') }}"
        type: time
        type_params:
          time_granularity: day 
      - name: city_id
        type: categorical
```
I have tested the descriptions of entities, measures and dimensions separately as well, none of them accept the doc block. 

### Relevant log output

_No response_

### Environment

```markdown
- OS: Mac OS
- Python: 3.9.17
- dbt: 1.6.0
```


### Which database adapter are you using with dbt?

spark

### Additional Context

_No response_


Discussion:
Hey thanks for catching this! We definitely want to support docs blocks in semantic model yml files.
This feels very similar to:
- https://github.com/dbt-labs/dbt-core/issues/8540

Resolving patch:
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
@@ -98,6 +98,7 @@
     ResultNode,
     ModelNode,
     NodeRelation,
+    SemanticModel,
 )
 from dbt.contracts.graph.unparsed import NodeVersion
 from dbt.contracts.util import Writable
@@ -1169,6 +1170,16 @@ def process_docs(self, config: RuntimeConfig):
                 config.project_name,
             )
             _process_docs_for_metrics(ctx, metric)
+        for semantic_model in self.manifest.semantic_models.values():
+            if semantic_model.created_at < self.started_at:
+                continue
+            ctx = generate_runtime_docs_context(
+                config,
+                semantic_model,
+                self.manifest,
+                config.project_name,
+            )
+            _process_docs_for_semantic_model(ctx, semantic_model)
 
     # Loops through all nodes and exposures, for each element in
     # 'sources' array finds the source node and updates the
@@ -1398,6 +1409,25 @@ def _process_docs_for_metrics(context: Dict[str, Any], metric: Metric) -> None:
     metric.description = get_rendered(metric.description, context)
 
 
+def _process_docs_for_semantic_model(
+    context: Dict[str, Any], semantic_model: SemanticModel
+) -> None:
+    if semantic_model.description:
+        semantic_model.description = get_rendered(semantic_model.description, context)
+
+    for dimension in semantic_model.dimensions:
+        if dimension.description:
+            dimension.description = get_rendered(dimension.description, context)
+
+    for measure in semantic_model.measures:
+        if measure.description:
+            measure.description = get_rendered(measure.description, context)
+
+    for entity in semantic_model.entities:
+        if entity.description:
+            entity.description = get_rendered(entity.description, context)
+
+
 def _process_refs(
     manifest: Manifest, current_project: str, node, dependencies: Optional[Mapping[str, Project]]
 ) -> None:
diff --git a/core/dbt/parser/schema_renderer.py b/core/dbt/parser/schema_renderer.py
--- a/core/dbt/parser/schema_renderer.py
+++ b/core/dbt/parser/schema_renderer.py
@@ -42,7 +42,7 @@ def _is_norender_key(self, keypath: Keypath) -> bool:
 
         if (
             len(keypath) >= 3
-            and keypath[0] == "columns"
+            and keypath[0] in ("columns", "dimensions", "measures", "entities")
             and keypath[2] in ("tests", "description")
         ):
             return True


Test patch:
diff --git a/tests/functional/semantic_models/fixtures.py b/tests/functional/semantic_models/fixtures.py
--- a/tests/functional/semantic_models/fixtures.py
+++ b/tests/functional/semantic_models/fixtures.py
@@ -86,6 +86,44 @@
       agg_time_dimension: created_at
 """
 
+semantic_model_descriptions = """
+{% docs semantic_model_description %} foo {% enddocs %}
+{% docs dimension_description %} bar {% enddocs %}
+{% docs measure_description %} baz {% enddocs %}
+{% docs entity_description %} qux {% enddocs %}
+"""
+
+semantic_model_people_yml_with_docs = """
+version: 2
+
+semantic_models:
+  - name: semantic_people
+    model: ref('people')
+    description: "{{ doc('semantic_model_description') }}"
+    dimensions:
+      - name: favorite_color
+        type: categorical
+        description: "{{ doc('dimension_description') }}"
+      - name: created_at
+        type: TIME
+        type_params:
+          time_granularity: day
+    measures:
+      - name: years_tenure
+        agg: SUM
+        expr: tenure
+        description: "{{ doc('measure_description') }}"
+      - name: people
+        agg: count
+        expr: id
+    entities:
+      - name: id
+        description: "{{ doc('entity_description') }}"
+        type: primary
+    defaults:
+      agg_time_dimension: created_at
+"""
+
 enabled_semantic_model_people_yml = """
 version: 2
 
diff --git a/tests/functional/semantic_models/test_semantic_models.py b/tests/functional/semantic_models/test_semantic_models.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/semantic_models/test_semantic_models.py
@@ -0,0 +1,73 @@
+import pytest
+
+from dbt.contracts.graph.manifest import Manifest
+from dbt.exceptions import CompilationError
+from dbt.tests.util import run_dbt
+from tests.functional.semantic_models.fixtures import (
+    models_people_sql,
+    simple_metricflow_time_spine_sql,
+    semantic_model_people_yml,
+    models_people_metrics_yml,
+    semantic_model_people_yml_with_docs,
+    semantic_model_descriptions,
+)
+
+
+class TestSemanticModelDependsOn:
+    @pytest.fixture(scope="class")
+    def models(self):
+        return {
+            "people.sql": models_people_sql,
+            "metricflow_time_spine.sql": simple_metricflow_time_spine_sql,
+            "semantic_models.yml": semantic_model_people_yml,
+            "people_metrics.yml": models_people_metrics_yml,
+        }
+
+    def test_depends_on(self, project):
+        manifest = run_dbt(["parse"])
+        assert isinstance(manifest, Manifest)
+
+        expected_depends_on_for_people_semantic_model = ["model.test.people"]
+
+        number_of_people_metric = manifest.semantic_models["semantic_model.test.semantic_people"]
+        assert (
+            number_of_people_metric.depends_on.nodes
+            == expected_depends_on_for_people_semantic_model
+        )
+
+
+class TestSemanticModelNestedDocs:
+    @pytest.fixture(scope="class")
+    def models(self):
+        return {
+            "people.sql": models_people_sql,
+            "metricflow_time_spine.sql": simple_metricflow_time_spine_sql,
+            "semantic_models.yml": semantic_model_people_yml_with_docs,
+            "people_metrics.yml": models_people_metrics_yml,
+            "docs.md": semantic_model_descriptions,
+        }
+
+    def test_depends_on(self, project):
+        manifest = run_dbt(["parse"])
+        node = manifest.semantic_models["semantic_model.test.semantic_people"]
+
+        assert node.description == "foo"
+        assert node.dimensions[0].description == "bar"
+        assert node.measures[0].description == "baz"
+        assert node.entities[0].description == "qux"
+
+
+class TestSemanticModelUnknownModel:
+    @pytest.fixture(scope="class")
+    def models(self):
+        return {
+            "not_people.sql": models_people_sql,
+            "metricflow_time_spine.sql": simple_metricflow_time_spine_sql,
+            "semantic_models.yml": semantic_model_people_yml,
+            "people_metrics.yml": models_people_metrics_yml,
+        }
+
+    def test_unknown_model_raises_issue(self, project):
+        with pytest.raises(CompilationError) as excinfo:
+            run_dbt(["parse"])
+        assert "depends on a node named 'people' which was not found" in str(excinfo.value)