using System;
using System.Data;
using System.IO;
using SubSonic.Sugar;
namespace SubSonic.Migrations
{
///
/// The Migrator class is responsible for running through a collection of migrationFiles
/// and applying them towards the specified provider.
///
/// This class is very similiar to the Rails Migrator class. They did something very
/// interesting with a static method acting as a mini-factory for itself, notice how
/// the static Migrate() method in turn creates a Migrator instance and calls methods
/// on that. Thought that was very neat, so I borrowed it.
///
public class Migrator
{
private const string SCHEMA_INFO = "SubSonicSchemaInfo";
private readonly int currentVersion;
private readonly Migration.MigrationDirection direction;
private readonly string migrationDirectory;
private readonly string providerName;
private readonly int? toVersion;
///
/// Constructor
///
/// Direction to migrate.
/// Name of the provider.
/// Directory to find the migrations.
/// Version to migrate up to.
public Migrator(Migration.MigrationDirection direction, string providerName, string migrationDirectory, int? toVersion)
{
this.direction = direction;
this.providerName = providerName;
this.migrationDirectory = migrationDirectory;
this.toVersion = toVersion;
currentVersion = GetCurrentVersion(providerName);
}
///
/// Begins the migration.
///
/// Name of the provider.
/// Directory to find the migrations.
/// Version to migrate to.
public static void Migrate(string providerName, string migrationDirectory, int? toVersion)
{
int currentVersion = GetCurrentVersion(providerName);
if (!toVersion.HasValue || currentVersion < toVersion.Value)
Up(providerName, migrationDirectory, toVersion);
else if (currentVersion > toVersion.Value)
{
if (toVersion.Value == -1)
{
if (currentVersion - 1 > -1)
{
Down(providerName, migrationDirectory, currentVersion - 1);
}
}
else
{
Down(providerName, migrationDirectory, toVersion);
}
}
return;
}
///
/// Migreate Up
///
/// Name of the provider.
/// Directory to find the migrations.
/// Version to migrate up to.
public static void Up(string providerName, string migrationDirectory, int? toVersion)
{
Migrator migrator = new Migrator(Migration.MigrationDirection.Up, providerName, migrationDirectory, toVersion);
migrator.Migrate();
}
///
/// Migreate Down
///
/// Name of the provider.
/// Directory to find the migrations.
/// Version to migrate down to.
public static void Down(string providerName, string migrationDirectory, int? toVersion)
{
Migrator migrator = new Migrator(Migration.MigrationDirection.Down, providerName, migrationDirectory, toVersion);
migrator.Migrate();
}
///
/// Start migration.
///
public void Migrate()
{
//grab all migration files from migrationDirectory, list will be returned sorted according to direction.
string[] migrations = GetMigrations();
if(migrations.Length == 0)
Console.WriteLine("There are no migrations files found. You sure this is the right directory?");
else
Console.WriteLine("Found " + migrations.Length + " migration files");
//if no version was passed in use the highest version we have
int targetVersion = toVersion ?? GetMigrationVersion(migrations[migrations.Length - 1]);
if(targetVersion == currentVersion)
return;
//determine the range of migrations to run so we're not looping through every single file
int startIndex = Array.FindIndex(migrations, delegate(string m) { return GetMigrationVersion(m) == currentVersion; });
int finishIndex = Array.FindIndex(migrations, delegate(string m) { return GetMigrationVersion(m) == targetVersion; });
// skip the current migration if going up
if(startIndex == -1)
startIndex = 0;
else if(direction == Migration.MigrationDirection.Up)
startIndex++;
// don't execute the very last migration when going down
if(finishIndex == -1)
finishIndex = migrations.Length - 1;
else if(direction == Migration.MigrationDirection.Down)
finishIndex--;
//let the migrations begin!
for(int i = startIndex; i <= finishIndex; i++)
{
string migrationFile = migrations[i];
string migrationName = Path.GetFileNameWithoutExtension(migrationFile);
Console.WriteLine("Migrating to {0} ({1})", migrationName, GetMigrationVersion(migrationFile));
try
{
ExecuteMigrationCode(migrationFile);
}
catch(Exception x)
{
string rawError = x.Message;
if(x.InnerException != null)
rawError = x.InnerException.Message;
string errorMesage = string.Format("There was an error running migration ({0}): \r\n {1}", migrationName, rawError);
errorMesage += "\r\nStack Trace: \r\n" + x.StackTrace;
Console.WriteLine(errorMesage);
Console.ReadLine();
return;
}
UpdateSchemaVersion();
DataService.ClearSchemaCache(providerName);
}
}
private void ExecuteMigrationCode(string migrationFile)
{
//pull the whole code bits in - we're going to compile this to code
//and execute it :):):):)
string migrationCode = Files.GetFileText(migrationFile);
//get the extension
string codeExtension = Path.GetExtension(migrationFile);
ICodeLanguage codeLang = new CSharpCodeLanguage();
if(codeExtension.EndsWith(FileExtension.VB, StringComparison.InvariantCultureIgnoreCase))
codeLang = new VBCodeLanguage();
object[] parameters = new object[2];
parameters[0] = providerName;
parameters[1] = direction;
CodeRunner.RunAndExecute(codeLang, migrationCode, "Migrate", parameters);
}
private string[] GetMigrations()
{
if(!Directory.Exists(migrationDirectory))
throw new InvalidOperationException("Can't find the migration directory at " + migrationDirectory);
string[] migrations = Directory.GetFiles(migrationDirectory);
if(migrations.Length == 0)
throw new Exception("There are no migration files in this directory: " + migrationDirectory);
Array.Sort(migrations);
if(direction == Migration.MigrationDirection.Down)
Array.Reverse(migrations);
return migrations;
}
///
/// Gets the version of the migration file.
///
/// Migration file name.
///
private static int GetMigrationVersion(string migration)
{
int fileVersion;
string fileName = Path.GetFileName(migration);
string versionStub = fileName.Substring(0, 3);
int.TryParse(versionStub, out fileVersion);
return fileVersion;
}
#region Versioning Bits
///
/// Gets the current schema version for the named provider.
///
/// Name of the provider.
/// Current version of the schema as stored in the schema info table.
public static int GetCurrentVersion(string providerName)
{
int currentVersion = 0;
DataProvider p = DataService.Providers[providerName];
TableSchema.Table schemaTable = p.GetTableSchema(SCHEMA_INFO, TableType.Table);
if(schemaTable != null && schemaTable.GetColumn("version") != null)
currentVersion = new Select(p, "version").From(SCHEMA_INFO).ExecuteScalar();
else
{
//create schema table if it doesn't exist
if(schemaTable == null)
CreateSchemaInfo(providerName);
//delete all rows & add the version row
new Delete(providerName).From(SCHEMA_INFO).Execute();
new Insert(SCHEMA_INFO, providerName).Values(0).Execute();
}
return currentVersion;
}
private static void CreateSchemaInfo(string providerName)
{
TableSchema.Table tbl = new TableSchema.Table(providerName, SCHEMA_INFO);
tbl.AddColumn("version", DbType.Int32, 0, false, "0");
ISqlGenerator generator = DataService.GetGenerator(providerName);
string sql = generator.BuildCreateTableStatement(tbl);
DataService.ExecuteQuery(new QueryCommand(sql, providerName));
DataService.Providers[providerName].ReloadSchema();
}
private void UpdateSchemaVersion()
{
if(direction == Migration.MigrationDirection.Up)
IncrementVersion();
else
DecrementVersion();
}
///
/// Increments the version.
///
private void IncrementVersion()
{
new Update(SCHEMA_INFO, providerName).SetExpression("version").EqualTo("version+1").Execute();
}
///
/// Decrements the version.
///
private void DecrementVersion()
{
new Update(SCHEMA_INFO, providerName).SetExpression("version").EqualTo("version-1").Execute();
}
#endregion
}
}