Repository: dbt-labs/dbt-core

Issue:
configured hooks are eagerly rendered when prefixed with a `+`
### Describe the bug
When using config-version 2, users can prefix a config item with a `+` to make sure it isn't treated as a part of the "config hierarchy". While it's only important for dictionaries, it's accepted for any key inside a resource configuration block (`models`, `seeds`, etc). However, prefixing `pre-hook` or `post-hook` to make them `+pre-hook: ...` or `+post-hook: ...` caused them to be rendered during config parsing, instead of being deferred to runtime. 

### Steps To Reproduce
In `dbt_project.yml`:
```yml
name: test
version: '1.0'
config-version: 2
models:
  test:
    +post-hook:
      - grant select on {{ this }} to some_user
```

Your post-hooks will run without the `this`.

### Expected behavior
`+post-hook` should behave exactly like `post-hook`.

Note that I don't necessarily expect top-level fields like `on-run-start` to support the `+` prefix.

### System information
Any database, any python, `dbt==0.17.0`




Discussion:

Resolving patch:
diff --git a/core/dbt/config/renderer.py b/core/dbt/config/renderer.py
--- a/core/dbt/config/renderer.py
+++ b/core/dbt/config/renderer.py
@@ -78,7 +78,7 @@ def should_render_keypath_v1(self, keypath: Keypath) -> bool:
         if first in {'on-run-start', 'on-run-end', 'query-comment'}:
             return False
         # models have two things to avoid
-        if first in {'seeds', 'models', 'snapshots', 'seeds'}:
+        if first in {'seeds', 'models', 'snapshots'}:
             # model-level hooks
             if 'pre-hook' in keypath or 'post-hook' in keypath:
                 return False
@@ -102,11 +102,12 @@ def should_render_keypath_v2(self, keypath: Keypath) -> bool:
             return False
 
         if first in {'seeds', 'models', 'snapshots', 'seeds'}:
+            keypath_parts = {
+                (k.lstrip('+') if isinstance(k, str) else k)
+                for k in keypath
+            }
             # model-level hooks
-            if 'pre-hook' in keypath or 'post-hook' in keypath:
-                return False
-            # model-level 'vars' declarations
-            if 'vars' in keypath:
+            if 'pre-hook' in keypath_parts or 'post-hook' in keypath_parts:
                 return False
 
         return True


Test patch:
diff --git a/test/integration/014_hook_tests/test_model_hooks.py b/test/integration/014_hook_tests/test_model_hooks.py
--- a/test/integration/014_hook_tests/test_model_hooks.py
+++ b/test/integration/014_hook_tests/test_model_hooks.py
@@ -226,6 +226,23 @@ def test_postgres_hooks_on_seeds(self):
         self.assertEqual(len(res), 1, 'Expected exactly one item')
 
 
+class TestPrePostModelHooksOnSeedsPlusPrefixed(TestPrePostModelHooksOnSeeds):
+    @property
+    def project_config(self):
+        return {
+            'config-version': 2,
+            'data-paths': ['data'],
+            'models': {},
+            'seeds': {
+                '+post-hook': [
+                    'alter table {{ this }} add column new_col int',
+                    'update {{ this }} set new_col = 1'
+                ],
+                'quote_columns': False,
+            },
+        }
+
+
 class TestPrePostModelHooksOnSnapshots(DBTIntegrationTest):
     @property
     def schema(self):
diff --git a/test/integration/048_rpc_test/test_rpc.py b/test/integration/048_rpc_test/test_rpc.py
--- a/test/integration/048_rpc_test/test_rpc.py
+++ b/test/integration/048_rpc_test/test_rpc.py
@@ -66,7 +66,7 @@ def is_up(self):
 
     def start(self):
         super().start()
-        for _ in range(30):
+        for _ in range(60):
             if self.is_up():
                 break
             time.sleep(0.5)