196 lines
9.0 KiB
C#
196 lines
9.0 KiB
C#
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<Migration> MigrationSql { get; private set; } = new List<Migration>();
|
|
|
|
|
|
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();
|
|
}
|
|
|
|
}
|
|
}
|