Click here to Skip to main content
15,886,258 members
Articles / Programming Languages / SQL

SQL Editor for Database Developers

Rate me:
Please Sign up or sign in to vote.
4.55/5 (65 votes)
10 Mar 2010GPL317 min read 250.6K   9K   236  
SQL editor with syntax parser, direct editing, code execution, database backup, table comparison, script generation, time measurement
// -------------------------------------------------------
// SqlBuilder by Elm�Soft
// www.netcult.ch/elmue
// www.codeproject.com/KB/database/SqlBuilder.aspx
// -------------------------------------------------------

using System;
using System.Xml;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using System.Collections;
using System.Threading;
using System.Windows.Forms;
using SqlBuilder.Forms;

using eType  = SqlBuilder.Controls.ListViewEx.eType;

namespace SqlBuilder
{
	/// <summary>
	/// Access to Microsoft SQL server
	/// </summary>
	public class SQL
	{
		string        ms_Server;
		string        ms_DataBase;
		string        ms_User;
		string        ms_Password;
		string        ms_Sql;
		// data for thread
		Form          mi_Owner;
		frmWait       mi_WaitForm;
		Thread        mi_Thread;
		SqlConnection mi_Connect;
		SqlCommand    mi_Command;
		DataSet       mi_DataSet;
		string        ms_ErrorMsg;
		string        ms_ExecTime;
		bool          mb_Aborting;
		int         ms32_FirstLine;

		/// <summary>
		/// Constructor
		/// Set s_User and s_Password = null for a trusted connection
		/// </summary>
		public SQL(Form i_Owner, string s_Server, string s_DataBase, string s_User, string s_Password)
		{
			mi_Owner    = i_Owner;
			ms_Server   = s_Server;
			ms_DataBase = s_DataBase;
			ms_User     = s_User;
			ms_Password = s_Password;
		}

		/// <summary>
		/// returns the SQL execution time in seconds of the last SQL command
		/// </summary>
		public string ExecuteTime
		{
			get { return ms_ExecTime; }
		}

		/// <summary>
		/// returns a DataSet or null on error
		/// This function should be called from a GUI thread
		/// s32_FirstLine = -1 -> don't print line numbers in error messages
		/// otherwise it is added to the linenumber in the error message
		/// </summary>
		public DataSet ExecuteSQL(string s_Sql, int s32_FirstLine)
		{
			if (mi_Thread != null)
				// Due to heavy bugs in .NET framework you cannot abort a thread without problems
				// So if you want a Cancel functionality you must create a new thread each time
				// and let it run until it terminates alone !!!!
				// So for each thread a new SQL class instance is required
				throw new Exception("Application design error: Create a new instance of the SQL class for each query!");

			if (ms_User != null && ms_User.Length == 0) { frmMsgBox.Err(mi_Owner, "You must specify a user!"); return null; }

			if (ms_Server.   Length == 0) { frmMsgBox.Err(mi_Owner, "You must specify a SQL server!"); return null; }
			if (ms_DataBase. Length == 0) { frmMsgBox.Err(mi_Owner, "You must specify a database!");   return null; }
			if (s_Sql.Trim().Length == 0) { frmMsgBox.Err(mi_Owner, "No SQL code specified!");         return null; }

			ms_Sql         = s_Sql;
			mi_DataSet     = null;
			ms_ErrorMsg    = null;
			mb_Aborting    = false;
			ms32_FirstLine = s32_FirstLine;
			ms_ExecTime    = "0.000";

			mi_WaitForm = new frmWait(); // ORDER FIRST!
			mi_WaitForm.evUserAbort += new frmWait.UserAbort(OnUserAbort);

			mi_Thread = new Thread(new ThreadStart(WorkThread));
			mi_Thread.Start(); // ORDER AFTER!

			// ShowDialog() blocks until the thread or the user closes the frmWait window
			mi_WaitForm.ShowDialog(mi_Owner);

			if (ms_ErrorMsg != null && !mb_Aborting)
				frmMsgBox.Err(mi_Owner, ms_ErrorMsg);

			DataSet i_Set = mi_DataSet;  // this avoids a memory leak !

			mi_Connect  = null; // this avoids a memory leak !
			mi_Command  = null; // this avoids a memory leak !
			mi_DataSet  = null; // this avoids a memory leak !
			mi_WaitForm = null;

			return i_Set;
		}

