Repository: dbt-labs/dbt-core

Issue:
create config aliases for pre-hook and post-hook
## Feature

### Feature description
Pre-hooks and post-hooks can be specified in a `config()` block, but they require the use of a dictionary instead of a list of keyword arguments. Instead, dbt should support the specification of these hooks using `pre_hook` and `post_hook` aliases.

Before:
```
{{
  config({
    "post-hook" : "grant select on {{ this }} to db_reader"
  })
}}
```

After:
```
{{
  config(
    post_hook="grant select on {{ this }} to db_reader"
  )
}}
```


### Who will this benefit?
This will be a quality of life improvement for many dbt users :)


Discussion:

Resolving patch:
diff --git a/dbt/context/parser.py b/dbt/context/parser.py
--- a/dbt/context/parser.py
+++ b/dbt/context/parser.py
@@ -50,6 +50,19 @@ def __init__(self, model, source_config):
         self.model = model
         self.source_config = source_config
 
+    def _transform_config(self, config):
+        for oldkey in ('pre_hook', 'post_hook'):
+            if oldkey in config:
+                newkey = oldkey.replace('_', '-')
+                if newkey in config:
+                    dbt.exceptions.raise_compiler_error(
+                        'Invalid config, has conflicting keys "{}" and "{}"'
+                        .format(oldkey, newkey),
+                        self.model
+                    )
+                config[newkey] = config.pop(oldkey)
+        return config
+
     def __call__(self, *args, **kwargs):
         if len(args) == 1 and len(kwargs) == 0:
             opts = args[0]
@@ -60,6 +73,8 @@ def __call__(self, *args, **kwargs):
                 "Invalid inline model config",
                 self.model)
 
+        opts = self._transform_config(opts)
+
         self.source_config.update_in_model_config(opts)
         return ''
 


