diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 848973552c6..7203bd2ddd1 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -405,8 +405,7 @@ JsonScalarExpression jsonScalar 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 @@ -416,11 +415,11 @@ JsonScalarExpression jsonScalar 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..0f3862456b3 100644 --- a/src/EFCore.Relational/Query/RelationalTypeMappingPostprocessor.cs +++ b/src/EFCore.Relational/Query/RelationalTypeMappingPostprocessor.cs @@ -107,15 +107,7 @@ when TryGetInferredTypeMapping(columnExpression.TableAlias, columnExpression.Nam // 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 +127,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 +137,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 +147,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 +165,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 +186,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 48c1602fbaa..bf9de0d0db4 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, ShouldBeConstantized, typeMapping); + public SqlParameterExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping) + => new(InvariantName, Name, Type, IsNullable, ShouldBeConstantized, typeMapping); /// protected override Expression VisitChildren(ExpressionVisitor visitor) diff --git a/src/EFCore.Relational/Query/SqlTreePruner.cs b/src/EFCore.Relational/Query/SqlTreePruner.cs index bc80eac58b3..756aaea423c 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,98 @@ 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) + { + if (!ReferencedColumnMap.TryGetValue(values.Alias, out var referencedColumnNames)) + { + throw new UnreachableException("The case of a totally unreferenced ValuesExpression is already handled for all tables in PruneSelect."); + } + + // 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. + BitArray? referencedColumns = null; + List? newColumnNames = null; + 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; + } + } + + 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 6b09305e63d..3cff348a854 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -1158,6 +1158,17 @@ FROM root c } } + public override async Task Inline_collection_index_Column_with_EF_Constant(bool async) + { + // Always throws for sync. + if (async) + { + var exception = await Assert.ThrowsAsync(() => base.Inline_collection_index_Column_with_EF_Constant(async)); + + Assert.Equal(CoreStrings.EFConstantNotSupported, exception.Message); + } + } + public override async Task Inline_collection_value_index_Column(bool async) { // Always throws for sync. diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index 6c4b2596051..fb57278b48e 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -849,6 +849,18 @@ public virtual Task Inline_collection_index_Column(bool 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)); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Inline_collection_index_Column_with_EF_Constant(bool async) + { + int[] ints = [1, 2, 3]; + + return AssertQuery( + async, + ss => ss.Set().Where(c => EF.Constant(ints)[c.Int] == 1), + ss => ss.Set().Where(c => (c.Int <= 2 ? ints[c.Int] : -1) == 1)); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Inline_collection_value_index_Column(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs index cc0a312389c..bb88af3dad2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs @@ -2505,7 +2505,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]) """, // @@ -2529,7 +2529,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/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs index 3016f7c7b93..ec2060c523e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs @@ -795,7 +795,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 """); } @@ -890,7 +890,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 e2677da66c7..4c8e1ac47bd 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -791,7 +791,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) """); } @@ -806,7 +806,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 """); } @@ -901,6 +901,22 @@ ORDER BY [v].[_ord] """); } + public override async Task Inline_collection_index_Column_with_EF_Constant(bool async) + { + await base.Inline_collection_index_Column_with_EF_Constant(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].[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(bool async) { await base.Inline_collection_value_index_Column(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs index b57e0e025c2..c4046172e8c 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,22 @@ ORDER BY [v].[_ord] """); } + public override async Task Inline_collection_index_Column_with_EF_Constant(bool async) + { + await base.Inline_collection_index_Column_with_EF_Constant(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].[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(bool async) { await base.Inline_collection_value_index_Column(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs index 338bad1c1ff..23be1be6d54 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(bool async) + { + await base.Inline_collection_index_Column_with_EF_Constant(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].[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(bool async) { await base.Inline_collection_value_index_Column(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index c0164a1fe15..279b5910ce4 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,22 @@ ORDER BY [v].[_ord] """); } + public override async Task Inline_collection_index_Column_with_EF_Constant(bool async) + { + await base.Inline_collection_index_Column_with_EF_Constant(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].[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(bool async) { await base.Inline_collection_value_index_Column(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/AdHocMiscellaneousQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/AdHocMiscellaneousQuerySqliteTest.cs index e878daf1ff8..40c7640c0a1 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/NonSharedPrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs index fbddf89fa4f..f444383ffac 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs @@ -339,7 +339,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 """); } @@ -434,7 +434,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 cc161920c78..e9194f6b969 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,23 @@ ORDER BY "v"."_ord" """); } + public override async Task Inline_collection_index_Column_with_EF_Constant(bool async) + { + // SQLite doesn't support correlated subqueries where the outer column is used as the LIMIT/OFFSET (see OFFSET "p"."Int" below) + await Assert.ThrowsAsync(() => base.Inline_collection_index_Column_with_EF_Constant(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"."NullableString", "p"."NullableStrings", "p"."NullableWrappedId", "p"."NullableWrappedIdWithNullableComparer", "p"."String", "p"."Strings", "p"."WrappedId" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE ( + SELECT "i"."Value" + FROM (SELECT 0 AS "_ord", CAST(1 AS INTEGER) AS "Value" UNION ALL VALUES (1, 2), (2, 3)) AS "i" + ORDER BY "i"."_ord" + LIMIT 1 OFFSET "p"."Int") = 1 +"""); + } + public override async Task Inline_collection_value_index_Column(bool async) { // SQLite doesn't support correlated subqueries where the outer column is used as the LIMIT/OFFSET (see OFFSET "p"."Int" below)