Repository: dbt-labs/dbt-core

Issue:
[CT-2848] [Bug] `dbt show` adding decimal places to non-decimal values
### 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

`dbt show` is adding decimal places to numeric/integer values incorrectly. This is causing problems for users in the dbt Cloud IDE, as the IDE uses that command to render the preview tab. 

### Expected/Previous Behavior

Previously (we think!) this was not an issue, and decimal places were properly handled by dbt show

### Steps To Reproduce

1. use dbt-snowflake>=1.5.0
2. create a model in your project:

```sql
# in my_model.sql
select
  cast(1 as numeric) as _numeric,
  cast(1 as integer) as _integer,
  cast(1 as decimal) as _decimal
```
3. run `dbt show -s my_model --output JSON`
4. get this!
```zsh
❯ dbt show -s my_model --output json
20:33:15  Running with dbt=1.5.3
20:33:16  Registered adapter: snowflake=1.5.2
20:33:16  Unable to do partial parsing because of a version mismatch
20:33:17  Found 3 models, 4 tests, 0 snapshots, 0 analyses, 436 macros, 0 operations, 0 seed files, 0 sources, 0 exposures, 0 metrics, 0 groups
20:33:17  
20:33:18  Concurrency: 8 threads (target='dev')
20:33:18  
20:33:19  {
  "node": "my_model",
  "show": [
    {
      "_NUMERIC": 1.0,
      "_INTEGER": 1.0,
      "_DECIMAL": 1.0
    }
  ]
}
```

### Relevant log output

_No response_

### Environment

```markdown
- OS: mac
- Python: Python 3.9.16
- dbt (working version): unknown
- dbt (regression version): 1.5.3
```


### Which database adapter are you using with dbt?

snowflake

### Additional Context

_No response_


Discussion:
did some more local testing on other versions of 1.5 and observed the same behavior - -this may not be a regression, but just a regular bug!
Copying from #8167 (great minds think alike @dave-connors-3):

## What's going on?

We're just using the `agate` library's type system and JSON serialization:

https://github.com/dbt-labs/dbt-core/blob/eeb057085c19301f9803d1620d79645f27d501b5/core/dbt/task/show.py#L78-L83

This is being returned and saved as a `decimal.Decimal` type, rather than a Python `int` — and the `decimal.Decimal` type serializes to JSON `number` (`1.0`) instead of JSON `integer` (`1`)

```python
ipdb> table
<agate.table.Table object at 0x1078d1360>
ipdb> [list(row) for row in table]
[['0005', 'false', Decimal('10'), False, '01T00000aabbccdd'], ['0006', 'true', Decimal('20'), True, '01T00000aabbccde']]
ipdb> table.print_json()
[{"stringnum": "0005", "stringbool": "false", "intfield": 10.0, "boolfield": false, "dtstring": "01T00000aabbccdd"}, {"stringnum": "0006", "stringbool": "true", "intfield": 20.0, "boolfield": true, "dtstring": "01T00000aabbccde"}]
ipdb> type(table[0][2])
<class 'decimal.Decimal'>
```

Thanks to 2 great minds for opening this up! 

It does look like this was an issue with prior versions of the IDE (before the `dbt show` command existed), so it's not a regression.

