Repository: dbt-labs/dbt-core

Issue:
Add a generate_database_name() macro
### Describe the feature
In the current version (v14.0), there exists a `{{ generate_schema_name_for_env }}` macro which works very well in dev mode, allowing a production run to write to the specified schema but writing all tables and views to a dev schema when in dev mode.  In the same way we need a `{{generate_database_name_for_env}}` macro for when a database is configured in the `dbt_project.yml` file.

Currently:
If I have in my `dbt_project.yml` file a section in `models` that reads:
```
models:
   product:
      database: mart_db
      materialized: view
      schema: mart_schema
```

and in my `profiles.yml` file I have:
```
my_dbt:
  target: dev
  outputs:
    dev:
      type: snowflake
      account: *******

      user: "{{ env_var('DBT_USER') }}"
      password: "{{ env_var('DBT_PASSWORD') }}"

      role: "ur_{{ env_var('DBT_USER') }}"
      database: dbt_dev
```
Then my models in dev mode will be written using `mart_db` instead of `dev_db`.

### Describe alternatives you've considered
Right now to solve this I've created alternative `ref` macro called `xref` to override this behavior but it feels a bit clunky to do this and I will have to tell out dbt devs to all use `{{ xref('some table') }}` instead of the inbuilt `ref` function.

### Additional context
Not database specific, it's a dbt issue.

### Who will this benefit?
Anyone who wants to specify a set of **production** databases in their `dbt_project.yml` file in the same way that they might already do for their schemas using the existing `{{generate_database_name_for_env}}` macro but who also wants to have dbt write **all tables and views** into a single schema when in dev_mode.


Discussion:
Hey @whisperstream - I think this is a good idea!

We historically have recommended building all dbt models into a single database - can you tell me more about why you're interested in building models across databases? I'm all for building the functionality described in this feature request, but I also want to get a handle on what a typical use case looks like :)
Mostly this comes from an organizational perspective.  My (current) plan is to have 3 databases, staging, bizready and marts.  Within each of the first two databases I use one schema per source of data.

I'm working on a large enterprise project and I'm trying to enable multiple teams to contribute dbt scripts for the data sources they have access to.  This is further complicated by having two tiers of dbt developers.

Tier 1 - dev_raw
Can contribute dbt scripts for a specific source of data, transforming that source's data from staging to business ready.  Has access only to a single or subset of the sources, does not have access to build the entire staging -> bizready dbt project.

Tier 2 - dev analyst
Contributes dbt scripts that create tables and views from specific sources of the bizready data.  Has no access to the raw/staging data.

I could use different dbt_project.yml for the different dev types but I want to keep the lineage data when generating docs.

It seems that for most companies, it's assumed the dbt developer has access to run the whole dbt_project.yml in one go, in my case, only the integration and production environments will have this capability.  If there's a better way of structuring the project am open to that too, just haven't found/thought of another way and so worked around it by creating an `xref()` alternative that figures out what sort of dbt dev you are (tier1/tier2) and rewrites the fully qualified table name accordingly.
Thanks for the context @whisperstream - this is really interesting! One approach that may be interesting: you can use multiple different dbt projects and they can state each other as dependencies. Check out the docs on [packages](https://docs.getdbt.com/docs/package-management) here if you haven't seen them already.

In your case, the Tier 2 package could "import" the Tier 2 package. That would still let you `ref` across projects and the generated documentation would show the full lineage graph. If anything, this might make the docs _better_ because it would separate the two sets of models into different projects :)

I think there's merit to implementing a `generate_database_name` macro regardless (at the very least for consistency's sake!) but also wanted to throw this alternative approach out there. Thanks for sharing your use case with me!
@drewbanin thanks for that, let me try and arrange my current project into a multi package model and see if that works too.
Ok back again.  I think packages help separate out the projects into logical parts, but I still need to do the rewriting.

For Tier1 devs - the `ref()` function works normally

But for Tier2 devs, even when models are separated into different packages I still need to have an `xref()` macro because since Tier2 has only has access to bizready tables from Tier1, I have to override the normal `ref()` function and return instead the final production table name.  Does that make sense?

Another semi related item I wondered was if `ref` should be allowed to be overridden.  Before creating `xref` I tried just creating a macro called `ref` but that caused a recursive loop I think or some other error.  So I wondered if it would be better to rename the actual `ref` function and then for dbt to have a default macro which would be something like.

```
{%- macro ref(package_name, table_name=None) -%}
    {{ renamed_ref(package_name, table_name) }}
{% endmacro %}
```

That way if people have more complicated things to do with ref, they can do it in a more seamless way, that also keeps everything appearing as standard dbt syntax.
Hey @whisperstream - check out [this thread](https://github.com/fishtown-analytics/dbt/issues/1603). It's a long one, but we set forth some approaches in this thread to make it possible to override `ref`! I don't think that would be an unreasonable thing for us to do at all.

Can you show me what your `xref()` macro looks like? I follow a lot of what you're saying at a high-level, but this part isn't super clear to me:
>  I have to override the normal ref() function and return instead the final production table name

What happens when a Tier 2 analyst runs a Tier 1 model? Do they just exclude those models from their dbt runs? Can you also share what you would want your `generate_database_name_for_env()` macro to look like?

I think the pattern you're describing is _a really good one_ and I'm super interested in making sure that dbt does a good job of facilitating it! I appreciate you taking the time :)
Let me clean up my xref macro a bit and add some comments, in the mean time I'll try and explain this part `I have to override the normal ref() function and return instead the final production table name` a bit better:

