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(?@.*)\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)?\s*exists\s*\((?.+)\)\s*then\s*(?.*)$", RegexOptions.IgnoreCase); /// /// Initializes a new instance of the class. /// public JetCommand() { Initialize(null, null, null); } /// /// Initializes a new instance of the class. /// /// The command text. public JetCommand(string commandText) { this.Initialize(commandText, null, null); } /// /// Initializes a new instance of the class. /// /// The command text. /// The connection. public JetCommand(string commandText, JetConnection connection) { this.Initialize(commandText, connection, null); } /// /// Initializes a new instance of the class. /// /// The command text. /// The connection. /// The transaction. 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); } /// /// Attempts to Cancels the command execution /// public override void Cancel() { this._WrappedCommand.Cancel(); } /// /// Gets or sets the command text. /// /// /// The command text. /// public override string CommandText { get { return this._WrappedCommand.CommandText; } set { this._WrappedCommand.CommandText = value; } } /// /// Gets or sets the command timeout. /// /// /// The command timeout. /// public override int CommandTimeout { get { return this._WrappedCommand.CommandTimeout; } set { this._WrappedCommand.CommandTimeout = value; } } /// /// Gets or sets the type of the command. /// /// /// The type of the command. /// public override CommandType CommandType { get { return this._WrappedCommand.CommandType; } set { this._WrappedCommand.CommandType = value; } } /// /// Creates the database parameter. /// /// protected override DbParameter CreateDbParameter() { return this._WrappedCommand.CreateParameter(); } /// /// Gets or sets the database connection. /// /// /// The database connection. /// 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; } } } /// /// Gets the database parameter collection. /// /// /// The database parameter collection. /// protected override DbParameterCollection DbParameterCollection { get { return this._WrappedCommand.Parameters; } } /// /// Gets or sets the database transaction. /// /// /// The database transaction. /// protected override DbTransaction DbTransaction { get { return _Transaction; } set { _Transaction = (JetTransaction) value; } } /// /// Gets or sets a value indicating whether is design time visible. /// /// /// true if design time visible; otherwise, false. /// public override bool DesignTimeVisible { get { return this._DesignTimeVisible; } set { this._DesignTimeVisible = value; } } /// /// Executes the database data reader. /// /// The behavior. /// 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; } /// /// Executes the non query. /// /// 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; } /// /// 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 /// /// 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() .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 { 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 commands = new List(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 } /// /// Creates a prepared (or compiled) version of the command on the data source /// 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(); } /// /// Gets or sets how command results are applied to the DataRow when used by the Update method of a DbDataAdapter. /// /// /// The updated row source. /// 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; } /// /// Clones this instance. /// /// The created object object ICloneable.Clone() { JetCommand clone = new JetCommand(); clone._Connection = this._Connection; clone._WrappedCommand = (DbCommand) ((ICloneable) this._WrappedCommand).Clone(); return clone; } } }