Repository: dbt-labs/dbt-core

Issue:
Bools coerced to 0/1 in query results
This is probably the result of allowing newer versions of `agate` (https://github.com/dbt-labs/dbt-core/pull/3585):

<img width="624" alt="Screenshot 2021-12-17 at 23 30 54" src="https://user-images.githubusercontent.com/13897643/146615491-4d0195bf-c146-421e-a94c-e2fefb0c7acf.png">

I thought we had a fix in v0.21 (#3499) that prevented agate from coercing query results (including from booleans to `0`/`1`, https://github.com/wireservice/agate/pull/733), but it clearly still is, somehow.

Let's confirm whether the `agate` dependency is the cause, and if so:
- (worse fix) Roll it back to `1.6.1`
- (better fix) Fix the type coercion logic


Discussion:

Resolving patch:
diff --git a/core/dbt/clients/agate_helper.py b/core/dbt/clients/agate_helper.py
--- a/core/dbt/clients/agate_helper.py
+++ b/core/dbt/clients/agate_helper.py
@@ -13,6 +13,18 @@
 BOM = BOM_UTF8.decode('utf-8')  # '\ufeff'
 
 
+class Number(agate.data_types.Number):
+    # undo the change in https://github.com/wireservice/agate/pull/733
+    # i.e. do not cast True and False to numeric 1 and 0
+    def cast(self, d):
+        if type(d) == bool:
+            raise agate.exceptions.CastError(
+                'Do not cast True to 1 or False to 0.'
+            )
+        else:
+            return super().cast(d)
+
+
 class ISODateTime(agate.data_types.DateTime):
     def cast(self, d):
         # this is agate.data_types.DateTime.cast with the "clever" bits removed
@@ -41,7 +53,7 @@ def build_type_tester(
 ) -> agate.TypeTester:
 
     types = [
-        agate.data_types.Number(null_values=('null', '')),
+        Number(null_values=('null', '')),
         agate.data_types.Date(null_values=('null', ''),
                               date_format='%Y-%m-%d'),
         agate.data_types.DateTime(null_values=('null', ''),


Test patch:
diff --git a/test/unit/test_agate_helper.py b/test/unit/test_agate_helper.py
--- a/test/unit/test_agate_helper.py
+++ b/test/unit/test_agate_helper.py
@@ -154,3 +154,28 @@ def test_nocast_string_types(self):
 
         for i, row in enumerate(tbl):
             self.assertEqual(list(row), expected[i])
+
+    def test_nocast_bool_01(self):
+        # True and False values should not be cast to 1 and 0, and vice versa
+        # See: https://github.com/dbt-labs/dbt-core/issues/4511
+
+        column_names = ['a', 'b']
+        result_set = [
+            {'a': True, 'b': 1},
+            {'a': False, 'b': 0},
+        ]
+
+        tbl = agate_helper.table_from_data_flat(data=result_set, column_names=column_names)
+        self.assertEqual(len(tbl), len(result_set))
+
+        assert isinstance(tbl.column_types[0], agate.data_types.Boolean)
+        assert isinstance(tbl.column_types[1], agate.data_types.Number)
+
+        expected = [
+            [True, Decimal(1)],
+            [False, Decimal(0)],
+        ]
+
+        for i, row in enumerate(tbl):
+            self.assertEqual(list(row), expected[i])
+
diff --git a/test/unit/utils.py b/test/unit/utils.py
--- a/test/unit/utils.py
+++ b/test/unit/utils.py
@@ -255,7 +255,7 @@ def _get_tester_for(self, column_type):
             return agate.TimeDelta()
 
         for instance in agate_helper.DEFAULT_TYPE_TESTER._possible_types:
-            if type(instance) is column_type:
+            if isinstance(instance, column_type):  # include child types
                 return instance
 
         raise ValueError(f'no tester for {column_type}')