Repository: dbt-labs/dbt-core

Issue:
Backwards compatibility for adapter functions in 0.13.0
### Issue description
dbt v0.13.0 changes the signature of some existing adapter functions to be incompatible with the historically accepted syntax. While net-new adapter functions should indeed accept `Relation` objects as arguments, existing adapter functions should 1) retain their existing signatures 2) be deprecated and removed in a future release of dbt.

The relevant adapter functions are :
- `get_columns_in_table` (removed in 0.13.0)
- `already_exists` (modified to accept a relation in 0.13.0)

These adapter functions are user-facing and therefore used across many dbt projects. Other adapter methods like `get_missing_columns` and `expand_target_column_types` are more typically used in materializations, and as such, do not pose a significant hurdle to users upgrading to 0.13.0.

### `get_columns_in_table`
Old signature:
```
adapter.get_columns_in_table(schema, identifier)
```

Note that as of 0.13.0, dbt has become database-aware. The `get_columns_in_table` method should _always_ use the database specified in the active profile target. If users want to use a different database in this method call, they should instead use the `get_columns_in_relation` method, supplying a relation. dbt's internals should continue to use `get_columns_in_relation`.

### `already_exists`
Old signature:
```
adapter.already_exists(schema, name)
```
The same caveats listed for `get_columns_in_table` apply here. Already exists should take a schema and an identifier rather than a Relation object.

## Deprecation warning
0.13.0 includes code intended to show a helpful error message if non-relations are passed to adapter functions that now accept relations. In practice, dbt doesn't actually show these errors, as passing a schema and table (in lieu of a relation) results in:
```
already_exists() got multiple values for argument 'model_name
```

Rather than showing this particular error, dbt should instead show:
```
The adapter function adapter.[FUNC_NAME] is deprecated and will be removed in a future release of dbt. Please use `adapter.[SUPPORTED_FUNC_NAME]` instead.

Documentation for [SUPPORTED_FUNC_NAME] can be found here:
https://docs.getdbt.com/reference#adapter
```

Here, the mapping from deprecated method name to supported method name is:
`adapter.get_columns_in_table` --> `adapter.get_columns_in_relation`
`adapter.already_exists` --> `adapter.get_relation`

### Supplying relations

There's one more part of this that isn't sitting well with me: Creating relations is more difficult, I think, than it should be. If we're going to make Relations the currency of adapter methods, they should be super easy and memorable to create. The current paradigm is:
```
{% set relation = api.Relation.create(schema='snowplow', identifier='events') %}
```

This is likely confusing for end-users, as `create` is creating an object in memory, and _not_ a relation in the database. Moreover, the need to specify arguments is pretty funky. This is lower priority if we're able to preserve the existing signature for adapter methods, but it's something to be aware of going forwards.


Discussion:

Resolving patch:
diff --git a/core/dbt/adapters/base/impl.py b/core/dbt/adapters/base/impl.py
--- a/core/dbt/adapters/base/impl.py
+++ b/core/dbt/adapters/base/impl.py
@@ -18,7 +18,8 @@
 from dbt.schema import Column
 from dbt.utils import filter_null_values, translate_aliases
 
-from dbt.adapters.base.meta import AdapterMeta, available, available_raw
+from dbt.adapters.base.meta import AdapterMeta, available, available_raw, \
+    available_deprecated
 from dbt.adapters.base import BaseRelation
 from dbt.adapters.cache import RelationsCache
 
@@ -356,6 +357,17 @@ def get_columns_in_relation(self, relation, model_name=None):
             '`get_columns_in_relation` is not implemented for this adapter!'
         )
 
