Repository: dbt-labs/dbt-core

Issue:
[CT-3259] [Regression] Running empty seed file raises unhandled exception
### Is this a regression in a recent version of dbt-core?

- [X] I believe this is a regression in dbt-core functionality
- [X] I have searched the existing issues, and I could not find an existing issue for this regression

### Current Behavior

We have an empty seed file in one of our dbt projects (Sometimes we need to manually intervene and add lines to it) that has been running successfully for a while now but after upgrading to dbt 1.6.6 you get an unhandled exception when compiling:

```
12:21:14  Running with dbt=1.6.6
12:21:15  Registered adapter: bigquery=1.6.7
12:21:15  Unable to do partial parsing because of a version mismatch
12:21:16  Found 3 models, 2 seeds, 0 sources, 0 exposures, 0 metrics, 394 macros, 0 groups, 0 semantic models
12:21:16  
12:21:25  Concurrency: 8 threads (target='integration-local')
12:21:25  
12:21:25  2 of 2 START seed file dry_run.my_seed ............................................................................................................................................. [RUN]
12:21:25  1 of 2 START seed file dry_run.empty_seed .......................................................................................................................................... [RUN]
12:21:25  Unhandled error while executing 
'NoneType' object has no attribute 'upper'
12:21:25  1 of 2 ERROR loading seed file dry_run.empty_seed .................................................................................................................................. [ERROR in 0.09s]
12:21:30  2 of 2 OK loaded seed file dry_run.my_seed ......................................................................................................................................... [INSERT 2 in 4.28s]
12:21:30  
12:21:30  Finished running 2 seeds in 0 hours 0 minutes and 13.56 seconds (13.56s).
12:21:30  
12:21:30  Completed with 1 error and 0 warnings:
12:21:30  
12:21:30    'NoneType' object has no attribute 'upper'
12:21:30  
12:21:30  Done. PASS=1 WARN=0 ERROR=1 SKIP=0 TOTAL=2

```

### Expected/Previous Behavior

In `dbt 1.5.x` it works fine:

```
12:24:09  Running with dbt=1.5.8
12:24:10  Registered adapter: bigquery=1.5.7
12:24:10  Unable to do partial parsing because of a version mismatch
12:24:10  Found 3 models, 0 tests, 0 snapshots, 0 analyses, 360 macros, 0 operations, 2 seed files, 0 sources, 0 exposures, 0 metrics, 0 groups
12:24:10  
12:24:11  Concurrency: 8 threads (target='integration-local')
12:24:11  
12:24:11  1 of 2 START seed file dry_run.empty_seed .......................................................................................................................................... [RUN]
12:24:11  2 of 2 START seed file dry_run.my_seed ............................................................................................................................................. [RUN]
12:24:15  2 of 2 OK loaded seed file dry_run.my_seed ......................................................................................................................................... [INSERT 2 in 3.76s]
12:24:16  1 of 2 OK loaded seed file dry_run.empty_seed ...................................................................................................................................... [INSERT 0 in 4.34s]
12:24:16  
12:24:16  Finished running 2 seeds in 0 hours 0 minutes and 5.52 seconds (5.52s).
12:24:16  
12:24:16  Completed successfully
12:24:16  
12:24:16  Done. PASS=2 WARN=0 ERROR=0 SKIP=0 TOTAL=2

``` 

### Steps To Reproduce

1. In dbt 1.6.x (I am using Big Query) create a project and create a seed with just headings:

```
#my_seed.csv
a,b,c
```

Then when you run `dbt build/seed` you will see the error.

### Relevant log output

```shell
Already posted.
```


### Environment

```markdown
- OS: Mac OS
- Python: 3.8.16
- dbt (working version): 1.5.8
- dbt (regression version): 1.6.6
```


### Which database adapter are you using with dbt?

bigquery

### Additional Context

_No response_


Discussion:
Hi! So to confirm, the expected behavior is that when you have a csv file with just the headings:

```
#my_seed.csv
a,b,c
```

dbt will create an empty table in the warehouse? 

I'm seeing a slightly different error for snowflake, but only on --full-refresh:
<img width="803" alt="Screenshot 2023-10-25 at 11 13 15 AM" src="https://github.com/dbt-labs/dbt-core/assets/53586774/17f9218e-6d5f-4a6c-ba32-0f492d9c70a4">

