Skip to content
Merged
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
3 changes: 3 additions & 0 deletions src/EFCore.Relational/Query/Internal/ContainsTranslator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public ContainsTranslator(ISqlExpressionFactory sqlExpressionFactory)
IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{
// Note that almost all forms of Contains are queryable (e.g. over inline/parameter collections), and translated in
// RelationalQueryableMethodTranslatingExpressionVisitor.TranslateContains.
// This enumerable Contains translation is still needed for entity Contains (#30712)
SqlExpression? itemExpression = null, valuesExpression = null;

// Identify static Enumerable.Contains and instance List.Contains
Expand Down
11 changes: 10 additions & 1 deletion src/EFCore.Relational/Query/RelationalQueryRootProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,20 @@ public RelationalQueryRootProcessor(

/// <summary>
/// Indicates that a <see cref="ConstantExpression" /> can be converted to a <see cref="InlineQueryRootExpression" />;
/// this will later be translated to a SQL <see cref="ValuesExpression" />.
/// the latter will end up in <see cref="RelationalQueryableMethodTranslatingExpressionVisitor.VisitInlineQueryRoot" /> for
/// translation to a SQL <see cref="ValuesExpression" />.
/// </summary>
protected override bool ShouldConvertToInlineQueryRoot(NewArrayExpression newArrayExpression)
=> true;

/// <summary>
/// Indicates that a <see cref="ParameterExpression" /> can be converted to a <see cref="ParameterQueryRootExpression" />;
/// the latter will end up in <see cref="RelationalQueryableMethodTranslatingExpressionVisitor.TranslateCollection" /> for
/// translation to a provider-specific SQL expansion mechanism, e.g. <c>OPENJSON</c> on SQL Server.
/// </summary>
protected override bool ShouldConvertToParameterQueryRoot(ParameterExpression constantExpression)
=> true;

/// <inheritdoc />
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ protected RelationalQueryableMethodTranslatingExpressionVisitor(
/// <inheritdoc />
public override Expression Translate(Expression expression)
{
var visited = Visit(expression);
var visited = base.Translate(expression);

if (!_subquery)
{
Expand Down Expand Up @@ -256,25 +256,44 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp

var translated = base.VisitMethodCall(methodCallExpression);

// Attempt to translate access into a primitive collection property
if (translated == QueryCompilationContext.NotTranslatedExpression
&& _sqlTranslator.TryTranslatePropertyAccess(methodCallExpression, out var propertyAccessExpression)
&& propertyAccessExpression is
{
TypeMapping.ElementTypeMapping: RelationalTypeMapping elementTypeMapping
} collectionPropertyAccessExpression)
if (translated == QueryCompilationContext.NotTranslatedExpression)
{
var tableAlias = collectionPropertyAccessExpression switch
// Attempt to translate access into a primitive collection property (i.e. array column)
if (_sqlTranslator.TryTranslatePropertyAccess(methodCallExpression, out var propertyAccessExpression)
&& propertyAccessExpression is
{
TypeMapping.ElementTypeMapping: RelationalTypeMapping elementTypeMapping
} collectionPropertyAccessExpression)
{
ColumnExpression c => c.Name[..1].ToLowerInvariant(),
JsonScalarExpression { Path: [.., { PropertyName: string propertyName }] } => propertyName[..1].ToLowerInvariant(),
_ => "j"
};
var tableAlias = collectionPropertyAccessExpression switch
{
ColumnExpression c => c.Name[..1].ToLowerInvariant(),
JsonScalarExpression { Path: [.., { PropertyName: string propertyName }] } => propertyName[..1].ToLowerInvariant(),
_ => "j"
};

if (TranslateCollection(collectionPropertyAccessExpression, elementTypeMapping, tableAlias) is
{ } primitiveCollectionTranslation)
{
return primitiveCollectionTranslation;
if (TranslateCollection(collectionPropertyAccessExpression, elementTypeMapping, tableAlias) is
{ } primitiveCollectionTranslation)
{
return primitiveCollectionTranslation;
}
}

// For Contains over a collection parameter, if the provider hasn't implemented TranslateCollection (e.g. OPENJSON on SQL
// Server), we need to fall back to the previous IN translation.
if (method.IsGenericMethod
&& method.GetGenericMethodDefinition() == QueryableMethods.Contains
&& methodCallExpression.Arguments[0] is ParameterQueryRootExpression parameterSource
&& TranslateExpression(methodCallExpression.Arguments[1]) is SqlExpression item
&& _sqlTranslator.Visit(parameterSource.ParameterExpression) is SqlParameterExpression sqlParameterExpression)
{
var inExpression = _sqlExpressionFactory.In(item, sqlParameterExpression);
var selectExpression = new SelectExpression(inExpression);
var shaperExpression = Expression.Convert(
new ProjectionBindingExpression(selectExpression, new ProjectionMember(), typeof(bool?)), typeof(bool));
var shapedQueryExpression = new ShapedQueryExpression(selectExpression, shaperExpression)
.UpdateResultCardinality(ResultCardinality.Single);
return shapedQueryExpression;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ public static IServiceCollection AddEntityFrameworkSqlServer(this IServiceCollec
.TryAdd<IAggregateMethodCallTranslatorProvider, SqlServerAggregateMethodCallTranslatorProvider>()
.TryAdd<IMemberTranslatorProvider, SqlServerMemberTranslatorProvider>()
.TryAdd<IQuerySqlGeneratorFactory, SqlServerQuerySqlGeneratorFactory>()
.TryAdd<IQueryTranslationPreprocessorFactory, SqlServerQueryTranslationPreprocessorFactory>()
.TryAdd<IRelationalSqlTranslatingExpressionVisitorFactory, SqlServerSqlTranslatingExpressionVisitorFactory>()
.TryAdd<ISqlExpressionFactory, SqlServerSqlExpressionFactory>()
.TryAdd<IQueryTranslationPostprocessorFactory, SqlServerQueryTranslationPostprocessorFactory>()
Expand Down
16 changes: 13 additions & 3 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 31 additions & 28 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.resx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema

Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes

The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.

Example:

... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
Expand All @@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple

There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the

Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not

The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can

Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.

mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
Expand Down Expand Up @@ -126,6 +126,9 @@
<data name="CannotProduceUnterminatedSQLWithComments" xml:space="preserve">
<value>Can't produce unterminated SQL with comments when generating migrations SQL for {operation}.</value>
</data>
<data name="CompatibilityLevelTooLowForScalarCollections" xml:space="preserve">
<value>EF Core's SQL Server compatibility level is set to {compatibilityLevel}; compatibility level 130 (SQL Server 2016) is the minimum for most forms of querying of JSON arrays.</value>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: did we decide to mention compatibility level explicitly in exception messages? Either way, we should maybe unify with JsonValuePathExpressionsNotSupported

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think it's useful to mention the required and current compatibility level in the exceptions we throw... This way users know right away what's going on.

Changed JsonValuePathExpressionsNotSupported to also include the compatibility level, check out the added commit.

</data>
<data name="DuplicateColumnIdentityIncrementMismatch" xml:space="preserve">
<value>'{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different identity increment values.</value>
</data>
Expand Down Expand Up @@ -184,7 +187,7 @@
<value>The specified table '{table}' is not in a valid format. Specify tables using the format '[schema].[table]'.</value>
</data>
<data name="JsonValuePathExpressionsNotSupported" xml:space="preserve">
<value>A non-constant array index or property name was used when navigating inside a JSON document; this is only supported starting with SQL Server 2017.</value>
<value>A non-constant array index or property name was used when navigating inside a JSON document, but EF Core's SQL Server compatibility level is set to {compatibilityLevel}; this is only supported with compatibility level 140 (SQL Server 2017) or higher.</value>
</data>
<data name="LogByteIdentityColumn" xml:space="preserve">
<value>The property '{property}' on entity type '{entityType}' is of type 'byte', but is set up to use a SQL Server identity column; this requires that values starting at 255 and counting down will be used for temporary key values. A temporary key value is needed for every entity inserted in a single call to 'SaveChanges'. Care must be taken that these values do not collide with real key values.</value>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class SqlServerQuerySqlGenerator : QuerySqlGenerator
{
private readonly IRelationalTypeMappingSource _typeMappingSource;
private readonly ISqlGenerationHelper _sqlGenerationHelper;
private readonly bool _supportsJsonValueExpressions;
private readonly int _sqlServerCompatibilityLevel;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -35,10 +35,7 @@ public SqlServerQuerySqlGenerator(
{
_typeMappingSource = typeMappingSource;
_sqlGenerationHelper = dependencies.SqlGenerationHelper;

// JSON functions such as JSON_VALUE only support arbitrary expressions for the path parameter in SQL Server 2017 and above; before
// that, arguments must be constant strings.
_supportsJsonValueExpressions = sqlServerSingletonOptions.CompatibilityLevel >= 140;
_sqlServerCompatibilityLevel = sqlServerSingletonOptions.CompatibilityLevel;
}

/// <summary>
Expand Down Expand Up @@ -433,11 +430,13 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
case { ArrayIndex: SqlExpression arrayIndex }:
Sql.Append("[");

// JSON functions such as JSON_VALUE only support arbitrary expressions for the path parameter in SQL Server 2017 and
// above; before that, arguments must be constant strings.
if (arrayIndex is SqlConstantExpression)
{
Visit(pathSegment.ArrayIndex);
}
else if (_supportsJsonValueExpressions)
else if (_sqlServerCompatibilityLevel >= 140)
{
Sql.Append("' + CAST(");
Visit(arrayIndex);
Expand All @@ -447,7 +446,8 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
}
else
{
throw new InvalidOperationException(SqlServerStrings.JsonValuePathExpressionsNotSupported);
throw new InvalidOperationException(
SqlServerStrings.JsonValuePathExpressionsNotSupported(_sqlServerCompatibilityLevel));
}

Sql.Append("]");
Expand Down
Loading