Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,29 +31,31 @@ public static class RelationalDbFunctionsExtensions
/// <param name="_">The <see cref="DbFunctions" /> instance.</param>
/// <param name="operand">The operand to which to apply the collation.</param>
/// <param name="collation">The name of the collation.</param>
public static TProperty Collate<TProperty>(
this DbFunctions _,
TProperty operand,
[NotParameterized] string collation)
public static TProperty Collate<TProperty>(this DbFunctions _, TProperty operand, [NotParameterized] string collation)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Collate)));

/// <summary>
/// Returns the smallest value from the given list of values. Usually corresponds to the <c>LEAST</c> SQL function.
/// </summary>
/// <param name="_">The <see cref="DbFunctions" /> instance.</param>
/// <param name="values">The list of values from which return the smallest value.</param>
public static T Least<T>(
this DbFunctions _,
[NotParameterized] params T[] values)
public static T Least<T>(this DbFunctions _, [NotParameterized] params T[] values)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Least)));

/// <summary>
/// Returns the greatest value from the given list of values. Usually corresponds to the <c>GREATEST</c> SQL function.
/// </summary>
/// <param name="_">The <see cref="DbFunctions" /> instance.</param>
/// <param name="values">The list of values from which return the greatest value.</param>
public static T Greatest<T>(
this DbFunctions _,
[NotParameterized] params T[] values)
public static T Greatest<T>(this DbFunctions _, [NotParameterized] params T[] values)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Greatest)));

/// <summary>
/// Returns a value indicating whether a given JSON path exists within the specified JSON.
/// </summary>
/// <param name="_">The <see cref="DbFunctions" /> instance.</param>
/// <param name="json">The JSON value to check.</param>
/// <param name="path">The JSON path to look for.</param>
public static bool JsonExists(this DbFunctions _, object json, string path)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonExists)));
}
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,46 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
_typeMappingSource.FindMapping(isUnicode ? "nvarchar(max)" : "varchar(max)"));
}

// We translate EF.Functions.JsonExists here and not in a method translator since we need to support JsonExists over
// complex and owned JSON properties, which requires special handling.
case nameof(RelationalDbFunctionsExtensions.JsonExists)
when declaringType == typeof(RelationalDbFunctionsExtensions)
&& @object is null
&& arguments is [_, var json, var path]:
{
if (Translate(path) is not SqlExpression translatedPath)
{
return QueryCompilationContext.NotTranslatedExpression;
}

#pragma warning disable EF1001 // TranslateProjection() is pubternal
var translatedJson = TranslateProjection(json) switch
{
// The JSON argument is a scalar string property
SqlExpression scalar => scalar,

// The JSON argument is a complex or owned JSON property
RelationalStructuralTypeShaperExpression { ValueBufferExpression: JsonQueryExpression { JsonColumn: var c } } => c,

_ => null
};
#pragma warning restore EF1001

return translatedJson is null
? QueryCompilationContext.NotTranslatedExpression
: _sqlExpressionFactory.Equal(
_sqlExpressionFactory.Function(
"JSON_PATH_EXISTS",
[translatedJson, translatedPath],
nullable: true,
// Note that JSON_PATH_EXISTS() does propagate nullability; however, our query pipeline assumes that if
// arguments propagate nullability, that's the *only* reason for the function to return null; this means that
// if the arguments are non-nullable, the IS NOT NULL wrapping check can be optimized away.
argumentsPropagateNullability: [false, false],
typeof(int)),
_sqlExpressionFactory.Constant(1));
}

default:
return QueryCompilationContext.NotTranslatedExpression;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,21 +214,65 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
}

var method = methodCallExpression.Method;
var declaringType = method.DeclaringType;
var @object = methodCallExpression.Object;
var arguments = methodCallExpression.Arguments;

// https://learn.microsoft.com/dotnet/api/system.string.startswith#system-string-startswith(system-string)
// https://learn.microsoft.com/dotnet/api/system.string.startswith#system-string-startswith(system-char)
// https://learn.microsoft.com/dotnet/api/system.string.endswith#system-string-endswith(system-string)
// https://learn.microsoft.com/dotnet/api/system.string.endswith#system-string-endswith(system-char)
if (method.Name is nameof(string.StartsWith) or nameof(string.EndsWith)
&& methodCallExpression.Object is not null
&& method.DeclaringType == typeof(string)
&& methodCallExpression.Arguments is [Expression value]
&& (value.Type == typeof(string) || value.Type == typeof(char)))
switch (method.Name)
{
return TranslateStartsEndsWith(
methodCallExpression.Object,
value,
method.Name is nameof(string.StartsWith));
// https://learn.microsoft.com/dotnet/api/system.string.startswith#system-string-startswith(system-string)
// https://learn.microsoft.com/dotnet/api/system.string.startswith#system-string-startswith(system-char)
// https://learn.microsoft.com/dotnet/api/system.string.endswith#system-string-endswith(system-string)
// https://learn.microsoft.com/dotnet/api/system.string.endswith#system-string-endswith(system-char)
case nameof(string.StartsWith) or nameof(string.EndsWith)
when methodCallExpression.Object is not null
&& declaringType == typeof(string)
&& arguments is [Expression value]
&& (value.Type == typeof(string) || value.Type == typeof(char)):
{
return TranslateStartsEndsWith(
methodCallExpression.Object,
value,
method.Name is nameof(string.StartsWith));
}

// We translate EF.Functions.JsonExists here and not in a method translator since we need to support JsonExists over
// complex and owned JSON properties, which requires special handling.
case nameof(RelationalDbFunctionsExtensions.JsonExists)
when declaringType == typeof(RelationalDbFunctionsExtensions)
&& @object is null
&& arguments is [_, var json, var path]:
{
if (Translate(path) is not SqlExpression translatedPath)
{
return QueryCompilationContext.NotTranslatedExpression;
}

#pragma warning disable EF1001 // TranslateProjection() is pubternal
var translatedJson = TranslateProjection(json) switch
{
// The JSON argument is a scalar string property
SqlExpression scalar => scalar,

// The JSON argument is a complex or owned JSON property
RelationalStructuralTypeShaperExpression { ValueBufferExpression: JsonQueryExpression { JsonColumn: var c } } => c,
_ => null
};
#pragma warning restore EF1001

return translatedJson is null
? QueryCompilationContext.NotTranslatedExpression
: _sqlExpressionFactory.IsNotNull(
_sqlExpressionFactory.Function(
"json_type",
[translatedJson, translatedPath],
nullable: true,
// Note that json_type() does propagate nullability; however, our query pipeline assumes that if arguments
// propagate nullability, that's the *only* reason for the function to return null; this means that if the
// arguments are non-nullable, the IS NOT NULL wrapping check can be optimized away.
argumentsPropagateNullability: [false, false],
typeof(int)));
}
}

return QueryCompilationContext.NotTranslatedExpression;
Expand Down
Loading
Loading