Files
Sdaleo/Internal/SQLParser.cs
2016-07-21 16:55:03 -04:00

463 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Text.RegularExpressions;
/// <summary>
/// MySQLCommands are embedded in SQL Scripts as follows:
///--# [Command] [Version] [Arg1] [Arg2] ... [Arg3]
///--#
///~ Serves as the Command End Marker!
///-- Possible Commands are, PRE_CREATE,CREATE,POST_CREATE,PRE_ALTER,ALTER,POST_ALTER,PRE_DROP,DROP,POST_DROP,RENAME
///-- Command Execution is as follows.
///
///-- CREATE must exists.
///-- For New Install: *Script ID does not exist* -- WILL ALWAYS TAKE ONLY LATEST VERSION for CREATE, runs PRE-CREATEs and POST-CREATEs accordingly
///-- PRE-CREATE, CREATE, POST-CREATE (If you want nothing done, leave command blank)
///-- Allow Versions to use *, so that we can say for all [0.*.*] call this PRE-CREATE, or POST-CREATE
///
///-- For Update Install: *Script ID exists* - Will check db where to start and execute until to highest version of CREATE was reached
///-- PRE-DROP, DROP, POST-DROP, PRE-RENAME, RENAME, POST-RENAME, PRE-ALTER, ALTER, POST-ALTER, for each respective version
///
///-- Versioning is done as follows:
/// (Major version).(Minor version).(Revision number)
///
/// MySQLIdentifiers are embedded in SQL Scripts as follows:
///--$ [Identifier] [Value], Identifier structure
///
///--$ [ScriptId] [Guid]!
///-- Each script is versioned on it's own, so that is what we store in the db.
///-- Give each script a UNIQUE GUID.
///-- We will then store the ScriptID guid in the versioning table, along side the highest version of the script that most recently ran!,
///-- ~this way we can always know if something has to be done for each database object,
/// </summary>
namespace Sdaleo.Internal
{
/// <summary>
///
/// </summary>
public enum MySQLCommandType
{
PRE_CREATE,
CREATE,
POST_CREATE,
PRE_ALTER,
ALTER,
POST_ALTER,
PRE_DROP,
DROP,
POST_DROP,
PRE_RENAME,
RENAME,
POST_RENAME,
}
/// <summary>
/// Custom SQL Commands that are part of an SQL Script.
/// They start as follows:
/// --# [Command] [Version] [Arg1] [Arg2] ... [Arg3]
///
/// and end as follows:
/// --#
/// ~Serves as the Command End Marker!
/// Everything in between is SQL.
/// </summary>
internal class MySQLCommand
{
#region Members N' Properties
// Line Specifics
internal uint Line_CommandStart { get; private set; }
internal uint Line_CommandEnd { get; private set; }
internal string[] SQLScriptBlock { get; private set; }
// Command Specifics
internal MySQLCommandType Command { get; private set; }
internal string Version
{
get
{
if (_Versioning != null)
return _Versioning.Version;
else
return String.Empty;
}
}
internal Versioning _Versioning = null;
internal string[] Arguments = null;
/// <summary>
/// Returns true if there are Lines between CommandStart and CommandEnd,
/// this indicates that it contains sql in between
/// </summary>
internal bool ContainsSQLScript
{
get
{
if (Line_CommandStart >= Line_CommandEnd)
return false;
else if ((Line_CommandStart + 1) == Line_CommandEnd)
return false;
else if ((SQLScriptBlock != null) && (SQLScriptBlock.Length > 0))
return true;
else
return false;
}
}
#endregion
#region Internal Execute
internal DBError ExecuteScriptBlock(IConnectDb credential)
{
DBError dbError = DBError.Create("No Script Block to Execute");
if ((SQLScriptBlock != null) && (SQLScriptBlock.Length > 0))
{
foreach (string sqlLine in SQLScriptBlock)
{
DB db = DB.Create(credential);
dbError = db.ExecuteNonQuery(sqlLine);
if (dbError.ErrorOccured)
break;
}
}
return dbError;
}
#endregion
#region Construction
/// <summary>
/// Used to Parse a SQLCommand from an SQL Script
/// </summary>
/// <param name="Line_CommandStart">Specifies the line of code where the Command is located --# [Command] [Version] [Arg1] [Arg2] ... [Arg3] </param>
/// <param name="Line_CommandStartString">The String of Line_CommandStart, that contains all the Command Values the --# [Command] [Version] [Arg1] [Arg2] ... [Arg3] string</param>
/// <param name="Line_CommandEnd">Specifies the line of code where the Command ends --#</param>
internal MySQLCommand(uint Line_CommandStart, string Line_CommandStartString, uint Line_CommandEnd, string[] sqlScriptBlock)
{
this.Line_CommandStart = Line_CommandStart;
this.Line_CommandEnd = Line_CommandEnd;
ParseLine_CommandStartString(Line_CommandStartString);
this.SQLScriptBlock = sqlScriptBlock;
}
/// <summary>
/// Parses a CommmandStartString and fills the values for this class
/// </summary>
/// <param name="LineCommandStartString">a line that contains a CommandStartString</param>
private void ParseLine_CommandStartString(string LineCommandStartString)
{
/// --# [Command] [Version] [Arg1] [Arg2] ... [Arg3]
/// --# [Create] [1.0.0] [Arg1] [Arg2]
Regex rx = new Regex(@"\[([\w\a-\?\.\*\+]+)\]");
MatchCollection matches = rx.Matches(LineCommandStartString);
if (matches.Count < 2)
throw new ArgumentException("LineIdentifierString is Invalid");
// Get the Command and Command Version
string m1 = matches[0].ToString().Trim(new char[] { '[', ']' });
string m2 = matches[1].ToString().Trim(new char[] { '[', ']' });
this.Command = (MySQLCommandType)Enum.Parse(typeof(MySQLCommandType), m1.ToUpper());
this._Versioning = new Versioning(m2);
// Get Optional Arguments
int nArguments = matches.Count - 2;
if (nArguments > 0)
{
Arguments = new string[nArguments];
for (int i = 0; i < nArguments; ++i)
Arguments[i] = matches[2 + i].ToString().Trim(new char[] { '[', ']' });
}
}
#endregion
#region Internal Statics
internal const string IDENTIFY_SQLSCRIPT_COMMAND = "--#";
/// <summary>
/// Returns true if the passed in line is a MySQLCommandStartLine
/// </summary>
/// <param name="line">line to check</param>
/// <returns>true if MySQLCommandStartLine, false otherwise</returns>
internal static bool IsMySQLCommandStartLine(string line)
{
string lineTrimed = line.Trim();
if (lineTrimed.StartsWith(IDENTIFY_SQLSCRIPT_COMMAND) && lineTrimed.Length != IDENTIFY_SQLSCRIPT_COMMAND.Length)
return true;
return false;
}
/// <summary>
/// Returns ture if the passed in line is a MySQLCommandEndLine
/// </summary>
/// <param name="line">line to check</param>
/// <returns>true if MySQLCommandEndLine, false otherwise</returns>
internal static bool IsMySQLCommandEndLine(string line)
{
string lineTrimed = line.Trim();
if (lineTrimed.StartsWith(IDENTIFY_SQLSCRIPT_COMMAND) && lineTrimed.Length == IDENTIFY_SQLSCRIPT_COMMAND.Length)
return true;
return false;
}
#endregion
}
/// <summary>
///
/// </summary>
public enum MySQLIdentifierType
{
SCRIPTID,
SCRIPTVERSION,
}
/// <summary>
/// Custom SQL Identifiers that are part of an SQL Script.
/// They start as follows:
/// --$ [Identifier] [Value]
/// </summary>
public class MySQLIdentifier
{
#region Members N' Properties
// Line Specifics
internal uint Line_IdentifierStart { get; private set; }
// Identifier Specifics
internal MySQLIdentifierType Identity { get; private set; }
internal string Value { get; private set; }
#endregion
#region Construction
/// <summary>
/// Used to Parse a SQLCommand from an SQL Script
/// </summary>
/// <param name="Line_Identifier">Specifies the line of code where the Identifier is located --$ [Identifier] [Value] </param>
/// <param name="Line_IdentifierString">The String of Line_IdentifierString, that contains all the Identifier Values the --$ [Identifier] [Value] string</param>
internal MySQLIdentifier(uint Line_IdentifierStart, string Line_IdentifierString)
{
this.Line_IdentifierStart = Line_IdentifierStart;
ParseLine_IndentifierStartString(Line_IdentifierString);
}
/// <summary>
/// Parses a LineIdentifierString and fills the values for this class
/// </summary>
/// <param name="LineIdentifierString">a line that contains a LineIdentifierString</param>
private void ParseLine_IndentifierStartString(string LineIdentifierString)
{
/// --$ [Identifier] [Value]
/// --$ [ScriptID] [12312-312321-3213]
Regex rx = new Regex(@"\[([\w\a-\?\.\*\+]+)\]");
MatchCollection matches = rx.Matches(LineIdentifierString);
if (matches.Count < 2)
throw new ArgumentException("LineIdentifierString is Invalid");
// Get the Identity and Value
string m1 = matches[0].ToString().Trim(new char[] { '[', ']' });
string m2 = matches[1].ToString().Trim(new char[] { '[', ']' });
this.Identity = (MySQLIdentifierType)Enum.Parse(typeof(MySQLIdentifierType), m1.ToUpper());
this.Value = m2;
}
#endregion
#region Internal Statics
internal const string IDENTIFY_SQLSCRIPT_IDENTIFIER = "--$";
/// <summary>
/// Returns true if the passed in line is a MySQLIdentifier
/// </summary>
/// <param name="line">line to check</param>
/// <returns>true if MySQLIdentifier, false otherwise</returns>
internal static bool IsMySQLIdentifier(string line)
{
string lineTrimed = line.Trim();
if (lineTrimed.StartsWith(IDENTIFY_SQLSCRIPT_IDENTIFIER) && lineTrimed.Length != IDENTIFY_SQLSCRIPT_IDENTIFIER.Length)
return true;
return false;
}
#endregion
}
/// <summary>
/// Helper class that parses an SQL Script for Commands and Identifiers,
/// for processing.
/// </summary>
internal static class SQLParser
{
/// <summary>
/// Iterates thru the Identifiers and returns the value of the first identifier that matches the identity.
/// </summary>
/// <returns>the value of the first matched identity, String.Empty if not found</returns>
internal static string GetValueForFirstFoundMySQLIdentifierType(MySQLIdentifierType Identity, MySQLIdentifier[] identifiers)
{
if (identifiers != null && identifiers.Length > 0)
{
foreach (MySQLIdentifier identifier in identifiers)
{
if (identifier.Identity == Identity)
return identifier.Value;
}
}
return String.Empty;
}
/// <summary>
/// Parses an SQLScript for MySQLCommand Objects
/// </summary>
/// <param name="sqlscript">a string array from an sql script</param>
/// <param name="commands">all MySQLCommand objects found, if any, otherwise, empty []</param>
/// <param name="identifiers">all MySQLIdentifier objects found, if any, otherwise, empty []</param>
internal static void ParseSQLScriptForMySQLCommandsAndIdentifiers(string[] sqlscript, out MySQLCommand[] commands, out MySQLIdentifier[] identifiers)
{
// Result set
commands = null;
identifiers = null;
List<MySQLCommand> CommandsFound = new List<MySQLCommand>();
List<MySQLIdentifier> IdentifiersFound = new List<MySQLIdentifier>();
// intermediary variables
int nStartLine = -1;
string strStartLine = String.Empty;
// Iterate thru all lines
for (int i = 0; i < sqlscript.Length; ++i)
{
string curLine = sqlscript[i];
/// Parse for
/// --$ [Identifier] [Value]
if (MySQLIdentifier.IsMySQLIdentifier(curLine))
{
IdentifiersFound.Add(new MySQLIdentifier((uint)i, curLine));
continue;
}
/// Parse for
/// --# [Command] [Version] [Arg1] [Arg2] ... [Arg3]
/// ...
/// --#
if (MySQLCommand.IsMySQLCommandStartLine(curLine))
{
nStartLine = i;
strStartLine = curLine;
continue;
}
else if (MySQLCommand.IsMySQLCommandEndLine(curLine))
{
// Something is wrong with the script!
if ((nStartLine == -1) || String.IsNullOrEmpty(strStartLine))
{
Debug.Assert(false);
}
else
{
if (nStartLine == i || nStartLine == i + 1)
{
Debug.Assert(false); // There is no SQLCodeBlock! = something is wrong with the script
nStartLine = -1;
strStartLine = String.Empty;
continue;
}
// Create the SQLScriptBlock
List<String> sqlScriptBlock = new List<String>();
for (int j = nStartLine; j < i; ++j)
sqlScriptBlock.Add(sqlscript[j]);
// Convert the SQLScriptBlock into Executable Code
string[] executableSQLCodeBlock = GetExecutableSqlTextFromSQLScriptBlock(sqlScriptBlock.ToArray());
// Create the Commands Object that now contains the Executable SQL Code
CommandsFound.Add(new MySQLCommand((uint)nStartLine, strStartLine, (uint)i, executableSQLCodeBlock));
nStartLine = -1;
strStartLine = String.Empty;
}
continue;
}
}
// Delegate to Propertly Sort the MySQLCommands, First, by Command, then by Version
System.Comparison<MySQLCommand> comparer = delegate(MySQLCommand x, MySQLCommand y)
{
// First Compare the Command,
int nCompare = x.Command.CompareTo(y.Command);
// If equal, compare the versioning
if(nCompare == 0)
nCompare = x._Versioning.CompareTo(y._Versioning);
return nCompare;
};
CommandsFound.Sort(comparer);
// Pass out Result set
commands = CommandsFound.ToArray();
identifiers = IdentifiersFound.ToArray();
}
/// <summary>
/// Use this to Parse an SQL Script Block (a block of code, contained inside a MySQLCommand.
/// It Ignores Comment Lines, and also breaks out the SQL Script by GO Statements.
/// </summary>
/// <param name="sqlscriptBlock">an unprocessed Script block from an SQL Script</param>
/// <returns>a ADO.Net Friend sql script block that can be executed</returns>
internal static string[] GetExecutableSqlTextFromSQLScriptBlock(string[] sqlscriptBlock)
{
bool bIsInCommentMode = false;
StringBuilder sbSQLLines = new StringBuilder();
List<String> sbSQLBrokenOut = new List<String>();
foreach (string line in sqlscriptBlock)
{
// Let's deal with comments, which allows us to put comments in our SQL Scripts
if (bIsInCommentMode)
{
if (line.StartsWith("*/") || line.EndsWith("*/"))
bIsInCommentMode = false;
continue;
}
else if (line.StartsWith("/*") && !line.EndsWith("*/"))
{
bIsInCommentMode = true; // skip lines until end of comment
continue;
}
else if (line.StartsWith("/*") && line.EndsWith("*/"))
continue; // skip single comment line
else if (line.StartsWith("--"))
continue; // skip single comment line
// We break out GO stmts to allow execution seperatly
if ((line.Length == 2) && line.ToUpper().StartsWith("GO"))
{
if (sbSQLLines.Length > 0)
sbSQLBrokenOut.Add(sbSQLLines.ToString());
sbSQLLines.Remove(0, sbSQLLines.Length); // clear sbSQLLines
continue;
}
// Ready to use the SQL Line
sbSQLLines.Append((line + " "));
}
if (sbSQLLines.Length > 0)
sbSQLBrokenOut.Add(sbSQLLines.ToString());
// Return the 'GO' Broken out SQL Script Block
return sbSQLBrokenOut.ToArray();
}
}
}