#region License //The contents of this file are subject to the Mozilla Public License //Version 1.1 (the "License"); you may not use this file except in //compliance with the License. You may obtain a copy of the License at //http://www.mozilla.org/MPL/ //Software distributed under the License is distributed on an "AS IS" //basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the //License for the specific language governing rights and limitations //under the License. #endregion using System; using System.Collections.Generic; using System.Data; using Migrator.Framework; using Migrator.Framework.SchemaBuilder; using ForeignKeyConstraint = Migrator.Framework.ForeignKeyConstraint; using Migrator.Framework.Loggers; namespace Migrator.Providers { /// /// Base class for every transformation providers. /// A 'tranformation' is an operation that modifies the database. /// public abstract class TransformationProvider : ITransformationProvider { private ILogger _logger; protected IDbConnection _connection; private IDbTransaction _transaction; private List _appliedMigrations; protected readonly string _connectionString; protected Dialect _dialect; private readonly ForeignKeyConstraintMapper constraintMapper = new ForeignKeyConstraintMapper(); protected TransformationProvider(Dialect dialect, string connectionString) { _dialect = dialect; _connectionString = connectionString; _logger = new Logger(false); } /// /// Returns the event logger /// public virtual ILogger Logger { get { return _logger; } set { _logger = value; } } public Dialect Dialect { get { return _dialect; } } public ITransformationProvider this[string provider] { get { if (null != provider && IsThisProvider(provider)) return this; return NoOpTransformationProvider.Instance; } } public bool IsThisProvider(string provider) { // XXX: This might need to be more sophisticated. Currently just a convention return GetType().Name.ToLower().StartsWith(provider.ToLower()); } public virtual Column[] GetColumns(string table) { List columns = new List(); using ( IDataReader reader = ExecuteQuery( String.Format("select COLUMN_NAME, IS_NULLABLE from information_schema.columns where table_name = '{0}'", table))) { while (reader.Read()) { Column column = new Column(reader.GetString(0), DbType.String); string nullableStr = reader.GetString(1); bool isNullable = nullableStr == "YES"; column.ColumnProperty |= isNullable ? ColumnProperty.Null : ColumnProperty.NotNull; columns.Add(column); } } return columns.ToArray(); } public virtual Column GetColumnByName(string table, string columnName) { return Array.Find(GetColumns(table), delegate(Column column) { return column.Name == columnName; }); } public virtual string[] GetTables() { List tables = new List(); using (IDataReader reader = ExecuteQuery("SELECT table_name FROM information_schema.tables")) { while (reader.Read()) { tables.Add((string)reader[0]); } } return tables.ToArray(); } public virtual void RemoveForeignKey(string table, string name) { RemoveConstraint(table, name); } public virtual void RemoveConstraint(string table, string name) { if (TableExists(table) && ConstraintExists(table, name)) { table = _dialect.TableNameNeedsQuote ? _dialect.Quote(table) : table; name = _dialect.ConstraintNameNeedsQuote ? _dialect.Quote(name) : name; ExecuteNonQuery(String.Format("ALTER TABLE {0} DROP CONSTRAINT {1}", table, name)); } } public void RemoveIndex(string table, string name) { throw new NotImplementedException(); } public virtual void AddTable(string table, string engine, string columns) { table = _dialect.TableNameNeedsQuote ? _dialect.Quote(table) : table; string sqlCreate = String.Format("CREATE TABLE {0} ({1})", table, columns); ExecuteNonQuery(sqlCreate); } /// /// Add a new table /// /// Table name /// Columns /// /// Adds the Test table with two columns: /// /// Database.AddTable("Test", /// new Column("Id", typeof(int), ColumnProperty.PrimaryKey), /// new Column("Title", typeof(string), 100) /// ); /// /// public virtual void AddTable(string name, params Column[] columns) { // Most databases don't have the concept of a storage engine, so default is to not use it. AddTable(name, null, columns); } /// /// Add a new table /// /// Table name /// Columns /// the database storage engine to use /// /// Adds the Test table with two columns: /// /// Database.AddTable("Test", "INNODB", /// new Column("Id", typeof(int), ColumnProperty.PrimaryKey), /// new Column("Title", typeof(string), 100) /// ); /// /// public virtual void AddTable(string name, string engine, params Column[] columns) { if (TableExists(name)) { Logger.Warn("Table {0} already exists", name); return; } List pks = GetPrimaryKeys(columns); bool compoundPrimaryKey = pks.Count > 1; List columnProviders = new List(columns.Length); foreach (Column column in columns) { // Remove the primary key notation if compound primary key because we'll add it back later if (compoundPrimaryKey && column.IsPrimaryKey) column.ColumnProperty = ColumnProperty.Unsigned | ColumnProperty.NotNull; ColumnPropertiesMapper mapper = _dialect.GetAndMapColumnProperties(column); columnProviders.Add(mapper); } string columnsAndIndexes = JoinColumnsAndIndexes(columnProviders); AddTable(name, engine, columnsAndIndexes); if (compoundPrimaryKey) { AddPrimaryKey(String.Format("PK_{0}", name), name, pks.ToArray()); } } public List GetPrimaryKeys(IEnumerable columns) { List pks = new List(); foreach (Column col in columns) { if (col.IsPrimaryKey) pks.Add(col.Name); } return pks; } public virtual void RemoveTable(string name) { if (TableExists(name)) ExecuteNonQuery(String.Format("DROP TABLE {0}", name)); } public virtual void RenameTable(string oldName, string newName) { if (TableExists(newName)) throw new MigrationException(String.Format("Table with name '{0}' already exists", newName)); if (TableExists(oldName)) ExecuteNonQuery(String.Format("ALTER TABLE {0} RENAME TO {1}", oldName, newName)); } public virtual void RenameColumn(string tableName, string oldColumnName, string newColumnName) { if (ColumnExists(tableName, newColumnName)) throw new MigrationException(String.Format("Table '{0}' has column named '{1}' already", tableName, newColumnName)); if (ColumnExists(tableName, oldColumnName)) ExecuteNonQuery(String.Format("ALTER TABLE {0} RENAME COLUMN {1} TO {2}", tableName, oldColumnName, newColumnName)); } public virtual void AddColumn(string table, string sqlColumn) { ExecuteNonQuery(String.Format("ALTER TABLE {0} ADD COLUMN {1}", table, sqlColumn)); } public virtual void RemoveColumn(string table, string column) { if (ColumnExists(table, column)) { ExecuteNonQuery(String.Format("ALTER TABLE {0} DROP COLUMN {1} ", table, column)); } } public virtual bool ColumnExists(string table, string column) { try { ExecuteNonQuery(String.Format("SELECT {0} FROM {1}", column, table)); return true; } catch (Exception) { return false; } } public virtual void ChangeColumn(string table, Column column) { if (!ColumnExists(table, column.Name)) { Logger.Warn("Column {0}.{1} does not exist", table, column.Name); return; } ColumnPropertiesMapper mapper = _dialect.GetAndMapColumnProperties(column); ChangeColumn(table, mapper.ColumnSql); } public virtual void ChangeColumn(string table, string sqlColumn) { ExecuteNonQuery(String.Format("ALTER TABLE {0} ALTER COLUMN {1}", table, sqlColumn)); } public virtual bool TableExists(string table) { try { ExecuteNonQuery("SELECT COUNT(*) FROM " + table); return true; } catch (Exception) { return false; } } protected virtual string JoinColumnsAndIndexes(IEnumerable columns) { string indexes = JoinIndexes(columns); string columnsAndIndexes = JoinColumns(columns) + (indexes != null ? "," + indexes : String.Empty); return columnsAndIndexes; } protected virtual string JoinIndexes(IEnumerable columns) { List indexes = new List(); foreach (ColumnPropertiesMapper column in columns) { string indexSql = column.IndexSql; if (indexSql != null) indexes.Add(indexSql); } if (indexes.Count == 0) return null; return String.Join(", ", indexes.ToArray()); } protected virtual string JoinColumns(IEnumerable columns) { List columnStrings = new List(); foreach (ColumnPropertiesMapper column in columns) columnStrings.Add(column.ColumnSql); return String.Join(", ", columnStrings.ToArray()); } /// /// Add a new column to an existing table. /// /// Table to which to add the column /// Column name /// Date type of the column /// Max length of the column /// Properties of the column, see ColumnProperty, /// Default value public virtual void AddColumn(string table, string column, DbType type, int size, ColumnProperty property, object defaultValue) { if (ColumnExists(table, column)) { Logger.Warn("Column {0}.{1} already exists", table, column); return; } ColumnPropertiesMapper mapper = _dialect.GetAndMapColumnProperties(new Column(column, type, size, property, defaultValue)); AddColumn(table, mapper.ColumnSql); } /// /// /// AddColumn(string, string, Type, int, ColumnProperty, object) /// /// public virtual void AddColumn(string table, string column, DbType type) { AddColumn(table, column, type, 0, ColumnProperty.Null, null); } /// /// /// AddColumn(string, string, Type, int, ColumnProperty, object) /// /// public virtual void AddColumn(string table, string column, DbType type, int size) { AddColumn(table, column, type, size, ColumnProperty.Null, null); } public void AddColumn(string table, string column, DbType type, object defaultValue) { if (ColumnExists(table, column)) { Logger.Warn("Column {0}.{1} already exists", table, column); return; } ColumnPropertiesMapper mapper = _dialect.GetAndMapColumnProperties(new Column(column, type, defaultValue)); AddColumn(table, mapper.ColumnSql); } /// /// /// AddColumn(string, string, Type, int, ColumnProperty, object) /// /// public virtual void AddColumn(string table, string column, DbType type, ColumnProperty property) { AddColumn(table, column, type, 0, property, null); } /// /// /// AddColumn(string, string, Type, int, ColumnProperty, object) /// /// public virtual void AddColumn(string table, string column, DbType type, int size, ColumnProperty property) { AddColumn(table, column, type, size, property, null); } public void AddIndex(string name, string table, bool unique, params string[] columns) { try { var uniqueText = ""; if (unique) uniqueText = "UNIQUE"; var command = String.Format("CREATE {0} INDEX {1} ON {2} ({3})", uniqueText, name, table, String.Join(",", columns)); ExecuteNonQuery(command); } catch (Exception e) { Logger.Exception("Unable to add index", e); } } /// /// Append a primary key to a table. /// /// Constraint name /// Table name /// Primary column names public virtual void AddPrimaryKey(string name, string table, params string[] columns) { if (ConstraintExists(table, name)) { Logger.Warn("Primary key {0} already exists", name); return; } ExecuteNonQuery( String.Format("ALTER TABLE {0} ADD CONSTRAINT {1} PRIMARY KEY ({2}) ", table, name, String.Join(",", columns))); } public virtual void AddUniqueConstraint(string name, string table, params string[] columns) { if (ConstraintExists(table, name)) { Logger.Warn("Constraint {0} already exists", name); return; } ExecuteNonQuery(String.Format("ALTER TABLE {0} ADD CONSTRAINT {1} UNIQUE({2}) ", table, name, string.Join(", ", columns))); } public virtual void AddCheckConstraint(string name, string table, string checkSql) { if (ConstraintExists(table, name)) { Logger.Warn("Constraint {0} already exists", name); return; } ExecuteNonQuery(String.Format("ALTER TABLE {0} ADD CONSTRAINT {1} CHECK ({2}) ", table, name, checkSql)); } /// /// Guesses the name of the foreign key and add it /// public virtual void GenerateForeignKey(string primaryTable, string primaryColumn, string refTable, string refColumn) { AddForeignKey("FK_" + primaryTable + "_" + refTable, primaryTable, primaryColumn, refTable, refColumn); } /// /// Guesses the name of the foreign key and add it /// /// public virtual void GenerateForeignKey(string primaryTable, string[] primaryColumns, string refTable, string[] refColumns) { AddForeignKey("FK_" + primaryTable + "_" + refTable, primaryTable, primaryColumns, refTable, refColumns); } /// /// Guesses the name of the foreign key and add it /// public virtual void GenerateForeignKey(string primaryTable, string primaryColumn, string refTable, string refColumn, ForeignKeyConstraint constraint) { AddForeignKey("FK_" + primaryTable + "_" + refTable, primaryTable, primaryColumn, refTable, refColumn, constraint); } /// /// Guesses the name of the foreign key and add it /// /// public virtual void GenerateForeignKey(string primaryTable, string[] primaryColumns, string refTable, string[] refColumns, ForeignKeyConstraint constraint) { AddForeignKey("FK_" + primaryTable + "_" + refTable, primaryTable, primaryColumns, refTable, refColumns, constraint); } /// /// Append a foreign key (relation) between two tables. /// tables. /// /// Constraint name /// Table name containing the primary key /// Primary key column name /// Foreign table name /// Foreign column name public virtual void AddForeignKey(string name, string primaryTable, string primaryColumn, string refTable, string refColumn) { AddForeignKey(name, primaryTable, new string[] { primaryColumn }, refTable, new string[] { refColumn }); } /// /// /// AddForeignKey(string, string, string, string, string) /// /// public virtual void AddForeignKey(string name, string primaryTable, string[] primaryColumns, string refTable, string[] refColumns) { AddForeignKey(name, primaryTable, primaryColumns, refTable, refColumns, ForeignKeyConstraint.NoAction); } public virtual void AddForeignKey(string name, string primaryTable, string primaryColumn, string refTable, string refColumn, ForeignKeyConstraint constraint) { AddForeignKey(name, primaryTable, new string[] { primaryColumn }, refTable, new string[] { refColumn }, constraint); } public virtual void AddForeignKey(string name, string primaryTable, string[] primaryColumns, string refTable, string[] refColumns, ForeignKeyConstraint constraint) { if (ConstraintExists(primaryTable, name)) { Logger.Warn("Constraint {0} already exists", name); return; } string constraintResolved = constraintMapper.SqlForConstraint(constraint); ExecuteNonQuery( String.Format( "ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4}) ON UPDATE {5} ON DELETE {6}", primaryTable, name, String.Join(",", primaryColumns), refTable, String.Join(",", refColumns), constraintResolved, constraintResolved)); } /// /// Determines if a constraint exists. /// /// Constraint name /// Table owning the constraint /// true if the constraint exists. public abstract bool ConstraintExists(string table, string name); public virtual bool PrimaryKeyExists(string table, string name) { return ConstraintExists(table, name); } public int ExecuteNonQuery(string sql) { Logger.Trace(sql); Logger.ApplyingDBChange(sql); using (IDbCommand cmd = BuildCommand(sql)) { try { return cmd.ExecuteNonQuery(); } catch (Exception ex) { Logger.Warn(ex.Message); throw; } } } private IDbCommand BuildCommand(string sql) { IDbCommand cmd = _connection.CreateCommand(); cmd.CommandText = sql; cmd.CommandType = CommandType.Text; if (_transaction != null) { cmd.Transaction = _transaction; } return cmd; } /// /// Execute an SQL query returning results. /// /// The SQL command. /// A data iterator, IDataReader. public IDataReader ExecuteQuery(string sql) { Logger.Trace(sql); using (IDbCommand cmd = BuildCommand(sql)) { try { return cmd.ExecuteReader(); } catch { Logger.Warn("query failed: {0}", cmd.CommandText); throw; } } } public object ExecuteScalar(string sql) { Logger.Trace(sql); using (IDbCommand cmd = BuildCommand(sql)) { try { return cmd.ExecuteScalar(); } catch { Logger.Warn("Query failed: {0}", cmd.CommandText); throw; } } } public IDataReader Select(string what, string from) { return Select(what, from, "1=1"); } public virtual IDataReader Select(string what, string from, string where) { return ExecuteQuery(String.Format("SELECT {0} FROM {1} WHERE {2}", what, from, where)); } public object SelectScalar(string what, string from) { return SelectScalar(what, from, "1=1"); } public virtual object SelectScalar(string what, string from, string where) { return ExecuteScalar(String.Format("SELECT {0} FROM {1} WHERE {2}", what, from, where)); } public virtual int Update(string table, string[] columns, string[] values) { return Update(table, columns, values, null); } public virtual int Update(string table, string[] columns, string[] values, string where) { string namesAndValues = JoinColumnsAndValues(columns, values); string query = "UPDATE {0} SET {1}"; if (!String.IsNullOrEmpty(where)) { query += " WHERE " + where; } return ExecuteNonQuery(String.Format(query, table, namesAndValues)); } public virtual int Insert(string table, string[] columns, string[] values) { return ExecuteNonQuery(String.Format("INSERT INTO {0} ({1}) VALUES ({2})", table, String.Join(", ", columns), String.Join(", ", QuoteValues(values)))); } public virtual int Delete(string table) { return Delete(table, (string[])null, (string[])null); } public virtual int Delete(string table, string[] columns, string[] values) { if (null == columns || null == values) { return ExecuteNonQuery(String.Format("DELETE FROM {0}", table)); } else { return ExecuteNonQuery(String.Format("DELETE FROM {0} WHERE ({1})", table, JoinColumnsAndValues(columns, values))); } } public virtual int Delete(string table, string wherecolumn, string wherevalue) { return ExecuteNonQuery(String.Format("DELETE FROM {0} WHERE {1} = {2}", table, wherecolumn, QuoteValues(wherevalue))); } /// /// Starts a transaction. Called by the migration mediator. /// public void BeginTransaction() { if (_transaction == null && _connection != null) { EnsureHasConnection(); _transaction = _connection.BeginTransaction(IsolationLevel.ReadCommitted); } } protected void EnsureHasConnection() { if (_connection.State != ConnectionState.Open) { _connection.Open(); } } /// /// Rollback the current migration. Called by the migration mediator. /// public virtual void Rollback() { if (_transaction != null && _connection != null && _connection.State == ConnectionState.Open) { try { _transaction.Rollback(); } finally { _connection.Close(); } } _transaction = null; } /// /// Commit the current transaction. Called by the migrations mediator. /// public void Commit() { if (_transaction != null && _connection != null && _connection.State == ConnectionState.Open) { try { _transaction.Commit(); } finally { _connection.Close(); } } _transaction = null; } /// /// The list of Migrations currently applied to the database. /// public List AppliedMigrations { get { if (_appliedMigrations == null) { _appliedMigrations = new List(); CreateSchemaInfoTable(); using (IDataReader reader = Select("version", "SchemaInfo")) { while (reader.Read()) { _appliedMigrations.Add(Convert.ToInt64(reader.GetValue(0))); } } } return _appliedMigrations; } } /// /// Marks a Migration version number as having been applied /// /// The version number of the migration that was applied public void MigrationApplied(long version) { CreateSchemaInfoTable(); Insert("SchemaInfo", new string[] { "version" }, new string[] { version.ToString() }); _appliedMigrations.Add(version); } /// /// Marks a Migration version number as having been rolled back from the database /// /// The version number of the migration that was removed public void MigrationUnApplied(long version) { CreateSchemaInfoTable(); Delete("SchemaInfo", "version", version.ToString()); _appliedMigrations.Remove(version); } protected void CreateSchemaInfoTable() { EnsureHasConnection(); if (!TableExists("SchemaInfo")) { AddTable("SchemaInfo", new Column("Version", DbType.Int64, ColumnProperty.PrimaryKey)); } } public void AddColumn(string table, Column column) { AddColumn(table, column.Name, column.Type, column.Size, column.ColumnProperty, column.DefaultValue); } public void GenerateForeignKey(string primaryTable, string refTable) { GenerateForeignKey(primaryTable, refTable, ForeignKeyConstraint.NoAction); } public void GenerateForeignKey(string primaryTable, string refTable, ForeignKeyConstraint constraint) { GenerateForeignKey(primaryTable, refTable + "Id", refTable, "Id", constraint); } public IDbCommand GetCommand() { return BuildCommand(null); } public void ExecuteSchemaBuilder(SchemaBuilder builder) { foreach (ISchemaBuilderExpression expr in builder.Expressions) expr.Create(this); } public virtual string QuoteValues(string values) { return QuoteValues(new string[] { values })[0]; } public virtual string[] QuoteValues(string[] values) { return Array.ConvertAll(values, delegate(string val) { if (null == val) return "null"; else return String.Format("'{0}'", val.Replace("'", "''")); }); } public string JoinColumnsAndValues(string[] columns, string[] values) { string[] quotedValues = QuoteValues(values); string[] namesAndValues = new string[columns.Length]; for (int i = 0; i < columns.Length; i++) { namesAndValues[i] = String.Format("{0}={1}", columns[i], quotedValues[i]); } return String.Join(", ", namesAndValues); } public void Dispose() { if (_connection != null && _connection.State == ConnectionState.Open) { _connection.Close(); } } } }