Repository: tobymao/sqlglot

Issue:
Inaccurate sql generation for DATEPART in T-SQL
In TSQL queries while reading DATEPART function seems to be converted to FORMAT function, which would work fine but 'quarter' does not seem to be part of [format types](https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings). 
Code snippet:
`quarter_query = """
SELECT
    YEAR(sale_date) AS Year,
    DATEPART(QUARTER, sale_date) AS Quarter,
    SUM(sale_price) AS QuarterlySales
FROM
    Sales_AI
GROUP BY
    YEAR(sale_date),
    DATEPART(QUARTER, sale_date)
ORDER BY
    Year, Quarter
"""
print(parse_one(quarter_query, 'tsql').sql(dialect='tsql'))
`
Output: `"SELECT YEAR(sale_date) AS Year, FORMAT(CAST(sale_date AS DATETIME2), 'quarter') AS Quarter, SUM(sale_price) AS QuarterlySales FROM Sales_AI GROUP BY YEAR(sale_date), FORMAT(CAST(sale_date AS DATETIME2), 'quarter') ORDER BY Year, Quarter"`
Worked fine when time period was month. 
Would also like to know if there are any resources for creating a custom dialect to let datepart stay as datepart after parsing.

Discussion:

Patch:
diff --git a/sqlglot/dialects/tsql.py b/sqlglot/dialects/tsql.py
index 51040ca5..249d5211 100644
--- a/sqlglot/dialects/tsql.py
+++ b/sqlglot/dialects/tsql.py
@@ -153,6 +153,11 @@ def _format_sql(self: TSQL.Generator, expression: exp.NumberToStr | exp.TimeToSt
             )
         )
     )
+
+    # There is no format for "quarter"
+    if fmt.name.lower() == "quarter":
+        return self.func("DATEPART", "QUARTER", expression.this)
+
     return self.func("FORMAT", expression.this, fmt, expression.args.get("culture"))
 
 
@@ -245,9 +250,6 @@ class TSQL(Dialect):
 
     TIME_MAPPING = {
         "year": "%Y",
-        "qq": "%q",
-        "q": "%q",
-        "quarter": "%q",
         "dayofyear": "%j",
         "day": "%d",
         "dy": "%d",
@@ -684,9 +686,7 @@ class TSQL(Dialect):
             exp.Subquery: transforms.preprocess([qualify_derived_table_outputs]),
             exp.SHA: lambda self, e: self.func("HASHBYTES", exp.Literal.string("SHA1"), e.this),
             exp.SHA2: lambda self, e: self.func(
-                "HASHBYTES",
-                exp.Literal.string(f"SHA2_{e.args.get('length', 256)}"),
-                e.this,
+                "HASHBYTES", exp.Literal.string(f"SHA2_{e.args.get('length', 256)}"), e.this
             ),
             exp.TemporaryProperty: lambda self, e: "",
             exp.TimeStrToTime: timestrtotime_sql,


Test patch:
diff --git a/tests/dialects/test_tsql.py b/tests/dialects/test_tsql.py
index d94846cc..6188fd3a 100644
--- a/tests/dialects/test_tsql.py
+++ b/tests/dialects/test_tsql.py
@@ -175,18 +175,6 @@ class TestTSQL(Validator):
             "SELECT DISTINCT DepartmentName, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY BaseRate) OVER (PARTITION BY DepartmentName) AS MedianCont FROM dbo.DimEmployee"
         )
 
-        self.validate_all(
-            "SELECT DATEPART(year, CAST('2017-01-01' AS DATE))",
-            read={"postgres": "SELECT DATE_PART('year', '2017-01-01'::DATE)"},
-        )
-        self.validate_all(
-            "SELECT DATEPART(month, CAST('2017-03-01' AS DATE))",
-            read={"postgres": "SELECT DATE_PART('month', '2017-03-01'::DATE)"},
-        )
-        self.validate_all(
-            "SELECT DATEPART(day, CAST('2017-01-02' AS DATE))",
-            read={"postgres": "SELECT DATE_PART('day', '2017-01-02'::DATE)"},
-        )
         self.validate_all(
             "SELECT CAST([a].[b] AS SMALLINT) FROM foo",
             write={
@@ -900,11 +888,50 @@ WHERE
         )
 
     def test_datepart(self):
+        self.validate_identity("DATEPART(QUARTER, x)", "DATEPART(QUARTER, CAST(x AS DATETIME2))")
+        self.validate_identity("DATEPART(YEAR, x)", "FORMAT(CAST(x AS DATETIME2), 'yyyy')")
+
         self.validate_all(
             "SELECT DATEPART(month,'1970-01-01')",
-            write={"spark": "SELECT DATE_FORMAT(CAST('1970-01-01' AS TIMESTAMP), 'MM')"},
+            write={
+                "postgres": "SELECT TO_CHAR(CAST('1970-01-01' AS TIMESTAMP), 'MM')",
+                "spark": "SELECT DATE_FORMAT(CAST('1970-01-01' AS TIMESTAMP), 'MM')",
+                "tsql": "SELECT FORMAT(CAST('1970-01-01' AS DATETIME2), 'MM')",
+            },
+        )
+        self.validate_all(
+            "SELECT DATEPART(year, CAST('2017-01-01' AS DATE))",
+            read={
+                "postgres": "SELECT DATE_PART('year', '2017-01-01'::DATE)",
+            },
+            write={
+                "postgres": "SELECT TO_CHAR(CAST(CAST('2017-01-01' AS DATE) AS TIMESTAMP), 'YYYY')",
+                "spark": "SELECT DATE_FORMAT(CAST(CAST('2017-01-01' AS DATE) AS TIMESTAMP), 'yyyy')",
+                "tsql": "SELECT FORMAT(CAST(CAST('2017-01-01' AS DATE) AS DATETIME2), 'yyyy')",
+            },
+        )
+        self.validate_all(
+            "SELECT DATEPART(month, CAST('2017-03-01' AS DATE))",
+            read={
+                "postgres": "SELECT DATE_PART('month', '2017-03-01'::DATE)",
+            },
+            write={
+                "postgres": "SELECT TO_CHAR(CAST(CAST('2017-03-01' AS DATE) AS TIMESTAMP), 'MM')",
+                "spark": "SELECT DATE_FORMAT(CAST(CAST('2017-03-01' AS DATE) AS TIMESTAMP), 'MM')",
+                "tsql": "SELECT FORMAT(CAST(CAST('2017-03-01' AS DATE) AS DATETIME2), 'MM')",
+            },
+        )
+        self.validate_all(
+            "SELECT DATEPART(day, CAST('2017-01-02' AS DATE))",
+            read={
+                "postgres": "SELECT DATE_PART('day', '2017-01-02'::DATE)",
+            },
+            write={
+                "postgres": "SELECT TO_CHAR(CAST(CAST('2017-01-02' AS DATE) AS TIMESTAMP), 'DD')",
+                "spark": "SELECT DATE_FORMAT(CAST(CAST('2017-01-02' AS DATE) AS TIMESTAMP), 'dd')",
+                "tsql": "SELECT FORMAT(CAST(CAST('2017-01-02' AS DATE) AS DATETIME2), 'dd')",
+            },
         )
-        self.validate_identity("DATEPART(YEAR, x)", "FORMAT(CAST(x AS DATETIME2), 'yyyy')")
 
     def test_convert_date_format(self):
         self.validate_all(