diff --git a/src/EFCore.Jet/Infrastructure/Internal/IJetOptions.cs b/src/EFCore.Jet/Infrastructure/Internal/IJetOptions.cs index 9137fec..0b74562 100644 --- a/src/EFCore.Jet/Infrastructure/Internal/IJetOptions.cs +++ b/src/EFCore.Jet/Infrastructure/Internal/IJetOptions.cs @@ -13,5 +13,6 @@ namespace EntityFrameworkCore.Jet.Infrastructure.Internal { string ConnectionString { get; } DataAccessProviderType DataAccessProviderType { get; } + bool UseOuterSelectSkipEmulationViaDataReader { get; } } } diff --git a/src/EFCore.Jet/Infrastructure/Internal/JetOptionsExtension.cs b/src/EFCore.Jet/Infrastructure/Internal/JetOptionsExtension.cs index 73cf82e..685a125 100644 --- a/src/EFCore.Jet/Infrastructure/Internal/JetOptionsExtension.cs +++ b/src/EFCore.Jet/Infrastructure/Internal/JetOptionsExtension.cs @@ -22,6 +22,7 @@ namespace EntityFrameworkCore.Jet.Infrastructure.Internal // private bool? _rowNumberPaging; private DbProviderFactory _dataAccessProviderFactory; + private bool _useOuterSelectSkipEmulationViaDataReader; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -46,6 +47,7 @@ namespace EntityFrameworkCore.Jet.Infrastructure.Internal { // _rowNumberPaging = copyFrom._rowNumberPaging; _dataAccessProviderFactory = copyFrom._dataAccessProviderFactory; + _useOuterSelectSkipEmulationViaDataReader = copyFrom._useOuterSelectSkipEmulationViaDataReader; } /// @@ -114,6 +116,29 @@ namespace EntityFrameworkCore.Jet.Infrastructure.Internal return clone; } + /// + /// 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 bool UseOuterSelectSkipEmulationViaDataReader => _useOuterSelectSkipEmulationViaDataReader; + + /// + /// 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 JetOptionsExtension WithUseOuterSelectSkipEmulationViaDataReader(bool enabled) + { + var clone = (JetOptionsExtension) Clone(); + + clone._useOuterSelectSkipEmulationViaDataReader = enabled; + + return clone; + } + /// /// 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 @@ -160,6 +185,11 @@ namespace EntityFrameworkCore.Jet.Infrastructure.Internal builder.Append("DataAccessProviderFactory "); } + if (Extension._useOuterSelectSkipEmulationViaDataReader) + { + builder.Append("UseOuterSelectSkipEmulationViaDataReader "); + } + _logFragment = builder.ToString(); } @@ -172,7 +202,8 @@ namespace EntityFrameworkCore.Jet.Infrastructure.Internal if (_serviceProviderHash == null) { _serviceProviderHash = (base.GetServiceProviderHashCode() * 397) ^ - (Extension._dataAccessProviderFactory?.GetHashCode() ?? 0L) /* ^ + (Extension._dataAccessProviderFactory?.GetHashCode() ?? 0L) ^ + (Extension._useOuterSelectSkipEmulationViaDataReader.GetHashCode() * 397) /* ^ (Extension._rowNumberPaging?.GetHashCode() ?? 0L)*/; } @@ -185,8 +216,12 @@ namespace EntityFrameworkCore.Jet.Infrastructure.Internal debugInfo["Jet:" + nameof(JetDbContextOptionsBuilder.UseRowNumberForPaging)] = (Extension._rowNumberPaging?.GetHashCode() ?? 0L).ToString(CultureInfo.InvariantCulture); */ - debugInfo["Jet:" + nameof(JetOptionsExtension.DataAccessProviderFactory)] + debugInfo["Jet:" + nameof(DataAccessProviderFactory)] = (Extension._dataAccessProviderFactory?.GetHashCode() ?? 0L).ToString(CultureInfo.InvariantCulture); +#pragma warning disable 618 + debugInfo["Jet:" + nameof(JetDbContextOptionsBuilder.UseOuterSelectSkipEmulationViaDataReader)] +#pragma warning restore 618 + = Extension._useOuterSelectSkipEmulationViaDataReader.GetHashCode().ToString(CultureInfo.InvariantCulture); } } } diff --git a/src/EFCore.Jet/Infrastructure/JetDbContextOptionsBuilder.cs b/src/EFCore.Jet/Infrastructure/JetDbContextOptionsBuilder.cs index 8b80096..5dd53ca 100644 --- a/src/EFCore.Jet/Infrastructure/JetDbContextOptionsBuilder.cs +++ b/src/EFCore.Jet/Infrastructure/JetDbContextOptionsBuilder.cs @@ -40,6 +40,20 @@ namespace EntityFrameworkCore.Jet.Infrastructure // public virtual JetDbContextOptionsBuilder UseRowNumberForPaging(bool useRowNumberForPaging = true) // => WithOption(e => e.WithRowNumberPaging(useRowNumberForPaging)); + /// + /// Jet/ACE doesn't natively support row skipping. When this option is enabled, row skipping will be + /// emulated in the most outer SELECT statement, by letting the JetDataReader ignore as many returned rows + /// as should have been skipped by the database. + /// This will only work when `JetCommand.ExecuteDataReader()` is beeing used to execute the `JetCommand`. + /// It is recommanded to not use this option, but to switch to client evaluation instead, by inserting a + /// call to either `AsEnumerable()`, `AsAsyncEnumerable()`, `ToList()`, or `ToListAsync()` and only then + /// to use `Skip()`. This will work in all cases and independent of the specific `JetCommand.Execute()` + /// method called. + /// + [Obsolete("This method exists for backward compatibility reasons only. Switch to client evaluation instead.")] + public virtual JetDbContextOptionsBuilder UseOuterSelectSkipEmulationViaDataReader(bool enabled = true) + => WithOption(e => e.WithUseOuterSelectSkipEmulationViaDataReader(enabled)); + /// /// Configures the context to use the default retrying . /// diff --git a/src/EFCore.Jet/Internal/JetOptions.cs b/src/EFCore.Jet/Internal/JetOptions.cs index 0a56e1c..8bbc91d 100644 --- a/src/EFCore.Jet/Internal/JetOptions.cs +++ b/src/EFCore.Jet/Internal/JetOptions.cs @@ -27,6 +27,7 @@ namespace EntityFrameworkCore.Jet.Internal // RowNumberPagingEnabled = jetOptions.RowNumberPaging ?? false; DataAccessProviderType = GetDataAccessProviderTypeFromOptions(jetOptions); + UseOuterSelectSkipEmulationViaDataReader = jetOptions.UseOuterSelectSkipEmulationViaDataReader; ConnectionString = jetOptions.Connection?.ConnectionString ?? jetOptions.ConnectionString; } @@ -55,6 +56,14 @@ namespace EntityFrameworkCore.Jet.Internal nameof(JetOptionsExtension.DataAccessProviderFactory), nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); } + + if (UseOuterSelectSkipEmulationViaDataReader != jetOptions.UseOuterSelectSkipEmulationViaDataReader) + { + throw new InvalidOperationException( + CoreStrings.SingletonOptionChanged( + nameof(JetOptionsExtension.UseOuterSelectSkipEmulationViaDataReader), + nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); + } } private static DataAccessProviderType GetDataAccessProviderTypeFromOptions(JetOptionsExtension jetOptions) @@ -108,6 +117,14 @@ namespace EntityFrameworkCore.Jet.Internal /// public virtual DataAccessProviderType DataAccessProviderType { get; private set; } + /// + /// 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 bool UseOuterSelectSkipEmulationViaDataReader { get; private set; } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGenerator.cs b/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGenerator.cs index 5928057..3356cdd 100644 --- a/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGenerator.cs +++ b/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGenerator.cs @@ -5,13 +5,12 @@ using System.Collections.Generic; using System.Data.Jet; using System.Linq; using System.Linq.Expressions; -using System.Text; +using EntityFrameworkCore.Jet.Infrastructure.Internal; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Storage; using EntityFrameworkCore.Jet.Utilities; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; @@ -37,6 +36,7 @@ namespace EntityFrameworkCore.Jet.Query.Sql.Internal }; private readonly ITypeMappingSource _typeMappingSource; + private readonly IJetOptions _options; private readonly JetSqlExpressionFactory _sqlExpressionFactory; private readonly ISqlGenerationHelper _sqlGenerationHelper; private CoreTypeMapping _boolTypeMapping; @@ -48,11 +48,13 @@ namespace EntityFrameworkCore.Jet.Query.Sql.Internal public JetQuerySqlGenerator( [NotNull] QuerySqlGeneratorDependencies dependencies, ISqlExpressionFactory sqlExpressionFactory, - ITypeMappingSource typeMappingSource) + ITypeMappingSource typeMappingSource, + IJetOptions options) : base(dependencies) { _sqlExpressionFactory = (JetSqlExpressionFactory) sqlExpressionFactory; _typeMappingSource = typeMappingSource; + _options = options; _sqlGenerationHelper = dependencies.SqlGenerationHelper; _boolTypeMapping = _typeMappingSource.FindMapping(typeof(bool)); } @@ -351,7 +353,16 @@ namespace EntityFrameworkCore.Jet.Query.Sql.Internal if (selectExpression.Offset != null) { - throw new InvalidOperationException("Jet does not support skipping rows. Switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync() if needed."); + if (_options.UseOuterSelectSkipEmulationViaDataReader) + { + Sql.Append("SKIP "); + Visit(selectExpression.Offset); + Sql.Append(" "); + } + else + { + throw new InvalidOperationException("Jet does not support skipping rows. Switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync() if needed."); + } } if (selectExpression.Limit != null) diff --git a/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGeneratorFactory.cs b/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGeneratorFactory.cs index 67df0fd..72325ba 100644 --- a/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGeneratorFactory.cs +++ b/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGeneratorFactory.cs @@ -1,5 +1,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using EntityFrameworkCore.Jet.Infrastructure.Internal; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Storage; @@ -15,6 +16,7 @@ namespace EntityFrameworkCore.Jet.Query.Sql.Internal [NotNull] private readonly QuerySqlGeneratorDependencies _dependencies; [NotNull] private readonly JetSqlExpressionFactory _sqlExpressionFactory; [NotNull] private readonly ITypeMappingSource _typeMappingSource; + [NotNull] private readonly IJetOptions _options; /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used @@ -23,14 +25,16 @@ namespace EntityFrameworkCore.Jet.Query.Sql.Internal public JetQuerySqlGeneratorFactory( [NotNull] QuerySqlGeneratorDependencies dependencies, [NotNull] ISqlExpressionFactory sqlExpressionFactory, - [NotNull] ITypeMappingSource typeMappingSource) + [NotNull] ITypeMappingSource typeMappingSource, + [NotNull] IJetOptions options) { _dependencies = dependencies; _sqlExpressionFactory = (JetSqlExpressionFactory)sqlExpressionFactory; _typeMappingSource = typeMappingSource; + _options = options; } public virtual QuerySqlGenerator Create() - => new JetQuerySqlGenerator(_dependencies, _sqlExpressionFactory, _typeMappingSource); + => new JetQuerySqlGenerator(_dependencies, _sqlExpressionFactory, _typeMappingSource, _options); } } diff --git a/src/System.Data.Jet/JetCommand.cs b/src/System.Data.Jet/JetCommand.cs index f59e87c..860f66b 100644 --- a/src/System.Data.Jet/JetCommand.cs +++ b/src/System.Data.Jet/JetCommand.cs @@ -15,9 +15,13 @@ namespace System.Data.Jet #endif private readonly 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 _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); private static readonly Regex _ifStatementRegex = new Regex(@"^\s*if\s*(?not)?\s*exists\s*\((?.+)\)\s*then\s*(?.*)$", RegexOptions.IgnoreCase); @@ -204,10 +208,12 @@ namespace System.Data.Jet if ((dataReader = TryGetDataReaderForSelectRowCount(InnerCommand.CommandText)) == null) { FixupGlobalVariables(); + PrepareOuterSelectSkipEmulationViaDataReader(); InlineTopParameters(); + ModifyOuterSelectTopValueForOuterSelectSkipEmulationViaDataReader(); FixParameters(); - dataReader = new JetDataReader(InnerCommand.ExecuteReader(behavior)); + dataReader = new JetDataReader(InnerCommand.ExecuteReader(behavior), _outerSelectSkipEmulationViaDataReaderSkipCount); _connection.RowCount = dataReader.RecordsAffected; } @@ -526,6 +532,64 @@ namespace System.Data.Jet } } + private void ModifyOuterSelectTopValueForOuterSelectSkipEmulationViaDataReader() + { + // We modify the TOP clause parameter of the outer most SELECT statement if a SKIP clause was also + // specified, because Jet does not support skipping records at all, but we can optionally emulate skipping + // behavior for the outer most SELECT statement by controlling how the records are being fetched in + // JetDataReader. + + if (_outerSelectSkipEmulationViaDataReaderSkipCount > 0) + { + InnerCommand.CommandText = _outerSelectTopValueRegularExpression.Replace( + InnerCommand.CommandText, + match => (int.Parse(match.Value) + _outerSelectSkipEmulationViaDataReaderSkipCount).ToString()); + } + } + + private void PrepareOuterSelectSkipEmulationViaDataReader() + { + // We inline the SKIP clause parameter of the outer most SELECT statement, because Jet does not support + // skipping records at all, but we can optionally emulate skipping behavior for the outer most SELECT + // statement by controlling how the records are being fetched in JetDataReader. + + var match = _outerSelectSkipValueOrParameterRegularExpression.Match(InnerCommand.CommandText); + + if (!match.Success) + { + return; + } + + var skipValueOrParameter = match.Groups["SkipValueOrParameter"]; + + if (IsParameter(skipValueOrParameter.Value)) + { + var parameters = InnerCommand.Parameters.Cast() + .ToList(); + + if (parameters.Count <= 0) + { + throw new InvalidOperationException($@"Cannot find ""{skipValueOrParameter.Value}"" parameter for SKIP clause."); + } + + var parameter = ExtractParameter(InnerCommand.CommandText, match.Index, parameters); + _outerSelectSkipEmulationViaDataReaderSkipCount = Convert.ToInt32(parameter.Value); + + InnerCommand.Parameters.Clear(); + InnerCommand.Parameters.AddRange(parameters.ToArray()); + } + else + { + _outerSelectSkipEmulationViaDataReaderSkipCount = int.Parse(skipValueOrParameter.Value); + } + + 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)); diff --git a/src/System.Data.Jet/JetDataReader.cs b/src/System.Data.Jet/JetDataReader.cs index 48c63d2..dc81ea6 100644 --- a/src/System.Data.Jet/JetDataReader.cs +++ b/src/System.Data.Jet/JetDataReader.cs @@ -17,6 +17,16 @@ namespace System.Data.Jet #endif _wrappedDataReader = dataReader; } + + public JetDataReader(DbDataReader dataReader, int skipCount) + : this(dataReader) + { + var i = 0; + while (i < skipCount && _wrappedDataReader.Read()) + { + i++; + } + } private readonly DbDataReader _wrappedDataReader;