Test patch:
diff --git a/test/integration/014_hook_tests/configured-models/hooks.sql b/test/integration/014_hook_tests/configured-models/hooks.sql
--- a/test/integration/014_hook_tests/configured-models/hooks.sql
+++ b/test/integration/014_hook_tests/configured-models/hooks.sql
@@ -1,7 +1,7 @@
 
 {{
     config({
-        "pre-hook": "\
+        "pre_hook": "\
             insert into {{this.schema}}.on_model_hook (\
                 \"state\",\
                 \"target.dbname\",\
diff --git a/test/integration/014_hook_tests/error-models/hooks.sql b/test/integration/014_hook_tests/error-models/hooks.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/014_hook_tests/error-models/hooks.sql
@@ -0,0 +1,91 @@
+
+{{
+    config({
+        "pre_hook": "\
+            insert into {{this.schema}}.on_model_hook (\
+                \"state\",\
+                \"target.dbname\",\
+                \"target.host\",\
+                \"target.name\",\
+                \"target.schema\",\
+                \"target.type\",\
+                \"target.user\",\
+                \"target.pass\",\
+                \"target.port\",\
+                \"target.threads\",\
+                \"run_started_at\",\
+                \"invocation_id\"\
+            ) VALUES (\
+                'start',\
+                '{{ target.dbname }}',\
+                '{{ target.host }}',\
+                '{{ target.name }}',\
+                '{{ target.schema }}',\
+                '{{ target.type }}',\
+                '{{ target.user }}',\
+                '{{ target.pass }}',\
+                {{ target.port }},\
+                {{ target.threads }},\
+                '{{ run_started_at }}',\
+                '{{ invocation_id }}'\
+        )",
+        "pre-hook": "\
+            insert into {{this.schema}}.on_model_hook (\
+                \"state\",\
+                \"target.dbname\",\
+                \"target.host\",\
+                \"target.name\",\
+                \"target.schema\",\
+                \"target.type\",\
+                \"target.user\",\
+                \"target.pass\",\
+                \"target.port\",\
+                \"target.threads\",\
+                \"run_started_at\",\
+                \"invocation_id\"\
+            ) VALUES (\
+                'start',\
+                '{{ target.dbname }}',\
+                '{{ target.host }}',\
+                '{{ target.name }}',\
+                '{{ target.schema }}',\
+                '{{ target.type }}',\
+                '{{ target.user }}',\
+                '{{ target.pass }}',\
+                {{ target.port }},\
+                {{ target.threads }},\
+                '{{ run_started_at }}',\
+                '{{ invocation_id }}'\
+        )",
+        "post-hook": "\
+            insert into {{this.schema}}.on_model_hook (\
+                \"state\",\
+                \"target.dbname\",\
+                \"target.host\",\
+                \"target.name\",\
+                \"target.schema\",\
+                \"target.type\",\
+                \"target.user\",\
+                \"target.pass\",\
+                \"target.port\",\
+                \"target.threads\",\
+                \"run_started_at\",\
+                \"invocation_id\"\
+            ) VALUES (\
+                'end',\
+                '{{ target.dbname }}',\
+                '{{ target.host }}',\
+                '{{ target.name }}',\
+                '{{ target.schema }}',\
+                '{{ target.type }}',\
+                '{{ target.user }}',\
+                '{{ target.pass }}',\
+                {{ target.port }},\
+                {{ target.threads }},\
+                '{{ run_started_at }}',\
+                '{{ invocation_id }}'\
+            )"
+    })
+}}
+
+select 3 as id
diff --git a/test/integration/014_hook_tests/kwargs-models/hooks.sql b/test/integration/014_hook_tests/kwargs-models/hooks.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/014_hook_tests/kwargs-models/hooks.sql
@@ -0,0 +1,63 @@
+
+{{
+    config(
+        pre_hook="\
+            insert into {{this.schema}}.on_model_hook (\
+                \"state\",\
+                \"target.dbname\",\
+                \"target.host\",\
+                \"target.name\",\
+                \"target.schema\",\
+                \"target.type\",\
+                \"target.user\",\
+                \"target.pass\",\
+                \"target.port\",\
+                \"target.threads\",\
+                \"run_started_at\",\
+                \"invocation_id\"\
+            ) VALUES (\
+                'start',\
+                '{{ target.dbname }}',\
+                '{{ target.host }}',\
+                '{{ target.name }}',\
+                '{{ target.schema }}',\
+                '{{ target.type }}',\
+                '{{ target.user }}',\
+                '{{ target.pass }}',\
+                {{ target.port }},\
+                {{ target.threads }},\
+                '{{ run_started_at }}',\
+                '{{ invocation_id }}'\
+        )",
+        post_hook="\
+            insert into {{this.schema}}.on_model_hook (\
+                \"state\",\
+                \"target.dbname\",\
+                \"target.host\",\
+                \"target.name\",\
+                \"target.schema\",\
+                \"target.type\",\
+                \"target.user\",\
+                \"target.pass\",\
+                \"target.port\",\
+                \"target.threads\",\
+                \"run_started_at\",\
+                \"invocation_id\"\
+            ) VALUES (\
+                'end',\
+                '{{ target.dbname }}',\
+                '{{ target.host }}',\
+                '{{ target.name }}',\
+                '{{ target.schema }}',\
+                '{{ target.type }}',\
+                '{{ target.user }}',\
+                '{{ target.pass }}',\
+                {{ target.port }},\
+                {{ target.threads }},\
+                '{{ run_started_at }}',\
+                '{{ invocation_id }}'\
+            )"
+    )
+}}
+
+select 3 as id
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
@@ -1,5 +1,6 @@
 from nose.plugins.attrib import attr
 from test.integration.base import DBTIntegrationTest
+from dbt.exceptions import CompilationException
 
 
 MODEL_PRE_HOOK = """
@@ -64,7 +65,6 @@
 
 class BaseTestPrePost(DBTIntegrationTest):
     def setUp(self):
-        self.adapter_type = 'bigquery'
         DBTIntegrationTest.setUp(self)
 
         self.run_sql_file("test/integration/014_hook_tests/seed_model.sql")
@@ -147,7 +147,7 @@ def models(self):
         return "test/integration/014_hook_tests/models"
 
     @attr(type='postgres')
-    def test_pre_and_post_model_hooks(self):
+    def test_postgres_pre_and_post_model_hooks(self):
         self.run_dbt(['run'])
 
         self.check_hooks('start')
@@ -177,7 +177,7 @@ def project_config(self):
         }
 
     @attr(type='postgres')
-    def test_hooks_on_seeds(self):
+    def test_postgres_hooks_on_seeds(self):
         res = self.run_dbt(['seed'])
         self.assertEqual(len(res), 1, 'Expected exactly one item')
         res = self.run_dbt(['test'])
@@ -196,14 +196,14 @@ def models(self):
         return "test/integration/014_hook_tests/configured-models"
 
     @attr(type='postgres')
-    def test_pre_and_post_model_hooks_model(self):
+    def test_postgres_pre_and_post_model_hooks_model(self):
         self.run_dbt(['run'])
 
         self.check_hooks('start')
         self.check_hooks('end')
 
     @attr(type='postgres')
-    def test_pre_and_post_model_hooks_model_and_project(self):
+    def test_postgres_pre_and_post_model_hooks_model_and_project(self):
         self.use_default_project({
             'models': {
                 'test': {
@@ -230,3 +230,27 @@ def test_pre_and_post_model_hooks_model_and_project(self):
         self.check_hooks('start', count=2)
         self.check_hooks('end', count=2)
 
+class TestPrePostModelHooksInConfigKwargs(TestPrePostModelHooksInConfig):
+
+    @property
+    def models(self):
+        return "test/integration/014_hook_tests/kwargs-models"
+
+
+
+class TestDuplicateHooksInConfigs(DBTIntegrationTest):
+    @property
+    def schema(self):
+        return "model_hooks_014"
+
+    @property
+    def models(self):
+        return "test/integration/014_hook_tests/error-models"
+
+    @attr(type='postgres')
+    def test_postgres_run_duplicate_hook_defs(self):
+        with self.assertRaises(CompilationException) as exc:
+            self.run_dbt(['run'])
+
+            self.assertIn('pre_hook', str(exc.exception))
+            self.assertIn('pre-hook', str(exc.exception))