#region License
// Copyright 2005-2019 Paul Kohler (https://github.com/paulkohler/minisqlquery). All rights reserved.
// This source code is made available under the terms of the GNU Lesser General Public License v3.0
// https://github.com/paulkohler/minisqlquery/blob/master/LICENSE
#endregion
using System;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Diagnostics;
namespace MiniSqlQuery.Core
{
///
/// Helper class to encapsulate query execution
///
public class QueryRunner
{
///
/// The connection string value.
///
private readonly string _connectionString;
///
/// The enable query batching value.
///
private readonly bool _enableQueryBatching;
private readonly int _commandTimeout;
///
/// The provider factory.
///
private readonly DbProviderFactory _factory;
private DbCommand _command;
///
/// Initializes a new instance of the class.
///
/// The factory.
/// The connection string.
/// The enable query batching.
///
public QueryRunner(DbProviderFactory factory, string connectionString, bool enableQueryBatching, int commandTimeout)
{
_factory = factory;
_connectionString = connectionString;
_enableQueryBatching = enableQueryBatching;
_commandTimeout = commandTimeout;
Messages = string.Empty;
}
///
/// The batch progress.
///
public event EventHandler BatchProgress;
///
/// Gets or sets the for this query.
///
/// The query batch.
public QueryBatch Batch { get; protected set; }
///
/// Gets or sets Exception if any.
///
/// The exception.
public DbException Exception { get; protected set; }
///
/// Gets or sets a value indicating whether the query runner is busy.
///
/// The is busy value.
public bool IsBusy { get; set; }
///
/// Gets or sets the messages if any.
///
/// The messages.
public string Messages { get; protected set; }
///
/// Creates an instance of a query runner for the specified database.
///
/// The factory.
/// The connection string.
/// The enable query batching.
/// The command timeout.
///
/// A instance acording to the parameters.
///
///
///
/// var runner = QueryRunner.Create(DbProviderFactories.GetFactory("System.Data.SqlClient"), connStr, true);
/// runner.ExecuteQuery("select * from Employees\r\nGO\r\nSelect * from Products");
/// // runner.Batch.Queries.Count == 2 //
///
///
public static QueryRunner Create(DbProviderFactory factory, string connectionString, bool enableQueryBatching, int commandTimeout)
{
if (factory.GetType().Name == "SqlClientFactory")
{
return new SqlQueryRunner(factory, connectionString, enableQueryBatching, commandTimeout);
}
// otherwise ise the default
return new QueryRunner(factory, connectionString, enableQueryBatching, commandTimeout);
}
///
/// Tests the database connection using the specified provider.
///
/// Name of the provider.
/// The connection string.
/// If the connection was successful, null; otherwise the exception object.
public static Exception TestDbConnection(string providerName, string connectionString)
{
try
{
DbProviderFactory factory = DbProviderFactories.GetFactory(providerName);
using (DbConnection connection = factory.CreateConnection())
{
connection.ConnectionString = connectionString;
connection.Open();
if (connection.State == ConnectionState.Open)
{
return null;
}
}
}
catch (Exception e)
{
return e;
}
throw new InvalidOperationException("Connection test failed");
}
///
/// Executes the query.
///
/// The SQL to execute.
public void ExecuteQuery(string sql)
{
ValidateState();
DbConnection dbConnection = null;
DbDataAdapter adapter = null;
_command = null;
Query query;
// In the case of connection errors the error messages were getting lost, provide a default batch object:
Batch = new QueryBatch(sql);
try
{
IsBusy = true;
dbConnection = _factory.CreateConnection();
dbConnection.ConnectionString = _connectionString;
dbConnection.Open();
Messages = string.Empty;
SubscribeToMessages(dbConnection);
if (_enableQueryBatching)
{
Batch = QueryBatch.Parse(sql);
}
else
{
Batch = new QueryBatch(sql);
}
Batch.StartTime = DateTime.Now;
adapter = _factory.CreateDataAdapter();
_command = dbConnection.CreateCommand();
_command.CommandType = CommandType.Text;
SetCommandTimeout(_command, _commandTimeout);
adapter.SelectCommand = _command;
int queryCount = Batch.Queries.Count;
for (int i = 0; i < queryCount; i++)
{
query = Batch.Queries[i];
_command.CommandText = query.Sql;
query.Result = new DataSet("Batch " + (i + 1));
query.StartTime = DateTime.Now;
adapter.Fill(query.Result);
query.EndTime = DateTime.Now;
OnBatchProgress(new BatchProgressEventArgs(query, queryCount, i + 1));
}
}
catch (DbException dbException)
{
HandleBatchException(dbException);
}
finally
{
if (Batch != null)
{
Batch.EndTime = DateTime.Now;
}
if (adapter != null)
{
adapter.Dispose();
}
if (_command != null)
{
_command.Dispose();
}
IsBusy = false;
UnsubscribeFromMessages(dbConnection);
}
if (Batch != null)
{
Batch.Messages = Messages;
}
}
///
/// Cancel the executing command (if busy).
///
///
/// Note that this relies on the implementation of the DbCommand.Cancel operation.
///
public void Cancel()
{
if (IsBusy && _command != null)
{
_command.Cancel();
}
}
///
/// Sets the command timeout, currently only tested against MSSQL.
///
/// The command.
/// The command timeout.
private void SetCommandTimeout(IDbCommand cmd, int commandTimeout)
{
if (_factory is SqlClientFactory)
{
if (cmd == null)
{
throw new ArgumentNullException("cmd");
}
cmd.CommandTimeout = commandTimeout;
}
else
{
Trace.WriteLine("Command Timeout only supported by SQL Client (so far)");
}
}
///
/// The handle batch exception.
///
/// The db exception.
protected virtual void HandleBatchException(DbException dbException)
{
Exception = dbException;
Messages += dbException.Message + Environment.NewLine;
}
///
/// The on batch progress.
///
/// The events.
protected void OnBatchProgress(BatchProgressEventArgs e)
{
EventHandler progress = BatchProgress;
if (progress != null)
{
progress(this, e);
}
}
///
/// The subscribe to messages.
///
/// The connection.
protected virtual void SubscribeToMessages(DbConnection connection)
{
}
///
/// The unsubscribe from messages.
///
/// The connection.
protected virtual void UnsubscribeFromMessages(DbConnection connection)
{
}
///
/// Ensures that there is enough information available to the class to execute a query.
///
/// If there is no connection, "Supply a connection."
/// If there is no connection, "Supply a provider."
private void ValidateState()
{
if (string.IsNullOrEmpty(_connectionString))
{
throw new InvalidOperationException("Supply a connection.");
}
if (_factory == null)
{
throw new InvalidOperationException("Supply a provider.");
}
}
}
}