From f4450ccf4f76db7f6e18c483ac48f30e0907eb2a Mon Sep 17 00:00:00 2001
From: AlexRa
Date: Sun, 16 May 2010 16:22:38 +0300
Subject: Migration.cs supports single-file migration history format
Scans for migration resources in either old-style "scattered" (one file per version)
or new-style "integrated" format (single file "Resources/{StoreName}.migrations[.nnn]") with ":VERSION nnn" sections).
In the new-style migrations it also recognizes ':GO' separators for parts of the SQL script
that must be sent to the server separately. The old-style migrations are loaded each in one piece
and don't support the ':GO' feature.
Status: TESTED and works fine in all modes!
---
OpenSim/Data/Migration.cs | 337 ++++++++++++++++++++++++++++++++--------------
1 file changed, 233 insertions(+), 104 deletions(-)
(limited to 'OpenSim/Data/Migration.cs')
diff --git a/OpenSim/Data/Migration.cs b/OpenSim/Data/Migration.cs
index 06defe4..7980c35 100644
--- a/OpenSim/Data/Migration.cs
+++ b/OpenSim/Data/Migration.cs
@@ -70,61 +70,111 @@ namespace OpenSim.Data
public class Migration
{
- private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
+ protected static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
- private string _type;
- private DbConnection _conn;
- // private string _subtype;
- private Assembly _assem;
- private Regex _match;
+ protected string _type;
+ protected DbConnection _conn;
+ protected Assembly _assem;
- private static readonly string _migrations_create = "create table migrations(name varchar(100), version int)";
- // private static readonly string _migrations_init = "insert into migrations values('migrations', 1)";
- // private static readonly string _migrations_find = "select version from migrations where name='migrations'";
+ private Regex _match_old;
+ private Regex _match_new;
+ /// Have the parameterless constructor just so we can specify it as a generic parameter with the new() constraint.
+ /// Currently this is only used in the tests. A Migration instance created this way must be then
+ /// initialized with Initialize(). Regular creation should be through the parameterized constructors.
+ ///
+ public Migration()
+ {
+ }
- public Migration(DbConnection conn, Assembly assem, string type)
+ public Migration(DbConnection conn, Assembly assem, string subtype, string type)
{
- _type = type;
- _conn = conn;
- _assem = assem;
- _match = new Regex(@"\.(\d\d\d)_" + _type + @"\.sql");
- Initialize();
+ Initialize(conn, assem, type, subtype);
}
- public Migration(DbConnection conn, Assembly assem, string subtype, string type)
+ public Migration(DbConnection conn, Assembly assem, string type)
+ {
+ Initialize(conn, assem, type, "");
+ }
+
+ /// Must be called after creating with the parameterless constructor.
+ /// NOTE that the Migration class now doesn't access database in any way during initialization.
+ /// Specifically, it won't check if the [migrations] table exists. Such checks are done later:
+ /// automatically on Update(), or you can explicitly call InitMigrationsTable().
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Initialize (DbConnection conn, Assembly assem, string type, string subtype)
{
_type = type;
_conn = conn;
_assem = assem;
- _match = new Regex(subtype + @"\.(\d\d\d)_" + _type + @"\.sql");
- Initialize();
+ _match_old = new Regex(subtype + @"\.(\d\d\d)_" + _type + @"\.sql");
+ string s = String.IsNullOrEmpty(subtype) ? _type : _type + @"\." + subtype;
+ _match_new = new Regex(@"\." + s + @"\.migrations(?:\.(?\d+)$|.*)");
}
- private void Initialize()
+ public void InitMigrationsTable()
{
- // clever, eh, we figure out which migrations version we are
- int migration_version = FindVersion(_conn, "migrations");
-
- if (migration_version > 0)
- return;
+ // NOTE: normally when the [migrations] table is created, the version record for 'migrations' is
+ // added immediately. However, if for some reason the table is there but empty, we want to handle that as well.
+ int ver = FindVersion(_conn, "migrations");
+ if (ver <= 0) // -1 = no table, 0 = no version record
+ {
+ if( ver < 0 )
+ ExecuteScript("create table migrations(name varchar(100), version int)");
+ InsertVersion("migrations", 1);
+ }
+ }
- // If not, create the migration tables
- using (DbCommand cmd = _conn.CreateCommand())
+ /// Executes a script, possibly in a database-specific way.
+ /// It can be redefined for a specific DBMS, if necessary. Specifically,
+ /// to avoid problems with proc definitions in MySQL, we must use
+ /// MySqlScript class instead of just DbCommand. We don't want to bring
+ /// MySQL references here, so instead define a MySQLMigration class
+ /// in OpenSim.Data.MySQL
+ ///
+ ///
+ /// Array of strings, one-per-batch (often just one)
+ protected virtual void ExecuteScript(DbConnection conn, string[] script)
+ {
+ using (DbCommand cmd = conn.CreateCommand())
{
- cmd.CommandText = _migrations_create;
- cmd.ExecuteNonQuery();
+ cmd.CommandTimeout = 0;
+ foreach (string sql in script)
+ {
+ cmd.CommandText = sql;
+ cmd.ExecuteNonQuery();
+ }
}
+ }
+
+ protected void ExecuteScript(DbConnection conn, string sql)
+ {
+ ExecuteScript(conn, new string[]{sql});
+ }
+
+ protected void ExecuteScript(string sql)
+ {
+ ExecuteScript(_conn, sql);
+ }
- InsertVersion("migrations", 1);
+ protected void ExecuteScript(string[] script)
+ {
+ ExecuteScript(_conn, script);
}
+
+
public void Update()
{
- int version = 0;
- version = FindVersion(_conn, _type);
+ InitMigrationsTable();
- SortedList migrations = GetMigrationsAfter(version);
+ int version = FindVersion(_conn, _type);
+
+ SortedList migrations = GetMigrationsAfter(version);
if (migrations.Count < 1)
return;
@@ -132,57 +182,41 @@ namespace OpenSim.Data
m_log.InfoFormat("[MIGRATIONS] Upgrading {0} to latest revision {1}.", _type, migrations.Keys[migrations.Count - 1]);
m_log.Info("[MIGRATIONS] NOTE: this may take a while, don't interupt this process!");
- using (DbCommand cmd = _conn.CreateCommand())
+ foreach (KeyValuePair kvp in migrations)
{
- foreach (KeyValuePair kvp in migrations)
+ int newversion = kvp.Key;
+ // we need to up the command timeout to infinite as we might be doing long migrations.
+
+ /* [AlexRa 01-May-10]: We can't always just run any SQL in a single batch (= ExecuteNonQuery()). Things like
+ * stored proc definitions might have to be sent to the server each in a separate batch.
+ * This is certainly so for MS SQL; not sure how the MySQL connector sorts out the mess
+ * with 'delimiter @@'/'delimiter ;' around procs. So each "script" this code executes now is not
+ * a single string, but an array of strings, executed separately.
+ */
+ try
{
- int newversion = kvp.Key;
- cmd.CommandText = kvp.Value;
- // we need to up the command timeout to infinite as we might be doing long migrations.
- cmd.CommandTimeout = 0;
- try
- {
- cmd.ExecuteNonQuery();
- }
- catch (Exception e)
- {
- m_log.DebugFormat("[MIGRATIONS] Cmd was {0}", cmd.CommandText);
- m_log.DebugFormat("[MIGRATIONS]: An error has occurred in the migration {0}.\n This may mean you could see errors trying to run OpenSim. If you see database related errors, you will need to fix the issue manually. Continuing.", e.Message);
- cmd.CommandText = "ROLLBACK;";
- cmd.ExecuteNonQuery();
- }
+ ExecuteScript(kvp.Value);
+ }
+ catch (Exception e)
+ {
+ m_log.DebugFormat("[MIGRATIONS] Cmd was {0}", kvp.Value.ToString());
+ m_log.DebugFormat("[MIGRATIONS]: An error has occurred in the migration {0}.\n This may mean you could see errors trying to run OpenSim. If you see database related errors, you will need to fix the issue manually. Migration aborted.", e.Message);
+ ExecuteScript("ROLLBACK;");
+ return;
+ }
- if (version == 0)
- {
- InsertVersion(_type, newversion);
- }
- else
- {
- UpdateVersion(_type, newversion);
- }
- version = newversion;
+ if (version == 0)
+ {
+ InsertVersion(_type, newversion);
}
+ else
+ {
+ UpdateVersion(_type, newversion);
+ }
+ version = newversion;
}
}
- // private int MaxVersion()
- // {
- // int max = 0;
- // string[] names = _assem.GetManifestResourceNames();
-
- // foreach (string s in names)
- // {
- // Match m = _match.Match(s);
- // if (m.Success)
- // {
- // int MigrationVersion = int.Parse(m.Groups[1].ToString());
- // if (MigrationVersion > max)
- // max = MigrationVersion;
- // }
- // }
- // return max;
- // }
-
public int Version
{
get { return FindVersion(_conn, _type); }
@@ -206,7 +240,7 @@ namespace OpenSim.Data
try
{
cmd.CommandText = "select version from migrations where name='" + type + "' order by version desc";
- using (IDataReader reader = cmd.ExecuteReader())
+ using (DbDataReader reader = cmd.ExecuteReader())
{
if (reader.Read())
{
@@ -217,7 +251,8 @@ namespace OpenSim.Data
}
catch
{
- // Something went wrong, so we're version 0
+ // Something went wrong (probably no table), so we're at version -1
+ version = -1;
}
}
return version;
@@ -225,57 +260,151 @@ namespace OpenSim.Data
private void InsertVersion(string type, int version)
{
- using (DbCommand cmd = _conn.CreateCommand())
- {
- cmd.CommandText = "insert into migrations(name, version) values('" + type + "', " + version + ")";
- m_log.InfoFormat("[MIGRATIONS]: Creating {0} at version {1}", type, version);
- cmd.ExecuteNonQuery();
- }
+ m_log.InfoFormat("[MIGRATIONS]: Creating {0} at version {1}", type, version);
+ ExecuteScript("insert into migrations(name, version) values('" + type + "', " + version + ")");
}
private void UpdateVersion(string type, int version)
{
- using (DbCommand cmd = _conn.CreateCommand())
- {
- cmd.CommandText = "update migrations set version=" + version + " where name='" + type + "'";
- m_log.InfoFormat("[MIGRATIONS]: Updating {0} to version {1}", type, version);
- cmd.ExecuteNonQuery();
- }
+ m_log.InfoFormat("[MIGRATIONS]: Updating {0} to version {1}", type, version);
+ ExecuteScript("update migrations set version=" + version + " where name='" + type + "'");
}
- // private SortedList GetAllMigrations()
- // {
- // return GetMigrationsAfter(0);
- // }
+ private delegate void FlushProc();
- private SortedList GetMigrationsAfter(int after)
+ /// Scans for migration resources in either old-style "scattered" (one file per version)
+ /// or new-style "integrated" format (single file with ":VERSION nnn" sections).
+ /// In the new-style migrations it also recognizes ':GO' separators for parts of the SQL script
+ /// that must be sent to the server separately. The old-style migrations are loaded each in one piece
+ /// and don't support the ':GO' feature.
+ ///
+ /// The version we are currently at. Scan for any higher versions
+ /// A list of string arrays, representing the scripts.
+ private SortedList GetMigrationsAfter(int after)
{
+ SortedList migrations = new SortedList();
+
string[] names = _assem.GetManifestResourceNames();
- SortedList migrations = new SortedList();
- // because life is funny if we don't
- Array.Sort(names);
+ if( names.Length == 0 ) // should never happen
+ return migrations;
+
+ Array.Sort(names); // we want all the migrations ordered
+
+ int nLastVerFound = 0;
+ Match m = null;
+ string sFile = Array.FindLast(names, nm => { m = _match_new.Match(nm); return m.Success; }); // ; nm.StartsWith(sPrefix, StringComparison.InvariantCultureIgnoreCase
+
+ if( (m != null) && !String.IsNullOrEmpty(sFile) )
+ {
+ /* The filename should be '.migrations[.NNN]' where NNN
+ * is the last version number defined in the file. If the '.NNN' part is recognized, the code can skip
+ * the file without looking inside if we have a higher version already. Without the suffix we read
+ * the file anyway and use the version numbers inside. Any unrecognized suffix (such as '.sql')
+ * is valid but ignored.
+ *
+ * NOTE that we expect only one 'merged' migration file. If there are several, we take the last one.
+ * If you are numbering them, leave only the latest one in the project or at least make sure they numbered
+ * to come up in the correct order (e.g. 'SomeStore.migrations.001' rather than 'SomeStore.migrations.1')
+ */
+
+ if (m.Groups.Count > 1 && int.TryParse(m.Groups[1].Value, out nLastVerFound))
+ {
+ if( nLastVerFound <= after )
+ goto scan_old_style;
+ }
+
+ System.Text.StringBuilder sb = new System.Text.StringBuilder(4096);
+ int nVersion = -1;
+
+ List script = new List();
+
+ FlushProc flush = delegate()
+ {
+ if (sb.Length > 0) // last SQL stmt to script list
+ {
+ script.Add(sb.ToString());
+ sb.Length = 0;
+ }
+
+ if ( (nVersion > 0) && (nVersion > after) && (script.Count > 0) && !migrations.ContainsKey(nVersion)) // script to the versioned script list
+ {
+ migrations[nVersion] = script.ToArray();
+ }
+ script.Clear();
+ };
+ using (Stream resource = _assem.GetManifestResourceStream(sFile))
+ using (StreamReader resourceReader = new StreamReader(resource))
+ {
+ int nLineNo = 0;
+ while (!resourceReader.EndOfStream)
+ {
+ string sLine = resourceReader.ReadLine();
+ nLineNo++;
+
+ if( String.IsNullOrEmpty(sLine) || sLine.StartsWith("#") ) // ignore a comment or empty line
+ continue;
+
+ if (sLine.Trim().Equals(":GO", StringComparison.InvariantCultureIgnoreCase))
+ {
+ if (sb.Length == 0) continue;
+ if (nVersion > after)
+ script.Add(sb.ToString());
+ sb.Length = 0;
+ continue;
+ }
+
+ if (sLine.StartsWith(":VERSION ", StringComparison.InvariantCultureIgnoreCase)) // ":VERSION nnn"
+ {
+ flush();
+
+ int n = sLine.IndexOf('#'); // Comment is allowed in version sections, ignored
+ if (n >= 0)
+ sLine = sLine.Substring(0, n);
+
+ if (!int.TryParse(sLine.Substring(9).Trim(), out nVersion))
+ {
+ m_log.ErrorFormat("[MIGRATIONS]: invalid version marker at {0}: line {1}. Migration failed!", sFile, nLineNo);
+ break;
+ }
+ }
+ else
+ {
+ sb.AppendLine(sLine);
+ }
+ }
+ flush();
+
+ // If there are scattered migration files as well, only look for those with higher version numbers.
+ if (after < nVersion)
+ after = nVersion;
+ }
+ }
+
+scan_old_style:
+ // scan "old style" migration pieces anyway, ignore any versions already filled from the single file
foreach (string s in names)
{
- Match m = _match.Match(s);
+ m = _match_old.Match(s);
if (m.Success)
{
int version = int.Parse(m.Groups[1].ToString());
- if (version > after)
+ if ( (version > after) && !migrations.ContainsKey(version) )
{
using (Stream resource = _assem.GetManifestResourceStream(s))
{
using (StreamReader resourceReader = new StreamReader(resource))
{
- string resourceString = resourceReader.ReadToEnd();
- migrations.Add(version, resourceString);
+ string sql = resourceReader.ReadToEnd();
+ migrations.Add(version, new string[]{sql});
}
}
}
}
}
-
- if (migrations.Count < 1) {
+
+ if (migrations.Count < 1)
+ {
m_log.InfoFormat("[MIGRATIONS]: {0} up to date, no migrations to apply", _type);
}
return migrations;
--
cgit v1.1