Skip to content

Commit ab10851

Browse files
authored
fix(tsql)!: Preserve roundtrips of DATETIME/DATETIME2 (#4491)
* fix(tsql): Preserve roundtrips of DATETIME/DATETIME2 * Add Type.SMALLDATETIME
1 parent 822aea0 commit ab10851

File tree

8 files changed

+36
-26
lines changed

8 files changed

+36
-26
lines changed

sqlglot/dialects/clickhouse.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,8 @@ class Generator(generator.Generator):
884884
exp.DataType.Type.BIGINT: "Int64",
885885
exp.DataType.Type.DATE32: "Date32",
886886
exp.DataType.Type.DATETIME: "DateTime",
887+
exp.DataType.Type.DATETIME2: "DateTime",
888+
exp.DataType.Type.SMALLDATETIME: "DateTime",
887889
exp.DataType.Type.DATETIME64: "DateTime64",
888890
exp.DataType.Type.DECIMAL: "Decimal",
889891
exp.DataType.Type.DECIMAL32: "Decimal32",

sqlglot/dialects/mysql.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,8 @@ class Generator(generator.Generator):
788788
}
789789

790790
TIMESTAMP_TYPE_MAPPING = {
791+
exp.DataType.Type.DATETIME2: "DATETIME",
792+
exp.DataType.Type.SMALLDATETIME: "DATETIME",
791793
exp.DataType.Type.TIMESTAMP: "DATETIME",
792794
exp.DataType.Type.TIMESTAMPTZ: "TIMESTAMP",
793795
exp.DataType.Type.TIMESTAMPLTZ: "TIMESTAMP",

sqlglot/dialects/tsql.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def _builder(args: t.List) -> E:
111111
assert len(args) == 2
112112

113113
return exp_class(
114-
this=exp.cast(args[1], exp.DataType.Type.DATETIME),
114+
this=exp.cast(args[1], exp.DataType.Type.DATETIME2),
115115
format=exp.Literal.string(
116116
format_time(
117117
args[0].name.lower(),
@@ -492,7 +492,7 @@ class Tokenizer(tokens.Tokenizer):
492492
KEYWORDS = {
493493
**tokens.Tokenizer.KEYWORDS,
494494
"CLUSTERED INDEX": TokenType.INDEX,
495-
"DATETIME2": TokenType.DATETIME,
495+
"DATETIME2": TokenType.DATETIME2,
496496
"DATETIMEOFFSET": TokenType.TIMESTAMPTZ,
497497
"DECLARE": TokenType.DECLARE,
498498
"EXEC": TokenType.COMMAND,
@@ -507,7 +507,7 @@ class Tokenizer(tokens.Tokenizer):
507507
"PROC": TokenType.PROCEDURE,
508508
"REAL": TokenType.FLOAT,
509509
"ROWVERSION": TokenType.ROWVERSION,
510-
"SMALLDATETIME": TokenType.DATETIME,
510+
"SMALLDATETIME": TokenType.SMALLDATETIME,
511511
"SMALLMONEY": TokenType.SMALLMONEY,
512512
"SQL_VARIANT": TokenType.VARIANT,
513513
"SYSTEM_USER": TokenType.CURRENT_USER,
@@ -873,14 +873,15 @@ class Generator(generator.Generator):
873873
TYPE_MAPPING = {
874874
**generator.Generator.TYPE_MAPPING,
875875
exp.DataType.Type.BOOLEAN: "BIT",
876+
exp.DataType.Type.DATETIME2: "DATETIME2",
876877
exp.DataType.Type.DECIMAL: "NUMERIC",
877-
exp.DataType.Type.DATETIME: "DATETIME2",
878878
exp.DataType.Type.DOUBLE: "FLOAT",
879879
exp.DataType.Type.INT: "INTEGER",
880880
exp.DataType.Type.ROWVERSION: "ROWVERSION",
881881
exp.DataType.Type.TEXT: "VARCHAR(MAX)",
882882
exp.DataType.Type.TIMESTAMP: "DATETIME2",
883883
exp.DataType.Type.TIMESTAMPTZ: "DATETIMEOFFSET",
884+
exp.DataType.Type.SMALLDATETIME: "SMALLDATETIME",
884885
exp.DataType.Type.UTINYINT: "TINYINT",
885886
exp.DataType.Type.VARIANT: "SQL_VARIANT",
886887
}

sqlglot/expressions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4347,6 +4347,7 @@ class Type(AutoName):
43474347
DATEMULTIRANGE = auto()
43484348
DATERANGE = auto()
43494349
DATETIME = auto()
4350+
DATETIME2 = auto()
43504351
DATETIME64 = auto()
43514352
DECIMAL = auto()
43524353
DECIMAL32 = auto()
@@ -4406,6 +4407,7 @@ class Type(AutoName):
44064407
ROWVERSION = auto()
44074408
SERIAL = auto()
44084409
SET = auto()
4410+
SMALLDATETIME = auto()
44094411
SMALLINT = auto()
44104412
SMALLMONEY = auto()
44114413
SMALLSERIAL = auto()
@@ -4529,7 +4531,9 @@ class Type(AutoName):
45294531
Type.DATE,
45304532
Type.DATE32,
45314533
Type.DATETIME,
4534+
Type.DATETIME2,
45324535
Type.DATETIME64,
4536+
Type.SMALLDATETIME,
45334537
Type.TIME,
45344538
Type.TIMESTAMP,
45354539
Type.TIMESTAMPNTZ,

sqlglot/generator.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,7 @@ class Generator(metaclass=_Generator):
453453
ARRAY_SIZE_DIM_REQUIRED: t.Optional[bool] = None
454454

455455
TYPE_MAPPING = {
456+
exp.DataType.Type.DATETIME2: "TIMESTAMP",
456457
exp.DataType.Type.NCHAR: "CHAR",
457458
exp.DataType.Type.NVARCHAR: "VARCHAR",
458459
exp.DataType.Type.MEDIUMTEXT: "TEXT",
@@ -463,6 +464,7 @@ class Generator(metaclass=_Generator):
463464
exp.DataType.Type.TINYBLOB: "BLOB",
464465
exp.DataType.Type.INET: "INET",
465466
exp.DataType.Type.ROWVERSION: "VARBINARY",
467+
exp.DataType.Type.SMALLDATETIME: "TIMESTAMP",
466468
}
467469

468470
TIME_PART_SINGULARS = {
@@ -4057,7 +4059,7 @@ def convert_sql(self, expression: exp.Convert) -> str:
40574059

40584060
if to.this == exp.DataType.Type.DATE:
40594061
transformed = exp.StrToDate(this=value, format=fmt)
4060-
elif to.this == exp.DataType.Type.DATETIME:
4062+
elif to.this in (exp.DataType.Type.DATETIME, exp.DataType.Type.DATETIME2):
40614063
transformed = exp.StrToTime(this=value, format=fmt)
40624064
elif to.this in self.PARAMETERIZABLE_TEXT_TYPES:
40634065
transformed = cast(this=exp.TimeToStr(this=value, format=fmt), to=to, safe=safe)

sqlglot/parser.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,9 @@ class Parser(metaclass=_Parser):
332332
TokenType.TIMESTAMPLTZ,
333333
TokenType.TIMESTAMPNTZ,
334334
TokenType.DATETIME,
335+
TokenType.DATETIME2,
335336
TokenType.DATETIME64,
337+
TokenType.SMALLDATETIME,
336338
TokenType.DATE,
337339
TokenType.DATE32,
338340
TokenType.INT4RANGE,

sqlglot/tokens.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,9 @@ class TokenType(AutoName):
157157
TIMESTAMP_MS = auto()
158158
TIMESTAMP_NS = auto()
159159
DATETIME = auto()
160+
DATETIME2 = auto()
160161
DATETIME64 = auto()
162+
SMALLDATETIME = auto()
161163
DATE = auto()
162164
DATE32 = auto()
163165
INT4RANGE = auto()

tests/dialects/test_tsql.py

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -515,16 +515,6 @@ def test_types(self):
515515
self.validate_identity("CAST(x AS SQL_VARIANT)")
516516
self.validate_identity("CAST(x AS BIT)")
517517

518-
self.validate_all(
519-
"CAST(x AS DATETIME2)",
520-
read={
521-
"": "CAST(x AS DATETIME)",
522-
},
523-
write={
524-
"mysql": "CAST(x AS DATETIME)",
525-
"tsql": "CAST(x AS DATETIME2)",
526-
},
527-
)
528518
self.validate_all(
529519
"CAST(x AS DATETIME2(6))",
530520
write={
@@ -542,6 +532,19 @@ def test_types(self):
542532
},
543533
)
544534

535+
for temporal_type in ("SMALLDATETIME", "DATETIME", "DATETIME2"):
536+
self.validate_all(
537+
f"CAST(x AS {temporal_type})",
538+
read={
539+
"": f"CAST(x AS {temporal_type})",
540+
},
541+
write={
542+
"mysql": "CAST(x AS DATETIME)",
543+
"duckdb": "CAST(x AS TIMESTAMP)",
544+
"tsql": f"CAST(x AS {temporal_type})",
545+
},
546+
)
547+
545548
def test_types_ints(self):
546549
self.validate_all(
547550
"CAST(X AS INT)",
@@ -750,14 +753,6 @@ def test_types_date(self):
750753
},
751754
)
752755

753-
self.validate_all(
754-
"CAST(x as SMALLDATETIME)",
755-
write={
756-
"spark": "CAST(x AS TIMESTAMP)",
757-
"tsql": "CAST(x AS DATETIME2)",
758-
},
759-
)
760-
761756
def test_types_bin(self):
762757
self.validate_all(
763758
"CAST(x as BIT)",
@@ -1094,7 +1089,7 @@ def test_fullproc(self):
10941089

10951090
expected_sqls = [
10961091
"CREATE PROCEDURE [TRANSF].[SP_Merge_Sales_Real] @Loadid INTEGER, @NumberOfRows INTEGER WITH EXECUTE AS OWNER, SCHEMABINDING, NATIVE_COMPILATION AS BEGIN SET XACT_ABORT ON",
1097-
"DECLARE @DWH_DateCreated AS DATETIME2 = CONVERT(DATETIME2, GETDATE(), 104)",
1092+
"DECLARE @DWH_DateCreated AS DATETIME = CONVERT(DATETIME, GETDATE(), 104)",
10981093
"DECLARE @DWH_DateModified AS DATETIME2 = CONVERT(DATETIME2, GETDATE(), 104)",
10991094
"DECLARE @DWH_IdUserCreated AS INTEGER = SUSER_ID(CURRENT_USER())",
11001095
"DECLARE @DWH_IdUserModified AS INTEGER = SUSER_ID(CURRENT_USER())",
@@ -1438,7 +1433,7 @@ def test_convert(self):
14381433
"CONVERT(DATETIME, x, 121)",
14391434
write={
14401435
"spark": "TO_TIMESTAMP(x, 'yyyy-MM-dd HH:mm:ss.SSSSSS')",
1441-
"tsql": "CONVERT(DATETIME2, x, 121)",
1436+
"tsql": "CONVERT(DATETIME, x, 121)",
14421437
},
14431438
)
14441439
self.validate_all(
@@ -1899,7 +1894,7 @@ def test_openjson(self):
18991894
*
19001895
FROM OPENJSON(@json) WITH (
19011896
Number VARCHAR(200) '$.Order.Number',
1902-
Date DATETIME2 '$.Order.Date',
1897+
Date DATETIME '$.Order.Date',
19031898
Customer VARCHAR(200) '$.AccountNumber',
19041899
Quantity INTEGER '$.Item.Quantity',
19051900
[Order] NVARCHAR(MAX) AS JSON

0 commit comments

Comments
 (0)