From 9146e698c9c716916415f47408722ef257f6a187 Mon Sep 17 00:00:00 2001 From: Russ Kollmansberger Date: Fri, 5 Sep 2025 17:13:47 -0500 Subject: [PATCH] Updated SqlBuilder --- Model/Database.cs | 32 ++++++ SqlBuilder.cs | 255 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 251 insertions(+), 36 deletions(-) diff --git a/Model/Database.cs b/Model/Database.cs index f90873e..28f2d9a 100644 --- a/Model/Database.cs +++ b/Model/Database.cs @@ -196,6 +196,18 @@ namespace DbTools.Model { return SqlScript; } + /// + /// Imports the schema and metadata from the connected SQLite database into the application's internal + /// structures. + /// + /// This method reads the SQLite database schema, including tables, indexes, and + /// triggers, and populates the application's internal collections with the corresponding metadata. It + /// processes the `sqlite_master` table to extract the necessary information and organizes it into tables, + /// indexes, and triggers. The method clears any existing table metadata before importing new data. It + /// also formats the SQL definitions for tables to improve readability. This method assumes that + /// the database connection is already open and accessible. Ensure that the connection is valid before calling + /// this method. + /// if the import operation completes successfully. private bool importFromSqlite() { DbConnection.Open(); Tables.Clear(); @@ -264,6 +276,16 @@ namespace DbTools.Model { return true; } + /// + /// Appends index definitions to the corresponding tables in the database schema. + /// + /// This method updates the Indexes collection of each table in the Tables + /// dictionary with the provided index definitions. If a table does not exist in the Tables dictionary, + /// it is skipped. Only valid index definitions (those containing both a name and a SQL definition) are + /// added. + /// A dictionary where the key is the name of a table, and the value is a list of index definitions. Each index + /// definition is a semicolon-separated string, where the first part is the index name and the second part is + /// the index SQL definition. private void appendIndexes(Dictionary> indexes) { foreach (string index in indexes.Keys) { Table table = Tables[index]; @@ -278,6 +300,16 @@ namespace DbTools.Model { } } + /// + /// Appends trigger definitions to the corresponding tables in the database schema. + /// + /// This method updates the collection for each table + /// specified in the dictionary. If a table does not exist in the collection, it is skipped. Only trigger definitions with a valid format (name and SQL body + /// separated by a semicolon) are added. + /// A dictionary where each key represents the name of a table, and the associated value is a list of trigger + /// definitions for that table. Each trigger definition is a string containing the trigger name and its SQL + /// body, separated by a semicolon. private void appendTriggers(Dictionary> triggers) { foreach (string trigger in triggers.Keys) { Table table = Tables[trigger]; diff --git a/SqlBuilder.cs b/SqlBuilder.cs index 083619a..30c6009 100644 --- a/SqlBuilder.cs +++ b/SqlBuilder.cs @@ -1,11 +1,22 @@ using System; using System.Collections.Generic; using System.Data; +using System.Diagnostics; using System.Text; +using System.Text.RegularExpressions; namespace DbTools { public class SqlBuilder { + /// + /// Retrieves the names of all user-defined tables in the connected SQLite database. + /// + /// System tables (e.g., those starting with "sqlite_") are excluded from the results. + /// The method ensures the connection is open before executing the query. + /// An open to the SQLite database. The connection must not be null. + /// An array of strings containing the names of all user-defined tables in the database. The array will be empty + /// if no tables are found. + /// Thrown if is . public string[] GetTableNames(IDbConnection cn) { if (cn == null) { throw new ArgumentNullException("cn"); @@ -25,7 +36,23 @@ namespace DbTools { return tables.ToArray(); } - public string GetTableCreateSql(IDbConnection cn, string tableName, string selectStatement = "select * from $table") { + /// + /// Generates the SQL script to recreate the specified table, including its schema, indexes, triggers, and + /// optionally its data. + /// + /// The generated script includes the table's schema, indexes, and triggers. If a + /// is provided, the script will also include the data returned by the SELECT + /// statement. The method ensures the database connection is open before executing any commands. + /// The database connection to use. The connection must be to a SQLite database and can be in any state; the + /// method will ensure it is open. + /// The name of the table for which to generate the SQL script. Cannot be or empty. + /// An optional SQL SELECT statement to retrieve data from the table. If provided, the script will include the + /// data as part of the output. + /// A string containing the SQL script to recreate the table, including its schema, indexes, triggers, and + /// optionally its data. Returns an empty string if the table's schema cannot be determined. + /// Thrown if is or if is or empty. + public string GetTableCreateSql(IDbConnection cn, string tableName, string selectStatement = null) { if (cn == null) { throw new ArgumentNullException("cn"); } @@ -36,49 +63,205 @@ namespace DbTools { if (cn.State != ConnectionState.Open) { cn.Open(); } - selectStatement = selectStatement.Replace("$table", tableName); using (IDbCommand cmd = cn.CreateCommand()) { - cmd.CommandText = selectStatement; - using (IDataReader reader = cmd.ExecuteReader(CommandBehavior.SchemaOnly | CommandBehavior.KeyInfo)) { - DataTable schemaTable = reader.GetSchemaTable(); - if (schemaTable == null) { - throw new Exception("Could not get schema for table " + tableName); - } + cmd.CommandText = $"select sql from sqlite_master where tbl_name='{tableName}' and type='table'"; + using (IDataReader reader = cmd.ExecuteReader()) { StringBuilder sb = new StringBuilder(); - sb.AppendLine("CREATE TABLE " + tableName + " ("); - List pkColumns = new List(); - foreach (DataRow row in schemaTable.Rows) { - string columnName = row["ColumnName"].ToString(); - string dataType = row["DataTypeName"].ToString(); - int columnSize = Convert.ToInt32(row["ColumnSize"]); - bool allowDBNull = Convert.ToBoolean(row["AllowDBNull"]); - bool isKey = Convert.ToBoolean(row["IsKey"]); - if (isKey) { - pkColumns.Add(columnName); - } - sb.Append(" " + columnName + " " + dataType); - if (dataType.Equals("VARCHAR", StringComparison.OrdinalIgnoreCase) || - dataType.Equals("CHAR", StringComparison.OrdinalIgnoreCase) || - dataType.Equals("NVARCHAR", StringComparison.OrdinalIgnoreCase) || - dataType.Equals("NCHAR", StringComparison.OrdinalIgnoreCase)) { - sb.Append("(" + columnSize + ")"); - } - if (!allowDBNull) { - sb.Append(" NOT NULL"); - } - sb.AppendLine(","); + reader.Read(); + string sql = reader.GetString(0); + + Match m = Regex.Match(sql, "CREATE TABLE \\S+ \\((.*)\\)", RegexOptions.Singleline); + if (!m.Success) { + Trace.TraceWarning("Unable to match regex on table " + tableName); + return string.Empty; } - if (pkColumns.Count > 0) { - sb.AppendLine(" PRIMARY KEY (" + string.Join(", ", pkColumns) + ")"); - } else { - // Remove the last comma - sb.Length -= 3; + + sb.AppendLine("-- TABLE " + tableName + " --"); + int startIndex = m.Groups[1].Index; + int length = m.Groups[1].Length; + string columns = Regex.Replace(m.Groups[1].Value, "\\s{2,}", " "); + columns = Regex.Replace(columns.Replace(", ", ",").Replace(",\n", ","), ",(?!\\d+\\))", ",\r\n\t"); + + sb.AppendLine(sql.Substring(0, startIndex)); + sb.AppendLine("\t" + columns.Trim()); + sb.AppendLine(sql.Substring(startIndex + length) + ";"); + + string indexes = GetIndexCreateSql(cn, tableName); + if (!string.IsNullOrEmpty(indexes)) { sb.AppendLine(); + sb.AppendLine("-- INDEXES --"); + sb.AppendLine(indexes); } - sb.AppendLine(");"); + + string triggers = GetTriggerCreateSql(cn, tableName); + if (!string.IsNullOrEmpty(triggers)) { + sb.AppendLine(); + sb.AppendLine("-- TRIGGERS --"); + sb.AppendLine(triggers); + } + + if (!string.IsNullOrEmpty(selectStatement)) { + string data = GetTableDataSql(cn, tableName, selectStatement); + if (!string.IsNullOrEmpty(data)) { + sb.AppendLine(); + sb.AppendLine("-- DATA --"); + sb.Append(data); + } + } + sb.AppendLine("-- END TABLE " + tableName + " --"); return sb.ToString(); } } } + + /// + /// Retrieves the SQL statements used to create all indexes for the specified table in a SQLite database. + /// + /// This method queries the SQLite system table sqlite_master to retrieve the SQL + /// definitions of indexes associated with the specified table. The connection must be open before executing the + /// query; if it is not, the method will attempt to open it. + /// The open database connection to use for the query. The connection must be to a SQLite database. + /// The name of the table whose index creation SQL statements are to be retrieved. Cannot be null or empty. + /// A string containing the SQL statements for creating all indexes on the specified table, separated by + /// newlines. Returns an empty string if no indexes are found. + /// Thrown if is null or if is null or empty. + public string GetIndexCreateSql(IDbConnection cn, string tableName) { + if (cn == null) { + throw new ArgumentNullException("cn"); + } + if (string.IsNullOrEmpty(tableName)) { + throw new ArgumentNullException("tableName"); + } + if (cn.State != ConnectionState.Open) { + cn.Open(); + } + using (IDbCommand cmd = cn.CreateCommand()) { + cmd.CommandText = $"SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name='{tableName}' AND sql NOT NULL;"; + StringBuilder sb = new StringBuilder(); + using (IDataReader reader = cmd.ExecuteReader()) { + while (reader.Read()) { + sb.AppendLine(reader.GetString(0) + ";"); + } + } + return sb.ToString(); + } + } + + /// + /// Retrieves the SQL definition for all triggers associated with the specified table. + /// + /// This method queries the SQLite system table `sqlite_master` to retrieve the SQL + /// definitions of triggers. Ensure that the provided connection is valid and points to a SQLite + /// database. + /// An open database connection used to query the trigger definitions. The connection must be to a SQLite + /// database. + /// The name of the table whose triggers are to be retrieved. Cannot be null or empty. + /// A string containing the SQL definitions of all triggers for the specified table, separated by semicolons. + /// Returns an empty string if no triggers are found. + /// Thrown if is null or if is null or empty. + public string GetTriggerCreateSql(IDbConnection cn, string tableName) { + if (cn == null) { + throw new ArgumentNullException("cn"); + } + if (string.IsNullOrEmpty(tableName)) { + throw new ArgumentNullException("tableName"); + } + if (cn.State != ConnectionState.Open) { + cn.Open(); + } + using (IDbCommand cmd = cn.CreateCommand()) { + cmd.CommandText = $"SELECT sql FROM sqlite_master WHERE type='trigger' AND tbl_name='{tableName}' AND sql NOT NULL;"; + StringBuilder sb = new StringBuilder(); + using (IDataReader reader = cmd.ExecuteReader()) { + while (reader.Read()) { + sb.AppendLine(reader.GetString(0) + ";"); + } + } + return sb.ToString(); + } + } + + /// + /// Generates an SQL script to insert data from a specified table into another database. + /// + /// This method generates an SQL script that can be used to insert data into a table. The + /// script is constructed based on the data retrieved by the provided SELECT statement. The method ensures that + /// string and date values are properly escaped, and null values are represented as NULL in the + /// script. + /// An open used to execute the query. The connection must be open before calling + /// this method. + /// The name of the table whose data will be used in the generated SQL script. Cannot be null or empty. + /// A SQL SELECT statement that retrieves the data to be included in the script. The placeholder $table + /// in the statement will be replaced with the value of . + /// A string containing the generated SQL script. The script includes INSERT statements for the data retrieved + /// by the . + /// Thrown if is null or is null or empty. + public string GetTableDataSql(IDbConnection cn, string tableName, string selectStatement) { + if (cn == null) { + throw new ArgumentNullException("cn"); + } + + if (string.IsNullOrEmpty(tableName)) { + throw new ArgumentNullException("tableName"); + } + if (cn.State != ConnectionState.Open) { + cn.Open(); + } + + selectStatement = selectStatement.Replace("$table", tableName); + using (IDbCommand cmd = cn.CreateCommand()) { + cmd.CommandText = selectStatement; + using (IDataReader reader = cmd.ExecuteReader()) { + StringBuilder sb = new StringBuilder(); + int rowCount = 0; + while (reader.Read()) { + List values = new List(); + for (int i = 0; i < reader.FieldCount; i++) { + object value = DBNull.Value; + try { + value = reader.GetValue(i); + } catch { } + if (value == DBNull.Value) { + values.Add("NULL"); + } else if (value is string || value is DateTime) { + values.Add("'" + value.ToString().Replace("'", "''") + "'"); + } else if (value is bool) { + values.Add((bool)value ? "1" : "0"); + } else { + values.Add(value.ToString()); + } + } + + if (rowCount % 100 == 0) { + if (rowCount != 0) + sb.AppendLine(";"); + sb.Append($"INSERT INTO {tableName} ({string.Join(", ", GetColumnNames(reader))}) VALUES"); + sb.AppendLine(); + sb.Append($" ({string.Join(", ", values)})"); + } else { + sb.AppendLine(","); + sb.Append($" ({string.Join(", ", values)})"); + } + rowCount++; + } + if (rowCount > 0) + sb.AppendLine(";"); + + return sb.ToString(); + } + } + } + + /// + /// Retrieves the names of all columns from the specified . + /// + /// The instance from which to retrieve column names. Must not be . + /// An enumerable collection of strings representing the names of all columns in the data reader. + private IEnumerable GetColumnNames(IDataReader reader) { + for (int i = 0; i < reader.FieldCount; i++) { + yield return reader.GetName(i); + } + } } }