From 991feba943b5dca1362cb3aa75425137155382b4 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 28 May 2025 20:48:56 +0200 Subject: [PATCH] Work on inlined primitive collections (VALUES) * Refactor and generalize the pruning of the VALUES _ord column, moving it from RelationalTypeMappingPostprocessor to SqlPruner. * Ensure that inlined parameterized collections (EF.Constant) preserved ordering via _ord and specifies the type mapping. * Properly visit JsonScalarExpression in SqlNullabilityProcessor. * Add CAST() to VALUES generated in SqlNullabilityProcessor because of EF.Constant(). Fixes #36158 Fixes #36462 Fixes #36463 --- ...yableMethodTranslatingExpressionVisitor.cs | 7 +- .../RelationalTypeMappingPostprocessor.cs | 33 +---- .../SqlExpressions/SqlParameterExpression.cs | 4 +- .../Query/SqlNullabilityProcessor.cs | 25 +++- src/EFCore.Relational/Query/SqlTreePruner.cs | 117 ++++++++++++++++++ .../PrimitiveCollectionsQueryCosmosTest.cs | 7 ++ .../PrimitiveCollectionsQueryTestBase.cs | 10 ++ .../AdHocMiscellaneousQuerySqlServerTest.cs | 4 +- .../Query/GearsOfWarQuerySqlServerTest.cs | 2 +- ...dPrimitiveCollectionsQuerySqlServerTest.cs | 4 +- ...imitiveCollectionsQueryOldSqlServerTest.cs | 26 +++- ...imitiveCollectionsQuerySqlServer160Test.cs | 20 ++- ...veCollectionsQuerySqlServerJsonTypeTest.cs | 20 ++- .../PrimitiveCollectionsQuerySqlServerTest.cs | 20 ++- .../Query/TPCGearsOfWarQuerySqlServerTest.cs | 2 +- .../Query/TPTGearsOfWarQuerySqlServerTest.cs | 2 +- .../TemporalGearsOfWarQuerySqlServerTest.cs | 2 +- .../AdHocMiscellaneousQuerySqliteTest.cs | 4 +- .../Query/GearsOfWarQuerySqliteTest.cs | 2 +- ...aredPrimitiveCollectionsQuerySqliteTest.cs | 4 +- .../PrimitiveCollectionsQuerySqliteTest.cs | 20 ++- 21 files changed, 265 insertions(+), 70 deletions(-) diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 48f63e6c20c..2c9e9b6853d 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -420,8 +420,7 @@ ParameterTranslationMode.Constant or ParameterTranslationMode.MultipleParameters var sqlExpression = sqlExpressions[i]; rowExpressions[i] = new RowValueExpression( - new[] - { + [ // Since VALUES may not guarantee row ordering, we add an _ord value by which we'll order. _sqlExpressionFactory.Constant(i, intTypeMapping), // If no type mapping was inferred (i.e. no column in the inline collection), it's left null, to allow it to get @@ -431,11 +430,11 @@ ParameterTranslationMode.Constant or ParameterTranslationMode.MultipleParameters sqlExpression.TypeMapping is null && inferredTypeMaping is not null ? _sqlExpressionFactory.ApplyTypeMapping(sqlExpression, inferredTypeMaping) : sqlExpression - }); + ]); } var alias = _sqlAliasManager.GenerateTableAlias("values"); - var valuesExpression = new ValuesExpression(alias, rowExpressions, new[] { ValuesOrderingColumnName, ValuesValueColumnName }); + var valuesExpression = new ValuesExpression(alias, rowExpressions, [ValuesOrderingColumnName, ValuesValueColumnName]); return CreateShapedQueryExpressionForValuesExpression( valuesExpression, diff --git a/src/EFCore.Relational/Query/RelationalTypeMappingPostprocessor.cs b/src/EFCore.Relational/Query/RelationalTypeMappingPostprocessor.cs index 926e75ce5f5..e2851f99d60 100644 --- a/src/EFCore.Relational/Query/RelationalTypeMappingPostprocessor.cs +++ b/src/EFCore.Relational/Query/RelationalTypeMappingPostprocessor.cs @@ -106,16 +106,7 @@ when TryGetInferredTypeMapping(columnExpression.TableAlias, columnExpression.Nam 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, stripping the ordering 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: RelationalQueryableMethodTranslatingExpressionVisitor.ValuesOrderingColumnName - } c - && c.TableAlias == valuesExpression.Alias)); + return ApplyTypeMappingsOnValuesExpression(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. @@ -135,8 +126,7 @@ when TryGetInferredTypeMapping(columnExpression.TableAlias, columnExpression.Nam /// 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) + protected virtual ValuesExpression ApplyTypeMappingsOnValuesExpression(ValuesExpression valuesExpression) { var inferredTypeMappings = TryGetInferredTypeMapping( valuesExpression.Alias, RelationalQueryableMethodTranslatingExpressionVisitor.ValuesValueColumnName, out var typeMapping) @@ -146,9 +136,6 @@ protected virtual ValuesExpression ApplyTypeMappingsOnValuesExpression(ValuesExp Check.DebugAssert( valuesExpression.ColumnNames[0] == RelationalQueryableMethodTranslatingExpressionVisitor.ValuesOrderingColumnName, "First ValuesExpression column isn't the ordering column"); - var newColumnNames = stripOrdering - ? valuesExpression.ColumnNames.Skip(1).ToArray() - : valuesExpression.ColumnNames; switch (valuesExpression) { @@ -159,14 +146,9 @@ protected virtual ValuesExpression ApplyTypeMappingsOnValuesExpression(ValuesExp for (var i = 0; i < newRowValues.Length; i++) { var rowValue = rowValues[i]; - var newValues = new SqlExpression[newColumnNames.Count]; + var newValues = new SqlExpression[valuesExpression.ColumnNames.Count]; for (var j = 0; j < valuesExpression.ColumnNames.Count; j++) { - if (j == 0 && stripOrdering) - { - continue; - } - var value = rowValue.Values[j]; if (value.TypeMapping is null @@ -182,13 +164,13 @@ protected virtual ValuesExpression ApplyTypeMappingsOnValuesExpression(ValuesExp value = new SqlUnaryExpression(ExpressionType.Convert, value, value.Type, value.TypeMapping); } - newValues[j - (stripOrdering ? 1 : 0)] = value; + newValues[j] = value; } newRowValues[i] = new RowValueExpression(newValues); } - return new ValuesExpression(valuesExpression.Alias, newRowValues, null, newColumnNames); + return valuesExpression.Update(newRowValues); } // VALUES over a values parameter (i.e. a parameter representing the entire collection, that will be constantized into the SQL @@ -203,10 +185,7 @@ protected virtual ValuesExpression ApplyTypeMappingsOnValuesExpression(ValuesExp throw new UnreachableException("A RelationalTypeMapping collection type mapping could not be found"); } - return new ValuesExpression( - valuesExpression.Alias, - (SqlParameterExpression)valuesParameter.ApplyTypeMapping(collectionParameterTypeMapping), - newColumnNames); + return valuesExpression.Update(valuesParameter.ApplyTypeMapping(collectionParameterTypeMapping)); } default: diff --git a/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs index a279f21889f..447fe6daf3c 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs @@ -74,8 +74,8 @@ public SqlParameterExpression( /// /// A relational type mapping to apply. /// A new expression which has supplied type mapping. - public SqlExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping) - => new SqlParameterExpression(InvariantName, Name, Type, IsNullable, TranslationMode, typeMapping); + public SqlParameterExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping) + => new(InvariantName, Name, Type, IsNullable, TranslationMode, typeMapping); /// protected override Expression VisitChildren(ExpressionVisitor visitor) diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index 7d828280c86..644ebeb3438 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -122,7 +122,7 @@ protected override Expression VisitExtension(Expression node) var intTypeMapping = (IntTypeMapping?)Dependencies.TypeMappingSource.FindMapping(typeof(int)); Check.DebugAssert(intTypeMapping is not null); - var valuesOrderingCounter = 1; + var valuesOrderingCounter = 0; var processedValues = new List(); @@ -151,13 +151,26 @@ protected override Expression VisitExtension(Expression node) case ParameterTranslationMode.Constant: { - foreach (var value in values) + for (var i = 0; i < values.Count; i++) { + var value = _sqlExpressionFactory.Constant( + values[i], + values[i]?.GetType() ?? typeof(object), + sensitive: true, + elementTypeMapping); + + // We currently add explicit conversions on the first row (but not to the _ord column), to ensure that the inferred + // types are properly typed. See #30605 for removing that when not needed. + if (i == 0) + { + value = new SqlUnaryExpression(ExpressionType.Convert, value, value.Type, value.TypeMapping); + } + processedValues.Add( new RowValueExpression( ProcessValuesOrderingColumn( valuesExpression, - [_sqlExpressionFactory.Constant(value, value?.GetType() ?? typeof(object), sensitive: true, elementTypeMapping)], + [value], intTypeMapping, ref valuesOrderingCounter))); } @@ -1497,9 +1510,11 @@ protected virtual SqlExpression VisitJsonScalar( bool allowOptimizedExpansion, out bool nullable) { - nullable = jsonScalarExpression.IsNullable; + var json = Visit(jsonScalarExpression.Json, out var jsonNullable); + + nullable = jsonNullable || jsonScalarExpression.IsNullable; - return jsonScalarExpression; + return jsonScalarExpression.Update(json); } /// diff --git a/src/EFCore.Relational/Query/SqlTreePruner.cs b/src/EFCore.Relational/Query/SqlTreePruner.cs index bc80eac58b3..27c8e0301ed 100644 --- a/src/EFCore.Relational/Query/SqlTreePruner.cs +++ b/src/EFCore.Relational/Query/SqlTreePruner.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 System.Collections; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; namespace Microsoft.EntityFrameworkCore.Query; @@ -117,6 +118,9 @@ protected override Expression VisitExtension(Expression node) PruneSelect(source2, preserveProjection: true)); } + case ValuesExpression values: + return PruneValues(values); + default: return base.VisitExtension(node); } @@ -262,4 +266,117 @@ protected virtual SelectExpression PruneSelect(SelectExpression select, bool pre return select.Update( tables ?? select.Tables, predicate, groupBy, having, projections ?? select.Projection, orderings, offset, limit); } + + /// + /// Prunes a , removing columns inside it which aren't referenced. + /// This currently removes the _ord column that gets added to preserve ordering, for cases where + /// that ordering isn't actually necessary. + /// + /// + /// 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] + protected virtual ValuesExpression PruneValues(ValuesExpression values) + { + BitArray? referencedColumns = null; + List? newColumnNames = null; + + if (ReferencedColumnMap.TryGetValue(values.Alias, out var referencedColumnNames)) + { + // First, build a bitmap of which columns are referenced which we can efficiently access later + // as we traverse the rows. At the same time, build a list of the names of the referenced columns. + for (var i = 0; i < values.ColumnNames.Count; i++) + { + var columnName = values.ColumnNames[i]; + var isColumnReferenced = referencedColumnNames.Contains(columnName); + + if (newColumnNames is null && !isColumnReferenced) + { + newColumnNames = new List(values.ColumnNames.Count); + referencedColumns = new BitArray(values.ColumnNames.Count); + + for (var j = 0; j < i; j++) + { + referencedColumns[j] = true; + newColumnNames.Add(columnName); + } + } + + if (newColumnNames is not null) + { + if (isColumnReferenced) + { + newColumnNames.Add(columnName); + } + + referencedColumns![i] = isColumnReferenced; + } + } + } + else + { + // No columns were referenced at all on this ValuesExpression. + // This happens in some edge cases, e.g. there's a simple COUNT(*) over it (and so no specific columns are referenced). + // We can prune all columns but need to leave one, so that the ValuesExpression is still valid. + // Pick the first column, unless it happens to be the _ord column, in which case we pick the second one. + referencedColumns = new BitArray(values.ColumnNames.Count); + newColumnNames = new List(1); + + if (values.ColumnNames[0] is RelationalQueryableMethodTranslatingExpressionVisitor.ValuesOrderingColumnName) + { + referencedColumns[1] = true; + newColumnNames.Add(values.ColumnNames[1]); + } + else + { + referencedColumns[0] = true; + newColumnNames.Add(values.ColumnNames[0]); + } + } + + if (referencedColumns is null) + { + return values; + } + + // We know at least some columns are getting pruned. + Debug.Assert(newColumnNames is not null); + + switch (values) + { + // If we have a value parameter (row values aren't specific in line), we still prune the column names. + // Later in SqlNullabilityProcessor, when the parameterized collection is inline to constants, we'll take + // the column names into account. + case ValuesExpression { ValuesParameter: not null }: + return new ValuesExpression(values.Alias, rowValues: null, values.ValuesParameter, newColumnNames); + + // Go over the rows and create new ones without the pruned columns. + case ValuesExpression { RowValues: IReadOnlyList rowValues }: + var newRowValues = new RowValueExpression[rowValues.Count]; + + for (var i = 0; i < rowValues.Count; i++) + { + var oldValues = rowValues[i].Values; + var newValues = new List(newColumnNames.Count); + + for (var j = 0; j < values.ColumnNames.Count; j++) + { + if (referencedColumns[j]) + { + newValues.Add(oldValues[j]); + } + } + + newRowValues[i] = new RowValueExpression(newValues); + } + + return new ValuesExpression(values.Alias, newRowValues, valuesParameter: null, newColumnNames); + + default: + throw new UnreachableException(); + } + } } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs index 5cc00ae89ae..bec3d0e4134 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -1020,6 +1020,13 @@ FROM root c """); } + public override async Task Inline_collection_index_Column_with_EF_Constant() + { + var exception = await Assert.ThrowsAsync(() => base.Inline_collection_index_Column_with_EF_Constant()); + + Assert.Equal(CoreStrings.EFConstantNotSupported, exception.Message); + } + public override async Task Inline_collection_value_index_Column() { // Member indexer (c.Array[c.SomeMember]) isn't supported by Cosmos diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index 1cdc5d16e96..9d47e240d0b 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -680,6 +680,16 @@ public virtual Task Inline_collection_index_Column() 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)); + [ConditionalFact] + public virtual Task Inline_collection_index_Column_with_EF_Constant() + { + int[] ints = [1, 2, 3]; + + return AssertQuery( + ss => ss.Set().Where(c => EF.Constant(ints)[c.Int] == 1), + ss => ss.Set().Where(c => (c.Int <= 2 ? ints[c.Int] : -1) == 1)); + } + [ConditionalFact] public virtual Task Inline_collection_value_index_Column() => AssertQuery( diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs index e5d2742eccd..9d4e2768fc0 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs @@ -2604,7 +2604,7 @@ WHERE [t].[Id] IN (?, ?, ?) FROM [TestEntities] AS [t] WHERE EXISTS ( SELECT 1 - FROM (VALUES (?), (?), (?)) AS [i]([Value]) + FROM (VALUES (CAST(? AS int)), (?), (?)) AS [i]([Value]) WHERE [i].[Value] = [t].[Id]) """, // @@ -2628,7 +2628,7 @@ WHERE [t].[Id] IN (1, 2, 3) FROM [TestEntities] AS [t] WHERE EXISTS ( SELECT 1 - FROM (VALUES (1), (2), (3)) AS [i]([Value]) + FROM (VALUES (CAST(1 AS int)), (2), (3)) AS [i]([Value]) WHERE [i].[Value] = [t].[Id]) """, // diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index 5dd4ae31322..71867841ae7 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -9194,7 +9194,7 @@ FROM [Weapons] AS [w] ) AS [w0] WHERE [w0].[row] <= ISNULL(( SELECT [n].[Value] - FROM (VALUES (1, @numbers1), (2, @numbers2), (3, @numbers3)) AS [n]([_ord], [Value]) + FROM (VALUES (@numbers1), (@numbers2), (@numbers3)) AS [n]([Value]) ORDER BY [n].[Value] OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY), 0) ) AS [w1] ON [g].[FullName] = [w1].[OwnerFullName] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs index 865b66dabb5..50d83b6bee7 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs @@ -799,7 +799,7 @@ SELECT [t].[Id] FROM [TestEntity] AS [t] WHERE ( SELECT COUNT(*) - FROM (VALUES (2), (999)) AS [i]([Value]) + FROM (VALUES (CAST(2 AS int)), (999)) AS [i]([Value]) WHERE [i].[Value] > [t].[Id]) = 1 """); break; @@ -905,7 +905,7 @@ SELECT [t].[Id] FROM [TestEntity] AS [t] WHERE ( SELECT COUNT(*) - FROM (VALUES (2), (999)) AS [i]([Value]) + FROM (VALUES (CAST(2 AS int)), (999)) AS [i]([Value]) WHERE [i].[Value] > [t].[Id]) = 1 """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs index 4d85180064d..52d1624599e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -790,7 +790,7 @@ public override async Task Parameter_collection_Where_with_EF_Constant_Where_Any FROM [PrimitiveCollectionsEntity] AS [p] WHERE EXISTS ( SELECT 1 - FROM (VALUES (2), (999), (1000)) AS [i]([Value]) + FROM (VALUES (CAST(2 AS int)), (999), (1000)) AS [i]([Value]) WHERE [i].[Value] > 0) """); } @@ -805,7 +805,7 @@ public override async Task Parameter_collection_Count_with_column_predicate_with FROM [PrimitiveCollectionsEntity] AS [p] WHERE ( SELECT COUNT(*) - FROM (VALUES (2), (999), (1000)) AS [i]([Value]) + FROM (VALUES (CAST(2 AS int)), (999), (1000)) AS [i]([Value]) WHERE [i].[Value] > [p].[Id]) = 2 """); } @@ -900,6 +900,22 @@ ORDER BY [v].[_ord] """); } + public override async Task Inline_collection_index_Column_with_EF_Constant() + { + await base.Inline_collection_index_Column_with_EF_Constant(); + + 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].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT [i].[Value] + FROM (VALUES (0, CAST(1 AS int)), (1, 2), (2, 3)) AS [i]([_ord], [Value]) + ORDER BY [i].[_ord] + OFFSET [p].[Int] ROWS FETCH NEXT 1 ROWS ONLY) = 1 +"""); + } + public override async Task Inline_collection_value_index_Column() { await base.Inline_collection_value_index_Column(); @@ -946,7 +962,7 @@ public override async Task Parameter_collection_index_Column_equal_Column() FROM [PrimitiveCollectionsEntity] AS [p] WHERE ( SELECT [i].[Value] - FROM (VALUES (1, @ints1), (2, @ints2), (3, @ints3)) AS [i]([_ord], [Value]) + FROM (VALUES (0, @ints1), (1, @ints2), (2, @ints3)) AS [i]([_ord], [Value]) ORDER BY [i].[_ord] OFFSET [p].[Int] ROWS FETCH NEXT 1 ROWS ONLY) = [p].[Int] """); @@ -966,7 +982,7 @@ public override async Task Parameter_collection_index_Column_equal_constant() FROM [PrimitiveCollectionsEntity] AS [p] WHERE ( SELECT [i].[Value] - FROM (VALUES (1, @ints1), (2, @ints2), (3, @ints3)) AS [i]([_ord], [Value]) + FROM (VALUES (0, @ints1), (1, @ints2), (2, @ints3)) AS [i]([_ord], [Value]) ORDER BY [i].[_ord] OFFSET [p].[Int] ROWS FETCH NEXT 1 ROWS ONLY) = 1 """); @@ -1141,7 +1157,7 @@ FROM [PrimitiveCollectionsEntity] AS [p] SELECT COUNT(*) FROM ( SELECT [i].[Value] AS [Value0] - FROM (VALUES (1, @ints1), (2, @ints2)) AS [i]([_ord], [Value]) + FROM (VALUES (0, @ints1), (1, @ints2)) AS [i]([_ord], [Value]) ORDER BY [i].[_ord] OFFSET 1 ROWS ) AS [i0] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs index 061226598ea..c11fb0aac04 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs @@ -784,7 +784,7 @@ public override async Task Parameter_collection_Where_with_EF_Constant_Where_Any FROM [PrimitiveCollectionsEntity] AS [p] WHERE EXISTS ( SELECT 1 - FROM (VALUES (2), (999), (1000)) AS [i]([Value]) + FROM (VALUES (CAST(2 AS int)), (999), (1000)) AS [i]([Value]) WHERE [i].[Value] > 0) """); } @@ -799,7 +799,7 @@ public override async Task Parameter_collection_Count_with_column_predicate_with FROM [PrimitiveCollectionsEntity] AS [p] WHERE ( SELECT COUNT(*) - FROM (VALUES (2), (999), (1000)) AS [i]([Value]) + FROM (VALUES (CAST(2 AS int)), (999), (1000)) AS [i]([Value]) WHERE [i].[Value] > [p].[Id]) = 2 """); } @@ -1066,6 +1066,18 @@ ORDER BY [v].[_ord] """); } + public override async Task Inline_collection_index_Column_with_EF_Constant() + { + await base.Inline_collection_index_Column_with_EF_Constant(); + + 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].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE CAST(JSON_VALUE(N'[1,2,3]', '$[' + CAST([p].[Int] AS nvarchar(max)) + ']') AS int) = 1 +"""); + } + public override async Task Inline_collection_value_index_Column() { await base.Inline_collection_value_index_Column(); @@ -1664,7 +1676,7 @@ SELECT COUNT(*) SELECT [i1].[Value] FROM ( SELECT [i].[Value] - FROM (VALUES (1, @ints1), (2, @ints2)) AS [i]([_ord], [Value]) + FROM (VALUES (0, @ints1), (1, @ints2)) AS [i]([_ord], [Value]) ORDER BY [i].[_ord] OFFSET 1 ROWS ) AS [i1] @@ -1754,7 +1766,7 @@ FROM [PrimitiveCollectionsEntity] AS [p] SELECT COUNT(*) FROM ( SELECT [i].[Value] AS [Value0] - FROM (VALUES (1, @ints1), (2, @ints2)) AS [i]([_ord], [Value]) + FROM (VALUES (0, @ints1), (1, @ints2)) AS [i]([_ord], [Value]) ORDER BY [i].[_ord] OFFSET 1 ROWS ) AS [i0] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs index 540a875ecd7..f2f1a5425d4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs @@ -73,11 +73,11 @@ public override async Task Parameter_collection_Where_with_EF_Constant_Where_Any 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].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] FROM [PrimitiveCollectionsEntity] AS [p] WHERE EXISTS ( SELECT 1 - FROM (VALUES (2), (999), (1000)) AS [i]([Value]) + FROM (VALUES (CAST(2 AS int)), (999), (1000)) AS [i]([Value]) WHERE [i].[Value] > 0) """); } @@ -1095,6 +1095,22 @@ ORDER BY [v].[_ord] """); } + public override async Task Inline_collection_index_Column_with_EF_Constant() + { + await base.Inline_collection_index_Column_with_EF_Constant(); + + 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].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT [i].[Value] + FROM (VALUES (0, CAST(1 AS int)), (1, 2), (2, 3)) AS [i]([_ord], [Value]) + ORDER BY [i].[_ord] + OFFSET [p].[Int] ROWS FETCH NEXT 1 ROWS ONLY) = 1 +"""); + } + public override async Task Inline_collection_value_index_Column() { await base.Inline_collection_value_index_Column(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index 597ee0976cd..0583fd68035 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -807,7 +807,7 @@ public override async Task Parameter_collection_Where_with_EF_Constant_Where_Any FROM [PrimitiveCollectionsEntity] AS [p] WHERE EXISTS ( SELECT 1 - FROM (VALUES (2), (999), (1000)) AS [i]([Value]) + FROM (VALUES (CAST(2 AS int)), (999), (1000)) AS [i]([Value]) WHERE [i].[Value] > 0) """); } @@ -822,7 +822,7 @@ public override async Task Parameter_collection_Count_with_column_predicate_with FROM [PrimitiveCollectionsEntity] AS [p] WHERE ( SELECT COUNT(*) - FROM (VALUES (2), (999), (1000)) AS [i]([Value]) + FROM (VALUES (CAST(2 AS int)), (999), (1000)) AS [i]([Value]) WHERE [i].[Value] > [p].[Id]) = 2 """); } @@ -1089,6 +1089,18 @@ ORDER BY [v].[_ord] """); } + public override async Task Inline_collection_index_Column_with_EF_Constant() + { + await base.Inline_collection_index_Column_with_EF_Constant(); + + 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].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE CAST(JSON_VALUE(N'[1,2,3]', '$[' + CAST([p].[Int] AS nvarchar(max)) + ']') AS int) = 1 +"""); + } + public override async Task Inline_collection_value_index_Column() { await base.Inline_collection_value_index_Column(); @@ -1688,7 +1700,7 @@ SELECT COUNT(*) SELECT [i1].[Value] FROM ( SELECT [i].[Value] - FROM (VALUES (1, @ints1), (2, @ints2)) AS [i]([_ord], [Value]) + FROM (VALUES (0, @ints1), (1, @ints2)) AS [i]([_ord], [Value]) ORDER BY [i].[_ord] OFFSET 1 ROWS ) AS [i1] @@ -1778,7 +1790,7 @@ FROM [PrimitiveCollectionsEntity] AS [p] SELECT COUNT(*) FROM ( SELECT [i].[Value] AS [Value0] - FROM (VALUES (1, @ints1), (2, @ints2)) AS [i]([_ord], [Value]) + FROM (VALUES (0, @ints1), (1, @ints2)) AS [i]([_ord], [Value]) ORDER BY [i].[_ord] OFFSET 1 ROWS ) AS [i0] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs index f1c31a3f0a0..5f392ce814e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs @@ -12271,7 +12271,7 @@ FROM [Weapons] AS [w] ) AS [w0] WHERE [w0].[row] <= ISNULL(( SELECT [n].[Value] - FROM (VALUES (1, @numbers1), (2, @numbers2), (3, @numbers3)) AS [n]([_ord], [Value]) + FROM (VALUES (@numbers1), (@numbers2), (@numbers3)) AS [n]([Value]) ORDER BY [n].[Value] OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY), 0) ) AS [w1] ON [u].[FullName] = [w1].[OwnerFullName] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index 0bb1721925f..c6fbaa27acc 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -10412,7 +10412,7 @@ FROM [Weapons] AS [w] ) AS [w0] WHERE [w0].[row] <= ISNULL(( SELECT [n].[Value] - FROM (VALUES (1, @numbers1), (2, @numbers2), (3, @numbers3)) AS [n]([_ord], [Value]) + FROM (VALUES (@numbers1), (@numbers2), (@numbers3)) AS [n]([Value]) ORDER BY [n].[Value] OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY), 0) ) AS [w1] ON [g].[FullName] = [w1].[OwnerFullName] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs index bf8f65d8133..d02c75168d9 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs @@ -9086,7 +9086,7 @@ LEFT JOIN ( ) AS [w0] WHERE [w0].[row] <= ISNULL(( SELECT [n].[Value] - FROM (VALUES (1, @numbers1), (2, @numbers2), (3, @numbers3)) AS [n]([_ord], [Value]) + FROM (VALUES (@numbers1), (@numbers2), (@numbers3)) AS [n]([Value]) ORDER BY [n].[Value] OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY), 0) ) AS [w1] ON [g].[FullName] = [w1].[OwnerFullName] diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/AdHocMiscellaneousQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/AdHocMiscellaneousQuerySqliteTest.cs index f80084d99e5..196afbe559c 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/AdHocMiscellaneousQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/AdHocMiscellaneousQuerySqliteTest.cs @@ -108,7 +108,7 @@ public override async Task Check_inlined_constants_redacting(bool async, bool en FROM "TestEntities" AS "t" WHERE EXISTS ( SELECT 1 - FROM (SELECT ? AS "Value" UNION ALL VALUES (?), (?)) AS "i" + FROM (SELECT CAST(? AS INTEGER) AS "Value" UNION ALL VALUES (?), (?)) AS "i" WHERE "i"."Value" = "t"."Id") """, // @@ -132,7 +132,7 @@ SELECT 1 FROM "TestEntities" AS "t" WHERE EXISTS ( SELECT 1 - FROM (SELECT 1 AS "Value" UNION ALL VALUES (2), (3)) AS "i" + FROM (SELECT CAST(1 AS INTEGER) AS "Value" UNION ALL VALUES (2), (3)) AS "i" WHERE "i"."Value" = "t"."Id") """, // diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs index 728045eb5c2..718a6af99a1 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs @@ -8750,7 +8750,7 @@ LEFT JOIN ( ) AS "w0" WHERE "w0"."row" <= COALESCE(( SELECT "n"."Value" - FROM (SELECT 1 AS "_ord", @numbers1 AS "Value" UNION ALL VALUES (2, @numbers2), (3, @numbers3)) AS "n" + FROM (SELECT @numbers1 AS "Value" UNION ALL VALUES (@numbers2), (@numbers3)) AS "n" ORDER BY "n"."Value" LIMIT 1 OFFSET 1), 0) ) AS "w1" ON "g"."FullName" = "w1"."OwnerFullName" diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs index 2ec5a9e8044..be157b31860 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs @@ -343,7 +343,7 @@ public override async Task Parameter_collection_Count_with_column_predicate_with FROM "TestEntity" AS "t" WHERE ( SELECT COUNT(*) - FROM (SELECT 2 AS "Value" UNION ALL VALUES (999)) AS "i" + FROM (SELECT CAST(2 AS INTEGER) AS "Value" UNION ALL VALUES (999)) AS "i" WHERE "i"."Value" > "t"."Id") = 1 """); break; @@ -449,7 +449,7 @@ public override async Task Parameter_collection_Count_with_column_predicate_with FROM "TestEntity" AS "t" WHERE ( SELECT COUNT(*) - FROM (SELECT 2 AS "Value" UNION ALL VALUES (999)) AS "i" + FROM (SELECT CAST(2 AS INTEGER) AS "Value" UNION ALL VALUES (999)) AS "i" WHERE "i"."Value" > "t"."Id") = 1 """); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs index fb1ae264fae..6f92d944a02 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -795,7 +795,7 @@ public override async Task Parameter_collection_Where_with_EF_Constant_Where_Any FROM "PrimitiveCollectionsEntity" AS "p" WHERE EXISTS ( SELECT 1 - FROM (SELECT 2 AS "Value" UNION ALL VALUES (999), (1000)) AS "i" + FROM (SELECT CAST(2 AS INTEGER) AS "Value" UNION ALL VALUES (999), (1000)) AS "i" WHERE "i"."Value" > 0) """); } @@ -810,7 +810,7 @@ public override async Task Parameter_collection_Count_with_column_predicate_with FROM "PrimitiveCollectionsEntity" AS "p" WHERE ( SELECT COUNT(*) - FROM (SELECT 2 AS "Value" UNION ALL VALUES (999), (1000)) AS "i" + FROM (SELECT CAST(2 AS INTEGER) AS "Value" UNION ALL VALUES (999), (1000)) AS "i" WHERE "i"."Value" > "p"."Id") = 2 """); } @@ -1054,6 +1054,18 @@ ORDER BY "v"."_ord" """); } + public override async Task Inline_collection_index_Column_with_EF_Constant() + { + await base.Inline_collection_index_Column_with_EF_Constant(); + + 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"."NullableString", "p"."NullableStrings", "p"."NullableWrappedId", "p"."NullableWrappedIdWithNullableComparer", "p"."String", "p"."Strings", "p"."WrappedId" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE '[1,2,3]' ->> "p"."Int" = 1 +"""); + } + public override async Task Inline_collection_value_index_Column() { // SQLite doesn't support correlated subqueries where the outer column is used as the LIMIT/OFFSET (see OFFSET "p"."Int" below) @@ -1639,7 +1651,7 @@ SELECT COUNT(*) SELECT COUNT(*) FROM ( SELECT "i"."Value" AS "Value0" - FROM (SELECT 1 AS "_ord", @ints1 AS "Value" UNION ALL VALUES (2, @ints2)) AS "i" + FROM (SELECT 0 AS "_ord", @ints1 AS "Value" UNION ALL VALUES (1, @ints2)) AS "i" ORDER BY "i"."_ord" LIMIT -1 OFFSET 1 ) AS "i0" @@ -1671,7 +1683,7 @@ SELECT COUNT(*) SELECT "i1"."Value" FROM ( SELECT "i"."Value" - FROM (SELECT 1 AS "_ord", @ints1 AS "Value" UNION ALL VALUES (2, @ints2)) AS "i" + FROM (SELECT 0 AS "_ord", @ints1 AS "Value" UNION ALL VALUES (1, @ints2)) AS "i" ORDER BY "i"."_ord" LIMIT -1 OFFSET 1 ) AS "i1"