using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using System.Data.SQLite;
using System.Threading;
using System.Text.RegularExpressions;
using System.IO;
using Tango.Logging;
namespace Tango.Synchronization.Conversion
{
///
/// This class is responsible to take a single SQL Server database
/// and convert it to an SQLite database file.
///
/// The class knows how to convert table and index structures only.
public class SqlServerToSQLiteConverter
{
private LogManager LogManager = LogManager.Default;
#region Public Properties
///
/// Gets a value indicating whether this instance is active.
///
/// true if this instance is active; otherwise, false.
public bool IsActive
{
get { return _isActive; }
}
public bool GenerateForeignKeys { get; set; }
public bool GenerateIndexes { get; set; }
#endregion
#region Public Methods
///
/// Cancels the conversion.
///
public void CancelConversion()
{
_cancelled = true;
}
///
/// This method takes as input the connection string to an SQL Server database
/// and creates a corresponding SQLite database file with a schema derived from
/// the SQL Server database.
///
/// The connection string to the SQL Server database.
/// The path to the SQLite database file that needs to get created.
/// The password to use or NULL if no password should be used to encrypt the DB
/// A handler delegate for progress notifications.
/// The selection handler that allows the user to select which
/// tables to convert
/// The method continues asynchronously in the background and the caller returned
/// immediately.
public void ConvertSqlServerToSQLiteDatabase(string sqlServerConnString,
string sqlitePath, string password, SqlConversionHandler handler,
SqlTableSelectionHandler selectionHandler,
FailedViewDefinitionHandler viewFailureHandler,
bool createTriggers, bool createViews)
{
// Clear canceled flag
_cancelled = false;
WaitCallback wc = new WaitCallback(delegate (object state)
{
try
{
_isActive = true;
ConvertSqlServerDatabaseToSQLiteFile(sqlServerConnString, sqlitePath, password, handler, selectionHandler, viewFailureHandler, createTriggers, createViews);
_isActive = false;
handler(true, true, 100, "Finished converting database");
}
catch (Exception ex)
{
LogManager.Log(ex, "Failed to convert SQL Server database to SQLite database");
_isActive = false;
handler(true, false, 100, ex.Message);
} // catch
});
ThreadPool.QueueUserWorkItem(wc);
}
#endregion
#region Private Methods
///
/// Do the entire process of first reading the SQL Server schema, creating a corresponding
/// SQLite schema, and copying all rows from the SQL Server database to the SQLite database.
///
/// The SQL Server connection string
/// The path to the generated SQLite database file
/// The password to use or NULL if no password should be used to encrypt the DB
/// A handler to handle progress notifications.
/// The selection handler which allows the user to select which tables to
/// convert.
private void ConvertSqlServerDatabaseToSQLiteFile(
string sqlConnString, string sqlitePath, string password, SqlConversionHandler handler,
SqlTableSelectionHandler selectionHandler,
FailedViewDefinitionHandler viewFailureHandler,
bool createTriggers, bool createViews)
{
// Delete the target file if it exists already.
if (File.Exists(sqlitePath))
File.Delete(sqlitePath);
// Read the schema of the SQL Server database into a memory structure
DatabaseSchema ds = ReadSqlServerSchema(sqlConnString, handler, selectionHandler);
// Create the SQLite database and apply the schema
CreateSQLiteDatabase(sqlitePath, ds, password, handler, viewFailureHandler, createViews);
// Copy all rows from SQL Server tables to the newly created SQLite database
CopySqlServerRowsToSQLiteDB(sqlConnString, sqlitePath, ds.Tables, password, handler);
// Add triggers based on foreign key constraints
if (createTriggers)
AddTriggersForForeignKeys(sqlitePath, ds.Tables, password, handler);
}
///
/// Copies table rows from the SQL Server database to the SQLite database.
///
/// The SQL Server connection string
/// The path to the SQLite database file.
/// The schema of the SQL Server database.
/// The password to use for encrypting the file
/// A handler to handle progress notifications.
private void CopySqlServerRowsToSQLiteDB(
string sqlConnString, string sqlitePath, List schema,
string password, SqlConversionHandler handler)
{
CheckCancelled();
handler(false, true, 0, "Preparing to insert tables...");
LogManager.Log("preparing to insert tables ...");
// Connect to the SQL Server database
using (SqlConnection ssconn = new SqlConnection(sqlConnString))
{
ssconn.Open();
// Connect to the SQLite database next
string sqliteConnString = CreateSQLiteConnectionString(sqlitePath, password);
using (SQLiteConnection sqconn = new SQLiteConnection(sqliteConnString))
{
sqconn.Open();
// Go over all tables in the schema and copy their rows
for (int i = 0; i < schema.Count; i++)
{
if (schema[i].CopyData)
{
SQLiteTransaction tx = sqconn.BeginTransaction();
try
{
string tableQuery = BuildSqlServerTableQuery(schema[i]);
SqlCommand query = new SqlCommand(tableQuery, ssconn);
using (SqlDataReader reader = query.ExecuteReader())
{
SQLiteCommand insert = BuildSQLiteInsert(schema[i]);
int counter = 0;
while (reader.Read())
{
insert.Connection = sqconn;
insert.Transaction = tx;
List pnames = new List();
for (int j = 0; j < schema[i].Columns.Count; j++)
{
string pname = "@" + GetNormalizedName(schema[i].Columns[j].ColumnName, pnames);
insert.Parameters[pname].Value = CastValueForColumn(reader[j], schema[i].Columns[j]);
pnames.Add(pname);
}
insert.ExecuteNonQuery();
counter++;
if (counter % 1000 == 0)
{
CheckCancelled();
tx.Commit();
handler(false, true, (int)(100.0 * i / schema.Count),
"Added " + counter + " rows to table " + schema[i].TableName + " so far");
tx = sqconn.BeginTransaction();
}
} // while
} // using
CheckCancelled();
tx.Commit();
handler(false, true, (int)(100.0 * i / schema.Count), "Finished inserting rows for table " + schema[i].TableName);
LogManager.Log("finished inserting all rows for table [" + schema[i].TableName + "]");
}
catch (Exception ex)
{
LogManager.Log(ex, "unexpected exception");
tx.Rollback();
throw;
} // catch
}
}
} // using
} // using
}
///
/// Used in order to adjust the value received from SQL Server for the SQLite database.
///
/// The value object
/// The corresponding column schema
/// SQLite adjusted value.
private object CastValueForColumn(object val, ColumnSchema columnSchema)
{
if (val is DBNull)
return null;
DbType dt = GetDbTypeOfColumn(columnSchema);
switch (dt)
{
case DbType.Int32:
if (val is short)
return (int)(short)val;
if (val is byte)
return (int)(byte)val;
if (val is long)
return (int)(long)val;
if (val is decimal)
return (int)(decimal)val;
break;
case DbType.Int16:
if (val is int)
return (short)(int)val;
if (val is byte)
return (short)(byte)val;
if (val is long)
return (short)(long)val;
if (val is decimal)
return (short)(decimal)val;
break;
case DbType.Int64:
if (val is int)
return (long)(int)val;
if (val is short)
return (long)(short)val;
if (val is byte)
return (long)(byte)val;
if (val is decimal)
return (long)(decimal)val;
break;
case DbType.Single:
if (val is double)
return (float)(double)val;
if (val is decimal)
return (float)(decimal)val;
break;
case DbType.Double:
if (val is float)
return (double)(float)val;
if (val is double)
return (double)val;
if (val is decimal)
return (double)(decimal)val;
break;
case DbType.String:
if (val is Guid)
return ((Guid)val).ToString();
break;
case DbType.Guid:
if (val is string)
return ParseStringAsGuid((string)val);
if (val is byte[])
return ParseBlobAsGuid((byte[])val);
break;
case DbType.Binary:
case DbType.Boolean:
case DbType.DateTime:
break;
default:
LogManager.Log(new ArgumentException("argument exception - illegal database type"));
throw new ArgumentException("Illegal database type [" + Enum.GetName(typeof(DbType), dt) + "]");
} // switch
return val;
}
private Guid ParseBlobAsGuid(byte[] blob)
{
byte[] data = blob;
if (blob.Length > 16)
{
data = new byte[16];
for (int i = 0; i < 16; i++)
data[i] = blob[i];
}
else if (blob.Length < 16)
{
data = new byte[16];
for (int i = 0; i < blob.Length; i++)
data[i] = blob[i];
}
return new Guid(data);
}
private Guid ParseStringAsGuid(string str)
{
try
{
return new Guid(str);
}
catch
{
return Guid.Empty;
}
}
///
/// Creates a command object needed to insert values into a specific SQLite table.
///
/// The table schema object for the table.
/// A command object with the required functionality.
private SQLiteCommand BuildSQLiteInsert(TableSchema ts)
{
SQLiteCommand res = new SQLiteCommand();
StringBuilder sb = new StringBuilder();
sb.Append("INSERT INTO [" + ts.TableName + "] (");
for (int i = 0; i < ts.Columns.Count; i++)
{
sb.Append("[" + ts.Columns[i].ColumnName + "]");
if (i < ts.Columns.Count - 1)
sb.Append(", ");
} // for
sb.Append(") VALUES (");
List pnames = new List();
for (int i = 0; i < ts.Columns.Count; i++)
{
string pname = "@" + GetNormalizedName(ts.Columns[i].ColumnName, pnames);
sb.Append(pname);
if (i < ts.Columns.Count - 1)
sb.Append(", ");
DbType dbType = GetDbTypeOfColumn(ts.Columns[i]);
SQLiteParameter prm = new SQLiteParameter(pname, dbType, ts.Columns[i].ColumnName);
res.Parameters.Add(prm);
// Remember the parameter name in order to avoid duplicates
pnames.Add(pname);
} // for
sb.Append(")");
res.CommandText = sb.ToString();
res.CommandType = CommandType.Text;
return res;
}
///
/// Used in order to avoid breaking naming rules (e.g., when a table has
/// a name in SQL Server that cannot be used as a basis for a matching index
/// name in SQLite).
///
/// The name to change if necessary
/// Used to avoid duplicate names
/// A normalized name
private string GetNormalizedName(string str, List names)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < str.Length; i++)
{
if (Char.IsLetterOrDigit(str[i]) || str[i] == '_')
sb.Append(str[i]);
else
sb.Append("_");
} // for
// Avoid returning duplicate name
if (names.Contains(sb.ToString()))
return GetNormalizedName(sb.ToString() + "_", names);
else
return sb.ToString();
}
///
/// Matches SQL Server types to general DB types
///
/// The column schema to use for the match
/// The matched DB type
private DbType GetDbTypeOfColumn(ColumnSchema cs)
{
if (cs.ColumnType == "tinyint")
return DbType.Byte;
if (cs.ColumnType == "int")
return DbType.Int32;
if (cs.ColumnType == "smallint")
return DbType.Int16;
if (cs.ColumnType == "bigint")
return DbType.Int64;
if (cs.ColumnType == "bit")
return DbType.Boolean;
if (cs.ColumnType == "nvarchar" || cs.ColumnType == "varchar" ||
cs.ColumnType == "text" || cs.ColumnType == "ntext")
return DbType.String;
if (cs.ColumnType == "float")
return DbType.Double;
if (cs.ColumnType == "real")
return DbType.Single;
if (cs.ColumnType == "blob")
return DbType.Binary;
if (cs.ColumnType == "numeric")
return DbType.Double;
if (cs.ColumnType == "timestamp" || cs.ColumnType == "datetime" || cs.ColumnType == "datetime2" || cs.ColumnType == "date" || cs.ColumnType == "time")
return DbType.DateTime;
if (cs.ColumnType == "nchar" || cs.ColumnType == "char")
return DbType.String;
if (cs.ColumnType == "uniqueidentifier" || cs.ColumnType == "guid")
return DbType.Guid;
if (cs.ColumnType == "xml")
return DbType.String;
if (cs.ColumnType == "sql_variant")
return DbType.Object;
if (cs.ColumnType == "integer")
return DbType.Int64;
LogManager.Log("illegal db type found");
throw new ApplicationException("Illegal DB type found (" + cs.ColumnType + ")");
}
///
/// Builds a SELECT query for a specific table. Needed in the process of copying rows
/// from the SQL Server database to the SQLite database.
///
/// The table schema of the table for which we need the query.
/// The SELECT query for the table.
private string BuildSqlServerTableQuery(TableSchema ts)
{
StringBuilder sb = new StringBuilder();
sb.Append("SELECT ");
for (int i = 0; i < ts.Columns.Count; i++)
{
sb.Append("[" + ts.Columns[i].ColumnName + "]");
if (i < ts.Columns.Count - 1)
sb.Append(", ");
} // for
sb.Append(" FROM " + ts.TableSchemaName + "." + "[" + ts.TableName + "]");
return sb.ToString();
}
///
/// Creates the SQLite database from the schema read from the SQL Server.
///
/// The path to the generated DB file.
/// The schema of the SQL server database.
/// The password to use for encrypting the DB or null if non is needed.
/// A handle for progress notifications.
private void CreateSQLiteDatabase(string sqlitePath, DatabaseSchema schema, string password,
SqlConversionHandler handler,
FailedViewDefinitionHandler viewFailureHandler, bool createViews)
{
LogManager.Log("Creating SQLite database...");
// Create the SQLite database file
SQLiteConnection.CreateFile(sqlitePath);
LogManager.Log("SQLite file was created successfully at [" + sqlitePath + "]");
// Connect to the newly created database
string sqliteConnString = CreateSQLiteConnectionString(sqlitePath, password);
using (SQLiteConnection conn = new SQLiteConnection(sqliteConnString))
{
conn.Open();
// Create all tables in the new database
int count = 0;
foreach (TableSchema dt in schema.Tables)
{
try
{
AddSQLiteTable(conn, dt);
}
catch (Exception ex)
{
LogManager.Log(ex, "AddSQLiteTable failed");
throw;
}
count++;
CheckCancelled();
handler(false, true, (int)(count * 50.0 / schema.Tables.Count), "Added table " + dt.TableName + " to the SQLite database");
LogManager.Log("added schema for SQLite table [" + dt.TableName + "]");
} // foreach
// Create all views in the new database
count = 0;
if (createViews)
{
foreach (ViewSchema vs in schema.Views)
{
try
{
AddSQLiteView(conn, vs, viewFailureHandler);
}
catch (Exception ex)
{
LogManager.Log(ex, "AddSQLiteView failed");
throw;
} // catch
count++;
CheckCancelled();
handler(false, true, 50 + (int)(count * 50.0 / schema.Views.Count), "Added view " + vs.ViewName + " to the SQLite database");
LogManager.Log("added schema for SQLite view [" + vs.ViewName + "]");
} // foreach
} // if
} // using
LogManager.Log("finished adding all table/view schemas for SQLite database");
}
private void AddSQLiteView(SQLiteConnection conn, ViewSchema vs, FailedViewDefinitionHandler handler)
{
// Prepare a CREATE VIEW DDL statement
string stmt = vs.ViewSQL;
LogManager.Log("\n\n" + stmt + "\n\n");
// Execute the query in order to actually create the view.
SQLiteTransaction tx = conn.BeginTransaction();
try
{
SQLiteCommand cmd = new SQLiteCommand(stmt, conn, tx);
cmd.ExecuteNonQuery();
tx.Commit();
}
catch (SQLiteException)
{
tx.Rollback();
if (handler != null)
{
ViewSchema updated = new ViewSchema();
updated.ViewName = vs.ViewName;
updated.ViewSQL = vs.ViewSQL;
// Ask the user to supply the new view definition SQL statement
string sql = handler(updated);
if (sql == null)
return; // Discard the view
else
{
// Try to re-create the view with the user-supplied view definition SQL
updated.ViewSQL = sql;
AddSQLiteView(conn, updated, handler);
}
}
else
throw;
} // catch
}
///
/// Creates the CREATE TABLE DDL for SQLite and a specific table.
///
/// The SQLite connection
/// The table schema object for the table to be generated.
private void AddSQLiteTable(SQLiteConnection conn, TableSchema dt)
{
// Prepare a CREATE TABLE DDL statement
string stmt = BuildCreateTableQuery(dt);
LogManager.Log("\n\n" + stmt + "\n\n");
// Execute the query in order to actually create the table.
SQLiteCommand cmd = new SQLiteCommand(stmt, conn);
cmd.ExecuteNonQuery();
}
///
/// returns the CREATE TABLE DDL for creating the SQLite table from the specified
/// table schema object.
///
/// The table schema object from which to create the SQL statement.
/// CREATE TABLE DDL for the specified table.
private string BuildCreateTableQuery(TableSchema ts)
{
StringBuilder sb = new StringBuilder();
sb.Append("CREATE TABLE [" + ts.TableName + "] (\n");
bool pkey = false;
for (int i = 0; i < ts.Columns.Count; i++)
{
ColumnSchema col = ts.Columns[i];
string cline = BuildColumnStatement(col, ts, ref pkey);
sb.Append(cline);
if (i < ts.Columns.Count - 1)
sb.Append(",\n");
} // foreach
// add primary keys...
if (ts.PrimaryKey != null && ts.PrimaryKey.Count > 0 & !pkey)
{
sb.Append(",\n");
sb.Append(" PRIMARY KEY (");
for (int i = 0; i < ts.PrimaryKey.Count; i++)
{
sb.Append("[" + ts.PrimaryKey[i] + "]");
if (i < ts.PrimaryKey.Count - 1)
sb.Append(", ");
} // for
sb.Append(")\n");
}
else
sb.Append("\n");
if (GenerateForeignKeys)
{
// add foreign keys...
if (ts.ForeignKeys.Count > 0)
{
sb.Append(",\n");
for (int i = 0; i < ts.ForeignKeys.Count; i++)
{
ForeignKeySchema foreignKey = ts.ForeignKeys[i];
string stmt = string.Format(" FOREIGN KEY ([{0}])\n REFERENCES [{1}]([{2}])",
foreignKey.ColumnName, foreignKey.ForeignTableName, foreignKey.ForeignColumnName);
sb.Append(stmt);
if (i < ts.ForeignKeys.Count - 1)
sb.Append(",\n");
} // for
}
}
sb.Append("\n");
sb.Append(");\n");
// Create any relevant indexes
if (GenerateIndexes)
{
if (ts.Indexes != null)
{
for (int i = 0; i < ts.Indexes.Count; i++)
{
string stmt = BuildCreateIndex(ts.TableName, ts.Indexes[i]);
sb.Append(stmt + ";\n");
} // for
} // if
}
string query = sb.ToString();
return query;
}
///
/// Creates a CREATE INDEX DDL for the specified table and index schema.
///
/// The name of the indexed table.
/// The schema of the index object
/// A CREATE INDEX DDL (SQLite format).
private string BuildCreateIndex(string tableName, IndexSchema indexSchema)
{
StringBuilder sb = new StringBuilder();
sb.Append("CREATE ");
if (indexSchema.IsUnique)
sb.Append("UNIQUE ");
sb.Append("INDEX [" + tableName + "_" + indexSchema.IndexName + "]\n");
sb.Append("ON [" + tableName + "]\n");
sb.Append("(");
for (int i = 0; i < indexSchema.Columns.Count; i++)
{
sb.Append("[" + indexSchema.Columns[i].ColumnName + "]");
if (!indexSchema.Columns[i].IsAscending)
sb.Append(" DESC");
if (i < indexSchema.Columns.Count - 1)
sb.Append(", ");
} // for
sb.Append(")");
return sb.ToString();
}
///
/// Used when creating the CREATE TABLE DDL. Creates a single row
/// for the specified column.
///
/// The column schema
/// A single column line to be inserted into the general CREATE TABLE DDL statement
private string BuildColumnStatement(ColumnSchema col, TableSchema ts, ref bool pkey)
{
StringBuilder sb = new StringBuilder();
sb.Append("\t[" + col.ColumnName + "]\t");
// Special treatment for IDENTITY columns
if (col.IsIdentity)
{
if (ts.PrimaryKey.Count == 1 && (col.ColumnType == "tinyint" || col.ColumnType == "int" || col.ColumnType == "smallint" ||
col.ColumnType == "bigint" || col.ColumnType == "integer"))
{
sb.Append("integer PRIMARY KEY AUTOINCREMENT");
pkey = true;
}
else
sb.Append("integer");
}
else
{
if (col.ColumnType == "int")
sb.Append("integer");
else
{
sb.Append(col.ColumnType);
}
if (col.Length > 0)
sb.Append("(" + col.Length + ")");
}
if (!col.IsNullable)
sb.Append(" NOT NULL");
if (col.IsCaseSensitivite.HasValue && !col.IsCaseSensitivite.Value)
sb.Append(" COLLATE NOCASE");
string defval = StripParens(col.DefaultValue);
defval = DiscardNational(defval);
LogManager.Log("DEFAULT VALUE BEFORE [" + col.DefaultValue + "] AFTER [" + defval + "]");
if (defval != string.Empty && defval.ToUpper().Contains("GETDATE"))
{
LogManager.Log("converted SQL Server GETDATE() to CURRENT_TIMESTAMP for column [" + col.ColumnName + "]");
sb.Append(" DEFAULT (CURRENT_TIMESTAMP)");
}
else if (defval != string.Empty && IsValidDefaultValue(defval))
sb.Append(" DEFAULT " + defval);
return sb.ToString();
}
///
/// Discards the national prefix if exists (e.g., N'sometext') which is not
/// supported in SQLite.
///
/// The value.
///
private string DiscardNational(string value)
{
Regex rx = new Regex(@"N\'([^\']*)\'");
Match m = rx.Match(value);
if (m.Success)
return m.Groups[1].Value;
else
return value;
}
///
/// Check if the DEFAULT clause is valid by SQLite standards
///
///
///
private bool IsValidDefaultValue(string value)
{
if (IsSingleQuoted(value))
return true;
double testnum;
if (!double.TryParse(value, out testnum))
return false;
return true;
}
private bool IsSingleQuoted(string value)
{
value = value.Trim();
if (value.StartsWith("'") && value.EndsWith("'"))
return true;
return false;
}
///
/// Strip any parentheses from the string.
///
/// The string to strip
/// The stripped string
private string StripParens(string value)
{
Regex rx = new Regex(@"\(([^\)]*)\)");
Match m = rx.Match(value);
if (!m.Success)
return value;
else
return StripParens(m.Groups[1].Value);
}
///
/// Reads the entire SQL Server DB schema using the specified connection string.
///
/// The connection string used for reading SQL Server schema.
/// A handler for progress notifications.
/// The selection handler which allows the user to select
/// which tables to convert.
/// database schema objects for every table/view in the SQL Server database.
private DatabaseSchema ReadSqlServerSchema(string connString, SqlConversionHandler handler,
SqlTableSelectionHandler selectionHandler)
{
// First step is to read the names of all tables in the database
List tables = new List();
using (SqlConnection conn = new SqlConnection(connString))
{
conn.Open();
List tableNames = new List();
List tblschema = new List();
// This command will read the names of all tables in the database
SqlCommand cmd = new SqlCommand(@"select * from INFORMATION_SCHEMA.TABLES where TABLE_TYPE = 'BASE TABLE'", conn);
using (SqlDataReader reader = cmd.ExecuteReader())
{
while (reader.Read())
{
if (reader["TABLE_NAME"] == DBNull.Value)
continue;
if (reader["TABLE_SCHEMA"] == DBNull.Value)
continue;
tableNames.Add((string)reader["TABLE_NAME"]);
tblschema.Add((string)reader["TABLE_SCHEMA"]);
} // while
} // using
// Next step is to use ADO APIs to query the schema of each table.
int count = 0;
for (int i = 0; i < tableNames.Count; i++)
{
string tname = tableNames[i];
string tschma = tblschema[i];
TableSchema ts = CreateTableSchema(conn, tname, tschma);
CreateForeignKeySchema(conn, ts);
tables.Add(ts);
count++;
CheckCancelled();
handler(false, true, (int)(count * 50.0 / tableNames.Count), "Parsed table " + tname);
LogManager.Log("parsed table schema for [" + tname + "]");
} // foreach
} // using
LogManager.Log("finished parsing all tables in SQL Server schema");
// Allow the user a chance to select which tables to convert
if (selectionHandler != null)
{
List updated = selectionHandler(tables);
if (updated != null)
tables = updated;
} // if
Regex removedbo = new Regex(@"dbo\.", RegexOptions.Compiled | RegexOptions.IgnoreCase);
// Continue and read all of the views in the database
List views = new List();
using (SqlConnection conn = new SqlConnection(connString))
{
conn.Open();
SqlCommand cmd = new SqlCommand(@"SELECT TABLE_NAME, VIEW_DEFINITION from INFORMATION_SCHEMA.VIEWS", conn);
using (SqlDataReader reader = cmd.ExecuteReader())
{
int count = 0;
while (reader.Read())
{
ViewSchema vs = new ViewSchema();
if (reader["TABLE_NAME"] == DBNull.Value)
continue;
if (reader["VIEW_DEFINITION"] == DBNull.Value)
continue;
vs.ViewName = (string)reader["TABLE_NAME"];
vs.ViewSQL = (string)reader["VIEW_DEFINITION"];
// Remove all ".dbo" strings from the view definition
vs.ViewSQL = removedbo.Replace(vs.ViewSQL, string.Empty);
views.Add(vs);
count++;
CheckCancelled();
handler(false, true, 50 + (int)(count * 50.0 / views.Count), "Parsed view " + vs.ViewName);
LogManager.Log("parsed view schema for [" + vs.ViewName + "]");
} // while
} // using
} // using
DatabaseSchema ds = new DatabaseSchema();
ds.Tables = tables;
ds.Views = views;
return ds;
}
///
/// Convenience method for checking if the conversion progress needs to be cancelled.
///
private void CheckCancelled()
{
if (_cancelled)
throw new ApplicationException("User cancelled the conversion");
}
///
/// Creates a TableSchema object using the specified SQL Server connection
/// and the name of the table for which we need to create the schema.
///
/// The SQL Server connection to use
/// The name of the table for which we wants to create the table schema.
/// A table schema object that represents our knowledge of the table schema
private TableSchema CreateTableSchema(SqlConnection conn, string tableName, string tschma)
{
TableSchema res = new TableSchema();
res.TableName = tableName;
res.TableSchemaName = tschma;
res.Columns = new List();
SqlCommand cmd = new SqlCommand(@"SELECT COLUMN_NAME,COLUMN_DEFAULT,IS_NULLABLE,DATA_TYPE, " +
@" (columnproperty(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity')) AS [IDENT], " +
@"CHARACTER_MAXIMUM_LENGTH AS CSIZE " +
"FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '" + tableName + "' ORDER BY " +
"ORDINAL_POSITION ASC", conn);
using (SqlDataReader reader = cmd.ExecuteReader())
{
while (reader.Read())
{
object tmp = reader["COLUMN_NAME"];
if (tmp is DBNull)
continue;
string colName = (string)reader["COLUMN_NAME"];
tmp = reader["COLUMN_DEFAULT"];
string colDefault;
if (tmp is DBNull)
colDefault = string.Empty;
else
colDefault = (string)tmp;
tmp = reader["IS_NULLABLE"];
bool isNullable = ((string)tmp == "YES");
string dataType = (string)reader["DATA_TYPE"];
bool isIdentity = false;
if (reader["IDENT"] != DBNull.Value)
isIdentity = ((int)reader["IDENT"]) == 1 ? true : false;
int length = reader["CSIZE"] != DBNull.Value ? Convert.ToInt32(reader["CSIZE"]) : 0;
ValidateDataType(dataType);
// Note that not all data type names need to be converted because
// SQLite establishes type affinity by searching certain strings
// in the type name. For example - everything containing the string
// 'int' in its type name will be assigned an INTEGER affinity
if (dataType == "timestamp")
dataType = "blob";
else if (dataType == "datetime" || dataType == "smalldatetime" || dataType == "date" || dataType == "datetime2" || dataType == "time")
dataType = "datetime";
else if (dataType == "decimal")
dataType = "numeric";
else if (dataType == "money" || dataType == "smallmoney")
dataType = "numeric";
else if (dataType == "binary" || dataType == "varbinary" ||
dataType == "image")
dataType = "blob";
else if (dataType == "tinyint")
dataType = "smallint";
else if (dataType == "bigint")
dataType = "integer";
else if (dataType == "sql_variant")
dataType = "blob";
else if (dataType == "xml")
dataType = "varchar";
else if (dataType == "uniqueidentifier")
dataType = "guid";
else if (dataType == "ntext")
dataType = "text";
else if (dataType == "nchar")
dataType = "char";
if (dataType == "bit" || dataType == "int")
{
if (colDefault == "('False')")
colDefault = "(0)";
else if (colDefault == "('True')")
colDefault = "(1)";
}
colDefault = FixDefaultValueString(colDefault);
ColumnSchema col = new ColumnSchema();
col.ColumnName = colName;
col.ColumnType = dataType;
col.Length = length;
col.IsNullable = isNullable;
col.IsIdentity = isIdentity;
col.DefaultValue = AdjustDefaultValue(colDefault);
res.Columns.Add(col);
} // while
} // using
// Find PRIMARY KEY information
SqlCommand cmd2 = new SqlCommand(@"EXEC sp_pkeys '" + tableName + "'", conn);
using (SqlDataReader reader = cmd2.ExecuteReader())
{
res.PrimaryKey = new List();
while (reader.Read())
{
string colName = (string)reader["COLUMN_NAME"];
res.PrimaryKey.Add(colName);
} // while
} // using
// Find COLLATE information for all columns in the table
SqlCommand cmd4 = new SqlCommand(
@"EXEC sp_tablecollations '" + tschma + "." + tableName + "'", conn);
using (SqlDataReader reader = cmd4.ExecuteReader())
{
while (reader.Read())
{
bool? isCaseSensitive = null;
string colName = (string)reader["name"];
if (reader["tds_collation"] != DBNull.Value)
{
byte[] mask = (byte[])reader["tds_collation"];
if ((mask[2] & 0x10) != 0)
isCaseSensitive = false;
else
isCaseSensitive = true;
} // if
if (isCaseSensitive.HasValue)
{
// Update the corresponding column schema.
foreach (ColumnSchema csc in res.Columns)
{
if (csc.ColumnName == colName)
{
csc.IsCaseSensitivite = isCaseSensitive;
break;
}
} // foreach
} // if
} // while
} // using
try
{
// Find index information
SqlCommand cmd3 = new SqlCommand(
@"exec sp_helpindex '" + tschma + "." + tableName + "'", conn);
using (SqlDataReader reader = cmd3.ExecuteReader())
{
res.Indexes = new List();
while (reader.Read())
{
string indexName = (string)reader["index_name"];
string desc = (string)reader["index_description"];
string keys = (string)reader["index_keys"];
// Don't add the index if it is actually a primary key index
if (desc.Contains("primary key"))
continue;
IndexSchema index = BuildIndexSchema(indexName, desc, keys);
res.Indexes.Add(index);
} // while
} // using
}
catch (Exception)
{
LogManager.Log("failed to read index information for table [" + tableName + "]");
}
return res;
}
///
/// Small validation method to make sure we don't miss anything without getting
/// an exception.
///
/// The datatype to validate.
private void ValidateDataType(string dataType)
{
if (dataType == "int" || dataType == "smallint" ||
dataType == "bit" || dataType == "float" ||
dataType == "real" || dataType == "nvarchar" ||
dataType == "varchar" || dataType == "timestamp" ||
dataType == "varbinary" || dataType == "image" ||
dataType == "text" || dataType == "ntext" ||
dataType == "bigint" ||
dataType == "char" || dataType == "numeric" ||
dataType == "binary" || dataType == "smalldatetime" ||
dataType == "smallmoney" || dataType == "money" ||
dataType == "tinyint" || dataType == "uniqueidentifier" ||
dataType == "xml" || dataType == "sql_variant" || dataType == "datetime2" || dataType == "date" || dataType == "time" ||
dataType == "decimal" || dataType == "nchar" || dataType == "datetime")
return;
throw new ApplicationException("Validation failed for data type [" + dataType + "]");
}
///
/// Does some necessary adjustments to a value string that appears in a column DEFAULT
/// clause.
///
/// The original default value string (as read from SQL Server).
/// Adjusted DEFAULT value string (for SQLite)
private string FixDefaultValueString(string colDefault)
{
bool replaced = false;
string res = colDefault.Trim();
// Find first/last indexes in which to search
int first = -1;
int last = -1;
for (int i = 0; i < res.Length; i++)
{
if (res[i] == '\'' && first == -1)
first = i;
if (res[i] == '\'' && first != -1 && i > last)
last = i;
} // for
if (first != -1 && last > first)
return res.Substring(first, last - first + 1);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < res.Length; i++)
{
if (res[i] != '(' && res[i] != ')')
{
sb.Append(res[i]);
replaced = true;
}
}
if (replaced)
return "(" + sb.ToString() + ")";
else
return sb.ToString();
}
///
/// Add foreign key schema object from the specified components (Read from SQL Server).
///
/// The SQL Server connection to use
/// The table schema to whom foreign key schema should be added to
private void CreateForeignKeySchema(SqlConnection conn, TableSchema ts)
{
ts.ForeignKeys = new List();
SqlCommand cmd = new SqlCommand(
@"SELECT " +
@" ColumnName = CU.COLUMN_NAME, " +
@" ForeignTableName = PK.TABLE_NAME, " +
@" ForeignColumnName = PT.COLUMN_NAME, " +
@" DeleteRule = C.DELETE_RULE, " +
@" IsNullable = COL.IS_NULLABLE " +
@"FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS C " +
@"INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS FK ON C.CONSTRAINT_NAME = FK.CONSTRAINT_NAME " +
@"INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS PK ON C.UNIQUE_CONSTRAINT_NAME = PK.CONSTRAINT_NAME " +
@"INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CU ON C.CONSTRAINT_NAME = CU.CONSTRAINT_NAME " +
@"INNER JOIN " +
@" ( " +
@" SELECT i1.TABLE_NAME, i2.COLUMN_NAME " +
@" FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS i1 " +
@" INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE i2 ON i1.CONSTRAINT_NAME = i2.CONSTRAINT_NAME " +
@" WHERE i1.CONSTRAINT_TYPE = 'PRIMARY KEY' " +
@" ) " +
@"PT ON PT.TABLE_NAME = PK.TABLE_NAME " +
@"INNER JOIN INFORMATION_SCHEMA.COLUMNS AS COL ON CU.COLUMN_NAME = COL.COLUMN_NAME AND FK.TABLE_NAME = COL.TABLE_NAME " +
@"WHERE FK.Table_NAME='" + ts.TableName + "'", conn);
using (SqlDataReader reader = cmd.ExecuteReader())
{
while (reader.Read())
{
ForeignKeySchema fkc = new ForeignKeySchema();
fkc.ColumnName = (string)reader["ColumnName"];
fkc.ForeignTableName = (string)reader["ForeignTableName"];
fkc.ForeignColumnName = (string)reader["ForeignColumnName"];
fkc.CascadeOnDelete = (string)reader["DeleteRule"] == "CASCADE";
fkc.IsNullable = (string)reader["IsNullable"] == "YES";
fkc.TableName = ts.TableName;
ts.ForeignKeys.Add(fkc);
}
}
}
///
/// Builds an index schema object from the specified components (Read from SQL Server).
///
/// The name of the index
/// The description of the index
/// Key columns that are part of the index.
/// An index schema object that represents our knowledge of the index
private IndexSchema BuildIndexSchema(string indexName, string desc, string keys)
{
IndexSchema res = new IndexSchema();
res.IndexName = indexName;
// Determine if this is a unique index or not.
string[] descParts = desc.Split(',');
foreach (string p in descParts)
{
if (p.Trim().Contains("unique"))
{
res.IsUnique = true;
break;
}
} // foreach
// Get all key names and check if they are ASCENDING or DESCENDING
res.Columns = new List();
string[] keysParts = keys.Split(',');
foreach (string p in keysParts)
{
Match m = _keyRx.Match(p.Trim());
if (!m.Success)
{
throw new ApplicationException("Illegal key name [" + p + "] in index [" +
indexName + "]");
}
string key = m.Groups[1].Value;
IndexColumn ic = new IndexColumn();
ic.ColumnName = key;
if (m.Groups[2].Success)
ic.IsAscending = false;
else
ic.IsAscending = true;
res.Columns.Add(ic);
} // foreach
return res;
}
///
/// More adjustments for the DEFAULT value clause.
///
/// The value to adjust
/// Adjusted DEFAULT value string
private string AdjustDefaultValue(string val)
{
if (val == null || val == string.Empty)
return val;
Match m = _defaultValueRx.Match(val);
if (m.Success)
return m.Groups[1].Value;
return val;
}
///
/// Creates SQLite connection string from the specified DB file path.
///
/// The path to the SQLite database file.
/// SQLite connection string
private string CreateSQLiteConnectionString(string sqlitePath, string password)
{
SQLiteConnectionStringBuilder builder = new SQLiteConnectionStringBuilder();
builder.DataSource = sqlitePath;
if (password != null)
builder.Password = password;
builder.PageSize = 4096;
builder.UseUTF16Encoding = true;
string connstring = builder.ConnectionString;
return connstring;
}
#endregion
#region Trigger related
private void AddTriggersForForeignKeys(string sqlitePath, IEnumerable schema,
string password, SqlConversionHandler handler)
{
// Connect to the newly created database
string sqliteConnString = CreateSQLiteConnectionString(sqlitePath, password);
using (SQLiteConnection conn = new SQLiteConnection(sqliteConnString))
{
conn.Open();
// foreach
foreach (TableSchema dt in schema)
{
try
{
AddTableTriggers(conn, dt);
}
catch (Exception ex)
{
LogManager.Log(ex, "AddTableTriggers failed");
throw;
}
}
} // using
LogManager.Log("finished adding triggers to schema");
}
private void AddTableTriggers(SQLiteConnection conn, TableSchema dt)
{
IList triggers = TriggerBuilder.GetForeignKeyTriggers(dt);
foreach (TriggerSchema trigger in triggers)
{
SQLiteCommand cmd = new SQLiteCommand(WriteTriggerSchema(trigger), conn);
cmd.ExecuteNonQuery();
}
}
#endregion
///
/// Gets a create script for the triggerSchema in sqlite syntax
///
/// Trigger to script
/// Executable script
public string WriteTriggerSchema(TriggerSchema ts)
{
return @"CREATE TRIGGER [" + ts.Name + "] " +
ts.Type + " " + ts.Event +
" ON [" + ts.Table + "] " +
"BEGIN " + ts.Body + " END;";
}
#region Private Variables
private bool _isActive = false;
private bool _cancelled = false;
private Regex _keyRx = new Regex(@"(([a-zA-Z_äöüÄÖÜß0-9\.]|(\s+))+)(\(\-\))?");
private Regex _defaultValueRx = new Regex(@"\(N(\'.*\')\)");
#endregion
}
///
/// This handler is called whenever a progress is made in the conversion process.
///
/// TRUE indicates that the entire conversion process is finished.
/// TRUE indicates that the current step finished successfully.
/// Progress percent (0-100)
/// A message that accompanies the progress.
public delegate void SqlConversionHandler(bool done, bool success, int percent, string msg);
///
/// This handler allows the user to change which tables get converted from SQL Server
/// to SQLite.
///
/// The original SQL Server DB schema
/// The same schema minus any table we don't want to convert.
public delegate List SqlTableSelectionHandler(List schema);
///
/// This handler is called in order to handle the case when copying the SQL Server view SQL
/// statement is not enough and the user needs to either update the view definition himself
/// or discard the view definition from the generated SQLite database.
///
/// The problematic view definition
/// The updated view definition, or NULL in case the view should be discarded
public delegate string FailedViewDefinitionHandler(ViewSchema vs);
}