		void WorkThread()
		{
			try
			{
				// SetLabelText is threadsafe
				mi_WaitForm.SetLabelText("Connecting server...");

				// ####################################################################################
				// To see how to build a connection string look at: http://www.connectionstrings.com !
				// ####################################################################################

				string s_Connect = String.Format("Server={0}; Database={1}; ", ms_Server, ms_DataBase);

				if (ms_User == null && ms_Password == null)
					s_Connect += "Trusted_Connection=yes;";
				else
					s_Connect += String.Format("User ID={0}; Password={1}; Trusted_Connection=no;", ms_User, ms_Password);

				mi_Connect = new SqlConnection(s_Connect);
				mi_Connect.Open();

				if (mb_Aborting)
					goto _Exit; // User has canceled meanwhile
			}
			catch (Exception Ex)
			{
				mi_Connect  = null;
				ms_ErrorMsg = string.Format("Error while connecting server {0}.\n{1}", ms_Server, Ex.Message);
				goto _Exit;
			}

			int s32_FirstGoLine = 0;
			try
			{
				// SetLabelText is threadsafe
				mi_WaitForm.SetLabelText("Execute SQL...");

				DataSet i_DataSet = null;
				StringBuilder s_Cmd = new StringBuilder(50000);

				Measure i_Measure = new Measure();

				string[] s_Lines = ms_Sql.Replace("\r", "").Split('\n');
				for (int L=0; L<s_Lines.Length; L++)
				{
					bool b_GO = s_Lines[L].ToUpper().StartsWith("GO");
					// Check that it is not "GOTO"
					if (s_Lines[L].Length > 2 && s_Lines[L][2] != ' ')
						b_GO = false;

					if (!b_GO)
					{
						s_Cmd.Append(s_Lines[L]);
						s_Cmd.Append("\n");
					}
					
					if (b_GO || L == s_Lines.Length-1) // last line -> always execute
					{
						if (s_Cmd.Length > 0)
						{
							mi_Command = new SqlCommand(s_Cmd.ToString(), mi_Connect);
							mi_Command.CommandTimeout = Defaults.Timeout;
							SqlDataAdapter i_Adapter  = new SqlDataAdapter(mi_Command);
							i_DataSet = new DataSet();
							i_Adapter.Fill(i_DataSet); // here the SQL code is sent to the server
							s_Cmd.Length = 0;
						}
						s32_FirstGoLine = L+1; // store for the NEXT command
					}
				}

				ms_ExecTime = i_Measure.ElapsedTimeStr;
				mi_DataSet  = i_DataSet;
			}
			catch (Exception Ex)
			{
				ms_ErrorMsg = ParseErrorMessage(Ex, ms32_FirstLine + s32_FirstGoLine);
				mi_DataSet  = null;
			}

			mi_Command = null;
			try { mi_Connect.Close(); }
			catch {} // catch bugs in .NET framework

			_Exit:

			// Close() is threadsafe
			if (!mb_Aborting)
				mi_WaitForm.Close();
		}

		/// <summary>
		/// User clicked the Abort button in mi_WaitForm
		/// </summary>
		private void OnUserAbort()
		{
			if (mb_Aborting) // block multiple clicks on button "Abort"
				return;

			mb_Aborting = true;
			mi_WaitForm.SetLabelText("Aborting...");

			if (mi_Command != null)
			{
				mi_Command.Cancel();
				Thread.Sleep(500);
			}

			if (mi_Connect != null)
			{
				try { mi_Connect.Close(); }
				catch {} // Bugs in .NET framework !
			}

			// DO NOT ABORT THE THREAD HERE! (mi_Thread.Abort())
			// Due to a bad design in .NET framework this would cause multiple problems
		}

		/// <summary>
		/// returns a DataTable or null on error
		/// This function should be called from a GUI thread
		/// </summary>
		public DataTable ReadTable(string s_SQL, int s32_FirstLine)
		{
			DataSet i_DataSet = ExecuteSQL(s_SQL, s32_FirstLine);
			if (i_DataSet == null)
				return null;

			if (i_DataSet.Tables.Count != 1)
			{
				frmMsgBox.Err(mi_Owner, string.Format("The SQL command returned a recordset with {0} instead of 1 tables!", i_DataSet.Tables.Count));
				return null;
			}

			return i_DataSet.Tables[0];
		}

