diff --git a/src/EFCore.Jet.Data/JetCommand.cs b/src/EFCore.Jet.Data/JetCommand.cs index a6f35e6..6ebbd53 100644 --- a/src/EFCore.Jet.Data/JetCommand.cs +++ b/src/EFCore.Jet.Data/JetCommand.cs @@ -17,11 +17,12 @@ namespace EntityFrameworkCore.Jet.Data #endif private JetConnection? _connection; private JetTransaction? _transaction; - + private int _outerSelectSkipEmulationViaDataReaderSkipCount; private static readonly Regex _createProcedureExpression = new Regex(@"^\s*create\s*procedure\b", RegexOptions.IgnoreCase); private static readonly Regex _topParameterRegularExpression = new Regex(@"(?<=(?:^|\s)select\s+top\s+)(?:@\w+|\?)(?=\s)", RegexOptions.IgnoreCase); + private static readonly Regex _topMultiParameterRegularExpression = new Regex(@"(?<=(?:^|\s)select\s+top\s+)(?'first'@\w+|\?)(\s)(\+)(\s+)(?'sec'@\w+|\?)", RegexOptions.IgnoreCase); private static readonly Regex _outerSelectTopValueRegularExpression = new Regex(@"(?<=^\s*select\s+top\s+)\d+(?=\s)", RegexOptions.IgnoreCase); private static readonly Regex _outerSelectSkipValueOrParameterRegularExpression = new Regex(@"(?<=^\s*select)\s+skip\s+(?@\w+|\?|\d+)(?=\s)", RegexOptions.IgnoreCase); private static readonly Regex _selectRowCountRegularExpression = new Regex(@"^\s*select\s*@@rowcount\s*;?\s*$", RegexOptions.IgnoreCase); @@ -35,7 +36,7 @@ namespace EntityFrameworkCore.Jet.Data _connection = source._connection; _transaction = source._transaction; - InnerCommand = (DbCommand) ((ICloneable) source.InnerCommand).Clone(); + InnerCommand = (DbCommand)((ICloneable)source.InnerCommand).Clone(); } /// @@ -153,7 +154,7 @@ namespace EntityFrameworkCore.Jet.Data protected override DbTransaction? DbTransaction { get => _transaction; - set => _transaction = (JetTransaction?) value; + set => _transaction = (JetTransaction?)value; } /// @@ -269,7 +270,7 @@ namespace EntityFrameworkCore.Jet.Data } FixupGlobalVariables(); - + if (!CheckExists(InnerCommand.CommandText, out var newCommandText)) return 0; @@ -344,7 +345,7 @@ namespace EntityFrameworkCore.Jet.Data protected virtual IReadOnlyList SplitCommands() { // At this point, all parameters have already been expanded. - + var parser = new JetCommandParser(CommandText); var commandDelimiters = parser.GetStateIndices(';'); var currentCommandStart = 0; @@ -360,7 +361,7 @@ namespace EntityFrameworkCore.Jet.Data if (!string.IsNullOrEmpty(commandText)) { - var command = (JetCommand) ((ICloneable) this).Clone(); + var command = (JetCommand)((ICloneable)this).Clone(); command.CommandText = commandText; if (_createProcedureExpression.IsMatch(command.CommandText)) @@ -375,7 +376,7 @@ namespace EntityFrameworkCore.Jet.Data } var parameterIndices = parser.GetStateIndices( - new[] {'@', '?'}, + new[] { '@', '?' }, currentCommandStart, commandDelimiter - currentCommandStart); @@ -434,7 +435,7 @@ namespace EntityFrameworkCore.Jet.Data .Value; bool hasRows; - using (var command = (JetCommand) ((ICloneable) this).Clone()) + using (var command = (JetCommand)((ICloneable)this).Clone()) { command.CommandText = sqlCheckCommand; using (var reader = command.ExecuteReader()) @@ -470,18 +471,18 @@ namespace EntityFrameworkCore.Jet.Data protected virtual void FixupGlobalVariables() { var commandText = InnerCommand.CommandText; - + commandText = FixupIdentity(commandText); commandText = FixupRowCount(commandText); InnerCommand.CommandText = commandText; } - + protected virtual string FixupIdentity(string commandText) => FixupGlobalVariablePlaceholder( commandText, "@@identity", (outerCommand, placeholder) => { - var command = (DbCommand) ((ICloneable) outerCommand.InnerCommand).Clone(); + var command = (DbCommand)((ICloneable)outerCommand.InnerCommand).Clone(); command.CommandText = $"SELECT {placeholder}"; command.Parameters.Clear(); @@ -511,7 +512,7 @@ namespace EntityFrameworkCore.Jet.Data newCommandText.Insert(globalVariableIndex, placeholderValue.Value); } } - + return newCommandText.ToString(); } @@ -527,6 +528,21 @@ namespace EntityFrameworkCore.Jet.Data var lastCommandText = InnerCommand.CommandText; var commandText = lastCommandText; + var matchm = _topMultiParameterRegularExpression.Match(lastCommandText); + if (matchm.Success) + { + var first = matchm.Groups["first"]; + var sec = matchm.Groups["sec"]; + var sp = Convert.ToInt32(ExtractParameter(commandText, sec.Index, parameters).Value); + var fp = Convert.ToInt32(ExtractParameter(commandText, first.Index, parameters).Value); + var total = fp + sp; + commandText = _topMultiParameterRegularExpression.Replace( + lastCommandText, + match => total.ToString(), + 1); + lastCommandText = commandText; + } + while ((commandText = _topParameterRegularExpression.Replace( lastCommandText, match => Convert.ToInt32( @@ -544,7 +560,7 @@ namespace EntityFrameworkCore.Jet.Data InnerCommand.Parameters.AddRange(parameters.ToArray()); } } - + private void ModifyOuterSelectTopValueForOuterSelectSkipEmulationViaDataReader() { // We modify the TOP clause parameter of the outer most SELECT statement if a SKIP clause was also @@ -587,7 +603,7 @@ namespace EntityFrameworkCore.Jet.Data var parameter = ExtractParameter(InnerCommand.CommandText, match.Index, parameters); _outerSelectSkipEmulationViaDataReaderSkipCount = Convert.ToInt32(parameter.Value); - + InnerCommand.Parameters.Clear(); InnerCommand.Parameters.AddRange(parameters.ToArray()); } @@ -598,11 +614,11 @@ namespace EntityFrameworkCore.Jet.Data InnerCommand.CommandText = InnerCommand.CommandText.Remove(match.Index, match.Length); } - + protected virtual bool IsParameter(string fragment) => fragment.Equals("?") || fragment.Length >= 2 && fragment[0] == '@' && fragment[1] != '@'; - + protected virtual DbParameter ExtractParameter(string commandText, int count, List parameters) { var indices = GetParameterIndices(commandText.Substring(0, count)); @@ -685,7 +701,7 @@ namespace EntityFrameworkCore.Jet.Data throw new InvalidOperationException($"Cannot find parameter with same name as parameter placeholder \"{placeholder.Name}\"."); } - var newParameter = (DbParameter) (parameter as ICloneable)?.Clone(); + var newParameter = (DbParameter)(parameter as ICloneable)?.Clone(); if (newParameter == null) { @@ -710,7 +726,7 @@ namespace EntityFrameworkCore.Jet.Data throw new InvalidOperationException("Invalid parameter placeholder found."); } - placeholders.Add(new ParameterPlaceholder {Index = index, Name = match.Value}); + placeholders.Add(new ParameterPlaceholder { Index = index, Name = match.Value }); } return placeholders.AsReadOnly(); @@ -718,7 +734,7 @@ namespace EntityFrameworkCore.Jet.Data protected virtual IReadOnlyList GetParameterIndices(string sqlFragment) => new JetCommandParser(sqlFragment) - .GetStateIndices(new[] {'@', '?'}); + .GetStateIndices(new[] { '@', '?' }); /// /// Creates a prepared (or compiled) version of the command on the data source diff --git a/src/EFCore.Jet/Query/Internal/JetQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Jet/Query/Internal/JetQueryableMethodTranslatingExpressionVisitor.cs index cb6084c..74e7ce4 100644 --- a/src/EFCore.Jet/Query/Internal/JetQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Jet/Query/Internal/JetQueryableMethodTranslatingExpressionVisitor.cs @@ -1,10 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; namespace EntityFrameworkCore.Jet.Query.Internal; @@ -62,4 +68,73 @@ public class JetQueryableMethodTranslatingExpressionVisitor : RelationalQueryabl } return base.TranslateLeftJoin(outer, inner, outerKeySelector, innerKeySelector, resultSelector); } + + protected override ShapedQueryExpression? TranslateSkip(ShapedQueryExpression source, Expression count) + { + var selectExpression = (SelectExpression)source.QueryExpression; + var translation = TranslateExpression(count); + if (translation == null) + { + return null; + } + + if (selectExpression.Orderings.Count == 0) + { + //_queryCompilationContext.Logger.RowLimitingOperationWithoutOrderByWarning(); + } + + var clone = selectExpression.Clone(); + var limit = clone.Limit; + if (limit != null) + { + var total = new SqlBinaryExpression(ExpressionType.Add, limit, translation, typeof(int), + RelationalTypeMapping.NullMapping); + selectExpression.ApplyLimit(total); + } + else + { + selectExpression.ApplyLimit(translation); + selectExpression.Tags.Add("DeepSkip"); + } + selectExpression.ReverseOrderings(); + selectExpression.ApplyLimit(translation); + selectExpression.ReverseOrderings(); + return source; + } + + protected override ShapedQueryExpression? TranslateTake(ShapedQueryExpression source, Expression count) + { + var selectExpression = (SelectExpression)source.QueryExpression; + var translation = TranslateExpression(count); + if (translation == null) + { + return null; + } + if (selectExpression.Tags.Contains("DeepSkip")) + { + var f1 = selectExpression.Tables.First(d => d is SelectExpression) as SelectExpression; + var f2 = f1?.Tables.First(d => d is SelectExpression) as SelectExpression; + var limit = f2?.Limit; + if (limit != null) + { + var total = new SqlBinaryExpression(ExpressionType.Add, limit, translation, typeof(int), + RelationalTypeMapping.NullMapping); + //var sp = (SqlParameterExpression)limit; + //var sp2 = (SqlParameterExpression)translation; + //var newparam = Expression.Parameter(typeof(int), sp.Name + sp2.Name); + //var sqlparam = typeof(SqlParameterExpression).GetTypeInfo().DeclaredConstructors.First().Invoke(new object[] { newparam, RelationalTypeMapping.NullMapping }); + var tp = f2!.GetType().GetMember("Limit").First(); + MethodInfo? dynMethod2 = f2.GetType().GetMethod("set_Limit", + BindingFlags.NonPublic | BindingFlags.Instance); + dynMethod2?.Invoke(f2, new object[] { total }); + MethodInfo? dynMethod1 = f1!.GetType().GetMethod("set_Limit", + BindingFlags.NonPublic | BindingFlags.Instance); + dynMethod1?.Invoke(f1, new object[] { translation }); + selectExpression.Tags.Remove("DeepSkip"); + return source; + } + + } + return base.TranslateTake(source, count); + } } diff --git a/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGenerator.cs b/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGenerator.cs index 8b6eb56..6a0f7a0 100644 --- a/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGenerator.cs +++ b/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGenerator.cs @@ -95,7 +95,15 @@ namespace EntityFrameworkCore.Jet.Query.Sql.Internal Sql.Append("DISTINCT "); } - GenerateTop(selectExpression); + if (selectExpression.Tags.Contains("DeepSkip")) + { + + } + else + { + GenerateTop(selectExpression); + } + if (selectExpression.Projection.Any()) {