// Copyright (c) 2004, 2021, Oracle and/or its affiliates. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License, version 2.0, as // published by the Free Software Foundation. // // This program is also distributed with certain software (including // but not limited to OpenSSL) that is licensed under separate terms, // as designated in a particular file or component or in included license // documentation. The authors of MySQL hereby grant you an // additional permission to link the program and your derivative works // with the separately licensed software that they have included with // MySQL. // // Without limiting anything contained in the foregoing, this file, // which is part of MySQL Connector/NET, is also subject to the // Universal FOSS Exception, version 1.0, a copy of which can be found at // http://oss.oracle.com/licenses/universal-foss-exception. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // See the GNU General Public License, version 2.0, for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA using MySql.Data.Common; using MySql.Data.MySqlClient.Replication; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Data.Common; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using Sog.Properties; namespace MySql.Data.MySqlClient { /// public sealed partial class MySqlCommand : DbCommand, IDisposable { MySqlConnection connection; string cmdText; private PreparableStatement statement; private int commandTimeout; private bool resetSqlSelect; CommandTimer commandTimer; private bool useDefaultTimeout; private static List keywords = null; private bool disposed = false; /// public MySqlCommand() { this.CommandType = System.Data.CommandType.Text; this.Parameters = new MySqlParameterCollection(this); this.Attributes = new MySqlAttributeCollection(this); this.cmdText = String.Empty; this.useDefaultTimeout = true; this.UpdatedRowSource = UpdateRowSource.Both; } /// public MySqlCommand(string cmdText) : this() { this.CommandText = cmdText; } /// public MySqlCommand(string cmdText, MySqlConnection connection) : this(cmdText) { this.Connection = connection; } /// public MySqlCommand(string cmdText, MySqlConnection connection, MySqlTransaction transaction) : this(cmdText, connection) { this.Transaction = transaction; } #region Destructor ~MySqlCommand() { this.Dispose(false); } #endregion #region Properties /// /// Gets the last inserted id. /// /// [Browsable(false)] public Int64 LastInsertedId { get; internal set; } /// [Category("Data")] [Description("Command text to execute")] #if NET452 [Editor("MySql.Data.Common.Design.SqlCommandTextEditor,MySqlClient.Design", typeof(System.Drawing.Design.UITypeEditor))] #endif public override string CommandText { get { return this.cmdText; } set { this.cmdText = value ?? string.Empty; this.statement = null; this.BatchableCommandText = null; if (this.cmdText != null && this.cmdText.EndsWith("DEFAULT VALUES", StringComparison.OrdinalIgnoreCase)) { this.cmdText = this.cmdText.Substring(0, this.cmdText.Length - 14); this.cmdText = this.cmdText + "() VALUES ()"; } } } /// [Category("Misc")] [Description("Time to wait for command to execute")] [DefaultValue(30)] public override int CommandTimeout { get { return this.useDefaultTimeout ? 30 : this.commandTimeout; } set { if (value < 0) { this.Throw(new ArgumentException("Command timeout must not be negative")); } // Timeout in milliseconds should not exceed maximum for 32 bit // signed integer (~24 days), because underlying driver (and streams) // use milliseconds expressed ints for timeout values. // Hence, truncate the value. int timeout = Math.Min(value, Int32.MaxValue / 1000); if (timeout != value) { MySqlTrace.LogWarning( this.connection.ServerThread, "Command timeout value too large (" + value + " seconds). Changed to max. possible value (" + timeout + " seconds)"); } this.commandTimeout = timeout; this.useDefaultTimeout = false; } } /// [Category("Data")] public override CommandType CommandType { get; set; } /// /// Gets a boolean value that indicates whether the Prepared method has been called. /// /// [Browsable(false)] public bool IsPrepared => this.statement != null && this.statement.IsPrepared; /// [Category("Behavior")] [Description("Connection used by the command")] public new MySqlConnection Connection { get { return this.connection; } set { /* * The connection is associated with the transaction * so set the transaction object to return a null reference if the connection * is reset. */ if (this.connection != value) { this.Transaction = null; } this.connection = value; // if the user has not already set the command timeout, then // take the default from the connection if (this.connection == null) { return; } if (this.useDefaultTimeout) { this.commandTimeout = (int)this.connection.Settings.DefaultCommandTimeout; this.useDefaultTimeout = false; } this.EnableCaching = this.connection.Settings.TableCaching; this.CacheAge = this.connection.Settings.DefaultTableCacheAge; } } /// [Category("Data")] [Description("The parameters collection")] [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] public new MySqlParameterCollection Parameters { get; } /// [Category("Data")] [Description("The attributes collection")] [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] public MySqlAttributeCollection Attributes { get; } /// [Browsable(false)] public new MySqlTransaction Transaction { get; set; } /// /// Gets or sets a boolean value that indicates whether caching is enabled. /// public bool EnableCaching { get; set; } /// /// Gets or sets the seconds for how long a TableDirect result should be cached. /// public int CacheAge { get; set; } internal List Batch { get; private set; } internal bool Canceled { get; private set; } internal string BatchableCommandText { get; private set; } internal bool InternallyCreated { get; set; } /// /// Gets or sets how command results are applied to the DataRow when used by the /// Update method of the DbDataAdapter. /// public override UpdateRowSource UpdatedRowSource { get; set; } /// /// Gets or sets a value indicating whether the command object should be visible in a Windows Form Designer control. /// [Browsable(false)] public override bool DesignTimeVisible { get; set; } protected override DbParameter CreateDbParameter() { return new MySqlParameter(); } protected override DbConnection DbConnection { get { return this.Connection; } set { this.Connection = (MySqlConnection)value; } } protected override DbParameterCollection DbParameterCollection { get { return this.Parameters; } } protected override DbTransaction DbTransaction { get { return this.Transaction; } set { this.Transaction = (MySqlTransaction)value; } } #endregion #region Methods /// /// Attempts to cancel the execution of a currently active command /// /// /// Cancelling a currently active query only works with MySQL versions 5.0.0 and higher. /// public override void Cancel() { if (this.connection != null) { this.connection.CancelQuery(this.connection.ConnectionTimeout); } this.Canceled = true; } /// /// Creates a new instance of a object. /// /// /// This method is a strongly-typed version of . /// /// A object. /// public new MySqlParameter CreateParameter() { return (MySqlParameter)this.CreateDbParameter(); } /// /// Check the connection to make sure /// - it is open /// - it is not currently being used by a reader /// - and we have the right version of MySQL for the requested command type /// private void CheckState() { // There must be a valid and open connection. if (this.connection == null) { this.Throw(new InvalidOperationException("Connection must be valid and open.")); } if (this.connection.State != ConnectionState.Open && !this.connection.SoftClosed) { this.Throw(new InvalidOperationException("Connection must be valid and open.")); } // Data readers have to be closed first if (this.connection.IsInUse && !this.InternallyCreated) { this.Throw(new MySqlException("There is already an open DataReader associated with this Connection which must be closed first.")); } } /// public override int ExecuteNonQuery() { int records = -1; // give our interceptors a shot at it first if (this.connection?.commandInterceptor != null && this.connection.commandInterceptor.ExecuteNonQuery(this.CommandText, ref records)) { return records; } // ok, none of our interceptors handled this so we default using (MySqlDataReader reader = this.ExecuteReader()) { reader.Close(); return reader.RecordsAffected; } } internal void ClearCommandTimer() { if (this.commandTimer == null) { return; } this.commandTimer.Dispose(); this.commandTimer = null; } internal void Close(MySqlDataReader reader) { this.statement?.Close(reader); this.ResetSqlSelectLimit(); if (this.statement != null) { this.connection?.driver?.CloseQuery(this.connection, this.statement.StatementId); } this.ClearCommandTimer(); } protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) { return this.ExecuteReader(behavior); } /// /// Reset reader to null, to avoid "There is already an open data reader" /// on the next ExecuteReader(). Used in error handling scenarios. /// private void ResetReader() { if (this.connection?.Reader == null) { return; } this.connection.Reader.Close(); this.connection.Reader = null; } /// /// Reset SQL_SELECT_LIMIT that could have been modified by CommandBehavior. /// internal void ResetSqlSelectLimit() { // if we are supposed to reset the sql select limit, do that here if (!this.resetSqlSelect) { return; } this.resetSqlSelect = false; MySqlCommand command = new MySqlCommand("SET SQL_SELECT_LIMIT=DEFAULT", this.connection); command.InternallyCreated = true; command.ExecuteNonQuery(); } /// public new MySqlDataReader ExecuteReader() { return this.ExecuteReader(CommandBehavior.Default); } /// public new MySqlDataReader ExecuteReader(CommandBehavior behavior) { // give our interceptors a shot at it first MySqlDataReader interceptedReader = null; if (this.connection?.commandInterceptor != null && this.connection.commandInterceptor.ExecuteReader(this.CommandText, behavior, ref interceptedReader)) { return interceptedReader; } // interceptors didn't handle this so we fall through bool success = false; this.CheckState(); Driver driver = this.connection.driver; this.cmdText = this.cmdText.Trim(); if (String.IsNullOrEmpty(this.cmdText)) { this.Throw(new InvalidOperationException(Resources.CommandTextNotInitialized)); } string sql = this.cmdText.Trim(';'); // Load balancing getting a new connection if (this.connection.hasBeenOpen && !driver.HasStatus(ServerStatusFlags.InTransaction)) { ReplicationManager.GetNewConnection(this.connection.Settings.Server, !this.IsReadOnlyCommand(sql), this.connection); } lock (driver) { // We have to recheck that there is no reader, after we got the lock if (this.connection.Reader != null) { this.Throw(new MySqlException(Resources.DataReaderOpen)); } System.Transactions.Transaction curTrans = System.Transactions.Transaction.Current; if (curTrans != null) { bool inRollback = false; // TODO: ADD support for 452 and 46X if (driver.currentTransaction != null) { inRollback = driver.currentTransaction.InRollback; } if (!inRollback) { System.Transactions.TransactionStatus status = System.Transactions.TransactionStatus.InDoubt; try { // in some cases (during state transitions) this throws // an exception. Ignore exceptions, we're only interested // whether transaction was aborted or not. status = curTrans.TransactionInformation.Status; } catch (System.Transactions.TransactionException) { } if (status == System.Transactions.TransactionStatus.Aborted) { this.Throw(new System.Transactions.TransactionAbortedException()); } } } this.commandTimer = new CommandTimer(this.connection, this.CommandTimeout); this.LastInsertedId = -1; if (this.CommandType == CommandType.TableDirect) { sql = "SELECT * FROM " + sql; } else if (this.CommandType == CommandType.Text) { // validates single word statetment (maybe is a stored procedure call) if (sql.IndexOf(" ") == -1) { if (this.AddCallStatement(sql)) { sql = "call " + sql; } } } // if we are on a replicated connection, we are only allow readonly statements if (this.connection.Settings.Replication && !this.InternallyCreated) { this.EnsureCommandIsReadOnly(sql); } if (this.statement == null || !this.statement.IsPrepared) { if (this.CommandType == CommandType.StoredProcedure) { this.statement = new StoredProcedure(this, sql); } else { this.statement = new PreparableStatement(this, sql); } } // stored procs are the only statement type that need do anything during resolve this.statement.Resolve(false); // Now that we have completed our resolve step, we can handle our // command behaviors this.HandleCommandBehaviors(behavior); try { MySqlDataReader reader = new MySqlDataReader(this, this.statement, behavior); this.connection.Reader = reader; this.Canceled = false; // execute the statement this.statement.Execute(); // wait for data to return reader.NextResult(); success = true; return reader; } catch (TimeoutException tex) { this.connection.HandleTimeoutOrThreadAbort(tex); throw; // unreached } catch (ThreadAbortException taex) { this.connection.HandleTimeoutOrThreadAbort(taex); throw; } catch (IOException ioex) { this.connection.Abort(); // Closes connection without returning it to the pool throw new MySqlException(Resources.FatalErrorDuringExecute, ioex); } catch (MySqlException ex) { if (ex.InnerException is TimeoutException) { throw; // already handled } try { this.ResetReader(); this.ResetSqlSelectLimit(); } catch (Exception) { // Reset SqlLimit did not work, connection is hosed. this.Connection.Abort(); throw new MySqlException(ex.Message, true, ex); } // if we caught an exception because of a cancel, then just return null if (ex.IsQueryAborted) { return null; } if (ex.IsFatal) { this.Connection.Close(); } if (ex.Number == 0) { throw new MySqlException(Resources.FatalErrorDuringExecute, ex); } throw; } finally { if (this.connection != null) { if (this.connection.Reader == null) { // Something went seriously wrong, and reader would not // be able to clear timeout on closing. // So we clear timeout here. this.ClearCommandTimer(); } if (!success) { // ExecuteReader failed.Close Reader and set to null to // prevent subsequent errors with DataReaderOpen this.ResetReader(); } } } } } private void EnsureCommandIsReadOnly(string sql) { sql = StringUtility.ToLowerInvariant(sql); if (!sql.StartsWith("select") && !sql.StartsWith("show")) { this.Throw(new MySqlException(Resources.ReplicatedConnectionsAllowOnlyReadonlyStatements)); } if (sql.EndsWith("for update") || sql.EndsWith("lock in share mode")) { this.Throw(new MySqlException(Resources.ReplicatedConnectionsAllowOnlyReadonlyStatements)); } } private bool IsReadOnlyCommand(string sql) { sql = sql.ToLower(); return (sql.StartsWith("select") || sql.StartsWith("show")) && !(sql.EndsWith("for update") || sql.EndsWith("lock in share mode")); } /// public override object ExecuteScalar() { this.LastInsertedId = -1; object val = null; // give our interceptors a shot at it first if (this.connection != null && this.connection.commandInterceptor.ExecuteScalar(this.CommandText, ref val)) { return val; } using (MySqlDataReader reader = this.ExecuteReader()) { if (reader.Read()) { val = reader.GetValue(0); } } return val; } private void HandleCommandBehaviors(CommandBehavior behavior) { if ((behavior & CommandBehavior.SchemaOnly) != 0) { new MySqlCommand("SET SQL_SELECT_LIMIT=0", this.connection).ExecuteNonQuery(); this.resetSqlSelect = true; } else if ((behavior & CommandBehavior.SingleRow) != 0) { new MySqlCommand("SET SQL_SELECT_LIMIT=1", this.connection).ExecuteNonQuery(); this.resetSqlSelect = true; } } /// private void Prepare(int cursorPageSize) { using (new CommandTimer(this.Connection, this.CommandTimeout)) { // if the length of the command text is zero, then just return string psSQL = this.CommandText; if (psSQL == null || psSQL.Trim().Length == 0) { return; } this.statement = this.CommandType == CommandType.StoredProcedure ? new StoredProcedure(this, this.CommandText) : new PreparableStatement(this, this.CommandText); this.statement.Resolve(true); this.statement.Prepare(); } } /// public override void Prepare() { if (this.connection == null) { this.Throw(new InvalidOperationException("The connection property has not been set.")); } if (this.connection.State != ConnectionState.Open) { this.Throw(new InvalidOperationException("The connection is not open.")); } this.Prepare(0); } #endregion #region Async Methods private IAsyncResult asyncResult; internal delegate object AsyncDelegate(int type, CommandBehavior behavior); internal AsyncDelegate Caller; internal Exception thrownException; internal object AsyncExecuteWrapper(int type, CommandBehavior behavior) { this.thrownException = null; try { if (type == 1) { return this.ExecuteReader(behavior); } return this.ExecuteNonQuery(); } catch (Exception ex) { this.thrownException = ex; } return null; } /// /// Initiates the asynchronous execution of the SQL statement or stored procedure /// that is described by this , and retrieves one or more /// result sets from the server. /// /// An that can be used to poll, wait for results, /// or both; this value is also needed when invoking EndExecuteReader, /// which returns a instance that can be used to retrieve /// the returned rows. public IAsyncResult BeginExecuteReader() { return this.BeginExecuteReader(CommandBehavior.Default); } /// /// Initiates the asynchronous execution of the SQL statement or stored procedure /// that is described by this using one of the /// CommandBehavior values. /// /// One of the values, indicating /// options for statement execution and data retrieval. /// An that can be used to poll, wait for results, /// or both; this value is also needed when invoking EndExecuteReader, /// which returns a instance that can be used to retrieve /// the returned rows. public IAsyncResult BeginExecuteReader(CommandBehavior behavior) { if (this.Caller != null) { this.Throw(new MySqlException(Resources.UnableToStartSecondAsyncOp)); } this.Caller = this.AsyncExecuteWrapper; this.asyncResult = this.Caller.BeginInvoke(1, behavior, null, null); return this.asyncResult; } /// /// Finishes asynchronous execution of a SQL statement, returning the requested /// . /// /// The returned by the call to /// . /// A MySqlDataReader object that can be used to retrieve the requested rows. public MySqlDataReader EndExecuteReader(IAsyncResult result) { result.AsyncWaitHandle.WaitOne(); AsyncDelegate c = this.Caller; this.Caller = null; if (this.thrownException != null) { throw this.thrownException; } return (MySqlDataReader)c.EndInvoke(result); } /// /// Initiates the asynchronous execution of the SQL statement or stored procedure /// that is described by this . /// /// /// An delegate that is invoked when the command's /// execution has completed. Pass a null reference (Nothing in Visual Basic) /// to indicate that no callback is required. /// A user-defined state object that is passed to the /// callback procedure. Retrieve this object from within the callback procedure /// using the property. /// An that can be used to poll or wait for results, /// or both; this value is also needed when invoking , /// which returns the number of affected rows. public IAsyncResult BeginExecuteNonQuery(AsyncCallback callback, object stateObject) { if (this.Caller != null) { this.Throw(new MySqlException(Resources.UnableToStartSecondAsyncOp)); } this.Caller = this.AsyncExecuteWrapper; this.asyncResult = this.Caller.BeginInvoke(2, CommandBehavior.Default, callback, stateObject); return this.asyncResult; } /// /// Initiates the asynchronous execution of the SQL statement or stored procedure /// that is described by this . /// /// An that can be used to poll or wait for results, /// or both; this value is also needed when invoking , /// which returns the number of affected rows. public IAsyncResult BeginExecuteNonQuery() { if (this.Caller != null) { this.Throw(new MySqlException(Resources.UnableToStartSecondAsyncOp)); } this.Caller = this.AsyncExecuteWrapper; this.asyncResult = this.Caller.BeginInvoke(2, CommandBehavior.Default, null, null); return this.asyncResult; } /// /// Finishes asynchronous execution of a SQL statement. /// /// The returned by the call /// to . /// public int EndExecuteNonQuery(IAsyncResult asyncResult) { asyncResult.AsyncWaitHandle.WaitOne(); AsyncDelegate c = this.Caller; this.Caller = null; if (this.thrownException != null) { throw this.thrownException; } return (int)c.EndInvoke(asyncResult); } #endregion #region Private Methods /* private ArrayList PrepareSqlBuffers(string sql) { ArrayList buffers = new ArrayList(); MySqlStreamWriter writer = new MySqlStreamWriter(new MemoryStream(), connection.Encoding); writer.Version = connection.driver.Version; // if we are executing as a stored procedure, then we need to add the call // keyword. if (CommandType == CommandType.StoredProcedure) { if (storedProcedure == null) storedProcedure = new StoredProcedure(this); sql = storedProcedure.Prepare( CommandText ); } // tokenize the SQL sql = sql.TrimStart(';').TrimEnd(';'); ArrayList tokens = TokenizeSql( sql ); foreach (string token in tokens) { if (token.Trim().Length == 0) continue; if (token == ";" && ! connection.driver.SupportsBatch) { MemoryStream ms = (MemoryStream)writer.Stream; if (ms.Length > 0) buffers.Add( ms ); writer = new MySqlStreamWriter(new MemoryStream(), connection.Encoding); writer.Version = connection.driver.Version; continue; } else if (token[0] == parameters.ParameterMarker) { if (SerializeParameter(writer, token)) continue; } // our fall through case is to write the token to the byte stream writer.WriteStringNoNull(token); } // capture any buffer that is left over MemoryStream mStream = (MemoryStream)writer.Stream; if (mStream.Length > 0) buffers.Add( mStream ); return buffers; }*/ internal long EstimatedSize() { return this.CommandText.Length + this.Parameters.Cast().Sum(parameter => parameter.EstimatedSize()); } /// /// Verifies if a query is valid even if it has not spaces or is a stored procedure call /// /// Query to validate /// If it is necessary to add call statement private bool AddCallStatement(string query) { if (string.IsNullOrEmpty(query)) { return false; } string keyword = query.ToUpper(); int indexChar = keyword.IndexOfAny(new char[] { '(', '"', '@', '\'', '`' }); if (indexChar > 0) { keyword = keyword.Substring(0, indexChar); } if (keywords == null) { keywords = SchemaProvider.GetReservedWords().AsDataTable(). Select(). Select(x => x[0].ToString()).ToList(); } return !keywords.Contains(keyword); } #endregion #region Batching support internal void AddToBatch(MySqlCommand command) { if (this.Batch == null) { this.Batch = new List(); } this.Batch.Add(command); } internal string GetCommandTextForBatching() { if (this.BatchableCommandText == null) { // if the command starts with insert and is "simple" enough, then // we can use the multi-value form of insert if (String.Compare(this.CommandText.Substring(0, 6), "INSERT", StringComparison.OrdinalIgnoreCase) == 0) { MySqlCommand cmd = new MySqlCommand("SELECT @@sql_mode", this.Connection); string sql_mode = StringUtility.ToUpperInvariant(cmd.ExecuteScalar().ToString()); MySqlTokenizer tokenizer = new MySqlTokenizer(this.CommandText); tokenizer.AnsiQuotes = sql_mode.IndexOf("ANSI_QUOTES") != -1; tokenizer.BackslashEscapes = sql_mode.IndexOf("NO_BACKSLASH_ESCAPES") == -1; string token = StringUtility.ToLowerInvariant(tokenizer.NextToken()); while (token != null) { if (StringUtility.ToUpperInvariant(token) == "VALUES" && !tokenizer.Quoted) { token = tokenizer.NextToken(); Debug.Assert(token == "("); // find matching right paren, and ensure that parens // are balanced. int openParenCount = 1; while (token != null) { this.BatchableCommandText += token; token = tokenizer.NextToken(); if (token == "(") { openParenCount++; } else if (token == ")") { openParenCount--; } if (openParenCount == 0) { break; } } if (token != null) { this.BatchableCommandText += token; } token = tokenizer.NextToken(); if (token != null && (token == "," || StringUtility.ToUpperInvariant(token) == "ON")) { this.BatchableCommandText = null; break; } } token = tokenizer.NextToken(); } } // Otherwise use the command verbatim else { this.BatchableCommandText = this.CommandText; } } return this.BatchableCommandText; } #endregion // This method is used to throw all exceptions from this class. private void Throw(Exception ex) { this.connection?.Throw(ex); throw ex; } public new void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } protected override void Dispose(bool disposing) { if (this.disposed) { return; } if (!disposing) { return; } if (this.statement != null && this.statement.IsPrepared) { this.statement.CloseStatement(); } base.Dispose(disposing); this.disposed = true; } } }