+    @available_deprecated('get_columns_in_relation')
+    def get_columns_in_table(self, schema, identifier, model_name=None):
+        """DEPRECATED: Get a list of the columns in the given table."""
+        relation = self.Relation.create(
+            database=self.config.credentials.database,
+            schema=schema,
+            identifier=identifier,
+            quote_policy=self.config.quoting
+        )
+        return self.get_columns_in_relation(relation, model_name=model_name)
+
     @abc.abstractmethod
     def expand_column_types(self, goal, current, model_name=None):
         """Expand the current table's types to match the goal table. (passable)
@@ -513,6 +525,14 @@ def get_relation(self, database, schema, identifier, model_name=None):
 
         return None
 
+    @available_deprecated('get_relation')
+    def already_exists(self, schema, name, model_name=None):
+        """DEPRECATED: Return if a model already exists in the database"""
+        database = self.config.credentials.database
+        relation = self.get_relation(database, schema, name,
+                                     model_name=model_name)
+        return relation is not None
+
     ###
     # ODBC FUNCTIONS -- these should not need to change for every adapter,
     #                   although some adapters may override them
@@ -542,21 +562,6 @@ def drop_schema(self, database, schema, model_name=None):
             '`drop_schema` is not implemented for this adapter!'
         )
 
-    @available
-    def already_exists(self, relation, model_name=None):
-        if not isinstance(relation, self.Relation):
-            dbt.exceptions.invalid_type_error(
-                method_name='already_exists',
-                arg_name='relation',
-                got_value=relation,
-                expected_type=self.Relation)
-
-        relation = self.get_relation(database=relation.database,
-                                     schema=relation.schema,
-                                     identifier=relation.identifier,
-                                     model_name=model_name)
-        return relation is not None
-
     @available_raw
     @abstractclassmethod
     def quote(cls, identifier):
diff --git a/core/dbt/adapters/base/meta.py b/core/dbt/adapters/base/meta.py
--- a/core/dbt/adapters/base/meta.py
+++ b/core/dbt/adapters/base/meta.py
@@ -1,4 +1,6 @@
 import abc
+from functools import wraps
+from dbt.deprecations import warn, renamed_method
 
 
 def available(func):
@@ -21,6 +23,30 @@ def available_raw(func):
     return func
 
 
+def available_deprecated(supported_name):
+    """A decorator that marks a function as available, but also prints a
+    deprecation warning. Use like
+
+    @available_deprecated('my_new_method')
+    def my_old_method(self, arg, model_name=None):
+        args = compatability_shim(arg)
+        return self.my_new_method(*args, model_name=None)
+
+    To make `adapter.my_old_method` available but also print out a warning on
+    use directing users to `my_new_method`.
+    """
+    def wrapper(func):
+        func_name = func.__name__
+        renamed_method(func_name, supported_name)
+
+        @wraps(func)
+        def inner(*args, **kwargs):
+            warn('adapter:{}'.format(func_name))
+            return func(*args, **kwargs)
+        return available(inner)
+    return wrapper
+
+
 class AdapterMeta(abc.ABCMeta):
     def __new__(mcls, name, bases, namespace, **kwargs):
         cls = super(AdapterMeta, mcls).__new__(mcls, name, bases, namespace,
diff --git a/core/dbt/deprecations.py b/core/dbt/deprecations.py
--- a/core/dbt/deprecations.py
+++ b/core/dbt/deprecations.py
@@ -44,6 +44,24 @@ class SeedDropExistingDeprecation(DBTDeprecation):
   will be removed in a future version of dbt."""
 
 
+_adapter_renamed_description = """\
+The adapter function `adapter.{old_name}` is deprecated and will be removed in
+ a future release of dbt. Please use `adapter.{new_name}` instead.
+ Documentation for {new_name} can be found here:
+ https://docs.getdbt.com/reference#adapter"""
+
+
+def renamed_method(old_name, new_name):
+    class AdapterDeprecationWarning(DBTDeprecation):
+        name = 'adapter:{}'.format(old_name)
+        description = _adapter_renamed_description.format(old_name=old_name,
+                                                          new_name=new_name)
+
+    dep = AdapterDeprecationWarning()
+    deprecations_list.append(dep)
+    deprecations[dep.name] = dep
+
+
 def warn(name, *args, **kwargs):
     if name not in deprecations:
         # this should (hopefully) never happen


Test patch:
diff --git a/test/integration/001_simple_copy_test/models/advanced_incremental.sql b/test/integration/001_simple_copy_test/models/advanced_incremental.sql
--- a/test/integration/001_simple_copy_test/models/advanced_incremental.sql
+++ b/test/integration/001_simple_copy_test/models/advanced_incremental.sql
@@ -10,7 +10,7 @@
 select *
 from {{ ref('seed') }}
 
-{% if adapter.already_exists(this) and not flags.FULL_REFRESH %}
+{% if adapter.already_exists(this.schema, this.table) and not flags.FULL_REFRESH %}
 
     where id > (select max(id) from {{this}})
 
diff --git a/test/integration/001_simple_copy_test/test_simple_copy.py b/test/integration/001_simple_copy_test/test_simple_copy.py
--- a/test/integration/001_simple_copy_test/test_simple_copy.py
+++ b/test/integration/001_simple_copy_test/test_simple_copy.py
@@ -1,8 +1,12 @@
 from nose.plugins.attrib import attr
 from test.integration.base import DBTIntegrationTest, use_profile
+from dbt import deprecations
 
 
 class BaseTestSimpleCopy(DBTIntegrationTest):
+    def setUp(self):
+        super(BaseTestSimpleCopy, self).setUp()
+        deprecations.reset_deprecations()
 
     @property
     def schema(self):
@@ -22,8 +26,11 @@ class TestSimpleCopy(BaseTestSimpleCopy):
     def test__postgres__simple_copy(self):
         self.use_default_project({"data-paths": [self.dir("seed-initial")]})
 
+        self.assertEqual(deprecations.active_deprecations, set())
         results = self.run_dbt(["seed"])
         self.assertEqual(len(results),  1)
+        self.assertEqual({'adapter:already_exists', 'sql_where'},
+                         deprecations.active_deprecations)
         results = self.run_dbt()
         self.assertEqual(len(results),  7)
 
