// Copyright (c) 2004-2008 MySQL AB, 2008-2009 Sun Microsystems, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as published by // the Free Software Foundation // // There are special exceptions to the terms and conditions of the GPL // as it is applied to this software. View the full text of the // exception in file EXCEPTIONS in the directory of this software // distribution. // // 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA using System; using System.ComponentModel; using System.Data; using System.Data.Common; #if !CF using System.Drawing; using System.Drawing.Design; using System.Transactions; #endif using System.Text; using IsolationLevel=System.Data.IsolationLevel; using MySql.Data.Common; using System.Diagnostics; using MySql.Data.MySqlClient.Properties; namespace MySql.Data.MySqlClient { /// #if !CF [ToolboxBitmap(typeof (MySqlConnection), "MySqlClient.resources.connection.bmp")] [DesignerCategory("Code")] [ToolboxItem(true)] #endif public sealed class MySqlConnection : DbConnection, ICloneable { internal ConnectionState connectionState; internal Driver driver; private MySqlConnectionStringBuilder settings; private bool hasBeenOpen; private SchemaProvider schemaProvider; private ProcedureCache procedureCache; #if !CF private PerformanceMonitor perfMonitor; #endif private bool abortOnTimeout; private string database; private int commandTimeout; /// public event MySqlInfoMessageEventHandler InfoMessage; private static Cache connectionStringCache = new Cache(0, 25); /// public MySqlConnection() { //TODO: add event data to StateChange docs settings = new MySqlConnectionStringBuilder(); database = String.Empty; } /// public MySqlConnection(string connectionString) : this() { ConnectionString = connectionString; } #region Interal Methods & Properties #if !CF internal PerformanceMonitor PerfMonitor { get { return perfMonitor; } } #endif internal ProcedureCache ProcedureCache { get { return procedureCache; } } internal MySqlConnectionStringBuilder Settings { get { return settings; } } internal MySqlDataReader Reader { get { if (driver == null) return null; return driver.reader; } set { driver.reader = value; } } internal void OnInfoMessage(MySqlInfoMessageEventArgs args) { if (InfoMessage != null) { InfoMessage(this, args); } } internal bool SoftClosed { get { #if !CF return (State == ConnectionState.Closed) && driver != null && driver.CurrentTransaction != null; #else return false; #endif } } #endregion #region Properties /// /// Returns the id of the server thread this connection is executing on /// #if !CF [Browsable(false)] #endif public int ServerThread { get { return driver.ThreadID; } } /// /// Gets the name of the MySQL server to which to connect. /// #if !CF [Browsable(true)] #endif public override string DataSource { get { return settings.Server; } } /// #if !CF [Browsable(true)] #endif public override int ConnectionTimeout { get { return (int) settings.ConnectionTimeout; } } /// #if !CF [Browsable(true)] #endif public override string Database { get { return database; } } /// /// Indicates if this connection should use compression when communicating with the server. /// #if !CF [Browsable(false)] #endif public bool UseCompression { get { return settings.UseCompression; } } /// #if !CF [Browsable(false)] #endif public override ConnectionState State { get { return connectionState; } } /// #if !CF [Browsable(false)] #endif public override string ServerVersion { get { return driver.Version.ToString(); } } /// #if !CF [Editor("MySql.Data.MySqlClient.Design.ConnectionStringTypeEditor,MySqlClient.Design", typeof (UITypeEditor))] [Browsable(true)] [Category("Data")] [Description( "Information used to connect to a DataSource, such as 'Server=xxx;UserId=yyy;Password=zzz;Database=dbdb'.")] #endif public override string ConnectionString { get { // Always return exactly what the user set. // Security-sensitive information may be removed. return settings.GetConnectionString(!hasBeenOpen || settings.PersistSecurityInfo); } set { if (State != ConnectionState.Closed) throw new MySqlException( "Not allowed to change the 'ConnectionString' property while the connection (state=" + State + ")."); MySqlConnectionStringBuilder newSettings; lock (connectionStringCache) { if (value == null) newSettings = new MySqlConnectionStringBuilder(); else { newSettings = (MySqlConnectionStringBuilder)connectionStringCache[value]; if (null == newSettings) { newSettings = new MySqlConnectionStringBuilder(value); connectionStringCache.Add(value, newSettings); } } } settings = newSettings; if (settings.Database != null && settings.Database.Length > 0) this.database = settings.Database; if (driver != null) driver.Settings = newSettings; } } #if !CF && !__MonoCS__ protected override DbProviderFactory DbProviderFactory { get { return MySqlClientFactory.Instance; } } #endif #endregion #region Transactions #if !MONO && !CF /// /// Enlists in the specified transaction. /// /// /// A reference to an existing in which to enlist. /// public override void EnlistTransaction(Transaction transaction) { // enlisting in the null transaction is a noop if (transaction == null) return; // guard against trying to enlist in more than one transaction if (driver.CurrentTransaction != null) { if (driver.CurrentTransaction.BaseTransaction == transaction) return; throw new MySqlException("Already enlisted"); } // now see if we need to swap out drivers. We would need to do this since // we have to make sure all ops for a given transaction are done on the // same physical connection. Driver existingDriver = DriverTransactionManager.GetDriverInTransaction(transaction); if (existingDriver != null) { // we can't allow more than one driver to contribute to the same connection if (existingDriver.IsInActiveUse) throw new NotSupportedException(Resources.MultipleConnectionsInTransactionNotSupported); // there is an existing driver and it's not being currently used. // now we need to see if it is using the same connection string string text1 = existingDriver.Settings.ConnectionString; string text2 = Settings.ConnectionString; if (String.Compare(text1, text2, true) != 0) throw new NotSupportedException(Resources.MultipleConnectionsInTransactionNotSupported); // close existing driver // set this new driver as our existing driver CloseFully(); driver = existingDriver; } if (driver.CurrentTransaction == null) { MySqlPromotableTransaction t = new MySqlPromotableTransaction(this, transaction); if (!transaction.EnlistPromotableSinglePhase(t)) throw new NotSupportedException(Resources.DistributedTxnNotSupported); driver.CurrentTransaction = t; DriverTransactionManager.SetDriverInTransaction(driver); driver.IsInActiveUse = true; } } #endif /// public new MySqlTransaction BeginTransaction() { return BeginTransaction(IsolationLevel.RepeatableRead); } /// public new MySqlTransaction BeginTransaction(IsolationLevel iso) { //TODO: check note in help if (State != ConnectionState.Open) throw new InvalidOperationException(Resources.ConnectionNotOpen); // First check to see if we are in a current transaction if (driver.HasStatus(ServerStatusFlags.InTransaction)) throw new InvalidOperationException(Resources.NoNestedTransactions); MySqlTransaction t = new MySqlTransaction(this, iso); MySqlCommand cmd = new MySqlCommand("", this); cmd.CommandText = "SET SESSION TRANSACTION ISOLATION LEVEL "; switch (iso) { case IsolationLevel.ReadCommitted: cmd.CommandText += "READ COMMITTED"; break; case IsolationLevel.ReadUncommitted: cmd.CommandText += "READ UNCOMMITTED"; break; case IsolationLevel.RepeatableRead: cmd.CommandText += "REPEATABLE READ"; break; case IsolationLevel.Serializable: cmd.CommandText += "SERIALIZABLE"; break; case IsolationLevel.Chaos: throw new NotSupportedException(Resources.ChaosNotSupported); } cmd.ExecuteNonQuery(); cmd.CommandText = "BEGIN"; cmd.ExecuteNonQuery(); return t; } #endregion /// public override void ChangeDatabase(string databaseName) { if (databaseName == null || databaseName.Trim().Length == 0) throw new ArgumentException(Resources.ParameterIsInvalid, "databaseName"); if (State != ConnectionState.Open) throw new InvalidOperationException(Resources.ConnectionNotOpen); // This lock prevents promotable transaction rollback to run // in parallel lock (driver) { #if !CF if (Transaction.Current != null && Transaction.Current.TransactionInformation.Status == TransactionStatus.Aborted) { throw new TransactionAbortedException(); } #endif // We use default command timeout for SetDatabase using (new CommandTimer(this, (int)Settings.DefaultCommandTimeout)) { driver.SetDatabase(databaseName); } } this.database = databaseName; } internal void SetState(ConnectionState newConnectionState, bool broadcast) { if (newConnectionState == connectionState && !broadcast) return; ConnectionState oldConnectionState = connectionState; connectionState = newConnectionState; if (broadcast) OnStateChange(new StateChangeEventArgs(oldConnectionState, connectionState)); } /// /// Ping /// /// public bool Ping() { if (driver != null && driver.Ping()) return true; driver = null; SetState(ConnectionState.Closed, true); return false; } /// public override void Open() { if (State == ConnectionState.Open) throw new InvalidOperationException(Resources.ConnectionAlreadyOpen); SetState(ConnectionState.Connecting, true); #if !CF // if we are auto enlisting in a current transaction, then we will be // treating the connection as pooled if (settings.AutoEnlist && Transaction.Current != null) { driver = DriverTransactionManager.GetDriverInTransaction(Transaction.Current); if (driver != null && (driver.IsInActiveUse || !driver.Settings.EquivalentTo(this.Settings))) throw new NotSupportedException(Resources.MultipleConnectionsInTransactionNotSupported); } #endif try { if (settings.Pooling) { MySqlPool pool = MySqlPoolManager.GetPool(settings); if (driver == null) driver = pool.GetConnection(); procedureCache = pool.ProcedureCache; } else { if (driver == null) driver = Driver.Create(settings); procedureCache = new ProcedureCache((int) settings.ProcedureCacheSize); } } catch (Exception) { SetState(ConnectionState.Closed, true); throw; } // if the user is using old syntax, let them know if (driver.Settings.UseOldSyntax) MySqlTrace.LogWarning(ServerThread, "You are using old syntax that will be removed in future versions"); SetState(ConnectionState.Open, false); driver.Configure(this); if (settings.Database != null && settings.Database != String.Empty) ChangeDatabase(settings.Database); // setup our schema provider if (driver.Version.isAtLeast(5, 0, 0)) schemaProvider = new ISSchemaProvider(this); else schemaProvider = new SchemaProvider(this); #if !CF perfMonitor = new PerformanceMonitor(this); #endif // if we are opening up inside a current transaction, then autoenlist // TODO: control this with a connection string option #if !MONO && !CF if (Transaction.Current != null && settings.AutoEnlist) EnlistTransaction(Transaction.Current); #endif hasBeenOpen = true; SetState(ConnectionState.Open, true); } /// public new MySqlCommand CreateCommand() { // Return a new instance of a command object. MySqlCommand c = new MySqlCommand(); c.Connection = this; return c; } #region ICloneable /// /// Creates a new MySqlConnection object with the exact same ConnectionString value /// /// A cloned MySqlConnection object public MySqlConnection Clone() { MySqlConnection clone = new MySqlConnection(); string connectionString = settings.ConnectionString; if (connectionString != null) clone.ConnectionString = connectionString; return clone; } object ICloneable.Clone() { return this.Clone(); } #endregion #region IDisposeable protected override void Dispose(bool disposing) { if (State == ConnectionState.Open) Close(); base.Dispose(disposing); } #endregion protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) { if (isolationLevel == IsolationLevel.Unspecified) return BeginTransaction(); return BeginTransaction(isolationLevel); } protected override DbCommand CreateDbCommand() { return CreateCommand(); } internal void Abort() { try { if (settings.Pooling) MySqlPoolManager.ReleaseConnection(driver); else driver.Close(); } catch (Exception) { } SetState(ConnectionState.Closed, true); } internal void CloseFully() { if (settings.Pooling && driver.IsOpen) { // if we are in a transaction, roll it back if (driver.HasStatus(ServerStatusFlags.InTransaction)) { MySqlTransaction t = new MySqlTransaction(this, IsolationLevel.Unspecified); t.Rollback(); } MySqlPoolManager.ReleaseConnection(driver); } else driver.Close(); driver = null; } /// public override void Close() { if (State == ConnectionState.Closed) return; if (Reader != null) Reader.Close(); // if the reader was opened with CloseConnection then driver // will be null on the second time through if (driver != null) { #if !CF if (driver.CurrentTransaction == null) #endif CloseFully(); #if !CF else driver.IsInActiveUse = false; #endif } SetState(ConnectionState.Closed, true); } internal string CurrentDatabase() { if (Database != null && Database.Length > 0) return Database; MySqlCommand cmd = new MySqlCommand("SELECT database()", this); return cmd.ExecuteScalar().ToString(); } internal void HandleTimeout(TimeoutException tex) { bool isFatal = false; if (abortOnTimeout) { // Special connection started to cancel a query. // Timeout handler is disabled to prevent recursive connection // spawning when original query and KILL time out. Abort(); throw new MySqlException(Resources.Timeout, true , tex); } try { // Do a fast cancel.The reason behind small values for connection // and command timeout is that we do not want user to wait longer // after command has already expired. // Microsoft's SqlClient seems to be using 5 seconds timeouts // here as well. // Read the error packet with "interrupted" message. CancelQuery(5); driver.ResetTimeout(5000); if (Reader != null) { Reader.Close(); Reader = null; } } catch (Exception ex) { MySqlTrace.LogWarning(ServerThread, "Could not kill query in timeout handler, " + " aborting connection. Exception was " + ex.Message); Abort(); isFatal = true; } throw new MySqlException(Resources.Timeout, isFatal, tex); } public void CancelQuery(int timeout) { if (!driver.Version.isAtLeast(5, 0, 0)) throw new NotSupportedException(Resources.CancelNotSupported); MySqlConnectionStringBuilder cb = new MySqlConnectionStringBuilder( Settings.ConnectionString); cb.Pooling = false; cb.ConnectionTimeout = (uint) timeout; using(MySqlConnection c = new MySqlConnection(cb.ConnectionString)) { c.abortOnTimeout = true; c.Open(); string commandText = "KILL QUERY " + ServerThread; MySqlCommand cmd = new MySqlCommand(commandText, c); cmd.CommandTimeout = timeout; cmd.ExecuteNonQuery(); } } #region Routines for timeout support. // Problem description: // Sometimes, ExecuteReader is called recursively. This is the case if // command behaviors are used and we issue "set sql_select_limit" // before and after command. This is also the case with prepared // statements , where we set session variables. In these situations, we // have to prevent recursive ExecuteReader calls from overwriting // timeouts set by the top level command. // To solve the problem, SetCommandTimeout() and ClearCommandTimeout() are // introduced . Query timeout here is "sticky", that is once set with // SetCommandTimeout, it only be overwritten after ClearCommandTimeout // (SetCommandTimeout would return false if it timeout has not been // cleared). // The proposed usage pattern of there routines is following: // When timed operations starts, issue SetCommandTimeout(). When it // finishes, issue ClearCommandTimeout(), but _only_ if call to // SetCommandTimeout() was successful. /// /// Sets query timeout. If timeout has been set prior and not /// yet cleared ClearCommandTimeout(), it has no effect. /// /// timeout in seconds /// true if internal bool SetCommandTimeout(int value) { if (!hasBeenOpen) // Connection timeout is handled by driver return false; if (commandTimeout != 0) // someone is trying to set a timeout while command is already // running. It could be for example recursive call to ExecuteReader // Ignore the request, as only top-level (non-recursive commands) // can set timeouts. return false; if (driver == null) return false; commandTimeout = value; driver.ResetTimeout(commandTimeout * 1000); return true; } /// /// Clears query timeout, allowing next SetCommandTimeout() to succeed. /// internal void ClearCommandTimeout() { if (!hasBeenOpen) return; commandTimeout = 0; if (driver != null) { driver.ResetTimeout(0); } } #endregion #region GetSchema Support /// /// Returns schema information for the data source of this . /// /// A that contains schema information. public override DataTable GetSchema() { return GetSchema(null); } /// /// Returns schema information for the data source of this /// using the specified string for the schema name. /// /// Specifies the name of the schema to return. /// A that contains schema information. public override DataTable GetSchema(string collectionName) { if (collectionName == null) collectionName = SchemaProvider.MetaCollection; return GetSchema(collectionName, null); } /// /// Returns schema information for the data source of this /// using the specified string for the schema name and the specified string array /// for the restriction values. /// /// Specifies the name of the schema to return. /// Specifies a set of restriction values for the requested schema. /// A that contains schema information. public override DataTable GetSchema(string collectionName, string[] restrictionValues) { if (collectionName == null) collectionName = SchemaProvider.MetaCollection; string[] restrictions = schemaProvider.CleanRestrictions(restrictionValues); DataTable dt = schemaProvider.GetSchema(collectionName, restrictions); return dt; } #endregion #region Pool Routines /// public static void ClearPool(MySqlConnection connection) { MySqlPoolManager.ClearPool(connection.Settings); } /// public static void ClearAllPools() { MySqlPoolManager.ClearAllPools(); } #endregion } /// /// Represents the method that will handle the event of a /// . /// public delegate void MySqlInfoMessageEventHandler(object sender, MySqlInfoMessageEventArgs args); /// /// Provides data for the InfoMessage event. This class cannot be inherited. /// public class MySqlInfoMessageEventArgs : EventArgs { /// /// /// public MySqlError[] errors; } /// /// IDisposable wrapper around SetCommandTimeout and ClearCommandTimeout /// functionality /// internal class CommandTimer:IDisposable { bool timeoutSet; MySqlConnection connection; public CommandTimer(MySqlConnection connection, int timeout) { this.connection = connection; if (connection != null) { timeoutSet = connection.SetCommandTimeout(timeout); } } #region IDisposable Members public void Dispose() { if (timeoutSet) { timeoutSet = false; connection.ClearCommandTimeout(); connection = null; } } #endregion } }