Add project files.
This commit is contained in:
4
Model/Column.cs
Normal file
4
Model/Column.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
namespace DbTools.Model {
|
||||
internal class Column {
|
||||
}
|
||||
}
|
||||
59
Model/ColumnCollection.cs
Normal file
59
Model/ColumnCollection.cs
Normal 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
277
Model/Database.cs
Normal 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Model/SqliteTableDefinition.cs
Normal file
9
Model/SqliteTableDefinition.cs
Normal 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
97
Model/Table.cs
Normal 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
70
Model/TableCollection.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user