diff --git a/src/EFCore.Jet.Data/JetCommand.cs b/src/EFCore.Jet.Data/JetCommand.cs index 92e6c9b..21bc9b9 100644 --- a/src/EFCore.Jet.Data/JetCommand.cs +++ b/src/EFCore.Jet.Data/JetCommand.cs @@ -554,12 +554,11 @@ namespace EntityFrameworkCore.Jet.Data // in TOP clauses. var parameters = InnerCommand.Parameters.Cast() .ToList(); + var lastCommandText = InnerCommand.CommandText; + var commandText = lastCommandText; if (parameters.Count > 0) { - var lastCommandText = InnerCommand.CommandText; - var commandText = lastCommandText; - while ((commandText = _topMultiParameterRegularExpression.Replace( lastCommandText, match => @@ -587,24 +586,23 @@ namespace EntityFrameworkCore.Jet.Data lastCommandText = commandText; } - while ((commandText = _topMultiArgumentRegularExpression.Replace( - lastCommandText, - match => - { - var first = match.Groups["first"]; - var sec = match.Groups["sec"]; - return (Convert.ToInt32(first.Value) + Convert.ToInt32(sec.Value)).ToString(); - }, - 1)) != lastCommandText) - { - lastCommandText = commandText; - } - - InnerCommand.CommandText = commandText; - InnerCommand.Parameters.Clear(); InnerCommand.Parameters.AddRange(parameters.ToArray()); } + while ((commandText = _topMultiArgumentRegularExpression.Replace( + lastCommandText, + match => + { + var first = match.Groups["first"]; + var sec = match.Groups["sec"]; + return (Convert.ToInt32(first.Value) + Convert.ToInt32(sec.Value)).ToString(); + }, + 1)) != lastCommandText) + { + lastCommandText = commandText; + } + + InnerCommand.CommandText = commandText; } private void ModifyOuterSelectTopValueForOuterSelectSkipEmulationViaDataReader() diff --git a/src/EFCore.Jet/Query/Internal/JetParameterBasedSqlProcessor.cs b/src/EFCore.Jet/Query/Internal/JetParameterBasedSqlProcessor.cs index 858f59f..cfd8dc0 100644 --- a/src/EFCore.Jet/Query/Internal/JetParameterBasedSqlProcessor.cs +++ b/src/EFCore.Jet/Query/Internal/JetParameterBasedSqlProcessor.cs @@ -40,19 +40,19 @@ public class JetParameterBasedSqlProcessor : RelationalParameterBasedSqlProcesso IReadOnlyDictionary parametersValues, out bool canCache) { - queryExpression = base.Optimize(queryExpression, parametersValues, out canCache); + var optimizedQueryExpression = base.Optimize(queryExpression, parametersValues, out canCache); - /*optimizedQueryExpression = new SkipTakeCollapsingExpressionVisitor(Dependencies.SqlExpressionFactory) - .Process(optimizedQueryExpression, parametersValues, out var canCache2);*/ + optimizedQueryExpression = new SkipTakeCollapsingExpressionVisitor(Dependencies.SqlExpressionFactory) + .Process(optimizedQueryExpression, parametersValues, out var canCache2); - //canCache &= canCache2; + canCache &= canCache2; - queryExpression = new SearchConditionConvertingExpressionVisitor(Dependencies.SqlExpressionFactory).Visit(queryExpression); + optimizedQueryExpression = new SearchConditionConvertingExpressionVisitor(Dependencies.SqlExpressionFactory).Visit(optimizedQueryExpression); // Run the compatibility checks as late in the query pipeline (before the actual SQL translation happens) as reasonable. - queryExpression = new JetCompatibilityExpressionVisitor().Visit(queryExpression); + optimizedQueryExpression = new JetCompatibilityExpressionVisitor().Visit(optimizedQueryExpression); - return queryExpression; + return optimizedQueryExpression; } /// diff --git a/src/EFCore.Jet/Query/Internal/SkipTakeCollapsingExpressionVisitor.cs b/src/EFCore.Jet/Query/Internal/SkipTakeCollapsingExpressionVisitor.cs new file mode 100644 index 0000000..f2d3113 --- /dev/null +++ b/src/EFCore.Jet/Query/Internal/SkipTakeCollapsingExpressionVisitor.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace EntityFrameworkCore.Jet.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 SkipTakeCollapsingExpressionVisitor : ExpressionVisitor +{ + private readonly ISqlExpressionFactory _sqlExpressionFactory; + + private IReadOnlyDictionary _parameterValues; + private bool _canCache; + + /// + /// 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 SkipTakeCollapsingExpressionVisitor(ISqlExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + _parameterValues = null!; + } + + /// + /// 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 Expression Process( + Expression queryExpression, + IReadOnlyDictionary parametersValues, + out bool canCache) + { + _parameterValues = parametersValues; + _canCache = true; + + var result = Visit(queryExpression); + + canCache = _canCache; + + return result; + } + + /// + /// 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 extensionExpression) + { + if (extensionExpression is SelectExpression selectExpression) + { + if (IsZero(selectExpression.Limit)) + { + var result = selectExpression.Update( + selectExpression.Projection, + selectExpression.Tables, + selectExpression.GroupBy.Count > 0 ? selectExpression.Predicate : _sqlExpressionFactory.Constant(false), + selectExpression.GroupBy, + selectExpression.GroupBy.Count > 0 ? _sqlExpressionFactory.Constant(false) : null, + new List(0), + limit: null, + offset: null); + return base.VisitExtension(result); + } + + bool IsZero(SqlExpression? sqlExpression) + { + switch (sqlExpression) + { + case SqlConstantExpression { Value: int intValue }: + return intValue == 0; + case SqlParameterExpression parameter: + _canCache = false; + return _parameterValues[parameter.Name] is 0; + case SqlBinaryExpression { Left: SqlConstantExpression left, Right: SqlConstantExpression right }: + return left.Value is int leftValue && right.Value is int rightValue && leftValue + rightValue == 0; + case SqlBinaryExpression { Left: SqlParameterExpression left, Right: SqlConstantExpression right }: + _canCache = false; + return _parameterValues[left.Name] is 0 && right.Value is int and 0; + case SqlBinaryExpression { Left: SqlConstantExpression left, Right: SqlParameterExpression right }: + _canCache = false; + return _parameterValues[right.Name] is 0 && left.Value is int and 0; + case SqlBinaryExpression { Left: SqlParameterExpression left, Right: SqlParameterExpression right }: + _canCache = false; + return _parameterValues[left.Name] is 0 && _parameterValues[right.Name] is 0; + default: + return false; + } + } + } + + return base.VisitExtension(extensionExpression); + } +} diff --git a/test/EFCore.Jet.FunctionalTests/GreenTests/ace_2010_odbc_x86.txt b/test/EFCore.Jet.FunctionalTests/GreenTests/ace_2010_odbc_x86.txt index 6bbde7a..762b2af 100644 --- a/test/EFCore.Jet.FunctionalTests/GreenTests/ace_2010_odbc_x86.txt +++ b/test/EFCore.Jet.FunctionalTests/GreenTests/ace_2010_odbc_x86.txt @@ -9156,6 +9156,8 @@ EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindGroupByQueryJetTest.Group EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindGroupByQueryJetTest.GroupBy_after_anonymous_projection_and_distinct_followed_by_another_anonymous_projection(async: True) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindGroupByQueryJetTest.GroupBy_after_predicate_Constant_Select_Sum_Min_Key_Max_Avg(isAsync: False) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindGroupByQueryJetTest.GroupBy_after_predicate_Constant_Select_Sum_Min_Key_Max_Avg(isAsync: True) +EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindGroupByQueryJetTest.GroupBy_aggregate_after_skip_0_take_0(async: False) +EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindGroupByQueryJetTest.GroupBy_aggregate_after_skip_0_take_0(async: True) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindGroupByQueryJetTest.GroupBy_aggregate_Contains(isAsync: False) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindGroupByQueryJetTest.GroupBy_aggregate_Contains(isAsync: True) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindGroupByQueryJetTest.GroupBy_aggregate_followed_another_GroupBy_aggregate(async: False) @@ -10590,6 +10592,10 @@ EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Shaper_command_caching_when_parameter_names_different(isAsync: False) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Shaper_command_caching_when_parameter_names_different(isAsync: True) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Single_Predicate_Cancellation +EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Skip_0_Take_0_works_when_constant(async: False) +EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Skip_0_Take_0_works_when_constant(async: True) +EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Skip_0_Take_0_works_when_parameter(async: False) +EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Skip_0_Take_0_works_when_parameter(async: True) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Skip_Take_All(isAsync: False) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Skip_Take_All(isAsync: True) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Skip_Take_Any_with_predicate(isAsync: False) diff --git a/test/EFCore.Jet.FunctionalTests/GreenTests/ace_2010_oledb_x86.txt b/test/EFCore.Jet.FunctionalTests/GreenTests/ace_2010_oledb_x86.txt index 98885b4..76f7a28 100644 --- a/test/EFCore.Jet.FunctionalTests/GreenTests/ace_2010_oledb_x86.txt +++ b/test/EFCore.Jet.FunctionalTests/GreenTests/ace_2010_oledb_x86.txt @@ -12068,6 +12068,8 @@ EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindGroupByQueryJetTest.Group EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindGroupByQueryJetTest.GroupBy_after_anonymous_projection_and_distinct_followed_by_another_anonymous_projection(async: True) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindGroupByQueryJetTest.GroupBy_after_predicate_Constant_Select_Sum_Min_Key_Max_Avg(isAsync: False) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindGroupByQueryJetTest.GroupBy_after_predicate_Constant_Select_Sum_Min_Key_Max_Avg(isAsync: True) +EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindGroupByQueryJetTest.GroupBy_aggregate_after_skip_0_take_0(async: False) +EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindGroupByQueryJetTest.GroupBy_aggregate_after_skip_0_take_0(async: True) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindGroupByQueryJetTest.GroupBy_aggregate_Contains(isAsync: False) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindGroupByQueryJetTest.GroupBy_aggregate_Contains(isAsync: True) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindGroupByQueryJetTest.GroupBy_aggregate_followed_another_GroupBy_aggregate(async: False) @@ -13522,6 +13524,10 @@ EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Shaper_command_caching_when_parameter_names_different(isAsync: False) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Shaper_command_caching_when_parameter_names_different(isAsync: True) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Single_Predicate_Cancellation +EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Skip_0_Take_0_works_when_constant(async: False) +EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Skip_0_Take_0_works_when_constant(async: True) +EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Skip_0_Take_0_works_when_parameter(async: False) +EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Skip_0_Take_0_works_when_parameter(async: True) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Skip_Take_All(isAsync: False) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Skip_Take_All(isAsync: True) EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindMiscellaneousQueryJetTest.Skip_Take_Any_with_predicate(isAsync: False) diff --git a/test/EFCore.Jet.FunctionalTests/Query/NorthwindGroupByQueryJetTest.cs b/test/EFCore.Jet.FunctionalTests/Query/NorthwindGroupByQueryJetTest.cs index 51d9009..29c879e 100644 --- a/test/EFCore.Jet.FunctionalTests/Query/NorthwindGroupByQueryJetTest.cs +++ b/test/EFCore.Jet.FunctionalTests/Query/NorthwindGroupByQueryJetTest.cs @@ -2758,13 +2758,17 @@ INNER JOIN ( AssertSql( """ -SELECT [t].[CustomerID] AS [Key], COUNT(*) AS [Total] +SELECT `t0`.`CustomerID` AS `Key`, COUNT(*) AS `Total` FROM ( - SELECT [o].[CustomerID] - FROM [Orders] AS [o] + SELECT `t`.`CustomerID` + FROM ( + SELECT `o`.`CustomerID` + FROM `Orders` AS `o` + WHERE 0 = 1 + ) AS `t` WHERE 0 = 1 -) AS [t] -GROUP BY [t].[CustomerID] +) AS `t0` +GROUP BY `t0`.`CustomerID` """); } diff --git a/test/EFCore.Jet.FunctionalTests/Query/NorthwindMiscellaneousQueryJetTest.cs b/test/EFCore.Jet.FunctionalTests/Query/NorthwindMiscellaneousQueryJetTest.cs index d92c0cc..9541a22 100644 --- a/test/EFCore.Jet.FunctionalTests/Query/NorthwindMiscellaneousQueryJetTest.cs +++ b/test/EFCore.Jet.FunctionalTests/Query/NorthwindMiscellaneousQueryJetTest.cs @@ -5465,18 +5465,31 @@ CROSS APPLY ( AssertSql( """ -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 +SELECT `t0`.`CustomerID`, `t0`.`Address`, `t0`.`City`, `t0`.`CompanyName`, `t0`.`ContactName`, `t0`.`ContactTitle`, `t0`.`Country`, `t0`.`Fax`, `t0`.`Phone`, `t0`.`PostalCode`, `t0`.`Region` +FROM ( + SELECT `t`.`CustomerID`, `t`.`Address`, `t`.`City`, `t`.`CompanyName`, `t`.`ContactName`, `t`.`ContactTitle`, `t`.`Country`, `t`.`Fax`, `t`.`Phone`, `t`.`PostalCode`, `t`.`Region` + FROM ( + 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 + ) AS `t` + WHERE 0 = 1 +) AS `t0` +ORDER BY `t0`.`CustomerID` """, // """ -@__p_0='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] -ORDER BY [c].[CustomerID] -OFFSET @__p_0 ROWS FETCH NEXT @__p_0 ROWS ONLY +SELECT `t0`.`CustomerID`, `t0`.`Address`, `t0`.`City`, `t0`.`CompanyName`, `t0`.`ContactName`, `t0`.`ContactTitle`, `t0`.`Country`, `t0`.`Fax`, `t0`.`Phone`, `t0`.`PostalCode`, `t0`.`Region` +FROM ( + SELECT TOP 1 `t`.`CustomerID`, `t`.`Address`, `t`.`City`, `t`.`CompanyName`, `t`.`ContactName`, `t`.`ContactTitle`, `t`.`Country`, `t`.`Fax`, `t`.`Phone`, `t`.`PostalCode`, `t`.`Region` + FROM ( + SELECT TOP 2 `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 `c`.`CustomerID` + ) AS `t` + ORDER BY `t`.`CustomerID` DESC +) AS `t0` +ORDER BY `t0`.`CustomerID` """); } @@ -5486,16 +5499,20 @@ OFFSET @__p_0 ROWS FETCH NEXT @__p_0 ROWS ONLY AssertSql( """ -SELECT CASE - WHEN EXISTS ( +SELECT IIF(EXISTS ( SELECT 1 - FROM [Orders] AS [o] - WHERE 0 = 1) THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END -FROM [Customers] AS [c] -WHERE [c].[CustomerID] LIKE N'F%' -ORDER BY [c].[CustomerID] + FROM ( + SELECT `t`.`OrderID`, `t`.`CustomerID`, `t`.`EmployeeID`, `t`.`OrderDate` + FROM ( + SELECT `o`.`OrderID`, `o`.`CustomerID`, `o`.`EmployeeID`, `o`.`OrderDate` + FROM `Orders` AS `o` + WHERE 0 = 1 + ) AS `t` + WHERE 0 = 1 + ) AS `t0`), TRUE, FALSE) +FROM `Customers` AS `c` +WHERE `c`.`CustomerID` LIKE 'F%' +ORDER BY `c`.`CustomerID` """); }