diff --git a/test/integration/002_varchar_widening_test/models/incremental.sql b/test/integration/002_varchar_widening_test/models/incremental.sql
--- a/test/integration/002_varchar_widening_test/models/incremental.sql
+++ b/test/integration/002_varchar_widening_test/models/incremental.sql
@@ -7,7 +7,7 @@
 
 select * from {{ this.schema }}.seed
 
-{% if adapter.already_exists(this) %}
+{% if adapter.already_exists(this.schema, this.table) %}
 
     where id > (select max(id) from {{this}})
 
diff --git a/test/integration/012_profile_config_tests/models/view_model.sql b/test/integration/012_profile_config_tests/models/view_model.sql
deleted file mode 100644
--- a/test/integration/012_profile_config_tests/models/view_model.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-{{
-  config(
-    materialized = "view"
-  )
-}}
-
-select * from {{ this.schema }}.seed
diff --git a/test/integration/012_profile_config_tests/seed.sql b/test/integration/012_profile_config_tests/seed.sql
deleted file mode 100644
--- a/test/integration/012_profile_config_tests/seed.sql
+++ /dev/null
@@ -1,20 +0,0 @@
-create table {schema}.seed (
-	id BIGSERIAL PRIMARY KEY,
-	first_name VARCHAR(50),
-	last_name VARCHAR(50),
-	email VARCHAR(50),
-	gender VARCHAR(50),
-	ip_address VARCHAR(20)
-);
-
-
-insert into {schema}.seed (first_name, last_name, email, gender, ip_address) values ('Jack', 'Hunter', 'jhunter0@pbs.org', 'Male', '59.80.20.168');
-insert into {schema}.seed (first_name, last_name, email, gender, ip_address) values ('Kathryn', 'Walker', 'kwalker1@ezinearticles.com', 'Female', '194.121.179.35');
-insert into {schema}.seed (first_name, last_name, email, gender, ip_address) values ('Gerald', 'Ryan', 'gryan2@com.com', 'Male', '11.3.212.243');
-insert into {schema}.seed (first_name, last_name, email, gender, ip_address) values ('Bonnie', 'Spencer', 'bspencer3@ameblo.jp', 'Female', '216.32.196.175');
-insert into {schema}.seed (first_name, last_name, email, gender, ip_address) values ('Harold', 'Taylor', 'htaylor4@people.com.cn', 'Male', '253.10.246.136');
-insert into {schema}.seed (first_name, last_name, email, gender, ip_address) values ('Jacqueline', 'Griffin', 'jgriffin5@t.co', 'Female', '16.13.192.220');
-insert into {schema}.seed (first_name, last_name, email, gender, ip_address) values ('Wanda', 'Arnold', 'warnold6@google.nl', 'Female', '232.116.150.64');
-insert into {schema}.seed (first_name, last_name, email, gender, ip_address) values ('Craig', 'Ortiz', 'cortiz7@sciencedaily.com', 'Male', '199.126.106.13');
-insert into {schema}.seed (first_name, last_name, email, gender, ip_address) values ('Gary', 'Day', 'gday8@nih.gov', 'Male', '35.81.68.186');
-insert into {schema}.seed (first_name, last_name, email, gender, ip_address) values ('Rose', 'Wright', 'rwright9@yahoo.co.jp', 'Female', '236.82.178.100');
diff --git a/test/integration/012_profile_config_tests/test_profile_config.py b/test/integration/012_profile_config_tests/test_profile_config.py
deleted file mode 100644
--- a/test/integration/012_profile_config_tests/test_profile_config.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from nose.plugins.attrib import attr
-from test.integration.base import DBTIntegrationTest
-import dbt.deprecations
-
-class TestNoRunTargetDeprecation(DBTIntegrationTest):
-
-    def setUp(self):
-        DBTIntegrationTest.setUp(self)
-
-        self.run_sql_file("test/integration/012_profile_config_tests/seed.sql")
-        dbt.deprecations.reset_deprecations()
-
-    @property
-    def schema(self):
-        return "profile_config_012"
-
-    @property
-    def models(self):
-        return "test/integration/012_profile_config_tests/models"
-
-    @property
-    def profile_config(self):
-        return {
-            'test': {
-                'outputs': {
-                    'my-target': {
-                        'type': 'postgres',
-                        'threads': 1,
-                        'host': 'database',
-                        'port': 5432,
-                        'user': 'root',
-                        'pass': 'password',
-                        'dbname': 'dbt',
-                        'schema': self.unique_schema()
-                    }
-                },
-                'target': 'my-target'
-            }
-        }
-
-    @attr(type='postgres')
-    def test_deprecated_run_target_config(self):
-        results = self.run_dbt()
-        self.assertEqual(len(results), 1)
-
-        self.assertTablesEqual("seed","view_model")
-
-        self.assertFalse('run-target' in dbt.deprecations.active_deprecations)