// Copyright ?2004, 2018 Oracle and/or its affiliates. All rights reserved. // // MySQL Connector/NET is licensed under the terms of the GPLv2 // , like most // MySQL Connectors. There are special exceptions to the terms and // conditions of the GPLv2 as it is applied to this software, see the // FLOSS License Exception // . // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published // by the Free Software Foundation; version 2 of the License. // // 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 // 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 #if MYSQL_6_9 using System; using System.IO; using System.Collections; using System.Text; #if !RT using System.Data; using System.Data.Common; #endif using Externals.MySql.Data.Common; using System.ComponentModel; using System.Threading; using System.Diagnostics; using System.Globalization; using System.Collections.Generic; using Externals.MySql.Data.MySqlClient.Properties; using Externals.MySql.Data.MySqlClient.Replication; namespace Externals.MySql.Data.MySqlClient { internal sealed partial class MySqlCommand : ICloneable, IDisposable { MySqlConnection connection; MySqlTransaction curTransaction; string cmdText; CommandType cmdType; // long updatedRowCount = -1L; MySqlParameterCollection parameters; private IAsyncResult asyncResult; internal Int64 lastInsertedId; private PreparableStatement statement; private int commandTimeout; private bool canceled; private bool resetSqlSelect; List batch; private string batchableCommandText; CommandTimer commandTimer; private bool useDefaultTimeout; private bool shouldCache; private int cacheAge; private bool internallyCreated; private static List keywords = null; private bool disposed = false; public MySqlCommand() { cmdType = CommandType.Text; parameters = new MySqlParameterCollection(this); cmdText = String.Empty; useDefaultTimeout = true; Constructor(); } partial void Constructor(); public MySqlCommand(string cmdText) : this() { CommandText = cmdText; } public MySqlCommand(string cmdText, MySqlConnection connection) : this(cmdText) { Connection = connection; } public MySqlCommand(string cmdText, MySqlConnection connection, MySqlTransaction transaction) : this(cmdText, connection) { curTransaction = transaction; } #region Destructor ~MySqlCommand() { Dispose(false); } #endregion #region Properties [Browsable(false)] public Int64 LastInsertedId { get { return lastInsertedId; } } [Category("Data")] [Description("Command text to execute")] [Editor("Externals.MySql.Data.Common.Design.SqlCommandTextEditor,MySqlClient.Design", typeof(System.Drawing.Design.UITypeEditor))] public override string CommandText { get { return cmdText; } set { cmdText = value ?? string.Empty; statement = null; batchableCommandText = null; if (cmdText != null && cmdText.EndsWith("DEFAULT VALUES", StringComparison.OrdinalIgnoreCase)) { cmdText = cmdText.Substring(0, cmdText.Length - 14); cmdText = cmdText + "() VALUES ()"; } } } [Category("Misc")] [Description("Time to wait for command to execute")] [DefaultValue(30)] public override int CommandTimeout { get { return useDefaultTimeout ? 30 : commandTimeout; } set { if (commandTimeout < 0) 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(connection.ServerThread, "Command timeout value too large (" + value + " seconds). Changed to max. possible value (" + timeout + " seconds)"); } commandTimeout = timeout; useDefaultTimeout = false; } } [Category("Data")] public override CommandType CommandType { get { return cmdType; } set { cmdType = value; } } [Browsable(false)] public bool IsPrepared { get { return statement != null && statement.IsPrepared; } } [Category("Behavior")] [Description("Connection used by the command")] public new MySqlConnection Connection { get { return 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 (connection != value) Transaction = null; connection = value; // if the user has not already set the command timeout, then // take the default from the connection if (connection != null) { if (useDefaultTimeout) { commandTimeout = (int)connection.Settings.DefaultCommandTimeout; useDefaultTimeout = false; } EnableCaching = connection.Settings.TableCaching; CacheAge = connection.Settings.DefaultTableCacheAge; } } } [Category("Data")] [Description("The parameters collection")] [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] public new MySqlParameterCollection Parameters { get { return parameters; } } [Browsable(false)] public new MySqlTransaction Transaction { get { return curTransaction; } set { curTransaction = value; } } public bool EnableCaching { get { return shouldCache; } set { shouldCache = value; } } public int CacheAge { get { return cacheAge; } set { cacheAge = value; } } internal List Batch { get { return batch; } } internal bool Canceled { get { return canceled; } } internal string BatchableCommandText { get { return batchableCommandText; } } internal bool InternallyCreated { get { return internallyCreated; } set { internallyCreated = 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() { connection.CancelQuery(connection.ConnectionTimeout); 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)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 (connection == null) Throw(new InvalidOperationException("Connection must be valid and open.")); if (connection.State != ConnectionState.Open && !connection.SoftClosed) Throw(new InvalidOperationException("Connection must be valid and open.")); // Data readers have to be closed first if (connection.IsInUse && !this.internallyCreated) 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 (connection != null && connection.commandInterceptor != null && connection.commandInterceptor.ExecuteNonQuery(CommandText, ref records)) return records; // ok, none of our interceptors handled this so we default using (MySqlDataReader reader = ExecuteReader()) { reader.Close(); return reader.RecordsAffected; } } internal void ClearCommandTimer() { if (commandTimer != null) { commandTimer.Dispose(); commandTimer = null; } } internal void Close(MySqlDataReader reader) { if (statement != null) statement.Close(reader); ResetSqlSelectLimit(); if (statement != null && connection != null && connection.driver != null) connection.driver.CloseQuery(connection, statement.StatementId); ClearCommandTimer(); } /// /// 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 (connection != null && connection.Reader != null) { connection.Reader.Close(); 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 (resetSqlSelect) { resetSqlSelect = false; MySqlCommand command = new MySqlCommand("SET SQL_SELECT_LIMIT=DEFAULT", connection); command.internallyCreated = true; command.ExecuteNonQuery(); } } public new MySqlDataReader ExecuteReader() { return ExecuteReader(CommandBehavior.Default); } public new MySqlDataReader ExecuteReader(CommandBehavior behavior) { // give our interceptors a shot at it first MySqlDataReader interceptedReader = null; if (connection != null && connection.commandInterceptor != null && connection.commandInterceptor.ExecuteReader(CommandText, behavior, ref interceptedReader)) return interceptedReader; // interceptors didn't handle this so we fall through bool success = false; CheckState(); Driver driver = connection.driver; cmdText = cmdText.Trim(); if (String.IsNullOrEmpty(cmdText)) Throw(new InvalidOperationException(Resources.CommandTextNotInitialized)); string sql = cmdText.Trim(';'); // Load balancing getting a new connection if (connection.hasBeenOpen && !driver.HasStatus(ServerStatusFlags.InTransaction)) { ReplicationManager.GetNewConnection(connection.Settings.Server, !IsReadOnlyCommand(sql), connection); } lock (driver) { // We have to recheck that there is no reader, after we got the lock if (connection.Reader != null) { Throw(new MySqlException(Resources.DataReaderOpen)); } System.Transactions.Transaction curTrans = System.Transactions.Transaction.Current; if (curTrans != null) { bool inRollback = false; 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) Throw(new System.Transactions.TransactionAbortedException()); } } commandTimer = new CommandTimer(connection, CommandTimeout); lastInsertedId = -1; if (CommandType == CommandType.TableDirect) sql = "SELECT * FROM " + sql; else if (CommandType == CommandType.Text) { // validates single word statetment (maybe is a stored procedure call) if (sql.IndexOf(" ") == -1) { if (AddCallStatement(sql)) sql = "call " + sql; } } // if we are on a replicated connection, we are only allow readonly statements if (connection.Settings.Replication && !InternallyCreated) EnsureCommandIsReadOnly(sql); if (statement == null || !statement.IsPrepared) { if (CommandType == CommandType.StoredProcedure) statement = new StoredProcedure(this, sql); else statement = new PreparableStatement(this, sql); } // stored procs are the only statement type that need do anything during resolve statement.Resolve(false); // Now that we have completed our resolve step, we can handle our // command behaviors HandleCommandBehaviors(behavior); // updatedRowCount = -1; try { MySqlDataReader reader = new MySqlDataReader(this, statement, behavior); connection.Reader = reader; canceled = false; // execute the statement statement.Execute(); // wait for data to return reader.NextResult(); success = true; return reader; } catch (TimeoutException tex) { connection.HandleTimeoutOrThreadAbort(tex); throw; //unreached } catch (ThreadAbortException taex) { connection.HandleTimeoutOrThreadAbort(taex); throw; } catch (IOException ioex) { 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 { ResetReader(); ResetSqlSelectLimit(); } catch (Exception) { // Reset SqlLimit did not work, connection is hosed. 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) Connection.Close(); if (ex.Number == 0) throw new MySqlException(Resources.FatalErrorDuringExecute, ex); throw; } finally { if (connection != null) { if (connection.Reader == null) { // Something went seriously wrong, and reader would not // be able to clear timeout on closing. // So we clear timeout here. ClearCommandTimer(); } if (!success) { // ExecuteReader failed.Close Reader and set to null to // prevent subsequent errors with DataReaderOpen ResetReader(); } } } } } private void EnsureCommandIsReadOnly(string sql) { sql = StringUtility.ToLowerInvariant(sql); if (!sql.StartsWith("select") && !sql.StartsWith("show")) Throw(new MySqlException(Resources.ReplicatedConnectionsAllowOnlyReadonlyStatements)); if (sql.EndsWith("for update") || sql.EndsWith("lock in share mode")) 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() { lastInsertedId = -1; object val = null; // give our interceptors a shot at it first if (connection != null && connection.commandInterceptor.ExecuteScalar(CommandText, ref val)) return val; using (MySqlDataReader reader = 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", connection).ExecuteNonQuery(); resetSqlSelect = true; } else if ((behavior & CommandBehavior.SingleRow) != 0) { new MySqlCommand("SET SQL_SELECT_LIMIT=1", connection).ExecuteNonQuery(); resetSqlSelect = true; } } private void Prepare(int cursorPageSize) { using (new CommandTimer(Connection, CommandTimeout)) { // if the length of the command text is zero, then just return string psSQL = CommandText; if (psSQL == null || psSQL.Trim().Length == 0) return; if (CommandType == CommandType.StoredProcedure) statement = new StoredProcedure(this, CommandText); else statement = new PreparableStatement(this, CommandText); statement.Resolve(true); statement.Prepare(); } } public override void Prepare() { if (connection == null) Throw(new InvalidOperationException("The connection property has not been set.")); if (connection.State != ConnectionState.Open) Throw(new InvalidOperationException("The connection is not open.")); if (connection.Settings.IgnorePrepare) return; Prepare(0); } #endregion #region Async Methods internal delegate object AsyncDelegate(int type, CommandBehavior behavior); internal AsyncDelegate caller = null; internal Exception thrownException; internal object AsyncExecuteWrapper(int type, CommandBehavior behavior) { thrownException = null; try { if (type == 1) return ExecuteReader(behavior); return ExecuteNonQuery(); } catch (Exception ex) { 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 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 (caller != null) Throw(new MySqlException(Resources.UnableToStartSecondAsyncOp)); caller = new AsyncDelegate(AsyncExecuteWrapper); asyncResult = caller.BeginInvoke(1, behavior, null, null); return 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 = caller; caller = null; if (thrownException != null) throw 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 (caller != null) Throw(new MySqlException(Resources.UnableToStartSecondAsyncOp)); caller = new AsyncDelegate(AsyncExecuteWrapper); asyncResult = caller.BeginInvoke(2, CommandBehavior.Default, callback, stateObject); return 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 (caller != null) Throw(new MySqlException(Resources.UnableToStartSecondAsyncOp)); caller = new AsyncDelegate(AsyncExecuteWrapper); asyncResult = caller.BeginInvoke(2, CommandBehavior.Default, null, null); return asyncResult; } /// /// Finishes asynchronous execution of a SQL statement. /// /// The returned by the call /// to . /// public int EndExecuteNonQuery(IAsyncResult asyncResult) { asyncResult.AsyncWaitHandle.WaitOne(); AsyncDelegate c = caller; caller = null; if (thrownException != null) throw 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() { long size = CommandText.Length; foreach (MySqlParameter parameter in Parameters) size += parameter.EstimatedSize(); return size; } /// /// 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 = new List(Resources.Keywords); return !keywords.Contains(keyword); } #endregion #region ICloneable /// /// Creates a clone of this MySqlCommand object. CommandText, Connection, and Transaction properties /// are included as well as the entire parameter list. /// /// The cloned MySqlCommand object public MySqlCommand Clone() { MySqlCommand clone = new MySqlCommand(cmdText, connection, curTransaction); clone.CommandType = CommandType; clone.commandTimeout = commandTimeout; clone.useDefaultTimeout = useDefaultTimeout; clone.batchableCommandText = batchableCommandText; clone.EnableCaching = EnableCaching; clone.CacheAge = CacheAge; PartialClone(clone); foreach (MySqlParameter p in parameters) { clone.Parameters.Add(p.Clone()); } return clone; } partial void PartialClone(MySqlCommand clone); object ICloneable.Clone() { return this.Clone(); } #endregion #region Batching support internal void AddToBatch(MySqlCommand command) { if (batch == null) batch = new List(); batch.Add(command); } internal string GetCommandTextForBatching() { if (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(CommandText.Substring(0, 6), "INSERT", StringComparison.OrdinalIgnoreCase) == 0) { MySqlCommand cmd = new MySqlCommand("SELECT @@sql_mode", Connection); string sql_mode = StringUtility.ToUpperInvariant(cmd.ExecuteScalar().ToString()); MySqlTokenizer tokenizer = new MySqlTokenizer(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) { batchableCommandText += token; token = tokenizer.NextToken(); if (token == "(") openParenCount++; else if (token == ")") openParenCount--; if (openParenCount == 0) break; } if (token != null) batchableCommandText += token; token = tokenizer.NextToken(); if (token != null && (token == "," || StringUtility.ToUpperInvariant(token) == "ON")) { batchableCommandText = null; break; } } token = tokenizer.NextToken(); } } // Otherwise use the command verbatim else batchableCommandText = CommandText; } return batchableCommandText; } #endregion // This method is used to throw all exceptions from this class. private void Throw(Exception ex) { if (connection != null) connection.Throw(ex); throw ex; } public new void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected override void Dispose(bool disposing) { if (disposed) return; if (disposing) if (statement != null && statement.IsPrepared) statement.CloseStatement(); base.Dispose(disposing); disposed = true; } } } #endif