		/// <summary>
		/// returns null on error or if the query did not return any result!
		/// returns DBNull if a NULL in the database has been read!
		/// This function should be called from a GUI thread
		/// </summary>
		public object ReadScalar(string s_SQL, int s32_FirstLine)
		{
			DataTable i_Table = ReadTable(s_SQL, s32_FirstLine);
			if (i_Table == null)
				return null;

			object o_Value;
			if (!SqlTable.IsTableScalarOrEmpty(i_Table, out o_Value))
				frmMsgBox.Err(mi_Owner, string.Format("The SQL command returned a table with {0} rows and {1} columns instead of one scalar value.", i_Table.Rows.Count, i_Table.Columns.Count));

			i_Table.Clear();
			return o_Value;
		}

		/// <summary>
		/// returns a list of all databases or null on error
		/// </summary>
		public string[] ListAllDataBases()
		{
			ms_DataBase = "master";
			DataTable i_Table = ReadTable("SELECT name FROM sysdatabases ORDER BY name", -1);
			if (i_Table == null)
				return null;

			string[] s_Bases = new string[i_Table.Rows.Count];
			for (int R=0; R<i_Table.Rows.Count; R++)
			{
				s_Bases[R] = (string) i_Table.Rows[R]["name"];
			}
			i_Table.Clear();
			return s_Bases;
		}

		/// <summary>
		/// returns a list of all functions, procedures etc.. in a database
		/// returns null on error
		/// </summary>
		public string[] ListAllSysObjects(eType e_Type)
		{
			string  s_SQL = "SELECT name FROM sysobjects WHERE ";
			switch (e_Type)
			{
				case eType.PROCEDURE: s_SQL += "xtype='P'  OR xtype='X'"; break;
				case eType.FUNCTION:  s_SQL += "xtype='IF' OR xtype='FN' OR xtype='TF'"; break;
				case eType.VIEW:      s_SQL += "xtype='V'";  break;
				case eType.TRIGGER:   s_SQL += "xtype='TR'"; break;
				case eType.TABLE:     s_SQL += "xtype='U'";  break;
				default: return null;
			}
			s_SQL += "ORDER BY name";

			DataTable i_Table = ReadTable(s_SQL, -1);
			if (i_Table == null)
				return null;

			string[] s_SysObj = new string[i_Table.Rows.Count];
			for (int R=0; R<i_Table.Rows.Count; R++)
			{
				s_SysObj[R] = (string)i_Table.Rows[R]["name"];
			}
			i_Table.Clear();
			return s_SysObj;
		}

		/// <summary>
		/// Read a trigger, view, procedure, function
		/// This function should be called from a GUI thread
		/// returns null on error
		/// </summary>
		public string ReadSysObject(string s_SysObj, eType e_Type)
		{
			string s_SQL = string.Format("SELECT text, encrypted FROM syscomments WHERE id = object_id('[{0}]') ORDER BY number, colid", s_SysObj);
			DataTable i_Table = ReadTable(s_SQL, -1);
			if (i_Table == null)
				return null;

			if (i_Table.Rows.Count == 0)
			{
				frmMsgBox.Err(mi_Owner, string.Format("The {0} '{1}' does not exist on the SQL server.", e_Type.ToString().ToLower(), s_SysObj));
				return null;
			}

			if ((bool)i_Table.Rows[0]["encrypted"])
			{
				frmMsgBox.Err(mi_Owner, string.Format("The {0} '{1}' is encrypted. This is currently not supported.", e_Type.ToString().ToLower(), s_SysObj));
				return null;
			}

			string s_Content = "";
			for (int R=0; R<i_Table.Rows.Count; R++)
			{
				s_Content += i_Table.Rows[R]["text"];
			}

			// single LF -> CR + LF
			return Functions.ReplaceCRLF(s_Content.Trim());
		}

		/// <summary>
		/// returns true if a procedure, view or function exists in the database, false if not exists
		/// returns null on server error
		/// This function should be called from a GUI thread
		/// </summary>
		public object SysObjectExists(string s_SysObj)
		{
			string s_SQL = "SELECT COUNT(*) FROM sysobjects WHERE id= object_id('["+s_SysObj+"]')";
			object o_Exists = ReadScalar(s_SQL, -1);

			if (!(o_Exists is Int32))
				return null;

			return ((int)o_Exists > 0);
		}

