From c864418c1bb20fe92dcd5c705d0ed1c2d1e407df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Furga=C5=82a?= <83299832+00200200@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:14:16 +0200 Subject: [PATCH] fix(mssql): map bare Stddev to STDEV, not STDEVP sqlglot parses T-SQL STDEV (sample) into sge.Stddev, but the MSSQL generator rendered that node as STDEVP (population). Same-dialect round-trips through Table.sql() therefore changed the statistic. StddevPop already maps to STDEVP; Variance already maps to the sample function, so this matches that pattern. Closes #12057 --- ibis/backends/sql/dialects.py | 2 +- ibis/backends/sql/tests/test_compiler.py | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/ibis/backends/sql/dialects.py b/ibis/backends/sql/dialects.py index 41e416c9a001..be8be5ac6a39 100644 --- a/ibis/backends/sql/dialects.py +++ b/ibis/backends/sql/dialects.py @@ -360,7 +360,7 @@ class MSSQL(TSQL): class Generator(TSQL.Generator): TRANSFORMS = TSQL.Generator.TRANSFORMS.copy() | { sge.ApproxDistinct: rename_func("approx_count_distinct"), - sge.Stddev: rename_func("stdevp"), + sge.Stddev: rename_func("stdev"), sge.StddevPop: rename_func("stdevp"), sge.StddevSamp: rename_func("stdev"), sge.Variance: rename_func("var"), diff --git a/ibis/backends/sql/tests/test_compiler.py b/ibis/backends/sql/tests/test_compiler.py index 3596a7287b8c..f3966ced8517 100644 --- a/ibis/backends/sql/tests/test_compiler.py +++ b/ibis/backends/sql/tests/test_compiler.py @@ -4,7 +4,7 @@ import ibis from ibis import _ -from ibis.backends.sql.dialects import Trino +from ibis.backends.sql.dialects import MSSQL, Trino def test_window_with_row_number_compiles(): @@ -26,3 +26,24 @@ def test_transpile_join(): "SELECT * FROM t1 JOIN t2 ON x = y", read="duckdb", write=Trino ) assert "CROSS JOIN" not in result + + +def test_mssql_stdev_roundtrips_as_sample(): + # GH #12057: sqlglot parses T-SQL STDEV (sample) into sge.Stddev. The MSSQL + # dialect used to render that node as STDEVP (population), so a same- + # dialect round trip through Table.sql() / add_query_to_expr silently + # changed the statistic. Bare Stddev must render as STDEV; StddevPop + # already covers STDEVP. + (result,) = sg.transpile( + "SELECT STDEV([x]) AS [s] FROM [t]", read=MSSQL, write=MSSQL + ) + assert result == "SELECT STDEV([x]) AS [s] FROM [t]" + + +def test_mssql_std_how_emits_distinct_functions(): + t = ibis.table({"x": "float"}, name="t") + sample_sql = ibis.to_sql(t.x.std(how="sample"), dialect="mssql") + pop_sql = ibis.to_sql(t.x.std(how="pop"), dialect="mssql") + assert "STDEV(" in sample_sql + assert "STDEVP(" not in sample_sql + assert "STDEVP(" in pop_sql