From 6c9cb94ea481938126df3c4b0e4547e0841bb775 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 17 Mar 2023 15:27:51 +0100 Subject: [PATCH] Support primitive collections Closes #29427 Closes #30426 Closes #13617 --- All.sln.DotSettings | 2 + .../Properties/RelationalStrings.Designer.cs | 6 + .../Properties/RelationalStrings.resx | 3 + .../Query/Internal/ContainsTranslator.cs | 5 +- ...sionProjectionApplyingExpressionVisitor.cs | 4 +- .../Query/QuerySqlGenerator.cs | 182 +++++-- .../Query/RelationalQueryRootProcessor.cs | 84 +++ .../RelationalQueryTranslationPreprocessor.cs | 6 +- ...ueryTranslationPreprocessorDependencies.cs | 8 +- ...yableMethodTranslatingExpressionVisitor.cs | 481 +++++++++++++++++- ...lationalSqlTranslatingExpressionVisitor.cs | 55 +- .../Query/SqlExpressionFactory.cs | 41 +- .../Query/SqlExpressionVisitor.cs | 24 +- .../Query/SqlExpressions/ColumnExpression.cs | 7 + .../Query/SqlExpressions/InExpression.cs | 3 +- .../SqlExpressions/RowValueExpression.cs | 151 ++++++ .../ScalarSubqueryExpression.cs | 17 +- .../SqlExpressions/SelectExpression.Helper.cs | 7 +- .../Query/SqlExpressions/SelectExpression.cs | 67 ++- .../SqlExpressions/SqlParameterExpression.cs | 2 +- .../TableValuedFunctionExpression.cs | 67 ++- .../Query/SqlExpressions/ValuesExpression.cs | 166 ++++++ .../Query/SqlNullabilityProcessor.cs | 57 ++- .../ValueConversion/JsonStringConverter.cs | 57 +++ .../SqlServerServiceCollectionExtensions.cs | 1 + ...rchConditionConvertingExpressionVisitor.cs | 46 +- ...rNavigationExpansionExtensibilityHelper.cs | 3 +- .../Internal/SqlServerOpenJsonExpression.cs | 156 ++++++ .../Internal/SqlServerQuerySqlGenerator.cs | 136 +++++ .../SqlServerQueryTranslationPreprocessor.cs | 37 ++ ...verQueryTranslationPreprocessorFactory.cs} | 43 +- ...yableMethodTranslatingExpressionVisitor.cs | 220 ++++++-- .../Internal/SqlServerSqlExpressionFactory.cs | 21 +- .../Query/SqlServerQueryRootProcessor.cs | 35 ++ .../Internal/SqlServerStringTypeMapping.cs | 5 + .../Internal/SqlServerTypeMappingSource.cs | 90 +++- ...yableMethodTranslatingExpressionVisitor.cs | 67 +++ .../Internal/SqliteStringTypeMapping.cs | 10 + .../Internal/SqliteTypeMappingSource.cs | 59 ++- .../Query/ConstantQueryRootExpression.cs | 79 +++ ...ingExpressionVisitor.ExpressionVisitors.cs | 29 +- ...nExpandingExpressionVisitor.Expressions.cs | 42 ++ .../NavigationExpandingExpressionVisitor.cs | 86 ++-- ...yableMethodNormalizingExpressionVisitor.cs | 50 +- .../Query/ParameterQueryRootExpression.cs | 63 +++ src/EFCore/Query/QueryCompilationContext.cs | 2 +- src/EFCore/Query/QueryRootProcessor.cs | 99 ++++ .../Query/QueryTranslationPreprocessor.cs | 16 +- ...yableMethodTranslatingExpressionVisitor.cs | 15 +- src/EFCore/Storage/CoreTypeMapping.cs | 37 +- ...itiveCollectionsQueryRelationalTestBase.cs | 16 + .../Query/QueryNoClientEvalTestBase.cs | 12 - ...SharedPrimitiveCollectionsQueryTestBase.cs | 35 ++ .../Query/NorthwindCompiledQueryTestBase.cs | 15 +- .../NorthwindMiscellaneousQueryTestBase.cs | 1 + .../PrimitiveCollectionsQueryTestBase.cs | 463 +++++++++++++++++ ...avigationsCollectionsQuerySqlServerTest.cs | 21 +- ...CollectionsSharedTypeQuerySqlServerTest.cs | 21 +- ...tionsCollectionsSplitQuerySqlServerTest.cs | 35 +- .../ComplexNavigationsQuerySqlServerTest.cs | 7 +- ...NavigationsSharedTypeQuerySqlServerTest.cs | 7 +- .../Query/GearsOfWarQuerySqlServerTest.cs | 90 +++- ...dPrimitiveCollectionsQuerySqlServerTest.cs | 141 +++++ ...indAggregateOperatorsQuerySqlServerTest.cs | 170 ++++++- .../NorthwindCompiledQuerySqlServerTest.cs | 112 ++-- ...windEFPropertyIncludeQuerySqlServerTest.cs | 56 +- ...windIncludeNoTrackingQuerySqlServerTest.cs | 56 +- .../NorthwindIncludeQuerySqlServerTest.cs | 56 +- ...orthwindMiscellaneousQuerySqlServerTest.cs | 41 +- .../NorthwindNavigationsQuerySqlServerTest.cs | 8 +- .../NorthwindSelectQuerySqlServerTest.cs | 37 +- ...plitIncludeNoTrackingQuerySqlServerTest.cs | 86 +++- ...NorthwindSplitIncludeQuerySqlServerTest.cs | 86 +++- ...orthwindStringIncludeQuerySqlServerTest.cs | 56 +- .../Query/NorthwindWhereQuerySqlServerTest.cs | 43 +- .../Query/NullSemanticsQuerySqlServerTest.cs | 167 +++++- .../PrimitiveCollectionsQuerySqlServerTest.cs | 477 +++++++++++++++++ .../Query/QueryBugsTest.cs | 86 +++- .../Query/SimpleQuerySqlServerTest.cs | 2 +- .../SpatialQuerySqlServerGeographyFixture.cs | 6 +- .../SpatialQuerySqlServerGeometryFixture.cs | 6 +- .../Query/TPCGearsOfWarQuerySqlServerTest.cs | 92 +++- .../Query/TPTGearsOfWarQuerySqlServerTest.cs | 90 +++- ...avigationsCollectionsQuerySqlServerTest.cs | 21 +- ...CollectionsSharedTypeQuerySqlServerTest.cs | 21 +- .../TemporalGearsOfWarQuerySqlServerTest.cs | 90 +++- .../Update/SqlServerUpdateSqlGeneratorTest.cs | 11 +- ...aredPrimitiveCollectionsQuerySqliteTest.cs | 42 ++ .../Query/NorthwindCompiledQuerySqliteTest.cs | 4 +- .../PrimitiveCollectionsQuerySqliteTest.cs | 457 +++++++++++++++++ 90 files changed, 5594 insertions(+), 598 deletions(-) create mode 100644 src/EFCore.Relational/Query/RelationalQueryRootProcessor.cs create mode 100644 src/EFCore.Relational/Query/SqlExpressions/RowValueExpression.cs create mode 100644 src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs create mode 100644 src/EFCore.Relational/Storage/ValueConversion/JsonStringConverter.cs create mode 100644 src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs create mode 100644 src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessor.cs rename src/{EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs => EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessorFactory.cs} (53%) create mode 100644 src/EFCore.SqlServer/Query/SqlServerQueryRootProcessor.cs create mode 100644 src/EFCore/Query/ConstantQueryRootExpression.cs create mode 100644 src/EFCore/Query/ParameterQueryRootExpression.cs create mode 100644 src/EFCore/Query/QueryRootProcessor.cs create mode 100644 test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs create mode 100644 test/EFCore.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryTestBase.cs create mode 100644 test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs create mode 100644 test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs create mode 100644 test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs diff --git a/All.sln.DotSettings b/All.sln.DotSettings index 8229c429771..762d1ed2be6 100644 --- a/All.sln.DotSettings +++ b/All.sln.DotSettings @@ -308,8 +308,10 @@ The .NET Foundation licenses this file to you under the MIT license. True True True + True True True + True True True True diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 5de1ff3e3fa..7f71b06b607 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -621,6 +621,12 @@ public static string EitherOfTwoValuesMustBeNull(object? param1, object? param2) GetString("EitherOfTwoValuesMustBeNull", nameof(param1), nameof(param2)), param1, param2); + /// + /// Empty constant collections are not supported as constant query roots. + /// + public static string EmptyCollectionNotSupportedAsConstantQueryRoot + => GetString("EmptyCollectionNotSupportedAsConstantQueryRoot"); + /// /// The short name for '{entityType1}' is '{discriminatorValue}' which is the same for '{entityType2}'. Every concrete entity type in the hierarchy must have a unique short name. Either rename one of the types or call modelBuilder.Entity<TEntity>().Metadata.SetDiscriminatorValue("NewShortName"). /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index e89cace8c09..23fd4947e4a 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -346,6 +346,9 @@ Either {param1} or {param2} must be null. + + Empty constant collections are not supported as constant query roots. + The short name for '{entityType1}' is '{discriminatorValue}' which is the same for '{entityType2}'. Every concrete entity type in the hierarchy must have a unique short name. Either rename one of the types or call modelBuilder.Entity<TEntity>().Metadata.SetDiscriminatorValue("NewShortName"). diff --git a/src/EFCore.Relational/Query/Internal/ContainsTranslator.cs b/src/EFCore.Relational/Query/Internal/ContainsTranslator.cs index 0e523bf70a9..804a84ab14c 100644 --- a/src/EFCore.Relational/Query/Internal/ContainsTranslator.cs +++ b/src/EFCore.Relational/Query/Internal/ContainsTranslator.cs @@ -57,11 +57,10 @@ public ContainsTranslator(ISqlExpressionFactory sqlExpressionFactory) } private static bool ValidateValues(SqlExpression values) - => values is SqlConstantExpression || values is SqlParameterExpression; + => values is SqlConstantExpression or SqlParameterExpression; private static SqlExpression RemoveObjectConvert(SqlExpression expression) - => expression is SqlUnaryExpression sqlUnaryExpression - && sqlUnaryExpression.OperatorType == ExpressionType.Convert + => expression is SqlUnaryExpression { OperatorType: ExpressionType.Convert } sqlUnaryExpression && sqlUnaryExpression.Type == typeof(object) ? sqlUnaryExpression.Operand : expression; diff --git a/src/EFCore.Relational/Query/Internal/SelectExpressionProjectionApplyingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/SelectExpressionProjectionApplyingExpressionVisitor.cs index fb9496b2867..a8973e3a86a 100644 --- a/src/EFCore.Relational/Query/Internal/SelectExpressionProjectionApplyingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/SelectExpressionProjectionApplyingExpressionVisitor.cs @@ -35,11 +35,11 @@ public SelectExpressionProjectionApplyingExpressionVisitor(QuerySplittingBehavio protected override Expression VisitExtension(Expression extensionExpression) => extensionExpression switch { - ShapedQueryExpression shapedQueryExpression - when shapedQueryExpression.QueryExpression is SelectExpression selectExpression + ShapedQueryExpression { QueryExpression: SelectExpression selectExpression } shapedQueryExpression => shapedQueryExpression.UpdateShaperExpression( selectExpression.ApplyProjection( shapedQueryExpression.ShaperExpression, shapedQueryExpression.ResultCardinality, _querySplittingBehavior)), + NonQueryExpression nonQueryExpression => nonQueryExpression, _ => base.VisitExtension(extensionExpression), }; diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs index 07855455d00..3559eba973b 100644 --- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.CompilerServices; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage.Internal; @@ -169,15 +168,17 @@ protected override Expression VisitSqlFragment(SqlFragmentExpression sqlFragment } private static bool IsNonComposedSetOperation(SelectExpression selectExpression) - => selectExpression.Offset == null - && selectExpression.Limit == null - && !selectExpression.IsDistinct - && selectExpression.Predicate == null - && selectExpression.Having == null - && selectExpression.Orderings.Count == 0 - && selectExpression.GroupBy.Count == 0 - && selectExpression.Tables.Count == 1 - && selectExpression.Tables[0] is SetOperationBase setOperation + => selectExpression is + { + Tables: [SetOperationBase setOperation], + Offset: null, + Limit: null, + IsDistinct: false, + Predicate: null, + Having: null, + Orderings.Count: 0, + GroupBy.Count: 0 + } && selectExpression.Projection.Count == setOperation.Source1.Projection.Count && selectExpression.Projection.Select( (pe, index) => pe.Expression is ColumnExpression column @@ -226,12 +227,7 @@ protected override Expression VisitSelect(SelectExpression selectExpression) subQueryIndent = _relationalCommandBuilder.Indent(); } - if (IsNonComposedSetOperation(selectExpression)) - { - // Naked set operation - GenerateSetOperation((SetOperationBase)selectExpression.Tables[0]); - } - else + if (!TryGenerateWithoutWrappingSelect(selectExpression)) { _relationalCommandBuilder.Append("SELECT "); @@ -300,6 +296,39 @@ protected override Expression VisitSelect(SelectExpression selectExpression) return selectExpression; } + /// + /// If possible, generates the expression contained within the provided without the wrapping + /// SELECT. This can be done for set operations and VALUES, which can appear as top-level statements without needing to be wrapped + /// in SELECT. + /// + protected virtual bool TryGenerateWithoutWrappingSelect(SelectExpression selectExpression) + { + if (IsNonComposedSetOperation(selectExpression)) + { + // Naked set operation + GenerateSetOperation((SetOperationBase)selectExpression.Tables[0]); + return true; + } + + if (selectExpression is + { + Tables: [ValuesExpression valuesExpression], + Offset: null, + Limit: null, + IsDistinct: false, + Predicate: null, + Having: null, + Orderings.Count: 0, + GroupBy.Count: 0, + }) + { + GenerateValues(valuesExpression, withParentheses: false); + return true; + } + + return false; + } + /// /// Generates a pseudo FROM clause. Required by some providers when a query has no actual FROM clause. /// @@ -312,9 +341,7 @@ protected virtual void GeneratePseudoFromClause() /// /// SelectExpression for which the empty projection will be generated. protected virtual void GenerateEmptyProjection(SelectExpression selectExpression) - { - _relationalCommandBuilder.Append("1"); - } + => _relationalCommandBuilder.Append("1"); /// protected override Expression VisitProjection(ProjectionExpression projectionExpression) @@ -371,16 +398,16 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction /// protected override Expression VisitTableValuedFunction(TableValuedFunctionExpression tableValuedFunctionExpression) { - if (!string.IsNullOrEmpty(tableValuedFunctionExpression.StoreFunction.Schema)) + if (!string.IsNullOrEmpty(tableValuedFunctionExpression.Schema)) { _relationalCommandBuilder - .Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.StoreFunction.Schema)) + .Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Schema)) .Append("."); } - var name = tableValuedFunctionExpression.StoreFunction.IsBuiltIn - ? tableValuedFunctionExpression.StoreFunction.Name - : _sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.StoreFunction.Name); + var name = tableValuedFunctionExpression.IsBuiltIn + ? tableValuedFunctionExpression.Name + : _sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Name); _relationalCommandBuilder .Append(name) @@ -1132,6 +1159,28 @@ protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpres return rowNumberExpression; } + /// + protected override Expression VisitRowValue(RowValueExpression rowValueExpression) + { + Sql.Append("("); + + var values = rowValueExpression.Values; + var count = values.Count; + for (var i = 0; i < count; i++) + { + if (i > 0) + { + Sql.Append(", "); + } + + Visit(values[i]); + } + + Sql.Append(")"); + + return rowValueExpression; + } + /// /// Generates a set operation in the relational command. /// @@ -1141,18 +1190,16 @@ protected virtual void GenerateSetOperation(SetOperationBase setOperation) GenerateSetOperationOperand(setOperation, setOperation.Source1); _relationalCommandBuilder .AppendLine() - .Append(GetSetOperation(setOperation)) + .Append( + setOperation switch + { + ExceptExpression => "EXCEPT", + IntersectExpression => "INTERSECT", + UnionExpression => "UNION", + _ => throw new InvalidOperationException(CoreStrings.UnknownEntity("SetOperationType")) + }) .AppendLine(setOperation.IsDistinct ? string.Empty : " ALL"); GenerateSetOperationOperand(setOperation, setOperation.Source2); - - static string GetSetOperation(SetOperationBase operation) - => operation switch - { - ExceptExpression => "EXCEPT", - IntersectExpression => "INTERSECT", - UnionExpression => "UNION", - _ => throw new InvalidOperationException(CoreStrings.UnknownEntity("SetOperationType")) - }; } /// @@ -1311,6 +1358,73 @@ void LiftPredicate(TableExpressionBase joinTable) RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteUpdate))); } + /// + protected override Expression VisitValues(ValuesExpression valuesExpression) + { + GenerateValues(valuesExpression, withParentheses: true); + + return valuesExpression; + } + + /// + /// Generates a VALUES expression. + /// + protected virtual void GenerateValues(ValuesExpression valuesExpression, bool withParentheses = true) + { + // TODO: Review this! + if (withParentheses && valuesExpression.Alias is not null) + { + _relationalCommandBuilder.Append("("); + } + + var rowValues = valuesExpression.RowValues; + + // Some databases support providing the names of columns projected out of VALUES, e.g. + // SQL Server/PG: (VALUES (1, 3), (2, 4)) AS x(a, b). Others unfortunately don't; so by default, we extract out the first row, + // and generate a SELECT for it with the names, and a UNION ALL over the rest of the values. + _relationalCommandBuilder.Append("SELECT "); + + Check.DebugAssert(rowValues.Count > 0, "rowValues.Count > 0"); + var firstRowValues = rowValues[0].Values; + for (var i = 0; i < firstRowValues.Count; i++) + { + if (i > 0) + { + _relationalCommandBuilder.Append(", "); + } + + Visit(firstRowValues[i]); + + _relationalCommandBuilder + .Append(AliasSeparator) + .Append(valuesExpression.ColumnNames[i]); + } + + if (rowValues.Count > 1) + { + _relationalCommandBuilder.Append(" UNION ALL VALUES "); + + for (var i = 1; i < rowValues.Count; i++) + { + // TODO: Do we want newlines here? + if (i > 1) + { + _relationalCommandBuilder.Append(", "); + } + + Visit(valuesExpression.RowValues[i]); + } + } + + if (withParentheses && valuesExpression.Alias is not null) + { + _relationalCommandBuilder + .Append(")") + .Append(AliasSeparator) + .Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.Alias)); + } + } + /// protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression) => throw new InvalidOperationException( diff --git a/src/EFCore.Relational/Query/RelationalQueryRootProcessor.cs b/src/EFCore.Relational/Query/RelationalQueryRootProcessor.cs new file mode 100644 index 00000000000..1c2ddf74205 --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalQueryRootProcessor.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Query.Internal; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.Query; + +/// +public class RelationalQueryRootProcessor : QueryRootProcessor +{ + private readonly ITypeMappingSource _typeMappingSource; + private readonly IModel _model; + + /// + /// Creates a new instance of the class. + /// + /// The type mapping source. + /// The model. + public RelationalQueryRootProcessor(ITypeMappingSource typeMappingSource, IModel model) + { + _typeMappingSource = typeMappingSource; + _model = model; + } + + /// + protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) + { + // Create query root node for table-valued functions + if (_model.FindDbFunction(methodCallExpression.Method) is { IsScalar: false, StoreFunction: var storeFunction }) + { + // See issue #19970 + return new TableValuedFunctionQueryRootExpression( + storeFunction.EntityTypeMappings.Single().EntityType, + storeFunction, + methodCallExpression.Arguments); + } + + return base.VisitMethodCall(methodCallExpression); + } + + /// + /// Given a queryable constants over an element type which has a type mapping, converts it to a + /// ; it will be translated to a SQL . + /// + /// The constant expression to attempt to convert to a query root. + protected override Expression VisitQueryableConstant(ConstantExpression constantExpression) + // TODO: Note that we restrict to constants whose element type is mappable as-is. This excludes a constant list with an unsupported + // CLR type, where we could infer a type mapping with a value converter based on its later usage. However, converting all constant + // collections to query roots also carries risk. + => constantExpression.Type.TryGetSequenceType() is Type elementType && _typeMappingSource.FindMapping(elementType) is not null + ? new ConstantQueryRootExpression(constantExpression) + : constantExpression; + + /// + protected override Expression VisitQueryableParameter(ParameterExpression parameterExpression) + { + // TODO: Decide whether this belongs here or in specific provider code. This means parameter query roots always get created + // (in enumerable/queryable context), but may not be translatable in the provider's QueryableMethodTranslatingEV. As long + // as we unwrap there, we *should* be OK, and so don't need an additional provider extension point here... + + // TODO: Also, maybe this type checking should be in the base class. + // SQL Server's OpenJson, which we use to unpack the queryable parameter, does not support geometry (or any other non-built-in + // types) + + // We convert to query roots only parameters whose CLR type has a collection type mapping (i.e. ElementTypeMapping isn't null). + // This allows the provider to determine exactly which types are supported as queryable collections (e.g. OPENJSON on SQL Server). + return _typeMappingSource.FindMapping(parameterExpression.Type) is { ElementTypeMapping: not null } + ? new ParameterQueryRootExpression(parameterExpression.Type.GetSequenceType(), parameterExpression) + : parameterExpression; + } + + /// + protected override Expression VisitExtension(Expression node) + => node switch + { + // We skip FromSqlQueryRootExpression, since that contains the arguments as an object array parameter, and don't want to convert + // that to a query root + FromSqlQueryRootExpression e => e, + + _ => base.VisitExtension(node) + }; + +} diff --git a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs index 6077a62c474..03aef7b245d 100644 --- a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs @@ -36,9 +36,13 @@ public override Expression NormalizeQueryableMethod(Expression expression) { expression = new RelationalQueryMetadataExtractingExpressionVisitor(_relationalQueryCompilationContext).Visit(expression); expression = base.NormalizeQueryableMethod(expression); - expression = new TableValuedFunctionToQueryRootConvertingExpressionVisitor(QueryCompilationContext.Model).Visit(expression); expression = new CollectionIndexerToElementAtNormalizingExpressionVisitor().Visit(expression); return expression; } + + /// + public override Expression ProcessQueryRoots(Expression expression) + => new RelationalQueryRootProcessor(RelationalDependencies.RelationalTypeMappingSource, QueryCompilationContext.Model) + .Visit(expression); } diff --git a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessorDependencies.cs b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessorDependencies.cs index a8a505798c4..6bd82bdce6a 100644 --- a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessorDependencies.cs +++ b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessorDependencies.cs @@ -45,7 +45,13 @@ public sealed record RelationalQueryTranslationPreprocessorDependencies /// the constructor at any point in this process. /// [EntityFrameworkInternal] - public RelationalQueryTranslationPreprocessorDependencies() + public RelationalQueryTranslationPreprocessorDependencies(IRelationalTypeMappingSource relationalTypeMappingSource) { + RelationalTypeMappingSource = relationalTypeMappingSource; } + + /// + /// The type mapping source. + /// + public IRelationalTypeMappingSource RelationalTypeMappingSource { get; init; } } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 146af8fa44a..67a551a589e 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -1,8 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections; using System.Diagnostics.CodeAnalysis; -using System.Linq; +using System.Runtime.CompilerServices; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; @@ -12,13 +13,22 @@ namespace Microsoft.EntityFrameworkCore.Query; /// public class RelationalQueryableMethodTranslatingExpressionVisitor : QueryableMethodTranslatingExpressionVisitor { - private readonly RelationalSqlTranslatingExpressionVisitor _sqlTranslator; private readonly SharedTypeEntityExpandingExpressionVisitor _sharedTypeEntityExpandingExpressionVisitor; private readonly RelationalProjectionBindingExpressionVisitor _projectionBindingExpressionVisitor; private readonly QueryCompilationContext _queryCompilationContext; + private readonly IRelationalTypeMappingSource _typeMappingSource; private readonly ISqlExpressionFactory _sqlExpressionFactory; private readonly bool _subquery; + /// + /// References all columns in the tree that do not have type mappings set (via their name and the + /// they're on. This happens for queryable parameters (e.g. JSON arrays) or constants (VALUES). We attempt to infer the type + /// mappings for these columns based on their usage. + /// + private Dictionary _untypedColumns; + + private ColumnTypeMappingScanner? _columnTypeMappingScanner; + /// /// Creates a new instance of the class. /// @@ -34,12 +44,14 @@ public RelationalQueryableMethodTranslatingExpressionVisitor( RelationalDependencies = relationalDependencies; var sqlExpressionFactory = relationalDependencies.SqlExpressionFactory; + _typeMappingSource = relationalDependencies.TypeMappingSource; _queryCompilationContext = queryCompilationContext; - _sqlTranslator = relationalDependencies.RelationalSqlTranslatingExpressionVisitorFactory.Create(queryCompilationContext, this); + SqlTranslator = relationalDependencies.RelationalSqlTranslatingExpressionVisitorFactory.Create(queryCompilationContext, this); _sharedTypeEntityExpandingExpressionVisitor = - new SharedTypeEntityExpandingExpressionVisitor(_sqlTranslator, sqlExpressionFactory); - _projectionBindingExpressionVisitor = new RelationalProjectionBindingExpressionVisitor(this, _sqlTranslator); + new SharedTypeEntityExpandingExpressionVisitor(SqlTranslator, sqlExpressionFactory); + _projectionBindingExpressionVisitor = new RelationalProjectionBindingExpressionVisitor(this, SqlTranslator); _sqlExpressionFactory = sqlExpressionFactory; + _untypedColumns = new(); _subquery = false; } @@ -48,6 +60,8 @@ public RelationalQueryableMethodTranslatingExpressionVisitor( /// protected virtual RelationalQueryableMethodTranslatingExpressionVisitorDependencies RelationalDependencies { get; } + protected virtual RelationalSqlTranslatingExpressionVisitor SqlTranslator { get; } + /// /// Creates a new instance of the class. /// @@ -58,15 +72,39 @@ protected RelationalQueryableMethodTranslatingExpressionVisitor( { RelationalDependencies = parentVisitor.RelationalDependencies; _queryCompilationContext = parentVisitor._queryCompilationContext; - _sqlTranslator = RelationalDependencies.RelationalSqlTranslatingExpressionVisitorFactory.Create( + SqlTranslator = RelationalDependencies.RelationalSqlTranslatingExpressionVisitorFactory.Create( parentVisitor._queryCompilationContext, parentVisitor); _sharedTypeEntityExpandingExpressionVisitor = - new SharedTypeEntityExpandingExpressionVisitor(_sqlTranslator, parentVisitor._sqlExpressionFactory); - _projectionBindingExpressionVisitor = new RelationalProjectionBindingExpressionVisitor(this, _sqlTranslator); + new SharedTypeEntityExpandingExpressionVisitor(SqlTranslator, parentVisitor._sqlExpressionFactory); + _projectionBindingExpressionVisitor = new RelationalProjectionBindingExpressionVisitor(this, SqlTranslator); _sqlExpressionFactory = parentVisitor._sqlExpressionFactory; + _typeMappingSource = parentVisitor._typeMappingSource; + _untypedColumns = parentVisitor._untypedColumns; _subquery = true; } + /// + public override Expression Translate(Expression expression) + { + var visited = Visit(expression); + + // We've finished translating. If there were any untyped columns (i.e. queryable constants or parameters), we should have collected + // their inferred type mappings during SQL translation (see TranslateExpression below). For columns whose type mappings weren't + // inferred, get the default based on the CLR type, and apply all these back to the queryable root. + + if (!_subquery) + { + if (_untypedColumns.Any(kv => kv.Value is null)) + { + throw new InvalidOperationException("Queryable parameter or constant did not have its type mapping inferred"); + } + + visited = ProcessTypeMappings(visited, _untypedColumns!); + } + + return visited; + } + /// protected override Expression VisitExtension(Expression extensionExpression) { @@ -83,6 +121,7 @@ protected override Expression VisitExtension(Expression extensionExpression) fromSqlQueryRootExpression.Argument))); case TableValuedFunctionQueryRootExpression tableValuedFunctionQueryRootExpression: + { var function = tableValuedFunctionQueryRootExpression.Function; var arguments = new List(); foreach (var arg in tableValuedFunctionQueryRootExpression.Arguments) @@ -122,6 +161,7 @@ protected override Expression VisitExtension(Expression extensionExpression) var queryExpression = _sqlExpressionFactory.Select(entityType, translation); return CreateShapedQueryExpression(entityType, queryExpression); + } case EntityQueryRootExpression entityQueryRootExpression when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression) @@ -153,6 +193,7 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression) .Visit(shapedQueryExpression.ShaperExpression)); case SqlQueryRootExpression sqlQueryRootExpression: + { var typeMapping = RelationalDependencies.TypeMappingSource.FindMapping(sqlQueryRootExpression.ElementType); if (typeMapping == null) { @@ -177,12 +218,22 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression) } return new ShapedQueryExpression(selectExpression, shaperExpression); + } + + case ConstantQueryRootExpression constantQueryRootExpression: + return VisitConstantPrimitiveCollection(constantQueryRootExpression) ?? base.VisitExtension(extensionExpression); + + case ParameterQueryRootExpression: + return VisitPrimitiveCollection(extensionExpression) ?? base.VisitExtension(extensionExpression); default: return base.VisitExtension(extensionExpression); } } + protected virtual void AddUntypedColumnExpression(TableExpressionBase tableExpression) + => _untypedColumns[tableExpression] = null; + /// protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) { @@ -212,7 +263,99 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp } } - return base.VisitMethodCall(methodCallExpression); + var translated = base.VisitMethodCall(methodCallExpression); + + if (translated == QueryCompilationContext.NotTranslatedExpression + && SqlTranslator.TryTranslatePropertyAccess(methodCallExpression, out var propertyAccessExpression) + && propertyAccessExpression is ColumnExpression { TypeMapping.ElementTypeMapping: not null } + && VisitPrimitiveCollection(propertyAccessExpression) is { } primitiveCollectionTranslation) + { + return primitiveCollectionTranslation; + } + + return translated; + } + + /// + /// Translates a primitive collection. Providers can override this to translate e.g. int[] columns/parameters/constants to a + /// queryable table (OPENJSON on SQL Server, unnest on PostgreSQL...). The default implementation always + /// returns (no translation). + /// + /// + /// See for the translation of constant primitive collections. + /// + /// The expression to try to translate as a primitive collection expression. + /// A if the translation was successful, otherwise . + protected virtual ShapedQueryExpression? VisitPrimitiveCollection(Expression primitiveCollectionExpression) + => null; + + /// + /// Translates a constant primitive collection into a queryable SQL VALUES expression. + /// + /// The constant primitive collection to be translated. + /// A queryable SQL VALUES expression. + protected virtual ShapedQueryExpression? VisitConstantPrimitiveCollection(ConstantQueryRootExpression constantQueryRootExpression) + { + var rows = constantQueryRootExpression.ConstantExpression.Value as IEnumerable; + Check.DebugAssert(rows is not null, "ConstantQueryRootExpression with non-IEnumerable value"); + var elementType = constantQueryRootExpression.ElementType; + + var rowExpressions = new List(); + + var encounteredNull = false; + + foreach (var row in rows) + { + if (row is ITuple) + { + // TODO: Support multi-value rows (tuples) + // SelectExpression seems to require actual reflection MemberInfos for its projection mapping, so we'd need to + // (in theory) create an anonymous type here. See about changing the design to allow this; in the meantine, support + // only single-value row values. + throw new NotSupportedException("Tuples aren't supported"); // TODO + } + + // TODO: Infer the type mapping + // TODO: Construct the RowValueExpression in SqlExpressionFactory, to allow providers to build it with a proper type + // mapping? + if (row is null) + { + encounteredNull = true; + } + + rowExpressions.Add( + new RowValueExpression( + new SqlExpression[] { _sqlExpressionFactory.Constant(row, elementType, typeMapping: null) }, + typeof(ValueTuple))); + } + + if (rowExpressions.Count == 0) + { + AddTranslationErrorDetails(RelationalStrings.EmptyCollectionNotSupportedAsConstantQueryRoot); + return null; + } + + var valuesExpression = new ValuesExpression("v", rowExpressions, new[] { "Value" }); + AddUntypedColumnExpression(valuesExpression); + + // Note: we leave the element type mapping null, to allow it to get inferred based on queryable operators composed on top. + // TODO: confirm nullable/enum unwrapping on the elementType + var selectExpression = new SelectExpression( + elementType.UnwrapNullableType(), typeMapping: null, valuesExpression, isColumnNullable: encounteredNull); + + Expression shaperExpression = new ProjectionBindingExpression( + selectExpression, new ProjectionMember(), encounteredNull ? elementType.MakeNullable() : elementType); + + if (elementType != shaperExpression.Type) + { + Check.DebugAssert( + elementType.MakeNullable() == shaperExpression.Type, + "expression.Type must be nullable of targetType"); + + shaperExpression = Expression.Convert(shaperExpression, elementType); + } + + return new ShapedQueryExpression(selectExpression, shaperExpression); } /// @@ -244,7 +387,16 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent } var selectExpression = (SelectExpression)source.QueryExpression; - selectExpression.ApplyPredicate(_sqlExpressionFactory.Not(translation)); + selectExpression.ApplyPredicate( + translation is SqlUnaryExpression { OperatorType: ExpressionType.Not, Operand: var nestedOperand } + ? nestedOperand + : _sqlExpressionFactory.Not(translation)); + + if (TrySimplifyToInExpression(source, isNegated: true, out var simplifiedQuery)) + { + return simplifiedQuery; + } + selectExpression.ReplaceProjection(new List()); selectExpression.ApplyProjection(); if (selectExpression.Limit == null @@ -273,6 +425,11 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent } source = translatedSource; + + if (TrySimplifyToInExpression(source, isNegated: false, out var simplifiedQuery)) + { + return simplifiedQuery; + } } var selectExpression = (SelectExpression)source.QueryExpression; @@ -308,7 +465,13 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent /// protected override ShapedQueryExpression? TranslateConcat(ShapedQueryExpression source1, ShapedQueryExpression source2) { - ((SelectExpression)source1.QueryExpression).ApplyUnion((SelectExpression)source2.QueryExpression, distinct: false); + var select1 = ((SelectExpression)source1.QueryExpression); + select1.ApplyUnion((SelectExpression)source2.QueryExpression, distinct: false); + + if (_untypedColumns.Count > 0) + { + TryInferTypeMappingForSetOperation((SetOperationBase)select1.Tables[0]); + } return source1.UpdateShaperExpression( MatchShaperNullabilityForSetOperation(source1.ShaperExpression, source2.ShaperExpression, makeNullable: true)); @@ -418,7 +581,13 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent /// protected override ShapedQueryExpression? TranslateExcept(ShapedQueryExpression source1, ShapedQueryExpression source2) { - ((SelectExpression)source1.QueryExpression).ApplyExcept((SelectExpression)source2.QueryExpression, distinct: true); + var select1 = ((SelectExpression)source1.QueryExpression); + select1.ApplyExcept((SelectExpression)source2.QueryExpression, distinct: true); + + if (_untypedColumns.Count > 0) + { + TryInferTypeMappingForSetOperation((SetOperationBase)select1.Tables[0]); + } // Since except has result from source1, we don't need to change shaper return source1; @@ -578,7 +747,13 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent /// protected override ShapedQueryExpression? TranslateIntersect(ShapedQueryExpression source1, ShapedQueryExpression source2) { - ((SelectExpression)source1.QueryExpression).ApplyIntersect((SelectExpression)source2.QueryExpression, distinct: true); + var select1 = ((SelectExpression)source1.QueryExpression); + select1.ApplyIntersect((SelectExpression)source2.QueryExpression, distinct: true); + + if (_untypedColumns.Count > 0) + { + TryInferTypeMappingForSetOperation((SetOperationBase)select1.Tables[0]); + } // For intersect since result comes from both sides, if one of them is non-nullable then both are non-nullable return source1.UpdateShaperExpression( @@ -1014,7 +1189,13 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp /// protected override ShapedQueryExpression? TranslateUnion(ShapedQueryExpression source1, ShapedQueryExpression source2) { - ((SelectExpression)source1.QueryExpression).ApplyUnion((SelectExpression)source2.QueryExpression, distinct: true); + var select1 = ((SelectExpression)source1.QueryExpression); + select1.ApplyUnion((SelectExpression)source2.QueryExpression, distinct: true); + + if (_untypedColumns.Count > 0) + { + TryInferTypeMappingForSetOperation((SetOperationBase)select1.Tables[0]); + } return source1.UpdateShaperExpression( MatchShaperNullabilityForSetOperation(source1.ShaperExpression, source2.ShaperExpression, makeNullable: true)); @@ -1340,7 +1521,7 @@ static bool AreOtherNonOwnedEntityTypesInTheTable(IEntityType rootType, ITableBa // Later we decompose it back into left/right components so that the equality is not in the tree which can get affected by // null semantics or other visitor. var setter = Infrastructure.ExpressionExtensions.CreateEqualsExpression(left, right); - var translation = visitor._sqlTranslator.Translate(setter); + var translation = visitor.SqlTranslator.Translate(setter); if (translation is SqlBinaryExpression { OperatorType: ExpressionType.Equal, Left: ColumnExpression column @@ -1353,7 +1534,7 @@ static bool AreOtherNonOwnedEntityTypesInTheTable(IEntityType rootType, ITableBa // We would reach here only if the property is unmapped or value fails to translate. visitor.AddTranslationErrorDetails( RelationalStrings.UnableToTranslateSetProperty( - propertyExpression.Print(), valueExpression.Print(), visitor._sqlTranslator.TranslationErrorDetails)); + propertyExpression.Print(), valueExpression.Print(), visitor.SqlTranslator.TranslationErrorDetails)); return null; } } @@ -1583,10 +1764,22 @@ protected virtual bool IsValidSelectExpressionForExecuteUpdate( /// A which is translation of given expression or . protected virtual SqlExpression? TranslateExpression(Expression expression) { - var translation = _sqlTranslator.Translate(expression); - if (translation == null && _sqlTranslator.TranslationErrorDetails != null) + var translation = SqlTranslator.Translate(expression); + + if (translation is null) { - AddTranslationErrorDetails(_sqlTranslator.TranslationErrorDetails); + if (SqlTranslator.TranslationErrorDetails != null) + { + AddTranslationErrorDetails(SqlTranslator.TranslationErrorDetails); + } + } + else + { + if (_untypedColumns.Any(kv => kv.Value is null)) + { + _columnTypeMappingScanner ??= new(_untypedColumns); + _columnTypeMappingScanner.Visit(translation); + } } return translation; @@ -1603,6 +1796,102 @@ protected virtual bool IsValidSelectExpressionForExecuteUpdate( LambdaExpression lambdaExpression) => TranslateExpression(RemapLambdaBody(shapedQueryExpression, lambdaExpression)); + /// + /// Invoked at the end of top-level translation, applies inferred type mappings for queryable constants/parameters and verifies that + /// all have a type mapping. + /// + /// The query expression to process. + /// + /// Inferred type mappings for queryable constants/parameters collected during translation. These will be applied to the appropriate + /// nodes in the tree. + /// + protected virtual Expression ProcessTypeMappings( + Expression expression, + Dictionary inferredTypeMappings) + => new TypeMappingProcessor(inferredTypeMappings).Visit(expression); + + /// + /// Attempts to pattern-match for Contains over , which corresponds to + /// Where(b => new[] { 1, 2, 3 }.Contains(b.Id)). Simplifies this to the tighter [b].[Id] IN (1, 2, 3) instead of the + /// full subquery with VALUES. + /// + private bool TrySimplifyToInExpression( + ShapedQueryExpression source, + bool isNegated, + [NotNullWhen(true)] out ShapedQueryExpression? simplifiedQuery) + { + if (source.QueryExpression is SelectExpression + { + Tables: [ValuesExpression { RowValues: [ { Values.Count: 1 }, ..] } valuesExpression], + GroupBy: [], + Having: null, + IsDistinct: false, + Limit: null, + Offset: null, + // Note that we don't care about orderings, they get elided anyway by Any/All + Predicate: SqlBinaryExpression { OperatorType: ExpressionType.Equal, Left: var left, Right: var right }, + } selectExpression) + { + // TODO: We want to pattern match on the projection, i.e. compare the ColumnExpression in the predicate to the + // SelectExpression's projection, but we can't do that without applying the projection; we can't apply the projection because + // the pattern matching may fail and we'll have wrongly changed the SelectExpression. + // So we clone the expression to avoid any side-effects, but that also prevents us from directly comparing columns (since + // the ValuesExpression has been cloned too). + // We should simply be able to pattern-match directly on the projection mappings (currently private) without cloning/applying + var clonedSelect = selectExpression.Clone(); + clonedSelect.ApplyProjection(); + + if (clonedSelect.Projection is not [{ Expression: ColumnExpression { Name: var columnName}}]) + { + simplifiedQuery = null; + return false; + } + + SqlExpression item; + if (left is ColumnExpression leftColumn + && ReferenceEquals(leftColumn.Table, valuesExpression) + && leftColumn.Name == columnName) + { + item = right; + } + else if (right is ColumnExpression rightColumn + && ReferenceEquals(rightColumn.Table, valuesExpression) + && rightColumn.Name == columnName) + { + item = left; + } + else + { + simplifiedQuery = null; + return false; + } + + var values = new object?[valuesExpression.RowValues.Count]; + for (var i = 0; i < values.Length; i++) + { + Check.DebugAssert( + valuesExpression.RowValues[i].Values.Count == 1, "valuesExpression.RowValues[i].Values.Count == 1"); + + if (valuesExpression.RowValues[i].Values[0] is SqlConstantExpression { Value: var constantValue }) + { + values[i] = constantValue; + } + else + { + simplifiedQuery = null; + return false; + } + } + + var inExpression = _sqlExpressionFactory.In(item, _sqlExpressionFactory.Constant(values), isNegated); + simplifiedQuery = source.Update(_sqlExpressionFactory.Select(inExpression), source.ShaperExpression); + return true; + } + + simplifiedQuery = null; + return false; + } + private Expression RemapLambdaBody(ShapedQueryExpression shapedQueryExpression, LambdaExpression lambdaExpression) { var lambdaBody = ReplacingExpressionVisitor.Replace( @@ -1771,7 +2060,7 @@ static Expression PrepareFailedTranslationResult( static bool IsValidSelectorForJsonArrayElementAccess(Expression expression, JsonQueryExpression baselineJsonQuery) { // JSON_QUERY($[0]).Property - if (expression is MemberExpression + if (expression is MemberExpression { Expression: RelationalEntityShaperExpression { ValueBufferExpression: JsonQueryExpression memberJqe } } memberExpression @@ -2088,6 +2377,34 @@ private static void HandleGroupByForAggregate(SelectExpression selectExpression, } } + /// + /// For set operations involving a leg with a type mapping (e.g. some column) and a leg without one (queryable constant or + /// parameter), we infer the missing type mapping from the other side. + /// + private void TryInferTypeMappingForSetOperation(SetOperationBase setOperation) + { + if (setOperation is + { + Source1.Projection: [{ Expression: ColumnExpression column1 }], + Source2.Projection: [{ Expression: ColumnExpression column2 }] + }) + { + if (column1.TypeMapping is not null + && _untypedColumns.TryGetValue(column2.Table, out var knownTypeMapping2) + && knownTypeMapping2 is null) + { + _untypedColumns[column2.Table] = column1.TypeMapping; + } + + if (column2.TypeMapping is not null + && _untypedColumns.TryGetValue(column1.Table, out var knownTypeMapping1) + && knownTypeMapping1 is null) + { + _untypedColumns[column1.Table] = column2.TypeMapping; + } + } + } + private static Expression MatchShaperNullabilityForSetOperation(Expression shaper1, Expression shaper2, bool makeNullable) { switch (shaper1) @@ -2220,7 +2537,7 @@ private static Expression MatchShaperNullabilityForSetOperation(Expression shape methodGenerator(translatedSelector.Type), Expression.Call( QueryableMethods.AsQueryable.MakeGenericMethod(translatedSelector.Type), new EnumerableExpression(translatedSelector))); - var translation = _sqlTranslator.Translate(methodCall); + var translation = SqlTranslator.Translate(methodCall); if (translation == null) { return null; @@ -2276,4 +2593,126 @@ private static Expression MatchShaperNullabilityForSetOperation(Expression shape return source.UpdateShaperExpression(shaper); } + + /// + /// A visitor which scans an expression tree and attempts to find columns for which we were missing type mappings (projected out + /// of queryable constant/parameter), and those type mappings have been inferred. When such columns are found, sets the inferred + /// type mappings found on them in _untypedColumns; we'll apply them on their (currently untyped) query roots. + /// + private class ColumnTypeMappingScanner : ExpressionVisitor + { + private readonly Dictionary _untypedColumns; + + public ColumnTypeMappingScanner(Dictionary untypedColumns) + => _untypedColumns = untypedColumns; + + protected override Expression VisitExtension(Expression node) + { + var (columnExpression, typeMapping) = node switch + { + ColumnExpression c => (c, c.TypeMapping), + + // With ScalarSubqueryExpression we have this hack: the inferred type mapping is on the expression itself, while the + // ColumnExpression we need is on the subquery's projection; but we can't change when applying the inferred type mapping + // since SelectExpression is closed down (see ScalarSubqueryExpression.ApplyTypeMapping). + ScalarSubqueryExpression { Subquery.Projection: [{ Expression: ColumnExpression c2 }] } scalarSubqueryExpression + => (c2, scalarSubqueryExpression.TypeMapping), + _ => (null, null) + }; + + if (columnExpression is not null + && typeMapping is not null + && _untypedColumns.TryGetValue(columnExpression.Table, out var knownTypeMapping) + && knownTypeMapping is null) + { + // We've found a type mapping on a column whose type mapping was previously unknown - that means the type mapping was + // inferred. Record the inferred mapping so we can apply it later. + _untypedColumns[columnExpression.Table] = typeMapping; + } + + return base.VisitExtension(node); + } + } + + /// + /// A visitor executed at the end of translation, which verifies that all nodes have a type mapping, + /// and applies type mappings inferred for queryable constants (VALUES) and parameters (e.g. OPENJSON) back on their query roots. + /// + protected class TypeMappingProcessor : ExpressionVisitor + { + /// + /// The inferred type mappings to be applied back on their query roots. + /// + protected IReadOnlyDictionary InferredTypeMappings { get; } + + /// + /// Creates a new instance of the class. + /// + /// The inferred type mappings to be applied back on their query roots. + public TypeMappingProcessor(Dictionary inferredTypeMappings) + => InferredTypeMappings = inferredTypeMappings; + + /// + protected override Expression VisitExtension(Expression expression) + => expression switch + { + // ColumnExpressions don't have a type mapping when they refer to a queryable constant or parameter. + // When these appear inside lambdas (SQL context), their type mappings get inferred from the other side as usual, like + // constants/parameters. But they also appear e.g. in ProjectionExpressions of SelectExpression, where we need to apply + // the inferred type mapping here. + ColumnExpression { TypeMapping: null} columnExpression + when InferredTypeMappings.TryGetValue(columnExpression.Table, out var typeMapping) + => columnExpression.ApplyTypeMapping(typeMapping), + + // Any other SqlExpression without a type mapping indicates a problem in EF. + SqlExpression { TypeMapping: null } sqlExpression and not SqlFragmentExpression + => throw new InvalidOperationException(RelationalStrings.NullTypeMappingInSqlTree(sqlExpression.Print())), + + // For ValueExpression, apply the inferred type mapping on all constants inside. + ValuesExpression valuesExpression + when InferredTypeMappings.TryGetValue(valuesExpression, out var typeMapping) && typeMapping is not null + => ApplyTypeMappingsOnValuesExpression(valuesExpression, new[] { typeMapping }), + + ShapedQueryExpression shapedQueryExpression + => shapedQueryExpression.UpdateQueryExpression(Visit(shapedQueryExpression.QueryExpression)), + + _ => base.VisitExtension(expression) + }; + + /// + /// Applies the given type mappings to the values projected out by the given . + /// + /// The to apply the mappings to. + /// The type mappings to apply. + protected virtual ValuesExpression ApplyTypeMappingsOnValuesExpression( + ValuesExpression valuesExpression, + IReadOnlyList typeMappings) + { + var newRowValues = new RowValueExpression[valuesExpression.RowValues.Count]; + for (var i = 0; i < newRowValues.Length; i++) + { + var rowValue = valuesExpression.RowValues[i]; + var newValues = new SqlExpression[rowValue.Values.Count]; + for (var j = 0; j < newValues.Length; j++) + { + Check.DebugAssert(rowValue.Values[j] is SqlConstantExpression, "Non-constant SqlExpression in ValuesExpression"); + + var value = (SqlConstantExpression)rowValue.Values[j]; + SqlExpression newValue = new SqlConstantExpression(Expression.Constant(value.Value, value.Type), typeMappings[j]); + + // We currently add explicit conversions on the first row, to ensure that the inferred types are properly typed. + // See #30605 + if (i == 0) + { + newValue = new SqlUnaryExpression(ExpressionType.Convert, newValue, newValue.Type, newValue.TypeMapping); + } + + newValues[j] = newValue; + } + newRowValues[i] = new RowValueExpression(newValues, rowValue.Type); + } + + return new(valuesExpression.Alias, newRowValues, valuesExpression.ColumnNames, valuesExpression.GetAnnotations()); + } + } } diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 737d5fd13c3..e9a18856a2a 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -66,7 +66,6 @@ private static readonly MethodInfo StringEqualsWithStringComparisonStatic private readonly IModel _model; private readonly ISqlExpressionFactory _sqlExpressionFactory; private readonly QueryableMethodTranslatingExpressionVisitor _queryableMethodTranslatingExpressionVisitor; - private readonly SqlTypeMappingVerifyingExpressionVisitor _sqlTypeMappingVerifyingExpressionVisitor; private bool _throwForNotTranslatedEfProperty; @@ -86,7 +85,6 @@ public RelationalSqlTranslatingExpressionVisitor( _queryCompilationContext = queryCompilationContext; _model = queryCompilationContext.Model; _queryableMethodTranslatingExpressionVisitor = queryableMethodTranslatingExpressionVisitor; - _sqlTypeMappingVerifyingExpressionVisitor = new SqlTypeMappingVerifyingExpressionVisitor(); _throwForNotTranslatedEfProperty = true; } @@ -134,8 +132,7 @@ protected virtual void AddTranslationErrorDetails(string details) if (result is SqlExpression translation) { - if (translation is SqlUnaryExpression sqlUnaryExpression - && sqlUnaryExpression.OperatorType == ExpressionType.Convert + if (translation is SqlUnaryExpression { OperatorType: ExpressionType.Convert } sqlUnaryExpression && sqlUnaryExpression.Type == typeof(object)) { translation = sqlUnaryExpression.Operand; @@ -149,8 +146,6 @@ protected virtual void AddTranslationErrorDetails(string details) return null; } - _sqlTypeMappingVerifyingExpressionVisitor.Visit(translation); - return translation; } @@ -399,8 +394,7 @@ static Expression RemoveConvert(Expression e) var visitedLeft = Visit(left); var visitedRight = Visit(right); - if ((binaryExpression.NodeType == ExpressionType.Equal - || binaryExpression.NodeType == ExpressionType.NotEqual) + if (binaryExpression.NodeType is ExpressionType.Equal or ExpressionType.NotEqual // Visited expression could be null, We need to pass MemberInitExpression && TryRewriteEntityEquality( binaryExpression.NodeType, @@ -704,6 +698,12 @@ protected override Expression VisitExtension(Expression extensionExpression) return scalarSubqueryExpression; + // We have e.g. an array parameter inside a Where clause; this is represented as a QueryableParameterQueryRootExpression so + // that we can translate queryable operators over it (query root in subquery context), but in normal SQL translation context + // we just unwrap the query root expression to get the parameter out. + case ParameterQueryRootExpression queryableParameterQueryRootExpression: + return Visit(queryableParameterQueryRootExpression.ParameterExpression); + default: return QueryCompilationContext.NotTranslatedExpression; } @@ -738,6 +738,36 @@ protected override Expression VisitMember(MemberExpression memberExpression) protected override Expression VisitMemberInit(MemberInitExpression memberInitExpression) => GetConstantOrNotTranslated(memberInitExpression); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public virtual bool TryTranslatePropertyAccess(Expression expression, [NotNullWhen(true)] out SqlExpression? propertyAccessExpression) + { + if (expression is MethodCallExpression methodCallExpression) + { + if (methodCallExpression.TryGetEFPropertyArguments(out var source, out var propertyName) + && TryBindMember(Visit(source), MemberIdentity.Create(propertyName)) is { } result) + { + propertyAccessExpression = result; + return true; + } + + if (methodCallExpression.TryGetIndexerArguments(_model, out source, out propertyName) + && TryBindMember(Visit(source), MemberIdentity.Create(propertyName)) is { } indexerResult) + { + propertyAccessExpression = indexerResult; + return true; + } + } + + propertyAccessExpression = null; + return false; + } + /// protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) { @@ -1913,13 +1943,4 @@ public Expression Convert(Type type) : new EntityReferenceExpression(this, derivedEntityType); } } - - private sealed class SqlTypeMappingVerifyingExpressionVisitor : ExpressionVisitor - { - protected override Expression VisitExtension(Expression extensionExpression) - => extensionExpression is SqlExpression { TypeMapping: null } sqlExpression - && extensionExpression is not SqlFragmentExpression - ? throw new InvalidOperationException(RelationalStrings.NullTypeMappingInSqlTree(sqlExpression.Print())) - : base.VisitExtension(extensionExpression); - } } diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs index 1da3ecfb20c..7eb81a06742 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs @@ -44,30 +44,29 @@ public SqlExpressionFactory(SqlExpressionFactoryDependencies dependencies) [return: NotNullIfNotNull("sqlExpression")] public virtual SqlExpression? ApplyTypeMapping(SqlExpression? sqlExpression, RelationalTypeMapping? typeMapping) { -#pragma warning disable IDE0046 // Convert to conditional expression - if (sqlExpression == null -#pragma warning restore IDE0046 // Convert to conditional expression - || sqlExpression.TypeMapping != null) + if (sqlExpression is { TypeMapping: null }) { - return sqlExpression; + return sqlExpression switch + { + AtTimeZoneExpression e => ApplyTypeMappingOnAtTimeZone(e, typeMapping), + CaseExpression e => ApplyTypeMappingOnCase(e, typeMapping), + CollateExpression e => ApplyTypeMappingOnCollate(e, typeMapping), + ColumnExpression e => e.ApplyTypeMapping(typeMapping), + DistinctExpression e => ApplyTypeMappingOnDistinct(e, typeMapping), + InExpression e => ApplyTypeMappingOnIn(e), + LikeExpression e => ApplyTypeMappingOnLike(e), + ScalarSubqueryExpression e => e.ApplyTypeMapping(typeMapping), + SqlBinaryExpression e => ApplyTypeMappingOnSqlBinary(e, typeMapping), + SqlConstantExpression e => e.ApplyTypeMapping(typeMapping), + SqlFragmentExpression e => e, + SqlFunctionExpression e => e.ApplyTypeMapping(typeMapping), + SqlParameterExpression e => e.ApplyTypeMapping(typeMapping), + SqlUnaryExpression e => ApplyTypeMappingOnSqlUnary(e, typeMapping), + _ => sqlExpression + }; } - return sqlExpression switch - { - AtTimeZoneExpression e => ApplyTypeMappingOnAtTimeZone(e, typeMapping), - CaseExpression e => ApplyTypeMappingOnCase(e, typeMapping), - CollateExpression e => ApplyTypeMappingOnCollate(e, typeMapping), - DistinctExpression e => ApplyTypeMappingOnDistinct(e, typeMapping), - InExpression e => ApplyTypeMappingOnIn(e), - LikeExpression e => ApplyTypeMappingOnLike(e), - SqlBinaryExpression e => ApplyTypeMappingOnSqlBinary(e, typeMapping), - SqlConstantExpression e => e.ApplyTypeMapping(typeMapping), - SqlFragmentExpression e => e, - SqlFunctionExpression e => e.ApplyTypeMapping(typeMapping), - SqlParameterExpression e => e.ApplyTypeMapping(typeMapping), - SqlUnaryExpression e => ApplyTypeMappingOnSqlUnary(e, typeMapping), - _ => sqlExpression - }; + return sqlExpression; } private SqlExpression ApplyTypeMappingOnAtTimeZone(AtTimeZoneExpression atTimeZoneExpression, RelationalTypeMapping? typeMapping) diff --git a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs index 46fdcdb0e10..351a40a61ae 100644 --- a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs @@ -36,6 +36,7 @@ ShapedQueryExpression shapedQueryExpression InExpression inExpression => VisitIn(inExpression), IntersectExpression intersectExpression => VisitIntersect(intersectExpression), InnerJoinExpression innerJoinExpression => VisitInnerJoin(innerJoinExpression), + JsonScalarExpression jsonScalarExpression => VisitJsonScalar(jsonScalarExpression), LeftJoinExpression leftJoinExpression => VisitLeftJoin(leftJoinExpression), LikeExpression likeExpression => VisitLike(likeExpression), OrderingExpression orderingExpression => VisitOrdering(orderingExpression), @@ -43,6 +44,7 @@ ShapedQueryExpression shapedQueryExpression ProjectionExpression projectionExpression => VisitProjection(projectionExpression), TableValuedFunctionExpression tableValuedFunctionExpression => VisitTableValuedFunction(tableValuedFunctionExpression), RowNumberExpression rowNumberExpression => VisitRowNumber(rowNumberExpression), + RowValueExpression rowValueExpression => VisitRowValue(rowValueExpression), ScalarSubqueryExpression scalarSubqueryExpression => VisitScalarSubquery(scalarSubqueryExpression), SelectExpression selectExpression => VisitSelect(selectExpression), SqlBinaryExpression sqlBinaryExpression => VisitSqlBinary(sqlBinaryExpression), @@ -54,7 +56,7 @@ ShapedQueryExpression shapedQueryExpression TableExpression tableExpression => VisitTable(tableExpression), UnionExpression unionExpression => VisitUnion(unionExpression), UpdateExpression updateExpression => VisitUpdate(updateExpression), - JsonScalarExpression jsonScalarExpression => VisitJsonScalar(jsonScalarExpression), + ValuesExpression valuesExpression => VisitValues(valuesExpression), _ => base.VisitExtension(extensionExpression), }; @@ -149,6 +151,13 @@ ShapedQueryExpression shapedQueryExpression /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. protected abstract Expression VisitIntersect(IntersectExpression intersectExpression); + /// + /// Visits the children of the JSON scalar expression. + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. + protected abstract Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression); + /// /// Visits the children of the like expression. /// @@ -205,6 +214,13 @@ ShapedQueryExpression shapedQueryExpression /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. protected abstract Expression VisitRowNumber(RowNumberExpression rowNumberExpression); + /// + /// Visits the children of the row value expression. + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. + protected abstract Expression VisitRowValue(RowValueExpression rowValueExpression); + /// /// Visits the children of the scalar subquery expression. /// @@ -283,9 +299,9 @@ ShapedQueryExpression shapedQueryExpression protected abstract Expression VisitUpdate(UpdateExpression updateExpression); /// - /// Visits the children of the JSON scalar expression. + /// Visits the children of the values expression. /// - /// The expression to visit. + /// The expression to visit. /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. - protected abstract Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression); + protected abstract Expression VisitValues(ValuesExpression valuesExpression); } diff --git a/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs index 2a9a10cfb23..7d63dc2d58d 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs @@ -51,6 +51,13 @@ protected ColumnExpression(Type type, RelationalTypeMapping? typeMapping) /// A new expression which has property set to true. public abstract ColumnExpression MakeNullable(); + /// + /// Applies supplied type mapping to this expression. + /// + /// A relational type mapping to apply. + /// A new expression which has supplied type mapping. + public abstract SqlExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping); + /// protected override void Print(ExpressionPrinter expressionPrinter) { diff --git a/src/EFCore.Relational/Query/SqlExpressions/InExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/InExpression.cs index af0777b47bb..c1338eba217 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/InExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/InExpression.cs @@ -143,8 +143,7 @@ protected override void Print(ExpressionPrinter expressionPrinter) expressionPrinter.Visit(Subquery); } } - else if (Values is SqlConstantExpression constantValuesExpression - && constantValuesExpression.Value is IEnumerable constantValues) + else if (Values is SqlConstantExpression { Value: IEnumerable constantValues } constantValuesExpression) { var first = true; foreach (var item in constantValues) diff --git a/src/EFCore.Relational/Query/SqlExpressions/RowValueExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/RowValueExpression.cs new file mode 100644 index 00000000000..27aaed47ba0 --- /dev/null +++ b/src/EFCore.Relational/Query/SqlExpressions/RowValueExpression.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Data; +using System.Runtime.CompilerServices; + +namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +/// +/// +/// An expression that represents a SQL row. +/// +/// +/// This type is typically used by database providers (and other extensions). It is generally +/// not used in application code. +/// +/// +public class RowValueExpression : SqlExpression +{ + /// + /// The values of this row. + /// + public virtual IReadOnlyList Values { get; } + + /// + /// Creates a new instance of the class. + /// + /// The values of this row. + /// The of the expression. Must be an . + public RowValueExpression(IReadOnlyList values, Type type) + : base(type, new RowValueTypeMapping(type)) + { + Check.NotEmpty(values, nameof(values)); + + if (!type.IsAssignableTo(typeof(ITuple))) + { + // TODO: Strings + throw new ArgumentException($"Type '{type}' isn't an ITuple", nameof(type)); + } + + Values = values; + } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + Check.NotNull(visitor, nameof(visitor)); + + SqlExpression[]? newValues = null; + + for (var i = 0; i < Values.Count; i++) + { + var value = Values[i]; + var visited = (SqlExpression)visitor.Visit(value); + if (visited != value && newValues is null) + { + newValues = new SqlExpression[Values.Count]; + for (var j = 0; j < i; j++) + { + newValues[j] = Values[j]; + } + } + + if (newValues is not null) + { + newValues[i] = visited; + } + } + + return newValues is null ? this : new RowValueExpression(newValues, Type); + } + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + public virtual RowValueExpression Update(IReadOnlyList values) + => values.Count == Values.Count && values.Zip(Values, (x, y) => (x, y)).All(tup => tup.x == tup.y) + ? this + : new RowValueExpression(values, Type); + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append("("); + + var count = Values.Count; + for (var i = 0; i < count; i++) + { + expressionPrinter.Visit(Values[i]); + + if (i < count - 1) + { + expressionPrinter.Append(", "); + } + } + + expressionPrinter.Append(")"); + } + + /// + public override bool Equals(object? obj) + => obj is RowValueExpression other && Equals(other); + + private bool Equals(RowValueExpression? other) + { + if (ReferenceEquals(this, other)) + { + return true; + } + + if (other is null || !base.Equals(other) || other.Values.Count != Values.Count) + { + return false; + } + + for (var i = 0; i < Values.Count; i++) + { + if (!other.Values[i].Equals(Values[i])) + { + return false; + } + } + + return true; + } + + /// + public override int GetHashCode() + { + var hashCode = new HashCode(); + + foreach (var value in Values) + { + hashCode.Add(value); + } + + return hashCode.ToHashCode(); + } + + private class RowValueTypeMapping : RelationalTypeMapping + { + public RowValueTypeMapping(Type clrType) + : base("", clrType) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => this; + } +} diff --git a/src/EFCore.Relational/Query/SqlExpressions/ScalarSubqueryExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/ScalarSubqueryExpression.cs index 9f639a10520..276b83ba84a 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/ScalarSubqueryExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/ScalarSubqueryExpression.cs @@ -19,7 +19,13 @@ public class ScalarSubqueryExpression : SqlExpression /// /// A subquery projecting single row with a single scalar projection. public ScalarSubqueryExpression(SelectExpression subquery) - : base(Verify(subquery).Projection[0].Type, subquery.Projection[0].Expression.TypeMapping) + : this(subquery, subquery.Projection[0].Expression.TypeMapping) + { + Subquery = subquery; + } + + private ScalarSubqueryExpression(SelectExpression subquery, RelationalTypeMapping? typeMapping) + : base(Verify(subquery).Projection[0].Type, typeMapping) { Subquery = subquery; } @@ -46,6 +52,15 @@ private static SelectExpression Verify(SelectExpression selectExpression) /// public virtual SelectExpression Subquery { get; } + /// + /// Applies supplied type mapping to this expression. + /// + /// A relational type mapping to apply. + /// A new expression which has supplied type mapping. + public SqlExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping) + // TODO: We should also replace the column in the subquery projection, but SelectExpression is too closed for that. + => new ScalarSubqueryExpression(Subquery, typeMapping); + /// protected override Expression VisitChildren(ExpressionVisitor visitor) => Update((SelectExpression)visitor.Visit(Subquery)); diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs index ec9dd339dd7..c8e73bc5fa5 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs @@ -604,7 +604,7 @@ public ConcreteColumnExpression( string name, TableReferenceExpression table, Type type, - RelationalTypeMapping typeMapping, + RelationalTypeMapping? typeMapping, bool nullable) : base(type, typeMapping) { @@ -628,7 +628,10 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) => this; public override ConcreteColumnExpression MakeNullable() - => IsNullable ? this : new ConcreteColumnExpression(Name, _table, Type, TypeMapping!, true); + => IsNullable ? this : new ConcreteColumnExpression(Name, _table, Type, TypeMapping, true); + + public override SqlExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping) + => new ConcreteColumnExpression(Name, _table, Type, typeMapping, IsNullable); public void UpdateTableReference(SelectExpression oldSelect, SelectExpression newSelect) => _table.UpdateTableReference(oldSelect, newSelect); diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index 1c0b5dc22b0..5892c0d2b26 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -103,7 +103,8 @@ private SelectExpression(string? alias) { } - internal SelectExpression(SqlExpression? projection) + // TODO: Currently hacked, clean it up + public SelectExpression(SqlExpression? projection) : base(null) { if (projection != null) @@ -112,14 +113,22 @@ internal SelectExpression(SqlExpression? projection) } } - internal SelectExpression(Type type, RelationalTypeMapping typeMapping, FromSqlExpression fromSqlExpression) + // TODO: Currently hacked, clean it up + public SelectExpression( + Type type, + RelationalTypeMapping? typeMapping, + TableExpressionBase tableExpression, + string? columnName = null, + bool? isColumnNullable = null) : base(null) { - var tableReferenceExpression = new TableReferenceExpression(this, fromSqlExpression.Alias!); - AddTable(fromSqlExpression, tableReferenceExpression); + columnName ??= SqlQuerySingleColumnAlias; + + var tableReferenceExpression = new TableReferenceExpression(this, tableExpression.Alias!); + AddTable(tableExpression, tableReferenceExpression); var columnExpression = new ConcreteColumnExpression( - SqlQuerySingleColumnAlias, tableReferenceExpression, type, typeMapping, type.IsNullableType()); + columnName, tableReferenceExpression, type.UnwrapNullableType(), typeMapping, isColumnNullable ?? type.IsNullableType()); _projectionMapping[new ProjectionMember()] = columnExpression; } @@ -831,8 +840,7 @@ public Expression ApplyProjection( containsCollection = true; } - if (sqe.ResultCardinality == ResultCardinality.Single - || sqe.ResultCardinality == ResultCardinality.SingleOrDefault) + if (sqe.ResultCardinality is ResultCardinality.Single or ResultCardinality.SingleOrDefault) { containsSingleResult = true; } @@ -945,15 +953,15 @@ Expression AddGroupByKeySelectorToProjection( return memberInitExpression.Update((NewExpression)updatedNewExpression, newBindings); - case UnaryExpression unaryExpression - when unaryExpression.NodeType == ExpressionType.Convert - || unaryExpression.NodeType == ExpressionType.ConvertChecked: + case UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unaryExpression: return unaryExpression.Update( AddGroupByKeySelectorToProjection( selectExpression, clientProjectionList, projectionBindingMap, unaryExpression.Operand)); - case EntityShaperExpression entityShaperExpression - when entityShaperExpression.ValueBufferExpression is EntityProjectionExpression entityProjectionExpression: + case EntityShaperExpression + { + ValueBufferExpression: EntityProjectionExpression entityProjectionExpression + } entityShaperExpression: { var clientProjectionToAdd = AddEntityProjection(entityProjectionExpression); var existingIndex = clientProjectionList.FindIndex( @@ -1097,9 +1105,10 @@ static void UpdateLimit(SelectExpression selectExpression) break; } - case ShapedQueryExpression shapedQueryExpression - when shapedQueryExpression.ResultCardinality == ResultCardinality.Single - || shapedQueryExpression.ResultCardinality == ResultCardinality.SingleOrDefault: + case ShapedQueryExpression + { + ResultCardinality: ResultCardinality.Single or ResultCardinality.SingleOrDefault + } shapedQueryExpression: { var innerSelectExpression = (SelectExpression)shapedQueryExpression.QueryExpression; var innerShaperExpression = shapedQueryExpression.ShaperExpression; @@ -1113,8 +1122,7 @@ static void UpdateLimit(SelectExpression selectExpression) } var innerExpression = RemoveConvert(innerShaperExpression); - if (!(innerExpression is EntityShaperExpression - || innerExpression is IncludeExpression)) + if (innerExpression is not EntityShaperExpression and not IncludeExpression) { var sentinelExpression = innerSelectExpression.Limit!; var sentinelNullableType = sentinelExpression.Type.MakeNullable(); @@ -1164,14 +1172,12 @@ static void UpdateLimit(SelectExpression selectExpression) break; static Expression RemoveConvert(Expression expression) - => expression is UnaryExpression unaryExpression - && unaryExpression.NodeType == ExpressionType.Convert - ? RemoveConvert(unaryExpression.Operand) - : expression; + => expression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression + ? RemoveConvert(unaryExpression.Operand) + : expression; } - case ShapedQueryExpression shapedQueryExpression - when shapedQueryExpression.ResultCardinality == ResultCardinality.Enumerable: + case ShapedQueryExpression { ResultCardinality: ResultCardinality.Enumerable } shapedQueryExpression: { var innerSelectExpression = (SelectExpression)shapedQueryExpression.QueryExpression; if (_identifier.Count == 0 @@ -1747,9 +1753,7 @@ public void ReplaceProjection(IReadOnlyDictionary foreach (var (projectionMember, expression) in projectionMapping) { Check.DebugAssert( - expression is SqlExpression - || expression is EntityProjectionExpression - || expression is JsonQueryExpression, + expression is SqlExpression or EntityProjectionExpression or JsonQueryExpression, "Invalid operation in the projection."); _projectionMapping[projectionMember] = expression; } @@ -1767,10 +1771,7 @@ public void ReplaceProjection(IReadOnlyList clientProjections) foreach (var expression in clientProjections) { Check.DebugAssert( - expression is SqlExpression - || expression is EntityProjectionExpression - || expression is ShapedQueryExpression - || expression is JsonQueryExpression, + expression is SqlExpression or EntityProjectionExpression or ShapedQueryExpression or JsonQueryExpression, "Invalid operation in the projection."); _clientProjections.Add(expression); _aliasForClientProjections.Add(null); @@ -2987,8 +2988,7 @@ private void AddJoin( { innerPushdownOccurred = false; // Try to convert Apply to normal join - if (joinType == JoinType.CrossApply - || joinType == JoinType.OuterApply) + if (joinType is JoinType.CrossApply or JoinType.OuterApply) { var limit = innerSelectExpression.Limit; var offset = innerSelectExpression.Offset; @@ -3128,8 +3128,7 @@ private void AddJoin( if (_identifier.Count > 0 && innerSelectExpression._identifier.Count > 0) { - if (joinType == JoinType.LeftJoin - || joinType == JoinType.OuterApply) + if (joinType is JoinType.LeftJoin or JoinType.OuterApply) { _identifier.AddRange(innerSelectExpression._identifier.Select(e => (e.Column.MakeNullable(), e.Comparer))); } diff --git a/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs index 8540a8646ac..b84f92c5005 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs @@ -17,7 +17,7 @@ public sealed class SqlParameterExpression : SqlExpression private readonly ParameterExpression _parameterExpression; private readonly string _name; - internal SqlParameterExpression(ParameterExpression parameterExpression, RelationalTypeMapping? typeMapping) + public SqlParameterExpression(ParameterExpression parameterExpression, RelationalTypeMapping? typeMapping) : base(parameterExpression.Type.UnwrapNullableType(), typeMapping) { Check.DebugAssert(parameterExpression.Name != null, "Parameter must have name."); diff --git a/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs index 888e1bbe5ac..92acaa10620 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs @@ -35,9 +35,29 @@ private TableValuedFunctionExpression( IStoreFunction storeFunction, IReadOnlyList arguments, IEnumerable? annotations) + : this(alias, storeFunction.Name, storeFunction.Schema, storeFunction.IsBuiltIn, arguments, annotations) + { + _table = storeFunction; + Name = storeFunction.Name; + Schema = storeFunction.Schema; + IsBuiltIn = storeFunction.IsBuiltIn; + Arguments = arguments; + } + + // TODO: Don't expose alias parameter + // TODO: Understand exactly what we need to do with _table + public TableValuedFunctionExpression( + string alias, + string name, + string? schema, + bool builtIn, + IReadOnlyList arguments, + IEnumerable? annotations = null) : base(alias, annotations) { - StoreFunction = storeFunction; + Name = name; + Schema = schema; + IsBuiltIn = builtIn; Arguments = arguments; } @@ -52,18 +72,33 @@ public override string? Alias } /// - /// The store function. + /// The name of the table or view. + /// + public string Name { get; } + + /// + /// The schema of the table or view. /// - public virtual IStoreFunction StoreFunction { get; } + public string? Schema { get; } + + /// + /// Gets the value indicating whether the database function is built-in. + /// + public bool IsBuiltIn { get; } /// /// The list of arguments of this function. /// public virtual IReadOnlyList Arguments { get; } + // TODO. The Table is only actually needed when this is the first table in a SelectExpression for an entity type; this is never our case. /// - ITableBase ITableBasedExpression.Table - => StoreFunction; + ITableBase ITableBasedExpression.Table => _table!; + + private readonly ITableBase? _table; + + // ITableBase ITableBasedExpression.Table + // => StoreFunction; /// protected override Expression VisitChildren(ExpressionVisitor visitor) @@ -77,7 +112,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) } return changed - ? new TableValuedFunctionExpression(Alias, StoreFunction, arguments, GetAnnotations()) + ? new TableValuedFunctionExpression(Alias, Name, Schema, IsBuiltIn, arguments, GetAnnotations()) : this; } @@ -89,22 +124,22 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// This expression if no children changed, or an expression with the updated children. public virtual TableValuedFunctionExpression Update(IReadOnlyList arguments) => !arguments.SequenceEqual(Arguments) - ? new TableValuedFunctionExpression(Alias, StoreFunction, arguments, GetAnnotations()) + ? new TableValuedFunctionExpression(Alias, Name, Schema, IsBuiltIn, arguments, GetAnnotations()) : this; /// protected override TableExpressionBase CreateWithAnnotations(IEnumerable annotations) - => new TableValuedFunctionExpression(Alias, StoreFunction, Arguments, annotations); + => new TableValuedFunctionExpression(Alias, Name, Schema, IsBuiltIn, Arguments, annotations); /// protected override void Print(ExpressionPrinter expressionPrinter) { - if (!string.IsNullOrEmpty(StoreFunction.Schema)) + if (!string.IsNullOrEmpty(Schema)) { - expressionPrinter.Append(StoreFunction.Schema).Append("."); + expressionPrinter.Append(Schema).Append("."); } - expressionPrinter.Append(StoreFunction.Name); + expressionPrinter.Append(Name); expressionPrinter.Append("("); expressionPrinter.VisitCollection(Arguments); expressionPrinter.Append(")"); @@ -122,15 +157,19 @@ public override bool Equals(object? obj) private bool Equals(TableValuedFunctionExpression tableValuedFunctionExpression) => base.Equals(tableValuedFunctionExpression) - && StoreFunction == tableValuedFunctionExpression.StoreFunction - && Arguments.SequenceEqual(tableValuedFunctionExpression.Arguments); + && Name == tableValuedFunctionExpression.Name + && Schema == tableValuedFunctionExpression.Schema + && IsBuiltIn == tableValuedFunctionExpression.IsBuiltIn + && Arguments.SequenceEqual(tableValuedFunctionExpression.Arguments); /// public override int GetHashCode() { var hash = new HashCode(); hash.Add(base.GetHashCode()); - hash.Add(StoreFunction); + hash.Add(Name); + hash.Add(Schema); + hash.Add(IsBuiltIn); for (var i = 0; i < Arguments.Count; i++) { hash.Add(Arguments[i]); diff --git a/src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs new file mode 100644 index 00000000000..fc0a2f9f725 --- /dev/null +++ b/src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +/// +/// +/// An expression that represents a constant table in SQL, sometimes known as a table value constructor. +/// +/// +/// This type is typically used by database providers (and other extensions). It is generally +/// not used in application code. +/// +/// +public class ValuesExpression : TableExpressionBase +{ + /// + /// The row values for this table. + /// + public virtual IReadOnlyList RowValues { get; } + + /// + /// The names of the columns contained in this table. + /// + public virtual IReadOnlyList ColumnNames { get; } + + /// + /// Creates a new instance of the class. + /// + /// A string alias for the table source. + /// The row values for this table. + /// The names of the columns contained in this table. + /// A collection of annotations associated with this expression. + public ValuesExpression( + string? alias, + IReadOnlyList rowValues, + IReadOnlyList columnNames, + IEnumerable? annotations = null) + : base(alias, annotations) + { + Check.NotEmpty(rowValues, nameof(rowValues)); + +#if DEBUG + if (rowValues.Any(rv => rv.Values.Count != columnNames.Count)) + { + throw new ArgumentException("All number of all row values doesn't match the number of column names"); + } + + if (rowValues.SelectMany(rv => rv.Values).Any( + v => v is not SqlConstantExpression and not SqlUnaryExpression + { + Operand: SqlConstantExpression, OperatorType: ExpressionType.Convert + })) + { + throw new ArgumentException("Only constant expressions are supported in ValuesExpression"); + } +#endif + + RowValues = rowValues; + ColumnNames = columnNames; + } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + Check.NotNull(visitor, nameof(visitor)); + + RowValueExpression[]? newRowValues = null; + + for (var i = 0; i < RowValues.Count; i++) + { + var rowValue = RowValues[i]; + var visited = (RowValueExpression)visitor.Visit(rowValue); + if (visited != rowValue && newRowValues is null) + { + newRowValues = new RowValueExpression[RowValues.Count]; + for (var j = 0; j < i; j++) + { + newRowValues[j] = RowValues[j]; + } + } + + if (newRowValues is not null) + { + newRowValues[i] = visited; + } + } + + return newRowValues is null ? this : new ValuesExpression(Alias, newRowValues, ColumnNames); + } + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + public virtual ValuesExpression Update(IReadOnlyList rowValues) + => rowValues.Count == RowValues.Count && rowValues.Zip(RowValues, (x, y) => (x, y)).All(tup => tup.x == tup.y) + ? this + : new ValuesExpression(Alias, rowValues, ColumnNames); + + /// + protected override TableExpressionBase CreateWithAnnotations(IEnumerable annotations) + => new ValuesExpression(Alias, RowValues, ColumnNames, annotations); + + /// + /// Creates a printable string representation of the given expression using . + /// + /// The expression printer to use. + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append("VALUES ("); + + var count = RowValues.Count; + for (var i = 0; i < count; i++) + { + expressionPrinter.Visit(RowValues[i]); + + if (i < count - 1) + { + expressionPrinter.Append(", "); + } + } + + expressionPrinter.Append(")"); + } + + /// + public override bool Equals(object? obj) + => obj is ValuesExpression other && Equals(other); + + private bool Equals(ValuesExpression? other) + { + if (ReferenceEquals(this, other)) + { + return true; + } + + if (other is null || !base.Equals(other) || other.RowValues.Count != RowValues.Count) + { + return false; + } + + for (var i = 0; i < RowValues.Count; i++) + { + if (!other.RowValues[i].Equals(RowValues[i])) + { + return false; + } + } + + return true; + } + + /// + public override int GetHashCode() + { + var hashCode = new HashCode(); + + foreach (var rowValue in RowValues) + { + hashCode.Add(rowValue); + } + + return hashCode.ToHashCode(); + } +} diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index 1b413d0faa2..4bf770e9105 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -185,6 +185,18 @@ protected virtual TableExpressionBase Visit(TableExpressionBase tableExpressionB case OuterApplyExpression outerApplyExpression: return outerApplyExpression.Update(Visit(outerApplyExpression.Table)); + case ValuesExpression valuesExpression: + { + // TODO: Optimize to not allocate if nothing changes, also TableValuedFunctionExpression below + var rowValues = new List(); + foreach (var rowValue in valuesExpression.RowValues) + { + rowValues.Add((RowValueExpression)VisitRowValue(rowValue, allowOptimizedExpansion: false, out _)); + } + + return valuesExpression.Update(rowValues); + } + case SelectExpression selectExpression: return Visit(selectExpression); @@ -403,6 +415,8 @@ LikeExpression likeExpression => VisitLike(likeExpression, allowOptimizedExpansion, out nullable), RowNumberExpression rowNumberExpression => VisitRowNumber(rowNumberExpression, allowOptimizedExpansion, out nullable), + RowValueExpression rowValueExpression + => VisitRowValue(rowValueExpression, allowOptimizedExpansion, out nullable), ScalarSubqueryExpression scalarSubqueryExpression => VisitScalarSubquery(scalarSubqueryExpression, allowOptimizedExpansion, out nullable), SqlBinaryExpression sqlBinaryExpression @@ -605,7 +619,7 @@ protected virtual SqlExpression VisitExists( nullable = false; // if subquery has predicate which evaluates to false, we can simply return false - // if the exisits is negated we need to return true instead + // if the exists is negated we need to return true instead return TryGetBoolConstantValue(subquery.Predicate) == false ? _sqlExpressionFactory.Constant(existsExpression.IsNegated, existsExpression.TypeMapping) : existsExpression.Update(subquery); @@ -826,6 +840,47 @@ protected virtual SqlExpression VisitRowNumber( : rowNumberExpression; } + /// + /// Visits a and computes its nullability. + /// + /// A row value expression to visit. + /// A bool value indicating if optimized expansion which considers null value as false value is allowed. + /// A bool value indicating whether the sql expression is nullable. + /// An optimized sql expression. + protected virtual SqlExpression VisitRowValue( + RowValueExpression rowValueExpression, + bool allowOptimizedExpansion, + out bool nullable) + { + SqlExpression[]? newValues = null; + + for (var i = 0; i < rowValueExpression.Values.Count; i++) + { + var value = rowValueExpression.Values[i]; + + // Note that we disallow optimized expansion, since the null vs. false distinction does matter inside the row's values + var newValue = Visit(value, allowOptimizedExpansion: false, out _); + if (newValue != value && newValues is null) + { + newValues = new SqlExpression[rowValueExpression.Values.Count]; + for (var j = 0; j < i; j++) + { + newValues[j] = rowValueExpression.Values[j]; + } + } + + if (newValues is not null) + { + newValues[i] = newValue; + } + } + + // The row value expression itself can never be null + nullable = false; + + return rowValueExpression.Update(newValues ?? rowValueExpression.Values); + } + /// /// Visits a and computes its nullability. /// diff --git a/src/EFCore.Relational/Storage/ValueConversion/JsonStringConverter.cs b/src/EFCore.Relational/Storage/ValueConversion/JsonStringConverter.cs new file mode 100644 index 00000000000..b00ba4526b9 --- /dev/null +++ b/src/EFCore.Relational/Storage/ValueConversion/JsonStringConverter.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +// TODO: Nulls? Mapping hints? Customizable JsonSerializerOptions? +public class RelationalJsonStringConverter : ValueConverter +{ + private readonly RelationalTypeMapping _elementTypeMapping; + + public RelationalJsonStringConverter(Type modelClrType, RelationalTypeMapping elementTypeMapping) + : base( + (Expression>)(x => JsonSerializer.Serialize(x, (JsonSerializerOptions?)null)), + (Expression>)(s => JsonSerializer.Deserialize(s, modelClrType, (JsonSerializerOptions?)null)!)) // TODO: Nullability + { + ModelClrType = modelClrType; + _elementTypeMapping = elementTypeMapping; + + // TODO: Perform serialization via the element type mapping once we add those hooks + // TODO: Do we support value conversion on the *element* type mapping? That would mean another special API to configure that in + // metadata, and we'd need to apply it here for every element. Otherwise, only array-level converters would be supported, and we + // only see the already-converted values here. + // TODO: Full sanitization/nullability + ConvertToProvider = x => JsonSerializer.Serialize(x); + ConvertFromProvider = o + => o is string s + ? JsonSerializer.Deserialize(s, modelClrType)! + : throw new ArgumentException(); // TODO + } + + public override Func ConvertToProvider { get; } + public override Func ConvertFromProvider { get; } + + public override Type ModelClrType { get; } + + public override Type ProviderClrType + => typeof(string); + + // public static ValueConverterInfo DefaultInfo { get; } + // = new(typeof(PhysicalAddress), typeof(string), i => new PhysicalAddressToStringConverter(i.MappingHints), DefaultHints); +} + +// // TODO: Nulls? Mapping hints? Customizable JsonSerializerOptions? +// public class JsonStringConverter : ValueConverter, IJsonStringConverter +// { +// public JsonStringConverter() +// : base( +// m => JsonSerializer.Serialize(m, (JsonSerializerOptions?)null), +// s => JsonSerializer.Deserialize(s, (JsonSerializerOptions?)null)!) // TODO: Nullability +// { +// } +// +// public static ValueConverterInfo DefaultInfo { get; } +// = new(typeof(TModel), typeof(string), _ => new JsonStringConverter()); +// } diff --git a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs index a994c313168..d1d5024d23b 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs @@ -126,6 +126,7 @@ public static IServiceCollection AddEntityFrameworkSqlServer(this IServiceCollec .TryAdd() .TryAdd() .TryAdd() + .TryAdd() .TryAdd() .TryAdd(p => p.GetRequiredService()) .TryAddProviderSpecificServices( diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs index 3e0407691a0..a093a3f101e 100644 --- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs @@ -608,6 +608,15 @@ protected override Expression VisitInnerJoin(InnerJoinExpression innerJoinExpres return innerJoinExpression.Update(table, joinPredicate); } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression) + => ApplyConversion(jsonScalarExpression, condition: false); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -670,6 +679,27 @@ protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpres return ApplyConversion(rowNumberExpression.Update(partitions, orderings), condition: false); } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitRowValue(RowValueExpression rowValueExpression) + { + var parentSearchCondition = _isSearchCondition; + _isSearchCondition = false; + + var values = new SqlExpression[rowValueExpression.Values.Count]; + for (var i = 0; i < values.Length; i++) + { + values[i] = (SqlExpression)Visit(rowValueExpression.Values[i]); + } + + _isSearchCondition = parentSearchCondition; + return rowValueExpression.Update(values); + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -763,6 +793,18 @@ protected override Expression VisitUpdate(UpdateExpression updateExpression) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression) - => ApplyConversion(jsonScalarExpression, condition: false); + protected override Expression VisitValues(ValuesExpression valuesExpression) + { + var parentSearchCondition = _isSearchCondition; + _isSearchCondition = false; + + var rowValues = new RowValueExpression[valuesExpression.RowValues.Count]; + for (var i = 0; i < rowValues.Length; i++) + { + rowValues[i] = (RowValueExpression)Visit(valuesExpression.RowValues[i]); + } + + _isSearchCondition = parentSearchCondition; + return valuesExpression.Update(rowValues); + } } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerNavigationExpansionExtensibilityHelper.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerNavigationExpansionExtensibilityHelper.cs index 0e7698527cd..cfedfccc353 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerNavigationExpansionExtensibilityHelper.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerNavigationExpansionExtensibilityHelper.cs @@ -11,7 +11,8 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// -public class SqlServerNavigationExpansionExtensibilityHelper : NavigationExpansionExtensibilityHelper +public class SqlServerNavigationExpansionExtensibilityHelper + : NavigationExpansionExtensibilityHelper, INavigationExpansionExtensibilityHelper { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs new file mode 100644 index 00000000000..8cbd124f939 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs @@ -0,0 +1,156 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; + +/// +/// An expression that represents a SQL Server OPENJSON function call in a SQL tree. +/// +/// +/// +/// See OPENJSON (Transact-SQL) for more +/// information and examples. +/// +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +/// +public class SqlServerOpenJsonExpression : TableValuedFunctionExpression +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual SqlExpression JsonExpression + => Arguments[0]; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual SqlExpression? Path + => Arguments.Count == 1 ? null : Arguments[1]; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IReadOnlyList? ColumnInfos { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqlServerOpenJsonExpression( + SqlExpression jsonExpression, + SqlExpression? path = null, + IReadOnlyList? columnInfos = null) + : this("j", jsonExpression, path, columnInfos) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqlServerOpenJsonExpression( + string alias, + SqlExpression jsonExpression, + SqlExpression? path = null, + IReadOnlyList? columnInfos = null) + : base(alias, "OpenJson", schema: null, builtIn: true, path is null ? new[] { jsonExpression } : new[] { jsonExpression, path }) + { + ColumnInfos = columnInfos; + } + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + if (!string.IsNullOrEmpty(Schema)) + { + expressionPrinter.Append(Schema).Append("."); + } + + expressionPrinter.Append(Name); + expressionPrinter.Append("("); + expressionPrinter.VisitCollection(Arguments); + expressionPrinter.Append(")"); + + if (ColumnInfos is not null) + { + expressionPrinter.Append(" WITH ("); + + for (var i = 0; i < ColumnInfos.Count; i++) + { + var columnInfo = ColumnInfos[i]; + + if (i > 0) + { + expressionPrinter.Append(", "); + } + + expressionPrinter + .Append(columnInfo.Name) + .Append(" ") + .Append(columnInfo.StoreType ?? ""); + + if (columnInfo.Path is not null) + { + expressionPrinter.Append(" ").Append("'" + columnInfo.Path + "'"); + } + + if (columnInfo.AsJson) + { + expressionPrinter.Append(" AS JSON"); + } + } + + expressionPrinter.Append(")"); + } + + PrintAnnotations(expressionPrinter); + expressionPrinter.Append(" AS "); + expressionPrinter.Append(Alias); + } + + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is SqlServerOpenJsonExpression openJsonExpression + && Equals(openJsonExpression)); + + private bool Equals(SqlServerOpenJsonExpression openJsonExpression) + => base.Equals(openJsonExpression) + && JsonExpression.Equals(openJsonExpression.JsonExpression) + && (ColumnInfos is null + ? openJsonExpression.ColumnInfos is null + : openJsonExpression.ColumnInfos is not null && ColumnInfos.SequenceEqual(openJsonExpression.ColumnInfos)); + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), JsonExpression, Path); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public readonly record struct ColumnInfo(string Name, string? StoreType, string? Path = null, bool AsJson = false); +} diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs index 3f7fd0bc9eb..6031b81c2b5 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs @@ -16,6 +16,7 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; public class SqlServerQuerySqlGenerator : QuerySqlGenerator { private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly ISqlGenerationHelper _sqlGenerationHelper; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -29,8 +30,22 @@ public SqlServerQuerySqlGenerator( : base(dependencies) { _typeMappingSource = typeMappingSource; + _sqlGenerationHelper = dependencies.SqlGenerationHelper; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override bool TryGenerateWithoutWrappingSelect(SelectExpression selectExpression) + // SQL Server doesn't support VALUES as a top-level statement, so we need to wrap the VALUES in a SELECT: + // SELECT 1 UNION VALUES (2), (3) -- simple + // SELECT 1 AS x UNION SELECT * FROM (VALUES (2), (3)) AS f(x) -- SQL Server + => selectExpression.Tables is not [ValuesExpression] + && base.TryGenerateWithoutWrappingSelect(selectExpression); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -138,6 +153,62 @@ protected override Expression VisitUpdate(UpdateExpression updateExpression) RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteUpdate))); } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override void GenerateValues(ValuesExpression valuesExpression, bool withParentheses = true) + { + // Unlike other database, SQL Server doesn't support top-level VALUES + Check.DebugAssert(valuesExpression.Alias is not null, "ValuesExpression without alias"); + + // SQL Server supports providing the names of columns projected out of VALUES: (VALUES (1, 3), (2, 4)) AS x(a, b). + // But since other databases sometimes don't, the default relational implementation is complex, involving a SELECT for the first row + // and a UNION All on the rest. Override to do the nice simple thing. + if (withParentheses) + { + Sql.Append("("); + } + + var rowValues = valuesExpression.RowValues; + + Sql.Append("VALUES "); + + for (var i = 0; i < rowValues.Count; i++) + { + // TODO: Do we want newlines here? + if (i > 0) + { + Sql.Append(", "); + } + + Visit(valuesExpression.RowValues[i]); + } + + if (withParentheses) + { + Sql + .Append(")") + .Append(AliasSeparator) + .Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.Alias)) + .Append("("); + + for (var i = 0; i < valuesExpression.ColumnNames.Count; i++) + { + if (i > 0) + { + Sql.Append(", "); + } + + Sql.Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.ColumnNames[i])); + } + + Sql.Append(")"); + } + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -381,6 +452,71 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp return jsonScalarExpression; } + /// + protected override Expression VisitTableValuedFunction(TableValuedFunctionExpression tableValuedFunctionExpression) + => tableValuedFunctionExpression is SqlServerOpenJsonExpression openJsonExpression + ? VisitOpenJsonExpression(openJsonExpression) + : base.VisitTableValuedFunction(tableValuedFunctionExpression); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected virtual Expression VisitOpenJsonExpression(SqlServerOpenJsonExpression openJsonExpression) + { + // OPENJSON docs: https://learn.microsoft.com/sql/t-sql/functions/openjson-transact-sql + + // OPENJSON is a regular table-valued function with a special WITH clause at the end + // Copy-paste from VisitTableValuedFunction, because that appends the 'AS ' but we need to insert WITH before that + Sql.Append("OpenJson("); + + GenerateList(openJsonExpression.Arguments, e => Visit(e)); + + Sql.Append(")"); + + if (openJsonExpression.ColumnInfos is not null) + { + Sql.Append(" WITH ("); + + for (var i = 0; i < openJsonExpression.ColumnInfos.Count; i++) + { + var columnInfo = openJsonExpression.ColumnInfos[i]; + + if (i > 0) + { + Sql.Append(", "); + } + + Check.DebugAssert(columnInfo.StoreType is not null, "Unset OpenJson column store type"); + + Sql + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(columnInfo.Name)) + .Append(" ") + .Append(columnInfo.StoreType); + + if (columnInfo.Path is not null) + { + Sql + .Append(" ") + .Append(_typeMappingSource.GetMapping("varchar(max)").GenerateSqlLiteral(columnInfo.Path)); + } + + if (columnInfo.AsJson) + { + Sql.Append(" AS JSON"); + } + } + + Sql.Append(")"); + } + + Sql.Append(AliasSeparator).Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(openJsonExpression.Alias)); + + return openJsonExpression; + } + /// protected override void CheckComposableSqlTrimmed(ReadOnlySpan sql) { diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessor.cs new file mode 100644 index 00000000000..5dc9076321a --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessor.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class SqlServerQueryTranslationPreprocessor : RelationalQueryTranslationPreprocessor +{ + /// + /// Creates a new instance of the class. + /// + /// Parameter object containing dependencies for this class. + /// Parameter object containing relational dependencies for this class. + /// The query compilation context object to use. + public SqlServerQueryTranslationPreprocessor( + QueryTranslationPreprocessorDependencies dependencies, + RelationalQueryTranslationPreprocessorDependencies relationalDependencies, + QueryCompilationContext queryCompilationContext) + : base(dependencies, relationalDependencies, queryCompilationContext) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override Expression ProcessQueryRoots(Expression expression) + => new SqlServerQueryRootProcessor(RelationalDependencies.RelationalTypeMappingSource, QueryCompilationContext.Model) + .Visit(expression); +} diff --git a/src/EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessorFactory.cs similarity index 53% rename from src/EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs rename to src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessorFactory.cs index 7a433e8c5c5..c113445bab7 100644 --- a/src/EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessorFactory.cs @@ -1,7 +1,7 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.EntityFrameworkCore.Query.Internal; +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -9,9 +9,9 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal; /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// -public class TableValuedFunctionToQueryRootConvertingExpressionVisitor : ExpressionVisitor +public class SqlServerQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory { - private readonly IModel _model; + private readonly ITypeMappingSource _typeMappingSource; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -19,29 +19,32 @@ public class TableValuedFunctionToQueryRootConvertingExpressionVisitor : Express /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public TableValuedFunctionToQueryRootConvertingExpressionVisitor(IModel model) + public SqlServerQueryTranslationPreprocessorFactory( + QueryTranslationPreprocessorDependencies dependencies, + RelationalQueryTranslationPreprocessorDependencies relationalDependencies, + ITypeMappingSource typeMappingSource) { - _model = model; + Dependencies = dependencies; + RelationalDependencies = relationalDependencies; + _typeMappingSource = typeMappingSource; } + /// + /// Dependencies for this service. + /// + protected virtual QueryTranslationPreprocessorDependencies Dependencies { get; } + + /// + /// Relational provider-specific dependencies for this service. + /// + protected virtual RelationalQueryTranslationPreprocessorDependencies RelationalDependencies { get; } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) - { - var function = _model.FindDbFunction(methodCallExpression.Method); - - return function?.IsScalar == false - ? CreateTableValuedFunctionQueryRootExpression(function.StoreFunction, methodCallExpression.Arguments) - : base.VisitMethodCall(methodCallExpression); - } - - private static Expression CreateTableValuedFunctionQueryRootExpression( - IStoreFunction function, - IReadOnlyCollection arguments) - // See issue #19970 - => new TableValuedFunctionQueryRootExpression(function.EntityTypeMappings.Single().EntityType, function, arguments); + public virtual QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext) + => new SqlServerQueryTranslationPreprocessor(Dependencies, RelationalDependencies, queryCompilationContext); } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs index ea225751457..c9bd2e4aced 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; @@ -15,6 +16,10 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; /// public class SqlServerQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor { + private readonly QueryCompilationContext _queryCompilationContext; + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly ISqlExpressionFactory _sqlExpressionFactory; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -27,6 +32,9 @@ public SqlServerQueryableMethodTranslatingExpressionVisitor( QueryCompilationContext queryCompilationContext) : base(dependencies, relationalDependencies, queryCompilationContext) { + _queryCompilationContext = queryCompilationContext; + _typeMappingSource = relationalDependencies.TypeMappingSource; + _sqlExpressionFactory = relationalDependencies.SqlExpressionFactory; } /// @@ -39,6 +47,9 @@ protected SqlServerQueryableMethodTranslatingExpressionVisitor( SqlServerQueryableMethodTranslatingExpressionVisitor parentVisitor) : base(parentVisitor) { + _queryCompilationContext = parentVisitor._queryCompilationContext; + _typeMappingSource = parentVisitor._typeMappingSource; + _sqlExpressionFactory = parentVisitor._sqlExpressionFactory; } /// @@ -58,48 +69,124 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis /// protected override Expression VisitExtension(Expression extensionExpression) { - if (extensionExpression is TemporalQueryRootExpression queryRootExpression) + switch (extensionExpression) { - var selectExpression = RelationalDependencies.SqlExpressionFactory.Select(queryRootExpression.EntityType); - Func annotationApplyingFunc = queryRootExpression switch + case TemporalQueryRootExpression queryRootExpression: { - TemporalAllQueryRootExpression => te => te - .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.All), - TemporalAsOfQueryRootExpression asOf => te => te - .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.AsOf) - .AddAnnotation(SqlServerAnnotationNames.TemporalAsOfPointInTime, asOf.PointInTime), - TemporalBetweenQueryRootExpression between => te => te - .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.Between) - .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationFrom, between.From) - .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationTo, between.To), - TemporalContainedInQueryRootExpression containedIn => te => te - .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.ContainedIn) - .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationFrom, containedIn.From) - .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationTo, containedIn.To), - TemporalFromToQueryRootExpression fromTo => te => te - .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.FromTo) - .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationFrom, fromTo.From) - .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationTo, fromTo.To), - _ => throw new InvalidOperationException(queryRootExpression.Print()), - }; + var selectExpression = RelationalDependencies.SqlExpressionFactory.Select(queryRootExpression.EntityType); + Func annotationApplyingFunc = queryRootExpression switch + { + TemporalAllQueryRootExpression => te => te + .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.All), + TemporalAsOfQueryRootExpression asOf => te => te + .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.AsOf) + .AddAnnotation(SqlServerAnnotationNames.TemporalAsOfPointInTime, asOf.PointInTime), + TemporalBetweenQueryRootExpression between => te => te + .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.Between) + .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationFrom, between.From) + .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationTo, between.To), + TemporalContainedInQueryRootExpression containedIn => te => te + .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.ContainedIn) + .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationFrom, containedIn.From) + .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationTo, containedIn.To), + TemporalFromToQueryRootExpression fromTo => te => te + .AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.FromTo) + .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationFrom, fromTo.From) + .AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationTo, fromTo.To), + _ => throw new InvalidOperationException(queryRootExpression.Print()), + }; + + selectExpression = (SelectExpression)new TemporalAnnotationApplyingExpressionVisitor(annotationApplyingFunc) + .Visit(selectExpression); - selectExpression = (SelectExpression)new TemporalAnnotationApplyingExpressionVisitor(annotationApplyingFunc) - .Visit(selectExpression); - - return new ShapedQueryExpression( - selectExpression, - new RelationalEntityShaperExpression( - queryRootExpression.EntityType, - new ProjectionBindingExpression( - selectExpression, - new ProjectionMember(), - typeof(ValueBuffer)), - false)); + return new ShapedQueryExpression( + selectExpression, + new RelationalEntityShaperExpression( + queryRootExpression.EntityType, + new ProjectionBindingExpression( + selectExpression, + new ProjectionMember(), + typeof(ValueBuffer)), + false)); + } + + default: + return base.VisitExtension(extensionExpression); + } + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override ShapedQueryExpression? VisitPrimitiveCollection(Expression primitiveCollectionExpression) + { + var elementClrType = primitiveCollectionExpression.Type.GetSequenceType(); + + SqlExpression sqlExpression; + RelationalTypeMapping? elementTypeMapping = null; + + switch (primitiveCollectionExpression) + { + case ColumnExpression + { + TypeMapping: SqlServerStringTypeMapping + { + Converter: RelationalJsonStringConverter, + ElementTypeMapping: RelationalTypeMapping e + } + } column: + elementTypeMapping = e; + sqlExpression = column; + break; + + case ParameterQueryRootExpression parameterQueryRoot: + elementClrType = parameterQueryRoot.ElementType; + + // TODO: Hack until I finish proper type mapping inference on the parameter. We should pass null here as the parameter's + // type mapping, to allow it to properly get inferred later. + var typeMapping = _typeMappingSource.FindMapping(parameterQueryRoot.ParameterExpression.Type); + sqlExpression = new SqlParameterExpression(parameterQueryRoot.ParameterExpression, typeMapping /* null */); + break; + + default: + return null; + } + + var translation = new SqlServerOpenJsonExpression( + sqlExpression, + columnInfos: new[] { new SqlServerOpenJsonExpression.ColumnInfo("Value", elementTypeMapping?.StoreType, "$") }); + + if (elementTypeMapping is null) + { + AddUntypedColumnExpression(translation); } - return base.VisitExtension(extensionExpression); + // TODO: When we have metadata to determine if the element is nullable, pass that here to SelectExpression + var selectExpression = new SelectExpression(elementClrType, elementTypeMapping, translation); + + Expression shaperExpression = new ProjectionBindingExpression( + selectExpression, new ProjectionMember(), elementClrType.MakeNullable()); + + if (elementClrType != shaperExpression.Type) + { + Check.DebugAssert( + elementClrType.MakeNullable() == shaperExpression.Type, + "expression.Type must be nullable of targetType"); + + shaperExpression = Expression.Convert(shaperExpression, elementClrType); + } + + return new ShapedQueryExpression(selectExpression, shaperExpression); } + protected override Expression ProcessTypeMappings( + Expression expression, + Dictionary inferredTypeMappings) + => new SqlServerTypeMappingProcessor(inferredTypeMappings).Visit(expression); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -206,4 +293,67 @@ public TemporalAnnotationApplyingExpressionVisitor(Func + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected class SqlServerTypeMappingProcessor : TypeMappingProcessor + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqlServerTypeMappingProcessor( + Dictionary inferredTypeMappings) + : base(inferredTypeMappings) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitExtension(Expression expression) + => expression switch + { + SqlServerOpenJsonExpression openJsonExpression + when InferredTypeMappings.TryGetValue(openJsonExpression, out var typeMapping) + && typeMapping is not null + => ApplyTypeMappingsOnOpenJsonExpression(openJsonExpression, new[] { typeMapping }), + + _ => base.VisitExtension(expression) + }; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual SqlServerOpenJsonExpression ApplyTypeMappingsOnOpenJsonExpression( + SqlServerOpenJsonExpression openJsonExpression, + IReadOnlyList typeMappings) + { + Check.DebugAssert(openJsonExpression.ColumnInfos is not null, "ColumnInfos is not null"); + + // TODO: multiple column support (tuples) + Check.DebugAssert(openJsonExpression.ColumnInfos.Count == 1, "ColumnInfos.Count == 1"); + var oldColumnInfo = openJsonExpression.ColumnInfos[0]; + + Check.DebugAssert(typeMappings.Count == 1, "typeMappings.Count == 1"); + + return new SqlServerOpenJsonExpression( + openJsonExpression.Alias, + openJsonExpression.JsonExpression, + openJsonExpression.Path, + new[] { new SqlServerOpenJsonExpression.ColumnInfo(oldColumnInfo.Name, typeMappings[0].StoreType, oldColumnInfo.Path) }); + } + } } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlExpressionFactory.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlExpressionFactory.cs index 47185ef34ac..b9bc102c7ed 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlExpressionFactory.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlExpressionFactory.cs @@ -37,21 +37,18 @@ public SqlServerSqlExpressionFactory(SqlExpressionFactoryDependencies dependenci [return: NotNullIfNotNull("sqlExpression")] public override SqlExpression? ApplyTypeMapping(SqlExpression? sqlExpression, RelationalTypeMapping? typeMapping) { -#pragma warning disable IDE0046 // Convert to conditional expression - if (sqlExpression == null -#pragma warning restore IDE0046 // Convert to conditional expression - || sqlExpression.TypeMapping != null) + if (sqlExpression is { TypeMapping: null }) { - return sqlExpression; - } + return sqlExpression switch + { + AtTimeZoneExpression e => ApplyTypeMappingOnAtTimeZone(e, typeMapping), + SqlServerAggregateFunctionExpression e => e.ApplyTypeMapping(typeMapping), - return sqlExpression switch - { - AtTimeZoneExpression e => ApplyTypeMappingOnAtTimeZone(e, typeMapping), - SqlServerAggregateFunctionExpression e => e.ApplyTypeMapping(typeMapping), + _ => base.ApplyTypeMapping(sqlExpression, typeMapping) + }; + } - _ => base.ApplyTypeMapping(sqlExpression, typeMapping) - }; + return base.ApplyTypeMapping(sqlExpression, typeMapping); } private SqlExpression ApplyTypeMappingOnAtTimeZone(AtTimeZoneExpression atTimeZoneExpression, RelationalTypeMapping? typeMapping) diff --git a/src/EFCore.SqlServer/Query/SqlServerQueryRootProcessor.cs b/src/EFCore.SqlServer/Query/SqlServerQueryRootProcessor.cs new file mode 100644 index 00000000000..6c08a7afae0 --- /dev/null +++ b/src/EFCore.SqlServer/Query/SqlServerQueryRootProcessor.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query; + +/// +public class SqlServerQueryRootProcessor : RelationalQueryRootProcessor +{ + private readonly ITypeMappingSource _typeMappingSource; + + /// + /// Creates a new instance of the class. + /// + /// The type mapping source. + /// The model. + public SqlServerQueryRootProcessor(ITypeMappingSource typeMappingSource, IModel model) + : base(typeMappingSource, model) + => _typeMappingSource = typeMappingSource; + + // /// + // protected override Expression VisitQueryableParameter(ParameterExpression parameterExpression) + // { + // // TODO: Also, maybe this type checking should be in the base class. + // // TODO: don't convert anything if we're on an old SQL Server version. + // // SQL Server's OpenJson, which we use to unpack the queryable parameter, does not support geometry (or any other non-built-in + // // types) + // + // // We convert to query roots only parameters which are enumerable, and for which we have a type mapping converting them to a JSON + // // string. This ensures we only create query roots which can be transferred to SQL Server via OPENJSON. + // return parameterExpression.Type.TryGetSequenceType() is Type elementType + // && _typeMappingSource.FindMapping(parameterExpression.Type) is { Converter: IJsonStringConverter } + // ? new ParameterQueryRootExpression(elementType, parameterExpression) + // : parameterExpression; + // } +} diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs index 8d0d053cd62..d96b7852c5b 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs @@ -125,6 +125,11 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// + public virtual RelationalTypeMapping CloneWithElementTypeMapping(RelationalTypeMapping elementTypeMapping) + => new SqlServerStringTypeMapping( + Parameters.WithCoreParameters(Parameters.CoreParameters.WithElementTypeMapping(elementTypeMapping)), + _sqlDbType); + protected override void ConfigureParameter(DbParameter parameter) { var value = parameter.Value; diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs index 21f5884a764..368d11cd9f3 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Data; using System.Text.Json; +using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -175,6 +176,8 @@ private readonly SqlServerJsonTypeMapping _json private readonly Dictionary _storeTypeMappings; + private readonly bool _supportsOpenJson; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -183,7 +186,8 @@ private readonly SqlServerJsonTypeMapping _json /// public SqlServerTypeMappingSource( TypeMappingSourceDependencies dependencies, - RelationalTypeMappingSourceDependencies relationalDependencies) + RelationalTypeMappingSourceDependencies relationalDependencies, + ISqlServerSingletonOptions sqlServerSingletonOptions) : base(dependencies, relationalDependencies) { _clrTypeMappings @@ -270,6 +274,8 @@ public SqlServerTypeMappingSource( { "xml", new[] { _xml } } }; // ReSharper restore CoVariantArrayConversion + + _supportsOpenJson = sqlServerSingletonOptions.CompatibilityLevel >= 130; } /// @@ -279,7 +285,9 @@ public SqlServerTypeMappingSource( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo) - => base.FindMapping(mappingInfo) ?? FindRawMapping(mappingInfo)?.Clone(mappingInfo); + => base.FindMapping(mappingInfo) + ?? FindRawMapping(mappingInfo)?.Clone(mappingInfo) + ?? FindCollectionMapping(mappingInfo)?.Clone(mappingInfo); private RelationalTypeMapping? FindRawMapping(RelationalTypeMappingInfo mappingInfo) { @@ -398,7 +406,7 @@ public SqlServerTypeMappingSource( var isFixedLength = mappingInfo.IsFixedLength == true; var size = mappingInfo.Size ?? (mappingInfo.IsKeyOrIndex ? 900 : null); - if (size < 0 || size > 8000) + if (size is < 0 or > 8000) { size = isFixedLength ? 8000 : null; } @@ -415,6 +423,82 @@ public SqlServerTypeMappingSource( return null; } + private RelationalTypeMapping? FindCollectionMapping(RelationalTypeMappingInfo mappingInfo) + { + // Support mapping to a JSON array when the following is satisfied: + // 1. The ClrType is an array. + // 2. The store type is either not given or a string type. + // 3. The element CLR type has a type mapping. + + // TODO: We currently support mapping any IList<>, do we want to support HashSet? Any collection? Compare to our policy for + // TODO: regular navigations, but remember that here we have order. + if (mappingInfo.ClrType?.TryGetElementType(typeof(IList<>)) is not { } elementClrType) + { + return null; + } + + switch (mappingInfo.StoreTypeNameBase) + { + case "char varying": + case "char": + case "character varying": + case "character": + case "national char varying": + case "national character varying": + case "national character": + case "varchar": + case null: + break; + default: + return null; + } + + // TODO: need to allow the user to set the element store type + + if (FindMapping(elementClrType) is not { } elementTypeMapping) + { + return null; + } + + // Specifically exclude collections over Geometry, since there's a dedicated GeometryCollection type for that (see #30630) + if (elementClrType.Namespace == "NetTopologySuite.Geometries") + { + return null; + } + + // TODO: This can be moved into a SQL Server implementation of ValueConverterSelector.. But it seems better for this method's logic + // to be in the type mapping source. + var stringMappingInfo = new RelationalTypeMappingInfo( + typeof(string), + mappingInfo.StoreTypeName, + mappingInfo.StoreTypeNameBase, + mappingInfo.IsKeyOrIndex, + mappingInfo.IsUnicode, + mappingInfo.Size, + mappingInfo.IsRowVersion, + mappingInfo.IsFixedLength, + mappingInfo.Precision, + mappingInfo.Scale); + + if (FindMapping(stringMappingInfo) is not SqlServerStringTypeMapping stringTypeMapping) + { + return null; + } + + stringTypeMapping = (SqlServerStringTypeMapping)stringTypeMapping + .Clone(new RelationalJsonStringConverter(mappingInfo.ClrType, elementTypeMapping)); + + // OpenJson was introduced in SQL Server 2016 (compatibility level 130). If the user configures an older compatibility level, + // we allow mapping the column, but don't set the element type mapping on the mapping, so that it isn't queryable. + // This causes us to go into the old translation path for Contains over parameter via IN with constants. + if (_supportsOpenJson) + { + stringTypeMapping = (SqlServerStringTypeMapping)stringTypeMapping.CloneWithElementTypeMapping(elementTypeMapping); + } + + return stringTypeMapping; + } + private static readonly List NameBasesUsingPrecision = new() { "decimal", diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs index 140244153c2..9b05d97e02e 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs @@ -1,8 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Sqlite.Internal; +using Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal; namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal; @@ -14,6 +16,8 @@ namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal; /// public class SqliteQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor { + private readonly IRelationalTypeMappingSource _typeMappingSource; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -26,6 +30,7 @@ public SqliteQueryableMethodTranslatingExpressionVisitor( QueryCompilationContext queryCompilationContext) : base(dependencies, relationalDependencies, queryCompilationContext) { + _typeMappingSource = relationalDependencies.TypeMappingSource; } /// @@ -38,6 +43,7 @@ protected SqliteQueryableMethodTranslatingExpressionVisitor( SqliteQueryableMethodTranslatingExpressionVisitor parentVisitor) : base(parentVisitor) { + _typeMappingSource = parentVisitor._typeMappingSource; } /// @@ -111,6 +117,67 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis return translation; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override ShapedQueryExpression? VisitPrimitiveCollection(Expression primitiveCollectionExpression) + { + var elementClrType = primitiveCollectionExpression.Type.GetSequenceType(); + + SqlExpression sqlExpression; + RelationalTypeMapping? elementTypeMapping = null; + + switch (primitiveCollectionExpression) + { + case ColumnExpression + { + TypeMapping: SqliteStringTypeMapping + { + Converter: RelationalJsonStringConverter, + ElementTypeMapping: RelationalTypeMapping e + } + } column: + elementTypeMapping = e; + sqlExpression = column; + break; + + case ParameterQueryRootExpression parameterQueryRoot: + elementClrType = parameterQueryRoot.ElementType; + + // TODO: Hack until I finish proper type mapping inference on the parameter. We should pass null here as the parameter's + // type mapping, to allow it to properly get inferred later. + var typeMapping = _typeMappingSource.FindMapping(parameterQueryRoot.ParameterExpression.Type); + sqlExpression = new SqlParameterExpression(parameterQueryRoot.ParameterExpression, typeMapping /* null */); + break; + + default: + return null; + } + + // TODO: Decide what to do about type inference here. Not sure casting the output of json_each is actually needed. + // TODO: But there's also the SelectExpression projection. + // TODO: When we have metadata to determine if the element is nullable, pass that here to SelectExpression + var translation = new TableValuedFunctionExpression("j", "json_each", schema: null, builtIn: true, new[] { sqlExpression }); + var selectExpression = new SelectExpression(elementClrType, elementTypeMapping, translation, "value"); + + Expression shaperExpression = new ProjectionBindingExpression( + selectExpression, new ProjectionMember(), elementClrType.MakeNullable()); + + if (elementClrType != shaperExpression.Type) + { + Check.DebugAssert( + elementClrType.MakeNullable() == shaperExpression.Type, + "expression.Type must be nullable of targetType"); + + shaperExpression = Expression.Convert(shaperExpression, elementClrType); + } + + return new ShapedQueryExpression(selectExpression, shaperExpression); + } + private static Type GetProviderType(SqlExpression expression) => expression.TypeMapping?.Converter?.ProviderClrType ?? expression.TypeMapping?.ClrType diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteStringTypeMapping.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteStringTypeMapping.cs index 434c6766ad0..590417f511f 100644 --- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteStringTypeMapping.cs +++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteStringTypeMapping.cs @@ -47,6 +47,16 @@ protected SqliteStringTypeMapping(RelationalTypeMappingParameters parameters) protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) => new SqliteStringTypeMapping(parameters); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual RelationalTypeMapping CloneWithElementTypeMapping(RelationalTypeMapping elementTypeMapping) + => new SqliteStringTypeMapping( + Parameters.WithCoreParameters(Parameters.CoreParameters.WithElementTypeMapping(elementTypeMapping))); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs index 08ec4f9e887..dfa9fa30f83 100644 --- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs +++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text.Json; +using Microsoft.Data.Sqlite; namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal; @@ -94,6 +95,8 @@ private static readonly HashSet SpatialiteTypes { TextTypeName, Text } }; + private readonly bool _areJsonFunctionsSupported; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -105,6 +108,9 @@ public SqliteTypeMappingSource( RelationalTypeMappingSourceDependencies relationalDependencies) : base(dependencies, relationalDependencies) { + // Support for JSON functions was added in Sqlite 3.38.0 (2022-02-22, see https://www.sqlite.org/json1.html). + // This determines whether we have json_each, which is needed to query into JSON columns. + _areJsonFunctionsSupported = new Version(new SqliteConnection().ServerVersion) >= new Version(3, 38); } /// @@ -124,7 +130,9 @@ public static bool IsSpatialiteType(string columnType) /// protected override RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo) { - var mapping = base.FindMapping(mappingInfo) ?? FindRawMapping(mappingInfo); + var mapping = base.FindMapping(mappingInfo) + ?? FindRawMapping(mappingInfo) + ?? FindCollectionMapping(mappingInfo); return mapping != null && mappingInfo.StoreTypeName != null @@ -169,6 +177,55 @@ public static bool IsSpatialiteType(string columnType) return null; } + private RelationalTypeMapping? FindCollectionMapping(RelationalTypeMappingInfo mappingInfo) + { + // TODO: We currently support mapping any IList<>, do we want to support HashSet? Any collection? Compare to our policy for + // TODO: regular navigations, but remember that here we have order. + if (mappingInfo is { StoreTypeName: TextTypeName or null } + && mappingInfo.ClrType?.TryGetElementType(typeof(IList<>)) is { } elementClrType + && FindMapping(elementClrType) is { } elementTypeMapping) + { + var stringMappingInfo = new RelationalTypeMappingInfo( + typeof(string), + mappingInfo.StoreTypeName, + mappingInfo.StoreTypeNameBase, + mappingInfo.IsKeyOrIndex, + mappingInfo.IsUnicode, + mappingInfo.Size, + mappingInfo.IsRowVersion, + mappingInfo.IsFixedLength, + mappingInfo.Precision, + mappingInfo.Scale); + + if (FindMapping(stringMappingInfo) is not SqliteStringTypeMapping stringTypeMapping) + { + return null; + } + + // Specifically exclude collections over Geometry, since there's a dedicated GeometryCollection type for that (see #30630) + if (elementClrType.Namespace == "NetTopologySuite.Geometries") + { + return null; + } + + stringTypeMapping = (SqliteStringTypeMapping)stringTypeMapping + .Clone(new RelationalJsonStringConverter(mappingInfo.ClrType, elementTypeMapping)); + + // json_each was introduced in SQLite 3.38.0; on older SQLite version we allow mapping the column, but don't set the element + // type mapping on the mapping, so that it isn't queryable. This causes us to go into the old translation path for Contains + // over parameter via IN with constants. + if (_areJsonFunctionsSupported) + { + stringTypeMapping = (SqliteStringTypeMapping)stringTypeMapping + .CloneWithElementTypeMapping(elementTypeMapping); + } + + return stringTypeMapping; + } + + return null; + } + private readonly Func[] _typeRules = { name => Contains(name, "INT") diff --git a/src/EFCore/Query/ConstantQueryRootExpression.cs b/src/EFCore/Query/ConstantQueryRootExpression.cs new file mode 100644 index 00000000000..c432cdc3804 --- /dev/null +++ b/src/EFCore/Query/ConstantQueryRootExpression.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query; + +/// +/// +/// An expression that represents a constant query root within the query. +/// +/// +/// This type is typically used by database providers (and other extensions). It is generally +/// not used in application code. +/// +/// +public class ConstantQueryRootExpression : QueryRootExpression +{ + /// + /// A constant value containing the values that this query root represents. + /// + public ConstantExpression ConstantExpression { get; } + + // TODO: Currently must be an array, relax to allow arbitrary enumerables? + /// + /// Creates a new instance of the class. + /// + /// The query provider associated with this query root. + /// A constant value containing the values that this query root represents. + public ConstantQueryRootExpression(IAsyncQueryProvider asyncQueryProvider, ConstantExpression constantExpression) + : base( + asyncQueryProvider, + constantExpression.Type.TryGetSequenceType() ?? throw new ArgumentException("Must be an enumerable")) // TODO + { + ConstantExpression = constantExpression; + } + + /// + /// Creates a new instance of the class. + /// + /// A constant value containing the values that this query root represents. + public ConstantQueryRootExpression(ConstantExpression constantExpression) + : base( + constantExpression.Type.TryGetSequenceType() ?? throw new ArgumentException("Must be an enumerable")) // TODO + { + ConstantExpression = constantExpression; + } + + /// + public override Expression DetachQueryProvider() + => new ConstantQueryRootExpression(ConstantExpression); + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var visited = visitor.Visit(ConstantExpression); + + return visited == ConstantExpression + ? this + : new ConstantQueryRootExpression((ConstantExpression)visited); + } + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append("["); + + var array = (Array)ConstantExpression.Value!; + for (var i = 0; i < array.Length; i++) + { + if (i > 0) + { + expressionPrinter.Append(","); + } + + expressionPrinter.Append(array.GetValue(i)?.ToString() ?? ""); + } + + expressionPrinter.Append("]"); + } +} diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs index a09f19db5d3..cc60ba7b4e6 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs @@ -104,22 +104,34 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp } } - var navigation = memberIdentity.MemberInfo != null + var navigation = memberIdentity.MemberInfo is not null ? entityType.FindNavigation(memberIdentity.MemberInfo) - : entityType.FindNavigation(memberIdentity.Name!); - if (navigation != null) + : memberIdentity.Name is not null + ? entityType.FindNavigation(memberIdentity.Name) + : null; + if (navigation is not null) { - return ExpandNavigation(root, entityReference, navigation, convertedType != null); + return ExpandNavigation(root, entityReference, navigation, convertedType is not null); } - var skipNavigation = memberIdentity.MemberInfo != null + var skipNavigation = memberIdentity.MemberInfo is not null ? entityType.FindSkipNavigation(memberIdentity.MemberInfo) : memberIdentity.Name is not null ? entityType.FindSkipNavigation(memberIdentity.Name) : null; - if (skipNavigation != null) + if (skipNavigation is not null) + { + return ExpandSkipNavigation(root, entityReference, skipNavigation, convertedType is not null); + } + + var property = memberIdentity.MemberInfo != null + ? entityType.FindProperty(memberIdentity.MemberInfo) + : memberIdentity.Name is not null + ? entityType.FindProperty(memberIdentity.Name) + : null; + if (property?.GetTypeMapping().ElementTypeMapping != null) { - return ExpandSkipNavigation(root, entityReference, skipNavigation, convertedType != null); + return new QueryablePropertyReference(root, property); } } @@ -1015,6 +1027,9 @@ private sealed class ReducingExpressionVisitor : ExpressionVisitor case OwnedNavigationReference ownedNavigationReference: return Visit(ownedNavigationReference.Parent).CreateEFPropertyExpression(ownedNavigationReference.Navigation); + case QueryablePropertyReference queryablePropertyReference: + return Visit(queryablePropertyReference.Parent).CreateEFPropertyExpression(queryablePropertyReference.Property); + case IncludeExpression includeExpression: var entityExpression = Visit(includeExpression.EntityExpression); var navigationExpression = ReplacingExpressionVisitor.Replace( diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs index 69d8229eb31..7f9abec9c87 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs @@ -501,4 +501,46 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) } } } + + /// + /// Queryable properties are not expanded (similar to . + /// + private sealed class QueryablePropertyReference : Expression, IPrintableExpression + { + public QueryablePropertyReference(Expression parent, IProperty property /*, EntityReference entityReference */) + { + Parent = parent; + Property = property; + // EntityReference = entityReference; + } + + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + Parent = visitor.Visit(Parent); + + return this; + } + + public Expression Parent { get; private set; } + public new IProperty Property { get; } + // public EntityReference EntityReference { get; } + + public override Type Type + => Property.ClrType; + + public override ExpressionType NodeType + => ExpressionType.Extension; + + void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.AppendLine(nameof(OwnedNavigationReference)); + using (expressionPrinter.Indent()) + { + expressionPrinter.Append("Parent: "); + expressionPrinter.Visit(Parent); + expressionPrinter.AppendLine(); + expressionPrinter.Append("Property: " + Property.Name + " (QUERYABLE)"); + } + } + } } diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index f6fedc5ac46..51496705bd9 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -760,8 +760,7 @@ when QueryableMethods.IsSumWithSelector(method): if (genericMethod == QueryableMethods.AsQueryable) { - if (firstArgument is NavigationTreeExpression navigationTreeExpression - && navigationTreeExpression.Type.IsGenericType + if (firstArgument is NavigationTreeExpression { Type.IsGenericType: true } navigationTreeExpression && navigationTreeExpression.Type.GetGenericTypeDefinition() == typeof(IGrouping<,>)) { // This is groupingElement.AsQueryable so we preserve it @@ -770,6 +769,7 @@ when QueryableMethods.IsSumWithSelector(method): navigationTreeExpression); } + // HACK return UnwrapCollectionMaterialization(firstArgument); } @@ -782,7 +782,8 @@ when QueryableMethods.IsSumWithSelector(method): return ConvertToEnumerable(method, visitedArguments); } - throw new InvalidOperationException(CoreStrings.TranslationFailed(methodCallExpression.Print())); + return base.VisitMethodCall(methodCallExpression); + // throw new InvalidOperationException(CoreStrings.TranslationFailed(methodCallExpression.Print())); } // Remove MaterializeCollectionNavigationExpression when applying ToList/ToArray @@ -1954,17 +1955,6 @@ private NavigationExpansionExpression CreateNavigationExpansionExpression( return new NavigationExpansionExpression(sourceExpression, currentTree, currentTree, parameterName); } - private NavigationExpansionExpression CreateNavigationExpansionExpression( - Expression sourceExpression, - OwnedNavigationReference ownedNavigationReference) - { - var parameterName = GetParameterName("o"); - var entityReference = ownedNavigationReference.EntityReference; - var currentTree = new NavigationTreeExpression(entityReference); - - return new NavigationExpansionExpression(sourceExpression, currentTree, currentTree, parameterName); - } - private Expression ExpandNavigationsForSource(NavigationExpansionExpression source, Expression expression) { expression = _removeRedundantNavigationComparisonExpressionVisitor.Visit(expression); @@ -2033,8 +2023,7 @@ private LambdaExpression GenerateLambda(Expression body, ParameterExpression cur private Expression UnwrapCollectionMaterialization(Expression expression) { - while (expression is MethodCallExpression innerMethodCall - && innerMethodCall.Method.IsGenericMethod + while (expression is MethodCallExpression { Method.IsGenericMethod: true } innerMethodCall && innerMethodCall.Method.GetGenericMethodDefinition() is MethodInfo innerMethod && (innerMethod == EnumerableMethods.AsEnumerable || innerMethod == EnumerableMethods.ToList @@ -2048,14 +2037,38 @@ private Expression UnwrapCollectionMaterialization(Expression expression) expression = materializeCollectionNavigationExpression.Subquery; } - return expression is OwnedNavigationReference ownedNavigationReference - && ownedNavigationReference.Navigation.IsCollection - ? CreateNavigationExpansionExpression( + switch (expression) + { + case OwnedNavigationReference { Navigation.IsCollection: true } ownedNavigationReference: + { + var currentTree = new NavigationTreeExpression(ownedNavigationReference.EntityReference); + + return new NavigationExpansionExpression( Expression.Call( QueryableMethods.AsQueryable.MakeGenericMethod(ownedNavigationReference.Type.GetSequenceType()), ownedNavigationReference), - ownedNavigationReference) - : expression; + currentTree, + currentTree, + GetParameterName("o")); + } + + case QueryablePropertyReference queryablePropertyReference: + { + var currentTree = new NavigationTreeExpression(Expression.Default(queryablePropertyReference.Type.GetSequenceType())); + + return new NavigationExpansionExpression( + Expression.Call( + QueryableMethods.AsQueryable.MakeGenericMethod(queryablePropertyReference.Type.GetSequenceType()), + queryablePropertyReference), + currentTree, + currentTree, + GetParameterName("p")); + + } + + default: + return expression; + } } private string GetParameterName(string prefix) @@ -2242,26 +2255,19 @@ private static Expression SnapshotExpression(Expression selector) } private static EntityReference? UnwrapEntityReference(Expression? expression) - { - switch (expression) + => expression switch { - case EntityReference entityReference: - return entityReference; - - case NavigationTreeExpression navigationTreeExpression: - return UnwrapEntityReference(navigationTreeExpression.Value); - - case NavigationExpansionExpression navigationExpansionExpression - when navigationExpansionExpression.CardinalityReducingGenericMethodInfo != null: - return UnwrapEntityReference(navigationExpansionExpression.PendingSelector); - - case OwnedNavigationReference ownedNavigationReference: - return ownedNavigationReference.EntityReference; - - default: - return null; - } - } + EntityReference entityReference + => entityReference, + NavigationTreeExpression navigationTreeExpression + => UnwrapEntityReference(navigationTreeExpression.Value), + NavigationExpansionExpression navigationExpansionExpression + when navigationExpansionExpression.CardinalityReducingGenericMethodInfo != null + => UnwrapEntityReference(navigationExpansionExpression.PendingSelector), + OwnedNavigationReference ownedNavigationReference + => ownedNavigationReference.EntityReference, + _ => null + }; private sealed class Parameters : IParameterValues { diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs index b8ae88c3639..cf44649dea8 100644 --- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs @@ -66,8 +66,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp visitedExpression = TryConvertEnumerableToQueryable(methodCallExpression); } - if (method.DeclaringType != null - && method.DeclaringType.IsGenericType + if (method.DeclaringType is { IsGenericType: true } && (method.DeclaringType.GetGenericTypeDefinition() == typeof(ICollection<>) || method.DeclaringType.GetGenericTypeDefinition() == typeof(List<>)) && method.Name == nameof(List.Contains)) @@ -251,20 +250,27 @@ private Expression TryConvertEnumerableToQueryable(MethodCallExpression methodCa return base.VisitMethodCall(methodCallExpression); } + // HACK if (methodCallExpression.Arguments.Count > 0 - && ClientSource(methodCallExpression.Arguments[0])) + && methodCallExpression.Arguments[0] is MemberInitExpression or NewExpression) { - // this is methodCall over closure variable or constant return base.VisitMethodCall(methodCallExpression); } + // HACK + // if (methodCallExpression.Arguments.Count > 0 + // && ClientSource(methodCallExpression.Arguments[0])) + // { + // // this is methodCall over closure variable or constant + // return base.VisitMethodCall(methodCallExpression); + // } + var arguments = VisitAndConvert(methodCallExpression.Arguments, nameof(VisitMethodCall)).ToArray(); var enumerableMethod = methodCallExpression.Method; var enumerableParameters = enumerableMethod.GetParameters(); var genericTypeArguments = Array.Empty(); - if (enumerableMethod.Name == nameof(Enumerable.Min) - || enumerableMethod.Name == nameof(Enumerable.Max)) + if (enumerableMethod.Name is nameof(Enumerable.Min) or nameof(Enumerable.Max)) { genericTypeArguments = new Type[methodCallExpression.Arguments.Count]; @@ -332,8 +338,7 @@ private Expression TryConvertEnumerableToQueryable(MethodCallExpression methodCa // If innerArgument has ToList applied to it then unwrap it. // Also preserve generic argument of ToList is applied to different type if (arguments[i].Type.TryGetElementType(typeof(List<>)) != null - && arguments[i] is MethodCallExpression toListMethodCallExpression - && toListMethodCallExpression.Method.IsGenericMethod + && arguments[i] is MethodCallExpression { Method.IsGenericMethod: true } toListMethodCallExpression && toListMethodCallExpression.Method.GetGenericMethodDefinition() == EnumerableMethods.ToList) { genericType = toListMethodCallExpression.Method.GetGenericArguments()[0]; @@ -386,9 +391,9 @@ private Expression TryConvertEnumerableToQueryable(MethodCallExpression methodCa private Expression TryConvertListContainsToQueryableContains(MethodCallExpression methodCallExpression) { - if (ClientSource(methodCallExpression.Object)) + // if (ClientSource(methodCallExpression.Object)) + if (methodCallExpression.Object is ConstantExpression or MemberInitExpression or NewExpression) { - // this is methodCall over closure variable or constant return base.VisitMethodCall(methodCallExpression); } @@ -402,12 +407,10 @@ private Expression TryConvertListContainsToQueryableContains(MethodCallExpressio methodCallExpression.Arguments[0]); } - private static bool ClientSource(Expression? expression) - => expression is ConstantExpression - || expression is MemberInitExpression - || expression is NewExpression - || expression is ParameterExpression parameter - && parameter.Name?.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal) == true; + // private static bool ClientSource(Expression? expression) + // => expression is ConstantExpression or MemberInitExpression or NewExpression + // || expression is ParameterExpression parameter + // && parameter.Name?.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal) == true; private static bool CanConvertEnumerableToQueryable(Type enumerableType, Type queryableType) { @@ -438,8 +441,7 @@ private MethodCallExpression TryFlattenGroupJoinSelectMany(MethodCallExpression { // SelectMany var selectManySource = methodCallExpression.Arguments[0]; - if (selectManySource is MethodCallExpression groupJoinMethod - && groupJoinMethod.Method.IsGenericMethod + if (selectManySource is MethodCallExpression { Method.IsGenericMethod: true } groupJoinMethod && groupJoinMethod.Method.GetGenericMethodDefinition() == QueryableMethods.GroupJoin) { // GroupJoin @@ -455,8 +457,7 @@ private MethodCallExpression TryFlattenGroupJoinSelectMany(MethodCallExpression var collectionSelectorBody = selectManyCollectionSelector.Body; var defaultIfEmpty = false; - if (collectionSelectorBody is MethodCallExpression collectionEndingMethod - && collectionEndingMethod.Method.IsGenericMethod + if (collectionSelectorBody is MethodCallExpression { Method.IsGenericMethod: true } collectionEndingMethod && collectionEndingMethod.Method.GetGenericMethodDefinition() == QueryableMethods.DefaultIfEmptyWithoutArgument) { defaultIfEmpty = true; @@ -478,8 +479,7 @@ private MethodCallExpression TryFlattenGroupJoinSelectMany(MethodCallExpression ReplacingExpressionVisitor.Replace( groupJoinResultSelector.Parameters[1], inner, collectionSelectorBody)); - if (inner is MethodCallExpression innerMethodCall - && innerMethodCall.Method.IsGenericMethod + if (inner is MethodCallExpression { Method.IsGenericMethod: true } innerMethodCall && innerMethodCall.Method.GetGenericMethodDefinition() == QueryableMethods.AsQueryable && innerMethodCall.Type == innerMethodCall.Arguments[0].Type) { @@ -542,8 +542,7 @@ private MethodCallExpression TryFlattenGroupJoinSelectMany(MethodCallExpression { // SelectMany var selectManySource = methodCallExpression.Arguments[0]; - if (selectManySource is MethodCallExpression groupJoinMethod - && groupJoinMethod.Method.IsGenericMethod + if (selectManySource is MethodCallExpression { Method.IsGenericMethod: true } groupJoinMethod && groupJoinMethod.Method.GetGenericMethodDefinition() == QueryableMethods.GroupJoin) { // GroupJoin @@ -558,8 +557,7 @@ private MethodCallExpression TryFlattenGroupJoinSelectMany(MethodCallExpression var groupJoinResultSelectorBody = groupJoinResultSelector.Body; var defaultIfEmpty = false; - if (groupJoinResultSelectorBody is MethodCallExpression collectionEndingMethod - && collectionEndingMethod.Method.IsGenericMethod + if (groupJoinResultSelectorBody is MethodCallExpression { Method.IsGenericMethod: true } collectionEndingMethod && collectionEndingMethod.Method.GetGenericMethodDefinition() == QueryableMethods.DefaultIfEmptyWithoutArgument) { defaultIfEmpty = true; diff --git a/src/EFCore/Query/ParameterQueryRootExpression.cs b/src/EFCore/Query/ParameterQueryRootExpression.cs new file mode 100644 index 00000000000..e61506d2bb2 --- /dev/null +++ b/src/EFCore/Query/ParameterQueryRootExpression.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query; + +/// +/// +/// An expression that represents a parameter query root within the query. +/// +/// +/// This type is typically used by database providers (and other extensions). It is generally +/// not used in application code. +/// +/// +public class ParameterQueryRootExpression : QueryRootExpression +{ + /// + /// The parameter expression representing the values for this query root. + /// + public ParameterExpression ParameterExpression { get; } + + /// + /// Creates a new instance of the class. + /// + /// The query provider associated with this query root. + /// The values that this query root represents. + /// The parameter expression representing the values for this query root. + public ParameterQueryRootExpression( + IAsyncQueryProvider asyncQueryProvider, Type elementType, ParameterExpression parameterExpression) + : base(asyncQueryProvider, elementType) + { + ParameterExpression = parameterExpression; + } + + /// + /// Creates a new instance of the class. + /// + /// The values that this query root represents. + /// The parameter expression representing the values for this query root. + public ParameterQueryRootExpression(Type elementType, ParameterExpression parameterExpression) + : base(elementType) + { + ParameterExpression = parameterExpression; + } + + /// + public override Expression DetachQueryProvider() + => new ParameterQueryRootExpression(ElementType, ParameterExpression); + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var parameterExpression = (ParameterExpression)visitor.Visit(ParameterExpression); + + return parameterExpression == ParameterExpression + ? this + : new ParameterQueryRootExpression(ElementType, parameterExpression); + } + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + => expressionPrinter.Visit(ParameterExpression); +} diff --git a/src/EFCore/Query/QueryCompilationContext.cs b/src/EFCore/Query/QueryCompilationContext.cs index c83a7c74910..e51a1ab42f4 100644 --- a/src/EFCore/Query/QueryCompilationContext.cs +++ b/src/EFCore/Query/QueryCompilationContext.cs @@ -163,7 +163,7 @@ public virtual Func CreateQueryExecutor(Expressi query = _queryTranslationPreprocessorFactory.Create(this).Process(query); // Convert EntityQueryable to ShapedQueryExpression - query = _queryableMethodTranslatingExpressionVisitorFactory.Create(this).Visit(query); + query = _queryableMethodTranslatingExpressionVisitorFactory.Create(this).Translate(query); query = _queryTranslationPostprocessorFactory.Create(this).Process(query); // Inject actual entity materializer diff --git a/src/EFCore/Query/QueryRootProcessor.cs b/src/EFCore/Query/QueryRootProcessor.cs new file mode 100644 index 00000000000..ff6a282296c --- /dev/null +++ b/src/EFCore/Query/QueryRootProcessor.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query; + +/// +/// A visitor which adds additional query root nodes during preprocessing. +/// +public class QueryRootProcessor : ExpressionVisitor +{ + /// + protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) + { + // We cannot convert all arrays/enumerables everywhere to query roots, since query roots have an expression type of IQueryable, + // and that would be incompatible e.g. with an array parameter in some regular function. + // So for now, we only convert direct arguments where the parameter is IEnumerable/IQueryable; this is very conservative and may + // miss some cases, but it's safer and less likely to introduce bugs. + // Note that we unwrap constant and parameter query roots in SqlTranslatingEV to their wrapped constant/parameter in case they make + // it there; they're only translated to SQL query root expressions in QueryableMethodTranslatingEV. + var method = methodCallExpression.Method; + var parameters = method.GetParameters(); + + // Note that we don't need to look at methodCallExpression.Object, since IQueryable<> doesn't declare any methods. + // All methods over queryable are extensions. + Expression[]? newArguments = null; + + for (var i = 0; i < methodCallExpression.Arguments.Count; i++) + { + var argument = methodCallExpression.Arguments[i]; + var parameterType = parameters[i].ParameterType; + + Expression? visitedArgument = null; + + if (parameterType.IsGenericType + && (parameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>) + || parameterType.GetGenericTypeDefinition() == typeof(IQueryable<>))) + { + visitedArgument = argument switch + { + ConstantExpression constantArgument + => VisitQueryableConstant(constantArgument), + + ParameterExpression parameterExpression + when parameterExpression.Name?.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal) + == true + => VisitQueryableParameter(parameterExpression), + + _ => null + }; + } + + visitedArgument ??= Visit(argument); + + if (visitedArgument != argument) + { + if (newArguments is null) + { + newArguments = new Expression[methodCallExpression.Arguments.Count]; + + for (var j = 0; j < i; j++) + { + newArguments[j] = methodCallExpression.Arguments[j]; + } + } + } + + if (newArguments is not null) + { + newArguments[i] = visitedArgument; + } + } + + return newArguments is null + ? methodCallExpression + : methodCallExpression.Update(methodCallExpression.Object, newArguments); + } + + /// + /// A provider hook for converting a to a , which + /// would be translated later e.g. by converting its contents to a JSON string. + /// + /// + /// Returns the given argument by default, since parameters query roots are a provider-specific capability. + /// + /// The parameter expression to attempt to convert to a query root. + protected virtual Expression VisitQueryableParameter(ParameterExpression parameterExpression) + => parameterExpression; + + /// + /// A provider hook for converting a to a . + /// + /// + /// Returns the given argument by default, since constant query roots are a provider-specific capability. + /// + /// The constant expression to attempt to convert to a query root. + protected virtual Expression VisitQueryableConstant(ConstantExpression constantExpression) + => constantExpression; +} + diff --git a/src/EFCore/Query/QueryTranslationPreprocessor.cs b/src/EFCore/Query/QueryTranslationPreprocessor.cs index 1f2a0cc307b..6ff2be35925 100644 --- a/src/EFCore/Query/QueryTranslationPreprocessor.cs +++ b/src/EFCore/Query/QueryTranslationPreprocessor.cs @@ -52,6 +52,7 @@ public virtual Expression Process(Expression query) { query = new InvocationExpressionRemovingExpressionVisitor().Visit(query); query = NormalizeQueryableMethod(query); + query = ProcessQueryRoots(query); query = new NullCheckRemovingExpressionVisitor().Visit(query); query = new SubqueryMemberPushdownExpressionVisitor(QueryCompilationContext.Model).Visit(query); query = new NavigationExpandingExpressionVisitor( @@ -77,6 +78,17 @@ public virtual Expression Process(Expression query) /// The query expression to normalize. /// A query expression after normalization has been done. public virtual Expression NormalizeQueryableMethod(Expression expression) - => new QueryableMethodNormalizingExpressionVisitor(QueryCompilationContext) - .Normalize(expression); + { + expression = new QueryableMethodNormalizingExpressionVisitor(QueryCompilationContext).Normalize(expression); + + return expression; + } + + /// + /// Adds additional query root nodes to the query. + /// + /// The query expression to process. + /// A query expression after query roots have been added. + public virtual Expression ProcessQueryRoots(Expression expression) + => expression; } diff --git a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs index 4b90c04448f..0868932c14b 100644 --- a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs @@ -50,6 +50,14 @@ protected QueryableMethodTranslatingExpressionVisitor( /// public virtual string? TranslationErrorDetails { get; private set; } + /// + /// Translates an expression to an equivalent SQL representation. + /// + /// An expression to translate. + /// A SQL translation of the given expression. + public virtual Expression Translate(Expression expression) + => Visit(expression); + /// /// Adds detailed information about errors encountered during translation. /// @@ -85,7 +93,10 @@ protected override Expression VisitExtension(Expression extensionExpression) } throw new InvalidOperationException( - CoreStrings.QueryUnhandledQueryRootExpression(queryRootExpression.GetType().ShortDisplayName())); + CoreStrings.TranslationFailedWithDetails( + queryRootExpression, + TranslationErrorDetails + ?? CoreStrings.QueryUnhandledQueryRootExpression(queryRootExpression.GetType().ShortDisplayName()))); } return base.VisitExtension(extensionExpression); @@ -508,7 +519,7 @@ protected virtual Expression MarkShaperNullable(Expression shaperExpression) public virtual ShapedQueryExpression? TranslateSubquery(Expression expression) { var subqueryVisitor = CreateSubqueryVisitor(); - var translation = subqueryVisitor.Visit(expression) as ShapedQueryExpression; + var translation = subqueryVisitor.Translate(expression) as ShapedQueryExpression; if (translation == null && subqueryVisitor.TranslationErrorDetails != null) { AddTranslationErrorDetails(subqueryVisitor.TranslationErrorDetails); diff --git a/src/EFCore/Storage/CoreTypeMapping.cs b/src/EFCore/Storage/CoreTypeMapping.cs index bc39feacff8..04188d2eb11 100644 --- a/src/EFCore/Storage/CoreTypeMapping.cs +++ b/src/EFCore/Storage/CoreTypeMapping.cs @@ -35,13 +35,17 @@ protected readonly record struct CoreTypeMappingParameters /// Supports custom comparisons between keys--e.g. PK to FK comparison. /// Supports custom comparisons between converted provider values. /// An optional factory for creating a specific . + /// + /// If this type mapping represents a primitive collection, this holds the element's type mapping. + /// public CoreTypeMappingParameters( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type clrType, ValueConverter? converter = null, ValueComparer? comparer = null, ValueComparer? keyComparer = null, ValueComparer? providerValueComparer = null, - Func? valueGeneratorFactory = null) + Func? valueGeneratorFactory = null, + CoreTypeMapping? elementTypeMapping = null) { ClrType = clrType; Converter = converter; @@ -49,6 +53,7 @@ public CoreTypeMappingParameters( KeyComparer = keyComparer; ProviderValueComparer = providerValueComparer; ValueGeneratorFactory = valueGeneratorFactory; + ElementTypeMapping = elementTypeMapping; } /// @@ -83,6 +88,11 @@ public CoreTypeMappingParameters( /// public Func? ValueGeneratorFactory { get; } + /// + /// If this type mapping represents a primitive collection, this holds the element's type mapping. + /// + public CoreTypeMapping? ElementTypeMapping { get; } + /// /// Creates a new parameter object with the given /// converter composed with any existing converter and set on the new parameter object. @@ -96,7 +106,24 @@ public CoreTypeMappingParameters WithComposedConverter(ValueConverter? converter Comparer, KeyComparer, ProviderValueComparer, - ValueGeneratorFactory); + ValueGeneratorFactory, + ElementTypeMapping); + + /// + /// Creates a new parameter object with the given + /// element type mapping. + /// + /// The element type mapping. + /// The new parameter object. + public CoreTypeMappingParameters WithElementTypeMapping(CoreTypeMapping elementTypeMapping) + => new( + ClrType, + Converter, + Comparer, + KeyComparer, + ProviderValueComparer, + ValueGeneratorFactory, + elementTypeMapping); } private ValueComparer? _comparer; @@ -224,4 +251,10 @@ public virtual ValueComparer ProviderValueComparer /// An expression tree that can be used to generate code for the literal value. public virtual Expression GenerateCodeLiteral(object value) => throw new NotSupportedException(CoreStrings.LiteralGenerationNotSupported(ClrType.ShortDisplayName())); + + /// + /// If this type mapping represents a primitive collection, this holds the element's type mapping. + /// + public virtual CoreTypeMapping? ElementTypeMapping + => Parameters.ElementTypeMapping; } diff --git a/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs new file mode 100644 index 00000000000..111c1a46dae --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query; + +public abstract class NonSharedPrimitiveCollectionsQueryRelationalTestBase : NonSharedPrimitiveCollectionsQueryTestBase +{ + protected TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected void ClearLog() + => TestSqlLoggerFactory.Clear(); + + protected void AssertSql(params string[] expected) + => TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs index e63dffbd772..2530f17ab9f 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs @@ -108,18 +108,6 @@ public virtual void Throws_when_subquery_main_from_clause() CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } - [ConditionalFact] - public virtual void Throws_when_select_many() - { - using var context = CreateContext(); - - AssertTranslationFailed( - () => (from c1 in context.Customers - from i in new[] { 1, 2, 3 } - select c1) - .ToList()); - } - [ConditionalFact] public virtual void Throws_when_join() { diff --git a/test/EFCore.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryTestBase.cs new file mode 100644 index 00000000000..4d44fa73604 --- /dev/null +++ b/test/EFCore.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryTestBase.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using NetTopologySuite.Geometries; + +namespace Microsoft.EntityFrameworkCore.Query; + +public abstract class NonSharedPrimitiveCollectionsQueryTestBase : NonSharedModelTestBase +{ + // This ensures that collections of Geometry (e.g. Geometry[]) aren't mapped; NTS has GeometryCollection for that. + // See SQL Server/SQLite for a sample implementation. + [ConditionalFact] // #30630 + public abstract Task Collection_of_geometry_is_not_mapped(); + + private class MyContext : DbContext + { + public MyContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); + } + + private class MyEntity + { + public int Id { get; set; } + + public int[] Ints { get; set; } + } + + protected override string StoreName + => "NonSharedPrimitiveCollectionsTest"; +} diff --git a/test/EFCore.Specification.Tests/Query/NorthwindCompiledQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindCompiledQueryTestBase.cs index a8514731ae7..54e1a95f551 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindCompiledQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindCompiledQueryTestBase.cs @@ -208,12 +208,12 @@ public virtual void Query_with_array_parameter() using (var context = CreateContext()) { - query(context, new[] { "ALFKI" }); + Assert.Equal(1, query(context, new[] { "ALFKI" }).Count()); } using (var context = CreateContext()) { - query(context, new[] { "ANATR" }); + Assert.Equal(1, query(context, new[] { "ANATR" }).Count()); } } @@ -466,12 +466,12 @@ public virtual async Task Query_with_array_parameter_async() using (var context = CreateContext()) { - await Enumerate(query(context, new[] { "ALFKI" })); + Assert.Equal(1, await CountAsync(query(context, new[] { "ALFKI" }))); } using (var context = CreateContext()) { - await Enumerate(query(context, new[] { "ANATR" })); + Assert.Equal(1, await CountAsync(query(context, new[] { "ANATR" }))); } } @@ -861,13 +861,18 @@ public virtual void MakeBinary_does_not_throw_for_unsupported_operator() Assert.Single(result); } - protected async Task Enumerate(IAsyncEnumerable source) + protected async Task CountAsync(IAsyncEnumerable source) { + var count = 0; await foreach (var _ in source) { + count++; } + return count; } protected NorthwindContext CreateContext() => Fixture.CreateContext(); + + public static IEnumerable IsAsyncData = new[] { new object[] { false }, new object[] { true } }; } diff --git a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs index 81bf6547724..d15092b27cd 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs @@ -1929,6 +1929,7 @@ from c in ss.Set().OrderBy(c => c.CustomerID).Take(2) e => (e.e1.EmployeeID, e.c.CustomerID), entryCount: 4)); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task SelectMany_simple1(bool async) diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs new file mode 100644 index 00000000000..b96d483b55a --- /dev/null +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -0,0 +1,463 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query; + +public class PrimitiveCollectionsQueryTestBase : QueryTestBase + where TFixture : PrimitiveCollectionsQueryTestBase.PrimitiveCollectionsQueryFixtureBase, new() +{ + protected PrimitiveCollectionsQueryTestBase(TFixture fixture) + : base(fixture) + { + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Constant_of_ints_Contains(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => new[] { 10, 999 }.Contains(c.Int)), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Constant_of_nullable_ints_Contains(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => new int?[] { 10, 999 }.Contains(c.NullableInt)), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Constant_of_nullable_ints_Contains_null(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => new int?[] { null, 999 }.Contains(c.NullableInt)), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Constant_Count_with_zero_values(bool async) + => AssertQuery( + async, + // ReSharper disable once UseArrayEmptyMethod + ss => ss.Set().Where(c => new int[0].Count(i => i > c.Id) == 1), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Constant_Count_with_one_value(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => new[] { 2 }.Count(i => i > c.Id) == 1), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Constant_Count_with_two_values(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => new[] { 2, 999 }.Count(i => i > c.Id) == 1), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Constant_Count_with_three_values(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => new[] { 2, 999, 1000 }.Count(i => i > c.Id) == 2), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Constant_Contains_with_zero_values(bool async) + => AssertQuery( + async, + // ReSharper disable once UseArrayEmptyMethod + ss => ss.Set().Where(c => new int[0].Contains(c.Id)), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Constant_Contains_with_one_value(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => new[] { 2 }.Contains(c.Id)), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Constant_Contains_with_two_values(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => new[] { 2, 999 }.Contains(c.Id)), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Constant_Contains_with_three_values(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => new[] { 2, 999, 1000 }.Contains(c.Id)), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Parameter_Count(bool async) + { + var ids = new[] { 2, 999 }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => ids.Count(i => i > c.Id) == 1), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Parameter_of_ints_Contains(bool async) + { + var ints = new[] { 10, 999 }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => ints.Contains(c.Int)), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Parameter_of_nullable_ints_Contains(bool async) + { + var nullableInts = new int?[] { 10, 999 }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => nullableInts.Contains(c.NullableInt)), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Parameter_of_nullable_ints_Contains_null(bool async) + { + var nullableInts = new int?[] { null, 999 }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => nullableInts.Contains(c.NullableInt)), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Parameter_of_strings_Contains(bool async) + { + var strings = new[] { "10", "999" }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => strings.Contains(c.String)), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Parameter_of_DateTimes_Contains(bool async) + { + var dateTimes = new[] { new DateTime(2020, 1, 10, 12, 30, 0), new DateTime(9999, 1, 1, 0, 0, 0) }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => dateTimes.Contains(c.DateTime)), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Parameter_of_bools_Contains(bool async) + { + var bools = new[] { true }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => bools.Contains(c.Bool)), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Parameter_of_enums_Contains(bool async) + { + var enums = new[] { MyEnum.Value1, MyEnum.Value4 }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => enums.Contains(c.Enum)), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_of_ints_Contains(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.Ints.Contains(10)), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_of_nullable_ints_Contains(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.NullableInts.Contains(10)), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_of_nullable_ints_Contains_null(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.NullableInts.Contains(null)), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_of_bools_Contains(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.Bools.Contains(true)), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_Count(bool async) + => AssertQuery( + async, + // ReSharper disable once UseCollectionCountProperty + ss => ss.Set().Where(c => c.Ints.Count() == 2), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_and_parameter_Join(bool async) + { + var ints = new[] { 11, 111 }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Ints.Join(ints, i => i, j => j, (i, j) => new { I = i, J = j }).Count() == 2), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Parameter_Intersect_column(bool async) + { + var ints = new[] { 11, 111 }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => ints.Intersect(c.Ints).Count() == 2), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_Intersect_constant(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.Ints.Intersect(new[] { 11, 111 }).Count() == 2), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_equality_parameter(bool async) + { + var ints = new[] { 1, 10 }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Ints == ints), + ss => ss.Set().Where(c => c.Ints.SequenceEqual(ints)), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_equality_parameter_with_custom_converter(bool async) + { + var ints = new[] { 1, 10 }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => c.CustomConvertedInts == ints), + ss => ss.Set().Where(c => c.CustomConvertedInts.SequenceEqual(ints)), + entryCount: 1); + } + + public abstract class PrimitiveCollectionsQueryFixtureBase : SharedStoreFixtureBase, IQueryFixtureBase + { + private PrimitiveArrayData _expectedData; + + protected override string StoreName + => "PrimitiveCollectionsTest"; + + public Func GetContextCreator() + => () => CreateContext(); + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + => modelBuilder.Entity( + e => + { + e.Property(p => p.Id).ValueGeneratedNever(); + + e.Property(p => p.CustomConvertedInts) + .HasConversion( + i => string.Join(",", i), + s => s.Split(",", StringSplitOptions.None).Select(int.Parse).ToArray(), + new ValueComparer(favorStructuralComparisons: true)); + }); + + protected override void Seed(PrimitiveCollectionsContext context) + => new PrimitiveArrayData(context); + + public virtual ISetSource GetExpectedData() + => _expectedData ??= new PrimitiveArrayData(); + + public IReadOnlyDictionary EntitySorters { get; } = new Dictionary> + { + { typeof(PrimitiveCollectionsEntity), e => ((PrimitiveCollectionsEntity)e)?.Id } + }.ToDictionary(e => e.Key, e => (object)e.Value); + + public IReadOnlyDictionary EntityAsserters { get; } = new Dictionary> + { + { + typeof(PrimitiveCollectionsEntity), (e, a) => + { + Assert.Equal(e == null, a == null); + + if (a != null) + { + var ee = (PrimitiveCollectionsEntity)e; + var aa = (PrimitiveCollectionsEntity)a; + + Assert.Equal(ee.Id, aa.Id); + Assert.Equivalent(ee.Ints, aa.Ints, strict: true); + Assert.Equivalent(ee.Strings, aa.Strings, strict: true); + Assert.Equivalent(ee.DateTimes, aa.DateTimes, strict: true); + Assert.Equivalent(ee.Bools, aa.Bools, strict: true); + Assert.Equivalent(ee.CustomConvertedInts, aa.CustomConvertedInts, strict: true); + } + } + } + }.ToDictionary(e => e.Key, e => (object)e.Value); + } + + public class PrimitiveCollectionsContext : PoolableDbContext + { + public PrimitiveCollectionsContext(DbContextOptions options) + : base(options) + { + } + } + + public class PrimitiveCollectionsEntity + { + public int Id { get; set; } + + public string String { get; set; } + public int Int { get; set; } + public DateTime DateTime { get; set; } + public bool Bool { get; set; } + public MyEnum Enum { get; set; } + public int? NullableInt { get; set; } + + public string[] Strings { get; set; } + public int[] Ints { get; set; } + public DateTime[] DateTimes { get; set; } + public bool[] Bools { get; set; } + public MyEnum[] Enums { get; set; } + public int?[] NullableInts { get; set; } + + /// + /// An int array property with a custom, user-specified value converter. Should not be queryable, since we have no idea about the + /// actual representation. + /// + public int[] CustomConvertedInts { get; set; } + } + + public enum MyEnum { Value1, Value2, Value3, Value4 } + + public class PrimitiveArrayData : ISetSource + { + public IReadOnlyList PrimitiveArrayEntities { get; } + + public PrimitiveArrayData(PrimitiveCollectionsContext context = null) + { + PrimitiveArrayEntities = CreatePrimitiveArrayEntities(); + + if (context != null) + { + context.AddRange(PrimitiveArrayEntities); + context.SaveChanges(); + } + } + + public IQueryable Set() + where TEntity : class + { + if (typeof(TEntity) == typeof(PrimitiveCollectionsEntity)) + { + return (IQueryable)PrimitiveArrayEntities.AsQueryable(); + } + + throw new InvalidOperationException("Invalid entity type: " + typeof(TEntity)); + } + + private static IReadOnlyList CreatePrimitiveArrayEntities() + => new List + { + new() + { + Id = 1, + + Int = 10, + String = "10", + DateTime = new DateTime(2020, 1, 10, 12, 30, 0), + Bool = true, + Enum = MyEnum.Value1, + NullableInt = 10, + + Ints = new[] { 1, 10 }, + Strings = new[] { "1", "10" }, + DateTimes = new DateTime[] { new(2020, 1, 1, 12, 30, 0), new(2020, 1, 10, 12, 30, 0) }, + Bools = new[] { true, false }, + Enums = new[] { MyEnum.Value1, MyEnum.Value2 }, + NullableInts = new int?[] { 1, 10 }, + + CustomConvertedInts = new[] { 1, 10 }, + }, + new() + { + Id = 2, + + Int = 11, + String = "11", + DateTime = new DateTime(2020, 1, 11, 12, 30, 0), + Bool = false, + Enum = MyEnum.Value2, + NullableInt = null, + + Ints = new[] { 1, 11, 111 }, + Strings = new[] { "1", "11", "111" }, + DateTimes = new DateTime[] { new(2020, 1, 1, 12, 30, 0), new(2020, 1, 11, 12, 30, 0), new(2020, 1, 31, 12, 30, 0) }, + Bools = new[] { false }, + Enums = new[] { MyEnum.Value2, MyEnum.Value3 }, + NullableInts = new int?[] { 1, 11, null }, + + CustomConvertedInts = new[] { 1, 11, 111 } + } + }; + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs index e9c32a9894f..ab44bc5934a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs @@ -12,7 +12,7 @@ public ComplexNavigationsCollectionsQuerySqlServerTest( : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } [ConditionalFact] @@ -1214,6 +1214,8 @@ public override async Task LeftJoin_with_Any_on_outer_source_and_projecting_coll AssertSql( """ +@__validIds_0='["L1 01","L1 02"]' (Size = 4000) + SELECT CASE WHEN [l0].[Id] IS NULL THEN 0 ELSE [l0].[Id] @@ -1221,7 +1223,10 @@ ELSE [l0].[Id] FROM [LevelOne] AS [l] LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Required_Id] LEFT JOIN [LevelThree] AS [l1] ON [l0].[Id] = [l1].[OneToMany_Required_Inverse3Id] -WHERE [l].[Name] IN (N'L1 01', N'L1 02') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__validIds_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [l].[Name] OR ([j].[Value] IS NULL AND [l].[Name] IS NULL)) ORDER BY [l].[Id], [l0].[Id] """); } @@ -2325,17 +2330,25 @@ public override async Task Collection_projection_over_GroupBy_over_parameter(boo AssertSql( """ +@__validIds_0='["L1 01","L1 02"]' (Size = 4000) + SELECT [t].[Date], [t0].[Id] FROM ( SELECT [l].[Date] FROM [LevelOne] AS [l] - WHERE [l].[Name] IN (N'L1 01', N'L1 02') + WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__validIds_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [l].[Name] OR ([j].[Value] IS NULL AND [l].[Name] IS NULL)) GROUP BY [l].[Date] ) AS [t] LEFT JOIN ( SELECT [l0].[Id], [l0].[Date] FROM [LevelOne] AS [l0] - WHERE [l0].[Name] IN (N'L1 01', N'L1 02') + WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__validIds_0) WITH ([Value] nvarchar(max) '$') AS [j0] + WHERE [j0].[Value] = [l0].[Name] OR ([j0].[Value] IS NULL AND [l0].[Name] IS NULL)) ) AS [t0] ON [t].[Date] = [t0].[Date] ORDER BY [t].[Date] """); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs index f7041ff82ca..bb5a6f6f509 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs @@ -12,7 +12,7 @@ public ComplexNavigationsCollectionsSharedTypeQuerySqlServerTest( : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } [ConditionalFact] @@ -2848,6 +2848,8 @@ public override async Task LeftJoin_with_Any_on_outer_source_and_projecting_coll AssertSql( """ +@__validIds_0='["L1 01","L1 02"]' (Size = 4000) + SELECT CASE WHEN [t0].[OneToOne_Required_PK_Date] IS NULL OR [t0].[Level1_Required_Id] IS NULL OR [t0].[OneToMany_Required_Inverse2Id] IS NULL THEN 0 WHEN [t0].[OneToOne_Required_PK_Date] IS NOT NULL AND [t0].[Level1_Required_Id] IS NOT NULL AND [t0].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t0].[Id0] @@ -2872,7 +2874,10 @@ WHERE [l2].[Level2_Required_Id] IS NOT NULL AND [l2].[OneToMany_Required_Inverse ) AS [t1] ON CASE WHEN [t0].[OneToOne_Required_PK_Date] IS NOT NULL AND [t0].[Level1_Required_Id] IS NOT NULL AND [t0].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t0].[Id0] END = [t1].[OneToMany_Required_Inverse3Id] -WHERE [l].[Name] IN (N'L1 01', N'L1 02') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__validIds_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [l].[Name] OR ([j].[Value] IS NULL AND [l].[Name] IS NULL)) ORDER BY [l].[Id], [t0].[Id], [t0].[Id0] """); } @@ -3009,17 +3014,25 @@ public override async Task Collection_projection_over_GroupBy_over_parameter(boo AssertSql( """ +@__validIds_0='["L1 01","L1 02"]' (Size = 4000) + SELECT [t].[Date], [t0].[Id] FROM ( SELECT [l].[Date] FROM [Level1] AS [l] - WHERE [l].[Name] IN (N'L1 01', N'L1 02') + WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__validIds_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [l].[Name] OR ([j].[Value] IS NULL AND [l].[Name] IS NULL)) GROUP BY [l].[Date] ) AS [t] LEFT JOIN ( SELECT [l0].[Id], [l0].[Date] FROM [Level1] AS [l0] - WHERE [l0].[Name] IN (N'L1 01', N'L1 02') + WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__validIds_0) WITH ([Value] nvarchar(max) '$') AS [j0] + WHERE [j0].[Value] = [l0].[Name] OR ([j0].[Value] IS NULL AND [l0].[Name] IS NULL)) ) AS [t0] ON [t].[Date] = [t0].[Date] ORDER BY [t].[Date] """); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySqlServerTest.cs index ffc5d625c4d..a71f15f8a9d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySqlServerTest.cs @@ -12,7 +12,7 @@ public ComplexNavigationsCollectionsSplitQuerySqlServerTest( : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } [ConditionalFact] @@ -3187,22 +3187,32 @@ public override async Task LeftJoin_with_Any_on_outer_source_and_projecting_coll AssertSql( """ +@__validIds_0='["L1 01","L1 02"]' (Size = 4000) + SELECT CASE WHEN [l0].[Id] IS NULL THEN 0 ELSE [l0].[Id] END, [l].[Id], [l0].[Id] FROM [LevelOne] AS [l] LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Required_Id] -WHERE [l].[Name] IN (N'L1 01', N'L1 02') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__validIds_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [l].[Name] OR ([j].[Value] IS NULL AND [l].[Name] IS NULL)) ORDER BY [l].[Id], [l0].[Id] """, // """ +@__validIds_0='["L1 01","L1 02"]' (Size = 4000) + SELECT [l1].[Id], [l1].[Level2_Optional_Id], [l1].[Level2_Required_Id], [l1].[Name], [l1].[OneToMany_Optional_Inverse3Id], [l1].[OneToMany_Optional_Self_Inverse3Id], [l1].[OneToMany_Required_Inverse3Id], [l1].[OneToMany_Required_Self_Inverse3Id], [l1].[OneToOne_Optional_PK_Inverse3Id], [l1].[OneToOne_Optional_Self3Id], [l].[Id], [l0].[Id] FROM [LevelOne] AS [l] LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Required_Id] INNER JOIN [LevelThree] AS [l1] ON [l0].[Id] = [l1].[OneToMany_Required_Inverse3Id] -WHERE [l].[Name] IN (N'L1 01', N'L1 02') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__validIds_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [l].[Name] OR ([j].[Value] IS NULL AND [l].[Name] IS NULL)) ORDER BY [l].[Id], [l0].[Id] """); } @@ -3734,25 +3744,38 @@ public override async Task Collection_projection_over_GroupBy_over_parameter(boo AssertSql( """ +@__validIds_0='["L1 01","L1 02"]' (Size = 4000) + SELECT [l].[Date] FROM [LevelOne] AS [l] -WHERE [l].[Name] IN (N'L1 01', N'L1 02') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__validIds_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [l].[Name] OR ([j].[Value] IS NULL AND [l].[Name] IS NULL)) GROUP BY [l].[Date] ORDER BY [l].[Date] """, // """ +@__validIds_0='["L1 01","L1 02"]' (Size = 4000) + SELECT [t0].[Id], [t].[Date] FROM ( SELECT [l].[Date] FROM [LevelOne] AS [l] - WHERE [l].[Name] IN (N'L1 01', N'L1 02') + WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__validIds_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [l].[Name] OR ([j].[Value] IS NULL AND [l].[Name] IS NULL)) GROUP BY [l].[Date] ) AS [t] INNER JOIN ( SELECT [l0].[Id], [l0].[Date] FROM [LevelOne] AS [l0] - WHERE [l0].[Name] IN (N'L1 01', N'L1 02') + WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__validIds_0) WITH ([Value] nvarchar(max) '$') AS [j0] + WHERE [j0].[Value] = [l0].[Name] OR ([j0].[Value] IS NULL AND [l0].[Name] IS NULL)) ) AS [t0] ON [t].[Date] = [t0].[Date] ORDER BY [t].[Date] """); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs index d55fd460371..71a02fed772 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs @@ -3076,10 +3076,15 @@ public override async Task Accessing_optional_property_inside_result_operator_su AssertSql( """ +@__names_0='["Name1","Name2"]' (Size = 4000) + SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id] FROM [LevelOne] AS [l] LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Optional_Id] -WHERE [l0].[Name] NOT IN (N'Name1', N'Name2') OR [l0].[Name] IS NULL +WHERE NOT EXISTS ( + SELECT 1 + FROM OpenJson(@__names_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE ([l0].[Name] = [j].[Value] AND [l0].[Name] IS NOT NULL AND [j].[Value] IS NOT NULL) OR ([l0].[Name] IS NULL AND [j].[Value] IS NULL)) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs index 1c2cbc0bf24..b37cbe71166 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs @@ -5403,6 +5403,8 @@ public override async Task Accessing_optional_property_inside_result_operator_su AssertSql( """ +@__names_0='["Name1","Name2"]' (Size = 4000) + SELECT [l].[Id], [l].[Date], [l].[Name] FROM [Level1] AS [l] LEFT JOIN ( @@ -5410,7 +5412,10 @@ LEFT JOIN ( FROM [Level1] AS [l0] WHERE [l0].[OneToOne_Required_PK_Date] IS NOT NULL AND [l0].[Level1_Required_Id] IS NOT NULL AND [l0].[OneToMany_Required_Inverse2Id] IS NOT NULL ) AS [t] ON [l].[Id] = [t].[Level1_Optional_Id] -WHERE [t].[Level2_Name] NOT IN (N'Name1', N'Name2') OR [t].[Level2_Name] IS NULL +WHERE NOT EXISTS ( + SELECT 1 + FROM OpenJson(@__names_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE ([t].[Level2_Name] = [j].[Value] AND [t].[Level2_Name] IS NOT NULL AND [j].[Value] IS NOT NULL) OR ([t].[Level2_Name] IS NULL AND [j].[Value] IS NULL)) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index 46cf72f8201..32fc7676a80 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -13,7 +13,7 @@ public GearsOfWarQuerySqlServerTest(GearsOfWarQuerySqlServerFixture fixture, ITe : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } protected override bool CanExecuteQueryString @@ -211,10 +211,15 @@ FROM [Tags] AS [t] """, // """ +@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note] FROM [Gears] AS [g] LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId] -WHERE [t].[Id] IS NOT NULL AND [t].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57') +WHERE [t].[Id] IS NOT NULL AND EXISTS ( + SELECT 1 + FROM OpenJson(@__tags_0) WITH ([Value] uniqueidentifier '$') AS [j] + WHERE [j].[Value] = [t].[Id] OR ([j].[Value] IS NULL AND [t].[Id] IS NULL)) """); } @@ -229,11 +234,16 @@ FROM [Tags] AS [t] """, // """ +@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note] FROM [Gears] AS [g] INNER JOIN [Cities] AS [c] ON [g].[CityOfBirthName] = [c].[Name] LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId] -WHERE [c].[Location] IS NOT NULL AND [t].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57') +WHERE [c].[Location] IS NOT NULL AND EXISTS ( + SELECT 1 + FROM OpenJson(@__tags_0) WITH ([Value] uniqueidentifier '$') AS [j] + WHERE [j].[Value] = [t].[Id] OR ([j].[Value] IS NULL AND [t].[Id] IS NULL)) """); } @@ -248,10 +258,15 @@ FROM [Tags] AS [t] """, // """ +@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId] -WHERE [t].[Id] IS NOT NULL AND [t].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57') +WHERE [t].[Id] IS NOT NULL AND EXISTS ( + SELECT 1 + FROM OpenJson(@__tags_0) WITH ([Value] uniqueidentifier '$') AS [j] + WHERE [j].[Value] = [t].[Id] OR ([j].[Value] IS NULL AND [t].[Id] IS NULL)) """); } @@ -2085,9 +2100,14 @@ public override async Task Non_unicode_string_literals_in_contains_is_used_for_n AssertSql( """ +@__cities_0='["Unknown","Jacinto\u0027s location","Ephyra\u0027s location"]' (Size = 4000) + SELECT [c].[Name], [c].[Location], [c].[Nation] FROM [Cities] AS [c] -WHERE [c].[Location] IN ('Unknown', 'Jacinto''s location', 'Ephyra''s location') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__cities_0) WITH ([Value] varchar(100) '$') AS [j] + WHERE [j].[Value] = [c].[Location] OR ([j].[Value] IS NULL AND [c].[Location] IS NULL)) """); } @@ -3082,9 +3102,14 @@ public override async Task Contains_with_local_nullable_guid_list_closure(bool a AssertSql( """ +@__ids_0='["d2c26679-562b-44d1-ab96-23d1775e0926","23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3","ab1b82d7-88db-42bd-a132-7eef9aa68af4"]' (Size = 4000) + SELECT [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note] FROM [Tags] AS [t] -WHERE [t].[Id] IN ('d2c26679-562b-44d1-ab96-23d1775e0926', '23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3', 'ab1b82d7-88db-42bd-a132-7eef9aa68af4') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] uniqueidentifier '$') AS [j] + WHERE [j].[Value] = [t].[Id]) """); } @@ -3600,10 +3625,15 @@ public override async Task Contains_on_nullable_array_produces_correct_sql(bool AssertSql( """ +@__cities_0='["Ephyra",null]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] LEFT JOIN [Cities] AS [c] ON [g].[AssignedCityName] = [c].[Name] -WHERE [g].[SquadId] < 2 AND ([c].[Name] = N'Ephyra' OR [c].[Name] IS NULL) +WHERE [g].[SquadId] < 2 AND EXISTS ( + SELECT 1 + FROM OpenJson(@__cities_0) WITH ([Value] nvarchar(450) '$') AS [j] + WHERE [j].[Value] = [c].[Name] OR ([j].[Value] IS NULL AND [c].[Name] IS NULL)) """); } @@ -5886,10 +5916,18 @@ public override async Task Correlated_collection_with_complex_order_by_funcletiz AssertSql( """ +@__nicknames_0='[]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [w].[Name], [w].[Id] FROM [Gears] AS [g] LEFT JOIN [Weapons] AS [w] ON [g].[FullName] = [w].[OwnerFullName] -ORDER BY [g].[Nickname], [g].[SquadId] +ORDER BY CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__nicknames_0) WITH ([Value] nvarchar(450) '$') AS [j] + WHERE [j].[Value] = [g].[Nickname]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END DESC, [g].[Nickname], [g].[SquadId] """); } @@ -6625,10 +6663,14 @@ public override async Task DateTimeOffset_Contains_Less_than_Greater_than(bool a """ @__start_0='1902-01-01T10:00:00.1234567+01:30' @__end_1='1902-01-03T10:00:00.1234567+01:30' +@__dates_2='["1902-01-02T10:00:00.1234567+01:30"]' (Size = 4000) SELECT [m].[Id], [m].[BriefingDocument], [m].[BriefingDocumentFileExtension], [m].[CodeName], [m].[Date], [m].[Duration], [m].[Rating], [m].[Time], [m].[Timeline] FROM [Missions] AS [m] -WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND [m].[Timeline] = '1902-01-02T10:00:00.1234567+01:30' +WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dates_2) WITH ([Value] datetimeoffset '$') AS [j] + WHERE [j].[Value] = [m].[Timeline]) """); } @@ -7335,8 +7377,17 @@ public override async Task OrderBy_Contains_empty_list(bool async) AssertSql( """ +@__ids_0='[]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] +ORDER BY CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [g].[SquadId]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END """); } @@ -8087,10 +8138,15 @@ public override async Task Enum_array_contains(bool async) AssertSql( """ +@__types_0='[null,1]' (Size = 4000) + SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] LEFT JOIN [Weapons] AS [w0] ON [w].[SynergyWithId] = [w0].[Id] -WHERE [w0].[Id] IS NOT NULL AND ([w0].[AmmunitionType] = 1 OR [w0].[AmmunitionType] IS NULL) +WHERE [w0].[Id] IS NOT NULL AND EXISTS ( + SELECT 1 + FROM OpenJson(@__types_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [w0].[AmmunitionType] OR ([j].[Value] IS NULL AND [w0].[AmmunitionType] IS NULL)) """); } @@ -9319,9 +9375,14 @@ public override async Task Where_bool_column_and_Contains(bool async) AssertSql( """ +@__values_0='[false,true]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] -WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND [g].[HasSoulPatch] IN (CAST(0 AS bit), CAST(1 AS bit)) +WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__values_0) WITH ([Value] bit '$') AS [j] + WHERE [j].[Value] = [g].[HasSoulPatch]) """); } @@ -9331,9 +9392,14 @@ public override async Task Where_bool_column_or_Contains(bool async) AssertSql( """ +@__values_0='[false,true]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] -WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND [g].[HasSoulPatch] IN (CAST(0 AS bit), CAST(1 AS bit)) +WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__values_0) WITH ([Value] bit '$') AS [j] + WHERE [j].[Value] = [g].[HasSoulPatch]) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs new file mode 100644 index 00000000000..576ca53c16f --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using NetTopologySuite.Geometries; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class NonSharedPrimitiveCollectionsQuerySqlServerTest : NonSharedPrimitiveCollectionsQueryRelationalTestBase +{ + [ConditionalFact] // #30630 + public override async Task Collection_of_geometry_is_not_mapped() + { + var exception = await Assert.ThrowsAsync( + () => InitializeAsync( + onConfiguring: options => options.UseSqlServer(o => o.UseNetTopologySuite()), + addServices: s => s.AddEntityFrameworkSqlServerNetTopologySuite(), + onModelCreating: mb => mb.Entity().Property("Points"))); + + Assert.Equal(CoreStrings.PropertyNotMapped("Point[]", "MyEntity", "Points"), exception.Message); + } + + #region Old SQL Server versions (no OPENJSON) + + [ConditionalFact] + public virtual async Task Column_is_mapped_across_SqlServer_versions() + { + var contextFactory = await InitializeAsync( + onModelCreating: mb => mb.Entity(), + onConfiguring: options => options.UseSqlServer(o => o.UseCompatibilityLevel(120)), + seed: context => + { + context.Add(new MyEntity { Ints = new[] { 1, 2, 3 } }); + context.SaveChanges(); + }); + + await using var context = contextFactory.CreateContext(); + + var result = await context.Set().SingleAsync(); + Assert.Equivalent(new[] { 1, 2, 3 }, result.Ints); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Column_is_not_queryable_on_old_SqlServer_versions(bool useOldCompatibilityMode) + { + var contextFactory = await InitializeAsync( + onModelCreating: mb => mb.Entity(), + onConfiguring: options => options.UseSqlServer(o => o.UseCompatibilityLevel(useOldCompatibilityMode ? 120 : 130)), + seed: context => + { + context.Add(new MyEntity { Id = 1, Ints = new[] { 1, 2 } }); + context.Add(new MyEntity { Id = 2, Ints = new[] { 1, 2, 3 } }); + context.SaveChanges(); + }); + + await using var context = contextFactory.CreateContext(); + + var query = context.Set().Where(e => e.Ints.Count(i => i > 1) == 2); + + if (useOldCompatibilityMode) + { + await Assert.ThrowsAsync(async () => await query.ToListAsync()); + } + else + { + var result = await query.SingleAsync(); + Assert.Equal(2, result.Id); + } + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Contains_with_parameter_works_across_SqlServer_versions(bool useOldCompatibilityMode) + { + var contextFactory = await InitializeAsync( + onModelCreating: mb => mb.Entity(), + onConfiguring: options => options.UseSqlServer(o => o.UseCompatibilityLevel(useOldCompatibilityMode ? 120 : 130)), + seed: context => + { + context.AddRange( + new MyEntity { Id = 1 }, + new MyEntity { Id = 2 }); + context.SaveChanges(); + }); + + await using var context = contextFactory.CreateContext(); + + var ids = new[] { 2, 3, 4 }; + var result = await context.Set().Where(e => ids.Contains(e.Id)).SingleAsync(); + Assert.Equal(2, result.Id); + + if (useOldCompatibilityMode) + { + AssertSql( +""" +SELECT TOP(2) [m].[Id], [m].[Ints] +FROM [MyEntity] AS [m] +WHERE [m].[Id] IN (2, 3, 4) +"""); + } + else + { + AssertSql( +""" +@__ids_0='[2,3,4]' (Size = 4000) + +SELECT TOP(2) [m].[Id], [m].[Ints] +FROM [MyEntity] AS [m] +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [m].[Id]) +"""); + } + } + + #endregion + + private class MyContext : DbContext + { + public MyContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); + } + + private class MyEntity + { + public int Id { get; set; } + + public int[] Ints { get; set; } + } + + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs index e12bc821892..9a375a36163 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs @@ -14,7 +14,7 @@ public NorthwindAggregateOperatorsQuerySqlServerTest( : base(fixture) { ClearLog(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } protected override bool CanExecuteQueryString @@ -1550,15 +1550,25 @@ public override async Task Contains_with_local_array_closure(bool async) AssertSql( """ +@__ids_0='["ABCDE","ALFKI"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] IN (N'ABCDE', N'ALFKI') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) """, // """ +@__ids_0='["ABCDE"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] = N'ABCDE' +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) """); } @@ -1568,21 +1578,31 @@ public override async Task Contains_with_subquery_and_local_array_closure(bool a AssertSql( """ +@__ids_0='["London","Buenos Aires"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] WHERE EXISTS ( SELECT 1 FROM [Customers] AS [c0] - WHERE [c0].[City] IN (N'London', N'Buenos Aires') AND [c0].[CustomerID] = [c].[CustomerID]) + WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nvarchar(15) '$') AS [j] + WHERE [j].[Value] = [c0].[City] OR ([j].[Value] IS NULL AND [c0].[City] IS NULL)) AND [c0].[CustomerID] = [c].[CustomerID]) """, // """ +@__ids_0='["London"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] WHERE EXISTS ( SELECT 1 FROM [Customers] AS [c0] - WHERE [c0].[City] = N'London' AND [c0].[CustomerID] = [c].[CustomerID]) + WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nvarchar(15) '$') AS [j] + WHERE [j].[Value] = [c0].[City] OR ([j].[Value] IS NULL AND [c0].[City] IS NULL)) AND [c0].[CustomerID] = [c].[CustomerID]) """); } @@ -1592,15 +1612,25 @@ public override async Task Contains_with_local_uint_array_closure(bool async) AssertSql( """ +@__ids_0='[0,1]' (Size = 4000) + SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] -WHERE [e].[EmployeeID] IN (0, 1) +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[EmployeeID]) """, // """ +@__ids_0='[0]' (Size = 4000) + SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] -WHERE [e].[EmployeeID] = 0 +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[EmployeeID]) """); } @@ -1610,15 +1640,25 @@ public override async Task Contains_with_local_nullable_uint_array_closure(bool AssertSql( """ +@__ids_0='[0,1]' (Size = 4000) + SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] -WHERE [e].[EmployeeID] IN (0, 1) +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[EmployeeID]) """, // """ +@__ids_0='[0]' (Size = 4000) + SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] -WHERE [e].[EmployeeID] = 0 +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[EmployeeID]) """); } @@ -1640,9 +1680,14 @@ public override async Task Contains_with_local_list_closure(bool async) AssertSql( """ +@__ids_0='["ABCDE","ALFKI"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] IN (N'ABCDE', N'ALFKI') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) """); } @@ -1664,9 +1709,14 @@ public override async Task Contains_with_local_list_closure_all_null(bool async) AssertSql( """ +@__ids_0='[null,null]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE 0 = 1 +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) """); } @@ -1688,15 +1738,25 @@ public override async Task Contains_with_local_list_inline_closure_mix(bool asyn AssertSql( """ +@__p_0='["ABCDE","ALFKI"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] IN (N'ABCDE', N'ALFKI') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__p_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) """, // """ +@__p_0='["ABCDE","ANATR"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] IN (N'ABCDE', N'ANATR') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__p_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) """); } @@ -1736,9 +1796,14 @@ public override async Task Contains_with_local_collection_false(bool async) AssertSql( """ +@__ids_0='["ABCDE","ALFKI"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] NOT IN (N'ABCDE', N'ALFKI') +WHERE NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) """); } @@ -1748,9 +1813,14 @@ public override async Task Contains_with_local_collection_complex_predicate_and( AssertSql( """ +@__ids_0='["ABCDE","ALFKI"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] IN (N'ALFKI', N'ABCDE') AND [c].[CustomerID] IN (N'ABCDE', N'ALFKI') +WHERE [c].[CustomerID] IN (N'ALFKI', N'ABCDE') AND EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) """); } @@ -1784,9 +1854,14 @@ public override async Task Contains_with_local_collection_sql_injection(bool asy AssertSql( """ +@__ids_0='["ALFKI","ABC\u0027)); GO; DROP TABLE Orders; GO; --"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] IN (N'ALFKI', N'ABC'')); GO; DROP TABLE Orders; GO; --') OR [c].[CustomerID] IN (N'ALFKI', N'ABCDE') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) OR [c].[CustomerID] IN (N'ALFKI', N'ABCDE') """); } @@ -1796,9 +1871,14 @@ public override async Task Contains_with_local_collection_empty_closure(bool asy AssertSql( """ +@__ids_0='[]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE 0 = 1 +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) """); } @@ -2249,9 +2329,14 @@ public override async Task Where_subquery_any_equals_operator(bool async) AssertSql( """ +@__ids_0='["ABCDE","ALFKI","ANATR"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] IN (N'ABCDE', N'ALFKI', N'ANATR') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) """); } @@ -2273,9 +2358,14 @@ public override async Task Where_subquery_any_equals_static(bool async) AssertSql( """ +@__ids_0='["ABCDE","ALFKI","ANATR"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] IN (N'ABCDE', N'ALFKI', N'ANATR') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) """); } @@ -2285,15 +2375,25 @@ public override async Task Where_subquery_where_any(bool async) AssertSql( """ +@__ids_0='["ABCDE","ALFKI","ANATR"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[City] = N'México D.F.' AND [c].[CustomerID] IN (N'ABCDE', N'ALFKI', N'ANATR') +WHERE [c].[City] = N'México D.F.' AND EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) """, // """ +@__ids_0='["ABCDE","ALFKI","ANATR"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[City] = N'México D.F.' AND [c].[CustomerID] IN (N'ABCDE', N'ALFKI', N'ANATR') +WHERE [c].[City] = N'México D.F.' AND EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [c].[CustomerID] = [j].[Value]) """); } @@ -2303,9 +2403,14 @@ public override async Task Where_subquery_all_not_equals_operator(bool async) AssertSql( """ +@__ids_0='["ABCDE","ALFKI","ANATR"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] NOT IN (N'ABCDE', N'ALFKI', N'ANATR') +WHERE NOT EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID] AND [j].[Value] IS NOT NULL) """); } @@ -2327,9 +2432,14 @@ public override async Task Where_subquery_all_not_equals_static(bool async) AssertSql( """ +@__ids_0='["ABCDE","ALFKI","ANATR"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] NOT IN (N'ABCDE', N'ALFKI', N'ANATR') +WHERE NOT EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) """); } @@ -2339,15 +2449,25 @@ public override async Task Where_subquery_where_all(bool async) AssertSql( """ +@__ids_0='["ABCDE","ALFKI","ANATR"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[City] = N'México D.F.' AND [c].[CustomerID] NOT IN (N'ABCDE', N'ALFKI', N'ANATR') +WHERE [c].[City] = N'México D.F.' AND NOT EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID] AND [j].[Value] IS NOT NULL) """, // """ +@__ids_0='["ABCDE","ALFKI","ANATR"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[City] = N'México D.F.' AND [c].[CustomerID] NOT IN (N'ABCDE', N'ALFKI', N'ANATR') +WHERE [c].[City] = N'México D.F.' AND NOT EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [c].[CustomerID] = [j].[Value] AND [j].[Value] IS NOT NULL) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindCompiledQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindCompiledQuerySqlServerTest.cs index b6d6df1c188..d80aa72dae8 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindCompiledQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindCompiledQuerySqlServerTest.cs @@ -178,15 +178,25 @@ public override void Query_with_contains() AssertSql( """ +@__args='["ALFKI"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] = N'ALFKI' +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__args) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) """, // """ +@__args='["ANATR"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] = N'ANATR' +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__args) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) """); } @@ -395,56 +405,62 @@ public override void MakeBinary_does_not_throw_for_unsupported_operator() public override void Query_with_array_parameter() { - var query = EF.CompileQuery( - (NorthwindContext context, string[] args) - => context.Customers.Where(c => c.CustomerID == args[0])); - - using (var context = CreateContext()) - { - Assert.Equal( - CoreStrings.TranslationFailedWithDetails( - "DbSet() .Where(c => c.CustomerID == __args .ElementAt(0))", - CoreStrings.QueryUnableToTranslateMethod("System.Linq.Enumerable", nameof(Enumerable.ElementAt))), - Assert.Throws( - () => query(context, new[] { "ALFKI" }).First().CustomerID).Message.Replace("\r", "").Replace("\n", "")); - } - - using (var context = CreateContext()) - { - Assert.Equal( - CoreStrings.TranslationFailedWithDetails( - "DbSet() .Where(c => c.CustomerID == __args .ElementAt(0))", - CoreStrings.QueryUnableToTranslateMethod("System.Linq.Enumerable", nameof(Enumerable.ElementAt))), - Assert.Throws( - () => query(context, new[] { "ANATR" }).First().CustomerID).Message.Replace("\r", "").Replace("\n", "")); - } + base.Query_with_array_parameter(); + + AssertSql( +""" +@__args='["ALFKI"]' (Size = 4000) + +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Customers] AS [c] +WHERE [c].[CustomerID] = ( + SELECT [j].[Value] + FROM OpenJson(@__args) WITH ([Value] nchar(5) '$') AS [j] + ORDER BY (SELECT 1) + OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) +""", + // +""" +@__args='["ANATR"]' (Size = 4000) + +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Customers] AS [c] +WHERE [c].[CustomerID] = ( + SELECT [j].[Value] + FROM OpenJson(@__args) WITH ([Value] nchar(5) '$') AS [j] + ORDER BY (SELECT 1) + OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) +"""); } public override async Task Query_with_array_parameter_async() { - var query = EF.CompileAsyncQuery( - (NorthwindContext context, string[] args) - => context.Customers.Where(c => c.CustomerID == args[0])); - - using (var context = CreateContext()) - { - Assert.Equal( - CoreStrings.TranslationFailedWithDetails( - "DbSet() .Where(c => c.CustomerID == __args .ElementAt(0))", - CoreStrings.QueryUnableToTranslateMethod("System.Linq.Enumerable", nameof(Enumerable.ElementAt))), - (await Assert.ThrowsAsync( - () => Enumerate(query(context, new[] { "ALFKI" })))).Message.Replace("\r", "").Replace("\n", "")); - } - - using (var context = CreateContext()) - { - Assert.Equal( - CoreStrings.TranslationFailedWithDetails( - "DbSet() .Where(c => c.CustomerID == __args .ElementAt(0))", - CoreStrings.QueryUnableToTranslateMethod("System.Linq.Enumerable", nameof(Enumerable.ElementAt))), - (await Assert.ThrowsAsync( - () => Enumerate(query(context, new[] { "ANATR" })))).Message.Replace("\r", "").Replace("\n", "")); - } + await base.Query_with_array_parameter_async(); + + AssertSql( +""" +@__args='["ALFKI"]' (Size = 4000) + +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Customers] AS [c] +WHERE [c].[CustomerID] = ( + SELECT [j].[Value] + FROM OpenJson(@__args) WITH ([Value] nchar(5) '$') AS [j] + ORDER BY (SELECT 1) + OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) +""", + // +""" +@__args='["ANATR"]' (Size = 4000) + +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Customers] AS [c] +WHERE [c].[CustomerID] = ( + SELECT [j].[Value] + FROM OpenJson(@__args) WITH ([Value] nchar(5) '$') AS [j] + ORDER BY (SELECT 1) + OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) +"""); } public override void Multiple_queries() diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindEFPropertyIncludeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindEFPropertyIncludeQuerySqlServerTest.cs index e9cb3da6941..8e4804f2704 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindEFPropertyIncludeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindEFPropertyIncludeQuerySqlServerTest.cs @@ -616,18 +616,25 @@ public override async Task Include_collection_OrderBy_list_does_not_contains(boo AssertSql( """ +@__list_0='["ALFKI"]' (Size = 4000) @__p_1='1' SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM ( SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE - WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit) + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' ORDER BY CASE - WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit) + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END OFFSET @__p_1 ROWS @@ -957,14 +964,27 @@ public override async Task Include_collection_OrderBy_empty_list_contains(bool a AssertSql( """ +@__list_0='[]' (Size = 4000) @__p_1='1' SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM ( - SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CAST(0 AS bit) AS [c] + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' - ORDER BY (SELECT 1) + ORDER BY CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END OFFSET @__p_1 ROWS ) AS [t] LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] @@ -1340,18 +1360,25 @@ public override async Task Include_collection_OrderBy_list_contains(bool async) AssertSql( """ +@__list_0='["ALFKI"]' (Size = 4000) @__p_1='1' SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM ( SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE - WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit) + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' ORDER BY CASE - WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit) + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END OFFSET @__p_1 ROWS @@ -1812,14 +1839,27 @@ public override async Task Include_collection_OrderBy_empty_list_does_not_contai AssertSql( """ +@__list_0='[]' (Size = 4000) @__p_1='1' SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM ( - SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CAST(1 AS bit) AS [c] + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' - ORDER BY (SELECT 1) + ORDER BY CASE + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END OFFSET @__p_1 ROWS ) AS [t] LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeNoTrackingQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeNoTrackingQuerySqlServerTest.cs index b50fb07400b..f657fe5d0e7 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeNoTrackingQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeNoTrackingQuerySqlServerTest.cs @@ -170,18 +170,25 @@ public override async Task Include_collection_OrderBy_list_does_not_contains(boo AssertSql( """ +@__list_0='["ALFKI"]' (Size = 4000) @__p_1='1' SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM ( SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE - WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit) + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' ORDER BY CASE - WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit) + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END OFFSET @__p_1 ROWS @@ -445,18 +452,25 @@ public override async Task Include_collection_OrderBy_list_contains(bool async) AssertSql( """ +@__list_0='["ALFKI"]' (Size = 4000) @__p_1='1' SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM ( SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE - WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit) + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' ORDER BY CASE - WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit) + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END OFFSET @__p_1 ROWS @@ -2039,14 +2053,27 @@ public override async Task Include_collection_OrderBy_empty_list_contains(bool a AssertSql( """ +@__list_0='[]' (Size = 4000) @__p_1='1' SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM ( - SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CAST(0 AS bit) AS [c] + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' - ORDER BY (SELECT 1) + ORDER BY CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END OFFSET @__p_1 ROWS ) AS [t] LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] @@ -2060,14 +2087,27 @@ public override async Task Include_collection_OrderBy_empty_list_does_not_contai AssertSql( """ +@__list_0='[]' (Size = 4000) @__p_1='1' SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM ( - SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CAST(1 AS bit) AS [c] + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' - ORDER BY (SELECT 1) + ORDER BY CASE + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END OFFSET @__p_1 ROWS ) AS [t] LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeQuerySqlServerTest.cs index 5cbbb69b7b8..2d4f1a0ecdb 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeQuerySqlServerTest.cs @@ -1503,14 +1503,27 @@ public override async Task Include_collection_OrderBy_empty_list_contains(bool a AssertSql( """ +@__list_0='[]' (Size = 4000) @__p_1='1' SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM ( - SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CAST(0 AS bit) AS [c] + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' - ORDER BY (SELECT 1) + ORDER BY CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END OFFSET @__p_1 ROWS ) AS [t] LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] @@ -1524,14 +1537,27 @@ public override async Task Include_collection_OrderBy_empty_list_does_not_contai AssertSql( """ +@__list_0='[]' (Size = 4000) @__p_1='1' SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM ( - SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CAST(1 AS bit) AS [c] + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' - ORDER BY (SELECT 1) + ORDER BY CASE + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END OFFSET @__p_1 ROWS ) AS [t] LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] @@ -1545,18 +1571,25 @@ public override async Task Include_collection_OrderBy_list_contains(bool async) AssertSql( """ +@__list_0='["ALFKI"]' (Size = 4000) @__p_1='1' SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM ( SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE - WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit) + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' ORDER BY CASE - WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit) + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END OFFSET @__p_1 ROWS @@ -1572,18 +1605,25 @@ public override async Task Include_collection_OrderBy_list_does_not_contains(boo AssertSql( """ +@__list_0='["ALFKI"]' (Size = 4000) @__p_1='1' SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM ( SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE - WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit) + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' ORDER BY CASE - WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit) + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END OFFSET @__p_1 ROWS diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index 1d68ed3f0a1..ecf4a6ef3f9 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -17,7 +17,7 @@ public NorthwindMiscellaneousQuerySqlServerTest( : base(fixture) { ClearLog(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } protected override bool CanExecuteQueryString @@ -4035,15 +4035,25 @@ public override async Task Contains_with_DateTime_Date(bool async) AssertSql( """ +@__dates_0='["1996-07-04T00:00:00","1996-07-16T00:00:00"]' (Size = 4000) + SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM [Orders] AS [o] -WHERE CONVERT(date, [o].[OrderDate]) IN ('1996-07-04T00:00:00.000', '1996-07-16T00:00:00.000') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__dates_0) WITH ([Value] datetime '$') AS [j] + WHERE [j].[Value] = CONVERT(date, [o].[OrderDate])) """, // """ +@__dates_0='["1996-07-04T00:00:00"]' (Size = 4000) + SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM [Orders] AS [o] -WHERE CONVERT(date, [o].[OrderDate]) = '1996-07-04T00:00:00.000' +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__dates_0) WITH ([Value] datetime '$') AS [j] + WHERE [j].[Value] = CONVERT(date, [o].[OrderDate])) """); } @@ -5070,8 +5080,17 @@ public override async Task OrderBy_empty_list_contains(bool async) AssertSql( """ +@__list_0='[]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] +ORDER BY CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END """); } @@ -5081,8 +5100,17 @@ public override async Task OrderBy_empty_list_does_not_contains(bool async) AssertSql( """ +@__list_0='[]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] +ORDER BY CASE + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END """); } @@ -6241,10 +6269,15 @@ FROM [Customers] AS [c] """, // """ +@__orderIds_0='[10643,10692,10702,10835,10952,11011]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Orders] AS [o] LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] -WHERE [o].[OrderID] IN (10643, 10692, 10702, 10835, 10952, 11011) +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__orderIds_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [o].[OrderID]) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindNavigationsQuerySqlServerTest.cs index 9caea5b48a4..a207d1dd3b2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindNavigationsQuerySqlServerTest.cs @@ -12,6 +12,7 @@ public NorthwindNavigationsQuerySqlServerTest( : base(fixture) { fixture.TestSqlLoggerFactory.Clear(); + //fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } protected override bool CanExecuteQueryString @@ -773,6 +774,8 @@ public override async Task Collection_select_nav_prop_first_or_default_then_nav_ AssertSql( """ +@__orderIds_0='[10643,10692,10702,10835,10952,11011]' (Size = 4000) + SELECT [t0].[CustomerID], [t0].[Address], [t0].[City], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Country], [t0].[Fax], [t0].[Phone], [t0].[PostalCode], [t0].[Region] FROM [Customers] AS [c] LEFT JOIN ( @@ -781,7 +784,10 @@ LEFT JOIN ( SELECT [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region], [o].[CustomerID] AS [CustomerID0], ROW_NUMBER() OVER(PARTITION BY [o].[CustomerID] ORDER BY [o].[OrderID], [c0].[CustomerID]) AS [row] FROM [Orders] AS [o] LEFT JOIN [Customers] AS [c0] ON [o].[CustomerID] = [c0].[CustomerID] - WHERE [o].[OrderID] IN (10643, 10692, 10702, 10835, 10952, 11011) + WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__orderIds_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [o].[OrderID]) ) AS [t] WHERE [t].[row] <= 1 ) AS [t0] ON [c].[CustomerID] = [t0].[CustomerID0] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs index 646c2411e1e..8013d178410 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs @@ -12,7 +12,7 @@ public NorthwindSelectQuerySqlServerTest( : base(fixture) { ClearLog(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } protected override bool CanExecuteQueryString @@ -2078,6 +2078,8 @@ public override async Task Projecting_after_navigation_and_distinct(bool async) AssertSql( """ +@__filteredOrderIds_0='[10248,10249,10250]' (Size = 4000) + SELECT [t].[CustomerID], [t0].[CustomerID], [t0].[OrderID], [t0].[OrderDate] FROM ( SELECT DISTINCT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] @@ -2087,7 +2089,10 @@ FROM [Orders] AS [o] OUTER APPLY ( SELECT [t].[CustomerID], [o0].[OrderID], [o0].[OrderDate] FROM [Orders] AS [o0] - WHERE [t].[CustomerID] IS NOT NULL AND [t].[CustomerID] = [o0].[CustomerID] AND [o0].[OrderID] IN (10248, 10249, 10250) + WHERE [t].[CustomerID] IS NOT NULL AND [t].[CustomerID] = [o0].[CustomerID] AND EXISTS ( + SELECT 1 + FROM OpenJson(@__filteredOrderIds_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [o0].[OrderID]) ) AS [t0] ORDER BY [t].[CustomerID], [t0].[OrderID] """); @@ -2099,6 +2104,8 @@ public override async Task Correlated_collection_after_distinct_with_complex_pro AssertSql( """ +@__filteredOrderIds_0='[10248,10249,10250]' (Size = 4000) + SELECT [t].[OrderID], [t].[Complex], [t0].[Outer], [t0].[Inner], [t0].[OrderDate] FROM ( SELECT DISTINCT [o].[OrderID], DATEPART(month, [o].[OrderDate]) AS [Complex] @@ -2107,7 +2114,10 @@ FROM [Orders] AS [o] OUTER APPLY ( SELECT [t].[OrderID] AS [Outer], [o0].[OrderID] AS [Inner], [o0].[OrderDate] FROM [Orders] AS [o0] - WHERE [o0].[OrderID] = [t].[OrderID] AND [o0].[OrderID] IN (10248, 10249, 10250) + WHERE [o0].[OrderID] = [t].[OrderID] AND EXISTS ( + SELECT 1 + FROM OpenJson(@__filteredOrderIds_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [o0].[OrderID]) ) AS [t0] ORDER BY [t].[OrderID] """); @@ -2119,6 +2129,8 @@ public override async Task Correlated_collection_after_distinct_not_containing_o AssertSql( """ +@__filteredOrderIds_0='[10248,10249,10250]' (Size = 4000) + SELECT [t].[OrderDate], [t].[CustomerID], [t0].[Outer1], [t0].[Outer2], [t0].[Inner], [t0].[OrderDate] FROM ( SELECT DISTINCT [o].[OrderDate], [o].[CustomerID] @@ -2127,7 +2139,10 @@ FROM [Orders] AS [o] OUTER APPLY ( SELECT [t].[OrderDate] AS [Outer1], [t].[CustomerID] AS [Outer2], [o0].[OrderID] AS [Inner], [o0].[OrderDate] FROM [Orders] AS [o0] - WHERE ([o0].[CustomerID] = [t].[CustomerID] OR ([o0].[CustomerID] IS NULL AND [t].[CustomerID] IS NULL)) AND [o0].[OrderID] IN (10248, 10249, 10250) + WHERE ([o0].[CustomerID] = [t].[CustomerID] OR ([o0].[CustomerID] IS NULL AND [t].[CustomerID] IS NULL)) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__filteredOrderIds_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [o0].[OrderID]) ) AS [t0] ORDER BY [t].[OrderDate], [t].[CustomerID] """); @@ -2151,6 +2166,8 @@ public override async Task Correlated_collection_after_groupby_with_complex_proj AssertSql( """ +@__filteredOrderIds_0='[10248,10249,10250]' (Size = 4000) + SELECT [t0].[OrderID], [t0].[Complex], [t1].[Outer], [t1].[Inner], [t1].[OrderDate] FROM ( SELECT [t].[OrderID], [t].[Complex] @@ -2163,7 +2180,10 @@ FROM [Orders] AS [o] OUTER APPLY ( SELECT [t0].[OrderID] AS [Outer], [o0].[OrderID] AS [Inner], [o0].[OrderDate] FROM [Orders] AS [o0] - WHERE [o0].[OrderID] = [t0].[OrderID] AND [o0].[OrderID] IN (10248, 10249, 10250) + WHERE [o0].[OrderID] = [t0].[OrderID] AND EXISTS ( + SELECT 1 + FROM OpenJson(@__filteredOrderIds_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [o0].[OrderID]) ) AS [t1] ORDER BY [t0].[OrderID] """); @@ -2586,6 +2606,8 @@ public override async Task Correlated_collection_after_groupby_with_complex_proj AssertSql( """ +@__filteredOrderIds_0='[10248,10249,10250]' (Size = 4000) + SELECT [t0].[CustomerID], [t0].[Complex], [t1].[Outer], [t1].[Inner], [t1].[OrderDate] FROM ( SELECT [t].[CustomerID], [t].[Complex] @@ -2598,7 +2620,10 @@ FROM [Orders] AS [o] OUTER APPLY ( SELECT [t0].[CustomerID] AS [Outer], [o0].[OrderID] AS [Inner], [o0].[OrderDate] FROM [Orders] AS [o0] - WHERE ([o0].[CustomerID] = [t0].[CustomerID] OR ([o0].[CustomerID] IS NULL AND [t0].[CustomerID] IS NULL)) AND [o0].[OrderID] IN (10248, 10249, 10250) + WHERE ([o0].[CustomerID] = [t0].[CustomerID] OR ([o0].[CustomerID] IS NULL AND [t0].[CustomerID] IS NULL)) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__filteredOrderIds_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [o0].[OrderID]) ) AS [t1] ORDER BY [t0].[CustomerID], [t0].[Complex] """); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQuerySqlServerTest.cs index cf8f9e0e9ba..9a37f2fbe89 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQuerySqlServerTest.cs @@ -107,31 +107,42 @@ public override async Task Include_collection_OrderBy_list_does_not_contains(boo AssertSql( """ +@__list_0='["ALFKI"]' (Size = 4000) @__p_1='1' SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' ORDER BY CASE - WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit) + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END, [c].[CustomerID] OFFSET @__p_1 ROWS """, // """ +@__list_0='["ALFKI"]' (Size = 4000) @__p_1='1' SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [t].[CustomerID] FROM ( SELECT [c].[CustomerID], CASE - WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit) + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' ORDER BY CASE - WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit) + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END OFFSET @__p_1 ROWS @@ -1093,31 +1104,42 @@ public override async Task Include_collection_OrderBy_list_contains(bool async) AssertSql( """ +@__list_0='["ALFKI"]' (Size = 4000) @__p_1='1' SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' ORDER BY CASE - WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit) + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END, [c].[CustomerID] OFFSET @__p_1 ROWS """, // """ +@__list_0='["ALFKI"]' (Size = 4000) @__p_1='1' SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [t].[CustomerID] FROM ( SELECT [c].[CustomerID], CASE - WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit) + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' ORDER BY CASE - WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit) + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END OFFSET @__p_1 ROWS @@ -1529,24 +1551,44 @@ public override async Task Include_collection_OrderBy_empty_list_contains(bool a AssertSql( """ +@__list_0='[]' (Size = 4000) @__p_1='1' SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' -ORDER BY (SELECT 1), [c].[CustomerID] +ORDER BY CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END, [c].[CustomerID] OFFSET @__p_1 ROWS """, // """ +@__list_0='[]' (Size = 4000) @__p_1='1' SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [t].[CustomerID] FROM ( - SELECT [c].[CustomerID], CAST(0 AS bit) AS [c] + SELECT [c].[CustomerID], CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' - ORDER BY (SELECT 1) + ORDER BY CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END OFFSET @__p_1 ROWS ) AS [t] INNER JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] @@ -1688,24 +1730,44 @@ public override async Task Include_collection_OrderBy_empty_list_does_not_contai AssertSql( """ +@__list_0='[]' (Size = 4000) @__p_1='1' SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' -ORDER BY (SELECT 1), [c].[CustomerID] +ORDER BY CASE + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END, [c].[CustomerID] OFFSET @__p_1 ROWS """, // """ +@__list_0='[]' (Size = 4000) @__p_1='1' SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [t].[CustomerID] FROM ( - SELECT [c].[CustomerID], CAST(1 AS bit) AS [c] + SELECT [c].[CustomerID], CASE + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' - ORDER BY (SELECT 1) + ORDER BY CASE + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END OFFSET @__p_1 ROWS ) AS [t] INNER JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs index da9ff8928ea..f1538159682 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs @@ -2046,24 +2046,44 @@ public override async Task Include_collection_OrderBy_empty_list_contains(bool a AssertSql( """ +@__list_0='[]' (Size = 4000) @__p_1='1' SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' -ORDER BY (SELECT 1), [c].[CustomerID] +ORDER BY CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END, [c].[CustomerID] OFFSET @__p_1 ROWS """, // """ +@__list_0='[]' (Size = 4000) @__p_1='1' SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [t].[CustomerID] FROM ( - SELECT [c].[CustomerID], CAST(0 AS bit) AS [c] + SELECT [c].[CustomerID], CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' - ORDER BY (SELECT 1) + ORDER BY CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END OFFSET @__p_1 ROWS ) AS [t] INNER JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] @@ -2077,24 +2097,44 @@ public override async Task Include_collection_OrderBy_empty_list_does_not_contai AssertSql( """ +@__list_0='[]' (Size = 4000) @__p_1='1' SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' -ORDER BY (SELECT 1), [c].[CustomerID] +ORDER BY CASE + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END, [c].[CustomerID] OFFSET @__p_1 ROWS """, // """ +@__list_0='[]' (Size = 4000) @__p_1='1' SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [t].[CustomerID] FROM ( - SELECT [c].[CustomerID], CAST(1 AS bit) AS [c] + SELECT [c].[CustomerID], CASE + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' - ORDER BY (SELECT 1) + ORDER BY CASE + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END OFFSET @__p_1 ROWS ) AS [t] INNER JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] @@ -2108,31 +2148,42 @@ public override async Task Include_collection_OrderBy_list_contains(bool async) AssertSql( """ +@__list_0='["ALFKI"]' (Size = 4000) @__p_1='1' SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' ORDER BY CASE - WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit) + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END, [c].[CustomerID] OFFSET @__p_1 ROWS """, // """ +@__list_0='["ALFKI"]' (Size = 4000) @__p_1='1' SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [t].[CustomerID] FROM ( SELECT [c].[CustomerID], CASE - WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit) + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' ORDER BY CASE - WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit) + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END OFFSET @__p_1 ROWS @@ -2148,31 +2199,42 @@ public override async Task Include_collection_OrderBy_list_does_not_contains(boo AssertSql( """ +@__list_0='["ALFKI"]' (Size = 4000) @__p_1='1' SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' ORDER BY CASE - WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit) + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END, [c].[CustomerID] OFFSET @__p_1 ROWS """, // """ +@__list_0='["ALFKI"]' (Size = 4000) @__p_1='1' SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [t].[CustomerID] FROM ( SELECT [c].[CustomerID], CASE - WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit) + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' ORDER BY CASE - WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit) + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END OFFSET @__p_1 ROWS diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindStringIncludeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindStringIncludeQuerySqlServerTest.cs index 9dfc2f41cbd..3f69130b79e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindStringIncludeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindStringIncludeQuerySqlServerTest.cs @@ -616,18 +616,25 @@ public override async Task Include_collection_OrderBy_list_does_not_contains(boo AssertSql( """ +@__list_0='["ALFKI"]' (Size = 4000) @__p_1='1' SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM ( SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE - WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit) + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' ORDER BY CASE - WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit) + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END OFFSET @__p_1 ROWS @@ -957,14 +964,27 @@ public override async Task Include_collection_OrderBy_empty_list_contains(bool a AssertSql( """ +@__list_0='[]' (Size = 4000) @__p_1='1' SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM ( - SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CAST(0 AS bit) AS [c] + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' - ORDER BY (SELECT 1) + ORDER BY CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END OFFSET @__p_1 ROWS ) AS [t] LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] @@ -1340,18 +1360,25 @@ public override async Task Include_collection_OrderBy_list_contains(bool async) AssertSql( """ +@__list_0='["ALFKI"]' (Size = 4000) @__p_1='1' SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM ( SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE - WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit) + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' ORDER BY CASE - WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit) + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END OFFSET @__p_1 ROWS @@ -1812,14 +1839,27 @@ public override async Task Include_collection_OrderBy_empty_list_does_not_contai AssertSql( """ +@__list_0='[]' (Size = 4000) @__p_1='1' SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM ( - SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CAST(1 AS bit) AS [c] + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [c] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%' - ORDER BY (SELECT 1) + ORDER BY CASE + WHEN NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID])) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END OFFSET @__p_1 ROWS ) AS [t] LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs index 8d9fb99f2a0..1e8cb5eca99 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs @@ -12,7 +12,7 @@ public NorthwindWhereQuerySqlServerTest( : base(fixture) { ClearLog(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } protected override bool CanExecuteQueryString @@ -2081,9 +2081,14 @@ public override async Task Generic_Ilist_contains_translates_to_server(bool asyn AssertSql( """ +@__cities_0='["Seattle"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[City] = N'Seattle' +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__cities_0) WITH ([Value] nvarchar(15) '$') AS [j] + WHERE [j].[Value] = [c].[City] OR ([j].[Value] IS NULL AND [c].[City] IS NULL)) """); } @@ -2555,9 +2560,14 @@ public override async Task Array_of_parameters_Contains_OrElse_comparison_with_c // issue #21462 AssertSql( """ +@__p_0='["ALFKI","ANATR"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] IN (N'ALFKI', N'ANATR') OR [c].[CustomerID] = N'ANTON' +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__p_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) OR [c].[CustomerID] = N'ANTON' """); } @@ -2580,9 +2590,14 @@ public override async Task Parameter_array_Contains_OrElse_comparison_with_const AssertSql( """ +@__array_0='["ALFKI","ANATR"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] IN (N'ALFKI', N'ANATR') OR [c].[CustomerID] = N'ANTON' +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__array_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) OR [c].[CustomerID] = N'ANTON' """); } @@ -2593,11 +2608,15 @@ public override async Task Parameter_array_Contains_OrElse_comparison_with_param AssertSql( """ @__prm1_0='ANTON' (Size = 5) (DbType = StringFixedLength) +@__array_1='["ALFKI","ANATR"]' (Size = 4000) @__prm2_2='ALFKI' (Size = 5) (DbType = StringFixedLength) SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] = @__prm1_0 OR [c].[CustomerID] IN (N'ALFKI', N'ANATR') OR [c].[CustomerID] = @__prm2_2 +WHERE [c].[CustomerID] = @__prm1_0 OR EXISTS ( + SELECT 1 + FROM OpenJson(@__array_1) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) OR [c].[CustomerID] = @__prm2_2 """); } @@ -2899,9 +2918,14 @@ public override async Task Where_Contains_and_comparison(bool async) AssertSql( """ +@__customerIds_0='["ALFKI","FISSA"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] IN (N'ALFKI', N'FISSA') AND [c].[City] = N'Seattle' +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__customerIds_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) AND [c].[City] = N'Seattle' """); } @@ -2911,9 +2935,14 @@ public override async Task Where_Contains_or_comparison(bool async) AssertSql( """ +@__customerIds_0='["ALFKI","FISSA"]' (Size = 4000) + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] IN (N'ALFKI', N'FISSA') OR [c].[City] = N'Seattle' +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__customerIds_0) WITH ([Value] nchar(5) '$') AS [j] + WHERE [j].[Value] = [c].[CustomerID]) OR [c].[City] = N'Seattle' """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs index e302fe90f3d..d91705c392b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs @@ -12,7 +12,7 @@ public NullSemanticsQuerySqlServerTest(NullSemanticsQuerySqlServerFixture fixtur : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } public override async Task Compare_bool_with_bool_equal(bool async) @@ -911,9 +911,14 @@ public override async Task Contains_with_local_array_closure_with_null(bool asyn AssertSql( """ +@__ids_0='["Foo",null]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[NullableStringA] = N'Foo' OR [e].[NullableStringA] IS NULL +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [e].[NullableStringA] OR ([j].[Value] IS NULL AND [e].[NullableStringA] IS NULL)) """); } @@ -923,9 +928,14 @@ public override async Task Contains_with_local_array_closure_false_with_null(boo AssertSql( """ +@__ids_0='["Foo",null]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[NullableStringA] <> N'Foo' AND [e].[NullableStringA] IS NOT NULL +WHERE NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [e].[NullableStringA] OR ([j].[Value] IS NULL AND [e].[NullableStringA] IS NULL))) """); } @@ -935,9 +945,14 @@ public override async Task Contains_with_local_nullable_array_closure_negated(bo AssertSql( """ +@__ids_0='["Foo"]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[NullableStringA] <> N'Foo' OR [e].[NullableStringA] IS NULL +WHERE NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [e].[NullableStringA] OR ([j].[Value] IS NULL AND [e].[NullableStringA] IS NULL))) """); } @@ -947,9 +962,14 @@ public override async Task Contains_with_local_array_closure_with_multiple_nulls AssertSql( """ +@__ids_0='[null,"Foo",null,null]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[NullableStringA] = N'Foo' OR [e].[NullableStringA] IS NULL +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [e].[NullableStringA] OR ([j].[Value] IS NULL AND [e].[NullableStringA] IS NULL)) """); } @@ -1223,9 +1243,14 @@ public override async Task Where_conditional_search_condition_in_result(bool asy AssertSql( """ +@__list_0='["Foo","Bar"]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[StringA] IN (N'Foo', N'Bar') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__list_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [e].[StringA]) """, // """ @@ -1264,9 +1289,14 @@ public override void Where_contains_on_parameter_array_with_relational_null_sema AssertSql( """ +@__names_0='["Foo","Bar"]' (Size = 4000) + SELECT [e].[NullableStringA] FROM [Entities1] AS [e] -WHERE [e].[NullableStringA] IN (N'Foo', N'Bar') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__names_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [e].[NullableStringA]) """); } @@ -1276,9 +1306,14 @@ public override void Where_contains_on_parameter_empty_array_with_relational_nul AssertSql( """ +@__names_0='[]' (Size = 4000) + SELECT [e].[NullableStringA] FROM [Entities1] AS [e] -WHERE 0 = 1 +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__names_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [e].[NullableStringA]) """); } @@ -1288,9 +1323,14 @@ public override void Where_contains_on_parameter_array_with_just_null_with_relat AssertSql( """ +@__names_0='[null]' (Size = 4000) + SELECT [e].[NullableStringA] FROM [Entities1] AS [e] -WHERE [e].[NullableStringA] = NULL +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__names_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [e].[NullableStringA]) """); } @@ -1740,27 +1780,47 @@ public override async Task Null_semantics_contains(bool async) AssertSql( """ +@__ids_0='[1,2]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[NullableIntA] IN (1, 2) +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[NullableIntA] OR ([j].[Value] IS NULL AND [e].[NullableIntA] IS NULL)) """, // """ +@__ids_0='[1,2]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[NullableIntA] NOT IN (1, 2) OR [e].[NullableIntA] IS NULL +WHERE NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[NullableIntA] OR ([j].[Value] IS NULL AND [e].[NullableIntA] IS NULL))) """, // """ +@__ids2_0='[1,2,null]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[NullableIntA] IN (1, 2) OR [e].[NullableIntA] IS NULL +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids2_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[NullableIntA] OR ([j].[Value] IS NULL AND [e].[NullableIntA] IS NULL)) """, // """ +@__ids2_0='[1,2,null]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[NullableIntA] NOT IN (1, 2) AND [e].[NullableIntA] IS NOT NULL +WHERE NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__ids2_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[NullableIntA] OR ([j].[Value] IS NULL AND [e].[NullableIntA] IS NULL))) """, // """ @@ -1794,26 +1854,47 @@ public override async Task Null_semantics_contains_array_with_no_values(bool asy AssertSql( """ +@__ids_0='[]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE 0 = 1 +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[NullableIntA] OR ([j].[Value] IS NULL AND [e].[NullableIntA] IS NULL)) """, // """ +@__ids_0='[]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] +WHERE NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[NullableIntA] OR ([j].[Value] IS NULL AND [e].[NullableIntA] IS NULL))) """, // """ +@__ids2_0='[null]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[NullableIntA] IS NULL +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids2_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[NullableIntA] OR ([j].[Value] IS NULL AND [e].[NullableIntA] IS NULL)) """, // """ +@__ids2_0='[null]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[NullableIntA] IS NOT NULL +WHERE NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__ids2_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[NullableIntA] OR ([j].[Value] IS NULL AND [e].[NullableIntA] IS NULL))) """, // """ @@ -1846,49 +1927,91 @@ public override async Task Null_semantics_contains_non_nullable_argument(bool as AssertSql( """ +@__ids_0='[1,2,null]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[IntA] IN (1, 2) +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[IntA]) """, // """ +@__ids_0='[1,2,null]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[IntA] NOT IN (1, 2) +WHERE NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[IntA])) """, // """ +@__ids2_0='[1,2]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[IntA] IN (1, 2) +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids2_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[IntA]) """, // """ +@__ids2_0='[1,2]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[IntA] NOT IN (1, 2) +WHERE NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__ids2_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[IntA])) """, // """ +@__ids3_0='[]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE 0 = 1 +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids3_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[IntA]) """, // """ +@__ids3_0='[]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] +WHERE NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__ids3_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[IntA])) """, // """ +@__ids4_0='[null]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE 0 = 1 +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids4_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[IntA]) """, // """ +@__ids4_0='[null]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] +WHERE NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__ids4_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [e].[IntA])) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs new file mode 100644 index 00000000000..c32661ab8c6 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -0,0 +1,477 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query; + +public class PrimitiveCollectionsQuerySqlServerTest : PrimitiveCollectionsQueryTestBase< + PrimitiveCollectionsQuerySqlServerTest.PrimitiveCollectionsQuerySqlServerFixture> +{ + public PrimitiveCollectionsQuerySqlServerTest(PrimitiveCollectionsQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Constant_of_ints_Contains(bool async) + { + await base.Constant_of_ints_Contains(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Int] IN (10, 999) +"""); + } + + public override async Task Constant_of_nullable_ints_Contains(bool async) + { + await base.Constant_of_nullable_ints_Contains(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[NullableInt] IN (10, 999) +"""); + } + + public override async Task Constant_of_nullable_ints_Contains_null(bool async) + { + await base.Constant_of_nullable_ints_Contains_null(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[NullableInt] = 999 OR [p].[NullableInt] IS NULL +"""); + } + + public override Task Constant_Count_with_zero_values(bool async) + => AssertTranslationFailedWithDetails( + () => base.Constant_Count_with_zero_values(async), + RelationalStrings.EmptyCollectionNotSupportedAsConstantQueryRoot); + + public override async Task Constant_Count_with_one_value(bool async) + { + await base.Constant_Count_with_one_value(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT COUNT(*) + FROM (VALUES (CAST(2 AS int))) AS [v]([Value]) + WHERE [v].[Value] > [p].[Id]) = 1 +"""); + } + + public override async Task Constant_Count_with_two_values(bool async) + { + await base.Constant_Count_with_two_values(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT COUNT(*) + FROM (VALUES (CAST(2 AS int)), (999)) AS [v]([Value]) + WHERE [v].[Value] > [p].[Id]) = 1 +"""); + } + + public override async Task Constant_Count_with_three_values(bool async) + { + await base.Constant_Count_with_three_values(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT COUNT(*) + FROM (VALUES (CAST(2 AS int)), (999), (1000)) AS [v]([Value]) + WHERE [v].[Value] > [p].[Id]) = 2 +"""); + } + + public override Task Constant_Contains_with_zero_values(bool async) + => AssertTranslationFailedWithDetails( + () => base.Constant_Contains_with_zero_values(async), + RelationalStrings.EmptyCollectionNotSupportedAsConstantQueryRoot); + + public override async Task Constant_Contains_with_one_value(bool async) + { + await base.Constant_Contains_with_one_value(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Id] = 2 +"""); + } + + public override async Task Constant_Contains_with_two_values(bool async) + { + await base.Constant_Contains_with_two_values(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Id] IN (2, 999) +"""); + } + + public override async Task Constant_Contains_with_three_values(bool async) + { + await base.Constant_Contains_with_three_values(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Id] IN (2, 999, 1000) +"""); + } + + public override async Task Parameter_Count(bool async) + { + await base.Parameter_Count(async); + + AssertSql( +""" +@__ids_0='[2,999]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT COUNT(*) + FROM OpenJson(@__ids_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] > [p].[Id]) = 1 +"""); + } + + public override async Task Parameter_of_ints_Contains(bool async) + { + await base.Parameter_of_ints_Contains(async); + + AssertSql( +""" +@__ints_0='[10,999]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ints_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [p].[Int]) +"""); + } + + public override async Task Parameter_of_nullable_ints_Contains(bool async) + { + await base.Parameter_of_nullable_ints_Contains(async); + + AssertSql( +""" +@__nullableInts_0='[10,999]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__nullableInts_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [p].[NullableInt] OR ([j].[Value] IS NULL AND [p].[NullableInt] IS NULL)) +"""); + } + + public override async Task Parameter_of_nullable_ints_Contains_null(bool async) + { + await base.Parameter_of_nullable_ints_Contains_null(async); + + AssertSql( +""" +@__nullableInts_0='[null,999]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__nullableInts_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [p].[NullableInt] OR ([j].[Value] IS NULL AND [p].[NullableInt] IS NULL)) +"""); + } + + public override async Task Parameter_of_strings_Contains(bool async) + { + await base.Parameter_of_strings_Contains(async); + + AssertSql( +""" +@__strings_0='["10","999"]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__strings_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [p].[String] OR ([j].[Value] IS NULL AND [p].[String] IS NULL)) +"""); + } + + public override async Task Parameter_of_DateTimes_Contains(bool async) + { + await base.Parameter_of_DateTimes_Contains(async); + + AssertSql( +""" +@__dateTimes_0='["2020-01-10T12:30:00","9999-01-01T00:00:00"]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0) WITH ([Value] datetime '$') AS [j] + WHERE [j].[Value] = [p].[DateTime]) +"""); + } + + public override async Task Parameter_of_bools_Contains(bool async) + { + await base.Parameter_of_bools_Contains(async); + + AssertSql( +""" +@__bools_0='[true]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__bools_0) WITH ([Value] bit '$') AS [j] + WHERE [j].[Value] = [p].[Bool]) +"""); + } + + public override async Task Parameter_of_enums_Contains(bool async) + { + await base.Parameter_of_enums_Contains(async); + + AssertSql( +""" +@__enums_0='[0,3]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__enums_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [p].[Enum]) +"""); + } + + public override async Task Column_of_ints_Contains(bool async) + { + await base.Column_of_ints_Contains(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE EXISTS ( + SELECT 1 + FROM OpenJson([p].[Ints]) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = 10) +"""); + } + + public override async Task Column_of_nullable_ints_Contains(bool async) + { + await base.Column_of_nullable_ints_Contains(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE EXISTS ( + SELECT 1 + FROM OpenJson([p].[NullableInts]) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = 10) +"""); + } + + public override async Task Column_of_nullable_ints_Contains_null(bool async) + { + await base.Column_of_nullable_ints_Contains_null(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE EXISTS ( + SELECT 1 + FROM OpenJson([p].[NullableInts]) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] IS NULL) +"""); + } + + public override async Task Column_of_bools_Contains(bool async) + { + await base.Column_of_bools_Contains(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE EXISTS ( + SELECT 1 + FROM OpenJson([p].[Bools]) WITH ([Value] bit '$') AS [j] + WHERE [j].[Value] = CAST(1 AS bit)) +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Json_representation_of_bool_array(bool async) + { + await using var context = CreateContext(); + + Assert.Equal( + "[true,false]", + await context.Database.SqlQuery($"SELECT [Bools] AS [Value] FROM [PrimitiveCollectionsEntity] WHERE [Id] = 1").SingleAsync()); + } + + public override async Task Column_Count(bool async) + { + await base.Column_Count(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT COUNT(*) + FROM OpenJson([p].[Ints]) WITH ([Value] int '$') AS [j]) = 2 +"""); + } + + public override async Task Column_and_parameter_Join(bool async) + { + await base.Column_and_parameter_Join(async); + + AssertSql( +""" +@__ints_0='[11,111]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT COUNT(*) + FROM OpenJson([p].[Ints]) WITH ([Value] int '$') AS [j] + INNER JOIN OpenJson(@__ints_0) WITH ([Value] int '$') AS [j0] ON [j].[Value] = [j0].[Value]) = 2 +"""); + } + + public override async Task Parameter_Intersect_column(bool async) + { + await base.Parameter_Intersect_column(async); + + AssertSql( +""" +@__ints_0='[11,111]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT [j].[Value] + FROM OpenJson(@__ints_0) WITH ([Value] int '$') AS [j] + INTERSECT + SELECT [j0].[Value] + FROM OpenJson([p].[Ints]) WITH ([Value] int '$') AS [j0] + ) AS [t]) = 2 +"""); + } + + public override async Task Column_Intersect_constant(bool async) + { + await base.Column_Intersect_constant(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT [j].[Value] + FROM OpenJson([p].[Ints]) WITH ([Value] int '$') AS [j] + INTERSECT + SELECT [v].[Value] + FROM (VALUES (CAST(11 AS int)), (111)) AS [v]([Value]) + ) AS [t]) = 2 +"""); + } + + public override async Task Column_equality_parameter(bool async) + { + await base.Column_equality_parameter(async); + + AssertSql( +""" +@__ints_0='[1,10]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Ints] = @__ints_0 +"""); + } + + public override async Task Column_equality_parameter_with_custom_converter(bool async) + { + await base.Column_equality_parameter_with_custom_converter(async); + + AssertSql( +""" +@__ints_0='1,10' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[CustomConvertedInts], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[CustomConvertedInts] = @__ints_0 +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + private PrimitiveCollectionsContext CreateContext() + => Fixture.CreateContext(); + + public class PrimitiveCollectionsQuerySqlServerFixture : PrimitiveCollectionsQueryFixtureBase + { + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // Map DateTime to non-default datetime instead of the default datetime2 to exercise type mapping inference + modelBuilder.Entity().Property(p => p.DateTime).HasColumnType("datetime"); + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs index bfeefd43a8c..b68cbb985c6 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -149,18 +149,20 @@ public async Task Where_contains_DateTime_literals(bool async) var contextFactory = await InitializeDateTimeContextAsync(); using var context = contextFactory.CreateContext(); + // var query = context.Dates.Where( + // d => dateTimes.Contains(d.SmallDateTime) + // && dateTimes.Contains(d.DateTime) + // && dateTimes.Contains(d.DateTime2) + // && dateTimes.Contains(d.DateTime2_0) + // && dateTimes.Contains(d.DateTime2_1) + // && dateTimes.Contains(d.DateTime2_2) + // && dateTimes.Contains(d.DateTime2_3) + // && dateTimes.Contains(d.DateTime2_4) + // && dateTimes.Contains(d.DateTime2_5) + // && dateTimes.Contains(d.DateTime2_6) + // && dateTimes.Contains(d.DateTime2_7)); var query = context.Dates.Where( - d => dateTimes.Contains(d.SmallDateTime) - && dateTimes.Contains(d.DateTime) - && dateTimes.Contains(d.DateTime2) - && dateTimes.Contains(d.DateTime2_0) - && dateTimes.Contains(d.DateTime2_1) - && dateTimes.Contains(d.DateTime2_2) - && dateTimes.Contains(d.DateTime2_3) - && dateTimes.Contains(d.DateTime2_4) - && dateTimes.Contains(d.DateTime2_5) - && dateTimes.Contains(d.DateTime2_6) - && dateTimes.Contains(d.DateTime2_7)); + d => dateTimes.Contains(d.SmallDateTime)); var results = async ? await query.ToListAsync() @@ -170,10 +172,51 @@ public async Task Where_contains_DateTime_literals(bool async) AssertSql( """ +@__dateTimes_0='["1970-09-03T12:00:00","1971-09-03T12:00:10","1972-09-03T12:00:10","1973-09-03T12:00:10","1974-09-03T12:00:10","1975-09-03T12:00:10","1976-09-03T12:00:10","1977-09-03T12:00:10","1978-09-03T12:00:10" "1979-09-03T12:00:10","1980-09-03T12:00:10"]' + SELECT [d].[Id], [d].[DateTime], [d].[DateTime2], [d].[DateTime2_0], [d].[DateTime2_1], [d].[DateTime2_2], [d].[DateTime2_3], [d].[DateTime2_4], [d].[DateTime2_5], [d].[DateTime2_6], [d].[DateTime2_7], [d].[SmallDateTime] FROM [Dates] AS [d] -WHERE [d].[SmallDateTime] IN ('1970-09-03T12:00:00', '1971-09-03T12:00:10', '1972-09-03T12:00:10', '1973-09-03T12:00:10', '1974-09-03T12:00:10', '1975-09-03T12:00:10', '1976-09-03T12:00:10', '1977-09-03T12:00:10', '1978-09-03T12:00:10', '1979-09-03T12:00:10', '1980-09-03T12:00:10') AND [d].[DateTime] IN ('1970-09-03T12:00:00.000', '1971-09-03T12:00:10.220', '1972-09-03T12:00:10.333', '1973-09-03T12:00:10.000', '1974-09-03T12:00:10.500', '1975-09-03T12:00:10.660', '1976-09-03T12:00:10.777', '1977-09-03T12:00:10.888', '1978-09-03T12:00:10.999', '1979-09-03T12:00:10.111', '1980-09-03T12:00:10.222') AND [d].[DateTime2] IN ('1970-09-03T12:00:00.0000000', '1971-09-03T12:00:10.2200000', '1972-09-03T12:00:10.3330000', '1973-09-03T12:00:10.0000000', '1974-09-03T12:00:10.5000000', '1975-09-03T12:00:10.6600000', '1976-09-03T12:00:10.7770000', '1977-09-03T12:00:10.8880000', '1978-09-03T12:00:10.9990000', '1979-09-03T12:00:10.1110000', '1980-09-03T12:00:10.2220000') AND [d].[DateTime2_0] IN ('1970-09-03T12:00:00', '1971-09-03T12:00:10', '1972-09-03T12:00:10', '1973-09-03T12:00:10', '1974-09-03T12:00:10', '1975-09-03T12:00:10', '1976-09-03T12:00:10', '1977-09-03T12:00:10', '1978-09-03T12:00:10', '1979-09-03T12:00:10', '1980-09-03T12:00:10') AND [d].[DateTime2_1] IN ('1970-09-03T12:00:00.0', '1971-09-03T12:00:10.2', '1972-09-03T12:00:10.3', '1973-09-03T12:00:10.0', '1974-09-03T12:00:10.5', '1975-09-03T12:00:10.6', '1976-09-03T12:00:10.7', '1977-09-03T12:00:10.8', '1978-09-03T12:00:10.9', '1979-09-03T12:00:10.1', '1980-09-03T12:00:10.2') AND [d].[DateTime2_2] IN ('1970-09-03T12:00:00.00', '1971-09-03T12:00:10.22', '1972-09-03T12:00:10.33', '1973-09-03T12:00:10.00', '1974-09-03T12:00:10.50', '1975-09-03T12:00:10.66', '1976-09-03T12:00:10.77', '1977-09-03T12:00:10.88', '1978-09-03T12:00:10.99', '1979-09-03T12:00:10.11', '1980-09-03T12:00:10.22') AND [d].[DateTime2_3] IN ('1970-09-03T12:00:00.000', '1971-09-03T12:00:10.220', '1972-09-03T12:00:10.333', '1973-09-03T12:00:10.000', '1974-09-03T12:00:10.500', '1975-09-03T12:00:10.660', '1976-09-03T12:00:10.777', '1977-09-03T12:00:10.888', '1978-09-03T12:00:10.999', '1979-09-03T12:00:10.111', '1980-09-03T12:00:10.222') AND [d].[DateTime2_4] IN ('1970-09-03T12:00:00.0000', '1971-09-03T12:00:10.2200', '1972-09-03T12:00:10.3330', '1973-09-03T12:00:10.0000', '1974-09-03T12:00:10.5000', '1975-09-03T12:00:10.6600', '1976-09-03T12:00:10.7770', '1977-09-03T12:00:10.8880', '1978-09-03T12:00:10.9990', '1979-09-03T12:00:10.1110', '1980-09-03T12:00:10.2220') AND [d].[DateTime2_5] IN ('1970-09-03T12:00:00.00000', '1971-09-03T12:00:10.22000', '1972-09-03T12:00:10.33300', '1973-09-03T12:00:10.00000', '1974-09-03T12:00:10.50000', '1975-09-03T12:00:10.66000', '1976-09-03T12:00:10.77700', '1977-09-03T12:00:10.88800', '1978-09-03T12:00:10.99900', '1979-09-03T12:00:10.11100', '1980-09-03T12:00:10.22200') AND [d].[DateTime2_6] IN ('1970-09-03T12:00:00.000000', '1971-09-03T12:00:10.220000', '1972-09-03T12:00:10.333000', '1973-09-03T12:00:10.000000', '1974-09-03T12:00:10.500000', '1975-09-03T12:00:10.660000', '1976-09-03T12:00:10.777000', '1977-09-03T12:00:10.888000', '1978-09-03T12:00:10.999000', '1979-09-03T12:00:10.111000', '1980-09-03T12:00:10.222000') AND [d].[DateTime2_7] IN ('1970-09-03T12:00:00.0000000', '1971-09-03T12:00:10.2200000', '1972-09-03T12:00:10.3330000', '1973-09-03T12:00:10.0000000', '1974-09-03T12:00:10.5000000', '1975-09-03T12:00:10.6600000', '1976-09-03T12:00:10.7770000', '1977-09-03T12:00:10.8880000', '1978-09-03T12:00:10.9990000', '1979-09-03T12:00:10.1110000', '1980-09-03T12:00:10.2220000') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0) WITH ([Value] datetime2 '$') AS [j] + WHERE [j].[Value] = [d].[SmallDateTime]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0) WITH ([Value] datetime2 '$') AS [j0] + WHERE [j0].[Value] = [d].[DateTime]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0) WITH ([Value] datetime2 '$') AS [j1] + WHERE [j1].[Value] = [d].[DateTime2]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0) WITH ([Value] datetime2 '$') AS [j2] + WHERE [j2].[Value] = [d].[DateTime2_0]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0) WITH ([Value] datetime2 '$') AS [j3] + WHERE [j3].[Value] = [d].[DateTime2_1]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0) WITH ([Value] datetime2 '$') AS [j4] + WHERE [j4].[Value] = [d].[DateTime2_2]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0) WITH ([Value] datetime2 '$') AS [j5] + WHERE [j5].[Value] = [d].[DateTime2_3]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0) WITH ([Value] datetime2 '$') AS [j6] + WHERE [j6].[Value] = [d].[DateTime2_4]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0) WITH ([Value] datetime2 '$') AS [j7] + WHERE [j7].[Value] = [d].[DateTime2_5]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0) WITH ([Value] datetime2 '$') AS [j8] + WHERE [j8].[Value] = [d].[DateTime2_6]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0) WITH ([Value] datetime2 '$') AS [j9] + WHERE [j9].[Value] = [d].[DateTime2_7]) """); +// AssertSql( +// """ +// SELECT [d].[Id], [d].[DateTime], [d].[DateTime2], [d].[DateTime2_0], [d].[DateTime2_1], [d].[DateTime2_2], [d].[DateTime2_3], [d].[DateTime2_4], [d].[DateTime2_5], [d].[DateTime2_6], [d].[DateTime2_7], [d].[SmallDateTime] +// FROM [Dates] AS [d] +// WHERE [d].[SmallDateTime] IN ('1970-09-03T12:00:00', '1971-09-03T12:00:10', '1972-09-03T12:00:10', '1973-09-03T12:00:10', '1974-09-03T12:00:10', '1975-09-03T12:00:10', '1976-09-03T12:00:10', '1977-09-03T12:00:10', '1978-09-03T12:00:10', '1979-09-03T12:00:10', '1980-09-03T12:00:10') AND [d].[DateTime] IN ('1970-09-03T12:00:00.000', '1971-09-03T12:00:10.220', '1972-09-03T12:00:10.333', '1973-09-03T12:00:10.000', '1974-09-03T12:00:10.500', '1975-09-03T12:00:10.660', '1976-09-03T12:00:10.777', '1977-09-03T12:00:10.888', '1978-09-03T12:00:10.999', '1979-09-03T12:00:10.111', '1980-09-03T12:00:10.222') AND [d].[DateTime2] IN ('1970-09-03T12:00:00.0000000', '1971-09-03T12:00:10.2200000', '1972-09-03T12:00:10.3330000', '1973-09-03T12:00:10.0000000', '1974-09-03T12:00:10.5000000', '1975-09-03T12:00:10.6600000', '1976-09-03T12:00:10.7770000', '1977-09-03T12:00:10.8880000', '1978-09-03T12:00:10.9990000', '1979-09-03T12:00:10.1110000', '1980-09-03T12:00:10.2220000') AND [d].[DateTime2_0] IN ('1970-09-03T12:00:00', '1971-09-03T12:00:10', '1972-09-03T12:00:10', '1973-09-03T12:00:10', '1974-09-03T12:00:10', '1975-09-03T12:00:10', '1976-09-03T12:00:10', '1977-09-03T12:00:10', '1978-09-03T12:00:10', '1979-09-03T12:00:10', '1980-09-03T12:00:10') AND [d].[DateTime2_1] IN ('1970-09-03T12:00:00.0', '1971-09-03T12:00:10.2', '1972-09-03T12:00:10.3', '1973-09-03T12:00:10.0', '1974-09-03T12:00:10.5', '1975-09-03T12:00:10.6', '1976-09-03T12:00:10.7', '1977-09-03T12:00:10.8', '1978-09-03T12:00:10.9', '1979-09-03T12:00:10.1', '1980-09-03T12:00:10.2') AND [d].[DateTime2_2] IN ('1970-09-03T12:00:00.00', '1971-09-03T12:00:10.22', '1972-09-03T12:00:10.33', '1973-09-03T12:00:10.00', '1974-09-03T12:00:10.50', '1975-09-03T12:00:10.66', '1976-09-03T12:00:10.77', '1977-09-03T12:00:10.88', '1978-09-03T12:00:10.99', '1979-09-03T12:00:10.11', '1980-09-03T12:00:10.22') AND [d].[DateTime2_3] IN ('1970-09-03T12:00:00.000', '1971-09-03T12:00:10.220', '1972-09-03T12:00:10.333', '1973-09-03T12:00:10.000', '1974-09-03T12:00:10.500', '1975-09-03T12:00:10.660', '1976-09-03T12:00:10.777', '1977-09-03T12:00:10.888', '1978-09-03T12:00:10.999', '1979-09-03T12:00:10.111', '1980-09-03T12:00:10.222') AND [d].[DateTime2_4] IN ('1970-09-03T12:00:00.0000', '1971-09-03T12:00:10.2200', '1972-09-03T12:00:10.3330', '1973-09-03T12:00:10.0000', '1974-09-03T12:00:10.5000', '1975-09-03T12:00:10.6600', '1976-09-03T12:00:10.7770', '1977-09-03T12:00:10.8880', '1978-09-03T12:00:10.9990', '1979-09-03T12:00:10.1110', '1980-09-03T12:00:10.2220') AND [d].[DateTime2_5] IN ('1970-09-03T12:00:00.00000', '1971-09-03T12:00:10.22000', '1972-09-03T12:00:10.33300', '1973-09-03T12:00:10.00000', '1974-09-03T12:00:10.50000', '1975-09-03T12:00:10.66000', '1976-09-03T12:00:10.77700', '1977-09-03T12:00:10.88800', '1978-09-03T12:00:10.99900', '1979-09-03T12:00:10.11100', '1980-09-03T12:00:10.22200') AND [d].[DateTime2_6] IN ('1970-09-03T12:00:00.000000', '1971-09-03T12:00:10.220000', '1972-09-03T12:00:10.333000', '1973-09-03T12:00:10.000000', '1974-09-03T12:00:10.500000', '1975-09-03T12:00:10.660000', '1976-09-03T12:00:10.777000', '1977-09-03T12:00:10.888000', '1978-09-03T12:00:10.999000', '1979-09-03T12:00:10.111000', '1980-09-03T12:00:10.222000') AND [d].[DateTime2_7] IN ('1970-09-03T12:00:00.0000000', '1971-09-03T12:00:10.2200000', '1972-09-03T12:00:10.3330000', '1973-09-03T12:00:10.0000000', '1974-09-03T12:00:10.5000000', '1975-09-03T12:00:10.6600000', '1976-09-03T12:00:10.7770000', '1977-09-03T12:00:10.8880000', '1978-09-03T12:00:10.9990000', '1979-09-03T12:00:10.1110000', '1980-09-03T12:00:10.2220000') +// """); } protected class DateTimeContext : DbContext @@ -3871,9 +3914,14 @@ public virtual async Task DateTime_Contains_with_smalldatetime_generates_correct AssertSql( """ +@__testDateList_0='["2018-10-07T00:00:00"]' (Size = 4000) + SELECT [r].[Id], [r].[MyTime] FROM [ReproEntity] AS [r] -WHERE [r].[MyTime] = '2018-10-07T00:00:00' +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__testDateList_0) WITH ([Value] smalldatetime '$') AS [j] + WHERE [j].[Value] = [r].[MyTime]) """); } } @@ -3929,14 +3977,18 @@ public virtual async Task Nested_contains_with_enum() AssertSql( """ +@__keys_0='["0a47bcb7-a1cb-4345-8944-c58f82d6aac7","5f221fb9-66f4-442a-92c9-d97ed5989cc7"]' (Size = 4000) @__key_2='5f221fb9-66f4-442a-92c9-d97ed5989cc7' SELECT [t].[Id], [t].[Type] FROM [Todos] AS [t] -WHERE CASE - WHEN [t].[Type] = 0 THEN @__key_2 - ELSE @__key_2 -END IN ('0a47bcb7-a1cb-4345-8944-c58f82d6aac7', '5f221fb9-66f4-442a-92c9-d97ed5989cc7') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__keys_0) WITH ([Value] uniqueidentifier '$') AS [j] + WHERE [j].[Value] = CASE + WHEN [t].[Type] = 0 THEN @__key_2 + ELSE @__key_2 + END) """); } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs index 9b24b8e6149..e97ca7a9467 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs @@ -534,7 +534,7 @@ WHERE [t].[Species] IS NOT NULL AND [t].[Species] LIKE N'F%' [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Muliple_occurrences_of_FromSql_in_group_by_aggregate(bool async) + public virtual async Task Multiple_occurrences_of_FromSql_in_group_by_aggregate(bool async) { var contextFactory = await InitializeAsync(); using var context = contextFactory.CreateContext(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyFixture.cs index 42d0b34643f..2a108b2e6f6 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyFixture.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyFixture.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; using Microsoft.EntityFrameworkCore.TestModels.SpatialModel; using NetTopologySuite; @@ -39,8 +40,9 @@ protected class ReplacementTypeMappingSource : SqlServerTypeMappingSource { public ReplacementTypeMappingSource( TypeMappingSourceDependencies dependencies, - RelationalTypeMappingSourceDependencies relationalDependencies) - : base(dependencies, relationalDependencies) + RelationalTypeMappingSourceDependencies relationalDependencies, + ISqlServerSingletonOptions sqlServerSingletonOptions) + : base(dependencies, relationalDependencies, sqlServerSingletonOptions) { } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryFixture.cs index 935d63f5168..8084b613b7d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryFixture.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryFixture.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; using Microsoft.EntityFrameworkCore.TestModels.SpatialModel; using NetTopologySuite.Geometries; @@ -36,8 +37,9 @@ protected class ReplacementTypeMappingSource : SqlServerTypeMappingSource { public ReplacementTypeMappingSource( TypeMappingSourceDependencies dependencies, - RelationalTypeMappingSourceDependencies relationalDependencies) - : base(dependencies, relationalDependencies) + RelationalTypeMappingSourceDependencies relationalDependencies, + ISqlServerSingletonOptions sqlServerSingletonOptions) + : base(dependencies, relationalDependencies, sqlServerSingletonOptions) { } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs index dbd7228bf91..9348aee7fbb 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs @@ -13,7 +13,7 @@ public TPCGearsOfWarQuerySqlServerTest(TPCGearsOfWarQuerySqlServerFixture fixtur : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } protected override bool CanExecuteQueryString @@ -305,6 +305,8 @@ FROM [Tags] AS [t] """, // """ +@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000) + SELECT [t].[Nickname], [t].[SquadId], [t].[AssignedCityName], [t].[CityOfBirthName], [t].[FullName], [t].[HasSoulPatch], [t].[LeaderNickname], [t].[LeaderSquadId], [t].[Rank], [t].[Discriminator], [t0].[Id], [t0].[GearNickName], [t0].[GearSquadId], [t0].[IssueDate], [t0].[Note] FROM ( SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], N'Gear' AS [Discriminator] @@ -314,7 +316,10 @@ UNION ALL FROM [Officers] AS [o] ) AS [t] LEFT JOIN [Tags] AS [t0] ON [t].[Nickname] = [t0].[GearNickName] AND [t].[SquadId] = [t0].[GearSquadId] -WHERE [t0].[Id] IS NOT NULL AND [t0].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57') +WHERE [t0].[Id] IS NOT NULL AND EXISTS ( + SELECT 1 + FROM OpenJson(@__tags_0) WITH ([Value] uniqueidentifier '$') AS [j] + WHERE [j].[Value] = [t0].[Id] OR ([j].[Value] IS NULL AND [t0].[Id] IS NULL)) """); } @@ -329,6 +334,8 @@ FROM [Tags] AS [t] """, // """ +@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000) + SELECT [t].[Nickname], [t].[SquadId], [t].[AssignedCityName], [t].[CityOfBirthName], [t].[FullName], [t].[HasSoulPatch], [t].[LeaderNickname], [t].[LeaderSquadId], [t].[Rank], [t].[Discriminator], [t0].[Id], [t0].[GearNickName], [t0].[GearSquadId], [t0].[IssueDate], [t0].[Note] FROM ( SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], N'Gear' AS [Discriminator] @@ -339,7 +346,10 @@ FROM [Officers] AS [o] ) AS [t] INNER JOIN [Cities] AS [c] ON [t].[CityOfBirthName] = [c].[Name] LEFT JOIN [Tags] AS [t0] ON [t].[Nickname] = [t0].[GearNickName] AND [t].[SquadId] = [t0].[GearSquadId] -WHERE [c].[Location] IS NOT NULL AND [t0].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57') +WHERE [c].[Location] IS NOT NULL AND EXISTS ( + SELECT 1 + FROM OpenJson(@__tags_0) WITH ([Value] uniqueidentifier '$') AS [j] + WHERE [j].[Value] = [t0].[Id] OR ([j].[Value] IS NULL AND [t0].[Id] IS NULL)) """); } @@ -354,6 +364,8 @@ FROM [Tags] AS [t] """, // """ +@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000) + SELECT [t].[Nickname], [t].[SquadId], [t].[AssignedCityName], [t].[CityOfBirthName], [t].[FullName], [t].[HasSoulPatch], [t].[LeaderNickname], [t].[LeaderSquadId], [t].[Rank], [t].[Discriminator] FROM ( SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], N'Gear' AS [Discriminator] @@ -363,7 +375,10 @@ UNION ALL FROM [Officers] AS [o] ) AS [t] LEFT JOIN [Tags] AS [t0] ON [t].[Nickname] = [t0].[GearNickName] AND [t].[SquadId] = [t0].[GearSquadId] -WHERE [t0].[Id] IS NOT NULL AND [t0].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57') +WHERE [t0].[Id] IS NOT NULL AND EXISTS ( + SELECT 1 + FROM OpenJson(@__tags_0) WITH ([Value] uniqueidentifier '$') AS [j] + WHERE [j].[Value] = [t0].[Id] OR ([j].[Value] IS NULL AND [t0].[Id] IS NULL)) """); } @@ -2907,9 +2922,14 @@ public override async Task Non_unicode_string_literals_in_contains_is_used_for_n AssertSql( """ +@__cities_0='["Unknown","Jacinto\u0027s location","Ephyra\u0027s location"]' (Size = 4000) + SELECT [c].[Name], [c].[Location], [c].[Nation] FROM [Cities] AS [c] -WHERE [c].[Location] IN ('Unknown', 'Jacinto''s location', 'Ephyra''s location') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__cities_0) WITH ([Value] varchar(100) '$') AS [j] + WHERE [j].[Value] = [c].[Location] OR ([j].[Value] IS NULL AND [c].[Location] IS NULL)) """); } @@ -4124,9 +4144,14 @@ public override async Task Contains_with_local_nullable_guid_list_closure(bool a AssertSql( """ +@__ids_0='["d2c26679-562b-44d1-ab96-23d1775e0926","23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3","ab1b82d7-88db-42bd-a132-7eef9aa68af4"]' (Size = 4000) + SELECT [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note] FROM [Tags] AS [t] -WHERE [t].[Id] IN ('d2c26679-562b-44d1-ab96-23d1775e0926', '23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3', 'ab1b82d7-88db-42bd-a132-7eef9aa68af4') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] uniqueidentifier '$') AS [j] + WHERE [j].[Value] = [t].[Id]) """); } @@ -4772,6 +4797,8 @@ public override async Task Contains_on_nullable_array_produces_correct_sql(bool AssertSql( """ +@__cities_0='["Ephyra",null]' (Size = 4000) + SELECT [t].[Nickname], [t].[SquadId], [t].[AssignedCityName], [t].[CityOfBirthName], [t].[FullName], [t].[HasSoulPatch], [t].[LeaderNickname], [t].[LeaderSquadId], [t].[Rank], [t].[Discriminator] FROM ( SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], N'Gear' AS [Discriminator] @@ -4781,7 +4808,10 @@ UNION ALL FROM [Officers] AS [o] ) AS [t] LEFT JOIN [Cities] AS [c] ON [t].[AssignedCityName] = [c].[Name] -WHERE [t].[SquadId] < 2 AND ([c].[Name] = N'Ephyra' OR [c].[Name] IS NULL) +WHERE [t].[SquadId] < 2 AND EXISTS ( + SELECT 1 + FROM OpenJson(@__cities_0) WITH ([Value] nvarchar(450) '$') AS [j] + WHERE [j].[Value] = [c].[Name] OR ([j].[Value] IS NULL AND [c].[Name] IS NULL)) """); } @@ -8042,6 +8072,8 @@ public override async Task Correlated_collection_with_complex_order_by_funcletiz AssertSql( """ +@__nicknames_0='[]' (Size = 4000) + SELECT [t].[Nickname], [t].[SquadId], [w].[Name], [w].[Id] FROM ( SELECT [g].[Nickname], [g].[SquadId], [g].[FullName] @@ -8051,7 +8083,13 @@ UNION ALL FROM [Officers] AS [o] ) AS [t] LEFT JOIN [Weapons] AS [w] ON [t].[FullName] = [w].[OwnerFullName] -ORDER BY [t].[Nickname], [t].[SquadId] +ORDER BY CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__nicknames_0) WITH ([Value] nvarchar(450) '$') AS [j] + WHERE [j].[Value] = [t].[Nickname]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END DESC, [t].[Nickname], [t].[SquadId] """); } @@ -8921,10 +8959,14 @@ public override async Task DateTimeOffset_Contains_Less_than_Greater_than(bool a """ @__start_0='1902-01-01T10:00:00.1234567+01:30' @__end_1='1902-01-03T10:00:00.1234567+01:30' +@__dates_2='["1902-01-02T10:00:00.1234567+01:30"]' (Size = 4000) SELECT [m].[Id], [m].[CodeName], [m].[Date], [m].[Duration], [m].[Rating], [m].[Time], [m].[Timeline] FROM [Missions] AS [m] -WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND [m].[Timeline] = '1902-01-02T10:00:00.1234567+01:30' +WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dates_2) WITH ([Value] datetimeoffset '$') AS [j] + WHERE [j].[Value] = [m].[Timeline]) """); } @@ -9791,6 +9833,8 @@ public override async Task OrderBy_Contains_empty_list(bool async) AssertSql( """ +@__ids_0='[]' (Size = 4000) + SELECT [t].[Nickname], [t].[SquadId], [t].[AssignedCityName], [t].[CityOfBirthName], [t].[FullName], [t].[HasSoulPatch], [t].[LeaderNickname], [t].[LeaderSquadId], [t].[Rank], [t].[Discriminator] FROM ( SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], N'Gear' AS [Discriminator] @@ -9799,6 +9843,13 @@ UNION ALL SELECT [o].[Nickname], [o].[SquadId], [o].[AssignedCityName], [o].[CityOfBirthName], [o].[FullName], [o].[HasSoulPatch], [o].[LeaderNickname], [o].[LeaderSquadId], [o].[Rank], N'Officer' AS [Discriminator] FROM [Officers] AS [o] ) AS [t] +ORDER BY CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [t].[SquadId]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END """); } @@ -10806,10 +10857,15 @@ public override async Task Enum_array_contains(bool async) AssertSql( """ +@__types_0='[null,1]' (Size = 4000) + SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] LEFT JOIN [Weapons] AS [w0] ON [w].[SynergyWithId] = [w0].[Id] -WHERE [w0].[Id] IS NOT NULL AND ([w0].[AmmunitionType] = 1 OR [w0].[AmmunitionType] IS NULL) +WHERE [w0].[Id] IS NOT NULL AND EXISTS ( + SELECT 1 + FROM OpenJson(@__types_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [w0].[AmmunitionType] OR ([j].[Value] IS NULL AND [w0].[AmmunitionType] IS NULL)) """); } @@ -11909,6 +11965,8 @@ public override async Task Where_bool_column_and_Contains(bool async) AssertSql( """ +@__values_0='[false,true]' (Size = 4000) + SELECT [t].[Nickname], [t].[SquadId], [t].[AssignedCityName], [t].[CityOfBirthName], [t].[FullName], [t].[HasSoulPatch], [t].[LeaderNickname], [t].[LeaderSquadId], [t].[Rank], [t].[Discriminator] FROM ( SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], N'Gear' AS [Discriminator] @@ -11917,7 +11975,10 @@ UNION ALL SELECT [o].[Nickname], [o].[SquadId], [o].[AssignedCityName], [o].[CityOfBirthName], [o].[FullName], [o].[HasSoulPatch], [o].[LeaderNickname], [o].[LeaderSquadId], [o].[Rank], N'Officer' AS [Discriminator] FROM [Officers] AS [o] ) AS [t] -WHERE [t].[HasSoulPatch] = CAST(1 AS bit) AND [t].[HasSoulPatch] IN (CAST(0 AS bit), CAST(1 AS bit)) +WHERE [t].[HasSoulPatch] = CAST(1 AS bit) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__values_0) WITH ([Value] bit '$') AS [j] + WHERE [j].[Value] = [t].[HasSoulPatch]) """); } @@ -11926,7 +11987,9 @@ public override async Task Where_bool_column_or_Contains(bool async) await base.Where_bool_column_or_Contains(async); AssertSql( -""" + """ +@__values_0='[false,true]' (Size = 4000) + SELECT [t].[Nickname], [t].[SquadId], [t].[AssignedCityName], [t].[CityOfBirthName], [t].[FullName], [t].[HasSoulPatch], [t].[LeaderNickname], [t].[LeaderSquadId], [t].[Rank], [t].[Discriminator] FROM ( SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], N'Gear' AS [Discriminator] @@ -11935,7 +11998,10 @@ UNION ALL SELECT [o].[Nickname], [o].[SquadId], [o].[AssignedCityName], [o].[CityOfBirthName], [o].[FullName], [o].[HasSoulPatch], [o].[LeaderNickname], [o].[LeaderSquadId], [o].[Rank], N'Officer' AS [Discriminator] FROM [Officers] AS [o] ) AS [t] -WHERE [t].[HasSoulPatch] = CAST(1 AS bit) AND [t].[HasSoulPatch] IN (CAST(0 AS bit), CAST(1 AS bit)) +WHERE [t].[HasSoulPatch] = CAST(1 AS bit) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__values_0) WITH ([Value] bit '$') AS [j] + WHERE [j].[Value] = [t].[HasSoulPatch]) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index 63dfe44a6a1..426c0446777 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -13,7 +13,7 @@ public TPTGearsOfWarQuerySqlServerTest(TPTGearsOfWarQuerySqlServerFixture fixtur : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } protected override bool CanExecuteQueryString @@ -289,13 +289,18 @@ FROM [Tags] AS [t] """, // """ +@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE WHEN [o].[Nickname] IS NOT NULL THEN N'Officer' END AS [Discriminator], [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId] LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId] -WHERE [t].[Id] IS NOT NULL AND [t].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57') +WHERE [t].[Id] IS NOT NULL AND EXISTS ( + SELECT 1 + FROM OpenJson(@__tags_0) WITH ([Value] uniqueidentifier '$') AS [j] + WHERE [j].[Value] = [t].[Id] OR ([j].[Value] IS NULL AND [t].[Id] IS NULL)) """); } @@ -310,6 +315,8 @@ FROM [Tags] AS [t] """, // """ +@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE WHEN [o].[Nickname] IS NOT NULL THEN N'Officer' END AS [Discriminator], [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note] @@ -317,7 +324,10 @@ FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId] INNER JOIN [Cities] AS [c] ON [g].[CityOfBirthName] = [c].[Name] LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId] -WHERE [c].[Location] IS NOT NULL AND [t].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57') +WHERE [c].[Location] IS NOT NULL AND EXISTS ( + SELECT 1 + FROM OpenJson(@__tags_0) WITH ([Value] uniqueidentifier '$') AS [j] + WHERE [j].[Value] = [t].[Id] OR ([j].[Value] IS NULL AND [t].[Id] IS NULL)) """); } @@ -332,13 +342,18 @@ FROM [Tags] AS [t] """, // """ +@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE WHEN [o].[Nickname] IS NOT NULL THEN N'Officer' END AS [Discriminator] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId] LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId] -WHERE [t].[Id] IS NOT NULL AND [t].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57') +WHERE [t].[Id] IS NOT NULL AND EXISTS ( + SELECT 1 + FROM OpenJson(@__tags_0) WITH ([Value] uniqueidentifier '$') AS [j] + WHERE [j].[Value] = [t].[Id] OR ([j].[Value] IS NULL AND [t].[Id] IS NULL)) """); } @@ -2465,9 +2480,14 @@ public override async Task Non_unicode_string_literals_in_contains_is_used_for_n AssertSql( """ +@__cities_0='["Unknown","Jacinto\u0027s location","Ephyra\u0027s location"]' (Size = 4000) + SELECT [c].[Name], [c].[Location], [c].[Nation] FROM [Cities] AS [c] -WHERE [c].[Location] IN ('Unknown', 'Jacinto''s location', 'Ephyra''s location') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__cities_0) WITH ([Value] varchar(100) '$') AS [j] + WHERE [j].[Value] = [c].[Location] OR ([j].[Value] IS NULL AND [c].[Location] IS NULL)) """); } @@ -3545,9 +3565,14 @@ public override async Task Contains_with_local_nullable_guid_list_closure(bool a AssertSql( """ +@__ids_0='["d2c26679-562b-44d1-ab96-23d1775e0926","23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3","ab1b82d7-88db-42bd-a132-7eef9aa68af4"]' (Size = 4000) + SELECT [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note] FROM [Tags] AS [t] -WHERE [t].[Id] IN ('d2c26679-562b-44d1-ab96-23d1775e0926', '23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3', 'ab1b82d7-88db-42bd-a132-7eef9aa68af4') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] uniqueidentifier '$') AS [j] + WHERE [j].[Value] = [t].[Id]) """); } @@ -4126,13 +4151,18 @@ public override async Task Contains_on_nullable_array_produces_correct_sql(bool AssertSql( """ +@__cities_0='["Ephyra",null]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE WHEN [o].[Nickname] IS NOT NULL THEN N'Officer' END AS [Discriminator] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId] LEFT JOIN [Cities] AS [c] ON [g].[AssignedCityName] = [c].[Name] -WHERE [g].[SquadId] < 2 AND ([c].[Name] = N'Ephyra' OR [c].[Name] IS NULL) +WHERE [g].[SquadId] < 2 AND EXISTS ( + SELECT 1 + FROM OpenJson(@__cities_0) WITH ([Value] nvarchar(450) '$') AS [j] + WHERE [j].[Value] = [c].[Name] OR ([j].[Value] IS NULL AND [c].[Name] IS NULL)) """); } @@ -6794,10 +6824,18 @@ public override async Task Correlated_collection_with_complex_order_by_funcletiz AssertSql( """ +@__nicknames_0='[]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [w].[Name], [w].[Id] FROM [Gears] AS [g] LEFT JOIN [Weapons] AS [w] ON [g].[FullName] = [w].[OwnerFullName] -ORDER BY [g].[Nickname], [g].[SquadId] +ORDER BY CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__nicknames_0) WITH ([Value] nvarchar(450) '$') AS [j] + WHERE [j].[Value] = [g].[Nickname]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END DESC, [g].[Nickname], [g].[SquadId] """); } @@ -7584,10 +7622,14 @@ public override async Task DateTimeOffset_Contains_Less_than_Greater_than(bool a """ @__start_0='1902-01-01T10:00:00.1234567+01:30' @__end_1='1902-01-03T10:00:00.1234567+01:30' +@__dates_2='["1902-01-02T10:00:00.1234567+01:30"]' (Size = 4000) SELECT [m].[Id], [m].[CodeName], [m].[Date], [m].[Duration], [m].[Rating], [m].[Time], [m].[Timeline] FROM [Missions] AS [m] -WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND [m].[Timeline] = '1902-01-02T10:00:00.1234567+01:30' +WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dates_2) WITH ([Value] datetimeoffset '$') AS [j] + WHERE [j].[Value] = [m].[Timeline]) """); } @@ -8386,11 +8428,20 @@ public override async Task OrderBy_Contains_empty_list(bool async) AssertSql( """ +@__ids_0='[]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE WHEN [o].[Nickname] IS NOT NULL THEN N'Officer' END AS [Discriminator] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId] +ORDER BY CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [g].[SquadId]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END """); } @@ -9229,10 +9280,15 @@ public override async Task Enum_array_contains(bool async) AssertSql( """ +@__types_0='[null,1]' (Size = 4000) + SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] LEFT JOIN [Weapons] AS [w0] ON [w].[SynergyWithId] = [w0].[Id] -WHERE [w0].[Id] IS NOT NULL AND ([w0].[AmmunitionType] = 1 OR [w0].[AmmunitionType] IS NULL) +WHERE [w0].[Id] IS NOT NULL AND EXISTS ( + SELECT 1 + FROM OpenJson(@__types_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [w0].[AmmunitionType] OR ([j].[Value] IS NULL AND [w0].[AmmunitionType] IS NULL)) """); } @@ -10201,12 +10257,17 @@ public override async Task Where_bool_column_and_Contains(bool async) AssertSql( """ +@__values_0='[false,true]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE WHEN [o].[Nickname] IS NOT NULL THEN N'Officer' END AS [Discriminator] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId] -WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND [g].[HasSoulPatch] IN (CAST(0 AS bit), CAST(1 AS bit)) +WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__values_0) WITH ([Value] bit '$') AS [j] + WHERE [j].[Value] = [g].[HasSoulPatch]) """); } @@ -10216,12 +10277,17 @@ public override async Task Where_bool_column_or_Contains(bool async) AssertSql( """ +@__values_0='[false,true]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE WHEN [o].[Nickname] IS NOT NULL THEN N'Officer' END AS [Discriminator] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId] -WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND [g].[HasSoulPatch] IN (CAST(0 AS bit), CAST(1 AS bit)) +WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__values_0) WITH ([Value] bit '$') AS [j] + WHERE [j].[Value] = [g].[HasSoulPatch]) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsQuerySqlServerTest.cs index bea5f56d43f..3c1dcd13385 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsQuerySqlServerTest.cs @@ -15,7 +15,7 @@ public TemporalComplexNavigationsCollectionsQuerySqlServerTest( : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) @@ -2301,17 +2301,25 @@ public override async Task Collection_projection_over_GroupBy_over_parameter(boo AssertSql( """ +@__validIds_0='["L1 01","L1 02"]' (Size = 4000) + SELECT [t].[Date], [t0].[Id] FROM ( SELECT [l].[Date] FROM [LevelOne] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [l] - WHERE [l].[Name] IN (N'L1 01', N'L1 02') + WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__validIds_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [l].[Name] OR ([j].[Value] IS NULL AND [l].[Name] IS NULL)) GROUP BY [l].[Date] ) AS [t] LEFT JOIN ( SELECT [l0].[Id], [l0].[Date] FROM [LevelOne] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [l0] - WHERE [l0].[Name] IN (N'L1 01', N'L1 02') + WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__validIds_0) WITH ([Value] nvarchar(max) '$') AS [j0] + WHERE [j0].[Value] = [l0].[Name] OR ([j0].[Value] IS NULL AND [l0].[Name] IS NULL)) ) AS [t0] ON [t].[Date] = [t0].[Date] ORDER BY [t].[Date] """); @@ -2610,6 +2618,8 @@ public override async Task LeftJoin_with_Any_on_outer_source_and_projecting_coll AssertSql( """ +@__validIds_0='["L1 01","L1 02"]' (Size = 4000) + SELECT CASE WHEN [l0].[Id] IS NULL THEN 0 ELSE [l0].[Id] @@ -2617,7 +2627,10 @@ ELSE [l0].[Id] FROM [LevelOne] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [l] LEFT JOIN [LevelTwo] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [l0] ON [l].[Id] = [l0].[Level1_Required_Id] LEFT JOIN [LevelThree] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [l1] ON [l0].[Id] = [l1].[OneToMany_Required_Inverse3Id] -WHERE [l].[Name] IN (N'L1 01', N'L1 02') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__validIds_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [l].[Name] OR ([j].[Value] IS NULL AND [l].[Name] IS NULL)) ORDER BY [l].[Id], [l0].[Id] """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs index 4550597b143..fd91edaafdc 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs @@ -15,7 +15,7 @@ public TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest( : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) @@ -1842,6 +1842,8 @@ public override async Task LeftJoin_with_Any_on_outer_source_and_projecting_coll AssertSql( """ +@__validIds_0='["L1 01","L1 02"]' (Size = 4000) + SELECT CASE WHEN [t0].[OneToOne_Required_PK_Date] IS NULL OR [t0].[Level1_Required_Id] IS NULL OR [t0].[OneToMany_Required_Inverse2Id] IS NULL OR CASE WHEN [t0].[PeriodEnd0] IS NOT NULL AND [t0].[PeriodStart0] IS NOT NULL THEN [t0].[PeriodEnd0] @@ -1874,7 +1876,10 @@ WHERE [l2].[Level2_Required_Id] IS NOT NULL AND [l2].[OneToMany_Required_Inverse ) AS [t1] ON CASE WHEN [t0].[OneToOne_Required_PK_Date] IS NOT NULL AND [t0].[Level1_Required_Id] IS NOT NULL AND [t0].[OneToMany_Required_Inverse2Id] IS NOT NULL AND [t0].[PeriodEnd0] IS NOT NULL AND [t0].[PeriodStart0] IS NOT NULL THEN [t0].[Id0] END = [t1].[OneToMany_Required_Inverse3Id] -WHERE [l].[Name] IN (N'L1 01', N'L1 02') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__validIds_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [l].[Name] OR ([j].[Value] IS NULL AND [l].[Name] IS NULL)) ORDER BY [l].[Id], [t0].[Id], [t0].[Id0] """); } @@ -3029,17 +3034,25 @@ public override async Task Collection_projection_over_GroupBy_over_parameter(boo AssertSql( """ +@__validIds_0='["L1 01","L1 02"]' (Size = 4000) + SELECT [t].[Date], [t0].[Id] FROM ( SELECT [l].[Date] FROM [Level1] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [l] - WHERE [l].[Name] IN (N'L1 01', N'L1 02') + WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__validIds_0) WITH ([Value] nvarchar(max) '$') AS [j] + WHERE [j].[Value] = [l].[Name] OR ([j].[Value] IS NULL AND [l].[Name] IS NULL)) GROUP BY [l].[Date] ) AS [t] LEFT JOIN ( SELECT [l0].[Id], [l0].[Date] FROM [Level1] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [l0] - WHERE [l0].[Name] IN (N'L1 01', N'L1 02') + WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__validIds_0) WITH ([Value] nvarchar(max) '$') AS [j0] + WHERE [j0].[Value] = [l0].[Name] OR ([j0].[Value] IS NULL AND [l0].[Name] IS NULL)) ) AS [t0] ON [t].[Date] = [t0].[Date] ORDER BY [t].[Date] """); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs index 63b8a51dac9..0ef9325851f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs @@ -16,7 +16,7 @@ public TemporalGearsOfWarQuerySqlServerTest(TemporalGearsOfWarQuerySqlServerFixt : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) @@ -63,10 +63,15 @@ FROM [Tags] AS [t] """, // """ +@__tags_0='[]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank], [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note], [t].[PeriodEnd], [t].[PeriodStart] FROM [Gears] AS [g] LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId] -WHERE 0 = 1 +WHERE [t].[Id] IS NOT NULL AND EXISTS ( + SELECT 1 + FROM OpenJson(@__tags_0) WITH ([Value] uniqueidentifier '$') AS [j] + WHERE [j].[Value] = [t].[Id] OR ([j].[Value] IS NULL AND [t].[Id] IS NULL)) """); } @@ -84,11 +89,16 @@ FROM [Tags] AS [t] """, // """ +@__tags_0='[]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank], [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note], [t].[PeriodEnd], [t].[PeriodStart] FROM [Gears] AS [g] INNER JOIN [Cities] AS [c] ON [g].[CityOfBirthName] = [c].[Name] LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId] -WHERE 0 = 1 +WHERE [c].[Location] IS NOT NULL AND EXISTS ( + SELECT 1 + FROM OpenJson(@__tags_0) WITH ([Value] uniqueidentifier '$') AS [j] + WHERE [j].[Value] = [t].[Id] OR ([j].[Value] IS NULL AND [t].[Id] IS NULL)) """); } @@ -106,10 +116,15 @@ FROM [Tags] AS [t] """, // """ +@__tags_0='[]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank] FROM [Gears] AS [g] LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId] -WHERE 0 = 1 +WHERE [t].[Id] IS NOT NULL AND EXISTS ( + SELECT 1 + FROM OpenJson(@__tags_0) WITH ([Value] uniqueidentifier '$') AS [j] + WHERE [j].[Value] = [t].[Id] OR ([j].[Value] IS NULL AND [t].[Id] IS NULL)) """); } @@ -1613,9 +1628,14 @@ public override async Task Where_bool_column_or_Contains(bool async) AssertSql( """ +@__values_0='[false,true]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank] FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] -WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND [g].[HasSoulPatch] IN (CAST(0 AS bit), CAST(1 AS bit)) +WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__values_0) WITH ([Value] bit '$') AS [j] + WHERE [j].[Value] = [g].[HasSoulPatch]) """); } @@ -1719,10 +1739,15 @@ public override async Task Contains_on_nullable_array_produces_correct_sql(bool AssertSql( """ +@__cities_0='["Ephyra",null]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank] FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] LEFT JOIN [Cities] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [c] ON [g].[AssignedCityName] = [c].[Name] -WHERE [g].[SquadId] < 2 AND ([c].[Name] = N'Ephyra' OR [c].[Name] IS NULL) +WHERE [g].[SquadId] < 2 AND EXISTS ( + SELECT 1 + FROM OpenJson(@__cities_0) WITH ([Value] nvarchar(450) '$') AS [j] + WHERE [j].[Value] = [c].[Name] OR ([j].[Value] IS NULL AND [c].[Name] IS NULL)) """); } @@ -5558,10 +5583,15 @@ public override async Task Enum_array_contains(bool async) AssertSql( """ +@__types_0='[null,1]' (Size = 4000) + SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[PeriodEnd], [w].[PeriodStart], [w].[SynergyWithId] FROM [Weapons] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [w] LEFT JOIN [Weapons] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [w0] ON [w].[SynergyWithId] = [w0].[Id] -WHERE [w0].[Id] IS NOT NULL AND ([w0].[AmmunitionType] = 1 OR [w0].[AmmunitionType] IS NULL) +WHERE [w0].[Id] IS NOT NULL AND EXISTS ( + SELECT 1 + FROM OpenJson(@__types_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [w0].[AmmunitionType] OR ([j].[Value] IS NULL AND [w0].[AmmunitionType] IS NULL)) """); } @@ -6128,8 +6158,17 @@ public override async Task OrderBy_Contains_empty_list(bool async) AssertSql( """ +@__ids_0='[]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank] FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] +ORDER BY CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] int '$') AS [j] + WHERE [j].[Value] = [g].[SquadId]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END """); } @@ -6240,10 +6279,14 @@ public override async Task DateTimeOffset_Contains_Less_than_Greater_than(bool a """ @__start_0='1902-01-01T10:00:00.1234567+01:30' @__end_1='1902-01-03T10:00:00.1234567+01:30' +@__dates_2='["1902-01-02T10:00:00.1234567+01:30"]' (Size = 4000) SELECT [m].[Id], [m].[BriefingDocument], [m].[BriefingDocumentFileExtension], [m].[CodeName], [m].[Date], [m].[Duration], [m].[PeriodEnd], [m].[PeriodStart], [m].[Rating], [m].[Time], [m].[Timeline] FROM [Missions] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [m] -WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND [m].[Timeline] = '1902-01-02T10:00:00.1234567+01:30' +WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dates_2) WITH ([Value] datetimeoffset '$') AS [j] + WHERE [j].[Value] = [m].[Timeline]) """); } @@ -6620,9 +6663,14 @@ public override async Task Non_unicode_string_literals_in_contains_is_used_for_n AssertSql( """ +@__cities_0='["Unknown","Jacinto\u0027s location","Ephyra\u0027s location"]' (Size = 4000) + SELECT [c].[Name], [c].[Location], [c].[Nation], [c].[PeriodEnd], [c].[PeriodStart] FROM [Cities] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [c] -WHERE [c].[Location] IN ('Unknown', 'Jacinto''s location', 'Ephyra''s location') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__cities_0) WITH ([Value] varchar(100) '$') AS [j] + WHERE [j].[Value] = [c].[Location] OR ([j].[Value] IS NULL AND [c].[Location] IS NULL)) """); } @@ -7927,10 +7975,18 @@ public override async Task Correlated_collection_with_complex_order_by_funcletiz AssertSql( """ +@__nicknames_0='[]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [w].[Name], [w].[Id] FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] LEFT JOIN [Weapons] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [w] ON [g].[FullName] = [w].[OwnerFullName] -ORDER BY [g].[Nickname], [g].[SquadId] +ORDER BY CASE + WHEN EXISTS ( + SELECT 1 + FROM OpenJson(@__nicknames_0) WITH ([Value] nvarchar(450) '$') AS [j] + WHERE [j].[Value] = [g].[Nickname]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END DESC, [g].[Nickname], [g].[SquadId] """); } @@ -8243,9 +8299,14 @@ public override async Task Contains_with_local_nullable_guid_list_closure(bool a AssertSql( """ +@__ids_0='["d2c26679-562b-44d1-ab96-23d1775e0926","23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3","ab1b82d7-88db-42bd-a132-7eef9aa68af4"]' (Size = 4000) + SELECT [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note], [t].[PeriodEnd], [t].[PeriodStart] FROM [Tags] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [t] -WHERE [t].[Id] IN ('d2c26679-562b-44d1-ab96-23d1775e0926', '23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3', 'ab1b82d7-88db-42bd-a132-7eef9aa68af4') +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids_0) WITH ([Value] uniqueidentifier '$') AS [j] + WHERE [j].[Value] = [t].[Id]) """); } @@ -8896,9 +8957,14 @@ public override async Task Where_bool_column_and_Contains(bool async) AssertSql( """ +@__values_0='[false,true]' (Size = 4000) + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank] FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] -WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND [g].[HasSoulPatch] IN (CAST(0 AS bit), CAST(1 AS bit)) +WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__values_0) WITH ([Value] bit '$') AS [j] + WHERE [j].[Value] = [g].[HasSoulPatch]) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Update/SqlServerUpdateSqlGeneratorTest.cs b/test/EFCore.SqlServer.FunctionalTests/Update/SqlServerUpdateSqlGeneratorTest.cs index df55f71991d..7de26bdcb11 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Update/SqlServerUpdateSqlGeneratorTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Update/SqlServerUpdateSqlGeneratorTest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Update.Internal; @@ -10,13 +11,19 @@ namespace Microsoft.EntityFrameworkCore.Update; public class SqlServerUpdateSqlGeneratorTest : UpdateSqlGeneratorTestBase { protected override IUpdateSqlGenerator CreateSqlGenerator() - => new SqlServerUpdateSqlGenerator( + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlServer("Database=Foo"); + + return new SqlServerUpdateSqlGenerator( new UpdateSqlGeneratorDependencies( new SqlServerSqlGenerationHelper( new RelationalSqlGenerationHelperDependencies()), new SqlServerTypeMappingSource( TestServiceFactory.Instance.Create(), - TestServiceFactory.Instance.Create()))); + TestServiceFactory.Instance.Create(), + new SqlServerSingletonOptions()))); + } protected override TestHelpers TestHelpers => SqlServerTestHelpers.Instance; diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs new file mode 100644 index 00000000000..48d1ee7ac4a --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using NetTopologySuite.Geometries; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class NonSharedPrimitiveCollectionsQuerySqliteTest : NonSharedPrimitiveCollectionsQueryRelationalTestBase +{ + [ConditionalFact] // #30630 + public override async Task Collection_of_geometry_is_not_mapped() + { + var exception = await Assert.ThrowsAsync( + () => InitializeAsync( + onConfiguring: options => options.UseSqlite(o => o.UseNetTopologySuite()), + addServices: s => s.AddEntityFrameworkSqliteNetTopologySuite(), + onModelCreating: mb => mb.Entity().Property("Points"))); + + Assert.Equal(CoreStrings.PropertyNotMapped("Point[]", "MyEntity", "Points"), exception.Message); + } + + private class MyContext : DbContext + { + public MyContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); + } + + private class MyEntity + { + public int Id { get; set; } + + public int[] Ints { get; set; } + } + + protected override ITestStoreFactory TestStoreFactory + => SqliteTestStoreFactory.Instance; +} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindCompiledQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindCompiledQuerySqliteTest.cs index efb82dd7f20..108d5095ff1 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindCompiledQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindCompiledQuerySqliteTest.cs @@ -60,7 +60,7 @@ public override async Task Query_with_array_parameter_async() "DbSet() .Where(c => c.CustomerID == __args .ElementAt(0))", CoreStrings.QueryUnableToTranslateMethod("System.Linq.Enumerable", nameof(Enumerable.ElementAt))), (await Assert.ThrowsAsync( - () => Enumerate(query(context, new[] { "ALFKI" })))).Message.Replace("\r", "").Replace("\n", "")); + () => CountAsync(query(context, new[] { "ALFKI" })))).Message.Replace("\r", "").Replace("\n", "")); } using (var context = CreateContext()) @@ -70,7 +70,7 @@ public override async Task Query_with_array_parameter_async() "DbSet() .Where(c => c.CustomerID == __args .ElementAt(0))", CoreStrings.QueryUnableToTranslateMethod("System.Linq.Enumerable", nameof(Enumerable.ElementAt))), (await Assert.ThrowsAsync( - () => Enumerate(query(context, new[] { "ANATR" })))).Message.Replace("\r", "").Replace("\n", "")); + () => CountAsync(query(context, new[] { "ANATR" })))).Message.Replace("\r", "").Replace("\n", "")); } } } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs new file mode 100644 index 00000000000..de8c895ba3d --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -0,0 +1,457 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query; + +public class PrimitiveCollectionsQuerySqliteTest : PrimitiveCollectionsQueryTestBase< + PrimitiveCollectionsQuerySqliteTest.PrimitiveCollectionsQuerySqlServerFixture> +{ + public PrimitiveCollectionsQuerySqliteTest(PrimitiveCollectionsQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Constant_of_ints_Contains(bool async) + { + await base.Constant_of_ints_Contains(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."Int" IN (10, 999) +"""); + } + + public override async Task Constant_of_nullable_ints_Contains(bool async) + { + await base.Constant_of_nullable_ints_Contains(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."NullableInt" IN (10, 999) +"""); + } + + public override async Task Constant_of_nullable_ints_Contains_null(bool async) + { + await base.Constant_of_nullable_ints_Contains_null(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."NullableInt" = 999 OR "p"."NullableInt" IS NULL +"""); + } + + public override Task Constant_Count_with_zero_values(bool async) + => AssertTranslationFailedWithDetails( + () => base.Constant_Count_with_zero_values(async), + RelationalStrings.EmptyCollectionNotSupportedAsConstantQueryRoot); + + public override async Task Constant_Count_with_one_value(bool async) + { + await base.Constant_Count_with_one_value(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE ( + SELECT COUNT(*) + FROM (SELECT CAST(2 AS INTEGER) AS Value) AS "v" + WHERE "v"."Value" > "p"."Id") = 1 +"""); + } + + public override async Task Constant_Count_with_two_values(bool async) + { + await base.Constant_Count_with_two_values(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE ( + SELECT COUNT(*) + FROM (SELECT CAST(2 AS INTEGER) AS Value UNION ALL VALUES (999)) AS "v" + WHERE "v"."Value" > "p"."Id") = 1 +"""); + } + + public override async Task Constant_Count_with_three_values(bool async) + { + await base.Constant_Count_with_three_values(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE ( + SELECT COUNT(*) + FROM (SELECT CAST(2 AS INTEGER) AS Value UNION ALL VALUES (999), (1000)) AS "v" + WHERE "v"."Value" > "p"."Id") = 2 +"""); + } + + public override Task Constant_Contains_with_zero_values(bool async) + => AssertTranslationFailedWithDetails( + () => base.Constant_Contains_with_zero_values(async), + RelationalStrings.EmptyCollectionNotSupportedAsConstantQueryRoot); + + public override async Task Constant_Contains_with_one_value(bool async) + { + await base.Constant_Contains_with_one_value(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."Id" = 2 +"""); + } + + public override async Task Constant_Contains_with_two_values(bool async) + { + await base.Constant_Contains_with_two_values(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."Id" IN (2, 999) +"""); + } + + public override async Task Constant_Contains_with_three_values(bool async) + { + await base.Constant_Contains_with_three_values(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."Id" IN (2, 999, 1000) +"""); + } + + public override async Task Parameter_Count(bool async) + { + await base.Parameter_Count(async); + + AssertSql( +""" +@__ids_0='[2,999]' (Size = 7) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE ( + SELECT COUNT(*) + FROM json_each(@__ids_0) AS "j" + WHERE "j"."value" > "p"."Id") = 1 +"""); + } + + public override async Task Parameter_of_ints_Contains(bool async) + { + await base.Parameter_of_ints_Contains(async); + + AssertSql( +""" +@__ints_0='[10,999]' (Size = 8) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE EXISTS ( + SELECT 1 + FROM json_each(@__ints_0) AS "j" + WHERE "j"."value" = "p"."Int") +"""); + } + + public override async Task Parameter_of_nullable_ints_Contains(bool async) + { + await base.Parameter_of_nullable_ints_Contains(async); + + AssertSql( +""" +@__nullableInts_0='[10,999]' (Size = 8) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE EXISTS ( + SELECT 1 + FROM json_each(@__nullableInts_0) AS "j" + WHERE "j"."value" = "p"."NullableInt" OR ("j"."value" IS NULL AND "p"."NullableInt" IS NULL)) +"""); + } + + public override async Task Parameter_of_nullable_ints_Contains_null(bool async) + { + await base.Parameter_of_nullable_ints_Contains_null(async); + + AssertSql( +""" +@__nullableInts_0='[null,999]' (Size = 10) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE EXISTS ( + SELECT 1 + FROM json_each(@__nullableInts_0) AS "j" + WHERE "j"."value" = "p"."NullableInt" OR ("j"."value" IS NULL AND "p"."NullableInt" IS NULL)) +"""); + } + + public override async Task Parameter_of_strings_Contains(bool async) + { + await base.Parameter_of_strings_Contains(async); + + AssertSql( +""" +@__strings_0='["10","999"]' (Size = 12) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE EXISTS ( + SELECT 1 + FROM json_each(@__strings_0) AS "j" + WHERE "j"."value" = "p"."String" OR ("j"."value" IS NULL AND "p"."String" IS NULL)) +"""); + } + + public override async Task Parameter_of_DateTimes_Contains(bool async) + { + await base.Parameter_of_DateTimes_Contains(async); + + AssertSql( +""" +@__dateTimes_0='["2020-01-10T12:30:00","9999-01-01T00:00:00"]' (Size = 45) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE EXISTS ( + SELECT 1 + FROM json_each(@__dateTimes_0) AS "j" + WHERE "j"."value" = "p"."DateTime") +"""); + } + + public override async Task Parameter_of_bools_Contains(bool async) + { + await base.Parameter_of_bools_Contains(async); + + AssertSql( +""" +@__bools_0='[true]' (Size = 6) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE EXISTS ( + SELECT 1 + FROM json_each(@__bools_0) AS "j" + WHERE "j"."value" = "p"."Bool") +"""); + } + + public override async Task Parameter_of_enums_Contains(bool async) + { + await base.Parameter_of_enums_Contains(async); + + AssertSql( +""" +@__enums_0='[0,3]' (Size = 5) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE EXISTS ( + SELECT 1 + FROM json_each(@__enums_0) AS "j" + WHERE "j"."value" = "p"."Enum") +"""); + } + + public override async Task Column_of_ints_Contains(bool async) + { + await base.Column_of_ints_Contains(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE EXISTS ( + SELECT 1 + FROM json_each("p"."Ints") AS "j" + WHERE "j"."value" = 10) +"""); + } + + public override async Task Column_of_nullable_ints_Contains(bool async) + { + await base.Column_of_nullable_ints_Contains(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE EXISTS ( + SELECT 1 + FROM json_each("p"."NullableInts") AS "j" + WHERE "j"."value" = 10) +"""); + } + + public override async Task Column_of_nullable_ints_Contains_null(bool async) + { + await base.Column_of_nullable_ints_Contains_null(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE EXISTS ( + SELECT 1 + FROM json_each("p"."NullableInts") AS "j" + WHERE "j"."value" IS NULL) +"""); + } + + public override async Task Column_of_bools_Contains(bool async) + { + await base.Column_of_bools_Contains(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE EXISTS ( + SELECT 1 + FROM json_each("p"."Bools") AS "j" + WHERE "j"."value") +"""); + } + + public override async Task Column_Count(bool async) + { + await base.Column_Count(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE ( + SELECT COUNT(*) + FROM json_each("p"."Ints") AS "j") = 2 +"""); + } + + public override async Task Column_and_parameter_Join(bool async) + { + await base.Column_and_parameter_Join(async); + + AssertSql( +""" +@__ints_0='[11,111]' (Size = 8) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE ( + SELECT COUNT(*) + FROM json_each("p"."Ints") AS "j" + INNER JOIN json_each(@__ints_0) AS "j0" ON "j"."value" = "j0"."value") = 2 +"""); + } + + public override async Task Parameter_Intersect_column(bool async) + { + await base.Parameter_Intersect_column(async); + + AssertSql( +""" +@__ints_0='[11,111]' (Size = 8) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT "j"."value" + FROM json_each(@__ints_0) AS "j" + INTERSECT + SELECT "j0"."value" + FROM json_each("p"."Ints") AS "j0" + ) AS "t") = 2 +"""); + } + + public override async Task Column_Intersect_constant(bool async) + { + await base.Column_Intersect_constant(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT "j"."value" + FROM json_each("p"."Ints") AS "j" + INTERSECT + SELECT CAST(11 AS INTEGER) AS Value UNION ALL VALUES (111) + ) AS "t") = 2 +"""); + } + + public override async Task Column_equality_parameter(bool async) + { + await base.Column_equality_parameter(async); + + AssertSql( +""" +@__ints_0='[1,10]' (Size = 6) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."Ints" = @__ints_0 +"""); + } + + public override async Task Column_equality_parameter_with_custom_converter(bool async) + { + await base.Column_equality_parameter_with_custom_converter(async); + + AssertSql( +""" +@__ints_0='1,10' (Size = 4) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."CustomConvertedInts", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."CustomConvertedInts" = @__ints_0 +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + private PrimitiveCollectionsContext CreateContext() + => Fixture.CreateContext(); + + public class PrimitiveCollectionsQuerySqlServerFixture : PrimitiveCollectionsQueryFixtureBase + { + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override ITestStoreFactory TestStoreFactory + => SqliteTestStoreFactory.Instance; + } +}