From 527ca05503ab5d5dea2a3341eac8b96db915fc89 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 | 3 + .../Properties/RelationalStrings.Designer.cs | 22 +- .../Properties/RelationalStrings.resx | 9 + .../Query/Internal/EqualsTranslator.cs | 6 +- .../Query/QuerySqlGenerator.cs | 148 ++- .../Query/RelationalQueryRootProcessor.cs | 62 ++ .../RelationalQueryTranslationPreprocessor.cs | 5 +- ...ueryTranslationPreprocessorDependencies.cs | 8 +- ...yableMethodTranslatingExpressionVisitor.cs | 596 +++++++++++- ...lationalSqlTranslatingExpressionVisitor.cs | 49 +- .../Query/SqlExpressionFactory.cs | 9 +- .../Query/SqlExpressionVisitor.cs | 16 + .../Query/SqlExpressions/ColumnExpression.cs | 7 + .../Query/SqlExpressions/FromSqlExpression.cs | 2 +- .../SqlExpressions/ITableBasedExpression.cs | 6 +- .../SqlExpressions/RowValueExpression.cs | 146 +++ .../ScalarSubqueryExpression.cs | 16 +- .../SqlExpressions/SelectExpression.Helper.cs | 18 +- .../Query/SqlExpressions/SelectExpression.cs | 57 +- .../TableValuedFunctionExpression.cs | 99 +- .../Query/SqlExpressions/ValuesExpression.cs | 181 ++++ .../Query/SqlNullabilityProcessor.cs | 70 ++ .../SqlServerServiceCollectionExtensions.cs | 1 + ...rchConditionConvertingExpressionVisitor.cs | 42 + .../Internal/SqlServerOpenJsonExpression.cs | 149 +++ .../Internal/SqlServerQueryRootProcessor.cs | 44 + .../Internal/SqlServerQuerySqlGenerator.cs | 133 +++ .../SqlServerQueryTranslationPreprocessor.cs | 41 + ...rverQueryTranslationPreprocessorFactory.cs | 53 ++ ...yableMethodTranslatingExpressionVisitor.cs | 235 +++++ .../Internal/SqlServerStringTypeMapping.cs | 11 + .../Internal/SqlServerTypeMappingSource.cs | 93 +- .../SqliteServiceCollectionExtensions.cs | 1 + .../Internal/SqliteQueryRootProcessor.cs | 44 + .../SqliteQueryTranslationPreprocessor.cs} | 32 +- ...liteQueryTranslationPreprocessorFactory.cs | 46 + ...yableMethodTranslatingExpressionVisitor.cs | 239 +++++ .../Internal/SqliteStringTypeMapping.cs | 10 + .../Internal/SqliteTypeMappingSource.cs | 73 +- src/EFCore/Query/InlineQueryRootExpression.cs | 93 ++ ...ingExpressionVisitor.ExpressionVisitors.cs | 29 +- ...nExpandingExpressionVisitor.Expressions.cs | 40 + .../NavigationExpandingExpressionVisitor.cs | 44 +- ...yableMethodNormalizingExpressionVisitor.cs | 13 +- .../Query/ParameterQueryRootExpression.cs | 63 ++ src/EFCore/Query/QueryCompilationContext.cs | 2 +- src/EFCore/Query/QueryRootProcessor.cs | 137 +++ .../Query/QueryTranslationPreprocessor.cs | 12 +- ...ueryTranslationPreprocessorDependencies.cs | 7 + ...yableMethodTranslatingExpressionVisitor.cs | 15 +- src/EFCore/Storage/CoreTypeMapping.cs | 37 +- .../CollectionToJsonStringConverter.cs | 64 ++ .../CosmosModelBuilderGenericTest.cs | 14 - ...itiveCollectionsQueryRelationalTestBase.cs | 21 + .../Query/QueryNoClientEvalTestBase.cs | 23 - ...SharedPrimitiveCollectionsQueryTestBase.cs | 273 ++++++ .../Query/NorthwindCompiledQueryTestBase.cs | 29 +- .../Query/NorthwindJoinQueryTestBase.cs | 32 +- .../PrimitiveCollectionsQueryTestBase.cs | 722 +++++++++++++++ .../Query/QueryTestBase.cs | 6 + ...avigationsCollectionsQuerySqlServerTest.cs | 19 +- ...CollectionsSharedTypeQuerySqlServerTest.cs | 19 +- ...tionsCollectionsSplitQuerySqlServerTest.cs | 33 +- .../ComplexNavigationsQuerySqlServerTest.cs | 7 +- ...NavigationsSharedTypeQuerySqlServerTest.cs | 7 +- .../Query/GearsOfWarQuerySqlServerTest.cs | 88 +- ...dPrimitiveCollectionsQuerySqlServerTest.cs | 309 +++++++ ...indAggregateOperatorsQuerySqlServerTest.cs | 198 +++- .../NorthwindCompiledQuerySqlServerTest.cs | 122 +-- ...windEFPropertyIncludeQuerySqlServerTest.cs | 56 +- ...windIncludeNoTrackingQuerySqlServerTest.cs | 56 +- .../NorthwindIncludeQuerySqlServerTest.cs | 56 +- .../Query/NorthwindJoinQuerySqlServerTest.cs | 17 +- ...orthwindMiscellaneousQuerySqlServerTest.cs | 39 +- .../NorthwindNavigationsQuerySqlServerTest.cs | 8 +- .../NorthwindSelectQuerySqlServerTest.cs | 35 +- ...plitIncludeNoTrackingQuerySqlServerTest.cs | 86 +- ...NorthwindSplitIncludeQuerySqlServerTest.cs | 86 +- ...orthwindStringIncludeQuerySqlServerTest.cs | 56 +- .../Query/NorthwindWhereQuerySqlServerTest.cs | 57 +- .../Query/NullSemanticsQuerySqlServerTest.cs | 165 +++- ...imitiveCollectionsQueryOldSqlServerTest.cs | 428 +++++++++ .../PrimitiveCollectionsQuerySqlServerTest.cs | 846 ++++++++++++++++++ .../Query/QueryBugsTest.cs | 68 +- .../SpatialQuerySqlServerGeographyFixture.cs | 6 +- .../SpatialQuerySqlServerGeometryFixture.cs | 6 +- .../Query/TPCGearsOfWarQuerySqlServerTest.cs | 88 +- .../Query/TPTGearsOfWarQuerySqlServerTest.cs | 88 +- ...avigationsCollectionsQuerySqlServerTest.cs | 19 +- ...CollectionsSharedTypeQuerySqlServerTest.cs | 19 +- .../TemporalGearsOfWarQuerySqlServerTest.cs | 88 +- .../Update/SqlServerUpdateSqlGeneratorTest.cs | 11 +- ...aredPrimitiveCollectionsQuerySqliteTest.cs | 219 +++++ .../Query/NorthwindCompiledQuerySqliteTest.cs | 12 +- .../PrimitiveCollectionsQuerySqliteTest.cs | 796 ++++++++++++++++ .../ModelBuilding/NonRelationshipTestBase.cs | 39 - 96 files changed, 8180 insertions(+), 530 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.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs create mode 100644 src/EFCore.SqlServer/Query/Internal/SqlServerQueryRootProcessor.cs create mode 100644 src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessor.cs create mode 100644 src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessorFactory.cs create mode 100644 src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryRootProcessor.cs rename src/{EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs => EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessor.cs} (57%) create mode 100644 src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessorFactory.cs create mode 100644 src/EFCore/Query/InlineQueryRootExpression.cs create mode 100644 src/EFCore/Query/ParameterQueryRootExpression.cs create mode 100644 src/EFCore/Query/QueryRootProcessor.cs create mode 100644 src/EFCore/Storage/ValueConversion/CollectionToJsonStringConverter.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/PrimitiveCollectionsQueryOldSqlServerTest.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..edd36d48c74 100644 --- a/All.sln.DotSettings +++ b/All.sln.DotSettings @@ -308,9 +308,12 @@ The .NET Foundation licenses this file to you under the MIT license. True True 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 fcc660d5a5f..0fe96122904 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -167,6 +167,14 @@ public static string ConflictingRowValuesSensitive(object? firstEntityType, obje GetString("ConflictingRowValuesSensitive", nameof(firstEntityType), nameof(secondEntityType), nameof(keyValue), nameof(firstConflictingValue), nameof(secondConflictingValue), nameof(column)), firstEntityType, secondEntityType, keyValue, firstConflictingValue, secondConflictingValue, column); + /// + /// Store type '{storeType1}' was inferred for a primitive collection, but that primitive collection was previously inferred to have store type '{storeType2}'. + /// + public static string ConflictingTypeMappingsForPrimitiveCollection(object? storeType1, object? storeType2) + => string.Format( + GetString("ConflictingTypeMappingsForPrimitiveCollection", nameof(storeType1), nameof(storeType2)), + storeType1, storeType2); + /// /// A seed entity for entity type '{entityType}' has the same key value as another seed entity mapped to the same table '{table}', but have different values for the column '{column}'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting values. /// @@ -621,6 +629,12 @@ public static string EitherOfTwoValuesMustBeNull(object? param1, object? param2) GetString("EitherOfTwoValuesMustBeNull", nameof(param1), nameof(param2)), param1, param2); + /// + /// Empty collections are not supported as constant query roots. + /// + public static string EmptyCollectionNotSupportedAsInlineQueryRoot + => GetString("EmptyCollectionNotSupportedAsInlineQueryRoot"); + /// /// 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"). /// @@ -1409,6 +1423,12 @@ public static string OptionalDependentWithDependentWithoutIdentifyingProperty(ob GetString("OptionalDependentWithDependentWithoutIdentifyingProperty", nameof(entityType)), entityType); + /// + /// Only constants are supported inside inline collection query roots. + /// + public static string OnlyConstantsSupportedInInlineCollectionQueryRoots + => GetString("OnlyConstantsSupportedInInlineCollectionQueryRoots"); + /// /// Cannot use the value provided for parameter '{parameter}' because it isn't assignable to type object[]. /// @@ -1966,7 +1986,7 @@ public static string UnsupportedOperatorForSqlExpression(object? nodeType, objec nodeType, expressionType); /// - /// No relational type mapping can be found for property '{entity}.{property}' and the current provider doesn't specify a default store type for the properties of type '{clrType}'. + /// No relational type mapping can be found for property '{entity}.{property}' and the current provider doesn't specify a default store type for the properties of type '{clrType}'. /// public static string UnsupportedPropertyType(object? entity, object? property, object? clrType) => string.Format( diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 128be2b2177..938c9f6d455 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -175,6 +175,9 @@ Instances of entity types '{firstEntityType}' and '{secondEntityType}' are mapped to the same row with the key value '{keyValue}', but have different property values '{firstConflictingValue}' and '{secondConflictingValue}' for the column '{column}'. + + Store type '{storeType1}' was inferred for a primitive collection, but that primitive collection was previously inferred to have store type '{storeType2}'. + A seed entity for entity type '{entityType}' has the same key value as another seed entity mapped to the same table '{table}', but have different values for the column '{column}'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting values. @@ -346,6 +349,9 @@ Either {param1} or {param2} must be null. + + Empty collections are not supported as inline 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"). @@ -950,6 +956,9 @@ Entity type '{entityType}' is an optional dependent using table sharing and containing other dependents without any required non shared property to identify whether the entity exists. If all nullable properties contain a null value in database then an object instance won't be created in the query causing nested dependent's values to be lost. Add a required property to create instances with null values for other properties or mark the incoming navigation as required to always create an instance. + + Only constants are supported inside inline collection query roots. + Cannot use the value provided for parameter '{parameter}' because it isn't assignable to type object[]. diff --git a/src/EFCore.Relational/Query/Internal/EqualsTranslator.cs b/src/EFCore.Relational/Query/Internal/EqualsTranslator.cs index 69d98027268..ad58fbe7636 100644 --- a/src/EFCore.Relational/Query/Internal/EqualsTranslator.cs +++ b/src/EFCore.Relational/Query/Internal/EqualsTranslator.cs @@ -60,8 +60,10 @@ public EqualsTranslator(ISqlExpressionFactory sqlExpressionFactory) && right != null) { if (left.Type == right.Type - || (right.Type == typeof(object) && (right is SqlParameterExpression || right is SqlConstantExpression)) - || (left.Type == typeof(object) && (left is SqlParameterExpression || left is SqlConstantExpression))) + || (right.Type == typeof(object) + && right is SqlParameterExpression or SqlConstantExpression or ColumnExpression { TypeMapping: null }) + || (left.Type == typeof(object) + && left is SqlParameterExpression or SqlConstantExpression or ColumnExpression { TypeMapping: null })) { return _sqlExpressionFactory.Equal(left, right); } diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs index 07855455d00..7ab0ad8706e 100644 --- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs @@ -226,12 +226,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 +295,43 @@ 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)) + { + 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, + } + && selectExpression.Projection.Count == valuesExpression.ColumnNames.Count + && selectExpression.Projection.Select( + (pe, index) => pe.Expression is ColumnExpression column + && column.Name == valuesExpression.ColumnNames[index]) + .All(e => e)) + { + GenerateValues(valuesExpression); + return true; + } + + return false; + } + /// /// Generates a pseudo FROM clause. Required by some providers when a query has no actual FROM clause. /// @@ -371,16 +403,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) @@ -607,19 +639,22 @@ protected override Expression VisitSqlParameter(SqlParameterExpression sqlParame { var invariantName = sqlParameterExpression.Name; var parameterName = sqlParameterExpression.Name; + var typeMapping = sqlParameterExpression.TypeMapping!; // Try to see if a parameter already exists - if so, just integrate the same placeholder into the SQL instead of sending the same // data twice. // Note that if the type mapping differs, we do send the same data twice (e.g. the same string may be sent once as Unicode, once as // non-Unicode). + // TODO: Note that we perform Equals comparison on the value converter. We should be able to do reference comparison, but for + // that we need to ensure that there's only ever one type mapping instance (i.e. no type mappings are ever instantiated out of the + // type mapping source). See #30677. var parameter = _relationalCommandBuilder.Parameters.FirstOrDefault( p => p.InvariantName == parameterName - && p is TypeMappedRelationalParameter typeMappedRelationalParameter - && string.Equals( - typeMappedRelationalParameter.RelationalTypeMapping.StoreType, sqlParameterExpression.TypeMapping!.StoreType, - StringComparison.OrdinalIgnoreCase) - && typeMappedRelationalParameter.RelationalTypeMapping.Converter == sqlParameterExpression.TypeMapping!.Converter); + && p is TypeMappedRelationalParameter { RelationalTypeMapping: var existingTypeMapping } + && string.Equals(existingTypeMapping.StoreType, typeMapping.StoreType, StringComparison.OrdinalIgnoreCase) + && (existingTypeMapping.Converter is null && typeMapping.Converter is null + || existingTypeMapping.Converter is not null && existingTypeMapping.Converter.Equals(typeMapping.Converter))); if (parameter is null) { @@ -1132,6 +1167,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. /// @@ -1311,6 +1368,65 @@ void LiftPredicate(TableExpressionBase joinTable) RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteUpdate))); } + /// + protected override Expression VisitValues(ValuesExpression valuesExpression) + { + _relationalCommandBuilder.Append("("); + + GenerateValues(valuesExpression); + + _relationalCommandBuilder + .Append(")") + .Append(AliasSeparator) + .Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.Alias)); + + return valuesExpression; + } + + /// + /// Generates a VALUES expression. + /// + protected virtual void GenerateValues(ValuesExpression valuesExpression) + { + 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(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.ColumnNames[i])); + } + + if (rowValues.Count > 1) + { + _relationalCommandBuilder.Append(" UNION ALL VALUES "); + + for (var i = 1; i < rowValues.Count; i++) + { + if (i > 1) + { + _relationalCommandBuilder.Append(", "); + } + + Visit(valuesExpression.RowValues[i]); + } + } + } + /// 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..d2c218bb5c9 --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalQueryRootProcessor.cs @@ -0,0 +1,62 @@ +// 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 IModel _model; + + /// + /// 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 RelationalQueryRootProcessor( + QueryTranslationPreprocessorDependencies dependencies, + RelationalQueryTranslationPreprocessorDependencies relationalDependencies, + QueryCompilationContext queryCompilationContext) + : base(dependencies, queryCompilationContext) + { + _model = queryCompilationContext.Model; + } + + /// + /// Indicates that a can be converted to a ; + /// this will later be translated to a SQL . + /// + protected override bool ShouldConvertToInlineQueryRoot(ConstantExpression constantExpression) + => true; + + /// + 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); + } + + /// + 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..82f1e95c46f 100644 --- a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs @@ -36,9 +36,12 @@ 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; } + + /// + protected override Expression ProcessQueryRoots(Expression expression) + => new RelationalQueryRootProcessor(Dependencies, RelationalDependencies, QueryCompilationContext).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 9c6ee1a791a..55fb7762ba9 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Linq; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; @@ -12,10 +11,13 @@ namespace Microsoft.EntityFrameworkCore.Query; /// public class RelationalQueryableMethodTranslatingExpressionVisitor : QueryableMethodTranslatingExpressionVisitor { + private const string SqlQuerySingleColumnAlias = "Value"; + 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; @@ -39,6 +41,7 @@ public RelationalQueryableMethodTranslatingExpressionVisitor( _sharedTypeEntityExpandingExpressionVisitor = new SharedTypeEntityExpandingExpressionVisitor(_sqlTranslator, sqlExpressionFactory); _projectionBindingExpressionVisitor = new RelationalProjectionBindingExpressionVisitor(this, _sqlTranslator); + _typeMappingSource = relationalDependencies.TypeMappingSource; _sqlExpressionFactory = sqlExpressionFactory; _subquery = false; } @@ -63,10 +66,32 @@ protected RelationalQueryableMethodTranslatingExpressionVisitor( _sharedTypeEntityExpandingExpressionVisitor = new SharedTypeEntityExpandingExpressionVisitor(_sqlTranslator, parentVisitor._sqlExpressionFactory); _projectionBindingExpressionVisitor = new RelationalProjectionBindingExpressionVisitor(this, _sqlTranslator); + _typeMappingSource = parentVisitor._typeMappingSource; _sqlExpressionFactory = parentVisitor._sqlExpressionFactory; _subquery = true; } + /// + public override Expression Translate(Expression expression) + { + var visited = Visit(expression); + + if (!_subquery) + { + // We've finished translating the entire query. + + // If any constant/parameter query roots exist in the query, their columns don't yet have a type mapping. + // First, scan the query tree for inferred type mappings (e.g. based on a comparison of those columns to some regular column + // with a type mapping). + var inferredColumns = new ColumnTypeMappingScanner().Scan(visited); + + // Then, apply those type mappings back on the constant/parameter tables (e.g. ValuesExpression). + visited = ApplyInferredTypeMappings(visited, inferredColumns); + } + + return visited; + } + /// protected override Expression VisitExtension(Expression extensionExpression) { @@ -83,6 +108,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 +148,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 +180,7 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression) .Visit(shapedQueryExpression.ShaperExpression)); case SqlQueryRootExpression sqlQueryRootExpression: + { var typeMapping = RelationalDependencies.TypeMappingSource.FindMapping(sqlQueryRootExpression.ElementType); if (typeMapping == null) { @@ -161,8 +189,7 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression) } var selectExpression = new SelectExpression( - sqlQueryRootExpression.Type, typeMapping, - new FromSqlExpression("t", sqlQueryRootExpression.Sql, sqlQueryRootExpression.Argument)); + new FromSqlExpression("t", sqlQueryRootExpression.Sql, sqlQueryRootExpression.Argument), SqlQuerySingleColumnAlias, sqlQueryRootExpression.Type, typeMapping); Expression shaperExpression = new ProjectionBindingExpression( selectExpression, new ProjectionMember(), sqlQueryRootExpression.ElementType.MakeNullable()); @@ -177,6 +204,20 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression) } return new ShapedQueryExpression(selectExpression, shaperExpression); + } + + case InlineQueryRootExpression constantQueryRootExpression: + return VisitInlineQueryRoot(constantQueryRootExpression) ?? base.VisitExtension(extensionExpression); + + case ParameterQueryRootExpression parameterQueryRootExpression: + var sqlParameterExpression = + _sqlTranslator.Visit(parameterQueryRootExpression.ParameterExpression) as SqlParameterExpression; + Check.DebugAssert(sqlParameterExpression is not null, "sqlParameterExpression is not null"); + return TranslateCollection( + sqlParameterExpression, + elementTypeMapping: null, + char.ToLowerInvariant(sqlParameterExpression.Name.First(c => c != '_')).ToString()) + ?? base.VisitExtension(extensionExpression); default: return base.VisitExtension(extensionExpression); @@ -212,7 +253,122 @@ 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: RelationalTypeMapping elementTypeMapping + } columnExpression + && TranslateCollection( + columnExpression, + elementTypeMapping, + columnExpression.Name[..1].ToLowerInvariant()) + is { } primitiveCollectionTranslation) + { + return primitiveCollectionTranslation; + } + + return translated; + } + + /// + /// Translates a parameter or column 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). + /// + /// + /// Inline collections aren't passed to this method; see for the translation of inline + /// collections. + /// + /// The expression to try to translate as a primitive collection expression. + /// + /// The type mapping of the collection's element, or when it's not known (i.e. for parameters). + /// + /// + /// Provides an alias to be used for the table returned from translation, which will represent the collection. + /// + /// A if the translation was successful, otherwise . + protected virtual ShapedQueryExpression? TranslateCollection( + SqlExpression sqlExpression, + RelationalTypeMapping? elementTypeMapping, + string tableAlias) + => null; + + /// + /// Translates an inline collection into a queryable SQL VALUES expression. + /// + /// The inline collection to be translated. + /// A queryable SQL VALUES expression. + protected virtual ShapedQueryExpression? VisitInlineQueryRoot(InlineQueryRootExpression inlineQueryRootExpression) + { + var elementType = inlineQueryRootExpression.ElementType; + + var rowExpressions = new List(); + var encounteredNull = false; + var intTypeMapping = _typeMappingSource.FindMapping(typeof(int)); + + for (var i = 0; i < inlineQueryRootExpression.Values.Count; i++) + { + var value = inlineQueryRootExpression.Values[i]; + + // We currently support constants only; supporting non-constant values in VALUES is tracked by #30734. + if (value is not ConstantExpression constantExpression) + { + AddTranslationErrorDetails(RelationalStrings.OnlyConstantsSupportedInInlineCollectionQueryRoots); + return null; + } + + if (constantExpression.Value is null) + { + encounteredNull = true; + } + + rowExpressions.Add(new RowValueExpression(new[] + { + // Since VALUES may not guarantee row ordering, we add an _ord value by which we'll order. + _sqlExpressionFactory.Constant(i, intTypeMapping), + // Note that for the actual value, we must leave the type mapping null to allow it to get inferred later based on usage + _sqlExpressionFactory.Constant(constantExpression.Value, elementType, typeMapping: null) + })); + } + + if (rowExpressions.Count == 0) + { + AddTranslationErrorDetails(RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot); + return null; + } + + var valuesExpression = new ValuesExpression("v", rowExpressions, new[] { "_ord", "Value" }); + + // Note: we leave the element type mapping null, to allow it to get inferred based on queryable operators composed on top. + var selectExpression = new SelectExpression( + valuesExpression, columnName: "Value", columnType: elementType.UnwrapNullableType(), columnTypeMapping: null, isColumnNullable: encounteredNull); + + selectExpression.AppendOrdering( + new OrderingExpression( + selectExpression.CreateColumnExpression( + valuesExpression, + "_ord", + typeof(int), + intTypeMapping, + isColumnNullable: false), + ascending: true)); + + 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 +400,18 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent } var selectExpression = (SelectExpression)source.QueryExpression; - selectExpression.ApplyPredicate(_sqlExpressionFactory.Not(translation)); + + // Negate the predicate, unless it's already negated, in which case remove that. + selectExpression.ApplyPredicate( + translation is SqlUnaryExpression { OperatorType: ExpressionType.Not, Operand: var nestedOperand } + ? nestedOperand + : _sqlExpressionFactory.Not(translation)); + + if (TrySimplifyValuesToInExpression(source, isNegated: true, out var simplifiedQuery)) + { + return simplifiedQuery; + } + selectExpression.ReplaceProjection(new List()); selectExpression.ApplyProjection(); if (selectExpression.Limit == null @@ -273,6 +440,11 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent } source = translatedSource; + + if (TrySimplifyValuesToInExpression(source, isNegated: false, out var simplifiedQuery)) + { + return simplifiedQuery; + } } var selectExpression = (SelectExpression)source.QueryExpression; @@ -404,7 +576,7 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent return null; } - if (selectExpression.Orderings.Count == 0) + if (!IsOrdered(selectExpression)) { _queryCompilationContext.Logger.RowLimitingOperationWithoutOrderByWarning(); } @@ -952,7 +1124,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp return null; } - if (selectExpression.Orderings.Count == 0) + if (!IsOrdered(selectExpression)) { _queryCompilationContext.Logger.RowLimitingOperationWithoutOrderByWarning(); } @@ -980,7 +1152,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp return null; } - if (selectExpression.Orderings.Count == 0) + if (!IsOrdered(selectExpression)) { _queryCompilationContext.Logger.RowLimitingOperationWithoutOrderByWarning(); } @@ -1583,9 +1755,13 @@ protected virtual bool IsValidSelectExpressionForExecuteUpdate( protected virtual SqlExpression? TranslateExpression(Expression expression) { var translation = _sqlTranslator.Translate(expression); - if (translation == null && _sqlTranslator.TranslationErrorDetails != null) + + if (translation is null) { - AddTranslationErrorDetails(_sqlTranslator.TranslationErrorDetails); + if (_sqlTranslator.TranslationErrorDetails != null) + { + AddTranslationErrorDetails(_sqlTranslator.TranslationErrorDetails); + } } return translation; @@ -1602,6 +1778,112 @@ 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 ApplyInferredTypeMappings( + Expression expression, + IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings) + => new RelationalInferredTypeMappingApplier(inferredTypeMappings).Visit(expression); + + /// + /// Determines whether the given is ordered, typically because orderings have been added to it. + /// + /// The to check for ordering. + /// Whether is ordered. + protected virtual bool IsOrdered(SelectExpression selectExpression) + => selectExpression.Orderings.Count > 0; + + /// + /// 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 TrySimplifyValuesToInExpression( + ShapedQueryExpression source, + bool isNegated, + [NotNullWhen(true)] out ShapedQueryExpression? simplifiedQuery) + { + if (source.QueryExpression is SelectExpression + { + Tables: [ValuesExpression + { + RowValues: [{ Values.Count: 2 }, ..], + ColumnNames: [ "_ord", "Value" ] + } 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++) + { + // Skip the first value (_ord), which is irrelevant for Contains + if (valuesExpression.RowValues[i].Values[1] 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( @@ -2275,4 +2557,298 @@ 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. + /// + /// + /// + /// This handles two cases: (1) an untyped column which type-inferred in the regular way, e.g. through comparison to a typed + /// column, and (2) set operations where on side is typed and the other is untyped. + /// + /// + /// Note that this visitor follows type columns across subquery projections. That is, if a root constant/parameter is buried + /// within subqueries, and somewhere above the column projected out of a subquery is inferred, this is picked up and propagated + /// all the way down. + /// + /// + /// The visitor dose not change the query tree in any way - it only populates the inferred type mappings it identified in + /// the given dictionary; actual application of the inferred type mappings happens later in + /// . We can't do this in a single pass since untyped roots + /// (e.g. may get visited before the type-inferred column referring to them (e.g. CROSS APPLY, + /// correlated subquery). + /// + /// + private class ColumnTypeMappingScanner : ExpressionVisitor + { + private readonly Dictionary<(TableExpressionBase, string), RelationalTypeMapping> _inferredColumns = new(); + + private SelectExpression? _currentSelectExpression; + private ProjectionExpression? _currentProjectionExpression; + + public IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> Scan(Expression expression) + { + _inferredColumns.Clear(); + + Visit(expression); + + return _inferredColumns; + } + + protected override Expression VisitExtension(Expression node) + { + switch (node) + { + // A column on a table which was possibly originally untyped (constant/parameter root or a subquery projection of one), + // which now does have a type mapping - this would mean in got inferred in the usual manner (comparison with typed column). + // Registered the inferred type mapping so it can be later applied back to its table, if it's untyped. + case ColumnExpression { TypeMapping: { } typeMapping } c when WasMaybeOriginallyUntyped(c): + { + RegisterInferredTypeMapping(c, typeMapping); + + return base.VisitExtension(node); + } + + // Similar to the above, but with ScalarSubqueryExpression the inferred type mapping is on the expression itself, while the + // ColumnExpression we need is on the subquery's projection. + case ScalarSubqueryExpression + { + TypeMapping: { } typeMapping, + Subquery.Projection: [{ Expression: ColumnExpression columnExpression }] + } + when WasMaybeOriginallyUntyped(columnExpression): + { + RegisterInferredTypeMapping(columnExpression, typeMapping); + + return base.VisitExtension(node); + } + + // 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. + case SetOperationBase + { + Source1.Projection: [{ Expression: var projection1 }], + Source2.Projection: [{ Expression: var projection2 }] + } + when UnwrapConvert(projection1) is ColumnExpression column1 && UnwrapConvert(projection2) is ColumnExpression column2: + { + if (projection1.TypeMapping is not null && WasMaybeOriginallyUntyped(column2)) + { + RegisterInferredTypeMapping(column2, projection1.TypeMapping); + } + + if (projection2.TypeMapping is not null && WasMaybeOriginallyUntyped(column1)) + { + RegisterInferredTypeMapping(column1, projection2.TypeMapping); + } + + return base.VisitExtension(node); + } + + // Record state on the SelectExpression and ProjectionExpression so that we can associate ColumnExpressions to the + // projections they're in (see below). + case SelectExpression selectExpression: + { + var parentSelectExpression = _currentSelectExpression; + _currentSelectExpression = selectExpression; + var visited = base.VisitExtension(selectExpression); + _currentSelectExpression = parentSelectExpression; + return visited; + } + + case ProjectionExpression projectionExpression: + { + var parentProjectionExpression = _currentProjectionExpression; + _currentProjectionExpression = projectionExpression; + var visited = base.VisitExtension(projectionExpression); + _currentProjectionExpression = parentProjectionExpression; + return visited; + } + + // When visiting subqueries, we want to propagate the inferred type mappings from above into the subquery, recursively. + // So we record state above to know which subquery and projection we're visiting; when visiting columns inside a projection + // which has an inferred type mapping from above, we register the inferred type mapping for that column too. + case ColumnExpression { TypeMapping: null } columnExpression + when _currentSelectExpression is not null + && _currentProjectionExpression is not null + && _inferredColumns.TryGetValue( + (_currentSelectExpression, _currentProjectionExpression.Alias), out var inferredTypeMapping) + && WasMaybeOriginallyUntyped(columnExpression): + { + RegisterInferredTypeMapping(columnExpression, inferredTypeMapping); + return base.VisitExtension(node); + } + + case ShapedQueryExpression shapedQueryExpression: + return shapedQueryExpression.UpdateQueryExpression(Visit(shapedQueryExpression.QueryExpression)); + + default: + return base.VisitExtension(node); + } + + bool WasMaybeOriginallyUntyped(ColumnExpression columnExpression) + { + var underlyingTable = columnExpression.Table is JoinExpressionBase joinExpression + ? joinExpression.Table + : columnExpression.Table; + + return underlyingTable switch + { + TableExpression + => false, + + SelectExpression subquery + => subquery.Projection.FirstOrDefault(p => p.Alias == columnExpression.Name) is { Expression.TypeMapping: null }, + + JoinExpressionBase + => throw new InvalidOperationException("Impossible: nested join"), + + // Any other table expression is considered a root (TableValuedFunctionExpression, ValuesExpression...) which *may* be + // untyped, so we record the possible inference (note that TableValuedFunctionExpression may be typed, or not) + _ => true, + }; + } + + SqlExpression UnwrapConvert(SqlExpression expression) + => expression is SqlUnaryExpression { OperatorType: ExpressionType.Convert } convert + ? UnwrapConvert(convert.Operand) + : expression; + } + + private void RegisterInferredTypeMapping(ColumnExpression columnExpression, RelationalTypeMapping inferredTypeMapping) + { + var underlyingTable = columnExpression.Table is JoinExpressionBase joinExpression + ? joinExpression.Table + : columnExpression.Table; + + if (_inferredColumns.TryGetValue((underlyingTable, columnExpression.Name), out var knownTypeMapping) + && inferredTypeMapping != knownTypeMapping) + { + throw new InvalidOperationException( + RelationalStrings.ConflictingTypeMappingsForPrimitiveCollection( + inferredTypeMapping.StoreType, knownTypeMapping.StoreType)); + } + + _inferredColumns[(underlyingTable, columnExpression.Name)] = inferredTypeMapping; + } + } + + /// + /// 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 root tables. + /// + protected class RelationalInferredTypeMappingApplier : ExpressionVisitor + { + private SelectExpression? _currentSelectExpression; + + /// + /// The inferred type mappings to be applied back on their query roots. + /// + protected IReadOnlyDictionary<(TableExpressionBase Table, string ColumnName), RelationalTypeMapping> InferredTypeMappings { get; } + + /// + /// Creates a new instance of the class. + /// + /// The inferred type mappings to be applied back on their query roots. + public RelationalInferredTypeMappingApplier( + IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings) + => InferredTypeMappings = inferredTypeMappings; + + /// + protected override Expression VisitExtension(Expression expression) + { + switch (expression) + { + case ColumnExpression { TypeMapping: null } columnExpression + when InferredTypeMappings.TryGetValue((columnExpression.Table, columnExpression.Name), out var typeMapping): + return columnExpression.ApplyTypeMapping(typeMapping); + + case SelectExpression selectExpression: + var parentSelectExpression = _currentSelectExpression; + _currentSelectExpression = selectExpression; + var visited = base.VisitExtension(expression); + _currentSelectExpression = parentSelectExpression; + return visited; + + // For ValueExpression, apply the inferred type mapping on all constants inside. + case ValuesExpression valuesExpression: + // By default, the ValuesExpression also contains an ordering by a synthetic increasing _ord. If the containing + // SelectExpression doesn't project it out or require it (limit/offset), strip that out. + // TODO: Strictly-speaking, this doesn't belong in this visitor which is about applying type mappings + return ApplyTypeMappingsOnValuesExpression( + valuesExpression, + stripOrdering: _currentSelectExpression is { Limit: null, Offset: null } + && !_currentSelectExpression.Projection.Any( + p => p.Expression is ColumnExpression { Name: "_ord" } c && c.Table == valuesExpression)); + + // SqlExpressions without an inferred type mapping indicates a problem in EF - everything should have been inferred. + // One exception is SqlFragmentExpression, which never has a type mapping. + case SqlExpression { TypeMapping: null } sqlExpression and not SqlFragmentExpression and not ColumnExpression: + throw new InvalidOperationException(RelationalStrings.NullTypeMappingInSqlTree(sqlExpression.Print())); + + case ShapedQueryExpression shapedQueryExpression: + return shapedQueryExpression.UpdateQueryExpression(Visit(shapedQueryExpression.QueryExpression)); + + default: + return base.VisitExtension(expression); + } + } + + /// + /// Applies the given type mappings to the values projected out by the given . + /// As an optimization, it can also strip the first _ord column if it's determined that it isn't needed (most cases). + /// + /// The to apply the mappings to. + /// Whether to strip the _ord column. + protected virtual ValuesExpression ApplyTypeMappingsOnValuesExpression(ValuesExpression valuesExpression, bool stripOrdering) + { + var inferredTypeMappings = InferredTypeMappings.TryGetValue((valuesExpression, "Value"), out var typeMapping) + ? new[] { null, typeMapping } + : new RelationalTypeMapping?[] { null, null }; + + Check.DebugAssert(valuesExpression.ColumnNames[0] == "_ord", "First ValuesExpression column isn't _ord"); + var newColumnNames = stripOrdering + ? valuesExpression.ColumnNames.Skip(1).ToArray() + : valuesExpression.ColumnNames; + + var newRowValues = new RowValueExpression[valuesExpression.RowValues.Count]; + for (var i = 0; i < newRowValues.Length; i++) + { + var rowValue = valuesExpression.RowValues[i]; + var newValues = new SqlExpression[valuesExpression.ColumnNames.Count - (stripOrdering ? 1 : 0)]; + for (var j = 0; j < valuesExpression.ColumnNames.Count; j++) + { + Check.DebugAssert(rowValue.Values[j] is SqlConstantExpression, "Non-constant SqlExpression in ValuesExpression"); + + if (j == 0 && stripOrdering) + { + continue; + } + + var value = (SqlConstantExpression)rowValue.Values[j]; + SqlExpression newValue = value; + + var inferredTypeMapping = inferredTypeMappings[j]; + if (inferredTypeMapping is not null && value.TypeMapping is null) + { + newValue = new SqlConstantExpression(Expression.Constant(value.Value, value.Type), inferredTypeMapping); + + // We currently add explicit conversions on the first row, to ensure that the inferred types are properly typed. + // See #30605 for removing that when not needed. + if (i == 0) + { + newValue = new SqlUnaryExpression(ExpressionType.Convert, newValue, newValue.Type, newValue.TypeMapping); + } + } + + newValues[j - (stripOrdering ? 1 : 0)] = newValue; + } + + newRowValues[i] = new RowValueExpression(newValues); + } + + return new(valuesExpression.Alias, newRowValues, newColumnNames, valuesExpression.GetAnnotations()); + } + } } diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 737d5fd13c3..38569b6d852 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; } @@ -149,8 +147,6 @@ protected virtual void AddTranslationErrorDetails(string details) return null; } - _sqlTypeMappingVerifyingExpressionVisitor.Visit(translation); - return translation; } @@ -704,6 +700,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 +740,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 +1945,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..e4a63704cc3 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs @@ -48,18 +48,18 @@ public SqlExpressionFactory(SqlExpressionFactoryDependencies dependencies) if (sqlExpression == null #pragma warning restore IDE0046 // Convert to conditional expression || sqlExpression.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, @@ -641,8 +641,9 @@ private void AddConditions(SelectExpression selectExpression, IEntityType entity return; } - var firstTable = selectExpression.Tables[0]; - var table = (firstTable as FromSqlExpression)?.Table ?? ((ITableBasedExpression)firstTable).Table; + var table = (selectExpression.Tables[0] as ITableBasedExpression)?.Table; + Check.DebugAssert(table is not null, "SelectExpression with unexpected missing table"); + if (table.IsOptional(entityType)) { SqlExpression? predicate = null; diff --git a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs index 46fdcdb0e10..bb4c44c9307 100644 --- a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs @@ -43,6 +43,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), @@ -55,6 +56,7 @@ ShapedQueryExpression shapedQueryExpression UnionExpression unionExpression => VisitUnion(unionExpression), UpdateExpression updateExpression => VisitUpdate(updateExpression), JsonScalarExpression jsonScalarExpression => VisitJsonScalar(jsonScalarExpression), + ValuesExpression valuesExpression => VisitValues(valuesExpression), _ => base.VisitExtension(extensionExpression), }; @@ -205,6 +207,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. /// @@ -288,4 +297,11 @@ ShapedQueryExpression shapedQueryExpression /// 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 values expression. + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. + 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/FromSqlExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs index f51942ea361..b82c4944f42 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs @@ -14,7 +14,7 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; /// not used in application code. /// /// -public class FromSqlExpression : TableExpressionBase, IClonableTableExpressionBase +public class FromSqlExpression : TableExpressionBase, ITableBasedExpression, IClonableTableExpressionBase { /// /// Creates a new instance of the class. diff --git a/src/EFCore.Relational/Query/SqlExpressions/ITableBasedExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/ITableBasedExpression.cs index 06eecbdab27..7254b9c5f08 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/ITableBasedExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/ITableBasedExpression.cs @@ -5,7 +5,7 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; /// /// -/// An interface that gives access to associated with given table source. +/// An interface that gives access to an optional associated with given table source. /// /// /// This type is typically used by database providers (and other extensions). It is generally @@ -15,7 +15,7 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; public interface ITableBasedExpression { /// - /// The associated with given table source. + /// The associated with given table source, if any. /// - ITableBase Table { get; } + ITableBase? Table { get; } } diff --git a/src/EFCore.Relational/Query/SqlExpressions/RowValueExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/RowValueExpression.cs new file mode 100644 index 00000000000..b87b95030b1 --- /dev/null +++ b/src/EFCore.Relational/Query/SqlExpressions/RowValueExpression.cs @@ -0,0 +1,146 @@ +// 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. + public RowValueExpression(IReadOnlyList values) + : base(typeof(ValueTuple), RowValueTypeMapping.Instance) + { + Check.NotEmpty(values, nameof(values)); + + 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); + } + + /// + /// 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); + + /// + 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 static RowValueTypeMapping Instance = new(typeof(ValueTuple)); + + private 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..cf47e26d965 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,14 @@ 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) + => 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 0af3351d300..defc4f156c1 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs @@ -605,7 +605,7 @@ public ConcreteColumnExpression( string name, TableReferenceExpression table, Type type, - RelationalTypeMapping typeMapping, + RelationalTypeMapping? typeMapping, bool nullable) : base(type, typeMapping) { @@ -629,7 +629,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); @@ -1019,12 +1022,11 @@ private sealed class CloningExpressionVisitor : ExpressionVisitor newArguments[i] = (SqlExpression)Visit(tableValuedFunctionExpression.Arguments[i]); } - var newTableValuedFunctionExpression = new TableValuedFunctionExpression( - tableValuedFunctionExpression.StoreFunction, - newArguments) - { - Alias = tableValuedFunctionExpression.Alias - }; + var newTableValuedFunctionExpression = tableValuedFunctionExpression.StoreFunction is null + ? new TableValuedFunctionExpression( + tableValuedFunctionExpression.Alias, tableValuedFunctionExpression.Name, newArguments) + : new TableValuedFunctionExpression( + tableValuedFunctionExpression.StoreFunction, newArguments) { Alias = tableValuedFunctionExpression.Alias }; foreach (var annotation in tableValuedFunctionExpression.GetAnnotations()) { diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index e3e46f7912c..d263d82608c 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -25,7 +25,6 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; public sealed partial class SelectExpression : TableExpressionBase { private const string DiscriminatorColumnAlias = "Discriminator"; - private const string SqlQuerySingleColumnAlias = "Value"; private static readonly IdentifierComparer IdentifierComparerInstance = new(); private static readonly Dictionary MirroredOperationMap = @@ -112,14 +111,32 @@ internal SelectExpression(SqlExpression? projection) } } - internal SelectExpression(Type type, RelationalTypeMapping typeMapping, FromSqlExpression fromSqlExpression) + /// + /// Creates a new instance of the class given a , with a single + /// column projection. + /// + /// The table expression. + /// The name of the column to add as the projection. + /// The type of the column to add as the projection. + /// The type mapping of the column to add as the projection. + /// Whether the column projected out is nullable. + public SelectExpression( + TableExpressionBase tableExpression, + string columnName, + Type columnType, + RelationalTypeMapping? columnTypeMapping, + bool? isColumnNullable = null) : base(null) { - var tableReferenceExpression = new TableReferenceExpression(this, fromSqlExpression.Alias!); - AddTable(fromSqlExpression, tableReferenceExpression); + var tableReferenceExpression = new TableReferenceExpression(this, tableExpression.Alias!); + AddTable(tableExpression, tableReferenceExpression); var columnExpression = new ConcreteColumnExpression( - SqlQuerySingleColumnAlias, tableReferenceExpression, type, typeMapping, type.IsNullableType()); + columnName, + tableReferenceExpression, + columnType.UnwrapNullableType(), + columnTypeMapping, + isColumnNullable ?? columnType.IsNullableType()); _projectionMapping[new ProjectionMember()] = columnExpression; } @@ -466,7 +483,9 @@ internal SelectExpression(IEntityType entityType, TableExpressionBase tableExpre throw new InvalidOperationException(RelationalStrings.SelectExpressionNonTphWithCustomTable(entityType.DisplayName())); } - var table = (tableExpressionBase as FromSqlExpression)?.Table ?? ((ITableBasedExpression)tableExpressionBase).Table; + var table = (tableExpressionBase as ITableBasedExpression)?.Table; + Check.DebugAssert(table is not null, "SelectExpression with unexpected missing table"); + var tableReferenceExpression = new TableReferenceExpression(this, tableExpressionBase.Alias!); AddTable(tableExpressionBase, tableReferenceExpression); @@ -3912,6 +3931,32 @@ public void PrepareForAggregate() } } + /// + /// Creates a that references a table on this . + /// + /// The table expression referenced by the column. + /// The column name. + /// The column CLR type. + /// The column's type mapping. + /// Whether the column is nullable. + public ColumnExpression CreateColumnExpression( + TableExpressionBase tableExpression, + string columnName, + Type type, + RelationalTypeMapping? typeMapping, + bool? isColumnNullable = null) + { + var tableIndex = _tables.FindIndex(teb => ReferenceEquals(teb, tableExpression)); + var tableReferenceExpression = _tableReferences[tableIndex]; + + return new ConcreteColumnExpression( + columnName, + tableReferenceExpression, + type.UnwrapNullableType(), + typeMapping, + isColumnNullable ?? type.IsNullableType()); + } + /// /// 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.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs index 888e1bbe5ac..dc9d8fdab3f 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs @@ -24,20 +24,66 @@ public class TableValuedFunctionExpression : TableExpressionBase, ITableBasedExp public TableValuedFunctionExpression(IStoreFunction storeFunction, IReadOnlyList arguments) : this( storeFunction.Name[..1].ToLowerInvariant(), - storeFunction, + storeFunction.Name, + storeFunction.Schema, + storeFunction.IsBuiltIn, arguments, annotations: null) + { + StoreFunction = storeFunction; + } + + /// + /// Creates a new instance of the class. + /// + /// The name of the function. + /// The arguments of the function. + /// A collection of annotations associated with this expression. + public TableValuedFunctionExpression( + string name, + IReadOnlyList arguments, + IEnumerable? annotations = null) + : this(name[..1].ToLowerInvariant(), name, schema: null, builtIn: true, arguments, annotations) { } - private TableValuedFunctionExpression( + /// + /// Creates a new instance of the class. + /// + /// A string alias for the table source. + /// The name of the function. + /// The arguments of the function. + /// A collection of annotations associated with this expression. + public TableValuedFunctionExpression( + string alias, + string name, + IReadOnlyList arguments, + IEnumerable? annotations = null) + : this(alias, name, schema: null, builtIn: true, arguments, annotations) + { + } + + /// + /// Creates a new instance of the class. + /// + /// A string alias for the table source. + /// The name of the function. + /// The schema of the function. + /// Whether the function is built-in. + /// The arguments of the function. + /// A collection of annotations associated with this expression. + protected TableValuedFunctionExpression( string alias, - IStoreFunction storeFunction, + string name, + string? schema, + bool builtIn, IReadOnlyList arguments, - IEnumerable? annotations) + IEnumerable? annotations = null) : base(alias, annotations) { - StoreFunction = storeFunction; + Name = name; + Schema = schema; + IsBuiltIn = builtIn; Arguments = arguments; } @@ -54,17 +100,32 @@ public override string? Alias /// /// The store function. /// - public virtual IStoreFunction StoreFunction { get; } + public virtual IStoreFunction? StoreFunction { get; } + + /// + ITableBase? ITableBasedExpression.Table + => StoreFunction; + + /// + /// The name of the function. + /// + public string Name { get; } + + /// + /// The schema of the function. + /// + public string? Schema { get; } + + /// + /// Gets the value indicating whether the function is built-in. + /// + public bool IsBuiltIn { get; } /// /// The list of arguments of this function. /// public virtual IReadOnlyList Arguments { get; } - /// - ITableBase ITableBasedExpression.Table - => StoreFunction; - /// protected override Expression VisitChildren(ExpressionVisitor visitor) { @@ -77,7 +138,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 +150,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,6 +183,9 @@ public override bool Equals(object? obj) private bool Equals(TableValuedFunctionExpression tableValuedFunctionExpression) => base.Equals(tableValuedFunctionExpression) + && Name == tableValuedFunctionExpression.Name + && Schema == tableValuedFunctionExpression.Schema + && IsBuiltIn == tableValuedFunctionExpression.IsBuiltIn && StoreFunction == tableValuedFunctionExpression.StoreFunction && Arguments.SequenceEqual(tableValuedFunctionExpression.Arguments); @@ -130,6 +194,9 @@ public override int GetHashCode() { var hash = new HashCode(); hash.Add(base.GetHashCode()); + hash.Add(Name); + hash.Add(Schema); + hash.Add(IsBuiltIn); hash.Add(StoreFunction); for (var i = 0; i < Arguments.Count; 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..d08e4628e51 --- /dev/null +++ b/src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs @@ -0,0 +1,181 @@ +// 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; + +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, IClonableTableExpressionBase +{ + /// + /// 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 + })) + { + // See #30734 for non-constants + throw new ArgumentException("Only constant expressions are supported in ValuesExpression"); + } +#endif + + RowValues = rowValues; + ColumnNames = columnNames; + } + + /// + /// The alias assigned to this table source. + /// + [NotNull] + public override string? Alias + { + get => base.Alias!; + internal set => base.Alias = value; + } + + /// + 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); + + /// + public TableExpressionBase Clone() + => CreateWithAnnotations(GetAnnotations()); + + /// + 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..efce96b4736 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -185,6 +185,33 @@ protected virtual TableExpressionBase Visit(TableExpressionBase tableExpressionB case OuterApplyExpression outerApplyExpression: return outerApplyExpression.Update(Visit(outerApplyExpression.Table)); + case ValuesExpression valuesExpression: + { + RowValueExpression[]? newRowValues = null; + + for (var i = 0; i < valuesExpression.RowValues.Count; i++) + { + var rowValue = valuesExpression.RowValues[i]; + var newRowValue = (RowValueExpression)VisitRowValue(rowValue, allowOptimizedExpansion: false, out _); + + if (newRowValue != rowValue && newRowValues is null) + { + newRowValues = new RowValueExpression[valuesExpression.RowValues.Count]; + for (var j = 0; j < i; j++) + { + newRowValues[j] = valuesExpression.RowValues[j]; + } + } + + if (newRowValues is not null) + { + newRowValues[i] = newRowValue; + } + } + + return newRowValues is null ? valuesExpression : valuesExpression.Update(newRowValues); + } + case SelectExpression selectExpression: return Visit(selectExpression); @@ -403,6 +430,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 @@ -826,6 +855,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.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs index a994c313168..536f658af60 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs @@ -120,6 +120,7 @@ public static IServiceCollection AddEntityFrameworkSqlServer(this IServiceCollec .TryAdd() .TryAdd() .TryAdd() + .TryAdd() .TryAdd() .TryAdd() .TryAdd() diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs index 3e0407691a0..7ea65b2626a 100644 --- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs @@ -670,6 +670,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 @@ -765,4 +786,25 @@ protected override Expression VisitUpdate(UpdateExpression updateExpression) /// 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 + /// 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 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/SqlServerOpenJsonExpression.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs new file mode 100644 index 00000000000..c46b07f60ae --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs @@ -0,0 +1,149 @@ +// 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( + 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; + } + + /// + /// 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 Update( + SqlExpression jsonExpression, + SqlExpression? path, + IReadOnlyList? columnInfos = null) + => jsonExpression == JsonExpression + && path == Path + && (columnInfos is null ? ColumnInfos is null : ColumnInfos is not null && columnInfos.SequenceEqual(ColumnInfos)) + ? this + : new SqlServerOpenJsonExpression(Alias, jsonExpression, path, columnInfos); + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + 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) + => ReferenceEquals(this, obj) || (obj is SqlServerOpenJsonExpression openJsonExpression && Equals(openJsonExpression)); + + private bool Equals(SqlServerOpenJsonExpression openJsonExpression) + => base.Equals(openJsonExpression) + && (ColumnInfos is null + ? openJsonExpression.ColumnInfos is null + : openJsonExpression.ColumnInfos is not null && ColumnInfos.SequenceEqual(openJsonExpression.ColumnInfos)); + + /// + public override int GetHashCode() + => base.GetHashCode(); + + /// + /// 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/SqlServerQueryRootProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryRootProcessor.cs new file mode 100644 index 00000000000..eaff1e7eb80 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryRootProcessor.cs @@ -0,0 +1,44 @@ +// 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; + +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 SqlServerQueryRootProcessor : RelationalQueryRootProcessor +{ + 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 + /// 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 SqlServerQueryRootProcessor( + QueryTranslationPreprocessorDependencies dependencies, + RelationalQueryTranslationPreprocessorDependencies relationalDependencies, + QueryCompilationContext queryCompilationContext, + ISqlServerSingletonOptions sqlServerSingletonOptions) + : base(dependencies, relationalDependencies, queryCompilationContext) + => _supportsOpenJson = sqlServerSingletonOptions.CompatibilityLevel >= 130; + + /// + /// Indicates that a can be converted to a , if the + /// configured SQL Server version supports OPENJSON. + /// + /// + /// 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 ShouldConvertToParameterQueryRoot(ParameterExpression parameterExpression) + => _supportsOpenJson; +} diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs index 3f7fd0bc9eb..227a5329192 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 AS x 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 Expression VisitValues(ValuesExpression valuesExpression) + { + base.VisitValues(valuesExpression); + + // SQL Server VALUES supports setting the projects column names: FROM (VALUES (1), (2)) AS v(foo) + Sql.Append("("); + + for (var i = 0; i < valuesExpression.ColumnNames.Count; i++) + { + if (i > 0) + { + Sql.Append(", "); + } + + Sql.Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.ColumnNames[i])); + } + + Sql.Append(")"); + + return valuesExpression; + } + + /// + /// 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) + { + // SQL Server supports providing the names of columns projected out of VALUES: (VALUES (1, 3), (2, 4)) AS x(a, b) + // (this is implemented in VisitValues above). + // 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. + + var rowValues = valuesExpression.RowValues; + + Sql.Append("VALUES "); + + for (var i = 0; i < rowValues.Count; i++) + { + if (i > 0) + { + Sql.Append(", "); + } + + Visit(valuesExpression.RowValues[i]); + } + } + /// /// 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 @@ -305,6 +376,9 @@ when tableExpression.FindAnnotation(SqlServerAnnotationNames.TemporalOperationTy case SqlServerAggregateFunctionExpression aggregateFunctionExpression: return VisitSqlServerAggregateFunction(aggregateFunctionExpression); + + case SqlServerOpenJsonExpression openJsonExpression: + return VisitOpenJsonExpression(openJsonExpression); } return base.VisitExtension(extensionExpression); @@ -381,6 +455,65 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp return jsonScalarExpression; } + /// + /// 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..f22ddc0236a --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessor.cs @@ -0,0 +1,41 @@ +// 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; + +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 +{ + private readonly ISqlServerSingletonOptions _sqlServerSingletonOptions; + + /// + /// 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 SqlServerQueryTranslationPreprocessor( + QueryTranslationPreprocessorDependencies dependencies, + RelationalQueryTranslationPreprocessorDependencies relationalDependencies, + ISqlServerSingletonOptions sqlServerSingletonOptions, + QueryCompilationContext queryCompilationContext) + : base(dependencies, relationalDependencies, queryCompilationContext) + => _sqlServerSingletonOptions = sqlServerSingletonOptions; + + /// + /// 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 ProcessQueryRoots(Expression expression) + => new SqlServerQueryRootProcessor(Dependencies, RelationalDependencies, QueryCompilationContext, _sqlServerSingletonOptions) + .Visit(expression); +} diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessorFactory.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessorFactory.cs new file mode 100644 index 00000000000..ca3f6d16d68 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessorFactory.cs @@ -0,0 +1,53 @@ +// 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; + +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 SqlServerQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory +{ + private readonly ISqlServerSingletonOptions _sqlServerSingletonOptions; + + /// + /// 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 SqlServerQueryTranslationPreprocessorFactory( + QueryTranslationPreprocessorDependencies dependencies, + RelationalQueryTranslationPreprocessorDependencies relationalDependencies, + ISqlServerSingletonOptions sqlServerSingletonOptions) + { + Dependencies = dependencies; + RelationalDependencies = relationalDependencies; + _sqlServerSingletonOptions = sqlServerSingletonOptions; + } + + /// + /// 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. + /// + public virtual QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext) + => new SqlServerQueryTranslationPreprocessor( + Dependencies, RelationalDependencies, _sqlServerSingletonOptions, queryCompilationContext); +} diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs index ea225751457..36979d361cf 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; } /// @@ -100,6 +111,86 @@ protected override Expression VisitExtension(Expression extensionExpression) 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 TranslateCollection( + SqlExpression sqlExpression, + RelationalTypeMapping? elementTypeMapping, + string tableAlias) + { + var elementClrType = sqlExpression.Type.GetSequenceType(); + + // Generate the OpenJson function expression, and wrap it in a SelectExpression. + // Note that we want to preserve the ordering of the element's, i.e. for the rows coming out of OpenJson to be the same as the + // element order in the original JSON array. + // Unfortunately, OpenJson with an explicit schema (with the WITH clause) doesn't support this; so we use the variant with the + // default schema, which returns a 'key' column containing the index, and order by that. This also means we need to explicitly + // apply a conversion from the values coming out of OpenJson (always NVARCHAR(MAX)) to the required relational store type. + var openJsonExpression = new TableValuedFunctionExpression(tableAlias, "OpenJson", new[] { sqlExpression }); + + // TODO: When we have metadata to determine if the element is nullable, pass that here to SelectExpression + var selectExpression = new SelectExpression(openJsonExpression, columnName: "value", columnType: elementClrType, columnTypeMapping: elementTypeMapping, isColumnNullable: null); + + if (elementTypeMapping is { StoreType: not "nvarchar(max)" }) + { + // For columns (where we know the type mapping), we need to overwrite the projection in order to insert a CAST() to the actual + // relational store type we expect out of the JSON array (e.g. OpenJson returns strings, we want datetime2). + // For parameters (where we don't yet know the type mapping), we'll need to do that later, after the type mapping has been + // inferred. + // TODO: Need to pass through the type mapping API for converting the JSON value (nvarchar) to the relational store type (e.g. + // datetime2), see #30677 + selectExpression.ReplaceProjection( + new Dictionary + { + { + new ProjectionMember(), _sqlExpressionFactory.Convert( + selectExpression.CreateColumnExpression( + openJsonExpression, + "value", + typeof(string), + _typeMappingSource.FindMapping("nvarchar(max)"), + // TODO: When we have metadata to determine if the element is nullable, pass that here to + // SelectExpression + isColumnNullable: null), + elementClrType, + elementTypeMapping) + } + }); + } + + // Append an ordering for the OpenJson 'key' column, converting it from nvarchar to int. + selectExpression.AppendOrdering( + new OrderingExpression( + _sqlExpressionFactory.Convert( + selectExpression.CreateColumnExpression( + openJsonExpression, + "key", + typeof(string), + typeMapping: _typeMappingSource.FindMapping("nvarchar(8000)"), + isColumnNullable: false), + typeof(int), + _typeMappingSource.FindMapping(typeof(int))), + ascending: true)); + + 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); + } + /// /// 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 +297,148 @@ 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 override Expression ApplyInferredTypeMappings( + Expression expression, + IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings) + => new SqlServerInferredTypeMappingApplier(_typeMappingSource, _sqlExpressionFactory, 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 + /// 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 SqlServerInferredTypeMappingApplier : RelationalInferredTypeMappingApplier + { + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private Dictionary? _currentSelectInferredTypeMappings; + + /// + /// 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 SqlServerInferredTypeMappingApplier( + IRelationalTypeMappingSource typeMappingSource, + ISqlExpressionFactory sqlExpressionFactory, + IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings) + : base(inferredTypeMappings) + => (_typeMappingSource, _sqlExpressionFactory) = (typeMappingSource, 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 + /// 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) + { + switch (expression) + { + case TableValuedFunctionExpression { Name: "OpenJson", Schema: null, IsBuiltIn: true } openJsonExpression + when InferredTypeMappings.TryGetValue((openJsonExpression, "value"), out var typeMapping): + return ApplyTypeMappingsOnOpenJsonExpression(openJsonExpression, new[] { typeMapping }); + + // Above, we applied the type mapping the the parameter that OpenJson accepts as an argument. + // But the inferred type mapping also needs to be applied as a SQL conversion on the column projections coming out of the + // SelectExpression containing the OpenJson call. So we set state to know about OpenJson tables and their type mappings + // in the immediate SelectExpression, and continue visiting down (see ColumnExpression visitation below). + case SelectExpression selectExpression: + { + Dictionary? previousSelectInferredTypeMappings = null; + + foreach (var table in selectExpression.Tables) + { + if (table is TableValuedFunctionExpression { Name: "OpenJson", Schema: null, IsBuiltIn: true } openJsonExpression + && InferredTypeMappings.TryGetValue((openJsonExpression, "value"), out var inferredTypeMapping)) + { + if (previousSelectInferredTypeMappings is null) + { + previousSelectInferredTypeMappings = _currentSelectInferredTypeMappings; + _currentSelectInferredTypeMappings = new(); + } + + _currentSelectInferredTypeMappings![openJsonExpression] = inferredTypeMapping; + } + } + + var visited = base.VisitExtension(expression); + + _currentSelectInferredTypeMappings = previousSelectInferredTypeMappings; + + return visited; + } + + case ColumnExpression { Name: "value" } columnExpression + when _currentSelectInferredTypeMappings is not null + && _currentSelectInferredTypeMappings.TryGetValue(columnExpression.Table, out var inferredTypeMapping): + return ApplyTypeMappingOnColumn(columnExpression, inferredTypeMapping); + + default: + return 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. + /// + protected virtual TableValuedFunctionExpression ApplyTypeMappingsOnOpenJsonExpression( + TableValuedFunctionExpression openJsonExpression, + IReadOnlyList typeMappings) + { + Check.DebugAssert(typeMappings.Count == 1, "typeMappings.Count == 1"); + var elementTypeMapping = typeMappings[0]; + + // Constant queryables are translated to VALUES, no need for JSON. + // Column queryables have their type mapping from the model, so we don't ever need to apply an inferred mapping on them. + if (openJsonExpression.Arguments[0] is not SqlParameterExpression parameterExpression) + { + return openJsonExpression; + } + + // TODO: We shouldn't need to manually construct the JSON string type mapping this way; we need to be able to provide the + // TODO: element's store type mapping as input to _typeMappingSource.FindMapping. + // TODO: When this is done, revert converter equality check in QuerySqlGenerator.VisitSqlParameter back to reference equality, + // since we'll always have the same instance of the type mapping returned from the type mapping source. Also remove + // CollectionToJsonStringConverter.Equals etc. + // TODO: Note: NpgsqlTypeMappingSource exposes FindContainerMapping() for this purpose. + if (_typeMappingSource.FindMapping(typeof(string)) is not SqlServerStringTypeMapping parameterTypeMapping) + { + throw new InvalidOperationException("Type mapping for 'string' could not be found or was not a SqlServerStringTypeMapping"); + } + + parameterTypeMapping = (SqlServerStringTypeMapping)parameterTypeMapping + .Clone(new CollectionToJsonStringConverter(parameterExpression.Type, elementTypeMapping)); + + parameterTypeMapping = (SqlServerStringTypeMapping)parameterTypeMapping.CloneWithElementTypeMapping(elementTypeMapping); + + var arguments = openJsonExpression.Arguments.ToArray(); + arguments[0] = parameterExpression.ApplyTypeMapping(parameterTypeMapping); + return openJsonExpression.Update(arguments); + } + + /// + /// 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 ApplyTypeMappingOnColumn(ColumnExpression columnExpression, RelationalTypeMapping typeMapping) + // OpenJson's value column has type nvarchar(max); apply a CAST() unless that's the inferred element type mapping + => typeMapping.StoreType is "nvarchar(max)" + ? columnExpression + : _sqlExpressionFactory.Convert(columnExpression, typeMapping.ClrType, typeMapping); + } } diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs index 8d0d053cd62..751da51b988 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs @@ -119,6 +119,17 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p return new SqlServerStringTypeMapping(parameters, _sqlDbType); } + /// + /// 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 SqlServerStringTypeMapping( + Parameters.WithCoreParameters(Parameters.CoreParameters.WithElementTypeMapping(elementTypeMapping)), + _sqlDbType); + /// /// 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.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs index 21f5884a764..2394c2e3892 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) { @@ -415,6 +423,87 @@ 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 IEnumerable. + // 2. The store type is either not given or a string type. + // 3. The element CLR type has a supported type mapping which isn't itself a collection (nested collections not yet supported). + + // Note that e.g. Newtonsoft.Json's JToken is enumerable over itself, exclude that scenario to avoid stack overflow. + if (mappingInfo.ClrType?.TryGetElementType(typeof(IEnumerable<>)) is not { } elementClrType + || elementClrType == mappingInfo.ClrType) + { + 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 + + // Make sure the element type is mapped and isn't itself a collection (nested collections not supported) + if (FindMapping(elementClrType) is not { ElementTypeMapping: null } 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 CollectionToJsonStringConverter(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) + { + // The JSON representation for new[] { 1, 2 } is AQI= (base64?), this cannot simply be cast to varbinary(max) (0x0102) + if (elementTypeMapping is not SqlServerByteArrayTypeMapping) + { + stringTypeMapping = (SqlServerStringTypeMapping)stringTypeMapping.CloneWithElementTypeMapping(elementTypeMapping); + } + } + + return stringTypeMapping; + } + private static readonly List NameBasesUsingPrecision = new() { "decimal", diff --git a/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs index ed862f528a2..f88ec4741b3 100644 --- a/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs +++ b/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs @@ -110,6 +110,7 @@ public static IServiceCollection AddEntityFrameworkSqlite(this IServiceCollectio .TryAdd() .TryAdd() .TryAdd() + .TryAdd() .TryAdd() .TryAdd() .TryAdd() diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryRootProcessor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryRootProcessor.cs new file mode 100644 index 00000000000..15abafb0867 --- /dev/null +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryRootProcessor.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Data.Sqlite; + +namespace Microsoft.EntityFrameworkCore.Sqlite.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 SqliteQueryRootProcessor : RelationalQueryRootProcessor +{ + 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 + /// 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 SqliteQueryRootProcessor( + QueryTranslationPreprocessorDependencies dependencies, + RelationalQueryTranslationPreprocessorDependencies relationalDependencies, + QueryCompilationContext queryCompilationContext) + : base(dependencies, relationalDependencies, queryCompilationContext) + => _areJsonFunctionsSupported = new Version(new SqliteConnection().ServerVersion) >= new Version(3, 38); + + + /// + /// Indicates that a can be converted to a , if the + /// configured SQL Server version supports JSON functions (json_each). + /// + /// + /// 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 ShouldConvertToParameterQueryRoot(ParameterExpression parameterExpression) + => _areJsonFunctionsSupported; +} diff --git a/src/EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessor.cs similarity index 57% rename from src/EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs rename to src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessor.cs index 7a433e8c5c5..214787bd6f2 100644 --- a/src/EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessor.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.Sqlite.Query.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -9,19 +9,20 @@ 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 SqliteQueryTranslationPreprocessor : RelationalQueryTranslationPreprocessor { - private readonly IModel _model; - /// /// 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 TableValuedFunctionToQueryRootConvertingExpressionVisitor(IModel model) + public SqliteQueryTranslationPreprocessor( + QueryTranslationPreprocessorDependencies dependencies, + RelationalQueryTranslationPreprocessorDependencies relationalDependencies, + QueryCompilationContext queryCompilationContext) + : base(dependencies, relationalDependencies, queryCompilationContext) { - _model = model; } /// @@ -30,18 +31,7 @@ public TableValuedFunctionToQueryRootConvertingExpressionVisitor(IModel model) /// 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); + protected override Expression ProcessQueryRoots(Expression expression) + => new SqliteQueryRootProcessor(Dependencies, RelationalDependencies, QueryCompilationContext) + .Visit(expression); } diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessorFactory.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessorFactory.cs new file mode 100644 index 00000000000..e7a8c021558 --- /dev/null +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessorFactory.cs @@ -0,0 +1,46 @@ +// 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.Sqlite.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 SqliteQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory +{ + /// + /// 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 SqliteQueryTranslationPreprocessorFactory( + QueryTranslationPreprocessorDependencies dependencies, + RelationalQueryTranslationPreprocessorDependencies relationalDependencies) + { + Dependencies = dependencies; + RelationalDependencies = relationalDependencies; + } + + /// + /// 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. + /// + public virtual QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext) + => new SqliteQueryTranslationPreprocessor(Dependencies, RelationalDependencies, queryCompilationContext); +} diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs index 140244153c2..e51e75d9916 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,9 @@ namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal; /// public class SqliteQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor { + 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 @@ -26,6 +31,8 @@ public SqliteQueryableMethodTranslatingExpressionVisitor( QueryCompilationContext queryCompilationContext) : base(dependencies, relationalDependencies, queryCompilationContext) { + _typeMappingSource = relationalDependencies.TypeMappingSource; + _sqlExpressionFactory = relationalDependencies.SqlExpressionFactory; } /// @@ -38,6 +45,8 @@ protected SqliteQueryableMethodTranslatingExpressionVisitor( SqliteQueryableMethodTranslatingExpressionVisitor parentVisitor) : base(parentVisitor) { + _typeMappingSource = parentVisitor._typeMappingSource; + _sqlExpressionFactory = parentVisitor._sqlExpressionFactory; } /// @@ -111,8 +120,238 @@ 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? TranslateCount(ShapedQueryExpression source, LambdaExpression? predicate) + { + // Simplify x.Array.Count() => json_array_length(x.Array) instead of SELECT COUNT(*) FROM json_each(x.Array) + if (predicate is null && source.QueryExpression is SelectExpression + { + Tables: [TableValuedFunctionExpression { Name: "json_each", Schema: null, IsBuiltIn: true, Arguments: [var array] }], + GroupBy: [], + Having: null, + IsDistinct: false, + Limit: null, + Offset: null + }) + { + var translation = _sqlExpressionFactory.Function( + "json_array_length", + new[] { array }, + nullable: true, + argumentsPropagateNullability: new[] { true }, + typeof(int)); + + return source.UpdateQueryExpression(_sqlExpressionFactory.Select(translation)); + } + + return base.TranslateCount(source, predicate); + } + + /// + /// 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 TranslateCollection( + SqlExpression sqlExpression, + RelationalTypeMapping? elementTypeMapping, + string tableAlias) + { + var elementClrType = sqlExpression.Type.GetSequenceType(); + + var jsonEachExpression = new TableValuedFunctionExpression(tableAlias, "json_each", new[] { sqlExpression }); + + // TODO: When we have metadata to determine if the element is nullable, pass that here to SelectExpression + var selectExpression = new SelectExpression( + jsonEachExpression, columnName: "value", columnType: elementClrType, columnTypeMapping: elementTypeMapping, + isColumnNullable: null); + + // TODO: SQLite does have REAL and BLOB types, which JSON does not. Need to possibly cast to that. + if (elementTypeMapping is not null) + { + // TODO: In any case, we still ned to pass through the type mapping API for doing any conversions (e.g. for datetime, from JSON + // ISO8601 to SQLite's format without the T), see #30677. Do this here. + } + + // Append an ordering for the json_each 'key' column. + selectExpression.AppendOrdering( + new OrderingExpression( + selectExpression.CreateColumnExpression( + jsonEachExpression, + "key", + typeof(int), + typeMapping: _typeMappingSource.FindMapping(typeof(int)), + isColumnNullable: false), + ascending: true)); + + 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 ?? expression.Type; + + /// + /// 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 ApplyInferredTypeMappings( + Expression expression, + IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings) + => new SqliteInferredTypeMappingApplier(_typeMappingSource, _sqlExpressionFactory, 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 + /// 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 SqliteInferredTypeMappingApplier : RelationalInferredTypeMappingApplier + { + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private Dictionary? _currentSelectInferredTypeMappings; + + /// + /// 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 SqliteInferredTypeMappingApplier( + IRelationalTypeMappingSource typeMappingSource, + ISqlExpressionFactory sqlExpressionFactory, + IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings) + : base(inferredTypeMappings) + => (_typeMappingSource, _sqlExpressionFactory) = (typeMappingSource, 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 + /// 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) + { + switch (expression) + { + case TableValuedFunctionExpression { Name: "json_each", Schema: null, IsBuiltIn: true } jsonEachExpression + when InferredTypeMappings.TryGetValue((jsonEachExpression, "value"), out var typeMapping): + return ApplyTypeMappingsOnJsonEachExpression(jsonEachExpression, typeMapping); + + // Above, we applied the type mapping the the parameter that json_each accepts as an argument. + // But the inferred type mapping also needs to be applied as a SQL conversion on the column projections coming out of the + // SelectExpression containing the json_each call. So we set state to know about json_each tables and their type mappings + // in the immediate SelectExpression, and continue visiting down (see ColumnExpression visitation below). + case SelectExpression selectExpression: + { + Dictionary? previousSelectInferredTypeMappings = null; + + foreach (var table in selectExpression.Tables) + { + if (table is TableValuedFunctionExpression { Name: "json_each", Schema: null, IsBuiltIn: true } jsonEachExpression + && InferredTypeMappings.TryGetValue((jsonEachExpression, "value"), out var inferredTypeMapping)) + { + if (previousSelectInferredTypeMappings is null) + { + previousSelectInferredTypeMappings = _currentSelectInferredTypeMappings; + _currentSelectInferredTypeMappings = new(); + } + + _currentSelectInferredTypeMappings![jsonEachExpression] = inferredTypeMapping; + } + } + + var visited = base.VisitExtension(expression); + + _currentSelectInferredTypeMappings = previousSelectInferredTypeMappings; + + return visited; + } + + case ColumnExpression { Name: "value" } columnExpression + when _currentSelectInferredTypeMappings is not null + && _currentSelectInferredTypeMappings.TryGetValue(columnExpression.Table, out var inferredTypeMapping): + return ApplyTypeMappingOnColumn(columnExpression, inferredTypeMapping); + + default: + return 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. + /// + protected virtual TableValuedFunctionExpression ApplyTypeMappingsOnJsonEachExpression( + TableValuedFunctionExpression jsonEachExpression, + RelationalTypeMapping inferredTypeMapping) + { + // Constant queryables are translated to VALUES, no need for JSON. + // Column queryables have their type mapping from the model, so we don't ever need to apply an inferred mapping on them. + if (jsonEachExpression.Arguments[0] is not SqlParameterExpression parameterExpression) + { + return jsonEachExpression; + } + + // TODO: We shouldn't need to manually construct the JSON string type mapping this way; we need to be able to provide the + // TODO: element's store type mapping as input to _typeMappingSource.FindMapping. + if (_typeMappingSource.FindMapping(typeof(string)) is not SqliteStringTypeMapping parameterTypeMapping) + { + throw new InvalidOperationException("Type mapping for 'string' could not be found or was not a SqliteStringTypeMapping"); + } + + parameterTypeMapping = (SqliteStringTypeMapping)parameterTypeMapping + .Clone(new CollectionToJsonStringConverter(parameterExpression.Type, inferredTypeMapping)); + + parameterTypeMapping = (SqliteStringTypeMapping)parameterTypeMapping.CloneWithElementTypeMapping(inferredTypeMapping); + + return jsonEachExpression.Update(new[] { parameterExpression.ApplyTypeMapping(parameterTypeMapping) }); + } + + /// + /// 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 ApplyTypeMappingOnColumn(ColumnExpression columnExpression, RelationalTypeMapping typeMapping) + => typeMapping switch + { + // TODO: These server-side conversions need to be managed on the type mapping + + // The "standard" JSON timestamp representation is ISO8601, with a T between date and time; but SQLite's representation has + // no T. Apply a conversion on the value coming out of json_each. + SqliteDateTimeTypeMapping => _sqlExpressionFactory.Function( + "datetime", new[] { columnExpression }, nullable: true, new[] { true }, typeof(DateTime), typeMapping), + + SqliteGuidTypeMapping => _sqlExpressionFactory.Function( + "upper", new[] { columnExpression }, nullable: true, new[] { true }, typeof(Guid), typeMapping), + + _ => columnExpression + }; + } } 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..08b5cdc4b89 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,69 @@ public static bool IsSpatialiteType(string columnType) return null; } + private RelationalTypeMapping? FindCollectionMapping(RelationalTypeMappingInfo mappingInfo) + { + // Make sure the element type is mapped and isn't itself a collection (nested collections not supported) + if (mappingInfo is { StoreTypeName: TextTypeName or null } + && mappingInfo.ClrType?.TryGetElementType(typeof(IEnumerable<>)) is { } elementClrType + && FindMapping(elementClrType) is { ElementTypeMapping: null } 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 CollectionToJsonStringConverter(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) + { + switch (elementTypeMapping) + { + // The JSON representation for DateTimeOffset is ISO8601 (2023-01-01T12:30:00+02:00), but our SQL literal representation + // is 2023-01-01 12:30:00+02:00 (no T). + // datetime('2023-01-01T12:30:00+02:00') yields '2023-01-01 10:30:00' - converted to UTC, no timezone. + case SqliteDateTimeOffsetTypeMapping: + // The JSON representation for decimal is e.g. 1 (JSON int), whereas our literal representation is "1.0" (string) + case SqliteDecimalTypeMapping: + // The JSON representation for new[] { 1, 2 } is AQI= (base64?), our SQL literal representation is X'0102' + case ByteArrayTypeMapping: + break; + + + default: + stringTypeMapping = (SqliteStringTypeMapping)stringTypeMapping.CloneWithElementTypeMapping(elementTypeMapping); + break; + } + } + + return stringTypeMapping; + } + + return null; + } + private readonly Func[] _typeRules = { name => Contains(name, "INT") diff --git a/src/EFCore/Query/InlineQueryRootExpression.cs b/src/EFCore/Query/InlineQueryRootExpression.cs new file mode 100644 index 00000000000..b94997b6d33 --- /dev/null +++ b/src/EFCore/Query/InlineQueryRootExpression.cs @@ -0,0 +1,93 @@ +// 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 an inline query root within the query (e.g. new[] { 1, 2, 3 }). +/// +/// +/// This type is typically used by database providers (and other extensions). It is generally not used in application code. +/// +/// +public class InlineQueryRootExpression : QueryRootExpression +{ + /// + /// The values contained in this query root. + /// + public IReadOnlyList Values { get; } + + /// + /// Creates a new instance of the class. + /// + /// The query provider associated with this query root. + /// The values contained in this query root. + /// The element type this query root represents. + public InlineQueryRootExpression(IAsyncQueryProvider asyncQueryProvider, IReadOnlyList values, Type elementType) + : base(asyncQueryProvider, elementType) + { + Values = values; + } + + /// + /// Creates a new instance of the class. + /// + /// An expression containing the values that this query root represents. + /// The element type this query root represents. + public InlineQueryRootExpression(IReadOnlyList values, Type elementType) + : base(elementType) + { + Values = values; + } + + /// + public override Expression DetachQueryProvider() + => new InlineQueryRootExpression(Values, ElementType); + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + Expression[]? newValues = null; + + for (var i = 0; i < Values.Count; i++) + { + var value = Values[i]; + var newValue = visitor.Visit(value); + + if (newValue != value && newValues is null) + { + newValues = new Expression[Values.Count]; + for (var j = 0; j < i; j++) + { + newValues[j] = Values[j]; + } + } + + if (newValues is not null) + { + newValues[i] = newValue; + } + } + + return newValues is null ? this : new InlineQueryRootExpression(newValues, Type); + } + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append("["); + + for (var i = 0; i < Values.Count; i++) + { + if (i > 0) + { + expressionPrinter.Append(","); + } + + expressionPrinter.Visit(Values[i]); + } + + expressionPrinter.Append("]"); + } +} diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs index a09f19db5d3..82c42039939 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 PrimitiveCollectionReference(root, property); } } @@ -1015,6 +1027,9 @@ private sealed class ReducingExpressionVisitor : ExpressionVisitor case OwnedNavigationReference ownedNavigationReference: return Visit(ownedNavigationReference.Parent).CreateEFPropertyExpression(ownedNavigationReference.Navigation); + case PrimitiveCollectionReference 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..6e2ef8d64c6 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs @@ -501,4 +501,44 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) } } } + + /// + /// Queryable properties are not expanded (similar to . + /// + private sealed class PrimitiveCollectionReference : Expression, IPrintableExpression + { + public PrimitiveCollectionReference(Expression parent, IProperty property) + { + Parent = parent; + Property = property; + } + + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + Parent = visitor.Visit(Parent); + + return this; + } + + public Expression Parent { get; private set; } + public new IProperty Property { 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..923b9da71ce 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -1954,17 +1954,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); @@ -2048,14 +2037,37 @@ 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 PrimitiveCollectionReference primitiveCollectionReference: + { + var currentTree = new NavigationTreeExpression(Expression.Default(primitiveCollectionReference.Type.GetSequenceType())); + + return new NavigationExpansionExpression( + Expression.Call( + QueryableMethods.AsQueryable.MakeGenericMethod(primitiveCollectionReference.Type.GetSequenceType()), + primitiveCollectionReference), + currentTree, + currentTree, + GetParameterName("p")); + } + + default: + return expression; + } } private string GetParameterName(string prefix) diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs index b8ae88c3639..2f5dec852bc 100644 --- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs @@ -252,9 +252,8 @@ private Expression TryConvertEnumerableToQueryable(MethodCallExpression methodCa } 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); } @@ -386,9 +385,8 @@ private Expression TryConvertEnumerableToQueryable(MethodCallExpression methodCa private Expression TryConvertListContainsToQueryableContains(MethodCallExpression methodCallExpression) { - if (ClientSource(methodCallExpression.Object)) + if (methodCallExpression.Object is MemberInitExpression or NewExpression) { - // this is methodCall over closure variable or constant return base.VisitMethodCall(methodCallExpression); } @@ -402,13 +400,6 @@ 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 CanConvertEnumerableToQueryable(Type enumerableType, Type queryableType) { if (enumerableType == typeof(IEnumerable) 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..5e679bd0c9d --- /dev/null +++ b/src/EFCore/Query/QueryRootProcessor.cs @@ -0,0 +1,137 @@ +// 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 Microsoft.EntityFrameworkCore.Internal; + +namespace Microsoft.EntityFrameworkCore.Query; + +/// +/// A visitor which adds additional query root nodes during preprocessing. +/// +public class QueryRootProcessor : ExpressionVisitor +{ + private readonly ITypeMappingSource _typeMappingSource; + private readonly IModel _model; + + /// + /// Creates a new instance of the class with associated query provider. + /// + /// Parameter object containing dependencies for this class. + /// The query compilation context object to use. + public QueryRootProcessor( + QueryTranslationPreprocessorDependencies dependencies, + QueryCompilationContext queryCompilationContext) + { + _typeMappingSource = dependencies.TypeMappingSource; + _model = queryCompilationContext.Model; + } + + /// + protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) + { + // We'll look for IEnumerable/IQueryable arguments to methods on Enumerable/Queryable, and convert these to constant/parameter query + // root nodes. These will later get translated to e.g. VALUES (constant) and OPENJSON (parameter) on SQL Server. + var method = methodCallExpression.Method; + if (method.DeclaringType != typeof(Queryable) + && method.DeclaringType != typeof(Enumerable) + && method.DeclaringType != typeof(QueryableExtensions) + && method.DeclaringType != typeof(EntityFrameworkQueryableExtensions)) + { + return base.VisitMethodCall(methodCallExpression); + } + + 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; + + // This converts collections over constants and parameters to query roots, for later translation of LINQ operators over them. + // The element type doesn't have to be directly mappable; we allow unknown CLR types in order to support value convertors + // (the precise type mapping - with the value converter - will be inferred later based on LINQ operators composed on the root). + // However, we do exclude element CLR types which are associated to entity types in our model, since Contains over entity + // collections isn't yet supported (#30712). + if (parameterType.IsGenericType + && (parameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>) + || parameterType.GetGenericTypeDefinition() == typeof(IQueryable<>)) + && parameterType.GetGenericArguments()[0] is var elementClrType + && !_model.FindEntityTypes(elementClrType).Any()) + { + switch (argument) + { + case ConstantExpression { Value: IEnumerable values } constantExpression + when ShouldConvertToInlineQueryRoot(constantExpression): + + var valueExpressions = new List(); + foreach (var value in values) + { + valueExpressions.Add(Expression.Constant(value, elementClrType)); + } + visitedArgument = new InlineQueryRootExpression(valueExpressions, elementClrType); + break; + + // TODO: Support NewArrayExpression, see #30734. + + case ParameterExpression parameterExpression + when parameterExpression.Name?.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal) + == true + && ShouldConvertToParameterQueryRoot(parameterExpression): + visitedArgument = new ParameterQueryRootExpression(parameterExpression.Type.GetSequenceType(), parameterExpression); + break; + + default: + visitedArgument = null; + break; + } + } + + 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); + } + + /// + /// Determines whether a should be converted to a . + /// This handles cases inline expressions whose elements are all constants. + /// + /// The constant expression that's a candidate for conversion to a query root. + protected virtual bool ShouldConvertToInlineQueryRoot(ConstantExpression constantExpression) + => false; + + /// + /// Determines whether a should be converted to a . + /// + /// The parameter expression that's a candidate for conversion to a query root. + protected virtual bool ShouldConvertToParameterQueryRoot(ParameterExpression parameterExpression) + => false; +} + diff --git a/src/EFCore/Query/QueryTranslationPreprocessor.cs b/src/EFCore/Query/QueryTranslationPreprocessor.cs index 1f2a0cc307b..27d69f862ac 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,13 @@ 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); + => new QueryableMethodNormalizingExpressionVisitor(QueryCompilationContext).Normalize(expression); + + /// + /// Adds additional query root nodes to the query. + /// + /// The query expression to process. + /// A query expression after query roots have been added. + protected virtual Expression ProcessQueryRoots(Expression expression) + => expression; } diff --git a/src/EFCore/Query/QueryTranslationPreprocessorDependencies.cs b/src/EFCore/Query/QueryTranslationPreprocessorDependencies.cs index f308fd98fe6..53abf26c65f 100644 --- a/src/EFCore/Query/QueryTranslationPreprocessorDependencies.cs +++ b/src/EFCore/Query/QueryTranslationPreprocessorDependencies.cs @@ -46,13 +46,20 @@ public sealed record QueryTranslationPreprocessorDependencies /// [EntityFrameworkInternal] public QueryTranslationPreprocessorDependencies( + ITypeMappingSource typeMappingSource, IEvaluatableExpressionFilter evaluatableExpressionFilter, INavigationExpansionExtensibilityHelper navigationExpansionExtensibilityHelper) { + TypeMappingSource = typeMappingSource; EvaluatableExpressionFilter = evaluatableExpressionFilter; NavigationExpansionExtensibilityHelper = navigationExpansionExtensibilityHelper; } + /// + /// Type mapping source. + /// + public ITypeMappingSource TypeMappingSource { get; init; } + /// /// Evaluatable expression filter. /// 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..868cb139aea 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/src/EFCore/Storage/ValueConversion/CollectionToJsonStringConverter.cs b/src/EFCore/Storage/ValueConversion/CollectionToJsonStringConverter.cs new file mode 100644 index 00000000000..0655938fd4d --- /dev/null +++ b/src/EFCore/Storage/ValueConversion/CollectionToJsonStringConverter.cs @@ -0,0 +1,64 @@ +// 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; + +/// +/// A value converter that converts a .NET primitive collection into a JSON string. +/// +// TODO: This currently just calls JsonSerialize.Serialize/Deserialize. It should go through the element type mapping's APIs for +// serializing/deserializing JSON instead, when those APIs are introduced. +// TODO: Nulls? Mapping hints? Customizable JsonSerializerOptions? +public class CollectionToJsonStringConverter : ValueConverter +{ + private readonly CoreTypeMapping _elementTypeMapping; + + /// + /// Creates a new instance of this converter. + /// + /// + /// See EF Core value converters for more information and examples. + /// + public CollectionToJsonStringConverter(Type modelClrType, CoreTypeMapping elementTypeMapping) + : base( + (Expression>)(x => JsonSerializer.Serialize(x, (JsonSerializerOptions?)null)), + (Expression>)(s => JsonSerializer.Deserialize(s, modelClrType, (JsonSerializerOptions?)null)!)) // TODO: Nullability + { + ModelClrType = modelClrType; + _elementTypeMapping = elementTypeMapping; + + // TODO: Value converters on the element type mapping should be supported + // 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 override bool Equals(object? obj) + => ReferenceEquals(this, obj) || (obj is CollectionToJsonStringConverter other && Equals(other)); + + private bool Equals(CollectionToJsonStringConverter other) + => ModelClrType == other.ModelClrType && _elementTypeMapping.Equals(other._elementTypeMapping); + + /// + public override int GetHashCode() + => ModelClrType.GetHashCode(); +} diff --git a/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs b/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs index 3cabc98b4f3..d30276b6423 100644 --- a/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs +++ b/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs @@ -23,20 +23,6 @@ public override void Properties_can_be_made_concurrency_tokens() Assert.Throws( () => base.Properties_can_be_made_concurrency_tokens()).Message); - protected override void Mapping_throws_for_non_ignored_array() - { - var modelBuilder = CreateModelBuilder(); - - modelBuilder.Entity(); - - var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(OneDee)); - - var property = entityType.FindProperty(nameof(OneDee.One)); - Assert.Null(property.GetProviderClrType()); - Assert.NotNull(property.FindTypeMapping()); - } - public override void Properties_can_have_provider_type_set_for_type() { var modelBuilder = CreateModelBuilder(c => c.Properties().HaveConversion()); 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..3ae808f294b --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs @@ -0,0 +1,21 @@ +// 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 +{ + // On relational databases, byte[] gets mapped to a special binary data type, which isn't queryable as a regular primitive collection. + [ConditionalFact] + public override Task Array_of_byte() + => AssertTranslationFailed(() => TestArray((byte)1, (byte)2)); + + 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..c779cb7ed2f 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs @@ -108,29 +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() - { - using var context = CreateContext(); - AssertTranslationFailed( - () => (from e1 in context.Employees - join i in new uint[] { 1, 2, 3 } on e1.EmployeeID equals i - select e1) - .ToList()); - } - [ConditionalFact] public virtual void Does_not_throws_when_group_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..f5fb2951ea9 --- /dev/null +++ b/test/EFCore.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryTestBase.cs @@ -0,0 +1,273 @@ +// 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 NonSharedPrimitiveCollectionsQueryTestBase : NonSharedModelTestBase +{ + #region Support for specific element types + + [ConditionalFact] + public virtual Task Array_of_string() + => TestArray("a", "b"); + + [ConditionalFact] + public virtual Task Array_of_int() + => TestArray(1, 2); + + [ConditionalFact] + public virtual Task Array_of_long() + => TestArray(1L, 2L); + + [ConditionalFact] + public virtual Task Array_of_short() + => TestArray((short)1, (short)2); + + [ConditionalFact] + public virtual Task Array_of_byte() + => TestArray((byte)1, (byte)2); + + [ConditionalFact] + public virtual Task Array_of_double() + => TestArray(1d, 2d); + + [ConditionalFact] + public virtual Task Array_of_float() + => TestArray(1f, 2f); + + [ConditionalFact] + public virtual Task Array_of_decimal() + => TestArray(1m, 2m); + + [ConditionalFact] + public virtual Task Array_of_DateTime() + => TestArray(new DateTime(2023, 1, 1, 12, 30, 0), new DateTime(2023, 1, 2, 12, 30, 0)); + + [ConditionalFact] + public virtual Task Array_of_DateOnly() + => TestArray(new DateOnly(2023, 1, 1), new DateOnly(2023, 1, 2)); + + [ConditionalFact] + public virtual Task Array_of_TimeOnly() + => TestArray(new TimeOnly(12, 30, 0), new TimeOnly(12, 30, 1)); + + [ConditionalFact] + public virtual Task Array_of_DateTimeOffset() + => TestArray( + new DateTimeOffset(2023, 1, 1, 12, 30, 0, TimeSpan.FromHours(2)), + new DateTimeOffset(2023, 1, 2, 12, 30, 0, TimeSpan.FromHours(2))); + + [ConditionalFact] + public virtual Task Array_of_bool() + => TestArray(true, false); + + [ConditionalFact] + public virtual Task Array_of_Guid() + => TestArray( + new Guid("dc8c903d-d655-4144-a0fd-358099d40ae1"), + new Guid("008719a5-1999-4798-9cf3-92a78ffa94a2")); + + [ConditionalFact] + public virtual Task Array_of_byte_array() + => TestArray(new byte[] { 1, 2 }, new byte[] { 3, 4 }); + + [ConditionalFact] + public virtual Task Array_of_enum() + => TestArray(MyEnum.Label1, MyEnum.Label2); + + enum MyEnum { Label1, Label2 } + + // 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 Array_of_geometry_is_not_supported(); + + [ConditionalFact] + public virtual async Task Array_of_array_is_not_supported() + { + var exception = await Assert.ThrowsAsync(() => TestArray(new[] { 1, 2, 3 }, new[] { 4, 5, 6 })); + Assert.Equal(CoreStrings.PropertyNotMapped("int[][]", "TestEntity", "SomeArray"), exception.Message); + } + + [ConditionalFact] + public virtual async Task Multidimensional_array_is_not_supported() + { + var exception = await Assert.ThrowsAsync(() => InitializeAsync( + onModelCreating: mb => mb.Entity().Property(typeof(int[,]), "MultidimensionalArray"))); + Assert.Equal(CoreStrings.PropertyNotMapped("int[,]", "TestEntity", "MultidimensionalArray"), exception.Message); + } + + [ConditionalFact] + public virtual async Task Column_with_custom_converter() + { + var contextFactory = await InitializeAsync( + onModelCreating: mb => mb.Entity() + .Property(m => m.Ints) + .HasConversion( + i => string.Join(",", i), + s => s.Split(",", StringSplitOptions.None).Select(int.Parse).ToArray(), + new ValueComparer(favorStructuralComparisons: true)), + seed: context => + { + context.AddRange( + new TestEntity { Id = 1, Ints = new[] { 1, 2, 3 } }, + new TestEntity { Id = 2, Ints = new[] { 1, 2, 4 } }); + context.SaveChanges(); + }); + + await using var context = contextFactory.CreateContext(); + + var ints = new[] { 1, 2, 3 }; + var result = await context.Set().SingleAsync(m => m.Ints == ints); + Assert.Equal(1, result.Id); + + // Custom converters allow reading/writing, but not querying, as we have no idea about the internal representation + await AssertTranslationFailed(() => context.Set().SingleAsync(m => m.Ints.Length == 2)); + } + + [ConditionalFact(Skip = "Currently fails because we don't use the element mapping when serializing to JSON, but just do JsonSerializer.Serialize, #30677")] + public virtual async Task Parameter_with_inferred_value_converter() + { + var contextFactory = await InitializeAsync( + onModelCreating: mb => mb.Entity( + b => + { + b.Property("PropertyWithValueConverter") + .HasConversion(w => w.Value, i => new IntWrapper(i)); + }), + seed: context => + { + var entry1 = context.Add(new TestEntity { Id = 1 }); + entry1.Property("PropertyWithValueConverter").CurrentValue = new IntWrapper(8); + var entry2 = context.Add(new TestEntity { Id = 2 }); + entry2.Property("PropertyWithValueConverter").CurrentValue = new IntWrapper(9); + context.SaveChanges(); + }); + + await using var context = contextFactory.CreateContext(); + + var ints = new IntWrapper[] { new(1), new(8) }; + var result = await context.Set() + .SingleAsync(m => ints.Count(i => i == EF.Property(m, "PropertyWithValueConverter")) == 1); + Assert.Equal(1, result.Id); + } + + [ConditionalFact] + public virtual async Task Constant_with_inferred_value_converter() + { + var contextFactory = await InitializeAsync( + onModelCreating: mb => mb.Entity( + b => + { + b.Property("PropertyWithValueConverter") + .HasConversion(w => w.Value, i => new IntWrapper(i)); + }), + seed: context => + { + var entry1 = context.Add(new TestEntity { Id = 1 }); + entry1.Property("PropertyWithValueConverter").CurrentValue = new IntWrapper(8); + var entry2 = context.Add(new TestEntity { Id = 2 }); + entry2.Property("PropertyWithValueConverter").CurrentValue = new IntWrapper(9); + context.SaveChanges(); + }); + + await using var context = contextFactory.CreateContext(); + + var result = await context.Set() + .SingleAsync( + m => new IntWrapper[] { new(1), new(8) }.Count(i => i == EF.Property(m, "PropertyWithValueConverter")) == 1); + Assert.Equal(1, result.Id); + } + + class IntWrapper + { + public IntWrapper(int value) + => Value = value; + + public int Value { get; set; } + } + + /// + /// A utility that allows easy testing of querying out arbitrary element types from a primitive collection, provided two distinct + /// element values. + /// + protected async Task TestArray( + TElement value1, + TElement value2, + Action onModelCreating = null) + { + var arrayClrType = typeof(TElement).MakeArrayType(); + + var contextFactory = await InitializeAsync( + onModelCreating: onModelCreating ?? (mb => mb.Entity().Property(arrayClrType, "SomeArray")), + seed: context => + { + var instance1 = new TestEntity { Id = 1 }; + context.Add(instance1); + var array1 = new TElement[2]; + array1.SetValue(value1, 0); + array1.SetValue(value1, 1); + context.Entry(instance1).Property("SomeArray").CurrentValue = array1; + + var instance2 = new TestEntity { Id = 2 }; + context.Add(instance2); + var array2 = new TElement[2]; + array2.SetValue(value1, 0); + array2.SetValue(value2, 1); + context.Entry(instance2).Property("SomeArray").CurrentValue = array2; + + context.SaveChanges(); + }); + + await using var context = contextFactory.CreateContext(); + + var entityParam = Expression.Parameter(typeof(TestEntity), "m"); + var efPropertyCall = Expression.Call( + typeof(EF).GetMethod(nameof(EF.Property), BindingFlags.Public | BindingFlags.Static)!.MakeGenericMethod(arrayClrType), + entityParam, + Expression.Constant("SomeArray")); + + var elementParam = Expression.Parameter(typeof(TElement), "a"); + var predicate = Expression.Lambda>( + Expression.Equal( + Expression.Call( + EnumerableMethods.CountWithPredicate.MakeGenericMethod(typeof(TElement)), + efPropertyCall, + Expression.Lambda( + Expression.Equal(elementParam, Expression.Constant(value1)), + elementParam)), + Expression.Constant(2)), + entityParam); + + var result = await context.Set().SingleAsync(predicate); + Assert.Equal(1, result.Id); + } + + #endregion Support for specific element types + + protected class TestContext : DbContext + { + public TestContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); + } + + protected class TestEntity + { + public int Id { get; set; } + public int[] Ints { get; set; } + } + + protected override string StoreName + => "NonSharedPrimitiveCollectionsTest"; + + protected static async Task AssertTranslationFailed(Func query) + => Assert.Contains( + CoreStrings.TranslationFailed("")[48..], + (await Assert.ThrowsAsync(query)) + .Message); +} diff --git a/test/EFCore.Specification.Tests/Query/NorthwindCompiledQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindCompiledQueryTestBase.cs index a8514731ae7..9b30b0cbf21 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" }))); } } @@ -847,27 +847,18 @@ await asyncSingleResultQueryWithCancellationToken( "CHOPS", "CONSH", default)); } - [ConditionalFact] - public virtual void MakeBinary_does_not_throw_for_unsupported_operator() - { - var query = EF.CompileQuery( - (NorthwindContext context, object[] parameters) - => context.Customers.Where(c => c.CustomerID == (string)parameters[0])); - - using var context = CreateContext(); - - var result = query(context, new[] { "ALFKI" }).ToList(); - - 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/NorthwindJoinQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs index ca678bdefb3..8fa586cf48b 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs @@ -230,30 +230,25 @@ join o in ss.Set().Where(o => o.OrderID < 10250) on true equals true public virtual async Task Join_local_collection_int_closure_is_cached_correctly(bool async) { var ids = new uint[] { 1, 2 }; - // Join with local collection using TVP. Issue #19016. - await AssertTranslationFailed( - () => AssertQueryScalar( - async, - ss => from e in ss.Set() - join id in ids on e.EmployeeID equals id - select e.EmployeeID)); + await AssertQueryScalar( + async, + ss => from e in ss.Set() + join id in ids on e.EmployeeID equals id + select e.EmployeeID); ids = new uint[] { 3 }; - // Join with local collection using TVP. Issue #19016. - await AssertTranslationFailed( - () => AssertQueryScalar( - async, - ss => from e in ss.Set() - join id in ids on e.EmployeeID equals id - select e.EmployeeID)); + await AssertQueryScalar( + async, + ss => from e in ss.Set() + join id in ids on e.EmployeeID equals id + select e.EmployeeID); } - [ConditionalTheory] + [ConditionalTheory(Skip = "#30677")] [MemberData(nameof(IsAsyncData))] public virtual async Task Join_local_string_closure_is_cached_correctly(bool async) { var ids = "12"; - // Join with local collection using TVP. Issue #19016. await AssertTranslationFailed( () => AssertQueryScalar( async, @@ -262,7 +257,6 @@ join id in ids on e.EmployeeID equals id select e.EmployeeID)); ids = "3"; - // Join with local collection using TVP. Issue #19016. await AssertTranslationFailed( () => AssertQueryScalar( async, @@ -271,13 +265,12 @@ join id in ids on e.EmployeeID equals id select e.EmployeeID)); } - [ConditionalTheory] + [ConditionalTheory(Skip = "#30677")] [MemberData(nameof(IsAsyncData))] public virtual async Task Join_local_bytes_closure_is_cached_correctly(bool async) { var ids = new byte[] { 1, 2 }; - // Join with local collection using TVP. Issue #19016. await AssertTranslationFailed( () => AssertQueryScalar( async, @@ -286,7 +279,6 @@ join id in ids on e.EmployeeID equals id select e.EmployeeID)); ids = new byte[] { 3 }; - // Join with local collection using TVP. Issue #19016. await AssertTranslationFailed( () => AssertQueryScalar( 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..0e6c97f9545 --- /dev/null +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -0,0 +1,722 @@ +// 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 Inline_collection_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 Inline_collection_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 Inline_collection_of_nullable_ints_Contains_null(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => new int?[] { null, 999 }.Contains(c.NullableInt)), + entryCount: 2); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Inline_collection_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 Inline_collection_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 Inline_collection_Count_with_two_values(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => new[] { 2, 999 }.Count(i => i > c.Id) == 1), + entryCount: 2); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Inline_collection_Count_with_three_values(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => new[] { 2, 999, 1000 }.Count(i => i > c.Id) == 2), + entryCount: 2); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Inline_collection_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 Inline_collection_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 Inline_collection_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 Inline_collection_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 Inline_collection_Contains_with_all_parameters(bool async) + { + var (i, j) = (2, 999); + + return AssertQuery( + async, + ss => ss.Set().Where(c => new[] { i, j }.Contains(c.Id)), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Inline_collection_Contains_with_parameter_and_column_based_expression(bool async) + { + var i = 2; + + await AssertTranslationFailed( + () => AssertQuery( + async, + ss => ss.Set().Where(c => new[] { i, c.Int }.Contains(c.Id)), + entryCount: 1)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Parameter_collection_Count(bool async) + { + var ids = new[] { 2, 999 }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => ids.Count(i => i > c.Id) == 1), + entryCount: 2); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Parameter_collection_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_collection_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_collection_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: 2); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Parameter_collection_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_collection_of_DateTimes_Contains(bool async) + { + var dateTimes = new[] + { + new DateTime(2020, 1, 10, 12, 30, 0, DateTimeKind.Utc), + new DateTime(9999, 1, 1, 0, 0, 0, DateTimeKind.Utc) + }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => dateTimes.Contains(c.DateTime)), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Parameter_collection_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_collection_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: 2); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_collection_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_collection_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_collection_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_collection_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_collection_Count_method(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_collection_Length(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.Ints.Length == 2), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_collection_index_int(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.Ints[1] == 10), + ss => ss.Set().Where(c => (c.Ints.Length >= 2 ? c.Ints[1] : -1) == 10), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_collection_index_string(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.Strings[1] == "10"), + ss => ss.Set().Where(c => (c.Strings.Length >= 2 ? c.Strings[1] : "-1") == "10"), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_collection_index_datetime(bool async) + => AssertQuery( + async, + ss => ss.Set().Where( + c => c.DateTimes[1] == new DateTime(2020, 1, 10, 12, 30, 0, DateTimeKind.Utc)), + ss => ss.Set().Where( + c => (c.DateTimes.Length >= 2 ? c.DateTimes[1] : default) == new DateTime(2020, 1, 10, 12, 30, 0, DateTimeKind.Utc)), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_collection_index_beyond_end(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.Ints[999] == 10), + ss => ss.Set().Where(c => false), + entryCount: 0); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Inline_collection_index_Column(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => new[] { 1, 2, 3 }[c.Int] == 1), + ss => ss.Set().Where(c => (c.Int <= 2 ? new[] { 1, 2, 3 }[c.Int] : -1) == 1), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Parameter_collection_index_Column(bool async) + { + var ints = new[] { 1, 2, 3 }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => ints[c.Int] == 1), + ss => ss.Set().Where(c => (c.Int <= 2 ? ints[c.Int] : -1) == 1), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_collection_ElementAt(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.Ints.ElementAt(1) == 10), + ss => ss.Set().Where(c => (c.Ints.Length >= 2 ? c.Ints.ElementAt(1) : -1) == 10), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_collection_Skip(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.Ints.Skip(1).Count() == 2), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_collection_Take(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.Ints.Take(2).Contains(11)), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_collection_Skip_Take(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.Ints.Skip(1).Take(2).Contains(11)), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_collection_Any(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.Ints.Any()), + entryCount: 2); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_collection_projection_from_top_level(bool async) + => AssertQuery( + async, + ss => ss.Set().OrderBy(c => c.Id).Select(c => c.Ints), + elementAsserter: (a, b) => Assert.Equivalent(a, b), + assertOrder: true); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_collection_and_Parameter_collection_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_collection_Concat_column_collection(bool async) + { + var ints = new[] { 11, 111 }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => ints.Concat(c.Ints).Count() == 2), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_collection_Union_parameter_collection(bool async) + { + var ints = new[] { 11, 111 }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Ints.Union(ints).Count() == 2), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_collection_Intersect_inline_collection(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 Inline_collection_Except_column_collection(bool async) + // Note that since the VALUES is on the left side of the set operation, it must assign column names, otherwise the column coming + // out of the set operation has undetermined naming. + => AssertQuery( + async, + ss => ss.Set().Where( + c => new[] { 11, 111 }.Except(c.Ints).Count(i => i % 2 == 1) == 2), + entryCount: 2); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_collection_equality_parameter_collection(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 async Task Column_collection_Concat_parameter_collection_equality_inline_collection_not_supported(bool async) + { + var ints = new[] { 1, 10 }; + + await AssertTranslationFailed( + () => AssertQuery( + async, + ss => ss.Set().Where(c => c.Ints.Concat(ints) == new[] { 1, 11, 111, 1, 10 }))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_collection_equality_inline_collection(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.Ints == new[] { 1, 10 }), + ss => ss.Set().Where(c => c.Ints.SequenceEqual(new[] { 1, 10 })), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Parameter_collection_in_subquery_Count_as_compiled_query(bool async) + { + // The Skip causes a pushdown into a subquery before the Union, and so the projection on the left side of the union points to the + // subquery as its table, and not directly to the parameter's table. + // This creates an initially untyped ColumnExpression referencing the pushed-down subquery; it must also be inferred. + // Note that this must be a compiled query, since with normal queries the Skip(1) gets client-evaluated. + // TODO: + var compiledQuery = EF.CompileQuery( + (PrimitiveCollectionsContext context, int[] ints) + => context.Set().Where(p => ints.Skip(1).Count(i => i > p.Id) == 1).Count()); + + await using var context = Fixture.CreateContext(); + var ints = new[] { 10, 111 }; + + // TODO: Complete + var results = compiledQuery(context, ints); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(bool async) + { + // The Skip causes a pushdown into a subquery before the Union, and so the projection on the left side of the union points to the + // subquery as its table, and not directly to the parameter's table. + // This creates an initially untyped ColumnExpression referencing the pushed-down subquery; it must also be inferred. + // Note that this must be a compiled query, since with normal queries the Skip(1) gets client-evaluated. + var compiledQuery = EF.CompileQuery( + (PrimitiveCollectionsContext context, int[] ints) + => context.Set().Where(p => ints.Skip(1).Union(p.Ints).Count() == 3)); + + await using var context = Fixture.CreateContext(); + var ints = new[] { 10, 111 }; + + // TODO: Complete + var results = compiledQuery(context, ints).ToList(); + } + + [ConditionalFact] + public virtual void Parameter_collection_in_subquery_and_Convert_as_compiled_query() + { + // The array indexing is translated as a subquery over e.g. OpenJson with LIMIT/OFFSET. + // Since there's a CAST over that, the type mapping inference from the other side (p.String) doesn't propagate inside to the + // subquery. In this case, the CAST operand gets the default CLR type mapping, but that's object in this case. + // We should apply the default type mapping to the parameter, but need to figure out the exact rules when to do this. + var query = EF.CompileQuery( + (PrimitiveCollectionsContext context, object[] parameters) + => context.Set().Where(p => p.String == (string)parameters[0])); + + using var context = Fixture.CreateContext(); + + var exception = Assert.Throws(() => query(context, new[] { "foo" }).ToList()); + + Assert.Contains("in the SQL tree does not have a type mapping assigned", exception.Message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_collection_in_subquery_Union_parameter_collection(bool async) + { + var ints = new[] { 10, 111 }; + + // The Skip causes a pushdown into a subquery before the Union. This creates an initially untyped ColumnExpression referencing the + // pushed-down subquery; it must also be inferred + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Ints.Skip(1).Union(ints).Count() == 3), + entryCount: 1); + } + + public abstract class PrimitiveCollectionsQueryFixtureBase : SharedStoreFixtureBase, IQueryFixtureBase + { + private PrimitiveArrayData _expectedData; + + protected override string StoreName + => "PrimitiveCollectionsTest"; + + public Func GetContextCreator() + => () => CreateContext(); + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder).ConfigureWarnings(c => c.Log(CoreEventId.DistinctAfterOrderByWithoutRowLimitingOperatorWarning)); + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + => modelBuilder.Entity().Property(p => p.Id).ValueGeneratedNever(); + + 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); + // TODO: Complete + } + } + } + }.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; } + } + + 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, DateTimeKind.Utc), + 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, DateTimeKind.Utc), + new(2020, 1, 10, 12, 30, 0, DateTimeKind.Utc) + }, + Bools = new[] { true, false }, + Enums = new[] { MyEnum.Value1, MyEnum.Value2 }, + NullableInts = new int?[] { 1, 10 }, + }, + new() + { + Id = 2, + + Int = 11, + String = "11", + DateTime = new DateTime(2020, 1, 11, 12, 30, 0, DateTimeKind.Utc), + 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, DateTimeKind.Utc), + new(2020, 1, 11, 12, 30, 0, DateTimeKind.Utc), + new(2020, 1, 31, 12, 30, 0, DateTimeKind.Utc) + }, + Bools = new[] { false }, + Enums = new[] { MyEnum.Value2, MyEnum.Value3 }, + NullableInts = new int?[] { 1, 11, null }, + }, + new() + { + Id = 3, + + Int = 0, + String = "", + DateTime = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc), + Bool = false, + Enum = MyEnum.Value1, + NullableInt = null, + + Ints = Array.Empty(), + Strings = Array.Empty(), + DateTimes = Array.Empty(), + Bools = Array.Empty(), + Enums = Array.Empty(), + NullableInts = Array.Empty(), + } + }; + } +} diff --git a/test/EFCore.Specification.Tests/Query/QueryTestBase.cs b/test/EFCore.Specification.Tests/Query/QueryTestBase.cs index 45fc352f56a..822cacbd7e8 100644 --- a/test/EFCore.Specification.Tests/Query/QueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/QueryTestBase.cs @@ -1165,6 +1165,12 @@ protected static async Task AssertTranslationFailed(Func query) (await Assert.ThrowsAsync(query)) .Message); + protected static void AssertTranslationFailed(Action query) + => Assert.Contains( + CoreStrings.TranslationFailed("")[48..], + Assert.Throws(query) + .Message); + protected static async Task AssertTranslationFailedWithDetails(Func query, string details) => Assert.Contains( CoreStrings.TranslationFailedWithDetails("", details)[21..], diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs index e9c32a9894f..ea1a611bc6e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs @@ -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) AS [v] + WHERE [v].[value] = [l].[Name] OR ([v].[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) AS [v] + WHERE [v].[value] = [l].[Name] OR ([v].[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) AS [v0] + WHERE [v0].[value] = [l0].[Name] OR ([v0].[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..fd5290ec280 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs @@ -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) AS [v] + WHERE [v].[value] = [l].[Name] OR ([v].[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) AS [v] + WHERE [v].[value] = [l].[Name] OR ([v].[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) AS [v0] + WHERE [v0].[value] = [l0].[Name] OR ([v0].[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..8cb7b155829 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySqlServerTest.cs @@ -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) AS [v] + WHERE [v].[value] = [l].[Name] OR ([v].[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) AS [v] + WHERE [v].[value] = [l].[Name] OR ([v].[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) AS [v] + WHERE [v].[value] = [l].[Name] OR ([v].[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) AS [v] + WHERE [v].[value] = [l].[Name] OR ([v].[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) AS [v0] + WHERE [v0].[value] = [l0].[Name] OR ([v0].[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..02910614534 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) AS [n] + WHERE ([l0].[Name] = [n].[value] AND [l0].[Name] IS NOT NULL AND [n].[value] IS NOT NULL) OR ([l0].[Name] IS NULL AND [n].[value] IS NULL)) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs index 1c2cbc0bf24..35913c190f3 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) AS [n] + WHERE ([t].[Level2_Name] = [n].[value] AND [t].[Level2_Name] IS NOT NULL AND [n].[value] IS NOT NULL) OR ([t].[Level2_Name] IS NULL AND [n].[value] IS NULL)) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index ce029b62c64..3dbb7320e7f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -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) AS [t0] + WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[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) AS [t0] + WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[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) AS [t0] + WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[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) AS [c0] + WHERE CAST([c0].[value] AS varchar(100)) = [c].[Location] OR ([c0].[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) AS [i] + WHERE CAST([i].[value] AS uniqueidentifier) = [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) AS [c0] + WHERE CAST([c0].[value] AS nvarchar(450)) = [c].[Name] OR ([c0].[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) AS [n] + WHERE CAST([n].[value] AS nvarchar(450)) = [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) AS [d] + WHERE CAST([d].[value] AS datetimeoffset) = [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) AS [i] + WHERE CAST([i].[value] AS int) = [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) AS [t] + WHERE CAST([t].[value] AS int) = [w0].[AmmunitionType] OR ([t].[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) AS [v] + WHERE CAST([v].[value] AS bit) = [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) AS [v] + WHERE CAST([v].[value] AS bit) = [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..553b151f61a --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs @@ -0,0 +1,309 @@ +// 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 +{ + #region Support for specific element types + + public override async Task Array_of_int() + { + await base.Array_of_int(); + + AssertSql( +""" +SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray] +FROM [TestEntity] AS [t] +WHERE ( + SELECT COUNT(*) + FROM OpenJson([t].[SomeArray]) AS [s] + WHERE CAST([s].[value] AS int) = 1) = 2 +"""); + } + + public override async Task Array_of_long() + { + await base.Array_of_long(); + + AssertSql( +""" +SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray] +FROM [TestEntity] AS [t] +WHERE ( + SELECT COUNT(*) + FROM OpenJson([t].[SomeArray]) AS [s] + WHERE CAST([s].[value] AS bigint) = CAST(1 AS bigint)) = 2 +"""); + } + + public override async Task Array_of_short() + { + await base.Array_of_short(); + + AssertSql( +""" +SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray] +FROM [TestEntity] AS [t] +WHERE ( + SELECT COUNT(*) + FROM OpenJson([t].[SomeArray]) AS [s] + WHERE CAST([s].[value] AS smallint) = CAST(1 AS smallint)) = 2 +"""); + } + + public override async Task Array_of_double() + { + await base.Array_of_double(); + + AssertSql( +""" +SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray] +FROM [TestEntity] AS [t] +WHERE ( + SELECT COUNT(*) + FROM OpenJson([t].[SomeArray]) AS [s] + WHERE CAST([s].[value] AS float) = 1.0E0) = 2 +"""); + } + + public override async Task Array_of_float() + { + await base.Array_of_float(); + + AssertSql( +""" +SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray] +FROM [TestEntity] AS [t] +WHERE ( + SELECT COUNT(*) + FROM OpenJson([t].[SomeArray]) AS [s] + WHERE CAST([s].[value] AS real) = CAST(1 AS real)) = 2 +"""); + } + + public override async Task Array_of_decimal() + { + await base.Array_of_decimal(); + + AssertSql( +""" +SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray] +FROM [TestEntity] AS [t] +WHERE ( + SELECT COUNT(*) + FROM OpenJson([t].[SomeArray]) AS [s] + WHERE CAST([s].[value] AS decimal(18,2)) = 1.0) = 2 +"""); + } + + public override async Task Array_of_DateTime() + { + await base.Array_of_DateTime(); + + AssertSql( +""" +SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray] +FROM [TestEntity] AS [t] +WHERE ( + SELECT COUNT(*) + FROM OpenJson([t].[SomeArray]) AS [s] + WHERE CAST([s].[value] AS datetime2) = '2023-01-01T12:30:00.0000000') = 2 +"""); + } + + public override async Task Array_of_DateOnly() + { + await base.Array_of_DateOnly(); + + AssertSql( +""" +SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray] +FROM [TestEntity] AS [t] +WHERE ( + SELECT COUNT(*) + FROM OpenJson([t].[SomeArray]) AS [s] + WHERE CAST([s].[value] AS date) = '2023-01-01') = 2 +"""); + } + + public override async Task Array_of_TimeOnly() + { + await base.Array_of_TimeOnly(); + + AssertSql( +""" +SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray] +FROM [TestEntity] AS [t] +WHERE ( + SELECT COUNT(*) + FROM OpenJson([t].[SomeArray]) AS [s] + WHERE CAST([s].[value] AS time) = '12:30:00') = 2 +"""); + } + + public override async Task Array_of_DateTimeOffset() + { + await base.Array_of_DateTimeOffset(); + + AssertSql( +""" +SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray] +FROM [TestEntity] AS [t] +WHERE ( + SELECT COUNT(*) + FROM OpenJson([t].[SomeArray]) AS [s] + WHERE CAST([s].[value] AS datetimeoffset) = '2023-01-01T12:30:00.0000000+02:00') = 2 +"""); + } + + public override async Task Array_of_bool() + { + await base.Array_of_bool(); + + AssertSql( +""" +SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray] +FROM [TestEntity] AS [t] +WHERE ( + SELECT COUNT(*) + FROM OpenJson([t].[SomeArray]) AS [s] + WHERE CAST([s].[value] AS bit) = CAST(1 AS bit)) = 2 +"""); + } + + public override async Task Array_of_Guid() + { + await base.Array_of_Guid(); + + AssertSql( +""" +SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray] +FROM [TestEntity] AS [t] +WHERE ( + SELECT COUNT(*) + FROM OpenJson([t].[SomeArray]) AS [s] + WHERE CAST([s].[value] AS uniqueidentifier) = 'dc8c903d-d655-4144-a0fd-358099d40ae1') = 2 +"""); + } + + // The JSON representation for new[] { 1, 2 } is AQI= (base64), this cannot simply be cast to varbinary(max) (0x0102). See #30727. + public override Task Array_of_byte_array() + => AssertTranslationFailed(() => base.Array_of_byte_array()); + + public override async Task Array_of_enum() + { + await base.Array_of_enum(); + + AssertSql( +""" +SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray] +FROM [TestEntity] AS [t] +WHERE ( + SELECT COUNT(*) + FROM OpenJson([t].[SomeArray]) AS [s] + WHERE CAST([s].[value] AS int) = 0) = 2 +"""); + } + + [ConditionalFact] // #30630 + public override async Task Array_of_geometry_is_not_supported() + { + 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[]", "TestEntity", "Points"), exception.Message); + } + + #endregion Support for specific element types + + #region Type mapping inference + + public override async Task Constant_with_inferred_value_converter() + { + await base.Constant_with_inferred_value_converter(); + + AssertSql( +""" +SELECT TOP(2) [t].[Id], [t].[Ints], [t].[PropertyWithValueConverter] +FROM [TestEntity] AS [t] +WHERE ( + SELECT COUNT(*) + FROM (VALUES (CAST(1 AS int)), (8)) AS [v]([Value]) + WHERE [v].[Value] = [t].[PropertyWithValueConverter]) = 1 +"""); + } + + [ConditionalFact] + public virtual async Task Same_parameter_with_different_type_mappings() + { + var contextFactory = await InitializeAsync( + onModelCreating: mb => mb.Entity( + b => + { + b.Property(typeof(DateTime), "DateTime").HasColumnType("datetime"); + b.Property(typeof(DateTime), "DateTime2").HasColumnType("datetime2"); + })); + + await using var context = contextFactory.CreateContext(); + + var dateTimes = new[] { new DateTime(2020, 1, 1, 12, 30, 00), new DateTime(2020, 1, 2, 12, 30, 00) }; + + _ = await context.Set() + .Where( + m => + dateTimes.Contains(EF.Property(m, "DateTime")) + && dateTimes.Contains(EF.Property(m, "DateTime2"))) + .ToArrayAsync(); + + AssertSql( +""" +@__dateTimes_0='["2020-01-01T12:30:00","2020-01-02T12:30:00"]' (Size = 4000) +@__dateTimes_0_1='["2020-01-01T12:30:00","2020-01-02T12:30:00"]' (Size = 4000) + +SELECT [t].[Id], [t].[DateTime], [t].[DateTime2], [t].[Ints] +FROM [TestEntity] AS [t] +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0) AS [d] + WHERE CAST([d].[value] AS datetime) = [t].[DateTime]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0_1) AS [d0] + WHERE CAST([d0].[value] AS datetime2) = [t].[DateTime2]) +"""); + } + + [ConditionalFact] + public virtual async Task Same_collection_with_conflicting_type_mappings_not_supported() + { + var contextFactory = await InitializeAsync( + onModelCreating: mb => mb.Entity( + b => + { + b.Property(typeof(DateTime), "DateTime").HasColumnType("datetime"); + b.Property(typeof(DateTime), "DateTime2").HasColumnType("datetime2"); + })); + + await using var context = contextFactory.CreateContext(); + + var dateTimes = new[] { new DateTime(2020, 1, 1, 12, 30, 00), new DateTime(2020, 1, 2, 12, 30, 00) }; + + var exception = await Assert.ThrowsAsync( + () => context.Set() + .Where( + m => dateTimes + .Any(d => d == EF.Property(m, "DateTime") && d == EF.Property(m, "DateTime2"))) + .ToArrayAsync()); + Assert.Equal(RelationalStrings.ConflictingTypeMappingsForPrimitiveCollection("datetime2", "datetime"), exception.Message); + } + + #endregion Type mapping inference + + 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..6265199c103 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) AS [i] + WHERE CAST([i].[value] AS nchar(5)) = [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) AS [i] + WHERE CAST([i].[value] AS nchar(5)) = [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) AS [i] + WHERE CAST([i].[value] AS nvarchar(15)) = [c0].[City] OR ([i].[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) AS [i] + WHERE CAST([i].[value] AS nvarchar(15)) = [c0].[City] OR ([i].[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) AS [i] + WHERE CAST([i].[value] AS int) = [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) AS [i] + WHERE CAST([i].[value] AS int) = [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) AS [i] + WHERE CAST([i].[value] AS int) = [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) AS [i] + WHERE CAST([i].[value] AS int) = [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) AS [i] + WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID]) """); } @@ -1652,9 +1697,14 @@ public override async Task Contains_with_local_object_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) AS [i] + WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID]) """); } @@ -1664,9 +1714,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) AS [i] + WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID]) """); } @@ -1688,15 +1743,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) AS [p] + WHERE CAST([p].[value] AS nchar(5)) = [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) AS [p] + WHERE CAST([p].[value] AS nchar(5)) = [c].[CustomerID]) """); } @@ -1706,15 +1771,25 @@ public override async Task Contains_with_local_non_primitive_list_inline_closure AssertSql( """ +@__Select_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(@__Select_0) AS [s] + WHERE CAST([s].[value] AS nchar(5)) = [c].[CustomerID]) """, // """ +@__Select_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(@__Select_0) AS [s] + WHERE CAST([s].[value] AS nchar(5)) = [c].[CustomerID]) """); } @@ -1724,9 +1799,14 @@ public override async Task Contains_with_local_non_primitive_list_closure_mix(bo AssertSql( """ +@__Select_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(@__Select_0) AS [s] + WHERE CAST([s].[value] AS nchar(5)) = [c].[CustomerID]) """); } @@ -1736,9 +1816,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) AS [i] + WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID])) """); } @@ -1748,9 +1833,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) AS [i] + WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID]) """); } @@ -1784,9 +1874,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) AS [i] + WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID]) OR [c].[CustomerID] IN (N'ALFKI', N'ABCDE') """); } @@ -1796,9 +1891,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) AS [i] + WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID]) """); } @@ -2249,9 +2349,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) AS [i] + WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID]) """); } @@ -2273,9 +2378,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) AS [i] + WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID]) """); } @@ -2285,15 +2395,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) AS [i] + WHERE CAST([i].[value] AS nchar(5)) = [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) AS [i] + WHERE [c].[CustomerID] = CAST([i].[value] AS nchar(5))) """); } @@ -2303,9 +2423,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) AS [i] + WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID] AND [i].[value] IS NOT NULL) """); } @@ -2327,9 +2452,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) AS [i] + WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID]) """); } @@ -2339,15 +2469,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) AS [i] + WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID] AND [i].[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) AS [i] + WHERE [c].[CustomerID] = CAST([i].[value] AS nchar(5)) AND [i].[value] IS NOT NULL) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindCompiledQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindCompiledQuerySqlServerTest.cs index b6d6df1c188..ed8e8e79695 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindCompiledQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindCompiledQuerySqlServerTest.cs @@ -13,7 +13,7 @@ public NorthwindCompiledQuerySqlServerTest( : base(fixture) { fixture.TestSqlLoggerFactory.Clear(); - //fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } [ConditionalFact] @@ -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) AS [a] + WHERE CAST([a].[value] AS nchar(5)) = [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) AS [a] + WHERE CAST([a].[value] AS nchar(5)) = [c].[CustomerID]) """); } @@ -385,66 +395,64 @@ FROM [Customers] AS [c] """); } - public override void MakeBinary_does_not_throw_for_unsupported_operator() - => Assert.Equal( - CoreStrings.TranslationFailedWithDetails( - "DbSet() .Where(c => c.CustomerID == (string)__parameters .ElementAt(0))", - CoreStrings.QueryUnableToTranslateMethod("System.Linq.Enumerable", nameof(Enumerable.ElementAt))), - Assert.Throws( - () => base.MakeBinary_does_not_throw_for_unsupported_operator()).Message.Replace("\r", "").Replace("\n", "")); - 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 CAST([a].[value] AS nchar(5)) AS [value] + FROM OpenJson(@__args) AS [a] + ORDER BY CAST([a].[key] AS int) + 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 CAST([a].[value] AS nchar(5)) AS [value] + FROM OpenJson(@__args) AS [a] + ORDER BY CAST([a].[key] AS int) + 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 CAST([a].[value] AS nchar(5)) AS [value] + FROM OpenJson(@__args) AS [a] + ORDER BY CAST([a].[key] AS int) + 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 CAST([a].[value] AS nchar(5)) AS [value] + FROM OpenJson(@__args) AS [a] + ORDER BY CAST([a].[key] AS int) + 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..520ad2ce690 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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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..193b4a52678 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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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..333392d6b4f 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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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/NorthwindJoinQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs index e333574f79e..653dae72de0 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs @@ -901,7 +901,22 @@ public override async Task Join_local_collection_int_closure_is_cached_correctly { await base.Join_local_collection_int_closure_is_cached_correctly(async); - AssertSql(); + AssertSql( +""" +@__p_0='[1,2]' (Size = 4000) + +SELECT [e].[EmployeeID] +FROM [Employees] AS [e] +INNER JOIN OpenJson(@__p_0) AS [p] ON [e].[EmployeeID] = [p].[value] +""", + // +""" +@__p_0='[3]' (Size = 4000) + +SELECT [e].[EmployeeID] +FROM [Employees] AS [e] +INNER JOIN OpenJson(@__p_0) AS [p] ON [e].[EmployeeID] = [p].[value] +"""); } public override async Task Join_local_string_closure_is_cached_correctly(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index 1d68ed3f0a1..22ddc3fac29 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -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) AS [d] + WHERE CAST([d].[value] AS datetime) = 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) AS [d] + WHERE CAST([d].[value] AS datetime) = 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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [o0] + WHERE CAST([o0].[value] AS int) = [o].[OrderID]) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindNavigationsQuerySqlServerTest.cs index 9caea5b48a4..32b023f2cf5 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) AS [o0] + WHERE CAST([o0].[value] AS int) = [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..9502a1c4211 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs @@ -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) AS [f] + WHERE CAST([f].[value] AS int) = [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) AS [f] + WHERE CAST([f].[value] AS int) = [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) AS [f] + WHERE CAST([f].[value] AS int) = [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) AS [f] + WHERE CAST([f].[value] AS int) = [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) AS [f] + WHERE CAST([f].[value] AS int) = [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..5c79cfbef7f 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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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..e68ab3cb97d 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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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..9091ace0ea5 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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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) AS [l] + WHERE CAST([l].[value] AS nchar(5)) = [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..91b291451db 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) AS [c0] + WHERE CAST([c0].[value] AS nvarchar(15)) = [c].[City] OR ([c0].[value] IS NULL AND [c].[City] IS NULL)) """); } @@ -2441,9 +2446,14 @@ public override async Task Where_list_object_contains_over_value_type(bool async AssertSql( """ +@__orderIds_0='[10248,10249]' (Size = 4000) + SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM [Orders] AS [o] -WHERE [o].[OrderID] IN (10248, 10249) +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__orderIds_0) AS [o0] + WHERE CAST([o0].[value] AS int) = [o].[OrderID]) """); } @@ -2453,9 +2463,14 @@ public override async Task Where_array_of_object_contains_over_value_type(bool a AssertSql( """ +@__orderIds_0='[10248,10249]' (Size = 4000) + SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM [Orders] AS [o] -WHERE [o].[OrderID] IN (10248, 10249) +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__orderIds_0) AS [o0] + WHERE CAST([o0].[value] AS int) = [o].[OrderID]) """); } @@ -2555,9 +2570,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) AS [p] + WHERE CAST([p].[value] AS nchar(5)) = [c].[CustomerID]) OR [c].[CustomerID] = N'ANTON' """); } @@ -2580,9 +2600,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) AS [a] + WHERE CAST([a].[value] AS nchar(5)) = [c].[CustomerID]) OR [c].[CustomerID] = N'ANTON' """); } @@ -2593,11 +2618,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) AS [a] + WHERE CAST([a].[value] AS nchar(5)) = [c].[CustomerID]) OR [c].[CustomerID] = @__prm2_2 """); } @@ -2899,9 +2928,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) AS [c0] + WHERE CAST([c0].[value] AS nchar(5)) = [c].[CustomerID]) AND [c].[City] = N'Seattle' """); } @@ -2911,9 +2945,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) AS [c0] + WHERE CAST([c0].[value] AS nchar(5)) = [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..79f3b958c09 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs @@ -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) AS [i] + WHERE [i].[value] = [e].[NullableStringA] OR ([i].[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) AS [i] + WHERE [i].[value] = [e].[NullableStringA] OR ([i].[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) AS [i] + WHERE [i].[value] = [e].[NullableStringA] OR ([i].[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) AS [i] + WHERE [i].[value] = [e].[NullableStringA] OR ([i].[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) AS [l] + WHERE [l].[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) AS [n] + WHERE [n].[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) AS [n] + WHERE [n].[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) AS [n] + WHERE [n].[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) AS [i] + WHERE CAST([i].[value] AS int) = [e].[NullableIntA] OR ([i].[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) AS [i] + WHERE CAST([i].[value] AS int) = [e].[NullableIntA] OR ([i].[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) AS [i] + WHERE CAST([i].[value] AS int) = [e].[NullableIntA] OR ([i].[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) AS [i] + WHERE CAST([i].[value] AS int) = [e].[NullableIntA] OR ([i].[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) AS [i] + WHERE CAST([i].[value] AS int) = [e].[NullableIntA] OR ([i].[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) AS [i] + WHERE CAST([i].[value] AS int) = [e].[NullableIntA] OR ([i].[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) AS [i] + WHERE CAST([i].[value] AS int) = [e].[NullableIntA] OR ([i].[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) AS [i] + WHERE CAST([i].[value] AS int) = [e].[NullableIntA] OR ([i].[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) AS [i] + WHERE CAST([i].[value] AS int) = [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) AS [i] + WHERE CAST([i].[value] AS int) = [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) AS [i] + WHERE CAST([i].[value] AS int) = [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) AS [i] + WHERE CAST([i].[value] AS int) = [e].[IntA])) """, // """ +@__ids3_0='[]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE 0 = 1 +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__ids3_0) AS [i] + WHERE CAST([i].[value] AS int) = [e].[IntA]) """, // """ +@__ids3_0='[]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] +WHERE NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__ids3_0) AS [i] + WHERE CAST([i].[value] AS int) = [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) AS [i] + WHERE CAST([i].[value] AS int) = [e].[IntA]) """, // """ +@__ids4_0='[null]' (Size = 4000) + SELECT [e].[Id] FROM [Entities1] AS [e] +WHERE NOT (EXISTS ( + SELECT 1 + FROM OpenJson(@__ids4_0) AS [i] + WHERE CAST([i].[value] AS int) = [e].[IntA])) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs new file mode 100644 index 00000000000..83cdd53e868 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -0,0 +1,428 @@ +// 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; + +/// +/// Runs all primitive collection tests with SQL Server compatibility level 120 (SQL Server 2014), which doesn't support OPENJSON. +/// This exercises the older translation paths for e.g. Contains, to make sure things work for providers with no queryable constant/ +/// parameter support. +/// +public class PrimitiveCollectionsQueryOldSqlServerTest : PrimitiveCollectionsQueryTestBase< + PrimitiveCollectionsQueryOldSqlServerTest.PrimitiveCollectionsQueryOldSqlServerFixture> +{ + public PrimitiveCollectionsQueryOldSqlServerTest(PrimitiveCollectionsQueryOldSqlServerFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Inline_collection_of_ints_Contains(bool async) + { + await base.Inline_collection_of_ints_Contains(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Inline_collection_of_nullable_ints_Contains(bool async) + { + await base.Inline_collection_of_nullable_ints_Contains(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Inline_collection_of_nullable_ints_Contains_null(bool async) + { + await base.Inline_collection_of_nullable_ints_Contains_null(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Inline_collection_Count_with_zero_values(bool async) + => AssertTranslationFailedWithDetails( + () => base.Inline_collection_Count_with_zero_values(async), + RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot); + + public override async Task Inline_collection_Count_with_one_value(bool async) + { + await base.Inline_collection_Count_with_one_value(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Inline_collection_Count_with_two_values(bool async) + { + await base.Inline_collection_Count_with_two_values(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Inline_collection_Count_with_three_values(bool async) + { + await base.Inline_collection_Count_with_three_values(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Inline_collection_Contains_with_zero_values(bool async) + => AssertTranslationFailedWithDetails( + () => base.Inline_collection_Contains_with_zero_values(async), + RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot); + + public override async Task Inline_collection_Contains_with_one_value(bool async) + { + await base.Inline_collection_Contains_with_one_value(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Inline_collection_Contains_with_two_values(bool async) + { + await base.Inline_collection_Contains_with_two_values(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Inline_collection_Contains_with_three_values(bool async) + { + await base.Inline_collection_Contains_with_three_values(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Inline_collection_Contains_with_all_parameters(bool async) + { + await base.Inline_collection_Contains_with_all_parameters(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Inline_collection_Contains_with_parameter_and_column_based_expression(bool async) + { + await base.Inline_collection_Contains_with_parameter_and_column_based_expression(async); + + AssertSql(); + } + + public override Task Parameter_collection_Count(bool async) + => AssertTranslationFailed(() => base.Parameter_collection_Count(async)); + + public override async Task Parameter_collection_of_ints_Contains(bool async) + { + await base.Parameter_collection_of_nullable_ints_Contains(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Parameter_collection_of_nullable_ints_Contains(bool async) + { + await base.Parameter_collection_of_nullable_ints_Contains(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Parameter_collection_of_nullable_ints_Contains_null(bool async) + { + await base.Parameter_collection_of_nullable_ints_Contains_null(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 async Task Parameter_collection_of_strings_Contains(bool async) + { + await base.Parameter_collection_of_strings_Contains(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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].[String] IN (N'10', N'999') +"""); + } + + public override async Task Parameter_collection_of_DateTimes_Contains(bool async) + { + await base.Parameter_collection_of_DateTimes_Contains(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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].[DateTime] IN ('2020-01-10T12:30:00.0000000Z', '9999-01-01T00:00:00.0000000Z') +"""); + } + + public override async Task Parameter_collection_of_bools_Contains(bool async) + { + await base.Parameter_collection_of_bools_Contains(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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].[Bool] = CAST(1 AS bit) +"""); + } + + public override async Task Parameter_collection_of_enums_Contains(bool async) + { + await base.Parameter_collection_of_enums_Contains(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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].[Enum] IN (0, 3) +"""); + } + + public override Task Column_collection_of_ints_Contains(bool async) + => AssertTranslationFailed(() => base.Column_collection_of_ints_Contains(async)); + + public override Task Column_collection_of_nullable_ints_Contains(bool async) + => AssertTranslationFailed(() => base.Column_collection_of_nullable_ints_Contains(async)); + + public override Task Column_collection_of_nullable_ints_Contains_null(bool async) + => AssertTranslationFailed(() => base.Column_collection_of_nullable_ints_Contains_null(async)); + + public override Task Column_collection_of_bools_Contains(bool async) + => AssertTranslationFailed(() => base.Column_collection_of_bools_Contains(async)); + + [ConditionalFact] + public virtual async Task Json_representation_of_bool_array() + { + 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 Task Column_collection_Count_method(bool async) + => AssertTranslationFailed(() => base.Column_collection_Count_method(async)); + + public override Task Column_collection_Length(bool async) + => AssertTranslationFailed(() => base.Column_collection_Length(async)); + + public override Task Column_collection_index_int(bool async) + => AssertTranslationFailed(() => base.Column_collection_index_int(async)); + + public override Task Column_collection_index_string(bool async) + => AssertTranslationFailed(() => base.Column_collection_index_string(async)); + + public override Task Column_collection_index_datetime(bool async) + => AssertTranslationFailed(() => base.Column_collection_index_datetime(async)); + + public override Task Column_collection_index_beyond_end(bool async) + => AssertTranslationFailed(() => base.Column_collection_index_beyond_end(async)); + + public override async Task Inline_collection_index_Column(bool async) + { + await base.Inline_collection_index_Column(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 [v].[Value] + FROM (VALUES (0, CAST(1 AS int)), (1, 2), (2, 3)) AS [v]([_ord], [Value]) + ORDER BY [v].[_ord] + OFFSET [p].[Int] ROWS FETCH NEXT 1 ROWS ONLY) = 1 +"""); + } + + public override Task Parameter_collection_index_Column(bool async) + => AssertTranslationFailed(() => base.Parameter_collection_index_Column(async)); + + public override Task Column_collection_ElementAt(bool async) + => AssertTranslationFailed(() => base.Column_collection_ElementAt(async)); + + public override Task Column_collection_Skip(bool async) + => AssertTranslationFailed(() => base.Column_collection_Skip(async)); + + public override Task Column_collection_Take(bool async) + => AssertTranslationFailed(() => base.Column_collection_Take(async)); + + public override Task Column_collection_Skip_Take(bool async) + => AssertTranslationFailed(() => base.Column_collection_Skip_Take(async)); + + public override Task Column_collection_Any(bool async) + => AssertTranslationFailed(() => base.Column_collection_Any(async)); + + public override async Task Column_collection_projection_from_top_level(bool async) + { + await base.Column_collection_projection_from_top_level(async); + + AssertSql( +""" +SELECT [p].[Ints] +FROM [PrimitiveCollectionsEntity] AS [p] +ORDER BY [p].[Id] +"""); + } + + public override Task Column_collection_and_parameter_collection_Join(bool async) + => AssertTranslationFailed(() => base.Column_collection_and_parameter_collection_Join(async)); + + public override Task Parameter_collection_Concat_column_collection(bool async) + => AssertTranslationFailed(() => base.Parameter_collection_Concat_column_collection(async)); + + public override Task Column_collection_Union_parameter_collection(bool async) + => AssertTranslationFailed(() => base.Column_collection_Union_parameter_collection(async)); + + public override Task Column_collection_Intersect_inline_collection(bool async) + => AssertTranslationFailed(() => base.Column_collection_Intersect_inline_collection(async)); + + public override Task Inline_collection_Except_column_collection(bool async) + => AssertTranslationFailed(() => base.Inline_collection_Except_column_collection(async)); + + public override async Task Column_collection_equality_parameter_collection(bool async) + { + await base.Column_collection_equality_parameter_collection(async); + + AssertSql( +""" +@__ints_0='[1,10]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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_collection_Concat_parameter_collection_equality_inline_collection_not_supported(bool async) + { + await base.Column_collection_Concat_parameter_collection_equality_inline_collection_not_supported(async); + + AssertSql(); + } + + public override async Task Column_collection_equality_inline_collection(bool async) + { + await base.Column_collection_equality_inline_collection(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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] = N'[1,10]' +"""); + } + + public override Task Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(bool async) + => AssertTranslationFailed(() => base.Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(async)); + + public override void Parameter_collection_in_subquery_and_Convert_as_compiled_query() + { + // Base implementation asserts that a different exception is thrown + } + + public override Task Parameter_collection_in_subquery_Count_as_compiled_query(bool async) + => AssertTranslationFailed(() => base.Parameter_collection_in_subquery_Count_as_compiled_query(async)); + + public override Task Column_collection_in_subquery_Union_parameter_collection(bool async) + => AssertTranslationFailed(() => base.Column_collection_in_subquery_Union_parameter_collection(async)); + + [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 PrimitiveCollectionsQueryOldSqlServerFixture : PrimitiveCollectionsQueryFixtureBase + { + // Use a different store name to prevent concurrency issues with the non-old PrimitiveCollectionsQuerySqlServerTest + protected override string StoreName + => "OldPrimitiveCollectionsTest"; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; + + // Compatibility level 120 (SQL Server 2014) doesn't support OPENJSON + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder).UseSqlServer(o => o.UseCompatibilityLevel(120)); + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs new file mode 100644 index 00000000000..6532ac5a61b --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -0,0 +1,846 @@ +// 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 Inline_collection_of_ints_Contains(bool async) + { + await base.Inline_collection_of_ints_Contains(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Inline_collection_of_nullable_ints_Contains(bool async) + { + await base.Inline_collection_of_nullable_ints_Contains(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Inline_collection_of_nullable_ints_Contains_null(bool async) + { + await base.Inline_collection_of_nullable_ints_Contains_null(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Inline_collection_Count_with_zero_values(bool async) + => AssertTranslationFailedWithDetails( + () => base.Inline_collection_Count_with_zero_values(async), + RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot); + + public override async Task Inline_collection_Count_with_one_value(bool async) + { + await base.Inline_collection_Count_with_one_value(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Inline_collection_Count_with_two_values(bool async) + { + await base.Inline_collection_Count_with_two_values(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Inline_collection_Count_with_three_values(bool async) + { + await base.Inline_collection_Count_with_three_values(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Inline_collection_Contains_with_zero_values(bool async) + => AssertTranslationFailedWithDetails( + () => base.Inline_collection_Contains_with_zero_values(async), + RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot); + + public override async Task Inline_collection_Contains_with_one_value(bool async) + { + await base.Inline_collection_Contains_with_one_value(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Inline_collection_Contains_with_two_values(bool async) + { + await base.Inline_collection_Contains_with_two_values(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Inline_collection_Contains_with_three_values(bool async) + { + await base.Inline_collection_Contains_with_three_values(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 Inline_collection_Contains_with_all_parameters(bool async) + { + await base.Inline_collection_Contains_with_all_parameters(async); + + // See #30732 for making this better + + AssertSql( +""" +@__p_0='[2,999]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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_0) AS [p0] + WHERE CAST([p0].[value] AS int) = [p].[Id]) +"""); + } + + public override async Task Inline_collection_Contains_with_parameter_and_column_based_expression(bool async) + { + await base.Inline_collection_Contains_with_parameter_and_column_based_expression(async); + + AssertSql(); + } + + public override async Task Parameter_collection_Count(bool async) + { + await base.Parameter_collection_Count(async); + + AssertSql( +""" +@__ids_0='[2,999]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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) AS [i] + WHERE CAST([i].[value] AS int) > [p].[Id]) = 1 +"""); + } + + public override async Task Parameter_collection_of_ints_Contains(bool async) + { + await base.Parameter_collection_of_ints_Contains(async); + + AssertSql( +""" +@__ints_0='[10,999]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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) AS [i] + WHERE CAST([i].[value] AS int) = [p].[Int]) +"""); + } + + public override async Task Parameter_collection_of_nullable_ints_Contains(bool async) + { + await base.Parameter_collection_of_nullable_ints_Contains(async); + + AssertSql( +""" +@__nullableInts_0='[10,999]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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) AS [n] + WHERE CAST([n].[value] AS int) = [p].[NullableInt] OR ([n].[value] IS NULL AND [p].[NullableInt] IS NULL)) +"""); + } + + public override async Task Parameter_collection_of_nullable_ints_Contains_null(bool async) + { + await base.Parameter_collection_of_nullable_ints_Contains_null(async); + + AssertSql( +""" +@__nullableInts_0='[null,999]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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) AS [n] + WHERE CAST([n].[value] AS int) = [p].[NullableInt] OR ([n].[value] IS NULL AND [p].[NullableInt] IS NULL)) +"""); + } + + public override async Task Parameter_collection_of_strings_Contains(bool async) + { + await base.Parameter_collection_of_strings_Contains(async); + + AssertSql( +""" +@__strings_0='["10","999"]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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) AS [s] + WHERE [s].[value] = [p].[String] OR ([s].[value] IS NULL AND [p].[String] IS NULL)) +"""); + } + + public override async Task Parameter_collection_of_DateTimes_Contains(bool async) + { + await base.Parameter_collection_of_DateTimes_Contains(async); + + AssertSql( +""" +@__dateTimes_0='["2020-01-10T12:30:00Z","9999-01-01T00:00:00Z"]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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) AS [d] + WHERE CAST([d].[value] AS datetime) = [p].[DateTime]) +"""); + } + + public override async Task Parameter_collection_of_bools_Contains(bool async) + { + await base.Parameter_collection_of_bools_Contains(async); + + AssertSql( +""" +@__bools_0='[true]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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) AS [b] + WHERE CAST([b].[value] AS bit) = [p].[Bool]) +"""); + } + + public override async Task Parameter_collection_of_enums_Contains(bool async) + { + await base.Parameter_collection_of_enums_Contains(async); + + AssertSql( +""" +@__enums_0='[0,3]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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) AS [e] + WHERE CAST([e].[value] AS int) = [p].[Enum]) +"""); + } + + public override async Task Column_collection_of_ints_Contains(bool async) + { + await base.Column_collection_of_ints_Contains(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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]) AS [i] + WHERE CAST([i].[value] AS int) = 10) +"""); + } + + public override async Task Column_collection_of_nullable_ints_Contains(bool async) + { + await base.Column_collection_of_nullable_ints_Contains(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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]) AS [n] + WHERE CAST([n].[value] AS int) = 10) +"""); + } + + public override async Task Column_collection_of_nullable_ints_Contains_null(bool async) + { + await base.Column_collection_of_nullable_ints_Contains_null(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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]) AS [n] + WHERE [n].[value] IS NULL) +"""); + } + + public override async Task Column_collection_of_bools_Contains(bool async) + { + await base.Column_collection_of_bools_Contains(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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]) AS [b] + WHERE CAST([b].[value] AS bit) = CAST(1 AS bit)) +"""); + } + + [ConditionalFact] + public virtual async Task Json_representation_of_bool_array() + { + 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_collection_Count_method(bool async) + { + await base.Column_collection_Count_method(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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]) AS [i]) = 2 +"""); + } + + public override async Task Column_collection_Length(bool async) + { + await base.Column_collection_Length(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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]) AS [i]) = 2 +"""); + } + + public override async Task Column_collection_index_int(bool async) + { + await base.Column_collection_index_int(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 CAST([i].[value] AS int) + FROM OpenJson([p].[Ints]) AS [i] + ORDER BY CAST([i].[key] AS int) + OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY) = 10 +"""); + } + + public override async Task Column_collection_index_string(bool async) + { + await base.Column_collection_index_string(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 [s].[value] + FROM OpenJson([p].[Strings]) AS [s] + ORDER BY CAST([s].[key] AS int) + OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY) = N'10' +"""); + } + + public override async Task Column_collection_index_datetime(bool async) + { + await base.Column_collection_index_datetime(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 CAST([d].[value] AS datetime2) + FROM OpenJson([p].[DateTimes]) AS [d] + ORDER BY CAST([d].[key] AS int) + OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY) = '2020-01-10T12:30:00.0000000Z' +"""); + } + + public override async Task Column_collection_index_beyond_end(bool async) + { + await base.Column_collection_index_beyond_end(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 CAST([i].[value] AS int) + FROM OpenJson([p].[Ints]) AS [i] + ORDER BY CAST([i].[key] AS int) + OFFSET 999 ROWS FETCH NEXT 1 ROWS ONLY) = 10 +"""); + } + + public override async Task Inline_collection_index_Column(bool async) + { + await base.Inline_collection_index_Column(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 [v].[Value] + FROM (VALUES (0, CAST(1 AS int)), (1, 2), (2, 3)) AS [v]([_ord], [Value]) + ORDER BY [v].[_ord] + OFFSET [p].[Int] ROWS FETCH NEXT 1 ROWS ONLY) = 1 +"""); + } + + public override async Task Parameter_collection_index_Column(bool async) + { + await base.Parameter_collection_index_Column(async); + + AssertSql( +""" +@__ints_0='[1,2,3]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 CAST([i].[value] AS int) AS [value] + FROM OpenJson(@__ints_0) AS [i] + ORDER BY CAST([i].[key] AS int) + OFFSET [p].[Int] ROWS FETCH NEXT 1 ROWS ONLY) = 1 +"""); + } + + public override async Task Column_collection_ElementAt(bool async) + { + await base.Column_collection_ElementAt(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 CAST([i].[value] AS int) + FROM OpenJson([p].[Ints]) AS [i] + ORDER BY CAST([i].[key] AS int) + OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY) = 10 +"""); + } + + public override async Task Column_collection_Skip(bool async) + { + await base.Column_collection_Skip(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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([i].[key] AS int) AS [c] + FROM OpenJson([p].[Ints]) AS [i] + ORDER BY CAST([i].[key] AS int) + OFFSET 1 ROWS + ) AS [t]) = 2 +"""); + } + + public override async Task Column_collection_Take(bool async) + { + await base.Column_collection_Take(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 ( + SELECT TOP(2) CAST([i].[value] AS int) AS [c], CAST([i].[key] AS int) AS [c0] + FROM OpenJson([p].[Ints]) AS [i] + ORDER BY CAST([i].[key] AS int) + ) AS [t] + WHERE [t].[c] = 11) +"""); + } + + public override async Task Column_collection_Skip_Take(bool async) + { + await base.Column_collection_Skip_Take(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 ( + SELECT CAST([i].[value] AS int) AS [c], CAST([i].[key] AS int) AS [c0] + FROM OpenJson([p].[Ints]) AS [i] + ORDER BY CAST([i].[key] AS int) + OFFSET 1 ROWS FETCH NEXT 2 ROWS ONLY + ) AS [t] + WHERE [t].[c] = 11) +"""); + } + + public override async Task Column_collection_Any(bool async) + { + await base.Column_collection_Any(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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]) AS [i]) +"""); + } + + public override async Task Column_collection_projection_from_top_level(bool async) + { + await base.Column_collection_projection_from_top_level(async); + + AssertSql( +""" +SELECT [p].[Ints] +FROM [PrimitiveCollectionsEntity] AS [p] +ORDER BY [p].[Id] +"""); + } + + public override async Task Column_collection_and_parameter_collection_Join(bool async) + { + await base.Column_collection_and_parameter_collection_Join(async); + + AssertSql( +""" +@__ints_0='[11,111]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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]) AS [i] + INNER JOIN OpenJson(@__ints_0) AS [i0] ON CAST([i].[value] AS int) = [i0].[value]) = 2 +"""); + } + + public override async Task Parameter_collection_Concat_column_collection(bool async) + { + await base.Parameter_collection_Concat_column_collection(async); + + AssertSql( +""" +@__ints_0='[11,111]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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([i].[value] AS int) AS [value] + FROM OpenJson(@__ints_0) AS [i] + UNION ALL + SELECT CAST([i0].[value] AS int) AS [value] + FROM OpenJson([p].[Ints]) AS [i0] + ) AS [t]) = 2 +"""); + } + + public override async Task Column_collection_Union_parameter_collection(bool async) + { + await base.Column_collection_Union_parameter_collection(async); + + AssertSql( +""" +@__ints_0='[11,111]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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([i].[value] AS int) AS [c] + FROM OpenJson([p].[Ints]) AS [i] + UNION + SELECT CAST([i0].[value] AS int) AS [c] + FROM OpenJson(@__ints_0) AS [i0] + ) AS [t]) = 2 +"""); + } + + public override async Task Column_collection_Intersect_inline_collection(bool async) + { + await base.Column_collection_Intersect_inline_collection(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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([i].[value] AS int) AS [c] + FROM OpenJson([p].[Ints]) AS [i] + INTERSECT + SELECT [v].[Value] AS [c] + FROM (VALUES (CAST(11 AS int)), (111)) AS [v]([Value]) + ) AS [t]) = 2 +"""); + } + + public override async Task Inline_collection_Except_column_collection(bool async) + { + await base.Inline_collection_Except_column_collection(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 [v].[Value] + FROM (VALUES (CAST(11 AS int)), (111)) AS [v]([Value]) + EXCEPT + SELECT CAST([i].[value] AS int) AS [Value] + FROM OpenJson([p].[Ints]) AS [i] + ) AS [t] + WHERE [t].[Value] % 2 = 1) = 2 +"""); + } + + public override async Task Column_collection_equality_parameter_collection(bool async) + { + await base.Column_collection_equality_parameter_collection(async); + + AssertSql( +""" +@__ints_0='[1,10]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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_collection_Concat_parameter_collection_equality_inline_collection_not_supported(bool async) + { + await base.Column_collection_Concat_parameter_collection_equality_inline_collection_not_supported(async); + + AssertSql(); + } + + public override async Task Column_collection_equality_inline_collection(bool async) + { + await base.Column_collection_equality_inline_collection(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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] = N'[1,10]' +"""); + } + + public override async Task Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(bool async) + { + await base.Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(async); + + AssertSql( +""" +@__ints='[10,111]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 [t].[value] + FROM ( + SELECT CAST([i].[value] AS int) AS [value] + FROM OpenJson(@__ints) AS [i] + ORDER BY CAST([i].[key] AS int) + OFFSET 1 ROWS + ) AS [t] + UNION + SELECT CAST([i0].[value] AS int) AS [value] + FROM OpenJson([p].[Ints]) AS [i0] + ) AS [t0]) = 3 +"""); + } + + public override void Parameter_collection_in_subquery_and_Convert_as_compiled_query() + { + base.Parameter_collection_in_subquery_and_Convert_as_compiled_query(); + + AssertSql(); + } + + public override async Task Parameter_collection_in_subquery_Count_as_compiled_query(bool async) + { + await base.Parameter_collection_in_subquery_Count_as_compiled_query(async); + + // TODO: the subquery projection contains two extra columns which we should remove + AssertSql( +""" +@__ints='[10,111]' (Size = 4000) + +SELECT COUNT(*) +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT CAST([i].[value] AS int) AS [value], CAST([i].[key] AS int) AS [c], CAST([i].[value] AS int) AS [value0] + FROM OpenJson(@__ints) AS [i] + ORDER BY CAST([i].[key] AS int) + OFFSET 1 ROWS + ) AS [t] + WHERE [t].[value0] > [p].[Id]) = 1 +"""); + } + + public override async Task Column_collection_in_subquery_Union_parameter_collection(bool async) + { + await base.Column_collection_in_subquery_Union_parameter_collection(async); + + AssertSql( +""" +@__ints_0='[10,111]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 [t].[c] + FROM ( + SELECT CAST([i].[value] AS int) AS [c] + FROM OpenJson([p].[Ints]) AS [i] + ORDER BY CAST([i].[key] AS int) + OFFSET 1 ROWS + ) AS [t] + UNION + SELECT CAST([i0].[value] AS int) AS [c] + FROM OpenJson(@__ints_0) AS [i0] + ) AS [t0]) = 3 +"""); + } + + [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..ddf7171c169 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -168,11 +168,58 @@ public async Task Where_contains_DateTime_literals(bool async) Assert.Single(results); + // TODO: The parameters values below are incorrect, since we currently don't take the element type mapping into account when + // generating the JSON representation (#30677) AssertSql( """ +@__dateTimes_0='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","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"]' (Size = 4000) +@__dateTimes_0_1='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","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"]' (Size = 4000) +@__dateTimes_0_2='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","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"]' (Size = 4000) +@__dateTimes_0_3='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","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"]' (Size = 4000) +@__dateTimes_0_4='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","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"]' (Size = 4000) +@__dateTimes_0_5='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","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"]' (Size = 4000) +@__dateTimes_0_6='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","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"]' (Size = 4000) +@__dateTimes_0_7='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","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"]' (Size = 4000) +@__dateTimes_0_8='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","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"]' (Size = 4000) +@__dateTimes_0_9='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","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"]' (Size = 4000) +@__dateTimes_0_10='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","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"]' (Size = 4000) + 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) AS [d0] + WHERE CAST([d0].[value] AS smalldatetime) = [d].[SmallDateTime]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0_1) AS [d1] + WHERE CAST([d1].[value] AS datetime) = [d].[DateTime]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0_2) AS [d2] + WHERE CAST([d2].[value] AS datetime2) = [d].[DateTime2]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0_3) AS [d3] + WHERE CAST([d3].[value] AS datetime2(0)) = [d].[DateTime2_0]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0_4) AS [d4] + WHERE CAST([d4].[value] AS datetime2(1)) = [d].[DateTime2_1]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0_5) AS [d5] + WHERE CAST([d5].[value] AS datetime2(2)) = [d].[DateTime2_2]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0_6) AS [d6] + WHERE CAST([d6].[value] AS datetime2(3)) = [d].[DateTime2_3]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0_7) AS [d7] + WHERE CAST([d7].[value] AS datetime2(4)) = [d].[DateTime2_4]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0_8) AS [d8] + WHERE CAST([d8].[value] AS datetime2(5)) = [d].[DateTime2_5]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0_9) AS [d9] + WHERE CAST([d9].[value] AS datetime2(6)) = [d].[DateTime2_6]) AND EXISTS ( + SELECT 1 + FROM OpenJson(@__dateTimes_0_10) AS [d10] + WHERE CAST([d10].[value] AS datetime2(7)) = [d].[DateTime2_7]) """); } @@ -3871,9 +3918,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) AS [t] + WHERE CAST([t].[value] AS smalldatetime) = [r].[MyTime]) """); } } @@ -3929,14 +3981,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) AS [k] + WHERE CAST([k].[value] AS uniqueidentifier) = CASE + WHEN [t].[Type] = 0 THEN @__key_2 + ELSE @__key_2 + END) """); } } 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 10608526023..8aa9b03fddb 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs @@ -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) AS [t1] + WHERE CAST([t1].[value] AS uniqueidentifier) = [t0].[Id] OR ([t1].[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) AS [t1] + WHERE CAST([t1].[value] AS uniqueidentifier) = [t0].[Id] OR ([t1].[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) AS [t1] + WHERE CAST([t1].[value] AS uniqueidentifier) = [t0].[Id] OR ([t1].[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) AS [c0] + WHERE CAST([c0].[value] AS varchar(100)) = [c].[Location] OR ([c0].[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) AS [i] + WHERE CAST([i].[value] AS uniqueidentifier) = [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) AS [c0] + WHERE CAST([c0].[value] AS nvarchar(450)) = [c].[Name] OR ([c0].[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) AS [n] + WHERE CAST([n].[value] AS nvarchar(450)) = [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) AS [d] + WHERE CAST([d].[value] AS datetimeoffset) = [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) AS [i] + WHERE CAST([i].[value] AS int) = [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) AS [t] + WHERE CAST([t].[value] AS int) = [w0].[AmmunitionType] OR ([t].[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) AS [v] + WHERE CAST([v].[value] AS bit) = [t].[HasSoulPatch]) """); } @@ -11927,6 +11988,8 @@ public override async Task Where_bool_column_or_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] @@ -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) AS [v] + WHERE CAST([v].[value] AS bit) = [t].[HasSoulPatch]) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index 9584725abb2..bc90b417e6e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -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) AS [t0] + WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[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) AS [t0] + WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[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) AS [t0] + WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[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) AS [c0] + WHERE CAST([c0].[value] AS varchar(100)) = [c].[Location] OR ([c0].[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) AS [i] + WHERE CAST([i].[value] AS uniqueidentifier) = [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) AS [c0] + WHERE CAST([c0].[value] AS nvarchar(450)) = [c].[Name] OR ([c0].[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) AS [n] + WHERE CAST([n].[value] AS nvarchar(450)) = [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) AS [d] + WHERE CAST([d].[value] AS datetimeoffset) = [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) AS [i] + WHERE CAST([i].[value] AS int) = [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) AS [t] + WHERE CAST([t].[value] AS int) = [w0].[AmmunitionType] OR ([t].[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) AS [v] + WHERE CAST([v].[value] AS bit) = [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) AS [v] + WHERE CAST([v].[value] AS bit) = [g].[HasSoulPatch]) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsQuerySqlServerTest.cs index bea5f56d43f..8dbf5a595e2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsQuerySqlServerTest.cs @@ -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) AS [v] + WHERE [v].[value] = [l].[Name] OR ([v].[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) AS [v0] + WHERE [v0].[value] = [l0].[Name] OR ([v0].[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) AS [v] + WHERE [v].[value] = [l].[Name] OR ([v].[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..331a4b9972d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs @@ -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) AS [v] + WHERE [v].[value] = [l].[Name] OR ([v].[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) AS [v] + WHERE [v].[value] = [l].[Name] OR ([v].[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) AS [v0] + WHERE [v0].[value] = [l0].[Name] OR ([v0].[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 2ec49776d41..3d70e089d77 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs @@ -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) AS [t0] + WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[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) AS [t0] + WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[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) AS [t0] + WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[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) AS [v] + WHERE CAST([v].[value] AS bit) = [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) AS [c0] + WHERE CAST([c0].[value] AS nvarchar(450)) = [c].[Name] OR ([c0].[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) AS [t] + WHERE CAST([t].[value] AS int) = [w0].[AmmunitionType] OR ([t].[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) AS [i] + WHERE CAST([i].[value] AS int) = [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) AS [d] + WHERE CAST([d].[value] AS datetimeoffset) = [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) AS [c0] + WHERE CAST([c0].[value] AS varchar(100)) = [c].[Location] OR ([c0].[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) AS [n] + WHERE CAST([n].[value] AS nvarchar(450)) = [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) AS [i] + WHERE CAST([i].[value] AS uniqueidentifier) = [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) AS [v] + WHERE CAST([v].[value] AS bit) = [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..fdd59b9dfde --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs @@ -0,0 +1,219 @@ +// 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 +{ + #region Support for specific element types + + public override async Task Array_of_int() + { + await base.Array_of_int(); + + AssertSql( +""" +SELECT "t"."Id", "t"."Ints", "t"."SomeArray" +FROM "TestEntity" AS "t" +WHERE ( + SELECT COUNT(*) + FROM json_each("t"."SomeArray") AS "s" + WHERE "s"."value" = 1) = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_long() + { + await base.Array_of_long(); + + AssertSql( +""" +SELECT "t"."Id", "t"."Ints", "t"."SomeArray" +FROM "TestEntity" AS "t" +WHERE ( + SELECT COUNT(*) + FROM json_each("t"."SomeArray") AS "s" + WHERE "s"."value" = 1) = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_short() + { + await base.Array_of_short(); + + AssertSql( +""" +SELECT "t"."Id", "t"."Ints", "t"."SomeArray" +FROM "TestEntity" AS "t" +WHERE ( + SELECT COUNT(*) + FROM json_each("t"."SomeArray") AS "s" + WHERE "s"."value" = 1) = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_double() + { + await base.Array_of_double(); + + AssertSql( +""" +SELECT "t"."Id", "t"."Ints", "t"."SomeArray" +FROM "TestEntity" AS "t" +WHERE ( + SELECT COUNT(*) + FROM json_each("t"."SomeArray") AS "s" + WHERE "s"."value" = 1.0) = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_float() + { + await base.Array_of_float(); + + AssertSql( +""" +SELECT "t"."Id", "t"."Ints", "t"."SomeArray" +FROM "TestEntity" AS "t" +WHERE ( + SELECT COUNT(*) + FROM json_each("t"."SomeArray") AS "s" + WHERE "s"."value" = 1) = 2 +LIMIT 2 +"""); + } + + // The JSON representation for decimal is e.g. 1 (JSON int), whereas our literal representation is "1.0" (string). See #30727. + public override Task Array_of_decimal() + => AssertTranslationFailed(() => base.Array_of_decimal()); + + public override async Task Array_of_DateTime() + { + await base.Array_of_DateTime(); + + AssertSql( +""" +SELECT "t"."Id", "t"."Ints", "t"."SomeArray" +FROM "TestEntity" AS "t" +WHERE ( + SELECT COUNT(*) + FROM json_each("t"."SomeArray") AS "s" + WHERE datetime("s"."value") = '2023-01-01 12:30:00') = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_DateOnly() + { + await base.Array_of_DateOnly(); + + AssertSql( +""" +SELECT "t"."Id", "t"."Ints", "t"."SomeArray" +FROM "TestEntity" AS "t" +WHERE ( + SELECT COUNT(*) + FROM json_each("t"."SomeArray") AS "s" + WHERE "s"."value" = '2023-01-01') = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_TimeOnly() + { + await base.Array_of_TimeOnly(); + + AssertSql( +""" +SELECT "t"."Id", "t"."Ints", "t"."SomeArray" +FROM "TestEntity" AS "t" +WHERE ( + SELECT COUNT(*) + FROM json_each("t"."SomeArray") AS "s" + WHERE "s"."value" = '12:30:00') = 2 +LIMIT 2 +"""); + } + + // The JSON representation for DateTimeOffset is ISO8601 (2023-01-01T12:30:00+02:00), but our SQL literal representation is + // 2023-01-01 12:30:00+02:00 (no T). + // datetime('2023-01-01T12:30:00+02:00') yields '2023-01-01 10:30:00' - converted to UTC, no timezone. + // See #30727. + public override Task Array_of_DateTimeOffset() + => AssertTranslationFailed(() => base.Array_of_DateTimeOffset()); + + public override async Task Array_of_bool() + { + await base.Array_of_bool(); + + AssertSql( +""" +SELECT "t"."Id", "t"."Ints", "t"."SomeArray" +FROM "TestEntity" AS "t" +WHERE ( + SELECT COUNT(*) + FROM json_each("t"."SomeArray") AS "s" + WHERE "s"."value") = 2 +LIMIT 2 +"""); + } + + public override async Task Array_of_Guid() + { + await base.Array_of_Guid(); + + AssertSql( +""" +SELECT "t"."Id", "t"."Ints", "t"."SomeArray" +FROM "TestEntity" AS "t" +WHERE ( + SELECT COUNT(*) + FROM json_each("t"."SomeArray") AS "s" + WHERE upper("s"."value") = 'DC8C903D-D655-4144-A0FD-358099D40AE1') = 2 +LIMIT 2 +"""); + } + + // The JSON representation for new[] { 1, 2 } is AQI= (base64), our SQL literal representation is X'0102'. See #30727. + public override Task Array_of_byte_array() + => AssertTranslationFailed(() => base.Array_of_byte_array()); + + public override async Task Array_of_enum() + { + await base.Array_of_enum(); + + AssertSql( +""" +SELECT "t"."Id", "t"."Ints", "t"."SomeArray" +FROM "TestEntity" AS "t" +WHERE ( + SELECT COUNT(*) + FROM json_each("t"."SomeArray") AS "s" + WHERE "s"."value" = 0) = 2 +LIMIT 2 +"""); + } + + [ConditionalFact] // #30630 + public override async Task Array_of_geometry_is_not_supported() + { + 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[]", "TestEntity", "Points"), exception.Message); + } + + #endregion Support for specific element types + + 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..3e12356b589 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindCompiledQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindCompiledQuerySqliteTest.cs @@ -12,14 +12,6 @@ public NorthwindCompiledQuerySqliteTest(NorthwindQuerySqliteFixture Assert.Equal( - CoreStrings.TranslationFailedWithDetails( - "DbSet() .Where(c => c.CustomerID == (string)__parameters .ElementAt(0))", - CoreStrings.QueryUnableToTranslateMethod("System.Linq.Enumerable", nameof(Enumerable.ElementAt))), - Assert.Throws( - () => base.MakeBinary_does_not_throw_for_unsupported_operator()).Message.Replace("\r", "").Replace("\n", "")); - public override void Query_with_array_parameter() { var query = EF.CompileQuery( @@ -60,7 +52,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 +62,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..19f8b5ab03a --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -0,0 +1,796 @@ +// 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 Inline_collection_of_ints_Contains(bool async) + { + await base.Inline_collection_of_ints_Contains(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 Inline_collection_of_nullable_ints_Contains(bool async) + { + await base.Inline_collection_of_nullable_ints_Contains(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 Inline_collection_of_nullable_ints_Contains_null(bool async) + { + await base.Inline_collection_of_nullable_ints_Contains_null(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 Inline_collection_Count_with_zero_values(bool async) + => AssertTranslationFailedWithDetails( + () => base.Inline_collection_Count_with_zero_values(async), + RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot); + + public override async Task Inline_collection_Count_with_one_value(bool async) + { + await base.Inline_collection_Count_with_one_value(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 Inline_collection_Count_with_two_values(bool async) + { + await base.Inline_collection_Count_with_two_values(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 Inline_collection_Count_with_three_values(bool async) + { + await base.Inline_collection_Count_with_three_values(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 Inline_collection_Contains_with_zero_values(bool async) + => AssertTranslationFailedWithDetails( + () => base.Inline_collection_Contains_with_zero_values(async), + RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot); + + public override async Task Inline_collection_Contains_with_one_value(bool async) + { + await base.Inline_collection_Contains_with_one_value(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 Inline_collection_Contains_with_two_values(bool async) + { + await base.Inline_collection_Contains_with_two_values(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 Inline_collection_Contains_with_three_values(bool async) + { + await base.Inline_collection_Contains_with_three_values(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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_collection_Count(bool async) + { + await base.Parameter_collection_Count(async); + + AssertSql( +""" +@__ids_0='[2,999]' (Size = 7) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "i" + WHERE "i"."value" > "p"."Id") = 1 +"""); + } + + public override async Task Parameter_collection_of_ints_Contains(bool async) + { + await base.Parameter_collection_of_ints_Contains(async); + + AssertSql( +""" +@__ints_0='[10,999]' (Size = 8) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "i" + WHERE "i"."value" = "p"."Int") +"""); + } + + public override async Task Parameter_collection_of_nullable_ints_Contains(bool async) + { + await base.Parameter_collection_of_nullable_ints_Contains(async); + + AssertSql( +""" +@__nullableInts_0='[10,999]' (Size = 8) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "n" + WHERE "n"."value" = "p"."NullableInt" OR ("n"."value" IS NULL AND "p"."NullableInt" IS NULL)) +"""); + } + + public override async Task Parameter_collection_of_nullable_ints_Contains_null(bool async) + { + await base.Parameter_collection_of_nullable_ints_Contains_null(async); + + AssertSql( +""" +@__nullableInts_0='[null,999]' (Size = 10) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "n" + WHERE "n"."value" = "p"."NullableInt" OR ("n"."value" IS NULL AND "p"."NullableInt" IS NULL)) +"""); + } + + public override async Task Parameter_collection_of_strings_Contains(bool async) + { + await base.Parameter_collection_of_strings_Contains(async); + + AssertSql( +""" +@__strings_0='["10","999"]' (Size = 12) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "s" + WHERE "s"."value" = "p"."String" OR ("s"."value" IS NULL AND "p"."String" IS NULL)) +"""); + } + + public override async Task Parameter_collection_of_DateTimes_Contains(bool async) + { + await base.Parameter_collection_of_DateTimes_Contains(async); + + AssertSql( +""" +@__dateTimes_0='["2020-01-10T12:30:00Z","9999-01-01T00:00:00Z"]' (Size = 47) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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(@__dateTimes_0) AS "d" + WHERE datetime("d"."value") = "p"."DateTime") +"""); + } + + public override async Task Parameter_collection_of_bools_Contains(bool async) + { + await base.Parameter_collection_of_bools_Contains(async); + + AssertSql( +""" +@__bools_0='[true]' (Size = 6) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "b" + WHERE "b"."value" = "p"."Bool") +"""); + } + + public override async Task Parameter_collection_of_enums_Contains(bool async) + { + await base.Parameter_collection_of_enums_Contains(async); + + AssertSql( +""" +@__enums_0='[0,3]' (Size = 5) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "e" + WHERE "e"."value" = "p"."Enum") +"""); + } + + public override async Task Column_collection_of_ints_Contains(bool async) + { + await base.Column_collection_of_ints_Contains(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "i" + WHERE "i"."value" = 10) +"""); + } + + public override async Task Column_collection_of_nullable_ints_Contains(bool async) + { + await base.Column_collection_of_nullable_ints_Contains(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "n" + WHERE "n"."value" = 10) +"""); + } + + public override async Task Column_collection_of_nullable_ints_Contains_null(bool async) + { + await base.Column_collection_of_nullable_ints_Contains_null(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "n" + WHERE "n"."value" IS NULL) +"""); + } + + public override async Task Column_collection_of_bools_Contains(bool async) + { + await base.Column_collection_of_bools_Contains(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "b" + WHERE "b"."value") +"""); + } + + public override async Task Column_collection_Count_method(bool async) + { + await base.Column_collection_Count_method(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 json_array_length("p"."Ints") = 2 +"""); + } + + public override async Task Column_collection_Length(bool async) + { + await base.Column_collection_Length(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 json_array_length("p"."Ints") = 2 +"""); + } + + public override async Task Column_collection_index_int(bool async) + { + await base.Column_collection_index_int(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "i"."value" + FROM json_each("p"."Ints") AS "i" + ORDER BY "i"."key" + LIMIT 1 OFFSET 1) = 10 +"""); + } + + public override async Task Column_collection_index_string(bool async) + { + await base.Column_collection_index_string(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "s"."value" + FROM json_each("p"."Strings") AS "s" + ORDER BY "s"."key" + LIMIT 1 OFFSET 1) = '10' +"""); + } + + public override async Task Column_collection_index_datetime(bool async) + { + await base.Column_collection_index_datetime(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 datetime("d"."value") AS "value" + FROM json_each("p"."DateTimes") AS "d" + ORDER BY "d"."key" + LIMIT 1 OFFSET 1) = '2020-01-10 12:30:00' +"""); + } + + public override async Task Column_collection_index_beyond_end(bool async) + { + await base.Column_collection_index_beyond_end(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "i"."value" + FROM json_each("p"."Ints") AS "i" + ORDER BY "i"."key" + LIMIT 1 OFFSET 999) = 10 +"""); + } + + [ConditionalTheory(Skip = "Sqlite issue, should be taken care of by #30724")] + public override async Task Inline_collection_index_Column(bool async) + { + await base.Inline_collection_index_Column(async); + + AssertSql( +""" +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 [v].[Value] + FROM (VALUES (0, CAST(1 AS int)), (1, 2), (2, 3)) AS [v]([_ord], [Value]) + ORDER BY [v].[_ord] + OFFSET [p].[Int] ROWS FETCH NEXT 1 ROWS ONLY) = 1 +"""); + } + + [ConditionalTheory(Skip = "Sqlite issue, should be taken care of by #30724")] + public override async Task Parameter_collection_index_Column(bool async) + { + await base.Parameter_collection_index_Column(async); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [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 [v].[Value] + FROM (VALUES (0, CAST(1 AS int)), (1, 2), (2, 3)) AS [v]([_ord], [Value]) + ORDER BY [v].[_ord] + OFFSET [p].[Int] ROWS FETCH NEXT 1 ROWS ONLY) = 1 +"""); + } + + public override async Task Column_collection_ElementAt(bool async) + { + await base.Column_collection_ElementAt(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "i"."value" + FROM json_each("p"."Ints") AS "i" + ORDER BY "i"."key" + LIMIT 1 OFFSET 1) = 10 +"""); + } + + public override async Task Column_collection_Skip(bool async) + { + await base.Column_collection_Skip(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "i"."key" + FROM json_each("p"."Ints") AS "i" + ORDER BY "i"."key" + LIMIT -1 OFFSET 1 + ) AS "t") = 2 +"""); + } + + public override async Task Column_collection_Take(bool async) + { + await base.Column_collection_Take(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 ( + SELECT "i"."value", "i"."key" + FROM json_each("p"."Ints") AS "i" + ORDER BY "i"."key" + LIMIT 2 + ) AS "t" + WHERE "t"."value" = 11) +"""); + } + + public override async Task Column_collection_Skip_Take(bool async) + { + await base.Column_collection_Skip_Take(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 ( + SELECT "i"."value", "i"."key" + FROM json_each("p"."Ints") AS "i" + ORDER BY "i"."key" + LIMIT 2 OFFSET 1 + ) AS "t" + WHERE "t"."value" = 11) +"""); + } + + public override async Task Column_collection_Any(bool async) + { + await base.Column_collection_Any(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "i") +"""); + } + + public override async Task Column_collection_projection_from_top_level(bool async) + { + await base.Column_collection_projection_from_top_level(async); + + AssertSql( +""" +SELECT "p"."Ints" +FROM "PrimitiveCollectionsEntity" AS "p" +ORDER BY "p"."Id" +"""); + } + + public override async Task Column_collection_and_parameter_collection_Join(bool async) + { + await base.Column_collection_and_parameter_collection_Join(async); + + AssertSql( +""" +@__ints_0='[11,111]' (Size = 8) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "i" + INNER JOIN json_each(@__ints_0) AS "i0" ON "i"."value" = "i0"."value") = 2 +"""); + } + + public override async Task Parameter_collection_Concat_column_collection(bool async) + { + await base.Parameter_collection_Concat_column_collection(async); + + AssertSql( +""" +@__ints_0='[11,111]' (Size = 8) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "i"."value" + FROM json_each(@__ints_0) AS "i" + UNION ALL + SELECT "i0"."value" + FROM json_each("p"."Ints") AS "i0" + ) AS "t") = 2 +"""); + } + + public override async Task Column_collection_Union_parameter_collection(bool async) + { + await base.Column_collection_Union_parameter_collection(async); + + AssertSql( +""" +@__ints_0='[11,111]' (Size = 8) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "i"."value" + FROM json_each("p"."Ints") AS "i" + UNION + SELECT "i0"."value" + FROM json_each(@__ints_0) AS "i0" + ) AS "t") = 2 +"""); + } + + public override async Task Column_collection_Intersect_constant_collection(bool async) + { + await base.Column_collection_Intersect_constant_collection(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "i"."value" + FROM json_each("p"."Ints") AS "i" + INTERSECT + SELECT CAST(11 AS INTEGER) AS "Value" UNION ALL VALUES (111) + ) AS "t") = 2 +"""); + } + + public override async Task Constant_collection_Except_column_collection(bool async) + { + await base.Constant_collection_Except_column_collection(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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(11 AS INTEGER) AS "Value" UNION ALL VALUES (111) + EXCEPT + SELECT "i"."value" AS "Value" + FROM json_each("p"."Ints") AS "i" + ) AS "t" + WHERE "t"."Value" % 2 = 1) = 2 +"""); + } + + public override async Task Column_collection_equality_parameter_collection(bool async) + { + await base.Column_collection_equality_parameter_collection(async); + + AssertSql( +""" +@__ints_0='[1,10]' (Size = 6) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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_collection_Concat_parameter_collection_equality_constant_collection_not_supported(bool async) + { + await base.Column_collection_Concat_parameter_collection_equality_constant_collection_not_supported(async); + + AssertSql(); + } + + public override async Task Column_collection_equality_constant_collection(bool async) + { + await base.Column_collection_equality_constant_collection(async); + + AssertSql( +""" +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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" = '[1,10]' +"""); + } + + public override async Task Parameter_collection_in_subquery_Count_as_compiled_query(bool async) + { + await base.Parameter_collection_in_subquery_Count_as_compiled_query(async); + + AssertSql( +""" +@__ints='[10,111]' (Size = 8) + +SELECT COUNT(*) +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT "i"."value", "i"."key", "i"."value" AS "value0" + FROM json_each(@__ints) AS "i" + ORDER BY "i"."key" + LIMIT -1 OFFSET 1 + ) AS "t" + WHERE "t"."value0" > "p"."Id") = 1 +"""); + } + + public override async Task Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(bool async) + { + await base.Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(async); + + AssertSql( +""" +@__ints='[10,111]' (Size = 8) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "t"."value" + FROM ( + SELECT "i"."value" + FROM json_each(@__ints) AS "i" + ORDER BY "i"."key" + LIMIT -1 OFFSET 1 + ) AS "t" + UNION + SELECT "i0"."value" + FROM json_each("p"."Ints") AS "i0" + ) AS "t0") = 3 +"""); + } + + public override void Parameter_collection_in_subquery_and_Convert_as_compiled_query() + { + base.Parameter_collection_in_subquery_and_Convert_as_compiled_query(); + + AssertSql(); + } + + public override async Task Column_collection_in_subquery_Union_parameter_collection(bool async) + { + await base.Column_collection_in_subquery_Union_parameter_collection(async); + + AssertSql( +""" +@__ints_0='[10,111]' (Size = 8) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "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 "t"."value" + FROM ( + SELECT "i"."value" + FROM json_each("p"."Ints") AS "i" + ORDER BY "i"."key" + LIMIT -1 OFFSET 1 + ) AS "t" + UNION + SELECT "i0"."value" + FROM json_each(@__ints_0) AS "i0" + ) AS "t0") = 3 +"""); + } + + [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; + } +} diff --git a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs index 037742e5ad8..ea60bd5b12f 100644 --- a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs @@ -1636,19 +1636,6 @@ private abstract class BadCustomValueGenerator2 : CustomValueGenerator { } - [ConditionalFact] - public virtual void Throws_for_collection_of_string() - { - var modelBuilder = CreateModelBuilder(); - - modelBuilder.Entity(); - - Assert.Equal( - CoreStrings.PropertyNotAdded( - nameof(StringCollectionEntity), nameof(StringCollectionEntity.Property), "ICollection"), - Assert.Throws(() => modelBuilder.FinalizeModel()).Message); - } - protected class StringCollectionEntity { public ICollection Property { get; set; } @@ -1676,19 +1663,6 @@ public virtual void Property_bag_cannot_be_configured_as_property() .Message); } - [ConditionalFact] - protected virtual void Mapping_throws_for_non_ignored_array() - { - var modelBuilder = CreateModelBuilder(); - - modelBuilder.Entity(); - - Assert.Equal( - CoreStrings.PropertyNotAdded( - typeof(OneDee).ShortDisplayName(), "One", typeof(int[]).ShortDisplayName()), - Assert.Throws(() => modelBuilder.FinalizeModel()).Message); - } - [ConditionalFact] protected virtual void Mapping_ignores_ignored_array() { @@ -1701,19 +1675,6 @@ protected virtual void Mapping_ignores_ignored_array() Assert.Null(model.FindEntityType(typeof(OneDee)).FindProperty("One")); } - [ConditionalFact] - protected virtual void Mapping_throws_for_non_ignored_two_dimensional_array() - { - var modelBuilder = CreateModelBuilder(); - - modelBuilder.Entity(); - - Assert.Equal( - CoreStrings.PropertyNotAdded( - typeof(TwoDee).ShortDisplayName(), "Two", typeof(int[,]).ShortDisplayName()), - Assert.Throws(() => modelBuilder.FinalizeModel()).Message); - } - [ConditionalFact] protected virtual void Mapping_ignores_ignored_two_dimensional_array() {