From 67e2959f3c5917e87a6b5a27c9a6dd1bdf4bca8b Mon Sep 17 00:00:00 2001 From: Russ Kollmansberger Date: Sun, 31 Aug 2025 10:31:37 -0500 Subject: [PATCH] Add project files. --- DbTools.csproj | 56 +++++++ DbTools.sln | 25 +++ Delta.cs | 106 ++++++++++++ Extensions/DatabaseExtensions.cs | 29 ++++ Extensions/TableExtensions.cs | 141 ++++++++++++++++ Model/Column.cs | 4 + Model/ColumnCollection.cs | 59 +++++++ Model/Database.cs | 277 +++++++++++++++++++++++++++++++ Model/SqliteTableDefinition.cs | 9 + Model/Table.cs | 97 +++++++++++ Model/TableCollection.cs | 70 ++++++++ Properties/AssemblyInfo.cs | 33 ++++ 12 files changed, 906 insertions(+) create mode 100644 DbTools.csproj create mode 100644 DbTools.sln create mode 100644 Delta.cs create mode 100644 Extensions/DatabaseExtensions.cs create mode 100644 Extensions/TableExtensions.cs create mode 100644 Model/Column.cs create mode 100644 Model/ColumnCollection.cs create mode 100644 Model/Database.cs create mode 100644 Model/SqliteTableDefinition.cs create mode 100644 Model/Table.cs create mode 100644 Model/TableCollection.cs create mode 100644 Properties/AssemblyInfo.cs diff --git a/DbTools.csproj b/DbTools.csproj new file mode 100644 index 0000000..b0e4f0c --- /dev/null +++ b/DbTools.csproj @@ -0,0 +1,56 @@ + + + + + Debug + AnyCPU + {B2FC6FFB-A182-4458-AA43-D0A0E6A0F118} + Library + Properties + DbTools + DbTools + v4.7.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DbTools.sln b/DbTools.sln new file mode 100644 index 0000000..d446fe3 --- /dev/null +++ b/DbTools.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36408.4 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbTools", "DbTools.csproj", "{B2FC6FFB-A182-4458-AA43-D0A0E6A0F118}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B2FC6FFB-A182-4458-AA43-D0A0E6A0F118}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2FC6FFB-A182-4458-AA43-D0A0E6A0F118}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2FC6FFB-A182-4458-AA43-D0A0E6A0F118}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2FC6FFB-A182-4458-AA43-D0A0E6A0F118}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9067B839-A068-4F44-B8AA-78ACB7DC3018} + EndGlobalSection +EndGlobal diff --git a/Delta.cs b/Delta.cs new file mode 100644 index 0000000..e7fdb4b --- /dev/null +++ b/Delta.cs @@ -0,0 +1,106 @@ +using DbTools.Model; +using System.Data; +using System.Linq; +using System.Text; + +namespace DbTools { + public class Delta { + + + public string BuildDelta(IDbConnection currentDb, IDbConnection newDb, bool removeUnusedTables = false, bool removeUnusedColumns = false, bool removeUnusedTriggers = true, bool removeUnusedIndexes = true) { + Database db1 = new Database(currentDb); + Database db2 = new Database(newDb); + + return BuildDelta(db1, db2, removeUnusedTables, removeUnusedColumns, removeUnusedTriggers, removeUnusedIndexes); + } + + public string BuildDelta(string currentDbSql, string newDbSql, bool removeUnusedTables = false, bool removeUnusedColumns = false, bool removeUnusedTriggers = true, bool removeUnusedIndexes = true) { + Database db1 = new Database(currentDbSql); + Database db2 = new Database(newDbSql); + return BuildDelta(db1, db2, removeUnusedTables, removeUnusedColumns, removeUnusedTriggers, removeUnusedIndexes); + } + + public string BuildDelta(IDbConnection currentDb, string newDbSql, bool removeUnusedTables = false, bool removeUnusedColumns = false, bool removeUnusedTriggers = true, bool removeUnusedIndexes = true) { + Database db1 = new Database(currentDb); + Database db2 = new Database(newDbSql); + return BuildDelta(db1, db2, removeUnusedTables, removeUnusedColumns, removeUnusedTriggers, removeUnusedIndexes); + } + + public string BuildDelta(string currentDbSql, IDbConnection newDb, bool removeUnusedTables = false, bool removeUnusedColumns = false, bool removeUnusedTriggers = true, bool removeUnusedIndexes = true) { + Database db1 = new Database(currentDbSql); + Database db2 = new Database(newDb); + return BuildDelta(db1, db2, removeUnusedTables, removeUnusedColumns, removeUnusedTriggers, removeUnusedIndexes); + } + + private string BuildDelta(Database db1, Database db2, bool removeUnusedTables, bool removeUnusedColumns, bool removeUnusedTriggers, bool removeUnusedIndexes) { + StringBuilder sb = new StringBuilder(); + + // Remove tables that are not in db2 if requested + if (removeUnusedTables) { + var unusedTables = db1.Tables.GetTables().Where(t1 => !db2.ContainsTable(t1.TableName)).ToArray(); + if (unusedTables.Length > 0) { + sb.AppendLine("-- DROP UNUSED TABLES --"); + foreach (var table in unusedTables) { + sb.AppendLine(table.GenerateDropTable()); + } + sb.AppendLine(); + } + } + + foreach (var table2 in db2.Tables.GetTables()) { + var table1 = db1.Tables[table2.TableName]; + if (table1 == null) { + sb.AppendLine(table2.FullSql()); + } else { + // Remove unused triggers if requested + if (removeUnusedTriggers) { + var unusedTriggers = table1.Triggers.Keys.Where(t1 => !table2.Triggers.ContainsKey(t1)).ToArray(); + if (unusedTriggers.Length > 0) { + sb.AppendLine("-- DROP UNUSED TRIGGERS " + table2.TableName + " --"); + foreach (var trigger in unusedTriggers) { + sb.AppendLine($"DROP TRIGGER IF EXISTS {trigger};"); + } + sb.AppendLine(); + } + } + + // Remove unused indexes if requested + if (removeUnusedIndexes) { + var unusedIndexes = table1.Indexes.Keys.Where(i1 => !table2.Indexes.ContainsKey(i1)).ToArray(); + if (unusedIndexes.Length > 0) { + sb.AppendLine("-- DROP UNUSED INDEXES IN TABLE " + table2.TableName + " --"); + foreach (var index in unusedIndexes) { + sb.AppendLine($"DROP INDEX IF EXISTS {index};"); + } + sb.AppendLine(); + } + } + + // Add missing columns + var commonColumns = table1.GetCommonColumns(table2); + var onlyInTable2 = table2.Columns.GetColumnNames().Where(c => !commonColumns.Contains(c)).ToArray(); + var onlyInTable1 = table1.Columns.GetColumnNames().Where(c => !commonColumns.Contains(c)).ToArray(); + if (removeUnusedColumns && onlyInTable1.Length > 0) { + // We have columns in table1 that are not in table2 and we want to remove them, + // so we need to recreate the table + sb.AppendLine("-- UNUSED COLUMNS EXIST IN TABLE " + table2.TableName + " --"); + string sql = table2.GenerateTableMigration(table1); + sb.AppendLine(sql); + } + if (onlyInTable2.Length > 0) { + // We have columns in table2 that are not in table1, so we can just add them + sb.AppendLine($"-- ALTER TABLE {table2.TableName} --"); + foreach (var colName in onlyInTable2) { + var col = table2.Columns[colName]; + sb.AppendLine($"ALTER TABLE {table2.TableName} ADD COLUMN {col};"); + } + sb.AppendLine($"-- END ALTER TABLE {table2.TableName} --"); + sb.AppendLine(); + } + } + } + return sb.ToString(); + } + + } +} diff --git a/Extensions/DatabaseExtensions.cs b/Extensions/DatabaseExtensions.cs new file mode 100644 index 0000000..bb2b2be --- /dev/null +++ b/Extensions/DatabaseExtensions.cs @@ -0,0 +1,29 @@ +using DbTools.Model; +using System.Collections.Generic; +using System.Text; + +namespace DbTools { + internal static class DatabaseExtensions { + + public static string ExportSql(this Database db) { + StringBuilder sb = new StringBuilder(); + foreach (var table in db.Tables.GetTables()) { + sb.AppendLine(table.FullSql()); + } + return sb.ToString(); + } + + public static void ImportFromSqlite(this Database db) { + + } + + public static string[] GetAllIndexes(this Database db) { + List indexes = new List(); + foreach (var table in db.Tables.GetTables()) { + indexes.AddRange(table.Indexes.Keys); + } + return indexes.ToArray(); + } + + } +} diff --git a/Extensions/TableExtensions.cs b/Extensions/TableExtensions.cs new file mode 100644 index 0000000..814b10b --- /dev/null +++ b/Extensions/TableExtensions.cs @@ -0,0 +1,141 @@ +using DbTools.Model; +using System; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace DbTools { + internal static class TableExtensions { + + /// + /// Retrieves the column names that are common between the current table and the specified table. + /// + /// The source table from which to retrieve column names. + /// The table to compare against for common column names. + /// An array of strings containing the names of columns that exist in both tables. The array will be empty if no + /// common columns are found. + public static string[] GetCommonColumns(this Table table1, Table table2) { + return table1.GetColumnNames().Where(f => table2.HasColumn(f)).ToArray(); + } + + /// + /// Parses the provided SQL script to extract and populate table metadata, including the table name, columns, + /// indexes, and triggers. + /// + /// This method processes the SQL script line by line to identify and extract the table + /// definition, column definitions, indexes, and triggers. - If is specified, only + /// metadata for the specified table is extracted. - If is not specified, the table + /// name is inferred from the first `CREATE TABLE` statement in the script. Lines starting with `--` are + /// ignored as comments. The method stops processing once the metadata for the specified or inferred table is + /// fully extracted. + /// The instance to populate with metadata extracted from the SQL script. + /// The SQL script to parse. The script should define a table and optionally include indexes and triggers. + /// An optional name of the table to extract from the SQL script. If not provided, the table name will be + /// inferred from the SQL script. + public static void ParseSql(this Table table, string sql, string tableName = null) { + if (tableName != null) { + table.TableName = tableName; + } + + bool inTable = false; + bool inColumns = false; + + Match m = null; + foreach (string line in Regex.Split(sql, "\\r\\n")) { + if (string.IsNullOrEmpty(line) || line.StartsWith("--")) { + continue; + } + + m = Regex.Match(line, "^CREATE TABLE( IF NOT EXISTS)? (\\S+) "); + if (m.Success) { + if (string.IsNullOrEmpty(tableName) && string.IsNullOrEmpty(table.TableName)) { + // Set table name from the above regex match + table.TableName = m.Groups[2].Value.Trim(); + } + if (!inTable && (table.TableName == null || m.Groups[2].Value.Trim() == table.TableName)) { + table.CreateTableSql += line + Environment.NewLine; + inTable = true; + inColumns = true; + continue; + } else { + if (inTable) { + // We are done with this table + return; + } + continue; + } + } + + m = Regex.Match(line, "^CREATE INDEX( IF NOT EXISTS)? (\\S+) "); + if (m.Success && inTable) { + table.Indexes.Add(m.Groups[2].Value.Trim(), line.Trim()); + continue; + } + + m = Regex.Match(line, "^CREATE TRIGGER( IF NOT EXISTS)? (\\S+) "); + if (m.Success && inTable) { + table.Triggers.Add(m.Groups[2].Value.Trim(), line.Trim()); + continue; + } + + if (inColumns) { + if (line.Trim().StartsWith(")")) { + inColumns = false; + } else { + m = Regex.Match(line.Trim(), "^(\\S+).*"); + if (m.Success) { + string columnLine = line.Trim(); + if (columnLine.EndsWith(",")) { + columnLine = columnLine.Substring(0, columnLine.Length - 1); + } + table.Columns.Add(m.Groups[1].Value.Trim(), columnLine); + } + } + } + + table.CreateTableSql += line + Environment.NewLine; + } + } + + public static string GenerateDropTable(this Table table) { + StringBuilder sb = new StringBuilder(); + if (table.HasTriggers()) { + foreach (var trigger in table.Triggers.Keys) { + sb.AppendLine($"DROP TRIGGER IF EXISTS {trigger};"); + } + } + if (table.HasIndexes()) { + foreach (var index in table.Indexes.Keys) { + sb.AppendLine($"DROP INDEX IF EXISTS {index};"); + } + } + sb.AppendLine($"DROP TABLE IF EXISTS {table.TableName};"); + return sb.ToString(); + } + + public static string GenerateTableMigration(this Table newTable, Table oldTable) { + StringBuilder sb = new StringBuilder(); + string tableName = newTable.TableName; + + sb.AppendLine("\r\n-- Table " + tableName + " requires alteration - Create temp table and move data"); + string[] commonColumns = newTable.GetCommonColumns(oldTable); + string columnList = string.Join(",", commonColumns); + + string sql = newTable.CreateTableSql; + string newTableName = tableName + "_" + DateTime.Now.ToString("yyyyMMddHHmmss"); + sql = Regex.Replace(sql, "CREATE TABLE (\\w*)", "CREATE TABLE IF NOT EXISTS $1_" + newTableName.Substring(newTableName.IndexOf("_") + 1)); + sb.AppendLine(sql); + + sb.AppendLine("\r\n-- Copy data from existing table to new table"); + sb.AppendLine("INSERT INTO " + newTableName + " (" + columnList + ")"); + sb.AppendLine("\tSELECT " + columnList); + sb.AppendLine("\tFROM " + tableName + ";"); + sb.AppendLine("\r\n-- Drop existing table"); + sb.AppendLine("DROP TABLE " + tableName + ";"); + sb.AppendLine("\r\n-- Rename the new table to replace the old table"); + sb.AppendLine("ALTER TABLE " + newTableName + " RENAME TO " + tableName + ";"); + + return sb.ToString(); + } + } +} diff --git a/Model/Column.cs b/Model/Column.cs new file mode 100644 index 0000000..8a948b0 --- /dev/null +++ b/Model/Column.cs @@ -0,0 +1,4 @@ +namespace DbTools.Model { + internal class Column { + } +} diff --git a/Model/ColumnCollection.cs b/Model/ColumnCollection.cs new file mode 100644 index 0000000..ee0018f --- /dev/null +++ b/Model/ColumnCollection.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DbTools.Model { + internal class ColumnCollection { + internal Dictionary Items { get; private set; } = new Dictionary(); + + public ColumnCollection() { + Items = new Dictionary(); + } + + public string this[string columnName] { + get { + if (Items.ContainsKey(columnName)) { + return Items[columnName]; + } + return null; + } + set { + if (Items.ContainsKey(columnName)) { + Items[columnName] = value; + } else { + Items.Add(columnName, value); + } + } + } + + public void Add(string columnName, string columnDefinition) { + if (!Items.ContainsKey(columnName)) { + Items.Add(columnName, columnDefinition); + } + } + + public bool Contains(string columnName) { + return Items.ContainsKey(columnName); + } + + public string[] GetColumnNames() { + return Items.Keys.ToArray(); + } + + public int Count() { + return Items.Count; + } + + public void Clear() { + Items.Clear(); + } + + public override string ToString() { + StringBuilder sb = new StringBuilder(); + foreach (var kvp in Items) { + sb.AppendLine($"{kvp.Key}: {kvp.Value}"); + } + return sb.ToString(); + } + } +} diff --git a/Model/Database.cs b/Model/Database.cs new file mode 100644 index 0000000..41fedbc --- /dev/null +++ b/Model/Database.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace DbTools.Model { + internal class Database { + public string ConnectionString { get; set; } + public string SqlScript { get; set; } + public TableCollection Tables { get; set; } = new TableCollection(); + public IDbConnection DbConnection { get; private set; } + + public Database() { } + + public Database(string connectionString) { + ConnectionString = connectionString; + } + + public Database(IDbConnection dbConnection, bool importImmediately = false) { + DbConnection = dbConnection; + ConnectionString = dbConnection.ConnectionString; + + // Give the user the option to import later to avoid unnecessary work + if (importImmediately) { + importFromSqlite(); + } + } + + /// + /// Loads the specified SQL script and initializes an in-memory SQLite database using the script. + /// + /// This method creates a temporary SQLite database file, sets up a connection string, + /// and executes the provided SQL script to initialize the database. The connection string for the database is + /// stored in the property. + /// The SQL script to be executed for creating and populating the database. + /// A representing the asynchronous operation. + public void LoadSql(string sql) { + if (!sql.Contains("-- Generated with DbTools")) { + throw new ArgumentException("The provided SQL script does not appear to be generated by DbTools."); + } + + ParseTablesFromSql(sql); + SqlScript = ToSql(); + } + + /// + /// Determines whether a table with the specified name exists in the collection. + /// + /// The name of the table to search for. The comparison is case-insensitive. + /// if a table with the specified name exists; otherwise, . + public bool ContainsTable(string tableName) { + return Tables.Contains(tableName); + } + + /// + /// Gets the with the specified table name, or null if no matching table is found. + /// + /// The name of the table to retrieve. The comparison is case-insensitive. + /// + public Table this[string tableName] { + get { + return Tables[tableName]; + } + } + + /// + /// Parses the provided SQL script to extract table definitions, along with their associated indexes and + /// triggers. + /// + /// This method processes the SQL script line by line to identify and parse table, index, + /// and trigger definitions. It supports SQL scripts that include "CREATE TABLE", "CREATE INDEX", and "CREATE + /// TRIGGER" statements. The method yields each parsed table as it is processed, allowing for efficient + /// streaming of results. NOTE: This method requires the SQL script to be in the expected format, which can + /// be generated using this project. + /// The SQL script containing table, index, and trigger definitions. The script must be in a valid SQL format. + /// An enumerable collection of objects, each representing a table parsed from the SQL + /// script. The collection includes the table's structure, indexes, and triggers as defined in the script. + public IEnumerable ParseTablesFromSql(string sql) { + Table table = null; + StringBuilder sb = new StringBuilder(); + + Dictionary> indexes = new Dictionary>(); + Dictionary> triggers = new Dictionary>(); + + bool inTable = false; + foreach (string line in Regex.Split(sql, "\\r\\n")) { + if (string.IsNullOrEmpty(line) || line.StartsWith("--")) { + continue; + } + + string trimmedLine = Regex.Replace(line.Trim(), @"\s+", " "); + + if (trimmedLine.ToUpper().StartsWith("CREATE TABLE ")) { + // Start a new table + var match = Regex.Match(trimmedLine, "CREATE TABLE( IF NOT EXISTS)? (\\w*) .*\\("); + if (match.Success) { + string tableName = match.Groups[2].Value.Trim(); + table = new Table() { + TableName = tableName + }; + sb = new StringBuilder(); + sb.AppendLine(trimmedLine); + inTable = true; + } + continue; + } + + // We assume indexes are always single line + if (trimmedLine.ToUpper().StartsWith("CREATE INDEX ")) { + var matches = Regex.Match(trimmedLine, "CREATE INDEX( IF NOT EXISTS)? (\\w*) ON (\\w*).*\\);"); + if (matches.Success) { + string tableName = matches.Groups[3].Value.Trim(); + string indexName = matches.Groups[2].Value.Trim(); + + if (indexes.ContainsKey(tableName)) { + indexes[tableName].Add(indexName + ";" + trimmedLine); + } else { + indexes[tableName] = new List { indexName + ";" + trimmedLine }; + } + continue; + } + } + + // We assume triggers are always single line + if (trimmedLine.ToUpper().StartsWith("CREATE TRIGGER ")) { + var matches = Regex.Match(trimmedLine, "CREATE TRIGGER( IF NOT EXISTS)? (\\w*) .* ON (\\w*).*END;"); + if (matches.Success) { + string tableName = matches.Groups[3].Value.Trim(); + string triggerName = matches.Groups[2].Value.Trim(); + + if (triggers.ContainsKey(tableName)) { + triggers[tableName].Add(triggerName + ";" + trimmedLine); + } else { + triggers[tableName] = new List { triggerName + ";" + trimmedLine }; + } + } + continue; + } + + // Inside a table definition, accumulate lines + if (!inTable) { + if (trimmedLine == ");") { + // End of table definition + sb.AppendLine(trimmedLine); + table.ParseSql(sb.ToString()); + Tables.Add(table); + inTable = false; + yield return table; + } else { + sb.AppendLine("\t" + trimmedLine); + } + } + } + + // Append indexes and triggers to their respective tables + appendIndexes(indexes); + appendTriggers(triggers); + } + + + /// + /// Builds and returns an SQL statement based on the currently loaded database connection. + /// + /// A boolean value indicating whether the generated SQL statement should include a conditional check to ensure + /// the existence of the target object before performing the operation. to include the + /// conditional check; otherwise, . + /// A string containing the generated SQL statement. + public string BuildSql(bool includeIfNotExist = false) { + importFromSqlite(); + return ToSql(includeIfNotExist); + } + + /// + /// Generates a SQL script for the current database schema. + /// + /// The generated script includes metadata such as the generation timestamp and is + /// appended to the existing value. The method processes all tables in the schema and + /// generates their corresponding SQL definitions. + /// A value indicating whether to include conditional checks (e.g., "IF NOT EXISTS") in the generated SQL + /// script. + /// A string containing the generated SQL script, including all tables in the current schema. + public string ToSql(bool includeIfNotExist = false) { + StringBuilder sb = new StringBuilder(); + sb.AppendLine("--"); + sb.AppendLine("-- Generated with DbTools on " + DateTime.Now.ToString("f")); + sb.AppendLine("--"); + + foreach (var table in Tables.GetTables()) { + sb.AppendLine(table.FullSql()); + } + + SqlScript += sb.ToString(); + return SqlScript; + } + + private bool importFromSqlite() { + DbConnection.Open(); + Tables.Clear(); + + Dictionary> indexes = new Dictionary>(); + Dictionary> triggers = new Dictionary>(); + using (var cmd = DbConnection.CreateCommand()) { + cmd.CommandText = "select * from sqlite_master"; + using (var reader = cmd.ExecuteReader()) { + while (reader.Read()) { + if (reader["tbl_name"]?.ToString() == "sqlite_sequence") { continue; } + string recordType = reader["type"]?.ToString(); + + if (recordType == "table") { + Table table = new Table() { + TableName = reader["tbl_name"]?.ToString(), + CreateTableSql = reader["sql"]?.ToString(), + }; + Tables.Add(table); + } else if (recordType == "index") { + string tableName = reader["tbl_name"]?.ToString(); + string indexName = reader["name"]?.ToString(); + string indexSql = reader["sql"]?.ToString(); + + if (indexes.ContainsKey(tableName)) { + indexes[tableName].Add(indexName + ";" + indexSql); + } else { + indexes[tableName] = new List { indexName + ";" + indexSql }; + } + } else if (recordType == "trigger") { + string tableName = reader["tbl_name"]?.ToString(); + string triggerName = reader["name"]?.ToString(); + string triggerSql = reader["sql"]?.ToString(); + + if (triggers.ContainsKey(tableName)) { + triggers[tableName].Add(triggerName + ";" + triggerSql); + } else { + triggers[tableName] = new List { triggerName + ";" + triggerSql }; + } + } + } + + } + } + + appendIndexes(indexes); + appendTriggers(triggers); + + return true; + } + + private void appendIndexes(Dictionary> indexes) { + foreach (string index in indexes.Keys) { + Table table = Tables[index]; + if (table != null) { + foreach (string indexSql in indexes[index]) { + var parts = indexSql.Split(new char[] { ';' }, 2); + if (parts.Length == 2) { + table.Indexes[parts[0]] = parts[1]; + } + } + } + } + } + + private void appendTriggers(Dictionary> triggers) { + foreach (string trigger in triggers.Keys) { + Table table = Tables[trigger]; + if (table != null) { + foreach (string triggerSql in triggers[trigger]) { + var parts = triggerSql.Split(new char[] { ';' }, 2); + if (parts.Length == 2) { + table.Triggers[parts[0]] = parts[1]; + } + } + } + } + } + } +} diff --git a/Model/SqliteTableDefinition.cs b/Model/SqliteTableDefinition.cs new file mode 100644 index 0000000..cf35c42 --- /dev/null +++ b/Model/SqliteTableDefinition.cs @@ -0,0 +1,9 @@ +namespace DbTools.Model { + internal class SqliteTableDefinition { + public string @type { get; set; } + public string name { get; set; } + public string tbl_name { get; set; } + public int rootpage { get; set; } + public string sql { get; set; } + } +} diff --git a/Model/Table.cs b/Model/Table.cs new file mode 100644 index 0000000..4b8bcdc --- /dev/null +++ b/Model/Table.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DbTools.Model { + internal class Table { + public string TableName { get; set; } + public string CreateTableSql { get; set; } + public string OriginalSql { get; set; } + public ColumnCollection Columns { get; set; } + public Dictionary Indexes { get; set; } + public Dictionary Triggers { get; set; } + + + + public Table() { + initTable(); + } + + public Table(string sql) { + initTable(); + OriginalSql = sql; + this.ParseSql(sql); + } + + private void initTable() { + Columns = new ColumnCollection(); + Indexes = new Dictionary(); + Triggers = new Dictionary(); + CreateTableSql = ""; + } + + public string FullSql() { + StringBuilder sb = new StringBuilder(); + sb.AppendLine("-- BEGIN TABLE " + TableName + " --"); + sb.AppendLine(CreateTableSql); + + if (Indexes.Count > 0) { + sb.AppendLine("\r\n-- INDEXES --"); + foreach (string index in Indexes.Keys) { + sb.AppendLine(Indexes[index]); + } + } + + if (Triggers.Count > 0) { + sb.AppendLine("\r\n-- TRIGGERS --"); + foreach (string trigger in Triggers.Keys) { + sb.AppendLine(Triggers[trigger]); + } + } + + sb.AppendLine("-- END TABLE " + TableName + " --"); + sb.AppendLine(); + return sb.ToString(); + } + + public string[] GetColumnNames() { + return Columns.GetColumnNames(); + } + + public bool HasColumn(string columnName) { + return Columns.Contains(columnName); + } + + public string[] GetTriggerNames() { + return Triggers.Keys.ToArray(); + } + + public string[] GetTriggers() { + return Triggers.Values.ToArray(); + } + + public bool HasTrigger(string triggerName) { + return Triggers.ContainsKey(triggerName); + } + + public bool HasTriggers() { + return Triggers.Count > 0; + } + + public bool HasIndex(string indexName) { + return Indexes.ContainsKey(indexName); + } + + public bool HasIndexes() { + return Indexes.Count > 0; + } + + public string[] GetIndexNames() { + return Indexes.Keys.ToArray(); + } + + public string[] GetIndexes() { + return Indexes.Values.ToArray(); + } + } +} diff --git a/Model/TableCollection.cs b/Model/TableCollection.cs new file mode 100644 index 0000000..8a8fb1d --- /dev/null +++ b/Model/TableCollection.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DbTools.Model { + internal class TableCollection { + internal Dictionary Items { get; private set; } = new Dictionary(); + + public TableCollection() { + Items = new Dictionary(); + } + + public Table this[string tableName] { + get { + if (Items.ContainsKey(tableName)) { + return Items[tableName]; + } + return null; + } + set { + if (Items.ContainsKey(tableName)) { + Items[tableName] = value; + } else { + Items.Add(tableName, value); + } + } + } + + public void Add(string tableName, string tableDefinition) { + Table table = new Table(tableDefinition) { + TableName = tableName + }; + Add(table); + } + + public void Add(Table table) { + if (!Items.ContainsKey(table.TableName)) { + Items.Add(table.TableName, table); + } + } + + public bool Contains(string tableName) { + return Items.ContainsKey(tableName); + } + + public string[] GetTableNames() { + return Items.Keys.ToArray(); + } + + public Table[] GetTables() { + return Items.Values.ToArray(); + } + + public int Count() { + return Items.Count; + } + + public void Clear() { + Items.Clear(); + } + + public override string ToString() { + StringBuilder sb = new StringBuilder(); + foreach (var kvp in Items) { + sb.AppendLine($"{kvp.Key}: {kvp.Value}"); + } + return sb.ToString(); + } + } +} diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..3158626 --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("DbTools")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("DbTools")] +[assembly: AssemblyCopyright("Copyright © 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("b2fc6ffb-a182-4458-aa43-d0a0e6a0f118")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")]