Repository: dbt-labs/dbt-core

Issue:
[CT-64] Model `meta` missing from top-level node `meta` in manifest
### Describe the bug
A clear and concise description of what the bug is. What command did you run? What happened?

### Steps To Reproduce
Add any meta tags to a model. e.g.
```
{{ config(
    meta = {
    'single_key': 'override'
    }
    ) 
}}
```

Or similarly specified using .yml schemas.

### Expected behavior
Expect this to show up like it does in "Exposures" as a table detail formatted key-value list (also like tags does)

How it looks in a model:
![image](https://user-images.githubusercontent.com/22769102/145233967-2b936966-ba9a-4ca8-903d-bfac41994c38.png)
How it is expected to look (like it does implemented the same in an exposure):
![image](https://user-images.githubusercontent.com/22769102/145234238-8302eb08-23b1-4d9d-979d-f9e61b8656dd.png)

### Screenshots and log output

**The output of `dbt --version`:**
```
installed version: 0.21.0
   latest version: 1.0.0

Plugins:
  - spark: 0.21.0
  - postgres: 0.21.0
  - redshift: 0.21.0
  - bigquery: 0.21.0
  - snowflake: 0.21.0
```


Discussion:
@troyel Thanks for opening!

I think this is actually a bug in `dbt-core`, stemming from the config/property changes in v0.21. When defining `meta` for a model, the key is added (and resolved) to `config.meta`, but it isn't mirrored to the top-level `meta` key on the model object.

If I check in `manifest.json`, I see:
```json
"model.my_project.my_model": {
  "config": {
    "enabled": true,
    "alias": null,
    "schema": null,
    "database": null,
    "tags": [],
    "meta": {
      "my_key": "my_value"
    },
  },
  "meta": {},
}
```

@gshank Do you have a sense of where that mirroring ought to be happening? I do think it's reasonable for downstream metadata consumers (including the `dbt-docs` site) to expect those values in `node.meta`, rather than the more deeply nested `node.config.meta`.

I'm going to transfer this one to `dbt-core`
related to the same problem, consumers of the `graph` jinja variable also have access to meta tag information of a model in `graph["model_name"].config.meta` rather than `graph["model_name"].meta`
The 'meta' dictionary is copied to the top-level node in core/dbt/parser/base.py, update_parsed_node_config. I would have thought that would be called for config in a model. Clearly there's some hole here.
@jtcohen6 continuing this from https://github.com/dbt-labs/dbt-docs/issues/230 - is there some sense in not having to duplicate the meta config to the top level meta for this?

Just thinking out loud since typically `manifest.json` gets to be quite large plus now we have to maintain logic that does the `node.config.meta` bits as well as the `node.meta` bits. On top of that we may introduce new `config` that we similarly want to display in the future thus having to duplicate them too. Feels a bit wasteful. But I suppose as you pointed out we want to keep to some expected behaviour (_editors note: but at the cost of manifest size and additional complexity_) and I'm certainly missing additional context on why we need to have this meta repeated.
+1 in dbt Core version 1.0.1 in dbt Cloud

User wants to add 'owner' and 'group' [meta](https://docs.getdbt.com/reference/resource-configs/meta) fields at the model level, but they are not  visible in the 'details' section of the docs site.

Let me know if there's any additional context I can provide!

Resolving patch:
diff --git a/core/dbt/contracts/graph/parsed.py b/core/dbt/contracts/graph/parsed.py
--- a/core/dbt/contracts/graph/parsed.py
+++ b/core/dbt/contracts/graph/parsed.py
@@ -153,7 +153,6 @@ def patch(self, patch: 'ParsedNodePatch'):
         self.created_at = time.time()
         self.description = patch.description
         self.columns = patch.columns
-        self.meta = patch.meta
         self.docs = patch.docs
 
     def get_materialization(self):
diff --git a/core/dbt/parser/snapshots.py b/core/dbt/parser/snapshots.py
--- a/core/dbt/parser/snapshots.py
+++ b/core/dbt/parser/snapshots.py
@@ -63,8 +63,14 @@ def get_fqn(self, path: str, name: str) -> List[str]:
 
     def transform(self, node: IntermediateSnapshotNode) -> ParsedSnapshotNode:
         try:
+            # The config_call_dict is not serialized, because normally
+            # it is not needed after parsing. But since the snapshot node
+            # does this extra to_dict, save and restore it, to keep
+            # the model config when there is also schema config.
+            config_call_dict = node.config_call_dict
             dct = node.to_dict(omit_none=True)
             parsed_node = ParsedSnapshotNode.from_dict(dct)
+            parsed_node.config_call_dict = config_call_dict
             self.set_snapshot_attributes(parsed_node)
             return parsed_node
         except ValidationError as exc:


Test patch:
diff --git a/test/integration/004_simple_snapshot_test/models/schema.yml b/test/integration/004_simple_snapshot_test/models/schema.yml
--- a/test/integration/004_simple_snapshot_test/models/schema.yml
+++ b/test/integration/004_simple_snapshot_test/models/schema.yml
@@ -3,3 +3,6 @@ snapshots:
   - name: snapshot_actual
     tests:
       - mutually_exclusive_ranges
+    config:
+      meta:
+        owner: 'a_owner'
diff --git a/test/integration/039_config_test/test_configs_in_schema_files.py b/test/integration/039_config_test/test_configs_in_schema_files.py
--- a/test/integration/039_config_test/test_configs_in_schema_files.py
+++ b/test/integration/039_config_test/test_configs_in_schema_files.py
@@ -65,6 +65,9 @@ def test_postgres_config_layering(self):
         manifest = get_manifest()
         model_id = 'model.test.model'
         model_node = manifest.nodes[model_id]
+        meta_expected = {'company': 'NuMade', 'project': 'test', 'team': 'Core Team', 'owner': 'Julie Smith', 'my_attr': 'TESTING'}
+        self.assertEqual(model_node.meta, meta_expected)
+        self.assertEqual(model_node.config.meta, meta_expected)
         model_tags = ['tag_1_in_model', 'tag_2_in_model', 'tag_in_project', 'tag_in_schema']
         model_node_tags = model_node.tags.copy()
         model_node_tags.sort()