diff --git a/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs b/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs index 0ea6731..a7e52ae 100644 --- a/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs +++ b/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs @@ -293,13 +293,20 @@ internal static class CodeGenerationSupporter internal const string DataRetention = "DATA_RETENTION"; internal const string DataSource = "DATA_SOURCE"; internal const string Date = "DATE"; + internal const string DateAdd = "DATEADD"; + internal const string DateBucket = "DATE_BUCKET"; internal const string DateCorrelationOptimization = "DATE_CORRELATION_OPTIMIZATION"; + internal const string DateDiff = "DATEDIFF"; + internal const string DateDiffBig = "DATEDIFF_BIG"; internal const string DateFirst = "DATEFIRST"; internal const string DateFormat = "DATEFORMAT"; internal const string DateFormat2 = "DATE_FORMAT"; + internal const string DateName = "DATENAME"; + internal const string DatePart = "DATEPART"; internal const string DateTime = "DATETIME"; internal const string DateTime2 = "DATETIME2"; internal const string DateTimeOffset = "DATETIMEOFFSET"; + internal const string DateTrunc = "DATETRUNC"; internal const string Deterministic = "DETERMINISTIC"; internal const string DboOnly = "DBO_ONLY"; internal const string DbChaining = "DB_CHAINING"; diff --git a/SqlScriptDom/Parser/TSql/TSql100.g b/SqlScriptDom/Parser/TSql/TSql100.g index c0cbb0e..c17c2f9 100644 --- a/SqlScriptDom/Parser/TSql/TSql100.g +++ b/SqlScriptDom/Parser/TSql/TSql100.g @@ -20874,6 +20874,9 @@ regularBuiltInFunctionCall [FunctionCall vParent] : ( expressionList[vParent, vParent.Parameters] + { + NormalizeDatePartFunctionFirstParameter(vParent); + } | vColumn=starColumnReferenceExpression { diff --git a/SqlScriptDom/Parser/TSql/TSql110.g b/SqlScriptDom/Parser/TSql/TSql110.g index 508e6b7..a4974e1 100644 --- a/SqlScriptDom/Parser/TSql/TSql110.g +++ b/SqlScriptDom/Parser/TSql/TSql110.g @@ -23849,6 +23849,9 @@ regularBuiltInFunctionCall [FunctionCall vParent] : ( expressionList[vParent, vParent.Parameters] + { + NormalizeDatePartFunctionFirstParameter(vParent); + } | vColumn=starColumnReferenceExpression { diff --git a/SqlScriptDom/Parser/TSql/TSql120.g b/SqlScriptDom/Parser/TSql/TSql120.g index 78126da..ca52e50 100644 --- a/SqlScriptDom/Parser/TSql/TSql120.g +++ b/SqlScriptDom/Parser/TSql/TSql120.g @@ -24598,6 +24598,9 @@ regularBuiltInFunctionCall [FunctionCall vParent] : ( expressionList[vParent, vParent.Parameters] + { + NormalizeDatePartFunctionFirstParameter(vParent); + } | vColumn=starColumnReferenceExpression { diff --git a/SqlScriptDom/Parser/TSql/TSql130.g b/SqlScriptDom/Parser/TSql/TSql130.g index a51aeb9..1212d2b 100644 --- a/SqlScriptDom/Parser/TSql/TSql130.g +++ b/SqlScriptDom/Parser/TSql/TSql130.g @@ -29420,6 +29420,9 @@ regularBuiltInFunctionCall [FunctionCall vParent] : ( expressionList[vParent, vParent.Parameters] + { + NormalizeDatePartFunctionFirstParameter(vParent); + } | vColumn=starColumnReferenceExpression { diff --git a/SqlScriptDom/Parser/TSql/TSql140.g b/SqlScriptDom/Parser/TSql/TSql140.g index 2424bd5..65ccc86 100644 --- a/SqlScriptDom/Parser/TSql/TSql140.g +++ b/SqlScriptDom/Parser/TSql/TSql140.g @@ -30253,6 +30253,9 @@ regularBuiltInFunctionCall [FunctionCall vParent] : ( expressionList[vParent, vParent.Parameters] + { + NormalizeDatePartFunctionFirstParameter(vParent); + } | vColumn=starColumnReferenceExpression { diff --git a/SqlScriptDom/Parser/TSql/TSql150.g b/SqlScriptDom/Parser/TSql/TSql150.g index ea468dc..2b01332 100644 --- a/SqlScriptDom/Parser/TSql/TSql150.g +++ b/SqlScriptDom/Parser/TSql/TSql150.g @@ -31406,6 +31406,9 @@ regularBuiltInFunctionCall [FunctionCall vParent] : ( expressionList[vParent, vParent.Parameters] + { + NormalizeDatePartFunctionFirstParameter(vParent); + } | vColumn=starColumnReferenceExpression { diff --git a/SqlScriptDom/Parser/TSql/TSql160.g b/SqlScriptDom/Parser/TSql/TSql160.g index 1fbb91e..ed4fe5f 100644 --- a/SqlScriptDom/Parser/TSql/TSql160.g +++ b/SqlScriptDom/Parser/TSql/TSql160.g @@ -32218,6 +32218,9 @@ regularBuiltInFunctionCall [FunctionCall vParent] : ( expressionList[vParent, vParent.Parameters] + { + NormalizeDatePartFunctionFirstParameter(vParent); + } | vColumn=starColumnReferenceExpression { diff --git a/SqlScriptDom/Parser/TSql/TSql170.g b/SqlScriptDom/Parser/TSql/TSql170.g index 0fb0514..28673c8 100644 --- a/SqlScriptDom/Parser/TSql/TSql170.g +++ b/SqlScriptDom/Parser/TSql/TSql170.g @@ -33346,6 +33346,9 @@ regularBuiltInFunctionCall [FunctionCall vParent] : ( expressionList[vParent, vParent.Parameters] + { + NormalizeDatePartFunctionFirstParameter(vParent); + } | vColumn=starColumnReferenceExpression { diff --git a/SqlScriptDom/Parser/TSql/TSql180.g b/SqlScriptDom/Parser/TSql/TSql180.g index 4af433a..59ed7b8 100644 --- a/SqlScriptDom/Parser/TSql/TSql180.g +++ b/SqlScriptDom/Parser/TSql/TSql180.g @@ -33346,6 +33346,9 @@ regularBuiltInFunctionCall [FunctionCall vParent] : ( expressionList[vParent, vParent.Parameters] + { + NormalizeDatePartFunctionFirstParameter(vParent); + } | vColumn=starColumnReferenceExpression { diff --git a/SqlScriptDom/Parser/TSql/TSql80.g b/SqlScriptDom/Parser/TSql/TSql80.g index b97ddc0..87fc761 100644 --- a/SqlScriptDom/Parser/TSql/TSql80.g +++ b/SqlScriptDom/Parser/TSql/TSql80.g @@ -9103,6 +9103,9 @@ identifierBuiltInFunctionCallDefaultParams [FunctionCall vParent] } : expressionList[vParent, vParent.Parameters] + { + NormalizeDatePartFunctionFirstParameter(vParent); + } | vColumn=starColumnReferenceExpression { diff --git a/SqlScriptDom/Parser/TSql/TSql80ParserBaseInternal.cs b/SqlScriptDom/Parser/TSql/TSql80ParserBaseInternal.cs index 4f47429..5607c18 100644 --- a/SqlScriptDom/Parser/TSql/TSql80ParserBaseInternal.cs +++ b/SqlScriptDom/Parser/TSql/TSql80ParserBaseInternal.cs @@ -1979,6 +1979,51 @@ protected void PutIdentifiersIntoFunctionCall(FunctionCall functionCall, MultiPa } } + protected void NormalizeDatePartFunctionFirstParameter(FunctionCall functionCall) + { + if (functionCall == null || + functionCall.FunctionName == null || + String.IsNullOrEmpty(functionCall.FunctionName.Value) || + functionCall.Parameters == null || + functionCall.Parameters.Count == 0 || + !IsDatePartFunction(functionCall.FunctionName.Value)) + { + return; + } + + ColumnReferenceExpression columnReference = functionCall.Parameters[0] as ColumnReferenceExpression; + if (columnReference == null || + columnReference.MultiPartIdentifier == null || + columnReference.MultiPartIdentifier.Count != 1) + { + return; + } + + Identifier identifier = columnReference.MultiPartIdentifier[0]; + IdentifierLiteral literal = FragmentFactory.CreateFragment(); + literal.Value = identifier.Value; + literal.QuoteType = identifier.QuoteType; + literal.UpdateTokenInfo(columnReference); + functionCall.Parameters[0] = literal; + } + + private static bool IsDatePartFunction(string functionName) + { + switch (functionName.ToUpper(CultureInfo.InvariantCulture)) + { + case CodeGenerationSupporter.DateAdd: + case CodeGenerationSupporter.DateBucket: + case CodeGenerationSupporter.DateDiff: + case CodeGenerationSupporter.DateDiffBig: + case CodeGenerationSupporter.DateName: + case CodeGenerationSupporter.DatePart: + case CodeGenerationSupporter.DateTrunc: + return true; + default: + return false; + } + } + protected void VerifyColumnDataType(ColumnDefinition column) { // If the scalarDataType is not parsed, the ColumnIdentifier has to be a timestamp. @@ -2426,4 +2471,4 @@ protected static TSqlParseErrorException GetUnexpectedTokenErrorException(Identi #endregion } -} \ No newline at end of file +} diff --git a/SqlScriptDom/Parser/TSql/TSql90.g b/SqlScriptDom/Parser/TSql/TSql90.g index 77f27a1..9b6d0ad 100644 --- a/SqlScriptDom/Parser/TSql/TSql90.g +++ b/SqlScriptDom/Parser/TSql/TSql90.g @@ -16550,6 +16550,9 @@ regularBuiltInFunctionCall [FunctionCall vParent] : ( expressionList[vParent, vParent.Parameters] + { + NormalizeDatePartFunctionFirstParameter(vParent); + } | vColumn=starColumnReferenceExpression { diff --git a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g index 21590b6..e4a7f24 100644 --- a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g +++ b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g @@ -32384,6 +32384,9 @@ regularBuiltInFunctionCall [FunctionCall vParent] : ( expressionList[vParent, vParent.Parameters] + { + NormalizeDatePartFunctionFirstParameter(vParent); + } | vColumn=starColumnReferenceExpression { diff --git a/Test/SqlDom/Baselines160/DatePartFunctionTests160.sql b/Test/SqlDom/Baselines160/DatePartFunctionTests160.sql new file mode 100644 index 0000000..fe22faf --- /dev/null +++ b/Test/SqlDom/Baselines160/DatePartFunctionTests160.sql @@ -0,0 +1,55 @@ +SELECT DATEADD(day, 1, OrderDate) AS AddedDate, + DATEDIFF(mm, StartDate, EndDate) AS MonthDifference, + DATEDIFF_BIG(second, StartDate, EndDate) AS BigSecondDifference, + DATENAME(month, OrderDate) AS MonthName, + DATEPART(wk, OrderDate) AS WeekPart, + DATE_BUCKET(WEEK, 2, OrderDate) AS WeekBucket, + DATE_BUCKET(DAY, 7, OrderDate, OriginDate) AS OriginDayBucket, + DATETRUNC(year, OrderDate) AS YearTruncation +FROM dbo.Orders; + +SELECT DATEPART(year, OrderDate) AS YearPart, + DATEPART(yy, OrderDate) AS YearShortPart, + DATEPART(yyyy, OrderDate) AS YearLongPart, + DATEPART(quarter, OrderDate) AS QuarterPart, + DATEPART(qq, OrderDate) AS QuarterShortPart, + DATEPART(q, OrderDate) AS QuarterSinglePart, + DATEPART(month, OrderDate) AS MonthPart, + DATEPART(mm, OrderDate) AS MonthShortPart, + DATEPART(m, OrderDate) AS MonthSinglePart, + DATEPART(dayofyear, OrderDate) AS DayOfYearPart, + DATEPART(dy, OrderDate) AS DayOfYearShortPart, + DATEPART(y, OrderDate) AS DayOfYearSinglePart, + DATEPART(day, OrderDate) AS DayPart, + DATEPART(dd, OrderDate) AS DayShortPart, + DATEPART(d, OrderDate) AS DaySinglePart, + DATEPART(week, OrderDate) AS WeekPart, + DATEPART(wk, OrderDate) AS WeekShortPart, + DATEPART(ww, OrderDate) AS WeekAltPart, + DATEPART(weekday, OrderDate) AS WeekdayPart, + DATEPART(dw, OrderDate) AS WeekdayShortPart, + DATEPART(w, OrderDate) AS WeekdaySinglePart, + DATEPART(hour, OrderDate) AS HourPart, + DATEPART(hh, OrderDate) AS HourShortPart, + DATEPART(minute, OrderDate) AS MinutePart, + DATEPART(mi, OrderDate) AS MinuteShortPart, + DATEPART(n, OrderDate) AS MinuteSinglePart, + DATEPART(second, OrderDate) AS SecondPart, + DATEPART(ss, OrderDate) AS SecondShortPart, + DATEPART(s, OrderDate) AS SecondSinglePart, + DATEPART(millisecond, OrderDate) AS MillisecondPart, + DATEPART(ms, OrderDate) AS MillisecondShortPart, + DATEPART(microsecond, OrderDate) AS MicrosecondPart, + DATEPART(mcs, OrderDate) AS MicrosecondShortPart, + DATEPART(nanosecond, OrderDate) AS NanosecondPart, + DATEPART(ns, OrderDate) AS NanosecondShortPart, + DATEPART(tzoffset, OrderDate) AS TimeZoneOffsetPart, + DATEPART(tz, OrderDate) AS TimeZoneOffsetShortPart, + DATEPART(iso_week, OrderDate) AS IsoWeekPart, + DATEPART(isowk, OrderDate) AS IsoWeekShortPart, + DATEPART(isoww, OrderDate) AS IsoWeekAltPart +FROM dbo.Orders; + +SELECT ABS(Amount) AS AbsoluteAmount, + UPPER(CustomerName) AS UpperCustomerName +FROM dbo.Orders; diff --git a/Test/SqlDom/Only160SyntaxTests.cs b/Test/SqlDom/Only160SyntaxTests.cs index ad033d2..966e51d 100644 --- a/Test/SqlDom/Only160SyntaxTests.cs +++ b/Test/SqlDom/Only160SyntaxTests.cs @@ -36,6 +36,7 @@ public partial class SqlDomTests new ParserTest160("BitManipulationFunctionTests160.sql", nErrors80: 0, nErrors90: 0, nErrors100: 0, nErrors110: 0, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0), new ParserTest160("ShiftOperatorTests160.sql", nErrors80: 3, nErrors90: 3, nErrors100: 3, nErrors110: 3, nErrors120: 3, nErrors130: 3, nErrors140: 3, nErrors150: 3), new ParserTest160("BuiltInFunctionTests160.sql", nErrors80: 1, nErrors90: 1, nErrors100: 2, nErrors110: 0, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0), + new ParserTest160("DatePartFunctionTests160.sql", nErrors80: 0, nErrors90: 0, nErrors100: 0, nErrors110: 0, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0), new ParserTest160("TrimFunctionTests160.sql", nErrors80: 7, nErrors90: 7, nErrors100: 7, nErrors110: 7, nErrors120: 7, nErrors130: 7, nErrors140: 4, nErrors150: 4), new ParserTest160("JsonFunctionTests160.sql", nErrors80: 9, nErrors90: 8, nErrors100: 14, nErrors110: 14, nErrors120: 14, nErrors130: 14, nErrors140: 14, nErrors150: 14), new ParserTest160("AlterFunctionJsonObjectTests160.sql", nErrors80: 1, nErrors90: 1, nErrors100: 1, nErrors110: 1, nErrors120: 1, nErrors130: 1, nErrors140: 1, nErrors150: 1), @@ -155,6 +156,54 @@ public void TSql160Azure_SyntaxIn160ParserTest() } } + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void DatePartFunctionParametersAreIdentifierLiteralsTest() + { + TSql160Parser parser = new TSql160Parser(true); + + VerifyDatePartParameter(parser, "SELECT DATEDIFF(mm, ColA, ColB) FROM my_table;", "mm", 3); + VerifyDatePartParameter(parser, "SELECT DATEADD(day, 1, ColA) FROM my_table;", "day", 3); + VerifyDatePartParameter(parser, "SELECT DATEDIFF_BIG(second, ColA, ColB) FROM my_table;", "second", 3); + VerifyDatePartParameter(parser, "SELECT DATENAME(month, ColA) FROM my_table;", "month", 2); + VerifyDatePartParameter(parser, "SELECT DATEPART(wk, ColA) FROM my_table;", "wk", 2); + VerifyDatePartParameter(parser, "SELECT DATE_BUCKET(WEEK, 10, ColA) FROM my_table;", "WEEK", 3); + VerifyDatePartParameter(parser, "SELECT DATE_BUCKET(DAY, 10, ColA, ColB) FROM my_table;", "DAY", 4); + VerifyDatePartParameter(parser, "SELECT DATETRUNC(year, ColA) FROM my_table;", "year", 2); + + VerifyNonDatePartParameter(parser, "SELECT ABS(ColA) FROM my_table;"); + VerifyNonDatePartParameter(parser, "SELECT UPPER(ColA) FROM my_table;"); + } + + private static void VerifyDatePartParameter(TSqlParser parser, string sql, string expectedDatePart, int expectedParameterCount) + { + FunctionCall functionCall = GetFirstSelectFunctionCall(parser, sql); + + Assert.AreEqual(expectedParameterCount, functionCall.Parameters.Count, sql); + Assert.IsInstanceOfType(functionCall.Parameters[0], typeof(IdentifierLiteral), sql); + Assert.AreEqual(expectedDatePart, ((IdentifierLiteral)functionCall.Parameters[0]).Value, sql); + } + + private static void VerifyNonDatePartParameter(TSqlParser parser, string sql) + { + FunctionCall functionCall = GetFirstSelectFunctionCall(parser, sql); + + Assert.IsInstanceOfType(functionCall.Parameters[0], typeof(ColumnReferenceExpression), sql); + } + + private static FunctionCall GetFirstSelectFunctionCall(TSqlParser parser, string sql) + { + TSqlFragment fragment = parser.Parse(new System.IO.StringReader(sql), out System.Collections.Generic.IList errors); + + Assert.AreEqual(0, errors.Count, sql); + TSqlScript script = (TSqlScript)fragment; + SelectStatement select = (SelectStatement)script.Batches[0].Statements[0]; + QuerySpecification query = (QuerySpecification)select.QueryExpression; + SelectScalarExpression selectExpression = (SelectScalarExpression)query.SelectElements[0]; + return (FunctionCall)selectExpression.Expression; + } + [TestMethod] [Priority(0)] [SqlStudioTestCategory(Category.UnitTest)] diff --git a/Test/SqlDom/TestScripts/DatePartFunctionTests160.sql b/Test/SqlDom/TestScripts/DatePartFunctionTests160.sql new file mode 100644 index 0000000..36a0027 --- /dev/null +++ b/Test/SqlDom/TestScripts/DatePartFunctionTests160.sql @@ -0,0 +1,58 @@ +-- Datepart functions with each supported arity. +SELECT DATEADD(day, 1, OrderDate) AS AddedDate, + DATEDIFF(mm, StartDate, EndDate) AS MonthDifference, + DATEDIFF_BIG(second, StartDate, EndDate) AS BigSecondDifference, + DATENAME(month, OrderDate) AS MonthName, + DATEPART(wk, OrderDate) AS WeekPart, + DATE_BUCKET(WEEK, 2, OrderDate) AS WeekBucket, + DATE_BUCKET(DAY, 7, OrderDate, OriginDate) AS OriginDayBucket, + DATETRUNC(year, OrderDate) AS YearTruncation +FROM dbo.Orders; + +-- Documented DATEPART datepart names and abbreviations should not round-trip as column references. +SELECT DATEPART(year, OrderDate) AS YearPart, + DATEPART(yy, OrderDate) AS YearShortPart, + DATEPART(yyyy, OrderDate) AS YearLongPart, + DATEPART(quarter, OrderDate) AS QuarterPart, + DATEPART(qq, OrderDate) AS QuarterShortPart, + DATEPART(q, OrderDate) AS QuarterSinglePart, + DATEPART(month, OrderDate) AS MonthPart, + DATEPART(mm, OrderDate) AS MonthShortPart, + DATEPART(m, OrderDate) AS MonthSinglePart, + DATEPART(dayofyear, OrderDate) AS DayOfYearPart, + DATEPART(dy, OrderDate) AS DayOfYearShortPart, + DATEPART(y, OrderDate) AS DayOfYearSinglePart, + DATEPART(day, OrderDate) AS DayPart, + DATEPART(dd, OrderDate) AS DayShortPart, + DATEPART(d, OrderDate) AS DaySinglePart, + DATEPART(week, OrderDate) AS WeekPart, + DATEPART(wk, OrderDate) AS WeekShortPart, + DATEPART(ww, OrderDate) AS WeekAltPart, + DATEPART(weekday, OrderDate) AS WeekdayPart, + DATEPART(dw, OrderDate) AS WeekdayShortPart, + DATEPART(w, OrderDate) AS WeekdaySinglePart, + DATEPART(hour, OrderDate) AS HourPart, + DATEPART(hh, OrderDate) AS HourShortPart, + DATEPART(minute, OrderDate) AS MinutePart, + DATEPART(mi, OrderDate) AS MinuteShortPart, + DATEPART(n, OrderDate) AS MinuteSinglePart, + DATEPART(second, OrderDate) AS SecondPart, + DATEPART(ss, OrderDate) AS SecondShortPart, + DATEPART(s, OrderDate) AS SecondSinglePart, + DATEPART(millisecond, OrderDate) AS MillisecondPart, + DATEPART(ms, OrderDate) AS MillisecondShortPart, + DATEPART(microsecond, OrderDate) AS MicrosecondPart, + DATEPART(mcs, OrderDate) AS MicrosecondShortPart, + DATEPART(nanosecond, OrderDate) AS NanosecondPart, + DATEPART(ns, OrderDate) AS NanosecondShortPart, + DATEPART(tzoffset, OrderDate) AS TimeZoneOffsetPart, + DATEPART(tz, OrderDate) AS TimeZoneOffsetShortPart, + DATEPART(iso_week, OrderDate) AS IsoWeekPart, + DATEPART(isowk, OrderDate) AS IsoWeekShortPart, + DATEPART(isoww, OrderDate) AS IsoWeekAltPart +FROM dbo.Orders; + +-- Non-datepart built-ins still parse and generate through the regular function path. +SELECT ABS(Amount) AS AbsoluteAmount, + UPPER(CustomerName) AS UpperCustomerName +FROM dbo.Orders;