Repository: dbt-labs/dbt-core

Issue:
Issue generating docs on BigQuery
### Describe the bug
Docs generation can fail in new and interesting ways in dbt v0.15.0. Whereas on most databases, the `information_schema` is accessed at the database-level, on BigQuery, it is sometimes accessed at the `project` level and other times at the `dataset` level. This means that the catalog query on BigQuery can fail if a _dataset_ does not exist. That is _not_ the case on other plugins.

I've seen some reports of users whose `dbt docs generate` task is failing on BigQuery with `dataset xxx does not exist in region US`. This appears to be happening because dbt is querying the information schema for a dataset which does not exist.

There are two ways this can happen:
1. dbt is checking an information schema errantly. From @ryanhempenstall on Slack:

> Each of the folders within our models folder uses its own schema mapping in the dbt_project.yml file (pasted in below). The schema for our models is then generated based on the target + the schema in our profile + the schema mapping in the yml file. For example, a model might go into `dbt_ryan_facts` for me but would be added to `dbt_alice_facts` for Alice when we run with the target dev but if we’re running in production it would just be `facts`.

```
# dbt_project.yml file
models:
  dbtpoc:
    materialized: table
    staging:
      materialized: ephemeral
      tags: ["staging", "hourly"]
      schema: stage
    dimensions:
      materialized: ephemeral
      schema: dbtpoc
    facts:
      materialized: table
      schema: dbtpoc
    test_models:
      materialized: table
      schema: test
      tags: testing
```

The error that dbt encounters is:
```
  404 Not found: Dataset dev-de1-eu-datatools:dbt_ryan was not found in location US
```

This seems to indicate that dbt is checking a dataset called `dbt_ryan` which _no models_ are configured to build into! Here, it's possible that dbt is errantly generating an `information_schema` relation for a resource that should not participate in docs generation (eg. an ephemeral model, or a disabled model).

2. There might not be an error at all here, but the project might be configured to build a resource into a schema that doesn't exist (eg. a seed file exists, but `dbt seed` has never been run).

In either case, I think that failing the entire docs generation step for a single missing dataset is suboptimal. Can we either:
 - check the list of existing datasets and prune the supplied `information_schemas` to only search in existing datasets
 - run a single catalog query for each information schema, then combine the results at the end. If one of these queries fails with a `DatabaseError: 404 not found`, we can catch the error and print a warning. Note: running one query per dataset might incur a non-trivial cost on BigQuery.

### Steps To Reproduce
case #1: configure a model to build into a dataset that does not exist. Run `dbt docs generate` on BigQuery and observe the failure.

case #2: configure a disabled model with:
```
{{ config(disabled=true, schema='notrealnotreal') }}

select 1 as id
```

run `dbt docs generate` on BigQuery and observe the failure

case #3: configure an ephemeral model with:
```
{{ config(materialized='ephemeral', schema='notrealnotreal') }}

select 1 as id
```

run `dbt docs generate` on BigQuery and observe the failure

### Expected behavior
`dbt docs generate` should succeed even if a dataset present in the manifest does not exist in the database.

### System information
**Which database are you using dbt with?**
- bigquery


**The output of `dbt --version`:**
```
0.15.0
```


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
@@ -27,7 +27,9 @@
 
 from dbt.adapters.base.connections import BaseConnectionManager, Connection
 from dbt.adapters.base.meta import AdapterMeta, available
-from dbt.adapters.base.relation import ComponentName, BaseRelation
+from dbt.adapters.base.relation import (
+    ComponentName, BaseRelation, InformationSchema
+)
 from dbt.adapters.base import Column as BaseColumn
 from dbt.adapters.cache import RelationsCache
 
