Skip to content

Commit fca9a3c

Browse files
committed
fix(console): omit empty per-column table-diff samples
Signed-off-by: Vamsi-klu <nrvamsi13@gmail.com>
1 parent c0026dc commit fca9a3c

3 files changed

Lines changed: 198 additions & 1 deletion

File tree

‎sqlmesh/core/console.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2602,6 +2602,8 @@ def show_row_diff(
26022602
axis=1,
26032603
)
26042604
]
2605+
if column_table.empty:
2606+
continue
26052607

26062608
# Rename the column headers for readability
26072609
column_table = column_table.rename(

‎tests/core/test_console.py‎

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,156 @@
1-
from sqlmesh.core.console import MarkdownConsole
1+
from io import StringIO
2+
3+
import pytest
4+
from rich.console import Console
5+
6+
from sqlmesh.core.console import MarkdownConsole, TerminalConsole
7+
from sqlmesh.core.table_diff import RowDiff
8+
9+
10+
@pytest.fixture
11+
def row_diff():
12+
import pandas as pd
13+
14+
return RowDiff(
15+
source="source",
16+
target="target",
17+
stats={
18+
"s_count": 2,
19+
"t_count": 2,
20+
"distinct_count_s": 2,
21+
"distinct_count_t": 2,
22+
"join_count": 2,
23+
"full_match_count": 0,
24+
"s_only_count": 0,
25+
"t_only_count": 0,
26+
"null_grain_count": 0,
27+
},
28+
column_stats=pd.DataFrame(
29+
{"pct_match": [50.0, 50.0, 100.0]}, index=["early", "late", "unchanged"]
30+
),
31+
sample=pd.DataFrame(),
32+
joined_sample=pd.DataFrame(
33+
{"id": [1], "s__early": [10], "t__early": [99], "s__late": [20], "t__late": [20]}
34+
),
35+
s_sample=pd.DataFrame(),
36+
t_sample=pd.DataFrame(),
37+
)
38+
39+
40+
@pytest.mark.parametrize("console_type", [TerminalConsole, MarkdownConsole])
41+
def test_show_row_diff_omits_empty_column_samples(row_diff, console_type):
42+
output = StringIO()
43+
console = console_type()
44+
console.console = Console(file=output, width=120)
45+
46+
console.show_row_diff(row_diff)
47+
48+
rendered = output.getvalue()
49+
assert "Column: early" in rendered
50+
assert "99" in rendered
51+
assert "Column: late" not in rendered
52+
assert "Column: unchanged" not in rendered
53+
assert "early" in rendered and "late" in rendered and "unchanged" in rendered
54+
assert "50.0" in rendered and "100.0" in rendered
55+
assert "PARTIAL MATCH: 2 rows (100.0%)" in rendered
56+
57+
58+
@pytest.mark.parametrize("dtype", ["object", "Int64", "boolean", "float64"])
59+
def test_show_row_diff_omits_equal_null_samples(row_diff, dtype):
60+
import pandas as pd
61+
62+
nulls = pd.Series([float("nan")] if dtype == "float64" else [None], dtype=dtype)
63+
row_diff = row_diff.copy(
64+
update={"joined_sample": row_diff.joined_sample.assign(s__late=nulls, t__late=nulls.copy())}
65+
)
66+
output = StringIO()
67+
68+
TerminalConsole(console=Console(file=output)).show_row_diff(row_diff)
69+
70+
assert "Column: early" in output.getvalue()
71+
assert "Column: late" not in output.getvalue()
72+
73+
74+
def test_show_row_diff_preserves_null_differences_aliases_keys_and_inputs(row_diff):
75+
import pandas as pd
76+
77+
row_diff = row_diff.copy(
78+
update={
79+
"source_alias": "dev",
80+
"target_alias": "prod",
81+
"stats": {
82+
**row_diff.stats,
83+
"s_count": 3,
84+
"t_count": 3,
85+
"distinct_count_s": 3,
86+
"distinct_count_t": 3,
87+
"s_only_count": 1,
88+
"t_only_count": 1,
89+
},
90+
"joined_sample": pd.DataFrame(
91+
{
92+
"id": [1, 2],
93+
"region": ["east", "west"],
94+
"DEV__early": pd.Series([10, None], dtype="Int64"),
95+
"PROD__early": pd.Series([None, 20], dtype="Int64"),
96+
"DEV__late": pd.Series([True, None], dtype="boolean"),
97+
"PROD__late": pd.Series([True, None], dtype="boolean"),
98+
}
99+
),
100+
"s_sample": pd.DataFrame({"id": [3], "region": ["source-only"]}),
101+
"t_sample": pd.DataFrame({"id": [4], "region": ["target-only"]}),
102+
}
103+
)
104+
original_stats = row_diff.stats.copy()
105+
frames = {
106+
name: getattr(row_diff, name).copy(deep=True)
107+
for name in ("sample", "joined_sample", "s_sample", "t_sample", "column_stats")
108+
}
109+
output = StringIO()
110+
111+
TerminalConsole(console=Console(file=output, width=120)).show_row_diff(row_diff)
112+
113+
rendered = output.getvalue()
114+
assert "Column: early" in rendered
115+
assert "Column: late" not in rendered
116+
assert all(label in rendered for label in ("id", "region", "DEV", "PROD"))
117+
assert all(value in rendered for value in ("east", "west", "10", "20", "<NA>"))
118+
assert "DEV ONLY: 1 rows" in rendered and "PROD ONLY: 1 rows" in rendered
119+
assert "DEV ONLY sample rows:" in rendered and "source-only" in rendered
120+
assert "PROD ONLY sample rows:" in rendered and "target-only" in rendered
121+
assert row_diff.stats == original_stats
122+
for name, frame in frames.items():
123+
pd.testing.assert_frame_equal(getattr(row_diff, name), frame)
124+
125+
126+
def test_show_row_diff_without_samples(row_diff):
127+
output = StringIO()
128+
129+
TerminalConsole(console=Console(file=output)).show_row_diff(row_diff, show_sample=False)
130+
131+
rendered = output.getvalue()
132+
assert "PARTIAL MATCH: 2 rows (100.0%)" in rendered
133+
assert "column comparison stats:" in rendered
134+
assert "late" in rendered and "unchanged" in rendered
135+
assert "sample" not in rendered
136+
assert "Column:" not in rendered
137+
138+
139+
def test_show_row_diff_without_joined_sample(row_diff):
140+
row_diff = row_diff.copy(
141+
update={
142+
"joined_sample": row_diff.joined_sample.iloc[:0],
143+
"stats": {**row_diff.stats, "full_match_count": 2},
144+
}
145+
)
146+
output = StringIO()
147+
148+
TerminalConsole(console=Console(file=output)).show_row_diff(row_diff)
149+
150+
rendered = output.getvalue()
151+
assert "FULL MATCH: 2 rows (100.0%)" in rendered
152+
assert "All joined rows match" in rendered
153+
assert "Column:" not in rendered
2154

3155

4156
def test_markdown_console_warning_block():

‎tests/core/test_table_diff.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,6 +1197,49 @@ def test_data_diff_sample_limit():
11971197
assert len(diff.joined_sample) == 3
11981198

11991199

1200+
@pytest.mark.parametrize("limit", [1, 2])
1201+
def test_data_diff_sample_omits_empty_column_tables(limit):
1202+
engine_adapter = DuckDBConnectionConfig().create_engine_adapter()
1203+
try:
1204+
engine_adapter.execute(
1205+
"CREATE TABLE src AS SELECT * FROM (VALUES (1, 10, 20, 30), (2, 10, 20, 30)) "
1206+
"AS records(id, early, late, unchanged)"
1207+
)
1208+
engine_adapter.execute(
1209+
"CREATE TABLE target AS SELECT * FROM (VALUES (1, 99, 20, 30), (2, 10, 99, 30)) "
1210+
"AS records(id, early, late, unchanged)"
1211+
)
1212+
table_diff = TableDiff(
1213+
adapter=engine_adapter, source="src", target="target", on=["id"], limit=limit
1214+
)
1215+
1216+
row_diff = table_diff.row_diff()
1217+
assert row_diff.partial_match_count == 2
1218+
assert row_diff.column_stats["pct_match"].to_dict() == {
1219+
"early": 50.0,
1220+
"late": 50.0,
1221+
"unchanged": 100.0,
1222+
}
1223+
assert row_diff.joined_sample["id"].tolist() == list(range(1, limit + 1))
1224+
assert "s__late" in row_diff.joined_sample
1225+
assert "s__unchanged" not in row_diff.joined_sample
1226+
original_stats = row_diff.column_stats.copy(deep=True)
1227+
original_sample = row_diff.joined_sample.copy(deep=True)
1228+
1229+
output = strip_ansi_codes(capture_console_output("show_row_diff", row_diff=row_diff))
1230+
1231+
assert "Column: early" in output
1232+
assert ("Column: late" in output) == (limit == 2)
1233+
assert "Column: unchanged" not in output
1234+
assert "99" in output
1235+
assert "PARTIAL MATCH: 2 rows (100.0%)" in output
1236+
assert "unchanged" in output and "100.0" in output
1237+
pd.testing.assert_frame_equal(row_diff.column_stats, original_stats)
1238+
pd.testing.assert_frame_equal(row_diff.joined_sample, original_sample)
1239+
finally:
1240+
engine_adapter.close()
1241+
1242+
12001243
def test_data_diff_nulls_in_some_grain_columns():
12011244
engine_adapter = DuckDBConnectionConfig().create_engine_adapter()
12021245

0 commit comments

Comments
 (0)