|
|
#if MYSQL_6_10
// Copyright © 2004, 2018, Oracle and/or its affiliates. All rights reserved.
//
// MySQL Connector/NET is licensed under the terms of the GPLv2
// <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, 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
// <http://www.mysql.com/about/legal/licensing/foss-exception.html>.
//
// 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
using System; using System.IO; using System.Data; using System.Data.Common; using System.ComponentModel; using System.Diagnostics; using System.Collections.Generic; using System.Linq; using System.Threading; using Externals.MySql.Data.Common; #if !NETSTANDARD1_3
using Externals.MySql.Data.MySqlClient.Replication; #endif
namespace Externals.MySql.Data.MySqlClient { internal 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<string> keywords = null; private bool disposed = false;
public MySqlCommand() { CommandType = System.Data.CommandType.Text; Parameters = new MySqlParameterCollection(this); cmdText = String.Empty; useDefaultTimeout = true; UpdatedRowSource = UpdateRowSource.Both; }
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) { Transaction = transaction; }
#region Destructor
~MySqlCommand() { Dispose(false); } #endregion
#region Properties
/// <summary>
/// Gets the last inserted id.
/// </summary>
[Browsable(false)] public Int64 LastInsertedId { get; internal set; }
[Category("Data")] [Description("Command text to execute")] 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; set; }
/// <summary>
/// Gets a boolean value that indicates whether the Prepared method has been called.
/// </summary>
[Browsable(false)] public bool IsPrepared => 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) return;
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; }
[Browsable(false)] public new MySqlTransaction Transaction { get; set; }
/// <summary>
/// Gets or sets a boolean value that indicates whether caching is enabled.
/// </summary>
public bool EnableCaching { get; set; }
/// <summary>
/// Gets or sets the seconds for how long a TableDirect result should be cached.
/// </summary>
public int CacheAge { get; set; }
internal List<MySqlCommand> Batch { get; private set; }
internal bool Canceled { get; private set; }
internal string BatchableCommandText { get; private set; }
internal bool InternallyCreated { get; set; }
/// <summary>
/// Gets or sets how command results are applied to the DataRow when used by the
/// Update method of the DbDataAdapter.
/// </summary>
public override UpdateRowSource UpdatedRowSource { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the command object should be visible in a Windows Form Designer control.
/// </summary>
[Browsable(false)] public override bool DesignTimeVisible { get; set; }
protected override DbParameter CreateDbParameter() { return new MySqlParameter(); }
protected override DbConnection DbConnection { get { return Connection; } set { Connection = (MySqlConnection)value; } }
protected override DbParameterCollection DbParameterCollection { get { return Parameters; } }
protected override DbTransaction DbTransaction { get { return Transaction; } set { Transaction = (MySqlTransaction)value; } }
#endregion
#region Methods
/// <summary>
/// Attempts to cancel the execution of a currently active command
/// </summary>
/// <remarks>
/// Cancelling a currently active query only works with MySQL versions 5.0.0 and higher.
/// </remarks>
public override void Cancel() { connection.CancelQuery(connection.ConnectionTimeout); Canceled = true; }
/// <summary>
/// Creates a new instance of a <see cref="MySqlParameter"/> object.
/// </summary>
/// <remarks>
/// This method is a strongly-typed version of <see cref="System.Data.IDbCommand.CreateParameter"/>.
/// </remarks>
/// <returns>A <see cref="MySqlParameter"/> object.</returns>
///
public new MySqlParameter CreateParameter() { return (MySqlParameter)CreateDbParameter(); }
/// <summary>
/// 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
/// </summary>
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?.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) return;
commandTimer.Dispose(); commandTimer = null; }
internal void Close(MySqlDataReader reader) { statement?.Close(reader); ResetSqlSelectLimit(); if (statement != null) connection?.driver?.CloseQuery(connection, statement.StatementId); ClearCommandTimer(); }
protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) { return ExecuteReader(behavior); }
/// <summary>
/// Reset reader to null, to avoid "There is already an open data reader"
/// on the next ExecuteReader(). Used in error handling scenarios.
/// </summary>
private void ResetReader() { if (connection?.Reader == null) return;
connection.Reader.Close(); connection.Reader = null; }
/// <summary>
/// Reset SQL_SELECT_LIMIT that could have been modified by CommandBehavior.
/// </summary>
internal void ResetSqlSelectLimit() { // if we are supposed to reset the sql select limit, do that here
if (!resetSqlSelect) return;
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?.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(';');
#if !NETSTANDARD1_3
// Load balancing getting a new connection
if (connection.hasBeenOpen && !driver.HasStatus(ServerStatusFlags.InTransaction)) { ReplicationManager.GetNewConnection(connection.Settings.Server, !IsReadOnlyCommand(sql), connection); } #endif
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)); }
#if !NETSTANDARD1_3
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) Throw(new System.Transactions.TransactionAbortedException()); } } #endif
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);
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
} #if !NETSTANDARD1_3
catch (ThreadAbortException taex) { connection.HandleTimeoutOrThreadAbort(taex); throw; } #endif
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;
statement = CommandType == CommandType.StoredProcedure ? new StoredProcedure(this, CommandText) : 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
#if !NETSTANDARD1_3
private IAsyncResult asyncResult;
internal delegate object AsyncDelegate(int type, CommandBehavior behavior); internal AsyncDelegate Caller; 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; }
/// <summary>
/// Initiates the asynchronous execution of the SQL statement or stored procedure
/// that is described by this <see cref="MySqlCommand"/>, and retrieves one or more
/// result sets from the server.
/// </summary>
/// <returns>An <see cref="IAsyncResult"/> that can be used to poll, wait for results,
/// or both; this value is also needed when invoking EndExecuteReader,
/// which returns a <see cref="MySqlDataReader"/> instance that can be used to retrieve
/// the returned rows. </returns>
public IAsyncResult BeginExecuteReader() { return BeginExecuteReader(CommandBehavior.Default); }
/// <summary>
/// Initiates the asynchronous execution of the SQL statement or stored procedure
/// that is described by this <see cref="MySqlCommand"/> using one of the
/// <b>CommandBehavior</b> values.
/// </summary>
/// <param name="behavior">One of the <see cref="CommandBehavior"/> values, indicating
/// options for statement execution and data retrieval.</param>
/// <returns>An <see cref="IAsyncResult"/> that can be used to poll, wait for results,
/// or both; this value is also needed when invoking EndExecuteReader,
/// which returns a <see cref="MySqlDataReader"/> instance that can be used to retrieve
/// the returned rows. </returns>
public IAsyncResult BeginExecuteReader(CommandBehavior behavior) { if (Caller != null) Throw(new MySqlException(Resources.UnableToStartSecondAsyncOp));
Caller = AsyncExecuteWrapper; asyncResult = Caller.BeginInvoke(1, behavior, null, null); return asyncResult; }
/// <summary>
/// Finishes asynchronous execution of a SQL statement, returning the requested
/// <see cref="MySqlDataReader"/>.
/// </summary>
/// <param name="result">The <see cref="IAsyncResult"/> returned by the call to
/// <see cref="BeginExecuteReader()"/>.</param>
/// <returns>A <b>MySqlDataReader</b> object that can be used to retrieve the requested rows. </returns>
public MySqlDataReader EndExecuteReader(IAsyncResult result) { result.AsyncWaitHandle.WaitOne(); AsyncDelegate c = Caller; Caller = null; if (thrownException != null) throw thrownException; return (MySqlDataReader)c.EndInvoke(result); }
/// <summary>
/// Initiates the asynchronous execution of the SQL statement or stored procedure
/// that is described by this <see cref="MySqlCommand"/>.
/// </summary>
/// <param name="callback">
/// An <see cref="AsyncCallback"/> delegate that is invoked when the command's
/// execution has completed. Pass a null reference (<b>Nothing</b> in Visual Basic)
/// to indicate that no callback is required.</param>
/// <param name="stateObject">A user-defined state object that is passed to the
/// callback procedure. Retrieve this object from within the callback procedure
/// using the <see cref="IAsyncResult.AsyncState"/> property.</param>
/// <returns>An <see cref="IAsyncResult"/> that can be used to poll or wait for results,
/// or both; this value is also needed when invoking <see cref="EndExecuteNonQuery"/>,
/// which returns the number of affected rows. </returns>
public IAsyncResult BeginExecuteNonQuery(AsyncCallback callback, object stateObject) { if (Caller != null) Throw(new MySqlException(Resources.UnableToStartSecondAsyncOp));
Caller = AsyncExecuteWrapper; asyncResult = Caller.BeginInvoke(2, CommandBehavior.Default, callback, stateObject); return asyncResult; }
/// <summary>
/// Initiates the asynchronous execution of the SQL statement or stored procedure
/// that is described by this <see cref="MySqlCommand"/>.
/// </summary>
/// <returns>An <see cref="IAsyncResult"/> that can be used to poll or wait for results,
/// or both; this value is also needed when invoking <see cref="EndExecuteNonQuery"/>,
/// which returns the number of affected rows. </returns>
public IAsyncResult BeginExecuteNonQuery() { if (Caller != null) Throw(new MySqlException(Resources.UnableToStartSecondAsyncOp));
Caller = AsyncExecuteWrapper; asyncResult = Caller.BeginInvoke(2, CommandBehavior.Default, null, null); return asyncResult; }
/// <summary>
/// Finishes asynchronous execution of a SQL statement.
/// </summary>
/// <param name="asyncResult">The <see cref="IAsyncResult"/> returned by the call
/// to <see cref="BeginExecuteNonQuery()"/>.</param>
/// <returns></returns>
public int EndExecuteNonQuery(IAsyncResult asyncResult) { asyncResult.AsyncWaitHandle.WaitOne(); AsyncDelegate c = Caller; Caller = null; if (thrownException != null) throw thrownException; return (int)c.EndInvoke(asyncResult); }
#endif
#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 CommandText.Length + Parameters.Cast<MySqlParameter>().Sum(parameter => parameter.EstimatedSize()); }
/// <summary>
/// Verifies if a query is valid even if it has not spaces or is a stored procedure call
/// </summary>
/// <param name="query">Query to validate</param>
/// <returns>If it is necessary to add call statement</returns>
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<string>(Resources.Keywords);
return !keywords.Contains(keyword); }
#endregion
#region Batching support
internal void AddToBatch(MySqlCommand command) { if (Batch == null) Batch = new List<MySqlCommand>(); 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) { 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) return;
if (statement != null && statement.IsPrepared) statement.CloseStatement();
ResetReader();
base.Dispose(disposing);
disposed = true; } } }
#endif
|