		/// <summary>
		/// returns column definitions like name, data type, primary key, indent, etc..
		/// s_TableName = null   --> read all tables
		/// s_TableName = "Name" --> read only the specified table
		/// returns a sorted ArrayList of TableCol definitions for each column in each table
		/// returns null on SQL error
		/// </summary>
		public TableDef LoadTableDefFromServer(string s_TableName)
		{
			string s_Sql = BuildSqlGetTableColumns(s_TableName); 
			DataTable i_DefintionsTable = ReadTable(s_Sql, -1);
			if (i_DefintionsTable == null)
				return null;

			return TableDef.FromDataTable(i_DefintionsTable);
		}

		/// <summary>
		/// See GetTableColumns()
		/// </summary>
		private string BuildSqlGetTableColumns(string s_TableName)
		{
			string s_Where = ""; 
			if (s_TableName != null) 
				s_Where += string.Format(" WHERE obj.name = '{0}'\n", s_TableName);

			return string.Format(Functions.ReadStringResource("GetTableColumns.sql"), s_Where) + "\n";
		}

		/// <summary>
		/// returns a Hashtable with
		/// -- Key = "Triggers", "Procedures", Views", "Functions", "Dates"
		/// -- Value = embedded hastable with
		///     --  Key   = Name of procedure, table etc..
		///     --  Value = Content of Proc, View etc.. (string) / TableDef instance / DateTime
		/// </summary>
		public Hashtable ReadAllSysObjects(bool b_Trig, bool b_Proc, bool b_View, bool b_Func, bool b_Tabl, bool b_Date)
		{
			Hashtable i_SysObjects = new Hashtable();

			const string s_Fmt = "SELECT object_name(id) AS name, text "
							   + "FROM syscomments "
							   + "WHERE encrypted=0 AND ({0}) "
							   + "ORDER BY id, colid \n"; // SORTING IMPORTANT!!!

			Hashtable i_List = new Hashtable();

			if (b_Trig) i_List.Add("Triggers",   string.Format(s_Fmt, "objectproperty(id, 'IsTrigger')=1"));
			if (b_Proc) i_List.Add("Procedures", string.Format(s_Fmt, "objectproperty(id, 'IsProcedure')=1 OR objectproperty(id, 'IsExtendedProc')=1"));
			if (b_View) i_List.Add("Views",      string.Format(s_Fmt, "objectproperty(id, 'IsView')=1"));
			if (b_Func) i_List.Add("Functions",  string.Format(s_Fmt, "objectproperty(id, 'IsTableFunction')=1 OR objectproperty(id, 'IsInlineFunction')=1 OR objectproperty(id, 'IsScalarFunction')=1"));
			if (b_Tabl) i_List.Add("Tables",     BuildSqlGetTableColumns(null));
			if (b_Date) i_List.Add("Dates",      "SELECT name, crdate FROM sysobjects\n");

			if (i_List.Count == 0)
				return null; // nothing to do

			string s_SQL = "";
			foreach (string s_Key in i_List.Keys)
			{
				s_SQL += (string)i_List[s_Key];
			}

			// Retrieve all tables in one dataset
			DataSet i_DataSet = ExecuteSQL(s_SQL, -1);
			if (i_DataSet == null)
				return null;

			// Copy from dataset
			StringBuilder s_Content = new StringBuilder(100000);
			int T=0;
			foreach (string s_Key in i_List.Keys) // NO for(...) here !!!! Same order as above !!!
			{
				Application.DoEvents();
				DataTable i_Table = i_DataSet.Tables[T++];
				Hashtable i_Hash  = new Hashtable();

				switch (s_Key)
				{
					case "Dates":
					{
						// Copy DataTable "Dates" into Hashtable
						foreach (DataRow i_Row in i_Table.Rows)
						{
							i_Hash[i_Row["name"]] = i_Row["crdate"];
						}
						break;
					}

					case "Tables":
					{
						// read all table definitions from the data table
						// with one TableDef (value) each table name (key)
						i_Hash = TableDef.TableFactory(i_Table);
						break;
					}

					default: // Proc, View, Func, Trig
					{
						for (int R=0; R<i_Table.Rows.Count; R++)
						{
							DataRow i_Row  = i_Table.Rows[R];
							string  s_Name = Functions.ToStr(i_Row["name"]);

							if (s_Name.Length == 0)
							{
								frmMsgBox.Err(mi_Owner, "FATAL SERVER ERROR: Sql Server returned invalid data!!");
								return null; // This may happen if the database is inconsistent -> re-install server!
							}

							s_Content.Append(i_Row["text"]);

							// Procedures, Functions > 8 kB Unicode data are split over multiple table rows !
							if (R+1 == i_Table.Rows.Count || s_Name != Functions.ToStr(i_Table.Rows[R+1]["name"]))
							{
								i_Hash[s_Name]   = s_Content.ToString();
								s_Content.Length = 0;
							}
						}
						break;
					}
				} // switch
				
				i_SysObjects[s_Key] = i_Hash;
			} // foreach

			i_DataSet.Clear(); // Avoid memory leak (.NET bug)
			return i_SysObjects;
		}