So just to preface this, what I describe below (and the condition in `xref()`) only happens when `target.name!=prod` AND `env_var('DEVELOPER_TYPE') == tier2`, otherwise dbt runs as normal.

Normally in dbt you create a hierarchy of models, i.e.

`[actual raw table] -> model_raw -> model_stage -> model_bizready -> model_mart`

This assumes you have access to `[actual raw table]`, but in my case tier2 developers only have access to `[production bizready table]` (created by a previous production run of dbt)

So when a tier2 developer executes:

`dbt -run -m +model_mart`

dbt will try and execute the whole DAG, which will fail because tier2 can't access the `[actual raw table]`.  To work around this, my `xref()` macro figures out that `xref(model_bizready)` should not keep traversing the DAG as normal, but instead should rewrite the DAG to reference the production table that the tier2 dev **does** have access to.  So for `xref(model_bizready)` the DAG becomes:

`[production bizready table]  -> model_mart`

That way the tier2 developer doesn't need access to anything including or proceeding bizready because they don't have access rights to it anyway, but they're still able to contribute to and execute dbt for any `mart` models they wish to work on.

Conversely a tier1 developer or a production  process executing the same `dbt -run -m +model_mart` will execute the whole DAG normally
i.e.
`[actual raw table] -> model_raw -> model_stage -> model_bizready -> model_mart`
@drewbanin 

Ok here's what my xref macro is looking like:

all bizready tables are prefixed with `br_`, so a sample table might be `br_s1__some_dataset`,

In prod mode or when `DEVELOPER_TYPE` is `RAW_DEV` then `xref` just acts like `ref`. but if developer mode is `BIZREADY_DEV` and a `br_` table is being referenced then it rewrites the table reference to point to production.  If `target != prod` I always do the mapping because I need xref to fail if anyone adds a new bizready schema.  Anyway hopefully the use case makes sense.  The database name override isn't the only piece I'd need to do this more elegantly, I'd also need to some how get the custom schema name from the config.  

Originally I was getting the custom schema name if the developer mode was `BIZREADY_DEV` but  dbt creates the schemas first instead of doing it only if something is written to that schema and this meant that if two devs were running dbt at the same time, one would error out because dbt wouldn't be able to create the custom schemas again (because the 1st dev would have created them)....that's why I have the clumsy lookup table.

Not sure if you have some better ways I might solve this use case, but being able to oevrride the `ref` function is probably the 1st step and then maybe having more ways look up information about a reference name, i.e. to see if it has a custom schema, custom database configured etc..?

```
{%- macro xref(package_name, table_name=None) -%}

    {%- if table_name is none -%}
        {%- set table_name = package_name -%}
        {%- set package_name = None -%}
    {%-  endif -%}

    {# -- only do this for bizready tables #}
    {%- if (package_name is none and target.name == 'prod') or not table_name.startswith('br_') -%}
        {{- return( ref(package_name, table_name) ) -}}
    {%- endif -%}

    {%- set orig_ref = ref(table_name) -%}
    {%- set final_table = orig_ref -%}

    {%- set prefix_mapping = {
            's1':  'schema1',
            's2':  'schema2',
            's3':  'schema3'
        }
    -%}

    {# -- lookup the mapping and get the schema name -- #}
    {%- set db_schema = prefix_mapping[table_name.split('__')[0][3:]] if (table_name|string).startswith('br_') else None -%}

    {%- if var('DEVELOPER_TYPE') == 'BIZREADY_DEV' -%}

        {%- set db = var('BIZREADY_PROD_DB') -%}

        {%- set final_table = db + '.' + db_schema + '.' + table_name -%}

        {{- log('[' ~ var('DEVELOPER_TYPE') ~ ' MODE] - Converted original table ref: ' ~ table_name
            ~ ' from expected conversion: ' ~ orig_ref ~ ' to: ' ~ final_table) -}}

    {%- else -%}

        {{- log('No changes made for table: ' ~ table_name ~ ' and ref: ' ~ orig_ref) -}}

    {%- endif -%}

    {{- log('Returning final table:' ~ final_table) -}}

    {{- final_table -}}

{%- endmacro -%}
```

