using Dapper; using System; using System.Collections.Generic; using System.Data.SQLite; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace DbMigrate { public class Migrations { public SqlDatabase BaseDatabase { get; set; } public SqlDatabase CompareDatabase { get; set; } public string DatabaseName { get; private set; } public string MigrationScriptPath { get; set; } = ".\\sql"; public bool RemoveUnusedTablesFromBaseDb { get; set; } = false; public bool RemoveUnusedColumnsFromBaseDb { get; set; } = false; public bool CopyDataFromNewDb { get; set; } = false; public string DeltaSql { get; private set; } public List MigrationSql { get; private set; } = new List(); public Migrations(SqlDatabase baseDb, SqlDatabase compareDb = null) { BaseDatabase = baseDb; CompareDatabase = compareDb; setDatbaseName(); } public Migrations(string baseDbConnectionString, string compareDbConnectionString = null) { BaseDatabase = new SqlDatabase(baseDbConnectionString); if (compareDbConnectionString != null) { CompareDatabase = new SqlDatabase(compareDbConnectionString); } setDatbaseName(); } private void setDatbaseName() { string fullPath = Regex.Match(BaseDatabase.ConnectionString, "Data Source=(.*?);", RegexOptions.IgnoreCase)?.Groups[1]?.Value; if (fullPath != null) { DatabaseName = Path.GetFileNameWithoutExtension(fullPath); } } public async Task PerformMigration(bool performDelta = true) { if (!Directory.Exists(MigrationScriptPath)) { Directory.CreateDirectory(MigrationScriptPath); } if (BaseDatabase == null || string.IsNullOrEmpty(BaseDatabase.ConnectionString) || string.IsNullOrEmpty(DatabaseName)) { throw new InvalidOperationException("Base Database is not set"); } using (SQLiteConnection cn = new SQLiteConnection(BaseDatabase.ConnectionString)) { if (performDelta) { // Generate the delta SQL string deltaSql = GetDeltaSql(); // Update the MigrationSql property MigrationSql.Add(new Migration(DatabaseName.ToLower() + "_delta_" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".sql", deltaSql)); } string[] completedMigrations = File.ReadAllLines(Path.Combine(MigrationScriptPath, "completed.txt")); var migrationFiles = Directory.GetFiles(MigrationScriptPath, DatabaseName + "_*.sql") .Where(f => !completedMigrations.Contains(Path.GetFileName(f))) .OrderBy(f => f); // Read each migration file and add it to the MigrationSql list foreach (var file in migrationFiles) { string sql = File.ReadAllText(file); // Add this migration to the list MigrationSql.Add(new Migration(Path.GetFileName(file), sql)); } foreach (var migration in MigrationSql) { try { // Execute the migration await cn.ExecuteAsync(migration.Sql); migration.Executed = true; // Mark this migration as completed by adding it to the completed.txt file File.AppendAllLines(Path.Combine(MigrationScriptPath, "completed.txt"), new[] { migration.MigrationName }); // TODO : Report progress } catch (Exception ex) { migration.Errors.Add(ex); } } } } public string GetDeltaSql() { if (CompareDatabase == null) { throw new InvalidOperationException("Compare Database is not set"); } if (BaseDatabase == null) { throw new InvalidOperationException("Base Database is not set"); } var sb = new StringBuilder(); foreach (var table in CompareDatabase.Tables) { if (!BaseDatabase.ContainsTable(table.TableName)) { // Table does not exist; Create it sb.AppendLine("\r\n-- Create table " + table.TableName); sb.Append(table.CreateTableSql); if (CopyDataFromNewDb) { // Copy data sb.AppendLine("\r\n-- Copy data into new table " + table.TableName); // TODO : Copy data from existing table in the new schema } continue; } // The table exists, now we need to compare everything to verify no updates are required! foreach (var index in table.Indexes) { if (!BaseDatabase[table.TableName].HasIndex(index.Key)) { sb.AppendLine("\r\n-- Create index " + index.Key); sb.AppendLine(index.Value); } else { // Index exists, check if it's the same if (BaseDatabase[table.TableName].Indexes[index.Key] != index.Value) { sb.AppendLine("\r\n-- Drop and recreate index " + index.Key); sb.AppendLine("DROP INDEX IF EXISTS " + index.Key + ";"); sb.AppendLine(index.Value); } } } foreach (var trigger in table.Triggers) { if (!BaseDatabase[table.TableName].HasTrigger(trigger.Key)) { sb.AppendLine("\r\n-- Create trigger " + trigger.Key); sb.AppendLine(trigger.Value); } else { // Trigger exists, check if it's the same if (BaseDatabase[table.TableName].Triggers[trigger.Key] != trigger.Value) { sb.AppendLine("\r\n-- Drop and recreate trigger " + trigger.Key); sb.AppendLine("DROP TRIGGER IF EXISTS " + trigger.Key + ";"); sb.AppendLine(trigger.Value); } } } bool alterTableRequired = false; foreach (var column in table.GetColumnNames()) { if (!BaseDatabase[table.TableName].HasColumn(column)) { // The database column does not exist alterTableRequired = true; break; } if (BaseDatabase[table.TableName].Columns[column] != table.Columns[column]) { // The database column exists but is different alterTableRequired = true; break; } } // TODO : This deletes unused columns - we should probably make this optional if (alterTableRequired) { sb.AppendLine("\r\n-- Table " + table.TableName + " requires alteration - Create temp table and move data"); string[] commonColumns = getCommonColumns(table.TableName); string columnList = string.Join(",", commonColumns); string sql = CompareDatabase[table.TableName].CreateTableSql; string newTableName = table.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 " + table.TableName + ";"); sb.AppendLine("\r\n-- Drop existing table"); sb.AppendLine("DROP TABLE " + table.TableName + ";"); sb.AppendLine("\r\n-- Rename the new table to replace the old table"); sb.AppendLine("ALTER TABLE " + newTableName + " RENAME TO " + table.TableName + ";"); } } DeltaSql = "BEGIN TRANSACTION;\r\n" + sb.ToString() + "\r\nCOMMIT;"; return DeltaSql; } private string[] getCommonColumns(string table) { string[] baseColumns = BaseDatabase[table].GetColumnNames(); string[] newColumns = CompareDatabase[table].GetColumnNames(); return baseColumns.Where(f => newColumns.Contains(f)).ToArray(); } } }