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.

491 lines
17 KiB

#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.Collections.Generic;
using System.Data;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using static System.String;
using Externals.MySql.Data.Common;
namespace Externals.MySql.Data.MySqlClient
{
/// <summary>
/// Provides a class capable of executing a SQL script containing
/// multiple SQL statements including CREATE PROCEDURE statements
/// that require changing the delimiter
/// </summary>
internal class MySqlScript
{
/// <summary>
/// Handles the event raised whenever a statement is executed.
/// </summary>
public event MySqlStatementExecutedEventHandler StatementExecuted;
/// <summary>
/// Handles the event raised whenever an error is raised by the execution of a script.
/// </summary>
public event MySqlScriptErrorEventHandler Error;
/// <summary>
/// Handles the event raised whenever a script execution is finished.
/// </summary>
public event EventHandler ScriptCompleted;
#region Constructors
/// <summary>
/// Initializes a new instance of the
/// <see cref="MySqlScript"/> class.
/// </summary>
public MySqlScript()
{
Delimiter = ";";
}
/// <summary>
/// Initializes a new instance of the
/// <see cref="MySqlScript"/> class.
/// </summary>
/// <param name="connection">The connection.</param>
public MySqlScript(MySqlConnection connection)
: this()
{
Connection = connection;
}
/// <summary>
/// Initializes a new instance of the
/// <see cref="MySqlScript"/> class.
/// </summary>
/// <param name="query">The query.</param>
public MySqlScript(string query)
: this()
{
Query = query;
}
/// <summary>
/// Initializes a new instance of the
/// <see cref="MySqlScript"/> class.
/// </summary>
/// <param name="connection">The connection.</param>
/// <param name="query">The query.</param>
public MySqlScript(MySqlConnection connection, string query)
: this()
{
Connection = connection;
Query = query;
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the connection.
/// </summary>
/// <value>The connection.</value>
public MySqlConnection Connection { get; set; }
/// <summary>
/// Gets or sets the query.
/// </summary>
/// <value>The query.</value>
public string Query { get; set; }
/// <summary>
/// Gets or sets the delimiter.
/// </summary>
/// <value>The delimiter.</value>
public string Delimiter { get; set; }
#endregion
#region Public Methods
/// <summary>
/// Executes this instance.
/// </summary>
/// <returns>The number of statements executed as part of the script.</returns>
public int Execute()
{
bool openedConnection = false;
if (Connection == null)
throw new InvalidOperationException(Resources.ConnectionNotSet);
if (IsNullOrEmpty(Query))
return 0;
// next we open up the connetion if it is not already open
if (Connection.State != ConnectionState.Open)
{
openedConnection = true;
Connection.Open();
}
// since we don't allow setting of parameters on a script we can
// therefore safely allow the use of user variables. no one should be using
// this connection while we are using it so we can temporarily tell it
// to allow the use of user variables
bool allowUserVars = Connection.Settings.AllowUserVariables;
Connection.Settings.AllowUserVariables = true;
try
{
string mode = Connection.driver.Property("sql_mode");
mode = StringUtility.ToUpperInvariant(mode);
bool ansiQuotes = mode.IndexOf("ANSI_QUOTES") != -1;
bool noBackslashEscapes = mode.IndexOf("NO_BACKSLASH_ESCAPES") != -1;
// first we break the query up into smaller queries
List<ScriptStatement> statements = BreakIntoStatements(ansiQuotes, noBackslashEscapes);
int count = 0;
MySqlCommand cmd = new MySqlCommand(null, Connection);
foreach (ScriptStatement statement in statements.Where(statement => !IsNullOrEmpty(statement.text)))
{
cmd.CommandText = statement.text;
try
{
cmd.ExecuteNonQuery();
count++;
OnQueryExecuted(statement);
}
catch (Exception ex)
{
if (Error == null)
throw;
if (!OnScriptError(ex))
break;
}
}
OnScriptCompleted();
return count;
}
finally
{
Connection.Settings.AllowUserVariables = allowUserVars;
if (openedConnection)
{
Connection.Close();
}
}
}
#endregion
private void OnQueryExecuted(ScriptStatement statement)
{
if (StatementExecuted == null) return;
MySqlScriptEventArgs args = new MySqlScriptEventArgs { Statement = statement };
StatementExecuted(this, args);
}
private void OnScriptCompleted()
{
ScriptCompleted?.Invoke(this, EventArgs.Empty);
}
private bool OnScriptError(Exception ex)
{
if (Error == null) return false;
MySqlScriptErrorEventArgs args = new MySqlScriptErrorEventArgs(ex);
Error(this, args);
return args.Ignore;
}
private List<int> BreakScriptIntoLines()
{
List<int> lineNumbers = new List<int>();
StringReader sr = new StringReader(Query);
string line = sr.ReadLine();
int pos = 0;
while (line != null)
{
lineNumbers.Add(pos);
pos += line.Length;
line = sr.ReadLine();
}
return lineNumbers;
}
private static int FindLineNumber(int position, List<int> lineNumbers)
{
int i = 0;
while (i < lineNumbers.Count && position < lineNumbers[i])
i++;
return i;
}
private List<ScriptStatement> BreakIntoStatements(bool ansiQuotes, bool noBackslashEscapes)
{
string currentDelimiter = Delimiter;
int startPos = 0;
List<ScriptStatement> statements = new List<ScriptStatement>();
List<int> lineNumbers = BreakScriptIntoLines();
MySqlTokenizer tokenizer = new MySqlTokenizer(Query);
tokenizer.AnsiQuotes = ansiQuotes;
tokenizer.BackslashEscapes = !noBackslashEscapes;
string token = tokenizer.NextToken();
while (token != null)
{
if (!tokenizer.Quoted)
{
#if !NETSTANDARD1_3
if (token.ToLower(CultureInfo.InvariantCulture) == "delimiter")
#else
if (token.ToLowerInvariant() == "delimiter")
#endif
{
tokenizer.NextToken();
AdjustDelimiterEnd(tokenizer);
currentDelimiter = Query.Substring(tokenizer.StartIndex,
tokenizer.StopIndex - tokenizer.StartIndex).Trim();
startPos = tokenizer.StopIndex;
}
else
{
// this handles the case where our tokenizer reads part of the
// delimiter
if (currentDelimiter.StartsWith(token, StringComparison.OrdinalIgnoreCase))
{
if ((tokenizer.StartIndex + currentDelimiter.Length) <= Query.Length)
{
if (Query.Substring(tokenizer.StartIndex, currentDelimiter.Length) == currentDelimiter)
{
token = currentDelimiter;
tokenizer.Position = tokenizer.StartIndex + currentDelimiter.Length;
tokenizer.StopIndex = tokenizer.Position;
}
}
}
int delimiterPos = token.IndexOf(currentDelimiter, StringComparison.OrdinalIgnoreCase);
if (delimiterPos != -1)
{
int endPos = tokenizer.StopIndex - token.Length + delimiterPos;
if (tokenizer.StopIndex == Query.Length - 1)
endPos++;
string currentQuery = Query.Substring(startPos, endPos - startPos);
ScriptStatement statement = new ScriptStatement();
statement.text = currentQuery.Trim();
statement.line = FindLineNumber(startPos, lineNumbers);
statement.position = startPos - lineNumbers[statement.line];
statements.Add(statement);
startPos = endPos + currentDelimiter.Length;
}
}
}
token = tokenizer.NextToken();
}
// now clean up the last statement
if (startPos < Query.Length - 1)
{
string sqlLeftOver = Query.Substring(startPos).Trim();
if (IsNullOrEmpty(sqlLeftOver)) return statements;
ScriptStatement statement = new ScriptStatement
{
text = sqlLeftOver,
line = FindLineNumber(startPos, lineNumbers)
};
statement.position = startPos - lineNumbers[statement.line];
statements.Add(statement);
}
return statements;
}
private void AdjustDelimiterEnd(MySqlTokenizer tokenizer)
{
if (tokenizer.StopIndex >= Query.Length) return;
int pos = tokenizer.StopIndex;
char c = Query[pos];
while (!Char.IsWhiteSpace(c) && pos < (Query.Length - 1))
{
c = Query[++pos];
}
tokenizer.StopIndex = pos;
tokenizer.Position = pos;
}
#region Async
#if NETSTANDARD1_3
/// <summary>
/// Initiates the asynchronous execution of SQL statements.
/// </summary>
/// <returns>The number of statements executed as part of the script inside.</returns>
public async Task<int> ExecuteAsync()
{
return await ExecuteAsync(CancellationToken.None);
}
/// <summary>
/// Initiates the asynchronous execution of SQL statements.
/// </summary>
/// <returns>The number of statements executed as part of the script inside.</returns>
public async Task<int> ExecuteAsync(CancellationToken cancellationToken)
{
var result = new TaskCompletionSource<int>();
if (cancellationToken == CancellationToken.None || !cancellationToken.IsCancellationRequested)
{
try
{
var executeResult = Execute();
result.SetResult(executeResult);
}
catch (Exception ex)
{
result.SetException(ex);
}
}
else
{
result.SetCanceled();
}
return await result.Task;
}
#else
/// <summary>
/// Initiates the asynchronous execution of SQL statements.
/// </summary>
/// <returns>The number of statements executed as part of the script inside.</returns>
public Task<int> ExecuteAsync()
{
return ExecuteAsync(CancellationToken.None);
}
/// <summary>
/// Initiates the asynchronous execution of SQL statements.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The number of statements executed as part of the script inside.</returns>
public Task<int> ExecuteAsync(CancellationToken cancellationToken)
{
var result = new TaskCompletionSource<int>();
if (cancellationToken == CancellationToken.None || !cancellationToken.IsCancellationRequested)
{
try
{
var executeResult = Execute();
result.SetResult(executeResult);
}
catch (Exception ex)
{
result.SetException(ex);
}
}
else
{
result.SetCanceled();
}
return result.Task;
}
#endif
#endregion
}
/// <summary>
/// Represents the method that will handle errors when executing MySQL statements.
/// </summary>
internal delegate void MySqlStatementExecutedEventHandler(object sender, MySqlScriptEventArgs args);
/// <summary>
/// Represents the method that will handle errors when executing MySQL scripts.
/// </summary>
internal delegate void MySqlScriptErrorEventHandler(object sender, MySqlScriptErrorEventArgs args);
/// <summary>
/// Sets the arguments associated to MySQL scripts.
/// </summary>
internal class MySqlScriptEventArgs : EventArgs
{
internal ScriptStatement Statement { get; set; }
/// <summary>
/// Gets the statement text.
/// </summary>
/// <value>The statement text.</value>
public string StatementText => Statement.text;
/// <summary>
/// Gets the line.
/// </summary>
/// <value>The line.</value>
public int Line => Statement.line;
/// <summary>
/// Gets the position.
/// </summary>
/// <value>The position.</value>
public int Position => Statement.position;
}
/// <summary>
/// Sets the arguments associated to MySQL script errors.
/// </summary>
internal class MySqlScriptErrorEventArgs : MySqlScriptEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="MySqlScriptErrorEventArgs"/> class.
/// </summary>
/// <param name="exception">The exception.</param>
public MySqlScriptErrorEventArgs(Exception exception)
{
Exception = exception;
}
/// <summary>
/// Gets the exception.
/// </summary>
/// <value>The exception.</value>
public Exception Exception { get; }
/// <summary>
/// Gets or sets a value indicating whether this <see cref="MySqlScriptErrorEventArgs"/> is ignored.
/// </summary>
/// <value><c>true</c> if ignore; otherwise, <c>false</c>.</value>
public bool Ignore { get; set; }
}
struct ScriptStatement
{
public string text;
public int line;
public int position;
}
}
#endif