Resolving patch:
diff --git a/core/dbt/contracts/graph/manifest.py b/core/dbt/contracts/graph/manifest.py
--- a/core/dbt/contracts/graph/manifest.py
+++ b/core/dbt/contracts/graph/manifest.py
@@ -541,21 +541,21 @@ def filter(candidate: MacroCandidate) -> bool:
         return candidates.last()
 
     def find_generate_macro_by_name(
-        self, name: str, root_project_name: str
+        self, component: str, root_project_name: str
     ) -> Optional[ParsedMacro]:
         """
         The `generate_X_name` macros are similar to regular ones, but ignore
         imported packages.
-            - if there is a `name` macro in the root project, return it
-            - if that does not exist but there is a `name` macro in the 'dbt'
-                internal project (or a plugin), return that
-            - if neither of those exist (unit tests?), return None
+            - if there is a `generate_{component}_name` macro in the root
+              project, return it
+            - return the `generate_{component}_name` macro from the 'dbt'
+              internal project
         """
         def filter(candidate: MacroCandidate) -> bool:
             return candidate.locality != Locality.Imported
 
         candidates: CandidateList = self._find_macros_by_name(
-            name=name,
+            name=f'generate_{component}_name',
             root_project_name=root_project_name,
             # filter out imported packages
             filter=filter,
diff --git a/core/dbt/parser/base.py b/core/dbt/parser/base.py
--- a/core/dbt/parser/base.py
+++ b/core/dbt/parser/base.py
@@ -2,7 +2,7 @@
 import itertools
 import os
 from typing import (
-    List, Dict, Any, Callable, Iterable, Optional, Generic, TypeVar
+    List, Dict, Any, Iterable, Generic, TypeVar
 )
 
 from hologram import ValidationError
@@ -11,7 +11,6 @@
 from dbt.clients.system import load_file_contents
 from dbt.context.providers import generate_parser_model, generate_parser_macro
 import dbt.flags
-from dbt import deprecations
 from dbt import hooks
 from dbt.adapters.factory import get_adapter
 from dbt.clients.jinja import get_rendered
@@ -19,10 +18,10 @@
 from dbt.contracts.graph.manifest import (
     Manifest, SourceFile, FilePath, FileHash
 )
-from dbt.contracts.graph.parsed import HasUniqueID, ParsedMacro
+from dbt.contracts.graph.parsed import HasUniqueID
 from dbt.contracts.graph.unparsed import UnparsedNode
 from dbt.exceptions import (
-    CompilationException, validator_error_message
+    CompilationException, validator_error_message, InternalException
 )
 from dbt.node_types import NodeType
 from dbt.source_config import SourceConfig
@@ -39,7 +38,6 @@
 FinalNode = TypeVar('FinalNode', bound=ManifestNodes)
 
 
-RelationUpdate = Callable[[Optional[str], IntermediateNode], str]
 ConfiguredBlockType = TypeVar('ConfiguredBlockType', bound=FileBlock)
 
 
@@ -94,6 +92,31 @@ def __init__(
         self.macro_manifest = macro_manifest
 
 
+class RelationUpdate:
+    def __init__(
+        self, config: RuntimeConfig, manifest: Manifest, component: str
+    ) -> None:
+        macro = manifest.find_generate_macro_by_name(
+            component=component,
+            root_project_name=config.project_name,
+        )
+        if macro is None:
+            raise InternalException(
+                f'No macro with name generate_{component}_name found'
+            )
+
+        root_context = generate_parser_macro(macro, config, manifest, None)
+        self.updater = MacroGenerator(macro, root_context)
+        self.component = component
+
+    def __call__(
+        self, parsed_node: Any, config_dict: Dict[str, Any]
+    ) -> None:
+        override = config_dict.get(self.component)
+        new_value = self.updater(override, parsed_node).strip()
+        setattr(parsed_node, self.component, new_value)
+
+
 class ConfiguredParser(
     Parser[FinalNode],
     Generic[ConfiguredBlockType, IntermediateNode, FinalNode],
@@ -106,8 +129,16 @@ def __init__(
         macro_manifest: Manifest,
     ) -> None:
         super().__init__(results, project, root_project, macro_manifest)
-        self._get_schema_func: Optional[RelationUpdate] = None
-        self._get_alias_func: Optional[RelationUpdate] = None
+
+        self._update_node_database = RelationUpdate(
+            manifest=macro_manifest, config=root_project, component='database'
+        )
+        self._update_node_schema = RelationUpdate(
+            manifest=macro_manifest, config=root_project, component='schema'
+        )
+        self._update_node_alias = RelationUpdate(
+            manifest=macro_manifest, config=root_project, component='alias'
+        )
 
     @abc.abstractclassmethod
     def get_compiled_path(cls, block: ConfiguredBlockType) -> str:
@@ -129,69 +160,6 @@ def default_schema(self):
     def default_database(self):
         return self.root_project.credentials.database
 
-    def _build_generate_macro_function(self, macro: ParsedMacro) -> Callable:
-        root_context = generate_parser_macro(
-            macro, self.root_project, self.macro_manifest, None
-        )
-        return MacroGenerator(macro, root_context)
-
-    def get_schema_func(self) -> RelationUpdate:
-        """The get_schema function is set by a few different things:
-            - if there is a 'generate_schema_name' macro in the root project,
-                it will be used.
-            - if that does not exist but there is a 'generate_schema_name'
-                macro in the 'dbt' internal project, that will be used
-            - if neither of those exist (unit tests?), a function that returns
-                the 'default schema' as set in the root project's 'credentials'
-                is used
-        """
-        if self._get_schema_func is not None:
-            return self._get_schema_func
-
-        get_schema_macro = self.macro_manifest.find_generate_macro_by_name(
-            name='generate_schema_name',
-            root_project_name=self.root_project.project_name,
-        )
-        # this is only true in tests!
-        if get_schema_macro is None:
-            def get_schema(custom_schema_name=None, node=None):
-                return self.default_schema
-        else:
-            get_schema = self._build_generate_macro_function(get_schema_macro)
-
-        self._get_schema_func = get_schema
-        return self._get_schema_func
-
-    def get_alias_func(self) -> RelationUpdate:
-        """The get_alias function is set by a few different things:
-            - if there is a 'generate_alias_name' macro in the root project,
-                it will be used.
-            - if that does not exist but there is a 'generate_alias_name'
-                macro in the 'dbt' internal project, that will be used
-            - if neither of those exist (unit tests?), a function that returns
-                the 'default alias' as set in the model's filename or alias
-                configuration.
-        """
-        if self._get_alias_func is not None:
-            return self._get_alias_func
-
-        get_alias_macro = self.macro_manifest.find_generate_macro_by_name(
-            name='generate_alias_name',
-            root_project_name=self.root_project.project_name,
-        )
-        # the generate_alias_name macro might not exist
-        if get_alias_macro is None:
-            def get_alias(custom_alias_name, node):
-                if custom_alias_name is None:
-                    return node.name
-                else:
-                    return custom_alias_name
-        else:
-            get_alias = self._build_generate_macro_function(get_alias_macro)
-
-        self._get_alias_func = get_alias
-        return self._get_alias_func
-
     def get_fqn(self, path: str, name: str) -> List[str]:
         """Get the FQN for the node. This impacts node selection and config
         application.
@@ -297,33 +265,6 @@ def render_with_context(
                 parsed_node.raw_sql, context, parsed_node, capture_macros=True
             )
 
-    def update_parsed_node_schema(
-        self, parsed_node: IntermediateNode, config_dict: Dict[str, Any]
-    ) -> None:
-        # Special macro defined in the global project. Use the root project's
-        # definition, not the current package
-        schema_override = config_dict.get('schema')
-        get_schema = self.get_schema_func()
-        try:
-            schema = get_schema(schema_override, parsed_node)
-        except dbt.exceptions.CompilationException as exc:
-            too_many_args = (
-                "macro 'dbt_macro__generate_schema_name' takes not more than "
-                "1 argument(s)"
-            )
-            if too_many_args not in str(exc):
-                raise
-            deprecations.warn('generate-schema-name-single-arg')
-            schema = get_schema(schema_override)  # type: ignore
-        parsed_node.schema = schema.strip()
-
-    def update_parsed_node_alias(
-        self, parsed_node: IntermediateNode, config_dict: Dict[str, Any]
-    ) -> None:
-        alias_override = config_dict.get('alias')
-        get_alias = self.get_alias_func()
-        parsed_node.alias = get_alias(alias_override, parsed_node).strip()
-
     def update_parsed_node_config(
         self, parsed_node: IntermediateNode, config_dict: Dict[str, Any]
     ) -> None:
@@ -334,6 +275,13 @@ def update_parsed_node_config(
         self._mangle_hooks(final_config_dict)
         parsed_node.config = parsed_node.config.from_dict(final_config_dict)
 
+    def update_parsed_node_name(
+        self, parsed_node: IntermediateNode, config_dict: Dict[str, Any]
+    ) -> None:
+        self._update_node_database(parsed_node, config_dict)
+        self._update_node_schema(parsed_node, config_dict)
+        self._update_node_alias(parsed_node, config_dict)
+
     def update_parsed_node(
         self, parsed_node: IntermediateNode, config: SourceConfig
     ) -> None:
@@ -347,15 +295,10 @@ def update_parsed_node(
         model_tags = config_dict.get('tags', [])
         parsed_node.tags.extend(model_tags)
 
-        # do this once before we parse the node schema/alias, so
+        # do this once before we parse the node database/schema/alias, so
         # parsed_node.config is what it would be if they did nothing
         self.update_parsed_node_config(parsed_node, config_dict)
-
-        parsed_node.database = config_dict.get(
-            'database', self.default_database
-        ).strip()
-        self.update_parsed_node_schema(parsed_node, config_dict)
-        self.update_parsed_node_alias(parsed_node, config_dict)
+        self.update_parsed_node_name(parsed_node, config_dict)
 
         # at this point, we've collected our hooks. Use the node context to
         # render each hook and collect refs/sources


Test patch:
diff --git a/test/integration/006_simple_dependency_test/local_dependency/macros/generate_schema_name.sql b/test/integration/006_simple_dependency_test/local_dependency/macros/generate_schema_name.sql
--- a/test/integration/006_simple_dependency_test/local_dependency/macros/generate_schema_name.sql
+++ b/test/integration/006_simple_dependency_test/local_dependency/macros/generate_schema_name.sql
@@ -1,4 +1,15 @@
 {# This should be ignored as it's in a subpackage #}
-{% macro generate_schema_name(custom_schema_name=none) -%}
-  invalid_schema_name
+{% macro generate_schema_name(custom_schema_name=none, node=none) -%}
+  {{ exceptions.raise_compiler_error('invalid', node=node) }}
+{%- endmacro %}
+
+{# This should be ignored as it's in a subpackage #}
+{% macro generate_database_name(custom_database_name=none, node=none) -%}
+  {{ exceptions.raise_compiler_error('invalid', node=node) }}
+{%- endmacro %}
+
+
+{# This should be ignored as it's in a subpackage #}
+{% macro generate_alias_name(custom_alias_name=none, node=none) -%}
+  {{ exceptions.raise_compiler_error('invalid', node=node) }}
 {%- endmacro %}
diff --git a/test/integration/012_deprecation_tests/deprecated-macros/schema.sql b/test/integration/012_deprecation_tests/deprecated-macros/schema.sql
deleted file mode 100644
--- a/test/integration/012_deprecation_tests/deprecated-macros/schema.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-{% macro generate_schema_name(schema_name) -%}
-	{%- if schema_name is none -%}
-		{{ target.schema }}
-	{%- else -%}
-		{{ schema_name }}
-	{%- endif -%}
-{%- endmacro %}
diff --git a/test/integration/012_deprecation_tests/test_deprecations.py b/test/integration/012_deprecation_tests/test_deprecations.py
--- a/test/integration/012_deprecation_tests/test_deprecations.py
+++ b/test/integration/012_deprecation_tests/test_deprecations.py
@@ -35,30 +35,6 @@ def test_postgres_deprecations(self):
         self.assertEqual(expected, deprecations.active_deprecations)
 
 
-class TestMacroDeprecations(BaseTestDeprecations):
-    @property
-    def models(self):
-        return self.dir('boring-models')
-
-    @property
-    def project_config(self):
-        return {
-            'macro-paths': [self.dir('deprecated-macros')],
-        }
-
-    @use_profile('postgres')
-    def test_postgres_deprecations_fail(self):
-        with self.assertRaises(dbt.exceptions.CompilationException):
-            self.run_dbt(strict=True)
-
-    @use_profile('postgres')
-    def test_postgres_deprecations(self):
-        self.assertEqual(deprecations.active_deprecations, set())
-        self.run_dbt(strict=False)
-        expected = {'generate-schema-name-single-arg'}
-        self.assertEqual(expected, deprecations.active_deprecations)
-
-
 class TestMaterializationReturnDeprecation(BaseTestDeprecations):
     @property
     def models(self):
diff --git a/test/integration/024_custom_schema_test/custom-db-macros/custom_db.sql b/test/integration/024_custom_schema_test/custom-db-macros/custom_db.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/024_custom_schema_test/custom-db-macros/custom_db.sql
@@ -0,0 +1,10 @@
+
+{% macro generate_database_name(database_name, node) %}
+    {% if database_name == 'alt' %}
+        {{ env_var('SNOWFLAKE_TEST_ALT_DATABASE') }}
+    {% elif database_name %}
+        {{ database_name }}
+    {% else %}
+        {{ target.database }}
+    {% endif %}
+{% endmacro %}
diff --git a/test/integration/024_custom_schema_test/db-models/view_1.sql b/test/integration/024_custom_schema_test/db-models/view_1.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/024_custom_schema_test/db-models/view_1.sql
@@ -0,0 +1,3 @@
+
+
+select * from {{ target.schema }}.seed
diff --git a/test/integration/024_custom_schema_test/db-models/view_2.sql b/test/integration/024_custom_schema_test/db-models/view_2.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/024_custom_schema_test/db-models/view_2.sql
@@ -0,0 +1,2 @@
+{{ config(database='alt') }}
+select * from {{ ref('view_1') }}
diff --git a/test/integration/024_custom_schema_test/db-models/view_3.sql b/test/integration/024_custom_schema_test/db-models/view_3.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/024_custom_schema_test/db-models/view_3.sql
@@ -0,0 +1,30 @@
+
+{{ config(database='alt', materialized='table') }}
+
+
+with v1 as (
+
+    select * from {{ ref('view_1') }}
+
+),
+
+v2 as (
+
+    select * from {{ ref('view_2') }}
+
+),
+
+combined as (
+
+    select last_name from v1
+    union all
+    select last_name from v2
+
+)
+
+select
+    last_name,
+    count(*) as count
+
+from combined
+group by 1
diff --git a/test/integration/024_custom_schema_test/seed.sql b/test/integration/024_custom_schema_test/seed.sql
--- a/test/integration/024_custom_schema_test/seed.sql
+++ b/test/integration/024_custom_schema_test/seed.sql
@@ -1,6 +1,6 @@
 
-drop table if exists {schema}.seed cascade;
-create table {schema}.seed (
+drop table if exists {database}.{schema}.seed cascade;
+create table {database}.{schema}.seed (
 	id BIGSERIAL PRIMARY KEY,
 	first_name VARCHAR(50),
 	last_name VARCHAR(50),
@@ -9,17 +9,17 @@ create table {schema}.seed (
 	ip_address VARCHAR(20)
 );
 
-drop table if exists {schema}.agg cascade;
-create table {schema}.agg (
+drop table if exists {database}.{schema}.agg cascade;
+create table {database}.{schema}.agg (
 	last_name VARCHAR(50),
 	count BIGINT
 );
 
 
-insert into {schema}.seed (first_name, last_name, email, gender, ip_address) values
+insert into {database}.{schema}.seed (first_name, last_name, email, gender, ip_address) values
 ('Jack', 'Hunter', 'jhunter0@pbs.org', 'Male', '59.80.20.168'),
 ('Kathryn', 'Walker', 'kwalker1@ezinearticles.com', 'Female', '194.121.179.35'),
 ('Gerald', 'Ryan', 'gryan2@com.com', 'Male', '11.3.212.243');
 
-insert into {schema}.agg (last_name, count) values
+insert into {database}.{schema}.agg (last_name, count) values
 ('Hunter', 2), ('Walker', 2), ('Ryan', 2);
diff --git a/test/integration/024_custom_schema_test/seed/agg.csv b/test/integration/024_custom_schema_test/seed/agg.csv
new file mode 100644
--- /dev/null
+++ b/test/integration/024_custom_schema_test/seed/agg.csv
@@ -0,0 +1,4 @@
+last_name,count
+Hunter,2
+Walker,2
+Ryan,2
diff --git a/test/integration/024_custom_schema_test/seed/seed.csv b/test/integration/024_custom_schema_test/seed/seed.csv
new file mode 100644
--- /dev/null
+++ b/test/integration/024_custom_schema_test/seed/seed.csv
@@ -0,0 +1,4 @@
+id,first_name,last_name,email,gender,ip_address
+1,Jack,Hunter,jhunter0@pbs.org,Male,59.80.20.168
+2,Kathryn,Walker,kwalker1@ezinearticles.com,Female,194.121.179.35
+3,Gerald,Ryan,gryan2@com.com,Male,11.3.212.243
diff --git a/test/integration/024_custom_schema_test/test_custom_database.py b/test/integration/024_custom_schema_test/test_custom_database.py
new file mode 100644
--- /dev/null
+++ b/test/integration/024_custom_schema_test/test_custom_database.py
@@ -0,0 +1,38 @@
+from test.integration.base import DBTIntegrationTest, use_profile
+
+
+class TestOverrideDatabase(DBTIntegrationTest):
+    setup_alternate_db = True
+
+    @property
+    def schema(self):
+        return "custom_schema_024"
+
+    @property
+    def models(self):
+        return "db-models"
+
+    @property
+    def project_config(self):
+        return {
+            'macro-paths': ['custom-db-macros'],
+        }
+
+    @use_profile('snowflake')
+    def test_snowflake_override_generate_db_name(self):
+        self.run_sql_file('seed.sql')
+        self.assertTableDoesExist('SEED', schema=self.unique_schema(), database=self.default_database)
+        self.assertTableDoesExist('AGG', schema=self.unique_schema(), database=self.default_database)
+
+        results = self.run_dbt()
+        self.assertEqual(len(results), 3)
+
+        self.assertTableDoesExist('VIEW_1', schema=self.unique_schema(), database=self.default_database)
+        self.assertTableDoesExist('VIEW_2', schema=self.unique_schema(), database=self.alternative_database)
+        self.assertTableDoesExist('VIEW_3', schema=self.unique_schema(), database=self.alternative_database)
+
+        # not overridden
+        self.assertTablesEqual('SEED', 'VIEW_1', table_b_db=self.default_database)
+        # overridden
+        self.assertTablesEqual('SEED', 'VIEW_2', table_b_db=self.alternative_database)
+        self.assertTablesEqual('AGG', 'VIEW_3', table_b_db=self.alternative_database)
diff --git a/test/integration/base.py b/test/integration/base.py
--- a/test/integration/base.py
+++ b/test/integration/base.py
@@ -619,7 +619,7 @@ def run_sql_common(self, sql, fetch, conn):
                 else:
                     return
             except BaseException as e:
-                if conn.handle and not conn.handle.closed:
+                if conn.handle and not getattr(conn.handle, 'closed', True):
                     conn.handle.rollback()
                 print(sql)
                 print(e)
@@ -1065,16 +1065,16 @@ def _assertTableRowCountsEqual(self, relation_a, relation_b):
             )
         )
 
-    def assertTableDoesNotExist(self, table, schema=None):
-        columns = self.get_table_columns(table, schema)
+    def assertTableDoesNotExist(self, table, schema=None, database=None):
+        columns = self.get_table_columns(table, schema, database)
 
         self.assertEqual(
             len(columns),
             0
         )
 
-    def assertTableDoesExist(self, table, schema=None):
-        columns = self.get_table_columns(table, schema)
+    def assertTableDoesExist(self, table, schema=None, database=None):
+        columns = self.get_table_columns(table, schema, database)
 
         self.assertGreater(
             len(columns),
diff --git a/test/unit/test_graph.py b/test/unit/test_graph.py
--- a/test/unit/test_graph.py
+++ b/test/unit/test_graph.py
@@ -11,9 +11,11 @@
 import dbt.config
 import dbt.utils
 import dbt.parser.manifest
-from dbt.contracts.graph.manifest import FilePath, SourceFile, FileHash
+from dbt.contracts.graph.manifest import FilePath, SourceFile, FileHash, Manifest
+from dbt.contracts.graph.parsed import ParsedMacro
 from dbt.parser.results import ParseResult
 from dbt.parser.base import BaseParser
+from dbt.node_types import NodeType
 
 try:
     from queue import Empty
@@ -22,7 +24,19 @@
 
 from dbt.logger import GLOBAL_LOGGER as logger # noqa
 
-from .utils import config_from_parts_or_dicts
+from .utils import config_from_parts_or_dicts, generate_name_macros
+
+
+def MockMacro(package, name='my_macro', kwargs={}):
+    macro = MagicMock(
+        __class__=ParsedMacro,
+        resource_type=NodeType.Macro,
+        package_name=package,
+        unique_id=f'macro.{package}.{name}',
+        **kwargs
+    )
+    macro.name = name
+    return macro
 
 
 class GraphTest(unittest.TestCase):
@@ -37,6 +51,7 @@ def tearDown(self):
         self.mock_hook_constructor.stop()
         self.load_patch.stop()
         self.load_source_file_patcher.stop()
+        # self.relation_update_patcher.stop()
 
     def setUp(self):
         dbt.flags.STRICT_MODE = True
@@ -101,6 +116,12 @@ def _mock_parse_result(config, all_projects):
         self.mock_source_file = self.load_source_file_patcher.start()
         self.mock_source_file.side_effect = lambda path: [n for n in self.mock_models if n.path == path][0]
 
+        # self.relation_update_patcher = patch.object(RelationUpdate, '_relation_components', lambda: [])
+        # self.mock_relation_update = self.relation_update_patcher.start()
+        self.internal_manifest = Manifest.from_macros(macros={
+            n.unique_id: n for n in generate_name_macros('test_models_compile')
+        })
+
         def filesystem_iter(iter_self):
             if 'sql' not in iter_self.extension:
                 return []
@@ -153,7 +174,7 @@ def use_models(self, models):
 
     def load_manifest(self, config):
         loader = dbt.parser.manifest.ManifestLoader(config, {config.project_name: config})
-        loader.load()
+        loader.load(internal_manifest=self.internal_manifest)
         return loader.create_manifest()
 
     def test__single_model(self):
@@ -303,7 +324,7 @@ def test__partial_parse(self):
         config = self.get_config()
 
         loader = dbt.parser.manifest.ManifestLoader(config, {config.project_name: config})
-        loader.load()
+        loader.load(internal_manifest=self.internal_manifest)
         loader.create_manifest()
         results = loader.results
 
diff --git a/test/unit/test_manifest.py b/test/unit/test_manifest.py
--- a/test/unit/test_manifest.py
+++ b/test/unit/test_manifest.py
@@ -633,10 +633,8 @@ def test__build_flat_graph(self):
         self.assertEqual(compiled_count, 2)
 
 
-
 # Tests of the manifest search code (find_X_by_Y)
 
-
 def MockMacro(package, name='my_macro', kwargs={}):
     macro = mock.MagicMock(
         __class__=ParsedMacro,
@@ -656,6 +654,11 @@ def MockMaterialization(package, name='my_materialization', adapter_type=None, k
     return MockMacro(package, f'materialization_{name}_{adapter_type}', kwargs)
 
 
+def MockGenerateMacro(package, component='some_component', kwargs={}):
+    name = f'generate_{component}_name'
+    return MockMacro(package, name=name, kwargs=kwargs)
+
+
 def MockSource(package, source_name, name, kwargs={}):
     src = mock.MagicMock(
         __class__=ParsedSourceDefinition,
@@ -834,43 +837,43 @@ def test_find_macro_by_name(macros, expectations):
 
     # just root
     FindMacroSpec(
-        macros=[MockMacro('root')],
+        macros=[MockGenerateMacro('root')],
         expected='root',
     ),
 
     # just dep
     FindMacroSpec(
-        macros=[MockMacro('dep')],
+        macros=[MockGenerateMacro('dep')],
         expected=None,
     ),
 
     # just dbt
     FindMacroSpec(
-        macros=[MockMacro('dbt')],
+        macros=[MockGenerateMacro('dbt')],
         expected='dbt',
     ),
 
     # root overrides dep
     FindMacroSpec(
-        macros=[MockMacro('root'), MockMacro('dep')],
+        macros=[MockGenerateMacro('root'), MockGenerateMacro('dep')],
         expected='root',
     ),
 
     # root overrides core
     FindMacroSpec(
-        macros=[MockMacro('root'), MockMacro('dbt')],
+        macros=[MockGenerateMacro('root'), MockGenerateMacro('dbt')],
         expected='root',
     ),
 
     # dep overrides core
     FindMacroSpec(
-        macros=[MockMacro('dep'), MockMacro('dbt')],
+        macros=[MockGenerateMacro('dep'), MockGenerateMacro('dbt')],
         expected='dbt',
     ),
 
     # root overrides dep overrides core
     FindMacroSpec(
-        macros=[MockMacro('root'), MockMacro('dep'), MockMacro('dbt')],
+        macros=[MockGenerateMacro('root'), MockGenerateMacro('dep'), MockGenerateMacro('dbt')],
         expected='root',
     ),
 ]
@@ -879,7 +882,9 @@ def test_find_macro_by_name(macros, expectations):
 @pytest.mark.parametrize('macros,expected', generate_name_parameter_sets, ids=id_macro)
 def test_find_generate_macro_by_name(macros, expected):
     manifest = make_manifest(macros=macros)
-    result = manifest.find_generate_macro_by_name(name='my_macro', root_project_name='root')
+    result = manifest.find_generate_macro_by_name(
+        component='some_component', root_project_name='root'
+    )
     if expected is None:
         assert result is expected
     else:
diff --git a/test/unit/test_parser.py b/test/unit/test_parser.py
--- a/test/unit/test_parser.py
+++ b/test/unit/test_parser.py
@@ -32,7 +32,7 @@
     FreshnessThreshold, ExternalTable, Docs
 )
 
-from .utils import config_from_parts_or_dicts, normalize
+from .utils import config_from_parts_or_dicts, normalize, generate_name_macros
 
 
 def get_abs_os_path(unix_path):
@@ -42,6 +42,32 @@ def get_abs_os_path(unix_path):
 class BaseParserTest(unittest.TestCase):
     maxDiff = None
 
+    def _generate_macros(self):
+        name_sql = {}
+        for component in ('database', 'schema', 'alias'):
+            if component == 'alias':
+                source = 'node.name'
+            else:
+                source = f'target.{component}'
+            name = f'generate_{component}_name'
+            sql = f'{{% macro {name}(value, node) %}} {{% if value %}} {{{{ value }}}} {{% else %}} {{{{ {source} }}}} {{% endif %}} {{% endmacro %}}'
+            name_sql[name] = sql
+
+        all_sql = '\n'.join(name_sql.values())
+        for name, sql in name_sql.items():
+            pm = ParsedMacro(
+                name=name,
+                resource_type=NodeType.Macro,
+                unique_id=f'macro.root.{name}',
+                package_name='root',
+                original_file_path=normalize('macros/macro.sql'),
+                root_path=get_abs_os_path('./dbt_modules/root'),
+                path=normalize('macros/macro.sql'),
+                raw_sql=all_sql,
+                macro_sql=sql,
+            )
+            yield pm
+
     def setUp(self):
         dbt.flags.STRICT_MODE = True
         dbt.flags.WARN_ERROR = True
@@ -98,7 +124,9 @@ def setUp(self):
         self.parser_patcher = mock.patch('dbt.parser.base.get_adapter')
         self.factory_parser = self.parser_patcher.start()
 
-        self.macro_manifest = Manifest.from_macros()
+        self.macro_manifest = Manifest.from_macros(
+            macros={m.unique_id: m for m in generate_name_macros('root')}
+        )
 
     def tearDown(self):
         self.parser_patcher.stop()
diff --git a/test/unit/utils.py b/test/unit/utils.py
--- a/test/unit/utils.py
+++ b/test/unit/utils.py
@@ -133,3 +133,32 @@ def assert_fails_validation(self, dct, cls=None):
 
         with self.assertRaises(ValidationError):
             cls.from_dict(dct)
+
+
+def generate_name_macros(package):
+    from dbt.contracts.graph.parsed import ParsedMacro
+    from dbt.node_types import NodeType
+    name_sql = {}
+    for component in ('database', 'schema', 'alias'):
+        if component == 'alias':
+            source = 'node.name'
+        else:
+            source = f'target.{component}'
+        name = f'generate_{component}_name'
+        sql = f'{{% macro {name}(value, node) %}} {{% if value %}} {{{{ value }}}} {{% else %}} {{{{ {source} }}}} {{% endif %}} {{% endmacro %}}'
+        name_sql[name] = sql
+
+    all_sql = '\n'.join(name_sql.values())
+    for name, sql in name_sql.items():
+        pm = ParsedMacro(
+            name=name,
+            resource_type=NodeType.Macro,
+            unique_id=f'macro.{package}.{name}',
+            package_name=package,
+            original_file_path=normalize('macros/macro.sql'),
+            root_path='./dbt_modules/root',
+            path=normalize('macros/macro.sql'),
+            raw_sql=all_sql,
+            macro_sql=sql,
+        )
+        yield pm