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")]