/tags/v1.1/fadd/Data/Migration/SqlXmlMigrator.cs
C# | 239 lines | 159 code | 35 blank | 45 comment | 47 complexity | 7bcf1afa1429092c93990c717d11f52b MD5 | raw file
- using System;
- using System.Collections.Generic;
- using System.Data;
- using System.IO;
- using System.Reflection;
- using System.Xml;
-
- namespace Fadd.Data.Migration
- {
- /// <summary>
- /// As the name implies the migrator uses sql specified for a certain driver to update the database.
- /// The sql is loaded using manifest resource xml files named xxxxx.fdmig.xml (fadd data migration)
- /// </summary>
- /// <remarks>
- /// The fdmig file should look like the following
- /// <![CDATA[
- /// <?xml version="1.0" encoding="utf-8" ?>
- /// <migrations>
- /// <migration name="My migration" version="0002">
- /// <description>
- /// An optional description of the migration.
- /// </description>
- ///
- /// <sql driver="MySql 1.0">
- /// alter table myTable rename column myColumn to myColumnValues;
- /// </sql>
- ///
- /// <sql driver="SqlServer">
- /// alter table myTable rename column myColumn to myColumnValues;
- /// </sql>
- /// </migration>
- ///
- /// <migration name="My next migration" version="0003" description="A short description">
- /// <!-- default driver will be used if specified and db doesn't match any other driver -->
- /// <sql driver="default">
- /// INSERT INTO myTable (myId, myValue) VALUES (13, '23123');
- /// </sql>
- /// </migration>
- ///
- /// </migrations>
- /// ]]>
- /// </remarks>
- public class SqlXmlMigrator<MigrationModelType> : IMigrator where MigrationModelType : IMigrationModel, new()
- {
- private readonly Assembly[] _assemblies;
- private readonly SortedList<int, SqlMigration> _migrations = new SortedList<int, SqlMigration>();
- private int _highestVersion;
- private int _lowestVersion = int.MaxValue;
-
- /// <summary>Initializes the <see cref="SqlXmlMigrator{MigrationModelType}"/></summary>
- /// <param name="assemblies">Assemblies to look for fdmig.xml files in</param>
- public SqlXmlMigrator(params Assembly[] assemblies)
- {
- Check.Require(assemblies, "assemblies");
-
- _assemblies = assemblies;
- }
-
- public void Migrate(DataLayer dataLayer)
- {
- FetchMigrations();
- //if(_migrations.Count == 0)
- // return;
-
- // Test migration
- if (TestMigration(dataLayer))
- return;
-
- // If migration failed we'll try if the first migration will fix it
- if (!_migrations.ContainsKey(_lowestVersion))
- throw new MigrationException("Could not find migration table or suitable first migration");
-
- ApplyMigration(dataLayer, _migrations[_lowestVersion]);
- TestMigration(dataLayer);
- }
-
- private bool TestMigration(DataLayer dataLayer)
- {
- string tableName = dataLayer.MappingProviders.GetMapping(typeof(MigrationModelType).Name).TableName.ToLower();
-
- try
- {
- MigrationModelType lastMigration = dataLayer.Find<MigrationModelType>(new Statement("VersionNumber > ?", 0).OrderBy("VersionNumber DESC"))[0];
- if (lastMigration.VersionNumber >= _highestVersion)
- return true;
-
- for (int i = lastMigration.VersionNumber + 1; i <= _highestVersion; ++i)
- {
- if(!_migrations.ContainsKey(i))
- throw new MigrationException("Could not find migration version: " + i);
-
- ApplyMigration(dataLayer, _migrations[i]);
- }
-
- return true;
- }
- catch (Exception e)
- {
- // If the errror contains something like 'tablename does not exist' we will try to create the table using migration
- string error = e.Message.ToLower();
- if (!error.Contains(tableName) || !error.Contains("not") || !error.Contains("exist"))
- throw;
- }
-
- return false;
- }
-
- private static void ApplyMigration(DataLayer dataLayer, SqlMigration migration)
- {
- string driver = dataLayer.ConnectionHelper.GetType().Name;
- string sql;
- if(!migration.Sql.TryGetValue(driver, out sql))
- if(!migration.Sql.TryGetValue("default", out sql))
- throw new MigrationException("Could not find sql for migration version '" + migration.Version + "' and driver: " + driver);
-
- try
- {
- using (Transaction transaction = (Transaction)dataLayer.CreateTransaction())
- {
- transaction.Execute(sql, new List<object>());
- MigrationModelType migrationUpdate = new MigrationModelType
- {
- VersionNumber = migration.Version,
- UpdateTime = DateTime.Now
- };
- transaction.Save(migrationUpdate);
- transaction.Commit();
- }
- }
- catch (Exception e)
- {
- throw new MigrationException("Failed to execute sql migration version " + migration.Version, e);
- }
- }
-
- /// <summary>Reads all manifest files ending with fdmig.xml and creates migrations from them</summary>
- private void FetchMigrations()
- {
- foreach (Assembly assembly in _assemblies)
- foreach (string manifestResourceName in assembly.GetManifestResourceNames())
- if(manifestResourceName.EndsWith("fdmig.xml"))
- ParseMigrationXml(assembly, manifestResourceName);
- }
-
- private void ParseMigrationXml(Assembly containingAssembly, string manifestResourceName)
- {
- using(Stream stream = containingAssembly.GetManifestResourceStream(manifestResourceName))
- {
- if(stream == null)
- throw new MigrationException("Could not read manifest resource file from assembly '" + containingAssembly.FullName + "': " + manifestResourceName);
-
- using (XmlTextReader reader = new XmlTextReader(stream))
- {
- reader.WhitespaceHandling = WhitespaceHandling.None;
- if (reader.Name == "xml")
- if(!reader.Read())
- throw new MigrationException("Invalid xml in migration file, missing node after 'xml' header node: " + manifestResourceName);
-
- do
- {
- if(reader.Name == "migration")
- ParseMigration(reader);
-
- } while (reader.Read());
- }
- }
- }
-
- /// <summary>Parses the current 'migration' element in the reader and adds its value to the collection of migrations</summary>
- /// <param name="reader">The reader to parse the migration from</param>
- /// <exception cref="ArgumentNullException">If <paramref name="reader"/> is null</exception>
- /// <exception cref="MigrationXmlException">Throw if parsing failes due to invalid data in xml</exception>
- public void ParseMigration(XmlTextReader reader)
- {
- if(reader == null)
- throw new ArgumentNullException("reader");
-
- if(reader.Name != "migration")
- throw new MigrationXmlException("Element on line " + reader.LineNumber + " is not of type 'migration'.");
-
- if (reader.NodeType != XmlNodeType.Element)
- throw new MigrationXmlException("Migration element on line " + reader.LineNumber + " is end element.");
-
- string versionString = reader.GetAttribute("version");
-
- int version;
- if(!int.TryParse(versionString, out version))
- throw new MigrationXmlException("Migration element on line " + reader.LineNumber + " lacks a proper 'version' attribute.");
-
- if (_migrations.ContainsKey(version))
- throw new MigrationXmlException("Migration element on line " + reader.LineNumber + " specifies a version number that has already been added.");
-
- SqlMigration migration = new SqlMigration
- {
- Name = reader.GetAttribute("name"),
- Version = version,
- Description = reader.GetAttribute("description")
- };
-
- if (_highestVersion < version)
- _highestVersion = version;
- if (version < _lowestVersion)
- _lowestVersion = version;
-
- reader.Read();
- while(true)
- {
- if(reader.NodeType != XmlNodeType.Element)
- continue;
-
- if (reader.Name == "sql")
- {
- string driver = reader.GetAttribute("driver");
- if (string.IsNullOrEmpty(driver))
- throw new MigrationXmlException("Sql element on line " + reader.LineNumber + " lack a 'driver' attribute.");
-
- if (migration.Sql.ContainsKey(driver))
- throw new MigrationXmlException("Sql element on line " + reader.LineNumber + " specifies a driver that has already been added.");
-
- string sql = reader.ReadElementContentAsString();
- if (string.IsNullOrEmpty(sql))
- throw new MigrationException("Sql element on line " + reader.LineNumber + " lacks proper sql content.");
-
- migration.Sql.Add(driver, sql.Trim());
- }
- else if (reader.Name == "description")
- migration.Description = reader.ReadElementContentAsString();
- else if (reader.Name != "migration")
- if(!reader.Read())
- break;
-
- if (reader.Name == "migration" && reader.NodeType == XmlNodeType.EndElement)
- break;
- }
-
- _migrations.Add(migration.Version, migration);
- }
- }
- }