Skip to content

Commit e150626

Browse files
Preserve decimal places for dbt show (#8561) (#8588)
* update `Number` class to handle integer values (#8306) * add show test for json data * oh changie my changie * revert unecessary cahnge to fixture * keep decimal class for precision methods, but return __int__ value * jerco updates * update integer type * update other tests * Update .changes/unreleased/Fixes-20230803-093502.yaml --------- Co-authored-by: Emily Rockman <emily.rockman@dbtlabs.com> * account for integer vs number on table merges * add tests for combining number with integer. * add unit test when nulls are added * cant none as an Integer * fix null tests --------- Co-authored-by: dave-connors-3 <73915542+dave-connors-3@users.noreply.github.com> Co-authored-by: Dave Connors <dave.connors@fishtownanalytics.com> (cherry picked from commit be94bf1) Co-authored-by: Emily Rockman <emily.rockman@dbtlabs.com>
1 parent 48ba14c commit e150626

File tree

5 files changed

+125
-36
lines changed

5 files changed

+125
-36
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Fixes
2+
body: Add explicit support for integers for the show command
3+
time: 2023-08-03T09:35:02.163968-05:00
4+
custom:
5+
Author: dave-connors-3
6+
Issue: "8153"

core/dbt/clients/agate_helper.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@
1212
BOM = BOM_UTF8.decode("utf-8") # '\ufeff'
1313

1414

15+
class Integer(agate.data_types.DataType):
16+
def cast(self, d):
17+
# by default agate will cast none as a Number
18+
# but we need to cast it as an Integer to preserve
19+
# the type when merging and unioning tables
20+
if type(d) == int or d is None:
21+
return d
22+
else:
23+
raise agate.exceptions.CastError('Can not parse value "%s" as Integer.' % d)
24+
25+
def jsonify(self, d):
26+
return d
27+
28+
1529
class Number(agate.data_types.Number):
1630
# undo the change in https://github.com/wireservice/agate/pull/733
1731
# i.e. do not cast True and False to numeric 1 and 0
@@ -47,6 +61,7 @@ def build_type_tester(
4761
) -> agate.TypeTester:
4862

4963
types = [
64+
Integer(null_values=("null", "")),
5065
Number(null_values=("null", "")),
5166
agate.data_types.Date(null_values=("null", ""), date_format="%Y-%m-%d"),
5267
agate.data_types.DateTime(null_values=("null", ""), datetime_format="%Y-%m-%d %H:%M:%S"),
@@ -165,6 +180,13 @@ def __setitem__(self, key, value):
165180
elif isinstance(value, _NullMarker):
166181
# use the existing value
167182
return
183+
# when one table column is Number while another is Integer, force the column to Number on merge
184+
elif isinstance(value, Integer) and isinstance(existing_type, agate.data_types.Number):
185+
# use the existing value
186+
return
187+
elif isinstance(existing_type, Integer) and isinstance(value, agate.data_types.Number):
188+
# overwrite
189+
super().__setitem__(key, value)
168190
elif not isinstance(value, type(existing_type)):
169191
# actual type mismatch!
170192
raise DbtRuntimeError(
@@ -176,8 +198,9 @@ def finalize(self) -> Dict[str, agate.data_types.DataType]:
176198
result: Dict[str, agate.data_types.DataType] = {}
177199
for key, value in self.items():
178200
if isinstance(value, _NullMarker):
179-
# this is what agate would do.
180-
result[key] = agate.data_types.Number()
201+
# agate would make it a Number but we'll make it Integer so that if this column
202+
# gets merged with another Integer column, it won't get forced to a Number
203+
result[key] = Integer()
181204
else:
182205
result[key] = value
183206
return result

tests/functional/show/fixtures.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,31 @@
22
select * from {{ ref('sample_seed') }}
33
"""
44

5+
models__sample_number_model = """
6+
select
7+
cast(1.0 as int) as float_to_int_field,
8+
3.0 as float_field,
9+
4.3 as float_with_dec_field,
10+
5 as int_field
11+
"""
12+
13+
models__sample_number_model_with_nulls = """
14+
select
15+
cast(1.0 as int) as float_to_int_field,
16+
3.0 as float_field,
17+
4.3 as float_with_dec_field,
18+
5 as int_field
19+
20+
union all
21+
22+
select
23+
cast(null as int) as float_to_int_field,
24+
cast(null as float) as float_field,
25+
cast(null as float) as float_with_dec_field,
26+
cast(null as int) as int_field
27+
28+
"""
29+
530
models__second_model = """
631
select
732
sample_num as col_one,

tests/functional/show/test_show.py

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
models__second_ephemeral_model,
77
seeds__sample_seed,
88
models__sample_model,
9+
models__sample_number_model,
10+
models__sample_number_model_with_nulls,
911
models__second_model,
1012
models__ephemeral_model,
1113
schema_yml,
@@ -14,11 +16,13 @@
1416
)
1517

1618

17-
class BaseTestShow:
19+
class TestShow:
1820
@pytest.fixture(scope="class")
1921
def models(self):
2022
return {
2123
"sample_model.sql": models__sample_model,
24+
"sample_number_model.sql": models__sample_number_model,
25+
"sample_number_model_with_nulls.sql": models__sample_number_model_with_nulls,
2226
"second_model.sql": models__second_model,
2327
"ephemeral_model.sql": models__ephemeral_model,
2428
"sql_header.sql": models__sql_header,
@@ -28,17 +32,13 @@ def models(self):
2832
def seeds(self):
2933
return {"sample_seed.csv": seeds__sample_seed}
3034

31-
32-
class TestNone(BaseTestShow):
3335
def test_none(self, project):
3436
with pytest.raises(
3537
DbtRuntimeError, match="Either --select or --inline must be passed to show"
3638
):
3739
run_dbt(["seed"])
3840
run_dbt(["show"])
3941

40-
41-
class TestSelectModelText(BaseTestShow):
4242
def test_select_model_text(self, project):
4343
run_dbt(["build"])
4444
(results, log_output) = run_dbt_and_capture(["show", "--select", "second_model"])
@@ -48,8 +48,6 @@ def test_select_model_text(self, project):
4848
assert "col_two" in log_output
4949
assert "answer" in log_output
5050

51-
52-
class TestSelectMultModelText(BaseTestShow):
5351
def test_select_multiple_model_text(self, project):
5452
run_dbt(["build"])
5553
(results, log_output) = run_dbt_and_capture(
@@ -59,8 +57,6 @@ def test_select_multiple_model_text(self, project):
5957
assert "sample_num" in log_output
6058
assert "sample_bool" in log_output
6159

62-
63-
class TestSelectSingleMultModelJson(BaseTestShow):
6460
def test_select_single_model_json(self, project):
6561
run_dbt(["build"])
6662
(results, log_output) = run_dbt_and_capture(
@@ -70,8 +66,32 @@ def test_select_single_model_json(self, project):
7066
assert "sample_num" in log_output
7167
assert "sample_bool" in log_output
7268

69+
def test_numeric_values(self, project):
70+
run_dbt(["build"])
71+
(results, log_output) = run_dbt_and_capture(
72+
["show", "--select", "sample_number_model", "--output", "json"]
73+
)
74+
assert "Previewing node 'sample_number_model'" not in log_output
75+
assert "1.0" not in log_output
76+
assert "1" in log_output
77+
assert "3.0" in log_output
78+
assert "4.3" in log_output
79+
assert "5" in log_output
80+
assert "5.0" not in log_output
81+
82+
def test_numeric_values_with_nulls(self, project):
83+
run_dbt(["build"])
84+
(results, log_output) = run_dbt_and_capture(
85+
["show", "--select", "sample_number_model_with_nulls", "--output", "json"]
86+
)
87+
assert "Previewing node 'sample_number_model_with_nulls'" not in log_output
88+
assert "1.0" not in log_output
89+
assert "1" in log_output
90+
assert "3.0" in log_output
91+
assert "4.3" in log_output
92+
assert "5" in log_output
93+
assert "5.0" not in log_output
7394

74-
class TestInlinePass(BaseTestShow):
7595
def test_inline_pass(self, project):
7696
run_dbt(["build"])
7797
(results, log_output) = run_dbt_and_capture(
@@ -81,8 +101,6 @@ def test_inline_pass(self, project):
81101
assert "sample_num" in log_output
82102
assert "sample_bool" in log_output
83103

84-
85-
class TestShowExceptions(BaseTestShow):
86104
def test_inline_fail(self, project):
87105
with pytest.raises(DbtException, match="Error parsing inline query"):
88106
run_dbt(["show", "--inline", "select * from {{ ref('third_model') }}"])
@@ -91,8 +109,6 @@ def test_inline_fail_database_error(self, project):
91109
with pytest.raises(DbtRuntimeError, match="Database Error"):
92110
run_dbt(["show", "--inline", "slect asdlkjfsld;j"])
93111

94-
95-
class TestEphemeralModels(BaseTestShow):
96112
def test_ephemeral_model(self, project):
97113
run_dbt(["build"])
98114
(results, log_output) = run_dbt_and_capture(["show", "--select", "ephemeral_model"])
@@ -105,8 +121,6 @@ def test_second_ephemeral_model(self, project):
105121
)
106122
assert "col_hundo" in log_output
107123

108-
109-
class TestLimit(BaseTestShow):
110124
@pytest.mark.parametrize(
111125
"args,expected",
112126
[
@@ -121,14 +135,10 @@ def test_limit(self, project, args, expected):
121135
results, log_output = run_dbt_and_capture(dbt_args)
122136
assert len(results.results[0].agate_table) == expected
123137

124-
125-
class TestSeed(BaseTestShow):
126138
def test_seed(self, project):
127139
(results, log_output) = run_dbt_and_capture(["show", "--select", "sample_seed"])
128140
assert "Previewing node 'sample_seed'" in log_output
129141

130-
131-
class TestSqlHeader(BaseTestShow):
132142
def test_sql_header(self, project):
133143
run_dbt(["build"])
134144
(results, log_output) = run_dbt_and_capture(["show", "--select", "sql_header"])

tests/unit/test_agate_helper.py

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -121,39 +121,64 @@ def test_datetime_formats(self):
121121
self.assertEqual(tbl[0][0], expected)
122122

123123
def test_merge_allnull(self):
124-
t1 = agate.Table([(1, "a", None), (2, "b", None)], ("a", "b", "c"))
125-
t2 = agate.Table([(3, "c", None), (4, "d", None)], ("a", "b", "c"))
124+
t1 = agate_helper.table_from_rows([(1, "a", None), (2, "b", None)], ("a", "b", "c"))
125+
t2 = agate_helper.table_from_rows([(3, "c", None), (4, "d", None)], ("a", "b", "c"))
126126
result = agate_helper.merge_tables([t1, t2])
127127
self.assertEqual(result.column_names, ("a", "b", "c"))
128-
assert isinstance(result.column_types[0], agate.data_types.Number)
128+
assert isinstance(result.column_types[0], agate_helper.Integer)
129129
assert isinstance(result.column_types[1], agate.data_types.Text)
130-
assert isinstance(result.column_types[2], agate.data_types.Number)
130+
assert isinstance(result.column_types[2], agate_helper.Integer)
131131
self.assertEqual(len(result), 4)
132132

133133
def test_merge_mixed(self):
134-
t1 = agate.Table([(1, "a", None), (2, "b", None)], ("a", "b", "c"))
135-
t2 = agate.Table([(3, "c", "dog"), (4, "d", "cat")], ("a", "b", "c"))
136-
t3 = agate.Table([(3, "c", None), (4, "d", None)], ("a", "b", "c"))
134+
t1 = agate_helper.table_from_rows(
135+
[(1, "a", None, None), (2, "b", None, None)], ("a", "b", "c", "d")
136+
)
137+
t2 = agate_helper.table_from_rows(
138+
[(3, "c", "dog", 1), (4, "d", "cat", 5)], ("a", "b", "c", "d")
139+
)
140+
t3 = agate_helper.table_from_rows(
141+
[(3, "c", None, 1.5), (4, "d", None, 3.5)], ("a", "b", "c", "d")
142+
)
137143

138144
result = agate_helper.merge_tables([t1, t2])
139-
self.assertEqual(result.column_names, ("a", "b", "c"))
140-
assert isinstance(result.column_types[0], agate.data_types.Number)
145+
self.assertEqual(result.column_names, ("a", "b", "c", "d"))
146+
assert isinstance(result.column_types[0], agate_helper.Integer)
141147
assert isinstance(result.column_types[1], agate.data_types.Text)
142148
assert isinstance(result.column_types[2], agate.data_types.Text)
149+
assert isinstance(result.column_types[3], agate_helper.Integer)
150+
self.assertEqual(len(result), 4)
151+
152+
result = agate_helper.merge_tables([t1, t3])
153+
self.assertEqual(result.column_names, ("a", "b", "c", "d"))
154+
assert isinstance(result.column_types[0], agate_helper.Integer)
155+
assert isinstance(result.column_types[1], agate.data_types.Text)
156+
assert isinstance(result.column_types[2], agate_helper.Integer)
157+
assert isinstance(result.column_types[3], agate.data_types.Number)
143158
self.assertEqual(len(result), 4)
144159

145160
result = agate_helper.merge_tables([t2, t3])
146-
self.assertEqual(result.column_names, ("a", "b", "c"))
147-
assert isinstance(result.column_types[0], agate.data_types.Number)
161+
self.assertEqual(result.column_names, ("a", "b", "c", "d"))
162+
assert isinstance(result.column_types[0], agate_helper.Integer)
148163
assert isinstance(result.column_types[1], agate.data_types.Text)
149164
assert isinstance(result.column_types[2], agate.data_types.Text)
165+
assert isinstance(result.column_types[3], agate.data_types.Number)
166+
self.assertEqual(len(result), 4)
167+
168+
result = agate_helper.merge_tables([t3, t2])
169+
self.assertEqual(result.column_names, ("a", "b", "c", "d"))
170+
assert isinstance(result.column_types[0], agate_helper.Integer)
171+
assert isinstance(result.column_types[1], agate.data_types.Text)
172+
assert isinstance(result.column_types[2], agate.data_types.Text)
173+
assert isinstance(result.column_types[3], agate.data_types.Number)
150174
self.assertEqual(len(result), 4)
151175

152176
result = agate_helper.merge_tables([t1, t2, t3])
153-
self.assertEqual(result.column_names, ("a", "b", "c"))
154-
assert isinstance(result.column_types[0], agate.data_types.Number)
177+
self.assertEqual(result.column_names, ("a", "b", "c", "d"))
178+
assert isinstance(result.column_types[0], agate_helper.Integer)
155179
assert isinstance(result.column_types[1], agate.data_types.Text)
156180
assert isinstance(result.column_types[2], agate.data_types.Text)
181+
assert isinstance(result.column_types[3], agate.data_types.Number)
157182
self.assertEqual(len(result), 6)
158183

159184
def test_nocast_string_types(self):
@@ -191,7 +216,7 @@ def test_nocast_bool_01(self):
191216
self.assertEqual(len(tbl), len(result_set))
192217

193218
assert isinstance(tbl.column_types[0], agate.data_types.Boolean)
194-
assert isinstance(tbl.column_types[1], agate.data_types.Number)
219+
assert isinstance(tbl.column_types[1], agate_helper.Integer)
195220

196221
expected = [
197222
[True, Decimal(1)],

0 commit comments

Comments
 (0)