Repository: tobymao/sqlglot

Issue:
STDDEV parsing for TSQL dialect
**Before you file an issue**
- Make sure you specify the "read" dialect eg. `parse_one(sql, read="spark")`
- Make sure you specify the "write" dialect eg. `ast.sql(dialect="duckdb")`
- Check if the issue still exists on main

**Fully reproducible code snippet**
1. Parsing `STDDEV` from any dialect to `tsql` produces STDDEV instead of STDEV

```python
from sqlglot import parse_one

sample_q = 'SELECT STDDEV("test_column") FROM "test_table"'
sample_pg = parse_one(sample_q, read="postgres")
print(sample_pg.sql("tsql"))
```
Output:
```SQL
SELECT STDDEV([test_column]) FROM [test_table]
```
Expected:
```SQL
SELECT STDEV([test_column]) FROM [test_table]
```
2. Parsing `STDEV` as `tsql` and writing to any dialect also produces STDEV instead of STDDEV:
sample_q_tsql = "SELECT STDEV([test_column]) FROM [test_table]"
sample_tsql = parse_one(sample_q_tsql, read="tsql")
print(sample_tsql.sql("postgres"))
```

Output:
```SQL
SELECT STDEV("test_column") FROM "test_table"
```
Expected:
```
SELECT STDDEV("test_column") FROM "test_table"
```

**Official Documentation**
Please include links to official SQL documentation related to your issue.


Discussion:

Patch:
diff --git a/sqlglot/dialects/tsql.py b/sqlglot/dialects/tsql.py
index f233e5d4..fd3e48cc 100644
--- a/sqlglot/dialects/tsql.py
+++ b/sqlglot/dialects/tsql.py
@@ -855,6 +855,7 @@ class TSQL(Dialect):
                     transforms.eliminate_qualify,
                 ]
             ),
+            exp.Stddev: rename_func("STDEV"),
             exp.StrPosition: lambda self, e: self.func(
                 "CHARINDEX", e.args.get("substr"), e.this, e.args.get("position")
             ),
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
index f28da45c..3368b56b 100644
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -1394,6 +1394,7 @@ class Create(DDL):
         "end": False,
         "clone": False,
         "concurrently": False,
+        "clustered": False,
     }
 
     @property
@@ -5887,7 +5888,7 @@ class Sqrt(Func):
 
 
 class Stddev(AggFunc):
-    pass
+    _sql_names = ["STDDEV", "STDEV"]
 
 
 class StddevPop(AggFunc):
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index a6bbd396..d91b6f4f 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -1027,6 +1027,14 @@ class Generator(metaclass=_Generator):
         replace = " OR REPLACE" if expression.args.get("replace") else ""
         unique = " UNIQUE" if expression.args.get("unique") else ""
 
+        clustered = expression.args.get("clustered")
+        if clustered is None:
+            clustered_sql = ""
+        elif clustered:
+            clustered_sql = " CLUSTERED COLUMNSTORE"
+        else:
+            clustered_sql = " NONCLUSTERED COLUMNSTORE"
+
         postcreate_props_sql = ""
         if properties_locs.get(exp.Properties.Location.POST_CREATE):
             postcreate_props_sql = self.properties(
@@ -1036,7 +1044,7 @@ class Generator(metaclass=_Generator):
                 wrapped=False,
             )
 
-        modifiers = "".join((replace, unique, postcreate_props_sql))
+        modifiers = "".join((clustered_sql, replace, unique, postcreate_props_sql))
 
         postexpression_props_sql = ""
         if properties_locs.get(exp.Properties.Location.POST_EXPRESSION):
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index c340a9e7..0026ae7a 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -1663,6 +1663,15 @@ class Parser(metaclass=_Parser):
 
         unique = self._match(TokenType.UNIQUE)
 
+        if self._match_text_seq("CLUSTERED", "COLUMNSTORE"):
+            clustered = True
+        elif self._match_text_seq("NONCLUSTERED", "COLUMNSTORE") or self._match_text_seq(
+            "COLUMNSTORE"
+        ):
+            clustered = False
+        else:
+            clustered = None
+
         if self._match_pair(TokenType.TABLE, TokenType.FUNCTION, advance=False):
             self._advance()
 
@@ -1804,6 +1813,7 @@ class Parser(metaclass=_Parser):
             end=end,
             clone=clone,
             concurrently=concurrently,
+            clustered=clustered,
         )
 
     def _parse_sequence_properties(self) -> t.Optional[exp.SequenceProperties]:


Test patch:
diff --git a/tests/dialects/test_tsql.py b/tests/dialects/test_tsql.py
index 11d60e7f..9658a02c 100644
--- a/tests/dialects/test_tsql.py
+++ b/tests/dialects/test_tsql.py
@@ -391,6 +391,17 @@ class TestTSQL(Validator):
         self.validate_identity("HASHBYTES('MD2', 'x')")
         self.validate_identity("LOG(n, b)")
 
+        self.validate_all(
+            "STDEV(x)",
+            read={
+                "": "STDDEV(x)",
+            },
+            write={
+                "": "STDDEV(x)",
+                "tsql": "STDEV(x)",
+            },
+        )
+
     def test_option(self):
         possible_options = [
             "HASH GROUP",
@@ -888,6 +899,14 @@ class TestTSQL(Validator):
             },
         )
 
+        for colstore in ("NONCLUSTERED COLUMNSTORE", "CLUSTERED COLUMNSTORE"):
+            self.validate_identity(f"CREATE {colstore} INDEX index_name ON foo.bar")
+
+        self.validate_identity(
+            "CREATE COLUMNSTORE INDEX index_name ON foo.bar",
+            "CREATE NONCLUSTERED COLUMNSTORE INDEX index_name ON foo.bar",
+        )
+
     def test_insert_cte(self):
         self.validate_all(
             "INSERT INTO foo.bar WITH cte AS (SELECT 1 AS one) SELECT * FROM cte",