PageRenderTime 41ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/tags/v1.1/fadd/Data/Migration/SqlXmlMigrator.cs

#
C# | 239 lines | 159 code | 35 blank | 45 comment | 47 complexity | 7bcf1afa1429092c93990c717d11f52b MD5 | raw file
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data;
  4. using System.IO;
  5. using System.Reflection;
  6. using System.Xml;
  7. namespace Fadd.Data.Migration
  8. {
  9. /// <summary>
  10. /// As the name implies the migrator uses sql specified for a certain driver to update the database.
  11. /// The sql is loaded using manifest resource xml files named xxxxx.fdmig.xml (fadd data migration)
  12. /// </summary>
  13. /// <remarks>
  14. /// The fdmig file should look like the following
  15. /// <![CDATA[
  16. /// <?xml version="1.0" encoding="utf-8" ?>
  17. /// <migrations>
  18. /// <migration name="My migration" version="0002">
  19. /// <description>
  20. /// An optional description of the migration.
  21. /// </description>
  22. ///
  23. /// <sql driver="MySql 1.0">
  24. /// alter table myTable rename column myColumn to myColumnValues;
  25. /// </sql>
  26. ///
  27. /// <sql driver="SqlServer">
  28. /// alter table myTable rename column myColumn to myColumnValues;
  29. /// </sql>
  30. /// </migration>
  31. ///
  32. /// <migration name="My next migration" version="0003" description="A short description">
  33. /// <!-- default driver will be used if specified and db doesn't match any other driver -->
  34. /// <sql driver="default">
  35. /// INSERT INTO myTable (myId, myValue) VALUES (13, '23123');
  36. /// </sql>
  37. /// </migration>
  38. ///
  39. /// </migrations>
  40. /// ]]>
  41. /// </remarks>
  42. public class SqlXmlMigrator<MigrationModelType> : IMigrator where MigrationModelType : IMigrationModel, new()
  43. {
  44. private readonly Assembly[] _assemblies;
  45. private readonly SortedList<int, SqlMigration> _migrations = new SortedList<int, SqlMigration>();
  46. private int _highestVersion;
  47. private int _lowestVersion = int.MaxValue;
  48. /// <summary>Initializes the <see cref="SqlXmlMigrator{MigrationModelType}"/></summary>
  49. /// <param name="assemblies">Assemblies to look for fdmig.xml files in</param>
  50. public SqlXmlMigrator(params Assembly[] assemblies)
  51. {
  52. Check.Require(assemblies, "assemblies");
  53. _assemblies = assemblies;
  54. }
  55. public void Migrate(DataLayer dataLayer)
  56. {
  57. FetchMigrations();
  58. //if(_migrations.Count == 0)
  59. // return;
  60. // Test migration
  61. if (TestMigration(dataLayer))
  62. return;
  63. // If migration failed we'll try if the first migration will fix it
  64. if (!_migrations.ContainsKey(_lowestVersion))
  65. throw new MigrationException("Could not find migration table or suitable first migration");
  66. ApplyMigration(dataLayer, _migrations[_lowestVersion]);
  67. TestMigration(dataLayer);
  68. }
  69. private bool TestMigration(DataLayer dataLayer)
  70. {
  71. string tableName = dataLayer.MappingProviders.GetMapping(typeof(MigrationModelType).Name).TableName.ToLower();
  72. try
  73. {
  74. MigrationModelType lastMigration = dataLayer.Find<MigrationModelType>(new Statement("VersionNumber > ?", 0).OrderBy("VersionNumber DESC"))[0];
  75. if (lastMigration.VersionNumber >= _highestVersion)
  76. return true;
  77. for (int i = lastMigration.VersionNumber + 1; i <= _highestVersion; ++i)
  78. {
  79. if(!_migrations.ContainsKey(i))
  80. throw new MigrationException("Could not find migration version: " + i);
  81. ApplyMigration(dataLayer, _migrations[i]);
  82. }
  83. return true;
  84. }
  85. catch (Exception e)
  86. {
  87. // If the errror contains something like 'tablename does not exist' we will try to create the table using migration
  88. string error = e.Message.ToLower();
  89. if (!error.Contains(tableName) || !error.Contains("not") || !error.Contains("exist"))
  90. throw;
  91. }
  92. return false;
  93. }
  94. private static void ApplyMigration(DataLayer dataLayer, SqlMigration migration)
  95. {
  96. string driver = dataLayer.ConnectionHelper.GetType().Name;
  97. string sql;
  98. if(!migration.Sql.TryGetValue(driver, out sql))
  99. if(!migration.Sql.TryGetValue("default", out sql))
  100. throw new MigrationException("Could not find sql for migration version '" + migration.Version + "' and driver: " + driver);
  101. try
  102. {
  103. using (Transaction transaction = (Transaction)dataLayer.CreateTransaction())
  104. {
  105. transaction.Execute(sql, new List<object>());
  106. MigrationModelType migrationUpdate = new MigrationModelType
  107. {
  108. VersionNumber = migration.Version,
  109. UpdateTime = DateTime.Now
  110. };
  111. transaction.Save(migrationUpdate);
  112. transaction.Commit();
  113. }
  114. }
  115. catch (Exception e)
  116. {
  117. throw new MigrationException("Failed to execute sql migration version " + migration.Version, e);
  118. }
  119. }
  120. /// <summary>Reads all manifest files ending with fdmig.xml and creates migrations from them</summary>
  121. private void FetchMigrations()
  122. {
  123. foreach (Assembly assembly in _assemblies)
  124. foreach (string manifestResourceName in assembly.GetManifestResourceNames())
  125. if(manifestResourceName.EndsWith("fdmig.xml"))
  126. ParseMigrationXml(assembly, manifestResourceName);
  127. }
  128. private void ParseMigrationXml(Assembly containingAssembly, string manifestResourceName)
  129. {
  130. using(Stream stream = containingAssembly.GetManifestResourceStream(manifestResourceName))
  131. {
  132. if(stream == null)
  133. throw new MigrationException("Could not read manifest resource file from assembly '" + containingAssembly.FullName + "': " + manifestResourceName);
  134. using (XmlTextReader reader = new XmlTextReader(stream))
  135. {
  136. reader.WhitespaceHandling = WhitespaceHandling.None;
  137. if (reader.Name == "xml")
  138. if(!reader.Read())
  139. throw new MigrationException("Invalid xml in migration file, missing node after 'xml' header node: " + manifestResourceName);
  140. do
  141. {
  142. if(reader.Name == "migration")
  143. ParseMigration(reader);
  144. } while (reader.Read());
  145. }
  146. }
  147. }
  148. /// <summary>Parses the current 'migration' element in the reader and adds its value to the collection of migrations</summary>
  149. /// <param name="reader">The reader to parse the migration from</param>
  150. /// <exception cref="ArgumentNullException">If <paramref name="reader"/> is null</exception>
  151. /// <exception cref="MigrationXmlException">Throw if parsing failes due to invalid data in xml</exception>
  152. public void ParseMigration(XmlTextReader reader)
  153. {
  154. if(reader == null)
  155. throw new ArgumentNullException("reader");
  156. if(reader.Name != "migration")
  157. throw new MigrationXmlException("Element on line " + reader.LineNumber + " is not of type 'migration'.");
  158. if (reader.NodeType != XmlNodeType.Element)
  159. throw new MigrationXmlException("Migration element on line " + reader.LineNumber + " is end element.");
  160. string versionString = reader.GetAttribute("version");
  161. int version;
  162. if(!int.TryParse(versionString, out version))
  163. throw new MigrationXmlException("Migration element on line " + reader.LineNumber + " lacks a proper 'version' attribute.");
  164. if (_migrations.ContainsKey(version))
  165. throw new MigrationXmlException("Migration element on line " + reader.LineNumber + " specifies a version number that has already been added.");
  166. SqlMigration migration = new SqlMigration
  167. {
  168. Name = reader.GetAttribute("name"),
  169. Version = version,
  170. Description = reader.GetAttribute("description")
  171. };
  172. if (_highestVersion < version)
  173. _highestVersion = version;
  174. if (version < _lowestVersion)
  175. _lowestVersion = version;
  176. reader.Read();
  177. while(true)
  178. {
  179. if(reader.NodeType != XmlNodeType.Element)
  180. continue;
  181. if (reader.Name == "sql")
  182. {
  183. string driver = reader.GetAttribute("driver");
  184. if (string.IsNullOrEmpty(driver))
  185. throw new MigrationXmlException("Sql element on line " + reader.LineNumber + " lack a 'driver' attribute.");
  186. if (migration.Sql.ContainsKey(driver))
  187. throw new MigrationXmlException("Sql element on line " + reader.LineNumber + " specifies a driver that has already been added.");
  188. string sql = reader.ReadElementContentAsString();
  189. if (string.IsNullOrEmpty(sql))
  190. throw new MigrationException("Sql element on line " + reader.LineNumber + " lacks proper sql content.");
  191. migration.Sql.Add(driver, sql.Trim());
  192. }
  193. else if (reader.Name == "description")
  194. migration.Description = reader.ReadElementContentAsString();
  195. else if (reader.Name != "migration")
  196. if(!reader.Read())
  197. break;
  198. if (reader.Name == "migration" && reader.NodeType == XmlNodeType.EndElement)
  199. break;
  200. }
  201. _migrations.Add(migration.Version, migration);
  202. }
  203. }
  204. }