You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1087 lines
40 KiB

// 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
{
/// <include file='docs/mysqlcommand.xml' path='docs/ClassSummary/*'/>
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<string> keywords = null;
private bool disposed = false;
/// <include file='docs/mysqlcommand.xml' path='docs/ctor1/*'/>
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;
}
/// <include file='docs/mysqlcommand.xml' path='docs/ctor2/*'/>
public MySqlCommand(string cmdText)
: this()
{
this.CommandText = cmdText;
}
/// <include file='docs/mysqlcommand.xml' path='docs/ctor3/*'/>
public MySqlCommand(string cmdText, MySqlConnection connection)
: this(cmdText)
{
this.Connection = connection;
}
/// <include file='docs/mysqlcommand.xml' path='docs/ctor4/*'/>
public MySqlCommand(string cmdText, MySqlConnection connection, MySqlTransaction transaction)
: this(cmdText, connection)
{
this.Transaction = transaction;
}
#region Destructor
~MySqlCommand()
{
this.Dispose(false);
}
#endregion
#region Properties
/// <summary>
/// Gets the last inserted id.
/// </summary>
/// <include file='docs/mysqlcommand.xml' path='docs/LastInseredId/*'/>
[Browsable(false)]
public Int64 LastInsertedId { get; internal set; }
/// <include file='docs/mysqlcommand.xml' path='docs/CommandText/*'/>
[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 ()";
}
}
}
/// <include file='docs/mysqlcommand.xml' path='docs/CommandTimeout/*'/>
[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;
}
}
/// <include file='docs/mysqlcommand.xml' path='docs/CommandType/*'/>
[Category("Data")]
public override CommandType CommandType { get; set; }
/// <summary>
/// Gets a boolean value that indicates whether the Prepared method has been called.
/// </summary>
/// <include file='docs/mysqlcommand.xml' path='docs/IsPrepared/*'/>
[Browsable(false)]
public bool IsPrepared => this.statement != null && this.statement.IsPrepared;
/// <include file='docs/mysqlcommand.xml' path='docs/Connection/*'/>
[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;
}
}
/// <include file='docs/mysqlcommand.xml' path='docs/Parameters/*'/>
[Category("Data")]
[Description("The parameters collection")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public new MySqlParameterCollection Parameters { get; }
/// <include file='docs/mysqlcommand.xml' path='docs/Attributes/*'/>
[Category("Data")]
[Description("The attributes collection")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public MySqlAttributeCollection Attributes { get; }
/// <include file='docs/mysqlcommand.xml' path='docs/Transaction/*'/>
[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 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
/// <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()
{
if (this.connection != null)
{
this.connection.CancelQuery(this.connection.ConnectionTimeout);
}
this.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)this.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 (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."));
}
}
/// <include file='docs/mysqlcommand.xml' path='docs/ExecuteNonQuery/*'/>
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);
}
/// <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 (this.connection?.Reader == null)
{
return;
}
this.connection.Reader.Close();
this.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 (!this.resetSqlSelect)
{
return;
}
this.resetSqlSelect = false;
MySqlCommand command = new MySqlCommand("SET SQL_SELECT_LIMIT=DEFAULT", this.connection);
command.InternallyCreated = true;
command.ExecuteNonQuery();
}
/// <include file='docs/mysqlcommand.xml' path='docs/ExecuteReader/*'/>
public new MySqlDataReader ExecuteReader()
{
return this.ExecuteReader(CommandBehavior.Default);
}
/// <include file='docs/mysqlcommand.xml' path='docs/ExecuteReader1/*'/>
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"));
}
/// <include file='docs/mysqlcommand.xml' path='docs/ExecuteScalar/*'/>
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;
}
}
/// <include file='docs/mysqlcommand.xml' path='docs/Prepare2/*'/>
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();
}
}
/// <include file='docs/mysqlcommand.xml' path='docs/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;
}
/// <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 this.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 (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;
}
/// <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 = this.Caller;
this.Caller = null;
if (this.thrownException != null)
{
throw this.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 (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;
}
/// <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 (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;
}
/// <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 = 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<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 = 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<MySqlCommand>();
}
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;
}
}
}