@@ -995,11 +997,16 @@ def _catalog_filter_table(
         """
         return table.where(_catalog_filter_schemas(manifest))
 
+    def _get_catalog_information_schemas(
+        self, manifest: Manifest
+    ) -> List[InformationSchema]:
+        return list(self._get_cache_schemas(manifest).keys())
+
     def get_catalog(self, manifest: Manifest) -> agate.Table:
         """Get the catalog for this manifest by running the get catalog macro.
         Returns an agate.Table of catalog information.
         """
-        information_schemas = list(self._get_cache_schemas(manifest).keys())
+        information_schemas = self._get_catalog_information_schemas(manifest)
         # make it a list so macros can index into it.
         kwargs = {'information_schemas': information_schemas}
         table = self.execute_macro(GET_CATALOG_MACRO_NAME,
diff --git a/plugins/bigquery/dbt/adapters/bigquery/impl.py b/plugins/bigquery/dbt/adapters/bigquery/impl.py
--- a/plugins/bigquery/dbt/adapters/bigquery/impl.py
+++ b/plugins/bigquery/dbt/adapters/bigquery/impl.py
@@ -13,6 +13,7 @@
 from dbt.adapters.bigquery import BigQueryColumn
 from dbt.adapters.bigquery import BigQueryConnectionManager
 from dbt.contracts.connection import Connection
+from dbt.contracts.graph.manifest import Manifest
 from dbt.logger import GLOBAL_LOGGER as logger
 from dbt.utils import filter_null_values
 
@@ -479,10 +480,32 @@ def load_dataframe(self, database, schema, table_name, agate_table,
         with self.connections.exception_handler("LOAD TABLE"):
             self.poll_until_job_completes(job, timeout)
 
-    def _catalog_filter_table(self, table, manifest):
-        # BigQuery doesn't allow ":" chars in column names -- remap them here.
+    @classmethod
+    def _catalog_filter_table(
+        cls, table: agate.Table, manifest: Manifest
+    ) -> agate.Table:
         table = table.rename(column_names={
             col.name: col.name.replace('__', ':') for col in table.columns
         })
-
         return super()._catalog_filter_table(table, manifest)
+
+    def _get_catalog_information_schemas(
+        self, manifest: Manifest
+    ) -> List[BigQueryInformationSchema]:
+
+        candidates = super()._get_catalog_information_schemas(manifest)
+        information_schemas = []
+        db_schemas = {}
+        for candidate in candidates:
+            database = candidate.database
+            if database not in db_schemas:
+                db_schemas[database] = set(self.list_schemas(database))
+            if candidate.schema in db_schemas[database]:
+                information_schemas.append(candidate)
+            else:
+                logger.debug(
+                    'Skipping catalog for {}.{} - schema does not exist'
+                    .format(database, candidate.schema)
+                )
+
+        return information_schemas


Test patch:
diff --git a/test/integration/029_docs_generate_tests/bq_models_noschema/disabled.sql b/test/integration/029_docs_generate_tests/bq_models_noschema/disabled.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/029_docs_generate_tests/bq_models_noschema/disabled.sql
@@ -0,0 +1,2 @@
+{{ config(disabled=true, schema='notrealnotreal') }}
+select 1 as id
diff --git a/test/integration/029_docs_generate_tests/bq_models_noschema/model.sql b/test/integration/029_docs_generate_tests/bq_models_noschema/model.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/029_docs_generate_tests/bq_models_noschema/model.sql
@@ -0,0 +1,2 @@
+{{ config(schema=var('extra_schema')) }}
+select 1 as id
diff --git a/test/integration/029_docs_generate_tests/test_docs_generate.py b/test/integration/029_docs_generate_tests/test_docs_generate.py
--- a/test/integration/029_docs_generate_tests/test_docs_generate.py
+++ b/test/integration/029_docs_generate_tests/test_docs_generate.py
@@ -3005,3 +3005,33 @@ def test__presto__run_and_generate(self):
         self.verify_run_results(self.expected_run_results(
             model_database=self.default_database
         ))
+
+
+class TestDocsGenerateMissingSchema(DBTIntegrationTest):
+    @property
+    def schema(self):
+        return 'docs_generate_029'
+
+    @staticmethod
+    def dir(path):
+        return normalize(path)
+
+    @property
+    def models(self):
+        return self.dir("bq_models_noschema")
+
+    def setUp(self):
+        super().setUp()
+        self.extra_schema = self.unique_schema() + '_bq_test'
+
+    def tearDown(self):
+        with self.adapter.connection_named('__test'):
+            self._drop_schema_named(self.default_database, self.extra_schema)
+        super().tearDown()
+
+    @use_profile('bigquery')
+    def test_bigquery_docs_generate_noschema(self):
+        self.run_dbt([
+            'docs', 'generate',
+            '--vars', "{{extra_schema: {}}}".format(self.extra_schema)
+        ])