That being said, we definitely want to enable users to be able to spot check their data using `dbt show` / preview in the IDE. 
Experiencing the same issue here, running dbt version 1.5.0. Following for updates.
I think we might be able to fix this with a couple of additional overrides in [our custom `Number` type](https://github.com/dbt-labs/dbt-core/blob/a433c31d6ebae79bd5b281653551899b7369067c/core/dbt/clients/agate_helper.py#L16-L23), which reimplements `agate`'s built-in one:

```python
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.")
        # preserve integers as native Python int
        elif type(d) == int:
            return d
        else:
            return super().cast(d)
            
    def jsonify(self, d):
        if d is None:
            return d
        # do not cast integers to floats
        elif type(d) == int:
            return d

        return float(d)
```

```
$ dbt show -s test --output json
20:08:09  Running with dbt=1.7.0-a1
20:08:09  Registered adapter: postgres=1.7.0-a1
20:08:09  Found 1 seed, 1 model, 0 sources, 0 exposures, 0 metrics, 352 macros, 0 groups, 0 semantic models
20:08:09
20:08:09  Concurrency: 5 threads (target='dev')
20:08:09
20:08:09  {
  "node": "test",
  "show": [
    {
      "_numeric": 1.0,
      "_integer": 1,
      "_decimal": 1.0
    }
  ]
}
```
@dave-connors-3 Okay, the failing CI in #8306 reminded me of what was missing with my suggestion above. While this fixes the JSON output, it now raises an error while trying to do normal pretty-printed output, because `agate` expects all its `Number` data types to be the Python type `decimal.Decimal` — [here](https://github.com/wireservice/agate/blob/a53c2b12452152e61d7c5e5eb0df27e66c266684/agate/table/print_table.py#L76-L77) where it calls `max_precision`, which then calls [`normalize`](https://github.com/wireservice/agate/blob/a53c2b12452152e61d7c5e5eb0df27e66c266684/agate/utils.py#L136).

## Approach 1: During JSON serialization, if it can be an int, make it an int

In this approach, we allow the `Number` type to always return a `Decimal`, and never a true Python `int`, so that `Decimal`-specific helper methods can be called later on.

Only during JSON serialization, we convert all numbers with 0-scale (no digits after the decimal) to integers instead of floats. (Unfortunately, the `agate` library seems to call this "precision" instead of "scale"...). This yields a similar result to what agate does when pretty-printing markdown tables: `1.0` just becomes `1`, regardless of type.

**Downside:** This would remove the distinction between integer and decimal/numeric type, in the case where someone is saying `1.00::numeric` and wants to keep seeing those zeroes.

```python
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) -> Decimal:
        if type(d) == bool:
            raise agate.exceptions.CastError("Do not cast True to 1 or False to 0.")
        else:
            return super().cast(d)

    def jsonify(self, d) -> Union[int, float]:
        if d is None:
            return d
        # do not cast integers to floats
        elif d == int(d):
        # fancier way of doing the same: "if agate.utils.max_precision([d]) == 0|
            return int(d)
        else:
            return float(d)
```
```
$ dbt show -s test --output json
14:49:17  Running with dbt=1.7.0-a1
14:49:17  Registered adapter: postgres=1.7.0-a1
14:49:17  Found 1 model, 0 sources, 0 exposures, 0 metrics, 352 macros, 0 groups, 0 semantic models
14:49:17
14:49:17  Concurrency: 5 threads (target='dev')
14:49:17
14:49:17  {
  "node": "test",
  "show": [
    {
      "_numeric": 1,
      "_integer": 1,
      "_decimal": 1
    }
  ]
}
```

It still _works_ for decimals that have nonzero digits after the decimal place, but we're dropping any trailing zeroes. (_Remember "significant digits"? Remember high-school science class?_)
```
$ dbt show --output json --inline 'select 1 as a, 1.0 as b, 1.1 as c, 1.10 as d'
15:08:34  Running with dbt=1.7.0-a1
15:08:34  Registered adapter: postgres=1.7.0-a1
15:08:34  Found 1 model, 0 sources, 0 exposures, 0 metrics, 352 macros, 0 groups, 0 semantic models
15:08:34
15:08:34  Concurrency: 5 threads (target='dev')
15:08:34
15:08:34  {
  "show": [
    {
      "a": 1,
      "b": 1,
      "c": 1.1,
      "d": 1.1
    }
  ]
}
```

## Approach 2: Integer != Number

Rather than changing the `Number` type, we add a new data type `Integer`, which expects to return a Python `int` rather than a `decimal.Decimal` type:
```python
# we do not want integers being coerced to decimals in JSON output
class Integer(agate.data_types.DataType):
    def cast(self, d) -> int:
        if type(d) == int:
            return d
        else:
            raise CastError('Can not parse value "%s" as Integer.' % d)

    def jsonify(self, d) -> int:
        return d


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) -> decimal.Decimal:
        if type(d) == bool:
            raise agate.exceptions.CastError("Do not cast True to 1 or False to 0.")
        else:
            return super().cast(d)
```

Then, within our type tester further down, we need to try casting to `Integer` _before_ `Number`:
```python
def build_type_tester(
    text_columns: Iterable[str], string_null_values: Optional[Iterable[str]] = ("null", "")
) -> agate.TypeTester:

    types = [
        Integer(null_values=("null", "")),
        Number(null_values=("null", "")),
...
```

Because `Integer` isn't an instance of `agate.data_types.Number`, `agate` won't try to call `max_precision`/`normalize` on it [here](https://github.com/wireservice/agate/blob/a53c2b12452152e61d7c5e5eb0df27e66c266684/agate/table/print_table.py#L76-L77) while formatting the markdown table.

This does preserve the distinction between integers and decimals/numerics, although it doesn't preserve the exact scale of the original result set:

```sql
$ psql
psql (14.8 (Homebrew), server 13.11)
Type "help" for help.

jerco=# select
  cast(1 as numeric(38,3)) as _numeric,
  cast(1 as integer) as _integer,
  cast(1 as decimal(38,2)) as _decimal
;
 _numeric | _integer | _decimal
----------+----------+----------
    1.000 |        1 |     1.00
(1 row)
```

```
$ dbt show -s test --output json
14:58:28  Running with dbt=1.7.0-a1
14:58:28  Registered adapter: postgres=1.7.0-a1
14:58:28  Unable to do partial parsing because config vars, config profile, or config target have changed
14:58:28  Unable to do partial parsing because a project dependency has been added
14:58:28  Found 1 model, 0 sources, 0 exposures, 0 metrics, 352 macros, 0 groups, 0 semantic models
14:58:28
14:58:28  Concurrency: 5 threads (target='dev')
14:58:28
14:58:28  {
  "node": "test",
  "show": [
    {
      "_numeric": 1.0,
      "_integer": 1,
      "_decimal": 1.0
    }
  ]
}
```

On its face, this feels like the More Correct approach. **HOWEVER!! There is a risk of a very subtle breaking change here.** Because we expose agate types to end users, in the form of `run_query` results — if someone has been in the habit of returning integers, and then using `Decimal`-type methods against those returned values—methods such as `normalize`, or [anything else from this list](https://docs.python.org/3/library/decimal.html#decimal-objects))—they would start seeing errors like, "AttributeError: 'int' object has no attribute '<Decimal method name>'`

```
-- models/test.sql
{% set my_query %}

select
  cast(1 as numeric(38,3)) as _numeric,
  cast(1 as integer) as _integer,
  cast(1 as decimal(38,2)) as _decimal

{% endset %}

{% set results = run_query(my_query) %}

{% for row in results %}

  {{ row[0].is_zero() }}
  {{ row[1].is_zero() }}
  {{ row[2].is_zero() }}
  
{% endfor %}
```
```
$ dbt show -s test
...
Runtime Error
  Compilation Error in model test (models/test.sql)
    'int object' has no attribute 'is_zero'. This can happen when calling a macro that does not exist. Check for typos and/or install package dependencies with "dbt deps".
```

We've never explicitly _documented_ that you can do those things, but it would be a subtle breaking change nevertheless. We don't encounter this issue with Approach 1, because the values remain `Decimal` type all the way through, until the very moment of serializing to JSON.
@jtcohen6 it seems like option 2 is better here, with us documenting the breaking part of the change and only backporting to 1.6. Does that seem reasonable?
Yup, that's what I'm thinking too!

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
@@ -12,6 +12,20 @@
 BOM = BOM_UTF8.decode("utf-8")  # '\ufeff'
 
 
+class Integer(agate.data_types.DataType):
+    def cast(self, d):
+        # by default agate will cast none as a Number
+        # but we need to cast it as an Integer to preserve
+        # the type when merging and unioning tables
+        if type(d) == int or d is None:
+            return d
+        else:
+            raise agate.exceptions.CastError('Can not parse value "%s" as Integer.' % d)
+
+    def jsonify(self, d):
+        return d
+
+
 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
@@ -47,6 +61,7 @@ def build_type_tester(
 ) -> agate.TypeTester:
 
     types = [
+        Integer(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", ""), datetime_format="%Y-%m-%d %H:%M:%S"),
@@ -165,6 +180,13 @@ def __setitem__(self, key, value):
         elif isinstance(value, _NullMarker):
             # use the existing value
             return
+        # when one table column is Number while another is Integer, force the column to Number on merge
+        elif isinstance(value, Integer) and isinstance(existing_type, agate.data_types.Number):
+            # use the existing value
+            return
+        elif isinstance(existing_type, Integer) and isinstance(value, agate.data_types.Number):
+            # overwrite
+            super().__setitem__(key, value)
         elif not isinstance(value, type(existing_type)):
             # actual type mismatch!
             raise DbtRuntimeError(
@@ -176,8 +198,9 @@ def finalize(self) -> Dict[str, agate.data_types.DataType]:
         result: Dict[str, agate.data_types.DataType] = {}
         for key, value in self.items():
             if isinstance(value, _NullMarker):
-                # this is what agate would do.
-                result[key] = agate.data_types.Number()
+                # agate would make it a Number but we'll make it Integer so that if this column
+                # gets merged with another Integer column, it won't get forced to a Number
+                result[key] = Integer()
             else:
                 result[key] = value
         return result


Test patch:
diff --git a/tests/functional/show/fixtures.py b/tests/functional/show/fixtures.py
--- a/tests/functional/show/fixtures.py
+++ b/tests/functional/show/fixtures.py
@@ -2,6 +2,31 @@
 select * from {{ ref('sample_seed') }}
 """
 
+models__sample_number_model = """
+select
+  cast(1.0 as int) as float_to_int_field,
+  3.0 as float_field,
+  4.3 as float_with_dec_field,
+  5 as int_field
+"""
+
+models__sample_number_model_with_nulls = """
+select
+  cast(1.0 as int) as float_to_int_field,
+  3.0 as float_field,
+  4.3 as float_with_dec_field,
+  5 as int_field
+
+union all
+
+select
+  cast(null as int) as float_to_int_field,
+  cast(null as float) as float_field,
+  cast(null as float) as float_with_dec_field,
+  cast(null as int) as int_field
+
+"""
+
 models__second_model = """
 select
     sample_num as col_one,
diff --git a/tests/functional/show/test_show.py b/tests/functional/show/test_show.py
--- a/tests/functional/show/test_show.py
+++ b/tests/functional/show/test_show.py
@@ -6,6 +6,8 @@
     models__second_ephemeral_model,
     seeds__sample_seed,
     models__sample_model,
+    models__sample_number_model,
+    models__sample_number_model_with_nulls,
     models__second_model,
     models__ephemeral_model,
     schema_yml,
@@ -14,11 +16,13 @@
 )
 
 
-class BaseTestShow:
+class TestShow:
     @pytest.fixture(scope="class")
     def models(self):
         return {
             "sample_model.sql": models__sample_model,
+            "sample_number_model.sql": models__sample_number_model,
+            "sample_number_model_with_nulls.sql": models__sample_number_model_with_nulls,
             "second_model.sql": models__second_model,
             "ephemeral_model.sql": models__ephemeral_model,
             "sql_header.sql": models__sql_header,
@@ -28,8 +32,6 @@ def models(self):
     def seeds(self):
         return {"sample_seed.csv": seeds__sample_seed}
 
-
-class TestNone(BaseTestShow):
     def test_none(self, project):
         with pytest.raises(
             DbtRuntimeError, match="Either --select or --inline must be passed to show"
@@ -37,8 +39,6 @@ def test_none(self, project):
             run_dbt(["seed"])
             run_dbt(["show"])
 
-
-class TestSelectModelText(BaseTestShow):
     def test_select_model_text(self, project):
         run_dbt(["build"])
         (results, log_output) = run_dbt_and_capture(["show", "--select", "second_model"])
@@ -48,8 +48,6 @@ def test_select_model_text(self, project):
         assert "col_two" in log_output
         assert "answer" in log_output
 
-
-class TestSelectMultModelText(BaseTestShow):
     def test_select_multiple_model_text(self, project):
         run_dbt(["build"])
         (results, log_output) = run_dbt_and_capture(
@@ -59,8 +57,6 @@ def test_select_multiple_model_text(self, project):
         assert "sample_num" in log_output
         assert "sample_bool" in log_output
 
-
-class TestSelectSingleMultModelJson(BaseTestShow):
     def test_select_single_model_json(self, project):
         run_dbt(["build"])
         (results, log_output) = run_dbt_and_capture(
@@ -70,8 +66,32 @@ def test_select_single_model_json(self, project):
         assert "sample_num" in log_output
         assert "sample_bool" in log_output
 
+    def test_numeric_values(self, project):
+        run_dbt(["build"])
+        (results, log_output) = run_dbt_and_capture(
+            ["show", "--select", "sample_number_model", "--output", "json"]
+        )
+        assert "Previewing node 'sample_number_model'" not in log_output
+        assert "1.0" not in log_output
+        assert "1" in log_output
+        assert "3.0" in log_output
+        assert "4.3" in log_output
+        assert "5" in log_output
+        assert "5.0" not in log_output
+
+    def test_numeric_values_with_nulls(self, project):
+        run_dbt(["build"])
+        (results, log_output) = run_dbt_and_capture(
+            ["show", "--select", "sample_number_model_with_nulls", "--output", "json"]
+        )
+        assert "Previewing node 'sample_number_model_with_nulls'" not in log_output
+        assert "1.0" not in log_output
+        assert "1" in log_output
+        assert "3.0" in log_output
+        assert "4.3" in log_output
+        assert "5" in log_output
+        assert "5.0" not in log_output
 
-class TestInlinePass(BaseTestShow):
     def test_inline_pass(self, project):
         run_dbt(["build"])
         (results, log_output) = run_dbt_and_capture(
@@ -81,8 +101,6 @@ def test_inline_pass(self, project):
         assert "sample_num" in log_output
         assert "sample_bool" in log_output
 
-
-class TestShowExceptions(BaseTestShow):
     def test_inline_fail(self, project):
         with pytest.raises(DbtException, match="Error parsing inline query"):
             run_dbt(["show", "--inline", "select * from {{ ref('third_model') }}"])
@@ -91,8 +109,6 @@ def test_inline_fail_database_error(self, project):
         with pytest.raises(DbtRuntimeError, match="Database Error"):
             run_dbt(["show", "--inline", "slect asdlkjfsld;j"])
 
-
-class TestEphemeralModels(BaseTestShow):
     def test_ephemeral_model(self, project):
         run_dbt(["build"])
         (results, log_output) = run_dbt_and_capture(["show", "--select", "ephemeral_model"])
@@ -105,8 +121,6 @@ def test_second_ephemeral_model(self, project):
         )
         assert "col_hundo" in log_output
 
-
-class TestLimit(BaseTestShow):
     @pytest.mark.parametrize(
         "args,expected",
         [
@@ -121,14 +135,10 @@ def test_limit(self, project, args, expected):
         results, log_output = run_dbt_and_capture(dbt_args)
         assert len(results.results[0].agate_table) == expected
 
-
-class TestSeed(BaseTestShow):
     def test_seed(self, project):
         (results, log_output) = run_dbt_and_capture(["show", "--select", "sample_seed"])
         assert "Previewing node 'sample_seed'" in log_output
 
-
-class TestSqlHeader(BaseTestShow):
     def test_sql_header(self, project):
         run_dbt(["build"])
         (results, log_output) = run_dbt_and_capture(["show", "--select", "sql_header"])
diff --git a/tests/unit/test_agate_helper.py b/tests/unit/test_agate_helper.py
--- a/tests/unit/test_agate_helper.py
+++ b/tests/unit/test_agate_helper.py
@@ -121,39 +121,64 @@ def test_datetime_formats(self):
             self.assertEqual(tbl[0][0], expected)
 
     def test_merge_allnull(self):
-        t1 = agate.Table([(1, "a", None), (2, "b", None)], ("a", "b", "c"))
-        t2 = agate.Table([(3, "c", None), (4, "d", None)], ("a", "b", "c"))
+        t1 = agate_helper.table_from_rows([(1, "a", None), (2, "b", None)], ("a", "b", "c"))
+        t2 = agate_helper.table_from_rows([(3, "c", None), (4, "d", None)], ("a", "b", "c"))
         result = agate_helper.merge_tables([t1, t2])
         self.assertEqual(result.column_names, ("a", "b", "c"))
-        assert isinstance(result.column_types[0], agate.data_types.Number)
+        assert isinstance(result.column_types[0], agate_helper.Integer)
         assert isinstance(result.column_types[1], agate.data_types.Text)
-        assert isinstance(result.column_types[2], agate.data_types.Number)
+        assert isinstance(result.column_types[2], agate_helper.Integer)
         self.assertEqual(len(result), 4)
 
     def test_merge_mixed(self):
-        t1 = agate.Table([(1, "a", None), (2, "b", None)], ("a", "b", "c"))
-        t2 = agate.Table([(3, "c", "dog"), (4, "d", "cat")], ("a", "b", "c"))
-        t3 = agate.Table([(3, "c", None), (4, "d", None)], ("a", "b", "c"))
+        t1 = agate_helper.table_from_rows(
+            [(1, "a", None, None), (2, "b", None, None)], ("a", "b", "c", "d")
+        )
+        t2 = agate_helper.table_from_rows(
+            [(3, "c", "dog", 1), (4, "d", "cat", 5)], ("a", "b", "c", "d")
+        )
+        t3 = agate_helper.table_from_rows(
+            [(3, "c", None, 1.5), (4, "d", None, 3.5)], ("a", "b", "c", "d")
+        )
 
         result = agate_helper.merge_tables([t1, t2])
-        self.assertEqual(result.column_names, ("a", "b", "c"))
-        assert isinstance(result.column_types[0], agate.data_types.Number)
+        self.assertEqual(result.column_names, ("a", "b", "c", "d"))
+        assert isinstance(result.column_types[0], agate_helper.Integer)
         assert isinstance(result.column_types[1], agate.data_types.Text)
         assert isinstance(result.column_types[2], agate.data_types.Text)
+        assert isinstance(result.column_types[3], agate_helper.Integer)
+        self.assertEqual(len(result), 4)
+
+        result = agate_helper.merge_tables([t1, t3])
+        self.assertEqual(result.column_names, ("a", "b", "c", "d"))
+        assert isinstance(result.column_types[0], agate_helper.Integer)
+        assert isinstance(result.column_types[1], agate.data_types.Text)
+        assert isinstance(result.column_types[2], agate_helper.Integer)
+        assert isinstance(result.column_types[3], agate.data_types.Number)
         self.assertEqual(len(result), 4)
 
         result = agate_helper.merge_tables([t2, t3])
-        self.assertEqual(result.column_names, ("a", "b", "c"))
-        assert isinstance(result.column_types[0], agate.data_types.Number)
+        self.assertEqual(result.column_names, ("a", "b", "c", "d"))
+        assert isinstance(result.column_types[0], agate_helper.Integer)
         assert isinstance(result.column_types[1], agate.data_types.Text)
         assert isinstance(result.column_types[2], agate.data_types.Text)
+        assert isinstance(result.column_types[3], agate.data_types.Number)
+        self.assertEqual(len(result), 4)
+
+        result = agate_helper.merge_tables([t3, t2])
+        self.assertEqual(result.column_names, ("a", "b", "c", "d"))
+        assert isinstance(result.column_types[0], agate_helper.Integer)
+        assert isinstance(result.column_types[1], agate.data_types.Text)
+        assert isinstance(result.column_types[2], agate.data_types.Text)
+        assert isinstance(result.column_types[3], agate.data_types.Number)
         self.assertEqual(len(result), 4)
 
         result = agate_helper.merge_tables([t1, t2, t3])
-        self.assertEqual(result.column_names, ("a", "b", "c"))
-        assert isinstance(result.column_types[0], agate.data_types.Number)
+        self.assertEqual(result.column_names, ("a", "b", "c", "d"))
+        assert isinstance(result.column_types[0], agate_helper.Integer)
         assert isinstance(result.column_types[1], agate.data_types.Text)
         assert isinstance(result.column_types[2], agate.data_types.Text)
+        assert isinstance(result.column_types[3], agate.data_types.Number)
         self.assertEqual(len(result), 6)
 
     def test_nocast_string_types(self):
@@ -191,7 +216,7 @@ def test_nocast_bool_01(self):
         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)
+        assert isinstance(tbl.column_types[1], agate_helper.Integer)
 
         expected = [
             [True, Decimal(1)],