/*
* Copyright (c) Contributors, http://opensimulator.org/
* See CONTRIBUTORS.TXT for a full list of copyright holders.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the OpenSimulator Project nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using log4net;
namespace OpenSim.Data
{
///
///
/// The Migration theory is based on the ruby on rails concept.
/// Each database driver is going to be allowed to have files in
/// Resources that specify the database migrations. They will be
/// of the form:
///
/// 001_Users.sql
/// 002_Users.sql
/// 003_Users.sql
/// 001_Prims.sql
/// 002_Prims.sql
/// ...etc...
///
/// When a database driver starts up, it specifies a resource that
/// needs to be brought up to the current revision. For instance:
///
/// Migration um = new Migration(DbConnection, Assembly, "Users");
/// um.Update();
///
/// This works out which version Users is at, and applies all the
/// revisions past it to it. If there is no users table, all
/// revisions are applied in order. Consider each future
/// migration to be an incremental roll forward of the tables in
/// question.
///
/// Assembly must be specifically passed in because otherwise you
/// get the assembly that Migration.cs is part of, and what you
/// really want is the assembly of your database class.
///
///
public class Migration
{
protected static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
protected string _type;
protected DbConnection _conn;
protected Assembly _assem;
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 subtype, string type)
{
Initialize(conn, assem, type, subtype);
}
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_old = new Regex(subtype + @"\.(\d\d\d)_" + _type + @"\.sql");
string s = String.IsNullOrEmpty(subtype) ? _type : _type + @"\." + subtype;
_match_new = new Regex(@"\." + s + @"\.migrations(?:\.(?\d+)$|.*)");
}
public void InitMigrationsTable()
{
// 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);
}
}
/// 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.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);
}
protected void ExecuteScript(string[] script)
{
ExecuteScript(_conn, script);
}
public void Update()
{
InitMigrationsTable();
int version = FindVersion(_conn, _type);
SortedList migrations = GetMigrationsAfter(version);
if (migrations.Count < 1)
return;
// to prevent people from killing long migrations.
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!");
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
{
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;
}
}
public int Version
{
get { return FindVersion(_conn, _type); }
set {
if (Version < 1)
{
InsertVersion(_type, value);
}
else
{
UpdateVersion(_type, value);
}
}
}
protected virtual int FindVersion(DbConnection conn, string type)
{
int version = 0;
using (DbCommand cmd = conn.CreateCommand())
{
try
{
cmd.CommandText = "select version from migrations where name='" + type + "' order by version desc";
using (DbDataReader reader = cmd.ExecuteReader())
{
if (reader.Read())
{
version = Convert.ToInt32(reader["version"]);
}
reader.Close();
}
}
catch
{
// Something went wrong (probably no table), so we're at version -1
version = -1;
}
}
return version;
}
private void InsertVersion(string type, int version)
{
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)
{
m_log.InfoFormat("[MIGRATIONS]: Updating {0} to version {1}", type, version);
ExecuteScript("update migrations set version=" + version + " where name='" + type + "'");
}
private delegate void FlushProc();
/// 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();
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)
{
m = _match_old.Match(s);
if (m.Success)
{
int version = int.Parse(m.Groups[1].ToString());
if ( (version > after) && !migrations.ContainsKey(version) )
{
using (Stream resource = _assem.GetManifestResourceStream(s))
{
using (StreamReader resourceReader = new StreamReader(resource))
{
string sql = resourceReader.ReadToEnd();
migrations.Add(version, new string[]{sql});
}
}
}
}
}
if (migrations.Count < 1)
{
m_log.InfoFormat("[MIGRATIONS]: {0} up to date, no migrations to apply", _type);
}
return migrations;
}
}
}