Remove workarounds for Skip(), which is unsupported by Jet/ACE.

Implement better parameter support (including named parameters for ODBC).
Remove support for multiple statements in a single command, to support named parameters.
pull/48/head
Lau 6 years ago
parent d80b53e9b2
commit 43d5d39c69

@ -5,11 +5,13 @@ using System.Collections.Generic;
using System.Data.Jet;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
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;
@ -349,8 +351,7 @@ namespace EntityFrameworkCore.Jet.Query.Sql.Internal
if (selectExpression.Offset != null)
{
// Jet does not support skipping rows. Use client evaluation instead.
throw new InvalidOperationException(CoreStrings.TranslationFailed(selectExpression.Offset));
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)
@ -430,5 +431,8 @@ namespace EntityFrameworkCore.Jet.Query.Sql.Internal
return caseExpression;
}
protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpression)
=> throw new InvalidOperationException(CoreStrings.TranslationFailed(rowNumberExpression));
}
}

@ -1,8 +1,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Data.Jet;
using System.Text;
using EntityFrameworkCore.Jet.Infrastructure.Internal;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Storage;
using EntityFrameworkCore.Jet.Utilities;
@ -15,18 +13,14 @@ namespace EntityFrameworkCore.Jet.Storage.Internal
/// </summary>
public class JetSqlGenerationHelper : RelationalSqlGenerationHelper
{
private readonly IJetOptions _jetOptions;
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public JetSqlGenerationHelper(
[NotNull] RelationalSqlGenerationHelperDependencies dependencies,
[NotNull] IJetOptions jetOptions)
[NotNull] RelationalSqlGenerationHelperDependencies dependencies)
: base(dependencies)
{
_jetOptions = jetOptions;
}
/// <summary>
@ -91,23 +85,6 @@ namespace EntityFrameworkCore.Jet.Storage.Internal
return DelimitIdentifier(Check.NotEmpty(name, nameof(name)));
}
public override string GenerateParameterNamePlaceholder(string name)
=> _jetOptions.DataAccessProviderType == DataAccessProviderType.OleDb
? base.GenerateParameterNamePlaceholder(name)
: "?";
public override void GenerateParameterNamePlaceholder(StringBuilder builder, string name)
{
if (_jetOptions.DataAccessProviderType == DataAccessProviderType.OleDb)
{
base.GenerateParameterNamePlaceholder(builder, name);
}
else
{
builder.Append("?");
}
}
public static string TruncateIdentifier(string identifier)
{
if (identifier.Length <= 64)

@ -95,7 +95,7 @@ namespace System.Data.Jet
connection.DataAccessProviderFactory = dataAccessProviderFactory;
connection.Open();
var sql = @"CREATE TABLE `MSysAccessStorage` (
var script = @"CREATE TABLE `MSysAccessStorage` (
`DateCreate` DATETIME NULL,
`DateUpdate` DATETIME NULL,
`Id` COUNTER NOT NULL,
@ -108,8 +108,11 @@ namespace System.Data.Jet
CREATE UNIQUE INDEX `ParentIdId` ON `MSysAccessStorage` (`ParentId`, `Id`);
CREATE UNIQUE INDEX `ParentIdName` ON `MSysAccessStorage` (`ParentId`, `Name`);";
using var command = connection.CreateCommand(sql);
command.ExecuteNonQuery();
foreach (var commandText in script.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
{
using var command = connection.CreateCommand(commandText);
command.ExecuteNonQuery();
}
}
catch (Exception e)
{

@ -18,7 +18,8 @@ namespace System.Data.Jet
private Guid? _lastGuid;
private int? _rowCount;
private static readonly Regex _topRegularExpression = new Regex(@"(?<=(?:^|\s)select\s+top\s+)(?:\d+|(?:@\w+)|\?)(?=\s)", RegexOptions.IgnoreCase);
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 _selectRowCountRegularExpression = new Regex(@"^\s*select\s*@@rowcount\s*;?\s*$", RegexOptions.IgnoreCase);
private static readonly Regex _ifStatementRegex = new Regex(@"^\s*if\s*(?<not>not)?\s*exists\s*\((?<sqlCheckCommand>.+)\)\s*then\s*(?<sqlCommand>.*)$", RegexOptions.IgnoreCase);
@ -176,6 +177,8 @@ namespace System.Data.Jet
// OLE DB forces us to use an existing active transaction, if one is available.
InnerCommand.Transaction = _transaction?.WrappedTransaction ?? _connection.ActiveTransaction?.WrappedTransaction;
ExpandParameters();
LogHelper.ShowCommandText("ExecuteDbDataReader", InnerCommand);
if (JetStoreSchemaDefinitionRetrieve.TryGetDataReaderFromShowCommand(InnerCommand, _connection.JetFactory.InnerFactory, out var dataReader))
@ -185,19 +188,17 @@ namespace System.Data.Jet
if (InnerCommand.CommandType != CommandType.Text)
return new JetDataReader(InnerCommand.ExecuteReader(behavior));
var commandTextList = SplitCommands(InnerCommand.CommandText);
dataReader = null;
foreach (var t in commandTextList)
if ((dataReader = TryGetDataReaderForSelectRowCount(InnerCommand.CommandText)) == null)
{
var commandText = t;
if ((dataReader = TryGetDataReaderForSelectRowCount(commandText)) != null)
continue;
InnerCommand.CommandText = ParseIdentity(InnerCommand.CommandText);
InnerCommand.CommandText = ParseGuid(InnerCommand.CommandText);
commandText = ParseIdentity(commandText);
commandText = ParseGuid(commandText);
InlineTopParameters();
FixParameters();
dataReader = InternalExecuteDbDataReader(commandText, behavior);
dataReader = new JetDataReader(InnerCommand.ExecuteReader(behavior));
_rowCount = dataReader.RecordsAffected;
}
return dataReader;
@ -205,8 +206,7 @@ namespace System.Data.Jet
private DbDataReader TryGetDataReaderForSelectRowCount(string commandText)
{
if (_selectRowCountRegularExpression.Match(commandText)
.Success)
if (_selectRowCountRegularExpression.Match(commandText).Success)
{
if (_rowCount == null)
throw new InvalidOperationException("Invalid " + commandText + ". Run a DataReader before.");
@ -227,6 +227,8 @@ namespace System.Data.Jet
{
if (Connection == null)
throw new InvalidOperationException(Messages.PropertyNotInitialized(nameof(Connection)));
ExpandParameters();
LogHelper.ShowCommandText("ExecuteNonQuery", InnerCommand);
@ -245,29 +247,30 @@ namespace System.Data.Jet
if (InnerCommand.CommandType != CommandType.Text)
return InnerCommand.ExecuteNonQuery();
if (_selectRowCountRegularExpression.Match(InnerCommand.CommandText)
.Success)
{
// TODO: Fix exception message.
if (_rowCount == null)
throw new InvalidOperationException("Invalid " + InnerCommand.CommandText + ". Run a DataReader before.");
return _rowCount.Value;
}
var commandTextList = SplitCommands(InnerCommand.CommandText);
InnerCommand.CommandText = ParseIdentity(InnerCommand.CommandText);
InnerCommand.CommandText = ParseGuid(InnerCommand.CommandText);
var returnValue = -1;
foreach (var t in commandTextList)
{
var commandText = t;
if (_selectRowCountRegularExpression.Match(commandText)
.Success)
{
if (_rowCount == null)
throw new InvalidOperationException("Invalid " + commandText + ". Run a DataReader before.");
returnValue = _rowCount.Value;
continue;
}
if (!CheckExists(InnerCommand.CommandText, out var newCommandText))
return 0;
commandText = ParseIdentity(commandText);
commandText = ParseGuid(commandText);
InnerCommand.CommandText = newCommandText;
InlineTopParameters();
FixParameters();
returnValue = InternalExecuteNonQuery(commandText);
}
_rowCount = InnerCommand.ExecuteNonQuery();
return returnValue;
return _rowCount.Value;
}
/// <summary>
@ -287,6 +290,8 @@ namespace System.Data.Jet
// OLE DB forces us to use an existing active transaction, if one is available.
InnerCommand.Transaction = _transaction?.WrappedTransaction ?? _connection.ActiveTransaction?.WrappedTransaction;
ExpandParameters();
LogHelper.ShowCommandText("ExecuteScalar", InnerCommand);
if (JetStoreSchemaDefinitionRetrieve.TryGetDataReaderFromShowCommand(InnerCommand, _connection.JetFactory.InnerFactory, out var dataReader))
@ -301,42 +306,10 @@ namespace System.Data.Jet
return DBNull.Value;
}
return InnerCommand.ExecuteScalar();
}
private JetDataReader InternalExecuteDbDataReader(string commandText, CommandBehavior behavior)
{
var newCommandText = ApplyTopParameters(commandText);
SortParameters(newCommandText, InnerCommand.Parameters);
FixParameters(InnerCommand.Parameters);
var command = (DbCommand) ((ICloneable) InnerCommand).Clone();
command.CommandText = newCommandText;
var dataReader = new JetDataReader(command.ExecuteReader(behavior));
_rowCount = dataReader.RecordsAffected;
return dataReader;
}
private int InternalExecuteNonQuery(string commandText)
{
if (!CheckExists(commandText, out var newCommandText))
return 0;
newCommandText = ApplyTopParameters(newCommandText);
SortParameters(newCommandText, InnerCommand.Parameters);
FixParameters(InnerCommand.Parameters);
var command = (DbCommand) ((ICloneable) InnerCommand).Clone();
command.CommandText = newCommandText;
_rowCount = command.ExecuteNonQuery();
InlineTopParameters();
FixParameters();
return _rowCount.Value;
return InnerCommand.ExecuteScalar();
}
private bool CheckExists(string commandText, out string newCommandText)
@ -368,10 +341,13 @@ namespace System.Data.Jet
return hasRows;
}
private void FixParameters(DbParameterCollection parameters)
private void FixParameters()
{
var parameters = InnerCommand.Parameters;
if (parameters.Count == 0)
return;
foreach (DbParameter parameter in parameters)
{
if (parameter.Value is TimeSpan ts)
@ -384,65 +360,9 @@ namespace System.Data.Jet
}
}
private void SortParameters(string query, DbParameterCollection parameters)
{
if (parameters.Count == 0)
return;
var parameterArray = parameters.Cast<DbParameter>()
.OrderBy(p => p, new ParameterPositionComparer(query))
.ToArray();
parameters.Clear();
parameters.AddRange(parameterArray);
}
private class ParameterPositionComparer : IComparer<DbParameter>
{
private readonly string _query;
public ParameterPositionComparer(string query)
{
_query = query;
}
public int Compare(DbParameter x, DbParameter y)
{
if (x == null)
throw new ArgumentNullException(nameof(x));
if (y == null)
throw new ArgumentNullException(nameof(y));
var xPosition = _query.IndexOf(x.ParameterName, StringComparison.Ordinal);
var yPosition = _query.IndexOf(y.ParameterName, StringComparison.Ordinal);
if (xPosition == -1)
xPosition = int.MaxValue;
if (yPosition == -1)
yPosition = int.MaxValue;
return xPosition.CompareTo(yPosition);
}
}
private string[] SplitCommands(string command)
{
var commandParts =
command.Replace("\r\n", "\n")
.Replace("\r", "\n")
.Split(new[] {";\n"}, StringSplitOptions.None);
var commands = new List<string>(commandParts.Length);
foreach (var commandPart in commandParts)
{
if (!string.IsNullOrWhiteSpace(
commandPart.Replace("\n", "")
.Replace(";", "")))
commands.Add(commandPart);
}
return commands.ToArray();
}
private string ParseIdentity(string commandText)
{
// TODO: Fix the following code, that does work only for common scenarios. Use state machine instead.
if (commandText.ToLower()
.Contains("@@identity"))
{
@ -460,6 +380,7 @@ namespace System.Data.Jet
private string ParseGuid(string commandText)
{
// TODO: Fix the following code, that does work only for common scenarios. Use state machine instead.
while (commandText.ToLower()
.Contains("newguid()"))
{
@ -477,59 +398,160 @@ namespace System.Data.Jet
return commandText;
}
private string ApplyTopParameters(string commandText)
private void InlineTopParameters()
{
// We inline all TOP clause parameters of all SELECT statements, because Jet does not support parameters
// in TOP clauses.
var lastCommandText = commandText;
var parameters = InnerCommand.Parameters.Cast<DbParameter>().ToList();
while ((commandText = _topRegularExpression.Replace(
commandText,
match => (IsParameter(match.Value)
? Convert.ToInt32(GetOrExtractParameter(commandText, match.Value, match.Index, parameters).Value)
: int.Parse(match.Value))
.ToString(), 1)) != lastCommandText)
if (parameters.Count > 0)
{
lastCommandText = commandText;
}
InnerCommand.Parameters.Clear();
InnerCommand.Parameters.AddRange(parameters.ToArray());
var lastCommandText = InnerCommand.CommandText;
var commandText = lastCommandText;
return commandText;
while ((commandText = _topParameterRegularExpression.Replace(
lastCommandText,
match => Convert.ToInt32(ExtractParameter(commandText, match.Value, match.Index, parameters).Value).ToString(),
1)) != lastCommandText)
{
lastCommandText = commandText;
}
InnerCommand.CommandText = commandText;
InnerCommand.Parameters.Clear();
InnerCommand.Parameters.AddRange(parameters.ToArray());
}
}
protected virtual bool IsParameter(string fragment)
=> fragment.StartsWith("@") ||
fragment.Equals("?");
protected virtual DbParameter GetOrExtractParameter(string commandText, string name, int count, List<DbParameter> parameters)
protected virtual DbParameter ExtractParameter(string commandText, string name, int count, List<DbParameter> parameters)
{
if (name.Equals("?"))
var indices = GetParameterIndices(commandText.Substring(0, count));
var parameter = InnerCommand.Parameters[indices.Count];
parameters.RemoveAt(indices.Count);
return parameter;
}
protected virtual void ExpandParameters()
{
if (_createProcedureExpression.IsMatch(InnerCommand.CommandText))
{
return;
}
var indices = GetParameterIndices(InnerCommand.CommandText);
if (indices.Count <= 0)
{
return;
}
var placeholders = GetParameterPlaceholders(InnerCommand.CommandText, indices);
if (placeholders.All(t => t.Name.StartsWith("@")))
{
var index = GetOdbcParameterCount(commandText.Substring(0, count));
var parameter = InnerCommand.Parameters[index];
MatchParametersAndPlaceholders(placeholders);
if (JetConnection.GetDataAccessProviderType(_connection.DataAccessProviderFactory) == DataAccessProviderType.Odbc)
{
foreach (var placeholder in placeholders.Reverse())
{
InnerCommand.CommandText = InnerCommand.CommandText
.Remove(placeholder.Index, placeholder.Name.Length)
.Insert(placeholder.Index, "?");
}
}
parameters.RemoveAt(index);
InnerCommand.Parameters.Clear();
InnerCommand.Parameters.AddRange(placeholders.Select(p => p.Parameter).ToArray());
}
else if (placeholders.All(t => t.Name == "?"))
{
throw new InvalidOperationException("Parameter placeholder count does not match parameter count.");
}
else
{
throw new InvalidOperationException("Inconsistent parameter placeholder naming used.");
}
}
protected virtual void MatchParametersAndPlaceholders(IReadOnlyList<ParameterPlaceholder> placeholders)
{
var unusedParameters = InnerCommand.Parameters
.Cast<DbParameter>()
.ToList();
foreach (var placeholder in placeholders)
{
var parameter = unusedParameters
.FirstOrDefault(p => placeholder.Name.Equals(p.ParameterName, StringComparison.Ordinal));
if (parameter != null)
{
placeholder.Parameter = parameter;
unusedParameters.Remove(parameter);
}
else
{
parameter = placeholders
.FirstOrDefault(p => placeholder.Name.Equals(p.Name, StringComparison.Ordinal))
?.Parameter;
if (parameter == null)
{
throw new InvalidOperationException($"Cannot find parameter with same name as parameter placeholder \"{placeholder.Name}\".");
}
var newParameter = (DbParameter) (parameter as ICloneable)?.Clone();
if (newParameter == null)
{
throw new InvalidOperationException($"Cannot clone parameter \"{parameter.ParameterName}\".");
}
placeholder.Parameter = newParameter;
}
}
}
protected virtual IReadOnlyList<ParameterPlaceholder> GetParameterPlaceholders(string commandText, IEnumerable<int> indices)
{
var placeholders = new List<ParameterPlaceholder>();
foreach (var index in indices)
{
var match = Regex.Match(commandText.Substring(index), @"^(?:\?|@\w+)");
if (!match.Success)
{
throw new InvalidOperationException("Invalid parameter placeholder found.");
}
return parameter;
placeholders.Add(new ParameterPlaceholder{ Index = index, Name = match.Value });
}
return InnerCommand.Parameters[name];
return placeholders.AsReadOnly();
}
private static int GetOdbcParameterCount(string sqlFragment)
protected virtual IReadOnlyList<int> GetParameterIndices(string sqlFragment)
{
var parameterCount = 0;
var parameterIndices = new List<int>();
// We use '\0' as the default state and char.
var state = '\0';
var lastChar = '\0';
// State machine to count ODBC parameter occurrences.
foreach (var c in sqlFragment)
for (var i = 0; i < sqlFragment.Length; i++)
{
var c = sqlFragment[i];
if (state == '\'')
{
// We are currently inside a string, or closed the string in the last iteration but didn't
@ -604,9 +626,10 @@ namespace System.Data.Jet
{
state = '`';
}
else if (c == '?')
else if (c == '?' ||
c == '@')
{
parameterCount++;
parameterIndices.Add(i);
}
}
@ -617,7 +640,7 @@ namespace System.Data.Jet
}
}
return parameterCount;
return parameterIndices.AsReadOnly();
}
/// <summary>
@ -657,5 +680,12 @@ namespace System.Data.Jet
/// <returns>The created object</returns>
object ICloneable.Clone()
=> new JetCommand(this);
protected class ParameterPlaceholder
{
public int Index { get; set; }
public string Name { get; set; }
public DbParameter Parameter { get; set; }
}
}
}
Loading…
Cancel
Save