Add project files.

This commit is contained in:
2025-08-31 10:31:37 -05:00
parent 22897c0b91
commit 67e2959f3c
12 changed files with 906 additions and 0 deletions

56
DbTools.csproj Normal file
View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{B2FC6FFB-A182-4458-AA43-D0A0E6A0F118}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>DbTools</RootNamespace>
<AssemblyName>DbTools</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Delta.cs" />
<Compile Include="Extensions\DatabaseExtensions.cs" />
<Compile Include="Extensions\TableExtensions.cs" />
<Compile Include="Model\Column.cs" />
<Compile Include="Model\ColumnCollection.cs" />
<Compile Include="Model\Database.cs" />
<Compile Include="Model\SqliteTableDefinition.cs" />
<Compile Include="Model\Table.cs" />
<Compile Include="Model\TableCollection.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

25
DbTools.sln Normal file
View File

@@ -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

106
Delta.cs Normal file
View File

@@ -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();
}
}
}

View File

@@ -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<string> indexes = new List<string>();
foreach (var table in db.Tables.GetTables()) {
indexes.AddRange(table.Indexes.Keys);
}
return indexes.ToArray();
}
}
}

View File

@@ -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 {
/// <summary>
/// Retrieves the column names that are common between the current table and the specified table.
/// </summary>
/// <param name="table1">The source table from which to retrieve column names.</param>
/// <param name="table2">The table to compare against for common column names.</param>
/// <returns>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.</returns>
public static string[] GetCommonColumns(this Table table1, Table table2) {
return table1.GetColumnNames().Where(f => table2.HasColumn(f)).ToArray();
}
/// <summary>
/// Parses the provided SQL script to extract and populate table metadata, including the table name, columns,
/// indexes, and triggers.
/// </summary>
/// <remarks>This method processes the SQL script line by line to identify and extract the table
/// definition, column definitions, indexes, and triggers. - If <paramref name="tableName"/> is specified, only
/// metadata for the specified table is extracted. - If <paramref name="tableName"/> 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.</remarks>
/// <param name="table">The <see cref="Table"/> instance to populate with metadata extracted from the SQL script.</param>
/// <param name="sql">The SQL script to parse. The script should define a table and optionally include indexes and triggers.</param>
/// <param name="tableName">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.</param>
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();
}
}
}

4
Model/Column.cs Normal file
View File

@@ -0,0 +1,4 @@
namespace DbTools.Model {
internal class Column {
}
}

59
Model/ColumnCollection.cs Normal file
View File

@@ -0,0 +1,59 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DbTools.Model {
internal class ColumnCollection {
internal Dictionary<string, string> Items { get; private set; } = new Dictionary<string, string>();
public ColumnCollection() {
Items = new Dictionary<string, string>();
}
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();
}
}
}

277
Model/Database.cs Normal file
View File

@@ -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();
}
}
/// <summary>
/// Loads the specified SQL script and initializes an in-memory SQLite database using the script.
/// </summary>
/// <remarks>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 <see cref="ConnectionString"/> property.</remarks>
/// <param name="sql">The SQL script to be executed for creating and populating the database.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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();
}
/// <summary>
/// Determines whether a table with the specified name exists in the collection.
/// </summary>
/// <param name="tableName">The name of the table to search for. The comparison is case-insensitive.</param>
/// <returns><see langword="true"/> if a table with the specified name exists; otherwise, <see langword="false"/>.</returns>
public bool ContainsTable(string tableName) {
return Tables.Contains(tableName);
}
/// <summary>
/// Gets the <see cref="SqlTable"/> with the specified table name, or <c>null</c> if no matching table is found.
/// </summary>
/// <param name="tableName">The name of the table to retrieve. The comparison is case-insensitive.</param>
/// <returns></returns>
public Table this[string tableName] {
get {
return Tables[tableName];
}
}
/// <summary>
/// Parses the provided SQL script to extract table definitions, along with their associated indexes and
/// triggers.
/// </summary>
/// <remarks>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.</remarks>
/// <param name="sql">The SQL script containing table, index, and trigger definitions. The script must be in a valid SQL format.</param>
/// <returns>An enumerable collection of <see cref="Table"/> 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.</returns>
public IEnumerable<Table> ParseTablesFromSql(string sql) {
Table table = null;
StringBuilder sb = new StringBuilder();
Dictionary<string, List<string>> indexes = new Dictionary<string, List<string>>();
Dictionary<string, List<string>> triggers = new Dictionary<string, List<string>>();
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<string> { 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<string> { 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);
}
/// <summary>
/// Builds and returns an SQL statement based on the currently loaded database connection.
/// </summary>
/// <param name="includeIfNotExist">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. <see langword="true"/> to include the
/// conditional check; otherwise, <see langword="false"/>.</param>
/// <returns>A string containing the generated SQL statement.</returns>
public string BuildSql(bool includeIfNotExist = false) {
importFromSqlite();
return ToSql(includeIfNotExist);
}
/// <summary>
/// Generates a SQL script for the current database schema.
/// </summary>
/// <remarks>The generated script includes metadata such as the generation timestamp and is
/// appended to the existing <see cref="SqlScript"/> value. The method processes all tables in the schema and
/// generates their corresponding SQL definitions.</remarks>
/// <param name="includeIfNotExist">A value indicating whether to include conditional checks (e.g., "IF NOT EXISTS") in the generated SQL
/// script.</param>
/// <returns>A string containing the generated SQL script, including all tables in the current schema.</returns>
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<string, List<string>> indexes = new Dictionary<string, List<string>>();
Dictionary<string, List<string>> triggers = new Dictionary<string, List<string>>();
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<string> { 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<string> { triggerName + ";" + triggerSql };
}
}
}
}
}
appendIndexes(indexes);
appendTriggers(triggers);
return true;
}
private void appendIndexes(Dictionary<string, List<string>> 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<string, List<string>> 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];
}
}
}
}
}
}
}

View File

@@ -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; }
}
}

97
Model/Table.cs Normal file
View File

@@ -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<string, string> Indexes { get; set; }
public Dictionary<string, string> 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<string, string>();
Triggers = new Dictionary<string, string>();
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();
}
}
}

70
Model/TableCollection.cs Normal file
View File

@@ -0,0 +1,70 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DbTools.Model {
internal class TableCollection {
internal Dictionary<string, Table> Items { get; private set; } = new Dictionary<string, Table>();
public TableCollection() {
Items = new Dictionary<string, Table>();
}
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();
}
}
}

View File

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