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.Data.Jet;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
using EntityFrameworkCore.Jet.Utilities; using EntityFrameworkCore.Jet.Utilities;
using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
@ -349,8 +351,7 @@ namespace EntityFrameworkCore.Jet.Query.Sql.Internal
if (selectExpression.Offset != null) if (selectExpression.Offset != null)
{ {
// Jet does not support skipping rows. Use client evaluation instead. 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.");
throw new InvalidOperationException(CoreStrings.TranslationFailed(selectExpression.Offset));
} }
if (selectExpression.Limit != null) if (selectExpression.Limit != null)
@ -430,5 +431,8 @@ namespace EntityFrameworkCore.Jet.Query.Sql.Internal
return caseExpression; 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. // 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 System.Text;
using EntityFrameworkCore.Jet.Infrastructure.Internal;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
using EntityFrameworkCore.Jet.Utilities; using EntityFrameworkCore.Jet.Utilities;
@ -15,18 +13,14 @@ namespace EntityFrameworkCore.Jet.Storage.Internal
/// </summary> /// </summary>
public class JetSqlGenerationHelper : RelationalSqlGenerationHelper public class JetSqlGenerationHelper : RelationalSqlGenerationHelper
{ {
private readonly IJetOptions _jetOptions;
/// <summary> /// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used /// 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. /// directly from your code. This API may change or be removed in future releases.
/// </summary> /// </summary>
public JetSqlGenerationHelper( public JetSqlGenerationHelper(
[NotNull] RelationalSqlGenerationHelperDependencies dependencies, [NotNull] RelationalSqlGenerationHelperDependencies dependencies)
[NotNull] IJetOptions jetOptions)
: base(dependencies) : base(dependencies)
{ {
_jetOptions = jetOptions;
} }
/// <summary> /// <summary>
@ -91,23 +85,6 @@ namespace EntityFrameworkCore.Jet.Storage.Internal
return DelimitIdentifier(Check.NotEmpty(name, nameof(name))); 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) public static string TruncateIdentifier(string identifier)
{ {
if (identifier.Length <= 64) if (identifier.Length <= 64)

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

@ -18,7 +18,8 @@ namespace System.Data.Jet
private Guid? _lastGuid; private Guid? _lastGuid;
private int? _rowCount; 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 _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); 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. // OLE DB forces us to use an existing active transaction, if one is available.
InnerCommand.Transaction = _transaction?.WrappedTransaction ?? _connection.ActiveTransaction?.WrappedTransaction; InnerCommand.Transaction = _transaction?.WrappedTransaction ?? _connection.ActiveTransaction?.WrappedTransaction;
ExpandParameters();
LogHelper.ShowCommandText("ExecuteDbDataReader", InnerCommand); LogHelper.ShowCommandText("ExecuteDbDataReader", InnerCommand);
if (JetStoreSchemaDefinitionRetrieve.TryGetDataReaderFromShowCommand(InnerCommand, _connection.JetFactory.InnerFactory, out var dataReader)) if (JetStoreSchemaDefinitionRetrieve.TryGetDataReaderFromShowCommand(InnerCommand, _connection.JetFactory.InnerFactory, out var dataReader))
@ -185,19 +188,17 @@ namespace System.Data.Jet
if (InnerCommand.CommandType != CommandType.Text) if (InnerCommand.CommandType != CommandType.Text)
return new JetDataReader(InnerCommand.ExecuteReader(behavior)); return new JetDataReader(InnerCommand.ExecuteReader(behavior));
var commandTextList = SplitCommands(InnerCommand.CommandText); if ((dataReader = TryGetDataReaderForSelectRowCount(InnerCommand.CommandText)) == null)
dataReader = null;
foreach (var t in commandTextList)
{ {
var commandText = t; InnerCommand.CommandText = ParseIdentity(InnerCommand.CommandText);
if ((dataReader = TryGetDataReaderForSelectRowCount(commandText)) != null) InnerCommand.CommandText = ParseGuid(InnerCommand.CommandText);
continue;
commandText = ParseIdentity(commandText); InlineTopParameters();
commandText = ParseGuid(commandText); FixParameters();
dataReader = InternalExecuteDbDataReader(commandText, behavior); dataReader = new JetDataReader(InnerCommand.ExecuteReader(behavior));
_rowCount = dataReader.RecordsAffected;
} }
return dataReader; return dataReader;
@ -205,8 +206,7 @@ namespace System.Data.Jet
private DbDataReader TryGetDataReaderForSelectRowCount(string commandText) private DbDataReader TryGetDataReaderForSelectRowCount(string commandText)
{ {
if (_selectRowCountRegularExpression.Match(commandText) if (_selectRowCountRegularExpression.Match(commandText).Success)
.Success)
{ {
if (_rowCount == null) if (_rowCount == null)
throw new InvalidOperationException("Invalid " + commandText + ". Run a DataReader before."); throw new InvalidOperationException("Invalid " + commandText + ". Run a DataReader before.");
@ -227,6 +227,8 @@ namespace System.Data.Jet
{ {
if (Connection == null) if (Connection == null)
throw new InvalidOperationException(Messages.PropertyNotInitialized(nameof(Connection))); throw new InvalidOperationException(Messages.PropertyNotInitialized(nameof(Connection)));
ExpandParameters();
LogHelper.ShowCommandText("ExecuteNonQuery", InnerCommand); LogHelper.ShowCommandText("ExecuteNonQuery", InnerCommand);
@ -245,29 +247,30 @@ namespace System.Data.Jet
if (InnerCommand.CommandType != CommandType.Text) if (InnerCommand.CommandType != CommandType.Text)
return InnerCommand.ExecuteNonQuery(); 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; if (!CheckExists(InnerCommand.CommandText, out var newCommandText))
foreach (var t in commandTextList) return 0;
{
var commandText = t;
if (_selectRowCountRegularExpression.Match(commandText)
.Success)
{
if (_rowCount == null)
throw new InvalidOperationException("Invalid " + commandText + ". Run a DataReader before.");
returnValue = _rowCount.Value;
continue;
}
commandText = ParseIdentity(commandText); InnerCommand.CommandText = newCommandText;
commandText = ParseGuid(commandText);
InlineTopParameters();
FixParameters();
returnValue = InternalExecuteNonQuery(commandText); _rowCount = InnerCommand.ExecuteNonQuery();
}
return returnValue; return _rowCount.Value;
} }
/// <summary> /// <summary>
@ -287,6 +290,8 @@ namespace System.Data.Jet
// OLE DB forces us to use an existing active transaction, if one is available. // OLE DB forces us to use an existing active transaction, if one is available.
InnerCommand.Transaction = _transaction?.WrappedTransaction ?? _connection.ActiveTransaction?.WrappedTransaction; InnerCommand.Transaction = _transaction?.WrappedTransaction ?? _connection.ActiveTransaction?.WrappedTransaction;
ExpandParameters();
LogHelper.ShowCommandText("ExecuteScalar", InnerCommand); LogHelper.ShowCommandText("ExecuteScalar", InnerCommand);
if (JetStoreSchemaDefinitionRetrieve.TryGetDataReaderFromShowCommand(InnerCommand, _connection.JetFactory.InnerFactory, out var dataReader)) if (JetStoreSchemaDefinitionRetrieve.TryGetDataReaderFromShowCommand(InnerCommand, _connection.JetFactory.InnerFactory, out var dataReader))
@ -301,42 +306,10 @@ namespace System.Data.Jet
return DBNull.Value; return DBNull.Value;
} }
return InnerCommand.ExecuteScalar(); InlineTopParameters();
} FixParameters();
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();
return _rowCount.Value; return InnerCommand.ExecuteScalar();
} }
private bool CheckExists(string commandText, out string newCommandText) private bool CheckExists(string commandText, out string newCommandText)
@ -368,10 +341,13 @@ namespace System.Data.Jet
return hasRows; return hasRows;
} }
private void FixParameters(DbParameterCollection parameters) private void FixParameters()
{ {
var parameters = InnerCommand.Parameters;
if (parameters.Count == 0) if (parameters.Count == 0)
return; return;
foreach (DbParameter parameter in parameters) foreach (DbParameter parameter in parameters)
{ {
if (parameter.Value is TimeSpan ts) 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) private string ParseIdentity(string commandText)
{ {
// TODO: Fix the following code, that does work only for common scenarios. Use state machine instead.
if (commandText.ToLower() if (commandText.ToLower()
.Contains("@@identity")) .Contains("@@identity"))
{ {
@ -460,6 +380,7 @@ namespace System.Data.Jet
private string ParseGuid(string commandText) private string ParseGuid(string commandText)
{ {
// TODO: Fix the following code, that does work only for common scenarios. Use state machine instead.
while (commandText.ToLower() while (commandText.ToLower()
.Contains("newguid()")) .Contains("newguid()"))
{ {
@ -477,59 +398,160 @@ namespace System.Data.Jet
return commandText; 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 // We inline all TOP clause parameters of all SELECT statements, because Jet does not support parameters
// in TOP clauses. // in TOP clauses.
var lastCommandText = commandText;
var parameters = InnerCommand.Parameters.Cast<DbParameter>().ToList(); var parameters = InnerCommand.Parameters.Cast<DbParameter>().ToList();
while ((commandText = _topRegularExpression.Replace( if (parameters.Count > 0)
commandText,
match => (IsParameter(match.Value)
? Convert.ToInt32(GetOrExtractParameter(commandText, match.Value, match.Index, parameters).Value)
: int.Parse(match.Value))
.ToString(), 1)) != lastCommandText)
{ {
lastCommandText = commandText; var lastCommandText = InnerCommand.CommandText;
} var commandText = lastCommandText;
InnerCommand.Parameters.Clear();
InnerCommand.Parameters.AddRange(parameters.ToArray());
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) protected virtual bool IsParameter(string fragment)
=> fragment.StartsWith("@") || => fragment.StartsWith("@") ||
fragment.Equals("?"); 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)); MatchParametersAndPlaceholders(placeholders);
var parameter = InnerCommand.Parameters[index];
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. // We use '\0' as the default state and char.
var state = '\0'; var state = '\0';
var lastChar = '\0'; var lastChar = '\0';
// State machine to count ODBC parameter occurrences. // 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 == '\'') if (state == '\'')
{ {
// We are currently inside a string, or closed the string in the last iteration but didn't // 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 = '`'; 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> /// <summary>
@ -657,5 +680,12 @@ namespace System.Data.Jet
/// <returns>The created object</returns> /// <returns>The created object</returns>
object ICloneable.Clone() object ICloneable.Clone()
=> new JetCommand(this); => new JetCommand(this);
protected class ParameterPlaceholder
{
public int Index { get; set; }
public string Name { get; set; }
public DbParameter Parameter { get; set; }
}
} }
} }
Loading…
Cancel
Save