		/// <summary>
		/// returns an Array with all available types like "int", "varchar", "image",... 
		/// </summary>
		public string[] ReadAllDataTypes()
		{
			string s_Sql = "SELECT name FROM systypes ORDER BY name";
			DataTable i_Table = ReadTable(s_Sql, -1);
			if (i_Table == null)
				return null;

			string[] i_Types = new string[i_Table.Rows.Count];
			for(int R=0; R<i_Table.Rows.Count; R++)
			{
				i_Types[R] = (string)i_Table.Rows[R]["name"];
			}
			return i_Types;
		}

		// *************************************** STATIC *******************************************
		// *************************************** STATIC *******************************************
		// *************************************** STATIC *******************************************

		/// <summary>
		/// returns a SQL command to delete a sysobject
		/// </summary>
		public static string BuildDeleteSysObjectCommand(string s_SysObj, eType e_Type)
		{
			if (e_Type == eType.SQL || e_Type == eType.TABLE)
				return "";

			return string.Format("IF NOT object_id('{0}') IS NULL\nBEGIN\n  DROP {1} [{0}]\nEND\nGO\n\n",
			                     s_SysObj, e_Type);
		}

		/// <summary>
		/// ***************************************************************
		/// ATTENTION: Ex.LineNumber may be wrong due to server bugs (if 1)
		/// ***************************************************************
		/// Ex.Message = "Line 3: Incorrect syntax near 'LIKE'.\r\nLine 5: Incorrect syntax near 'AND'."
		/// Correct line numbers corresponding to the first line which is selected
		/// If the entire SQL code is executed s32_FirstSelectedLine must be = 1
		/// If no display of line numbers is desired s32_FirstSelectedLine must be = -1
		/// </summary>
		public static string ParseErrorMessage(Exception Ex, int s32_FirstSelectedLine)
		{
			int s32_ExcepLine = 0;
			if (Ex is SqlException)
				s32_ExcepLine = ((SqlException)Ex).LineNumber;

			string[] s_Parts = Ex.Message.Replace("\r", "").Split('\n');
			string   s_Msg   = "";

			for (int i=0; i<s_Parts.Length; i++)
			{
				int s32_ErrLine = 0;
				if (s_Parts[i].StartsWith("Line ") || 
					s_Parts[i].StartsWith("L�nea ")|| 
					s_Parts[i].StartsWith("Zeile "))
				{
					int End = s_Parts[i].IndexOf(":");
					if (End > 5 && End < 10)
					{
						string s_Number = s_Parts[i].Substring(5, End-5);
						s32_ErrLine = int.Parse(s_Number);

						s_Parts[i] = s_Parts[i].Substring(End+1).Trim(); // cut "Line No:"
					}
				}

				// If not "Line 23:" specified in error message use SqlException.LineNumber for the first error
				// ATTENTION: Ex.LineNumber may be wrongly 1 or 65536 due to server bugs
				// Use it only if greater than 1 and smaller than 50000
				if (s32_ErrLine == 0 && i == 0 && s32_ExcepLine > 1 && s32_ExcepLine < 50000)
					s32_ErrLine = s32_ExcepLine;

				if (s32_FirstSelectedLine > 0 && s32_ErrLine > 0)
					s_Msg += string.Format("Line {0}: {1}\n", s32_ErrLine + s32_FirstSelectedLine -1, s_Parts[i]);
				else
					s_Msg += s_Parts[i] + "\n";
			}

			if (s_Msg.Trim().Length == 0) // This happens ! (it's from Microsoft)
				s_Msg = "The Sql server created an exception without a message!";

			return s_Msg;
		}
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)


Written By
Software Developer (Senior) ElmüSoft
Chile Chile
Software Engineer since 40 years.

Comments and Discussions