From 0fe77cb654080d3ac4b6384b4e80037a63970b09 Mon Sep 17 00:00:00 2001 From: Russ Kollmansberger Date: Sun, 31 Aug 2025 07:32:30 -0500 Subject: [PATCH] Updates --- ColumnCollection.cs | 59 +++++++++ DbComparer.cs | 10 +- DbMigrate.csproj | 3 + Migration.cs | 16 +++ Migrations.cs | 195 ++++++++++++++++++++++++++++++ SqlDatabase.cs | 289 +++++++++++++++++++++++++------------------- SqlTable.cs | 12 +- 7 files changed, 446 insertions(+), 138 deletions(-) create mode 100644 ColumnCollection.cs create mode 100644 Migration.cs create mode 100644 Migrations.cs diff --git a/ColumnCollection.cs b/ColumnCollection.cs new file mode 100644 index 0000000..00e2c0d --- /dev/null +++ b/ColumnCollection.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DbMigrate { + public class ColumnCollection { + public Dictionary Items { get; private set; } = new Dictionary(); + + public ColumnCollection() { + Items = new Dictionary(); + } + + public string this[string columnName] { + get { + if (Items.ContainsKey(columnName)) { + return Items[columnName]; + } + return null; + } + set { + if (Items.ContainsKey(columnName)) { + Items[columnName] = value; + } else { + Items.Add(columnName, value); + } + } + } + + public void Add(string columnName, string columnDefinition) { + if (!Items.ContainsKey(columnName)) { + Items.Add(columnName, columnDefinition); + } + } + + public bool Contains(string columnName) { + return Items.ContainsKey(columnName); + } + + public string[] GetColumnNames() { + return Items.Keys.ToArray(); + } + + public int Count() { + return Items.Count; + } + + public void Clear() { + Items.Clear(); + } + + public override string ToString() { + StringBuilder sb = new StringBuilder(); + foreach (var kvp in Items) { + sb.AppendLine($"{kvp.Key}: {kvp.Value}"); + } + return sb.ToString(); + } + } +} diff --git a/DbComparer.cs b/DbComparer.cs index 2cabe8a..51bbc5f 100644 --- a/DbComparer.cs +++ b/DbComparer.cs @@ -21,8 +21,8 @@ namespace DbMigrate { private string[] getCommonColumns(string table) { - string[] baseColumns = BaseDatabase[table].Columns.Keys.ToArray(); - string[] newColumns = CompareDatabase[table].Columns.Keys.ToArray(); + string[] baseColumns = BaseDatabase[table].GetColumnNames(); + string[] newColumns = CompareDatabase[table].GetColumnNames(); return baseColumns.Where(f => newColumns.Contains(f)).ToArray(); } @@ -74,13 +74,13 @@ namespace DbMigrate { } bool alterTableRequired = false; - foreach (var column in table.Columns) { - if (!BaseDatabase[table.TableName].HasColumn(column.Key)) { + foreach (var column in table.GetColumnNames()) { + if (!BaseDatabase[table.TableName].HasColumn(column)) { // The database column does not exist alterTableRequired = true; break; } - if (BaseDatabase[table.TableName].Columns[column.Key] != column.Value) { + if (BaseDatabase[table.TableName].Columns[column] != table.Columns[column]) { // The database column exists but is different alterTableRequired = true; break; diff --git a/DbMigrate.csproj b/DbMigrate.csproj index 8ed9ad6..4836d9b 100644 --- a/DbMigrate.csproj +++ b/DbMigrate.csproj @@ -64,8 +64,11 @@ + + + diff --git a/Migration.cs b/Migration.cs new file mode 100644 index 0000000..1dc913a --- /dev/null +++ b/Migration.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace DbMigrate { + public class Migration { + public string MigrationName { get; set; } + public string Sql { get; set; } + public bool Executed { get; set; } = false; + public List Errors { get; set; } = new List(); + + public Migration(string name, string sql) { + MigrationName = name; + Sql = sql; + } + } +} diff --git a/Migrations.cs b/Migrations.cs new file mode 100644 index 0000000..2bcfed2 --- /dev/null +++ b/Migrations.cs @@ -0,0 +1,195 @@ +using Dapper; +using System; +using System.Collections.Generic; +using System.Data.SQLite; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace DbMigrate { + public class Migrations { + public SqlDatabase BaseDatabase { get; set; } + public SqlDatabase CompareDatabase { get; set; } + public string DatabaseName { get; private set; } + + public string MigrationScriptPath { get; set; } = ".\\sql"; + public bool RemoveUnusedTablesFromBaseDb { get; set; } = false; + public bool RemoveUnusedColumnsFromBaseDb { get; set; } = false; + public bool CopyDataFromNewDb { get; set; } = false; + + public string DeltaSql { get; private set; } + public List MigrationSql { get; private set; } = new List(); + + + public Migrations(SqlDatabase baseDb, SqlDatabase compareDb = null) { + BaseDatabase = baseDb; + CompareDatabase = compareDb; + setDatbaseName(); + } + + public Migrations(string baseDbConnectionString, string compareDbConnectionString = null) { + BaseDatabase = new SqlDatabase(baseDbConnectionString); + if (compareDbConnectionString != null) { + CompareDatabase = new SqlDatabase(compareDbConnectionString); + } + setDatbaseName(); + } + + private void setDatbaseName() { + string fullPath = Regex.Match(BaseDatabase.ConnectionString, "Data Source=(.*?);", RegexOptions.IgnoreCase)?.Groups[1]?.Value; + if (fullPath != null) { + DatabaseName = Path.GetFileNameWithoutExtension(fullPath); + } + } + + public async Task PerformMigration(bool performDelta = true) { + if (!Directory.Exists(MigrationScriptPath)) { + Directory.CreateDirectory(MigrationScriptPath); + } + if (BaseDatabase == null || string.IsNullOrEmpty(BaseDatabase.ConnectionString) || string.IsNullOrEmpty(DatabaseName)) { + throw new InvalidOperationException("Base Database is not set"); + } + + using (SQLiteConnection cn = new SQLiteConnection(BaseDatabase.ConnectionString)) { + if (performDelta) { + // Generate the delta SQL + string deltaSql = GetDeltaSql(); + + // Update the MigrationSql property + MigrationSql.Add(new Migration(DatabaseName.ToLower() + "_delta_" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".sql", deltaSql)); + } + + string[] completedMigrations = File.ReadAllLines(Path.Combine(MigrationScriptPath, "completed.txt")); + var migrationFiles = Directory.GetFiles(MigrationScriptPath, DatabaseName + "_*.sql") + .Where(f => !completedMigrations.Contains(Path.GetFileName(f))) + .OrderBy(f => f); + + // Read each migration file and add it to the MigrationSql list + foreach (var file in migrationFiles) { + string sql = File.ReadAllText(file); + // Add this migration to the list + MigrationSql.Add(new Migration(Path.GetFileName(file), sql)); + } + + foreach (var migration in MigrationSql) { + try { + // Execute the migration + await cn.ExecuteAsync(migration.Sql); + migration.Executed = true; + // Mark this migration as completed by adding it to the completed.txt file + File.AppendAllLines(Path.Combine(MigrationScriptPath, "completed.txt"), new[] { migration.MigrationName }); + // TODO : Report progress + } catch (Exception ex) { + migration.Errors.Add(ex); + } + } + } + + } + + public string GetDeltaSql() { + if (CompareDatabase == null) { + throw new InvalidOperationException("Compare Database is not set"); + } + if (BaseDatabase == null) { + throw new InvalidOperationException("Base Database is not set"); + } + + var sb = new StringBuilder(); + + foreach (var table in CompareDatabase.Tables) { + if (!BaseDatabase.ContainsTable(table.TableName)) { + // Table does not exist; Create it + sb.AppendLine("\r\n-- Create table " + table.TableName); + sb.Append(table.CreateTableSql); + + if (CopyDataFromNewDb) { + // Copy data + sb.AppendLine("\r\n-- Copy data into new table " + table.TableName); + // TODO : Copy data from existing table in the new schema + } + continue; + } + + // The table exists, now we need to compare everything to verify no updates are required! + foreach (var index in table.Indexes) { + if (!BaseDatabase[table.TableName].HasIndex(index.Key)) { + sb.AppendLine("\r\n-- Create index " + index.Key); + sb.AppendLine(index.Value); + } else { + // Index exists, check if it's the same + if (BaseDatabase[table.TableName].Indexes[index.Key] != index.Value) { + sb.AppendLine("\r\n-- Drop and recreate index " + index.Key); + sb.AppendLine("DROP INDEX IF EXISTS " + index.Key + ";"); + sb.AppendLine(index.Value); + } + } + } + + foreach (var trigger in table.Triggers) { + if (!BaseDatabase[table.TableName].HasTrigger(trigger.Key)) { + sb.AppendLine("\r\n-- Create trigger " + trigger.Key); + sb.AppendLine(trigger.Value); + } else { + // Trigger exists, check if it's the same + if (BaseDatabase[table.TableName].Triggers[trigger.Key] != trigger.Value) { + sb.AppendLine("\r\n-- Drop and recreate trigger " + trigger.Key); + sb.AppendLine("DROP TRIGGER IF EXISTS " + trigger.Key + ";"); + sb.AppendLine(trigger.Value); + } + } + } + + bool alterTableRequired = false; + foreach (var column in table.GetColumnNames()) { + if (!BaseDatabase[table.TableName].HasColumn(column)) { + // The database column does not exist + alterTableRequired = true; + break; + } + if (BaseDatabase[table.TableName].Columns[column] != table.Columns[column]) { + // The database column exists but is different + alterTableRequired = true; + break; + } + } + + // TODO : This deletes unused columns - we should probably make this optional + if (alterTableRequired) { + sb.AppendLine("\r\n-- Table " + table.TableName + " requires alteration - Create temp table and move data"); + string[] commonColumns = getCommonColumns(table.TableName); + string columnList = string.Join(",", commonColumns); + + string sql = CompareDatabase[table.TableName].CreateTableSql; + string newTableName = table.TableName + "_" + DateTime.Now.ToString("yyyyMMddHHmmss"); + sql = Regex.Replace(sql, "CREATE TABLE (\\w*)", "CREATE TABLE IF NOT EXISTS $1_" + newTableName.Substring(newTableName.IndexOf("_") + 1)); + sb.AppendLine(sql); + + sb.AppendLine("\r\n-- Copy data from existing table to new table"); + sb.AppendLine("INSERT INTO " + newTableName + " (" + columnList + ")"); + sb.AppendLine("\tSELECT " + columnList); + sb.AppendLine("\tFROM " + table.TableName + ";"); + sb.AppendLine("\r\n-- Drop existing table"); + sb.AppendLine("DROP TABLE " + table.TableName + ";"); + sb.AppendLine("\r\n-- Rename the new table to replace the old table"); + sb.AppendLine("ALTER TABLE " + newTableName + " RENAME TO " + table.TableName + ";"); + } + } + + + DeltaSql = "BEGIN TRANSACTION;\r\n" + sb.ToString() + "\r\nCOMMIT;"; + return DeltaSql; + } + + + private string[] getCommonColumns(string table) { + string[] baseColumns = BaseDatabase[table].GetColumnNames(); + string[] newColumns = CompareDatabase[table].GetColumnNames(); + + return baseColumns.Where(f => newColumns.Contains(f)).ToArray(); + } + + } +} diff --git a/SqlDatabase.cs b/SqlDatabase.cs index 404f9e5..7d668fc 100644 --- a/SqlDatabase.cs +++ b/SqlDatabase.cs @@ -1,11 +1,11 @@ using Dapper; using System; using System.Collections.Generic; +using System.Data; using System.Data.SQLite; using System.Diagnostics; using System.IO; using System.Linq; -using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -16,161 +16,199 @@ namespace DbMigrate { public List Tables { get; set; } = new List(); - public SqlDatabase() { + public SqlDatabase() { } - } - - public void Connect(string connectionString) { + public SqlDatabase(string connectionString) { ConnectionString = connectionString; - } - public void LoadSql(string sql) { + /// + /// Loads the specified SQL script and initializes an in-memory SQLite database using the script. + /// + /// This method creates a temporary SQLite database file, sets up a connection string, + /// and executes the provided SQL script to initialize the database. The connection string for the database is + /// stored in the property. + /// The SQL script to be executed for creating and populating the database. + /// A representing the asynchronous operation. + public async Task LoadSql(string sql) { SqlScript = sql; - Tables = ParseTablesFromSql(sql).ToList(); + string tempDbFile = Path.GetTempFileName(); + ConnectionString = "Data Source=" + tempDbFile + "; Version=3;"; + BuildDbFromSql(ConnectionString); + await BuildSql(ConnectionString); } - public void LoadSqlFromFile(string fileName) { - if (!File.Exists(fileName)) { - throw new FileNotFoundException("SQL file '" + fileName + "' was not found and could not be loaded."); - } - - string sql = File.ReadAllText(fileName); - LoadSql(sql); - } - - + /// + /// Determines whether a table with the specified name exists in the collection. + /// + /// The name of the table to search for. The comparison is case-insensitive. + /// if a table with the specified name exists; otherwise, . public bool ContainsTable(string tableName) { return Tables.Count(f => f.TableName.ToLower() == tableName.ToLower()) > 0; } + /// + /// Executes the provided SQL script to build or modify a database using the specified connection string. + /// + /// This method opens a connection to the database, executes the SQL script defined in + /// the SqlScript field, and then closes the connection. Ensure that SqlScript contains valid SQL + /// commands appropriate for the database schema. + /// The connection string used to establish a connection to the SQLite database. This must be a valid SQLite + /// connection string. + public void BuildDbFromSql(string dbConnectionString) { + using (SQLiteConnection cn = new SQLiteConnection(dbConnectionString)) { + ConnectionString = dbConnectionString; + using (SQLiteCommand cmd = new SQLiteCommand(cn)) { + cmd.CommandText = SqlScript; + cmd.ExecuteNonQuery(); + } + } + } + /// + /// Gets the with the specified table name, or null if no matching table is found. + /// + /// The name of the table to retrieve. The comparison is case-insensitive. + /// public SqlTable this[string tableName] { get { return Tables.FirstOrDefault(f => f.TableName.ToLower() == tableName.ToLower()); } } - public IEnumerable ParseTablesFromSql(string sql) { - SqlTable table = null; - StringBuilder sb = new StringBuilder(); + //public IEnumerable ParseTablesFromSql(string sql) { + // SqlTable table = null; + // StringBuilder sb = new StringBuilder(); - Dictionary> indexes = new Dictionary>(); - Dictionary> triggers = new Dictionary>(); + // Dictionary> indexes = new Dictionary>(); + // Dictionary> triggers = new Dictionary>(); - string currentElementType = ""; - foreach (string line in Regex.Split(sql, "\\r\\n")) { - if (string.IsNullOrEmpty(line) || line.StartsWith("--")) { - continue; - } + // string currentElementType = ""; + // foreach (string line in Regex.Split(sql, "\\r\\n")) { + // if (string.IsNullOrEmpty(line) || line.StartsWith("--")) { + // continue; + // } - string trimmedLine = line; - if (currentElementType == "trigger" || currentElementType == "index") { - trimmedLine = line.Trim(); - } else { - trimmedLine = line.StartsWith(" ") ? "&&" + line.Trim() : line; - trimmedLine = Regex.Replace(trimmedLine, @"\s+", " "); - trimmedLine = trimmedLine.Replace("&&", "\t"); - } + // string trimmedLine = line; + // if (currentElementType == "trigger" || currentElementType == "index") { + // trimmedLine = line.Trim(); + // } else { + // trimmedLine = line.StartsWith(" ") ? "&&" + line.Trim() : line; + // trimmedLine = Regex.Replace(trimmedLine, @"\s+", " "); + // trimmedLine = trimmedLine.Replace("&&", "\t"); + // } - if (trimmedLine.ToUpper().StartsWith("CREATE TABLE ")) { - // Start a new table - table = new SqlTable(); - sb = new StringBuilder(); - sb.AppendLine(trimmedLine); - currentElementType = "table"; - continue; - } + // if (trimmedLine.ToUpper().StartsWith("CREATE TABLE ")) { + // // Start a new table + // table = new SqlTable(); + // sb = new StringBuilder(); + // sb.AppendLine(trimmedLine); + // currentElementType = "table"; + // continue; + // } - if (trimmedLine.ToUpper().StartsWith("CREATE INDEX ")) { - var matches = Regex.Match(trimmedLine, "CREATE INDEX( IF NOT EXISTS)? (\\w*) ON (\\w*)"); - if (matches.Success) { - sb = new StringBuilder(); - sb.AppendLine(trimmedLine); - currentElementType = "index"; - if (!indexes.ContainsKey(matches.Groups[2].Value.Trim())) { - indexes[matches.Groups[2].Value.Trim()] = new List(); - } - continue; - } - } + // if (trimmedLine.ToUpper().StartsWith("CREATE INDEX ")) { + // var matches = Regex.Match(trimmedLine, "CREATE INDEX( IF NOT EXISTS)? (\\w*) ON (\\w*)"); + // if (matches.Success) { + // sb = new StringBuilder(); + // sb.AppendLine(trimmedLine); + // currentElementType = "index"; + // if (!indexes.ContainsKey(matches.Groups[2].Value.Trim())) { + // indexes[matches.Groups[2].Value.Trim()] = new List(); + // } + // continue; + // } + // } - if (trimmedLine.ToUpper().StartsWith("CREATE TRIGGER ")) { - sb = new StringBuilder(); - sb.AppendLine(trimmedLine); - currentElementType = "trigger"; - continue; - } + // if (trimmedLine.ToUpper().StartsWith("CREATE TRIGGER ")) { + // sb = new StringBuilder(); + // sb.AppendLine(trimmedLine); + // currentElementType = "trigger"; + // continue; + // } - // The element has concluded (may occur on the same line as above) - if (trimmedLine.EndsWith(");") || trimmedLine.EndsWith("END;")) { - sb.AppendLine(trimmedLine); - if (currentElementType == "table" && table != null) { - SqlScript += sb.ToString() + Environment.NewLine + Environment.NewLine; - table.ParseSql(sb.ToString()); - Tables.Add(table); - currentElementType = ""; - yield return table; - } else if (currentElementType == "index") { - string indexSql = sb.ToString().Replace(Environment.NewLine, " "); - SqlScript += indexSql + Environment.NewLine + Environment.NewLine; - var matches = Regex.Match(indexSql, "CREATE INDEX( IF NOT EXISTS)? (\\w*) ON (\\w*)"); - if (matches.Success) { - if (!indexes.ContainsKey(matches.Groups[3].Value.Trim())) { - indexes[matches.Groups[3].Value.Trim()] = new List(); - } - string indexName = matches.Groups[2].Value.Trim(); - string tableName = matches.Groups[3].Value.Trim(); - indexes[tableName].Add(indexName + ";" + indexSql); - } - currentElementType = ""; - } else if (currentElementType == "trigger") { - string triggerSql = sb.ToString().Replace(Environment.NewLine, " "); - SqlScript += triggerSql + Environment.NewLine + Environment.NewLine; - var matches = Regex.Match(triggerSql, "CREATE TRIGGER( IF NOT EXISTS)? (\\w*) ON (\\w*)"); - if (matches.Success) { - if (!triggers.ContainsKey(matches.Groups[3].Value.Trim())) { - triggers[matches.Groups[3].Value.Trim()] = new List(); - } - string triggerName = matches.Groups[2].Value.Trim(); - string tableName = matches.Groups[3].Value.Trim(); - triggers[tableName].Add(triggerName + ";" + triggerSql); - } - currentElementType = ""; - } - continue; - } + // // The element has concluded (may occur on the same line as above) + // if (trimmedLine.EndsWith(");") || trimmedLine.EndsWith("END;")) { + // sb.AppendLine(trimmedLine); + // if (currentElementType == "table" && table != null) { + // SqlScript += sb.ToString() + Environment.NewLine + Environment.NewLine; + // table.ParseSql(sb.ToString()); + // Tables.Add(table); + // currentElementType = ""; + // yield return table; + // } else if (currentElementType == "index") { + // string indexSql = sb.ToString().Replace(Environment.NewLine, " "); + // SqlScript += indexSql + Environment.NewLine + Environment.NewLine; + // var matches = Regex.Match(indexSql, "CREATE INDEX( IF NOT EXISTS)? (\\w*) ON (\\w*)"); + // if (matches.Success) { + // if (!indexes.ContainsKey(matches.Groups[3].Value.Trim())) { + // indexes[matches.Groups[3].Value.Trim()] = new List(); + // } + // string indexName = matches.Groups[2].Value.Trim(); + // string tableName = matches.Groups[3].Value.Trim(); + // indexes[tableName].Add(indexName + ";" + indexSql); + // } + // currentElementType = ""; + // } else if (currentElementType == "trigger") { + // string triggerSql = sb.ToString().Replace(Environment.NewLine, " "); + // SqlScript += triggerSql + Environment.NewLine + Environment.NewLine; + // var matches = Regex.Match(triggerSql, "CREATE TRIGGER( IF NOT EXISTS)? (\\w*) ON (\\w*)"); + // if (matches.Success) { + // if (!triggers.ContainsKey(matches.Groups[3].Value.Trim())) { + // triggers[matches.Groups[3].Value.Trim()] = new List(); + // } + // string triggerName = matches.Groups[2].Value.Trim(); + // string tableName = matches.Groups[3].Value.Trim(); + // triggers[tableName].Add(triggerName + ";" + triggerSql); + // } + // currentElementType = ""; + // } + // continue; + // } - sb.AppendLine(trimmedLine); - } + // sb.AppendLine(trimmedLine); + // } - foreach (string index in indexes.Keys) { - table = Tables.FirstOrDefault(t => t.TableName == index); - if (table != null) { - foreach (string indexSql in indexes[index]) { - var parts = indexSql.Split(new char[] { ';' }, 2); - if (parts.Length == 2) { - table.Indexes[parts[0]] = parts[1]; - } - } - } - } + // foreach (string index in indexes.Keys) { + // table = Tables.FirstOrDefault(t => t.TableName == index); + // if (table != null) { + // foreach (string indexSql in indexes[index]) { + // var parts = indexSql.Split(new char[] { ';' }, 2); + // if (parts.Length == 2) { + // table.Indexes[parts[0]] = parts[1]; + // } + // } + // } + // } - foreach (string trigger in triggers.Keys) { - table = Tables.FirstOrDefault(t => t.TableName == trigger); - if (table != null) { - foreach (string triggerSql in triggers[trigger]) { - var parts = triggerSql.Split(new char[] { ';' }, 2); - if (parts.Length == 2) { - table.Triggers[parts[0]] = parts[1]; - } - } - } - } - } + // foreach (string trigger in triggers.Keys) { + // table = Tables.FirstOrDefault(t => t.TableName == trigger); + // if (table != null) { + // foreach (string triggerSql in triggers[trigger]) { + // var parts = triggerSql.Split(new char[] { ';' }, 2); + // if (parts.Length == 2) { + // table.Triggers[parts[0]] = parts[1]; + // } + // } + // } + // } + //} + /// + /// Generates a SQL script that recreates the database schema, including tables, indexes, and triggers. + /// + /// This method queries the SQLite database schema to retrieve definitions for tables, + /// indexes, and triggers. It then constructs a SQL script that can be used to recreate the schema. The script + /// includes detailed CREATE statements for each table, along with associated indexes and triggers, if any. + /// Tables are processed in alphabetical order, and the "sqlite_sequence" table is excluded from the + /// output. + /// The connection string used to connect to the SQLite database. + /// A value indicating whether to include "IF NOT EXISTS" clauses in the generated SQL script. Defaults to . + /// A representing the asynchronous operation. The task result contains the generated + /// SQL script as a string. public async Task BuildSql(string dbConnectionString, bool includeIfNotExist = false) { using (SQLiteConnection cn = new SQLiteConnection(dbConnectionString)) { string sql = ""; @@ -221,6 +259,7 @@ namespace DbMigrate { sql += tableSql; } + SqlScript = sql; return sql; } } diff --git a/SqlTable.cs b/SqlTable.cs index d05c9d8..4fd69c2 100644 --- a/SqlTable.cs +++ b/SqlTable.cs @@ -7,7 +7,7 @@ namespace DbMigrate { public string TableName { get; set; } public string CreateTableSql { get; set; } public string OriginalSql { get; set; } - public Dictionary Columns { get; set; } + public ColumnCollection Columns { get; set; } public Dictionary Indexes { get; set; } public Dictionary Triggers { get; set; } @@ -24,7 +24,7 @@ namespace DbMigrate { } private void initTable() { - Columns = new Dictionary(); + Columns = new ColumnCollection(); Indexes = new Dictionary(); Triggers = new Dictionary(); CreateTableSql = ""; @@ -54,15 +54,11 @@ namespace DbMigrate { } public string[] GetColumnNames() { - return Columns.Keys.ToArray(); - } - - public string[] GetColumns() { - return Columns.Values.ToArray(); + return Columns.GetColumnNames(); } public bool HasColumn(string columnName) { - return Columns.ContainsKey(columnName); + return Columns.Contains(columnName); } public string[] GetTriggerNames() {