Skip to content

Commit

Permalink
DATEPART compartibility improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
MarkMpn committed Oct 4, 2024
1 parent 3ab7b8d commit 3ec0019
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 17 deletions.
10 changes: 5 additions & 5 deletions MarkMpn.Sql4Cds.Engine.Tests/ExpressionFunctionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public void DatePart_Week()
{
// https://learn.microsoft.com/en-us/sql/t-sql/functions/datepart-transact-sql?view=sql-server-ver16#week-and-weekday-datepart-arguments
// Assuming default SET DATEFIRST 7 -- ( Sunday )
var actual = ExpressionFunctions.DatePart("week", (SqlDateTime)new DateTime(2007, 4, 21));
var actual = ExpressionFunctions.DatePart("week", (SqlDateTime)new DateTime(2007, 4, 21), DataTypeHelpers.DateTime);
Assert.AreEqual(16, actual);
}

Expand All @@ -26,15 +26,15 @@ public void DatePart_WeekDay()
{
// https://learn.microsoft.com/en-us/sql/t-sql/functions/datepart-transact-sql?view=sql-server-ver16#week-and-weekday-datepart-arguments
// Assuming default SET DATEFIRST 7 -- ( Sunday )
var actual = ExpressionFunctions.DatePart("weekday", (SqlDateTime)new DateTime(2007, 4, 21));
var actual = ExpressionFunctions.DatePart("weekday", (SqlDateTime)new DateTime(2007, 4, 21), DataTypeHelpers.DateTime);
Assert.AreEqual(7, actual);
}

[TestMethod]
public void DatePart_TZOffset()
{
// https://learn.microsoft.com/en-us/sql/t-sql/functions/datepart-transact-sql?view=sql-server-ver16#tzoffset
var actual = ExpressionFunctions.DatePart("tzoffset", (SqlDateTimeOffset)new DateTimeOffset(2007, 5, 10, 0, 0, 1, TimeSpan.FromMinutes(5 * 60 + 10)));
var actual = ExpressionFunctions.DatePart("tzoffset", (SqlDateTimeOffset)new DateTimeOffset(2007, 5, 10, 0, 0, 1, TimeSpan.FromMinutes(5 * 60 + 10)), DataTypeHelpers.DateTimeOffset);
Assert.AreEqual(310, actual);
}

Expand All @@ -44,7 +44,7 @@ public void DatePart_ErrorOnInvalidPartsForTimeValue()
// https://learn.microsoft.com/en-us/sql/t-sql/functions/datepart-transact-sql?view=sql-server-ver16#default-returned-for-a-datepart-that-isnt-in-a-date-argument
try
{
ExpressionFunctions.DatePart("year", new SqlTime(new TimeSpan(0, 12, 10, 30, 123)));
ExpressionFunctions.DatePart("year", new SqlTime(new TimeSpan(0, 12, 10, 30, 123)), DataTypeHelpers.Time(7));
Assert.Fail();
}
catch (QueryExecutionException ex)
Expand All @@ -60,7 +60,7 @@ public void DatePart_ErrorOnInvalidPartsForTimeValue()
public void DatePart_FractionalSeconds(string part, int expected)
{
// https://learn.microsoft.com/en-us/sql/t-sql/functions/datepart-transact-sql?view=sql-server-ver16#fractional-seconds
var actual = ExpressionFunctions.DatePart(part, (SqlString)"00:00:01.1234567");
var actual = ExpressionFunctions.DatePart(part, (SqlString)"00:00:01.1234567", DataTypeHelpers.VarChar(100, Collation.USEnglish, CollationLabel.CoercibleDefault));
Assert.AreEqual(expected, actual);
}
}
Expand Down
5 changes: 5 additions & 0 deletions MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,11 @@ internal static Sql4CdsError InvalidProcedureParameterType(TSqlFragment fragment
return Create(214, fragment, parameter, type);
}

internal static Sql4CdsError InvalidDatePart(TSqlFragment fragment, string part, string function, DataTypeReference dataType)
{
return Create(9810, fragment, (SqlInt32)part.Length, Collation.USEnglish.ToSqlString(part), (SqlInt32)function.Length, Collation.USEnglish.ToSqlString(function), Collation.USEnglish.ToSqlString(GetTypeName(dataType)));
}

