public partial class SqlCeDataStore : SQLStoreBase<SqlEntityInfo> { private string m_connectionString; private int m_maxSize = 128; // Max Database Size defaults to 128MB private string Password { get; set; } public string FileName { get; protected set; } protected SqlCeDataStore() : base() { UseCommandCache = true; } public SqlCeDataStore(string fileName) : this(fileName, null) { } public SqlCeDataStore(string fileName, string password) : this() { FileName = fileName; Password = password; } public override bool StoreExists { get { return File.Exists(FileName); } } public override string Name { get { return FileName; } } protected override IDbCommand GetNewCommandObject() { return new SqlCeCommand(); } protected override string AutoIncrementFieldIdentifier { get { return "IDENTITY"; } } /// <summary> /// Deletes the underlying DataStore /// </summary> public override void DeleteStore() { File.Delete(FileName); } /// <summary> /// Creates the underlying DataStore /// </summary> public override void CreateStore() { if (StoreExists) { throw new StoreAlreadyExistsException(); } // create the file using (SqlCeEngine engine = new SqlCeEngine(ConnectionString)) { engine.CreateDatabase(); } var connection = GetConnection(true); try { foreach (var entity in this.Entities) { CreateTable(connection, entity); } } finally { DoneWithConnection(connection, true); } } /// <summary> /// Ensures that the underlying database tables contain all of the Fields to represent the known entities. /// This is useful if you need to add a Field to an existing store. Just add the Field to the Entity, then /// call EnsureCompatibility to have the field added to the database. /// </summary> public override void EnsureCompatibility() { if (!StoreExists) { CreateStore(); return; } var connection = GetConnection(true); try { foreach (var entity in this.Entities) { ValidateTable(connection, entity); } } finally { DoneWithConnection(connection, true); } } public override void CompactDatabase() { using (SqlCeEngine engine = new SqlCeEngine()) { engine.Compact(ConnectionString); } } public override int Count<T>(IEnumerable<FilterCondition> filters) { var t = typeof(T); string entityName = m_entities.GetNameForType(t); if (entityName == null) { throw new EntityNotFoundException(t); } var connection = GetConnection(true); try { using (var command = BuildFilterCommand<SqlCeCommand, SqlCeParameter>(entityName, filters, true)) { command.Transaction = CurrentTransaction as SqlCeTransaction; command.Connection = connection as SqlCeConnection; return (int)command.ExecuteScalar(); } } finally { DoneWithConnection(connection, true); } } /// <summary> /// Inserts the provided entity instance into the underlying data store. /// </summary> /// <param name="item"></param> /// <remarks> /// If the entity has an identity field, calling Insert will populate that field with the identity vale vefore returning /// </remarks> public override void OnInsert(object item, bool insertReferences) { string entityName; var itemType = item.GetType(); if (item is DynamicEntity) { entityName = (item as DynamicEntity).EntityName; } else { entityName = m_entities.GetNameForType(itemType); } var keyScheme = Entities[entityName].EntityAttribute.KeyScheme; if (entityName == null) { throw new EntityNotFoundException(item.GetType()); } if (insertReferences) { DoInsertReferences(item, entityName, keyScheme, true); } // we'll use table direct for inserts - no point in getting the query parser involved in this var connection = GetConnection(false); try { CheckOrdinals(entityName); FieldAttribute identity = null; using (var command = new SqlCeCommand()) { command.Connection = connection as SqlCeConnection; command.Transaction = CurrentTransaction as SqlCeTransaction; command.CommandText = entityName; command.CommandType = CommandType.TableDirect; using (var results = command.ExecuteResultSet(ResultSetOptions.Updatable)) { var record = results.CreateRecord(); FillEntity(record.SetValue, entityName, itemType, item, out identity); results.Insert(record); // did we have an identity field? If so, we need to update that value in the item if (identity != null) { var id = GetIdentity(connection); SetInstanceValue(identity, item, id); } if (insertReferences) { DoInsertReferences(item, entityName, keyScheme, false); } } command.Dispose(); } } finally { DoneWithConnection(connection, false); } } private void FillEntity(Action<int, object> setter, string entityName, Type itemType, object item, out FieldAttribute identity) { // The reason for this somewhat convoluted Action parameter is that while the SqlCeUpdateableRecord (from Insert) // and SqlCeResultSet (from Update) both contain a SetValue method, they don't share it on any common // interface. using an Action allows us to share this code anyway. identity = null; var keyScheme = Entities[entityName].EntityAttribute.KeyScheme; foreach (var field in Entities[entityName].Fields) { if (field.IsPrimaryKey) { switch(keyScheme) { case KeyScheme.Identity: identity = field; break; case KeyScheme.GUID: var value = GetInstanceValue(field, item); if (value.Equals(Guid.Empty)) { value = Guid.NewGuid(); SetInstanceValue(field, item, value); } setter(field.Ordinal, value); break; } } else if (field.DataType == DbType.Object) { // get serializer var serializer = GetSerializer(itemType); if (serializer == null) { throw new MissingMethodException( string.Format("The field '{0}' requires a custom serializer/deserializer method pair in the '{1}' Entity", field.FieldName, entityName)); } var value = serializer.Invoke(item, new object[] { field.FieldName }); if (value == null) { setter(field.Ordinal, DBNull.Value); } else { setter(field.Ordinal, value); } } else if (field.DataType == DbType.DateTime) { var dtValue = GetInstanceValue(field, item); if (dtValue.Equals(DateTime.MinValue)) { if ((!field.AllowsNulls) && (field.DefaultType != DefaultType.CurrentDateTime)) { dtValue = SqlDateTime.MinValue; setter(field.Ordinal, dtValue); } else { // let the null pass through } } else { setter(field.Ordinal, dtValue); } } else if (field.IsRowVersion) { // read-only, so do nothing } else { var iv = GetInstanceValue(field, item); if((iv == DBNull.Value) && (field.DefaultValue != null)) { iv = field.DefaultValue; } setter(field.Ordinal, iv); } } } protected override IDataParameter CreateParameterObject(string parameterName, object parameterValue) { return new SqlCeParameter(parameterName, parameterValue); } private int GetIdentity(IDbConnection connection) { using (var command = new SqlCeCommand("SELECT @@IDENTITY", connection as SqlCeConnection)) { command.Transaction = CurrentTransaction as SqlCeTransaction; object id = command.ExecuteScalar(); return Convert.ToInt32(id); } } protected override void GetPrimaryKeyInfo(string entityName, out string indexName, out string columnName) { indexName = string.Empty; columnName = string.Empty; var connection = GetConnection(true); try { string sql = string.Format("SELECT INDEX_NAME FROM information_schema.indexes WHERE (TABLE_NAME = '{0}') AND (PRIMARY_KEY = 1)", entityName); using (var command = GetNewCommandObject()) { command.Transaction = CurrentTransaction as SqlCeTransaction; command.CommandText = sql; command.Connection = connection; indexName = command.ExecuteScalar() as string; } } finally { DoneWithConnection(connection, true); } } protected override void CheckOrdinals(string entityName) { if (Entities[entityName].Fields.OrdinalsAreValid) return; var connection = GetConnection(true); try { using (var command = new SqlCeCommand()) { command.Transaction = CurrentTransaction as SqlCeTransaction; command.Connection = connection as SqlCeConnection; command.CommandText = entityName; command.CommandType = CommandType.TableDirect; using (var reader = command.ExecuteReader()) { foreach (var field in Entities[entityName].Fields) { field.Ordinal = reader.GetOrdinal(field.FieldName); } Entities[entityName].Fields.OrdinalsAreValid = true; } command.Dispose(); } } finally { DoneWithConnection(connection, true); } } public int MaxDatabaseSizeInMB { get { return m_maxSize; } set { // min of 128MB if (value < 128) throw new ArgumentOutOfRangeException(); // max of 4GB if (value > 4096) throw new ArgumentOutOfRangeException(); m_maxSize = value; } } public override string ConnectionString { get { if (m_connectionString == null) { m_connectionString = string.Format("Data Source={0};Persist Security Info=False;Max Database Size={1};", FileName, MaxDatabaseSizeInMB); if (!string.IsNullOrEmpty(Password)) { m_connectionString += string.Format("Password={0};", Password); } } return m_connectionString; } } protected override IDbConnection GetNewConnectionObject() { return new SqlCeConnection(ConnectionString); } protected void ValidateIndex(IDbConnection connection, string indexName, string tableName, string fieldName, bool ascending) { var valid = false; string sql = string.Format("SELECT INDEX_NAME FROM information_schema.indexes WHERE (TABLE_NAME = '{0}') AND (COLUMN_NAME = '{1}')", tableName, fieldName); using (SqlCeCommand command = new SqlCeCommand(sql, connection as SqlCeConnection)) { command.Transaction = CurrentTransaction as SqlCeTransaction; var name = command.ExecuteScalar() as string; if (string.Compare(name, indexName, true) == 0) { valid = true; } if (!valid) { sql = string.Format("CREATE INDEX {0} ON {1}({2} {3})", indexName, tableName, fieldName, ascending ? "ASC" : "DESC"); command.CommandText = sql; command.ExecuteNonQuery(); } } } public override string[] GetTableNames() { var names = new List<string>(); var connection = GetConnection(true); try { using (var command = GetNewCommandObject()) { command.Transaction = CurrentTransaction as SqlCeTransaction; command.Connection = connection; var sql = "SELECT table_name FROM information_schema.tables"; command.CommandText = sql; using (var reader = command.ExecuteReader()) { while (reader.Read()) { names.Add(reader.GetString(0)); } } return names.ToArray(); } } finally { DoneWithConnection(connection, true); } } public override bool TableExists(string tableName) { var connection = GetConnection(true); try { using (var command = GetNewCommandObject()) { command.Transaction = CurrentTransaction as SqlCeTransaction; command.Connection = connection; var sql = string.Format("SELECT COUNT(*) FROM information_schema.tables WHERE table_name = '{0}'", tableName); command.CommandText = sql; var count = Convert.ToInt32(command.ExecuteScalar()); return (count > 0); } } finally { DoneWithConnection(connection, true); } } public override DynamicEntityDefinition DiscoverDynamicEntity(string entityName) { if (!TableExists(entityName)) { return null; } var connection = GetConnection(true); try { using (var cmd = GetNewCommandObject()) { cmd.Connection = connection; cmd.Transaction = CurrentTransaction; cmd.CommandText = string.Format("SELECT COLUMN_NAME, ORDINAL_POSITION, IS_NULLABLE, DATA_TYPE, NUMERIC_PRECISION, NUMERIC_SCALE FROM information_schema.columns WHERE TABLE_NAME = '{0}' ORDER BY ORDINAL_POSITION", entityName); var fields = new List<FieldAttribute>(); using (var reader = cmd.ExecuteReader()) { while(reader.Read()) { var name = reader.GetString(0); var nullable = string.Compare(reader.GetString(2), "YES", true) == 0; var type = reader.GetString(3).ParseToDbType(); var field = new FieldAttribute() { DataType = type, FieldName = name, AllowsNulls = nullable, }; if (!reader.IsDBNull(4)) { field.Precision = Convert.ToInt32(reader.GetValue(4)); } if (!reader.IsDBNull(5)) { field.Scale = Convert.ToInt32(reader.GetValue(5)); } fields.Add(field); } } cmd.CommandText = string.Format("SELECT COLUMN_NAME, PRIMARY_KEY, [UNIQUE], COLLATION FROM information_schema.indexes WHERE TABLE_NAME = '{0}'", entityName); using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { var column = reader.GetString(0); var pk = Convert.ToBoolean(reader.GetValue(1)); var unique = Convert.ToBoolean(reader.GetValue(2)); var field = fields.FirstOrDefault(f => f.FieldName == column); if (pk) { field.IsPrimaryKey = true; } else { var collation = Convert.ToInt32(reader.GetValue(3)); field.SearchOrder = collation == 1 ? FieldSearchOrder.Ascending : FieldSearchOrder.Descending; } if (unique) { field.RequireUniqueValue = true; } } } var entityDefinition = new DynamicEntityDefinition(entityName, fields); RegisterEntityInfo(entityDefinition); return entityDefinition; } } finally { DoneWithConnection(connection, true); } } protected override void ValidateTable(IDbConnection connection, IEntityInfo entity) { // prevent caches reads of entitiy fields m_lastEntity = null; // first make sure the table exists if (!TableExists(entity.EntityAttribute.NameInStore)) { CreateTable(connection, entity); return; } using (var command = new SqlCeCommand()) { command.Transaction = CurrentTransaction as SqlCeTransaction; command.Connection = connection as SqlCeConnection; foreach (var field in entity.Fields) { if (ReservedWords.Contains(field.FieldName, StringComparer.InvariantCultureIgnoreCase)) { throw new ReservedWordException(field.FieldName); } // yes, I realize hard-coded ordinals are not a good practice, but the SQL isn't changing, it's method specific var sql = string.Format("SELECT column_name, " // 0 + "data_type, " // 1 + "character_maximum_length, " // 2 + "numeric_precision, " // 3 + "numeric_scale, " // 4 + "is_nullable " + "FROM information_schema.columns " + "WHERE (table_name = '{0}' AND column_name = '{1}')", entity.EntityAttribute.NameInStore, field.FieldName); command.CommandText = sql; using (var reader = command.ExecuteReader()) { if (!reader.Read()) { // field doesn't exist - we must create it var alter = new StringBuilder(string.Format("ALTER TABLE {0} ", entity.EntityAttribute.NameInStore)); alter.Append(string.Format("ADD [{0}] {1} {2}", field.FieldName, GetFieldDataTypeString(entity.EntityName, field), GetFieldCreationAttributes(entity.EntityAttribute, field))); using (var altercmd = new SqlCeCommand(alter.ToString(), connection as SqlCeConnection)) { altercmd.ExecuteNonQuery(); } } else { // TODO: verify field length, etc. } } } } } }