You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
EntityFrameworkCore.Jet/src/System.Data.Jet/JetCommand.cs

657 lines
25 KiB
C#

using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Jet.JetStoreSchemaDefinition;
using System.Data.OleDb;
using System.Linq;
using System.Text.RegularExpressions;
namespace System.Data.Jet
{
public class JetCommand : DbCommand, ICloneable
{
private DbCommand _WrappedCommand;
private JetConnection _Connection;
private JetTransaction _Transaction;
private bool _DesignTimeVisible;
private Guid? _lastGuid = null;
private int? _rowCount = null;
private static readonly Regex _skipRegularExpression = new Regex(@"\bskip\s(?<stringSkipCount>@.*)\b", 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);
/// <summary>
/// Initializes a new instance of the <see cref="JetCommand"/> class.
/// </summary>
public JetCommand()
{
Initialize(null, null, null);
}
/// <summary>
/// Initializes a new instance of the <see cref="JetCommand"/> class.
/// </summary>
/// <param name="commandText">The command text.</param>
public JetCommand(string commandText)
{
this.Initialize(commandText, null, null);
}
/// <summary>
/// Initializes a new instance of the <see cref="JetCommand"/> class.
/// </summary>
/// <param name="commandText">The command text.</param>
/// <param name="connection">The connection.</param>
public JetCommand(string commandText, JetConnection connection)
{
this.Initialize(commandText, connection, null);
}
/// <summary>
/// Initializes a new instance of the <see cref="JetCommand"/> class.
/// </summary>
/// <param name="commandText">The command text.</param>
/// <param name="connection">The connection.</param>
/// <param name="transaction">The transaction.</param>
public JetCommand(string commandText, JetConnection connection, DbTransaction transaction)
{
Initialize(commandText, connection, transaction);
}
private void Initialize(string commandText, JetConnection connection, DbTransaction transaction)
{
_Connection = null;
_Transaction = null;
_DesignTimeVisible = true;
_WrappedCommand = new OleDbCommand();
this.CommandText = commandText;
this.Connection = connection;
this.Transaction = transaction;
}
protected override void Dispose(bool disposing)
{
if (disposing)
_WrappedCommand.Dispose();
base.Dispose(disposing);
}
/// <summary>
/// Attempts to Cancels the command execution
/// </summary>
public override void Cancel()
{
this._WrappedCommand.Cancel();
}
/// <summary>
/// Gets or sets the command text.
/// </summary>
/// <value>
/// The command text.
/// </value>
public override string CommandText
{
get { return this._WrappedCommand.CommandText; }
set { this._WrappedCommand.CommandText = value; }
}
/// <summary>
/// Gets or sets the command timeout.
/// </summary>
/// <value>
/// The command timeout.
/// </value>
public override int CommandTimeout
{
get { return this._WrappedCommand.CommandTimeout; }
set { this._WrappedCommand.CommandTimeout = value; }
}
/// <summary>
/// Gets or sets the type of the command.
/// </summary>
/// <value>
/// The type of the command.
/// </value>
public override CommandType CommandType
{
get { return this._WrappedCommand.CommandType; }
set { this._WrappedCommand.CommandType = value; }
}
/// <summary>
/// Creates the database parameter.
/// </summary>
/// <returns></returns>
protected override DbParameter CreateDbParameter()
{
return this._WrappedCommand.CreateParameter();
}
/// <summary>
/// Gets or sets the database connection.
/// </summary>
/// <value>
/// The database connection.
/// </value>
protected override DbConnection DbConnection
{
get { return this._Connection; }
set
{
if (value == null)
{
this._Connection = null;
}
else
{
if (!typeof(JetConnection).IsAssignableFrom(value.GetType()))
throw new InvalidOperationException("The JetCommand connection should be a JetConnection");
this._Connection = (JetConnection) value;
}
}
}
/// <summary>
/// Gets the database parameter collection.
/// </summary>
/// <value>
/// The database parameter collection.
/// </value>
protected override DbParameterCollection DbParameterCollection
{
get { return this._WrappedCommand.Parameters; }
}
/// <summary>
/// Gets or sets the database transaction.
/// </summary>
/// <value>
/// The database transaction.
/// </value>
protected override DbTransaction DbTransaction
{
get { return _Transaction; }
set { _Transaction = (JetTransaction) value; }
}
/// <summary>
/// Gets or sets a value indicating whether is design time visible.
/// </summary>
/// <value>
/// <c>true</c> if design time visible; otherwise, <c>false</c>.
/// </value>
public override bool DesignTimeVisible
{
get { return this._DesignTimeVisible; }
set { this._DesignTimeVisible = value; }
}
/// <summary>
/// Executes the database data reader.
/// </summary>
/// <param name="behavior">The behavior.</param>
/// <returns></returns>
protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior)
{
if (Connection == null)
throw new InvalidOperationException(Messages.PropertyNotInitialized(nameof(Connection)));
if (Connection.State != ConnectionState.Open)
throw new InvalidOperationException(Messages.CannotCallMethodInThisConnectionState("ExecuteReader", ConnectionState.Open, Connection.State));
_WrappedCommand.Connection = _Connection.InnerConnection;
// OLE DB forces us to use an existing active transaction, if one is available.
_WrappedCommand.Transaction = _Transaction?.WrappedTransaction ?? _Connection.ActiveTransaction?.WrappedTransaction;
LogHelper.ShowCommandText("ExecuteDbDataReader", _WrappedCommand);
DbDataReader dataReader;
if (JetStoreSchemaDefinitionRetrieve.TryGetDataReaderFromShowCommand(_WrappedCommand, out dataReader))
// Retrieve of store schema definition
return dataReader;
if (_WrappedCommand.CommandType != CommandType.Text)
return new JetDataReader(_WrappedCommand.ExecuteReader(behavior));
string[] commandTextList = SplitCommands(_WrappedCommand.CommandText);
dataReader = null;
for (int i = 0; i < commandTextList.Length; i++)
{
string commandText = commandTextList[i];
if ((dataReader = TryGetDataReaderForSelectRowCount(commandText)) != null)
continue;
commandText = ParseIdentity(commandText);
commandText = ParseGuid(commandText);
dataReader = InternalExecuteDbDataReader(commandText, behavior);
}
return dataReader;
}
private DbDataReader TryGetDataReaderForSelectRowCount(string commandText)
{
if (_selectRowCountRegularExpression.Match(commandText)
.Success)
{
if (_rowCount == null)
throw new InvalidOperationException("Invalid " + commandText + ". Run a DataReader before.");
DataTable dataTable = new DataTable("Rowcount");
dataTable.Columns.Add("ROWCOUNT", typeof(int));
dataTable.Rows.Add(_rowCount.Value);
return new DataTableReader(dataTable);
}
return null;
}
/// <summary>
/// Executes the non query.
/// </summary>
/// <returns></returns>
public override int ExecuteNonQuery()
{
if (Connection == null)
throw new InvalidOperationException(Messages.PropertyNotInitialized(nameof(Connection)));
LogHelper.ShowCommandText("ExecuteNonQuery", _WrappedCommand);
if (JetStoreDatabaseHandling.TryDatabaseOperation(_WrappedCommand.CommandText))
return 1;
if (JetRenameHandling.TryDatabaseOperation(Connection.ConnectionString, _WrappedCommand.CommandText))
return 1;
if (Connection.State != ConnectionState.Open)
throw new InvalidOperationException(Messages.CannotCallMethodInThisConnectionState(nameof(ExecuteNonQuery), ConnectionState.Open, Connection.State));
_WrappedCommand.Connection = _Connection.InnerConnection;
// OLE DB forces us to use an existing active transaction, if one is available.
_WrappedCommand.Transaction = _Transaction?.WrappedTransaction ?? _Connection.ActiveTransaction?.WrappedTransaction;
if (_WrappedCommand.CommandType != CommandType.Text)
return _WrappedCommand.ExecuteNonQuery();
string[] commandTextList = SplitCommands(_WrappedCommand.CommandText);
int returnValue = -1;
for (int i = 0; i < commandTextList.Length; i++)
{
string commandText = commandTextList[i];
if (_selectRowCountRegularExpression.Match(commandText)
.Success)
{
if (_rowCount == null)
throw new InvalidOperationException("Invalid " + commandText + ". Run a DataReader before.");
returnValue = _rowCount.Value;
continue;
}
commandText = ParseIdentity(commandText);
commandText = ParseGuid(commandText);
returnValue = InternalExecuteNonQuery(commandText);
}
return returnValue;
}
/// <summary>
/// Executes the query and returns the first column of the first row in the result set returned by the query. All other columns and rows are ignored
/// </summary>
/// <returns></returns>
public override object ExecuteScalar()
{
if (Connection == null)
throw new InvalidOperationException(Messages.PropertyNotInitialized(nameof(Connection)));
if (Connection.State != ConnectionState.Open)
throw new InvalidOperationException(Messages.CannotCallMethodInThisConnectionState(nameof(ExecuteScalar), ConnectionState.Open, Connection.State));
_WrappedCommand.Connection = _Connection.InnerConnection;
// OLE DB forces us to use an existing active transaction, if one is available.
_WrappedCommand.Transaction = _Transaction?.WrappedTransaction ?? _Connection.ActiveTransaction?.WrappedTransaction;
LogHelper.ShowCommandText("ExecuteScalar", _WrappedCommand);
DbDataReader dataReader;
if (JetStoreSchemaDefinitionRetrieve.TryGetDataReaderFromShowCommand(_WrappedCommand, out dataReader))
{
// Retrieve of store schema definition
if (dataReader.HasRows)
{
dataReader.Read();
return dataReader[0];
}
else
return DBNull.Value;
}
return this._WrappedCommand.ExecuteScalar();
}
private JetDataReader InternalExecuteDbDataReader(string commandText, CommandBehavior behavior)
{
int topCount;
int skipCount;
string newCommandText;
ParseSkipTop(commandText, out topCount, out skipCount, out newCommandText);
SortParameters(newCommandText, _WrappedCommand.Parameters);
FixParameters(_WrappedCommand.Parameters);
DbCommand command;
command = (DbCommand) ((ICloneable) this._WrappedCommand).Clone();
command.CommandText = newCommandText;
JetDataReader dataReader;
if (skipCount != 0)
dataReader = new JetDataReader(
command.ExecuteReader(behavior), topCount == -1
? 0
: topCount - skipCount, skipCount);
else if (topCount >= 0)
dataReader = new JetDataReader(command.ExecuteReader(behavior), topCount, 0);
else
dataReader = new JetDataReader(command.ExecuteReader(behavior));
_rowCount = dataReader.RecordsAffected;
return dataReader;
}
private int InternalExecuteNonQuery(string commandText)
{
// ReSharper disable NotAccessedVariable
int topCount;
int skipCount;
// ReSharper restore NotAccessedVariable
string newCommandText;
if (!CheckExists(commandText, out newCommandText))
return 0;
ParseSkipTop(newCommandText, out topCount, out skipCount, out newCommandText);
SortParameters(newCommandText, _WrappedCommand.Parameters);
FixParameters(_WrappedCommand.Parameters);
DbCommand command;
command = (DbCommand) ((ICloneable) this._WrappedCommand).Clone();
command.CommandText = newCommandText;
_rowCount = command.ExecuteNonQuery();
return _rowCount.Value;
}
private bool CheckExists(string commandText, out string newCommandText)
{
Match match = _ifStatementRegex.Match(commandText);
newCommandText = commandText;
if (!match.Success)
return true;
string not = match.Groups["not"]
.Value;
string sqlCheckCommand = match.Groups["sqlCheckCommand"]
.Value;
newCommandText = match.Groups["sqlCommand"]
.Value;
bool hasRows;
using (JetCommand command = (JetCommand) ((ICloneable) this).Clone())
{
command.CommandText = sqlCheckCommand;
using (var reader = command.ExecuteReader())
{
hasRows = reader.HasRows;
}
}
if (!string.IsNullOrWhiteSpace(not))
return !hasRows;
else
return hasRows;
}
private void FixParameters(DbParameterCollection parameters)
{
if (parameters.Count == 0)
return;
foreach (OleDbParameter parameter in parameters)
{
if (parameter.Value is TimeSpan)
parameter.Value = JetConfiguration.TimeSpanOffset + (TimeSpan) parameter.Value;
if (parameter.Value is DateTime)
{
// Hack: https://github.com/fsprojects/SQLProvider/issues/191
DateTime dt = (DateTime) parameter.Value;
parameter.Value = new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, dt.Kind);
}
}
}
private void SortParameters(string query, DbParameterCollection parameters)
{
if (parameters.Count == 0)
return;
var parameterArray = parameters.Cast<OleDbParameter>()
.ToArray();
// ReSharper disable once CoVariantArrayConversion
Array.Sort(parameterArray, new ParameterPositionComparer(query));
parameters.Clear();
foreach (OleDbParameter parameter in parameterArray)
parameters.Add(new OleDbParameter(parameter.ParameterName, parameter.Value));
}
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));
int xPosition = _query.IndexOf(x.ParameterName, StringComparison.Ordinal);
int 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)
{
string[] commandParts =
command.Replace("\r\n", "\n")
.Replace("\r", "\n")
.Split(new[] {";\n"}, StringSplitOptions.None);
List<string> commands = new List<string>(commandParts.Length);
foreach (string commandPart in commandParts)
{
if (!string.IsNullOrWhiteSpace(
commandPart.Replace("\n", "")
.Replace(";", "")))
commands.Add(commandPart);
}
return commands.ToArray();
}
private string ParseIdentity(string commandText)
{
if (commandText.ToLower()
.Contains("@@identity"))
{
DbCommand command;
command = (DbCommand) ((ICloneable) this._WrappedCommand).Clone();
command.CommandText = "Select @@identity";
object identity = command.ExecuteScalar();
int iIdentity = Convert.ToInt32(identity);
LogHelper.ShowInfo("@@identity = {0}", iIdentity);
return Regex.Replace(commandText, "@@identity", iIdentity.ToString(System.Globalization.CultureInfo.InvariantCulture), RegexOptions.IgnoreCase);
}
return commandText;
}
private string ParseGuid(string commandText)
{
while (commandText.ToLower()
.Contains("newguid()"))
{
_lastGuid = Guid.NewGuid();
commandText = Regex.Replace(commandText, @"newguid\(\)", string.Format("{{{0}}}", _lastGuid), RegexOptions.IgnoreCase);
}
if (commandText.ToLower()
.Contains("@@guid"))
{
LogHelper.ShowInfo("@@guid = {{{0}}}", _lastGuid);
commandText = Regex.Replace(commandText, "@@guid", string.Format("{{{0}}}", _lastGuid), RegexOptions.IgnoreCase);
}
return commandText;
}
private void ParseSkipTop(string commandText, out int topCount, out int skipCount, out string newCommandText)
{
newCommandText = commandText;
#region TOP clause
topCount = -1;
skipCount = 0;
var indexOfTop = newCommandText.IndexOf(" top ", StringComparison.InvariantCultureIgnoreCase);
while (indexOfTop != -1)
{
int indexOfTopEnd = newCommandText.IndexOf(" ", indexOfTop + 5, StringComparison.InvariantCultureIgnoreCase);
string stringTopCount = newCommandText.Substring(indexOfTop + 5, indexOfTopEnd - indexOfTop - 5)
.Trim();
string[] stringTopCountElements = stringTopCount.Split('+');
int topCount0;
int topCount1;
if (stringTopCountElements[0]
.StartsWith("@"))
topCount0 = Convert.ToInt32(
_WrappedCommand.Parameters[stringTopCountElements[0]]
.Value);
else if (!int.TryParse(stringTopCountElements[0], out topCount0))
throw new Exception("Invalid TOP clause parameter");
if (stringTopCountElements.Length == 1)
topCount1 = 0;
else if (stringTopCountElements[1]
.StartsWith("@"))
topCount1 = Convert.ToInt32(
_WrappedCommand.Parameters[stringTopCountElements[1]]
.Value);
else if (!int.TryParse(stringTopCountElements[1], out topCount1))
throw new Exception("Invalid TOP clause parameter");
int localTopCount = topCount0 + topCount1;
newCommandText = newCommandText.Remove(indexOfTop + 5, stringTopCount.Length)
.Insert(indexOfTop + 5, localTopCount.ToString());
if (indexOfTop <= 12)
topCount = localTopCount;
indexOfTop = newCommandText.IndexOf(" top ", indexOfTop + 5, StringComparison.InvariantCultureIgnoreCase);
}
#endregion
#region SKIP clause
Match matchSkipRegularExpression = _skipRegularExpression.Match(newCommandText);
if (matchSkipRegularExpression.Success)
{
string stringSkipCount;
stringSkipCount = matchSkipRegularExpression.Groups["stringSkipCount"]
.Value;
if (stringSkipCount.StartsWith("@"))
skipCount = Convert.ToInt32(
_WrappedCommand.Parameters[stringSkipCount]
.Value);
else if (!int.TryParse(stringSkipCount, out skipCount))
throw new Exception("Invalid SKIP clause parameter");
newCommandText = newCommandText.Remove(matchSkipRegularExpression.Index, matchSkipRegularExpression.Length);
}
#endregion
}
/// <summary>
/// Creates a prepared (or compiled) version of the command on the data source
/// </summary>
public override void Prepare()
{
if (Connection == null)
throw new InvalidOperationException(Messages.PropertyNotInitialized(nameof(Connection)));
if (Connection.State != ConnectionState.Open)
throw new InvalidOperationException(Messages.CannotCallMethodInThisConnectionState(nameof(Prepare), ConnectionState.Open, Connection.State));
_WrappedCommand.Connection = _Connection.InnerConnection;
// OLE DB forces us to use an existing active transaction, if one is available.
_WrappedCommand.Transaction = _Transaction?.WrappedTransaction ?? _Connection.ActiveTransaction?.WrappedTransaction;
this._WrappedCommand.Prepare();
}
/// <summary>
/// Gets or sets how command results are applied to the DataRow when used by the Update method of a DbDataAdapter.
/// </summary>
/// <value>
/// The updated row source.
/// </value>
public override UpdateRowSource UpdatedRowSource
{
get { return this._WrappedCommand.UpdatedRowSource; }
set { this._WrappedCommand.UpdatedRowSource = value; }
}
public static implicit operator OleDbCommand(JetCommand command)
{
return (OleDbCommand) command._WrappedCommand;
}
/// <summary>
/// Clones this instance.
/// </summary>
/// <returns>The created object</returns>
object ICloneable.Clone()
{
JetCommand clone = new JetCommand();
clone._Connection = this._Connection;
clone._WrappedCommand = (DbCommand) ((ICloneable) this._WrappedCommand).Clone();
return clone;
}
}
}