An alternative to UBB.threads
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.
 
 
 
 

867 lines
30 KiB

// 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
{
/// <include file='docs/MySqlConnection.xml' path='docs/ClassSummary/*'/>
#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;
/// <include file='docs/MySqlConnection.xml' path='docs/InfoMessage/*'/>
public event MySqlInfoMessageEventHandler InfoMessage;
private static Cache<string, MySqlConnectionStringBuilder> connectionStringCache =
new Cache<string, MySqlConnectionStringBuilder>(0, 25);
/// <include file='docs/MySqlConnection.xml' path='docs/DefaultCtor/*'/>
public MySqlConnection()
{
//TODO: add event data to StateChange docs
settings = new MySqlConnectionStringBuilder();
database = String.Empty;
}
/// <include file='docs/MySqlConnection.xml' path='docs/Ctor1/*'/>
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
/// <summary>
/// Returns the id of the server thread this connection is executing on
/// </summary>
#if !CF
[Browsable(false)]
#endif
public int ServerThread
{
get { return driver.ThreadID; }
}
/// <summary>
/// Gets the name of the MySQL server to which to connect.
/// </summary>
#if !CF
[Browsable(true)]
#endif
public override string DataSource
{
get { return settings.Server; }
}
/// <include file='docs/MySqlConnection.xml' path='docs/ConnectionTimeout/*'/>
#if !CF
[Browsable(true)]
#endif
public override int ConnectionTimeout
{
get { return (int) settings.ConnectionTimeout; }
}
/// <include file='docs/MySqlConnection.xml' path='docs/Database/*'/>
#if !CF
[Browsable(true)]
#endif
public override string Database
{
get { return database; }
}
/// <summary>
/// Indicates if this connection should use compression when communicating with the server.
/// </summary>
#if !CF
[Browsable(false)]
#endif
public bool UseCompression
{
get { return settings.UseCompression; }
}
/// <include file='docs/MySqlConnection.xml' path='docs/State/*'/>
#if !CF
[Browsable(false)]
#endif
public override ConnectionState State
{
get { return connectionState; }
}
/// <include file='docs/MySqlConnection.xml' path='docs/ServerVersion/*'/>
#if !CF
[Browsable(false)]
#endif
public override string ServerVersion
{
get { return driver.Version.ToString(); }
}
/// <include file='docs/MySqlConnection.xml' path='docs/ConnectionString/*'/>
#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
/// <summary>
/// Enlists in the specified transaction.
/// </summary>
/// <param name="transaction">
/// A reference to an existing <see cref="System.Transactions.Transaction"/> in which to enlist.
/// </param>
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
/// <include file='docs/MySqlConnection.xml' path='docs/BeginTransaction/*'/>
public new MySqlTransaction BeginTransaction()
{
return BeginTransaction(IsolationLevel.RepeatableRead);
}
/// <include file='docs/MySqlConnection.xml' path='docs/BeginTransaction1/*'/>
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
/// <include file='docs/MySqlConnection.xml' path='docs/ChangeDatabase/*'/>
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));
}
/// <summary>
/// Ping
/// </summary>
/// <returns></returns>
public bool Ping()
{
if (driver != null && driver.Ping())
return true;
driver = null;
SetState(ConnectionState.Closed, true);
return false;
}
/// <include file='docs/MySqlConnection.xml' path='docs/Open/*'/>
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);
}
/// <include file='docs/MySqlConnection.xml' path='docs/CreateCommand/*'/>
public new MySqlCommand CreateCommand()
{
// Return a new instance of a command object.
MySqlCommand c = new MySqlCommand();
c.Connection = this;
return c;
}
#region ICloneable
/// <summary>
/// Creates a new MySqlConnection object with the exact same ConnectionString value
/// </summary>
/// <returns>A cloned MySqlConnection object</returns>
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;
}
/// <include file='docs/MySqlConnection.xml' path='docs/Close/*'/>
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.
/// <summary>
/// Sets query timeout. If timeout has been set prior and not
/// yet cleared ClearCommandTimeout(), it has no effect.
/// </summary>
/// <param name="value">timeout in seconds</param>
/// <returns>true if </returns>
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;
}
/// <summary>
/// Clears query timeout, allowing next SetCommandTimeout() to succeed.
/// </summary>
internal void ClearCommandTimeout()
{
if (!hasBeenOpen)
return;
commandTimeout = 0;
if (driver != null)
{
driver.ResetTimeout(0);
}
}
#endregion
#region GetSchema Support
/// <summary>
/// Returns schema information for the data source of this <see cref="DbConnection"/>.
/// </summary>
/// <returns>A <see cref="DataTable"/> that contains schema information. </returns>
public override DataTable GetSchema()
{
return GetSchema(null);
}
/// <summary>
/// Returns schema information for the data source of this
/// <see cref="DbConnection"/> using the specified string for the schema name.
/// </summary>
/// <param name="collectionName">Specifies the name of the schema to return. </param>
/// <returns>A <see cref="DataTable"/> that contains schema information. </returns>
public override DataTable GetSchema(string collectionName)
{
if (collectionName == null)
collectionName = SchemaProvider.MetaCollection;
return GetSchema(collectionName, null);
}
/// <summary>
/// Returns schema information for the data source of this <see cref="DbConnection"/>
/// using the specified string for the schema name and the specified string array
/// for the restriction values.
/// </summary>
/// <param name="collectionName">Specifies the name of the schema to return.</param>
/// <param name="restrictionValues">Specifies a set of restriction values for the requested schema.</param>
/// <returns>A <see cref="DataTable"/> that contains schema information.</returns>
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
/// <include file='docs/MySqlConnection.xml' path='docs/ClearPool/*'/>
public static void ClearPool(MySqlConnection connection)
{
MySqlPoolManager.ClearPool(connection.Settings);
}
/// <include file='docs/MySqlConnection.xml' path='docs/ClearAllPools/*'/>
public static void ClearAllPools()
{
MySqlPoolManager.ClearAllPools();
}
#endregion
}
/// <summary>
/// Represents the method that will handle the <see cref="MySqlConnection.InfoMessage"/> event of a
/// <see cref="MySqlConnection"/>.
/// </summary>
public delegate void MySqlInfoMessageEventHandler(object sender, MySqlInfoMessageEventArgs args);
/// <summary>
/// Provides data for the InfoMessage event. This class cannot be inherited.
/// </summary>
public class MySqlInfoMessageEventArgs : EventArgs
{
/// <summary>
///
/// </summary>
public MySqlError[] errors;
}
/// <summary>
/// IDisposable wrapper around SetCommandTimeout and ClearCommandTimeout
/// functionality
/// </summary>
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
}
}