using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Diagnostics; using System.Text.RegularExpressions; /// /// 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, /// namespace Sdaleo.Internal { /// /// /// public enum MySQLCommandType { PRE_CREATE, CREATE, POST_CREATE, PRE_ALTER, ALTER, POST_ALTER, PRE_DROP, DROP, POST_DROP, PRE_RENAME, RENAME, POST_RENAME, } /// /// 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. /// 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; /// /// Returns true if there are Lines between CommandStart and CommandEnd, /// this indicates that it contains sql in between /// 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 /// /// Used to Parse a SQLCommand from an SQL Script /// /// Specifies the line of code where the Command is located --# [Command] [Version] [Arg1] [Arg2] ... [Arg3] /// The String of Line_CommandStart, that contains all the Command Values the --# [Command] [Version] [Arg1] [Arg2] ... [Arg3] string /// Specifies the line of code where the Command ends --# 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; } /// /// Parses a CommmandStartString and fills the values for this class /// /// a line that contains a CommandStartString 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 = "--#"; /// /// Returns true if the passed in line is a MySQLCommandStartLine /// /// line to check /// true if MySQLCommandStartLine, false otherwise 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; } /// /// Returns ture if the passed in line is a MySQLCommandEndLine /// /// line to check /// true if MySQLCommandEndLine, false otherwise 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 } /// /// /// public enum MySQLIdentifierType { SCRIPTID, SCRIPTVERSION, } /// /// Custom SQL Identifiers that are part of an SQL Script. /// They start as follows: /// --$ [Identifier] [Value] /// 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 /// /// Used to Parse a SQLCommand from an SQL Script /// /// Specifies the line of code where the Identifier is located --$ [Identifier] [Value] /// The String of Line_IdentifierString, that contains all the Identifier Values the --$ [Identifier] [Value] string internal MySQLIdentifier(uint Line_IdentifierStart, string Line_IdentifierString) { this.Line_IdentifierStart = Line_IdentifierStart; ParseLine_IndentifierStartString(Line_IdentifierString); } /// /// Parses a LineIdentifierString and fills the values for this class /// /// a line that contains a LineIdentifierString 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 = "--$"; /// /// Returns true if the passed in line is a MySQLIdentifier /// /// line to check /// true if MySQLIdentifier, false otherwise 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 } /// /// Helper class that parses an SQL Script for Commands and Identifiers, /// for processing. /// internal static class SQLParser { /// /// Iterates thru the Identifiers and returns the value of the first identifier that matches the identity. /// /// the value of the first matched identity, String.Empty if not found 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; } /// /// Parses an SQLScript for MySQLCommand Objects /// /// a string array from an sql script /// all MySQLCommand objects found, if any, otherwise, empty [] /// all MySQLIdentifier objects found, if any, otherwise, empty [] internal static void ParseSQLScriptForMySQLCommandsAndIdentifiers(string[] sqlscript, out MySQLCommand[] commands, out MySQLIdentifier[] identifiers) { // Result set commands = null; identifiers = null; List CommandsFound = new List(); List IdentifiersFound = new List(); // 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 sqlScriptBlock = new List(); 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 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(); } /// /// 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. /// /// an unprocessed Script block from an SQL Script /// a ADO.Net Friend sql script block that can be executed internal static string[] GetExecutableSqlTextFromSQLScriptBlock(string[] sqlscriptBlock) { bool bIsInCommentMode = false; StringBuilder sbSQLLines = new StringBuilder(); List sbSQLBrokenOut = new List(); 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(); } } }