private static string GetTypeName(DataTypeReference type)
{
if (type is SqlDataTypeReference sqlType)
Expand Down
14 changes: 7 additions & 7 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/ExpressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1077,7 +1077,7 @@ private static MethodInfo GetMethod(ExpressionCompilationContext context, Type t

var method = correctParameterCount[0].Method;
var parameters = correctParameterCount[0].Parameters;
DataTypeReference sourceType = null;
var parameterTypes = new Dictionary<string, DataTypeReference>();
cacheKey = method.Name;

if (correctParameterCount[0].Method.IsGenericMethodDefinition)
Expand All @@ -1091,10 +1091,7 @@ private static MethodInfo GetMethod(ExpressionCompilationContext context, Type t
for (var i = 0; i < genericArguments.Length; i++)
{
if (param.ParameterType == genericArguments[i] && genericArgumentValues[i] == null)
{
genericArgumentValues[i] = paramTypes[i].ToNetType(out _);
sourceType = paramTypes[i];
}
}
}

Expand Down Expand Up @@ -1189,14 +1186,15 @@ private static MethodInfo GetMethod(ExpressionCompilationContext context, Type t

if (paramType == typeof(DataTypeReference))
{
if (parameters[i].GetCustomAttribute<SourceTypeAttribute>() != null)
var sourceType = parameters[i].GetCustomAttribute<SourceTypeAttribute>();
if (sourceType != null)
{
cacheKey += $"(TYPE:{sourceType.ToSql()})";
cacheKey += $"(TYPE:{parameterTypes[sourceType.SourceParameter].ToSql()})";
if (createExpression)
{
var paramsWithType = new Expression[paramExpressions.Length + 1];
paramExpressions.CopyTo(paramsWithType, 0);
paramsWithType[i] = Expression.Constant(sourceType);
paramsWithType[i] = Expression.Constant(parameterTypes[sourceType.SourceParameter]);
paramExpressions = paramsWithType;
}
hiddenParams++;
Expand Down Expand Up @@ -1238,6 +1236,8 @@ private static MethodInfo GetMethod(ExpressionCompilationContext context, Type t

if (paramType != typeof(INullable) && !SqlTypeConverter.CanChangeTypeImplicit(paramTypes[i - hiddenParams], paramType.ToSqlType(primaryDataSource)))
throw new NotSupportedQueryFragmentException(Sql4CdsError.TypeClash(i < paramOffset ? func : func.Parameters[i - paramOffset], paramTypes[i], paramType.ToSqlType(primaryDataSource)));

parameterTypes[parameters[i].Name] = paramTypes[i - hiddenParams];
}

for (var i = parameters.Length; i < paramTypes.Length; i++)
Expand Down
48 changes: 43 additions & 5 deletions MarkMpn.Sql4Cds.Engine/ExpressionFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -397,36 +397,59 @@ public static SqlDateTime DateTrunc(SqlString datepart, SqlDateTime date)
/// <param name="date">The date to extract the <paramref name="datepart"/> from</param>
/// <returns>The <paramref name="datepart"/> of the <paramref name="date"/></returns>
[SqlFunction(IsDeterministic = true)]
public static SqlInt32 DatePart(SqlString datepart, SqlDateTimeOffset date)
public static SqlInt32 DatePart(SqlString datepart, SqlDateTimeOffset date, [SourceType(nameof(date))] DataTypeReference dateType)
{
if (date.IsNull)
return SqlInt32.Null;

if (!TryParseDatePart(datepart.Value, out var interval))
throw new QueryExecutionException(Sql4CdsError.InvalidOptionValue(new StringLiteral { Value = datepart.Value }, "datepart"));

var sqlDateType = dateType as SqlDataTypeReference;

switch (interval)
{
case Engine.DatePart.Year:
if (sqlDateType?.SqlDataTypeOption == SqlDataTypeOption.Time)
throw new QueryExecutionException(Sql4CdsError.InvalidDatePart(null, datepart.Value, "datepart", dateType));

return date.Value.Year;

case Engine.DatePart.Quarter:
if (sqlDateType?.SqlDataTypeOption == SqlDataTypeOption.Time)
throw new QueryExecutionException(Sql4CdsError.InvalidDatePart(null, datepart.Value, "datepart", dateType));

return (date.Value.Month - 1) / 3 + 1;

case Engine.DatePart.Month:
if (sqlDateType?.SqlDataTypeOption == SqlDataTypeOption.Time)
throw new QueryExecutionException(Sql4CdsError.InvalidDatePart(null, datepart.Value, "datepart", dateType));

return date.Value.Month;

case Engine.DatePart.DayOfYear:
if (sqlDateType?.SqlDataTypeOption == SqlDataTypeOption.Time)
throw new QueryExecutionException(Sql4CdsError.InvalidDatePart(null, datepart.Value, "datepart", dateType));

return date.Value.DayOfYear;

case Engine.DatePart.Day:
if (sqlDateType?.SqlDataTypeOption == SqlDataTypeOption.Time)
throw new QueryExecutionException(Sql4CdsError.InvalidDatePart(null, datepart.Value, "datepart", dateType));

return date.Value.Day;

case Engine.DatePart.Week:
if (sqlDateType?.SqlDataTypeOption == SqlDataTypeOption.Time)
throw new QueryExecutionException(Sql4CdsError.InvalidDatePart(null, datepart.Value, "datepart", dateType));

return CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(date.Value.DateTime, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Sunday);

case Engine.DatePart.WeekDay:
return (int)date.Value.DayOfWeek;
if (sqlDateType?.SqlDataTypeOption == SqlDataTypeOption.Time)
throw new QueryExecutionException(Sql4CdsError.InvalidDatePart(null, datepart.Value, "datepart", dateType));

return (int)date.Value.DayOfWeek + 1;

case Engine.DatePart.Hour:
return date.Value.Hour;
Expand All @@ -447,9 +470,15 @@ public static SqlInt32 DatePart(SqlString datepart, SqlDateTimeOffset date)
return (int)(date.Value.Ticks % 10_000_000) * 100;

case Engine.DatePart.TZOffset:
if (sqlDateType?.SqlDataTypeOption != SqlDataTypeOption.DateTimeOffset && sqlDateType?.SqlDataTypeOption != SqlDataTypeOption.DateTime2)
throw new QueryExecutionException(Sql4CdsError.InvalidDatePart(null, datepart.Value, "datepart", dateType));

return (int)date.Value.Offset.TotalMinutes;

case Engine.DatePart.ISOWeek:
if (sqlDateType?.SqlDataTypeOption == SqlDataTypeOption.Time)
throw new QueryExecutionException(Sql4CdsError.InvalidDatePart(null, datepart.Value, "datepart", dateType));

return CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(date.Value.DateTime, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);

default:
Expand Down Expand Up @@ -725,7 +754,7 @@ public static SqlInt32 Len(SqlString s)
/// <param name="value">Any expression</param>
/// <returns></returns>
[SqlFunction(IsDeterministic = true)]
public static SqlInt32 DataLength<T>(T value, [SourceType] DataTypeReference type)
public static SqlInt32 DataLength<T>(T value, [SourceType(nameof(value))] DataTypeReference type)
where T:INullable
{
if (value.IsNull)
Expand Down Expand Up @@ -982,7 +1011,7 @@ public static T IsNull<T>(T check, T replacement)
/// <param name="culture">Optional argument specifying a culture</param>
/// <returns></returns>
[SqlFunction(IsDeterministic = false)]
public static SqlString Format<T>(T value, SqlString format, [Optional] SqlString culture, [SourceType] DataTypeReference type, ExpressionExecutionContext context)
public static SqlString Format<T>(T value, SqlString format, [Optional] SqlString culture, [SourceType(nameof(value))] DataTypeReference type, ExpressionExecutionContext context)
where T : INullable
{
if (value.IsNull)
Expand Down Expand Up @@ -1912,11 +1941,20 @@ class TargetTypeAttribute : Attribute
}

/// <summary>
/// Indicates that the parameter gives the type of the preceding parameter
/// Indicates that the parameter gives the orignal SQL type of another parameter
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
class SourceTypeAttribute : Attribute
{
public SourceTypeAttribute(string sourceParameter)
{
SourceParameter = sourceParameter;
}

/// <summary>
/// Returns the name of the parameter this provides the original type of
/// </summary>
public string SourceParameter { get; }
}

/// <summary>
Expand Down

0 comments on commit 3ec0019

Please sign in to comment.