Interestingly, I do not see this error when running on snowflake 1.6.1 and core 1.6.0:
<img width="802" alt="Screenshot 2023-10-25 at 11 14 29 AM" src="https://github.com/dbt-labs/dbt-core/assets/53586774/f7f265fb-0d64-4a58-ba2f-de0e0b5a01dd">

Happens on both snowflake and big query.

### dbt-snowflake

```sh
dbt -d  seed -s empders --full-refresh

> Database Error in seed empders (seeds/empders.csv)
  002040 (42601): SQL compilation error:
  Unsupported data type 'NONE'.
```

There's no stacktrace here, but looking at Snowflake query history, this is the generated query that fails, obviously
```sql
create table TEST_DB.dbt_ajs.empders (col1 None,col2 None)
```
### dbt-bigquery

[here's dbt-bigquery stacktrace](https://gist.github.com/dataders/09669cae4dab2822f1eaf6e322a738f7) (relevant exract below).

[`BigQueryAdapter.load_dataframe()`](https://github.com/dbt-labs/dbt-bigquery/blob/6a3f45897f4e7ccd41de584d64605e7342dc75e0/dbt/adapters/bigquery/impl.py#L636-L648)


`AttributeError: 'NoneType' object has no attribute 'upper'`

```py
  File "/Users/dataders/miniforge3/envs/dbt/lib/python3.10/site-packages/dbt/adapters/bigquery/impl.py", line 781, in load_dataframe
    load_config.schema = bq_schema
  File "/Users/dataders/miniforge3/envs/dbt/lib/python3.10/site-packages/google/cloud/bigquery/job/base.py", line 172, in __setattr__
    super(_JobConfig, self).__setattr__(name, value)
  File "/Users/dataders/miniforge3/envs/dbt/lib/python3.10/site-packages/google/cloud/bigquery/job/load.py", line 477, in schema
    [field.to_api_repr() for field in value],
  File "/Users/dataders/miniforge3/envs/dbt/lib/python3.10/site-packages/google/cloud/bigquery/job/load.py", line 477, in <listcomp>
    [field.to_api_repr() for field in value],
  File "/Users/dataders/miniforge3/envs/dbt/lib/python3.10/site-packages/google/cloud/bigquery/schema.py", line 281, in to_api_repr
    if self.field_type.upper() in _STRUCT_TYPES:
AttributeError: 'NoneType' object has no attribute 'upper'
```


> Hi! So to confirm, the expected behavior is that when you have a csv file with just the headings:
> 
> ```
> #my_seed.csv
> a,b,c
> ```
> 
> dbt will create an empty table in the warehouse?
> 

Yes exactly we used to see dbt creating a blank table with 3 columns `a, b & c`

It looks like the `field_type` dbt 1.6 is passing to the BQ client is `None`. If I print the value of `bq_schema` here:

```python
    def load_dataframe(self, database, schema, table_name, agate_table, column_override):
        bq_schema = self._agate_to_schema(agate_table, column_override)
        print("BQ-SCHEMA")
        print(bq_schema)
```

```
BQ-SCHEMA
[SchemaField('a', None, 'NULLABLE', None, None, (), None), SchemaField('b', None, 'NULLABLE', None, None, (), None), SchemaField('c', None, 'NULLABLE', None, None, (), None)]
```

In 1.5 it infers it as an Integer (I didn't specify the seed schema in the yaml):

```
BQ-SCHEMA
[SchemaField('a', 'INT64', 'NULLABLE', None, None, (), None), SchemaField('b', 'INT64', 'NULLABLE', None, None, (), None), SchemaField('c', 'INT64', 'NULLABLE', None, None, (), None)]
```
This regression was caused by #8561 / #8153, released in 1.6.3.
Same happens in Redshift, after updating to 1.6.6
Any idea when is this going to be fixed?

This is my csv
```
"a","b","c","d","e","f","g"
```

When I add empty second row, it works
```
"a","b","c","d","e","f","g"
"","","","","","",""
```
but ofc I do not want that extra `null` row there.
@jan-benisek this issue is in our current sprint, so accounting for potential roll over: between 1.5-3 weeks.

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
@@ -51,6 +51,7 @@
     get_column_value_uncased,
     merge_tables,
     table_from_rows,
+    Integer,
 )
 from dbt.clients.jinja import MacroGenerator
 from dbt.contracts.graph.manifest import Manifest, MacroManifest
@@ -962,6 +963,17 @@ def convert_number_type(cls, agate_table: agate.Table, col_idx: int) -> str:
         """
         raise NotImplementedError("`convert_number_type` is not implemented for this adapter!")
 
+    @classmethod
+    def convert_integer_type(cls, agate_table: agate.Table, col_idx: int) -> str:
+        """Return the type in the database that best maps to the agate.Number
+        type for the given agate table and column index.
+
+        :param agate_table: The table
+        :param col_idx: The index into the agate table for the column.
+        :return: The name of the type in the database
+        """
+        return "integer"
+
     @classmethod
     @abc.abstractmethod
     def convert_boolean_type(cls, agate_table: agate.Table, col_idx: int) -> str:
@@ -1019,6 +1031,7 @@ def convert_type(cls, agate_table: agate.Table, col_idx: int) -> Optional[str]:
     def convert_agate_type(cls, agate_table: agate.Table, col_idx: int) -> Optional[str]:
         agate_type: Type = agate_table.column_types[col_idx]
         conversions: List[Tuple[Type, Callable[..., str]]] = [
+            (Integer, cls.convert_integer_type),
             (agate.Text, cls.convert_text_type),
             (agate.Number, cls.convert_number_type),
             (agate.Boolean, cls.convert_boolean_type),
diff --git a/core/dbt/adapters/sql/impl.py b/core/dbt/adapters/sql/impl.py
--- a/core/dbt/adapters/sql/impl.py
+++ b/core/dbt/adapters/sql/impl.py
@@ -75,6 +75,10 @@ def convert_number_type(cls, agate_table: agate.Table, col_idx: int) -> str:
         decimals = agate_table.aggregate(agate.MaxPrecision(col_idx))  # type: ignore[attr-defined]
         return "float8" if decimals else "integer"
 
+    @classmethod
+    def convert_integer_type(cls, agate_table: agate.Table, col_idx: int) -> str:
+        return "integer"
+
     @classmethod
     def convert_boolean_type(cls, agate_table: agate.Table, col_idx: int) -> str:
         return "boolean"


Test patch:
diff --git a/tests/adapter/dbt/tests/adapter/simple_seed/test_seed.py b/tests/adapter/dbt/tests/adapter/simple_seed/test_seed.py
--- a/tests/adapter/dbt/tests/adapter/simple_seed/test_seed.py
+++ b/tests/adapter/dbt/tests/adapter/simple_seed/test_seed.py
@@ -365,3 +365,26 @@ def seeds(self, test_data_dir):
     def test_simple_seed(self, project):
         results = run_dbt(["seed"])
         assert len(results) == 3
+
+
+class BaseTestEmptySeed:
+    @pytest.fixture(scope="class")
+    def project_config_update(self):
+        return {
+            "seeds": {
+                "quote_columns": False,
+            },
+        }
+
+    @pytest.fixture(scope="class")
+    def seeds(self):
+        return {"empty_with_header.csv": "a,b,c"}
+
+    def test_empty_seeds(self, project):
+        # Should create an empty table and not fail
+        results = run_dbt(["seed"])
+        assert len(results) == 1
+
+
+class TestEmptySeed(BaseTestEmptySeed):
+    pass
diff --git a/tests/unit/mock_adapter.py b/tests/unit/mock_adapter.py
--- a/tests/unit/mock_adapter.py
+++ b/tests/unit/mock_adapter.py
@@ -55,6 +55,9 @@ def convert_text_type(self, *args, **kwargs):
         def convert_number_type(self, *args, **kwargs):
             return self.responder.convert_number_type(*args, **kwargs)
 
+        def convert_integer_type(self, *args, **kwargs):
+            return self.responder.convert_integer_type(*args, **kwargs)
+
         def convert_boolean_type(self, *args, **kwargs):
             return self.responder.convert_boolean_type(*args, **kwargs)