Przeglądaj źródła

New deletions system

Kenric Nugteren 2 lat temu
rodzic
commit
6ed2e45bfe

+ 25 - 10
InABox.Core/Column.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq.Expressions;
@@ -11,11 +12,25 @@ namespace InABox.Core
     public interface IColumn
     {
         string Property { get; }
+
+        Type Type { get; }
     }
 
     public class Column<T> : SerializableExpression<T>, IColumn
     {
 
+        public Type Type
+        {
+            get
+            {
+                if (Expression == null)
+                    throw new Exception(string.Format("Expression [{0}] may not be null!", Property));
+                if (Expression is IndexExpression)
+                    return DatabaseSchema.Property(typeof(T), Property).PropertyType;
+                return Expression.Type;
+            }
+        }
+
         public bool IsEqualTo(String name) => 
             !String.IsNullOrWhiteSpace(name) && String.Equals(Property, name);
 
@@ -77,20 +92,16 @@ namespace InABox.Core
             return name;
             //return Property.ToString();
         }
-
-        public Type ExpressionType()
-        {
-            if (Expression == null)
-                throw new Exception(string.Format("Expression [{0}] may not be null!", Property));
-            if (Expression is IndexExpression)
-                return DatabaseSchema.Property(typeof(T), Property).PropertyType;
-            return Expression.Type;
-        }
     }
 
     public interface IColumns
     {
+        int Count { get; }
+
         bool Any();
+
+        IEnumerable<IColumn> GetColumns();
+
         IEnumerable<string> ColumnNames();
         Dictionary<String, Type> AsDictionary();
         IColumns Add(string column);
@@ -163,6 +174,10 @@ namespace InABox.Core
             }
         }
 
+        public int Count => Items.Length;
+
+        public IEnumerable<IColumn> GetColumns() => columns;
+
         public bool Any() => columns.Any();
 
         public IColumns Add(string column)
@@ -222,7 +237,7 @@ namespace InABox.Core
         { 
             Dictionary< String, Type> result = new Dictionary< String, Type>();
             foreach (var column in Items)
-                result[column.Property] = column.ExpressionType();
+                result[column.Property] = column.Type;
             return result;
         }
 

+ 3 - 4
InABox.Core/DataTable.cs

@@ -438,9 +438,8 @@ namespace InABox.Core
         
         public void LoadColumns(IColumns columns)
         {
-            var types = columns.AsDictionary();
-            foreach (var col in types.Keys)
-                Columns.Add(new CoreColumn { ColumnName = col, DataType = types[col] });
+            foreach (var col in columns.GetColumns())
+                Columns.Add(new CoreColumn { ColumnName = col.Property, DataType = col.Type });
         }
         
         public void LoadColumns(IEnumerable<CoreColumn> columns)
@@ -801,7 +800,7 @@ namespace InABox.Core
                         }
                         else
                         {
-                            writer.WriteValue(val);
+                            writer.WriteValue(val ?? CoreUtils.GetDefault(col.DataType));
                         }
                     }
 

+ 5 - 0
InABox.Core/DatabaseSchema/CustomProperty.cs

@@ -192,6 +192,11 @@ namespace InABox.Core
         [DoNotSerialize]
         public bool IsEntityLink { get; set; }
 
+        [NullEditor]
+        [DoNotPersist]
+        [DoNotSerialize]
+        public bool IsCalculated => false;
+
         private static string? NonWhiteSpaceOrNull(string? name)
         {
             return string.IsNullOrWhiteSpace(name) ? null : name;

+ 3 - 1
InABox.Core/DatabaseSchema/IProperty.cs

@@ -30,6 +30,8 @@ namespace InABox.Core
 
         string Caption { get; set; }
 
+        bool IsCalculated { get; }
+
         /// <summary>
         /// An <see cref="IProperty"/> is required if it has the <see cref="RequiredColumnAttribute"/> defined on it.<br/>
         /// If it is part of an <see cref="IEntityLink"/>, then it is only required if the <see cref="IEntityLink"/> property on the parent class
@@ -62,7 +64,7 @@ namespace InABox.Core
         {
             return property.Parent != null && (property.Parent.HasEditor || property.Parent.HasParentEditor());
         }
-        private static bool HasParentEntityLink(this IProperty property)
+        public static bool HasParentEntityLink(this IProperty property)
         {
             return property.Parent != null && (property.Parent.IsEntityLink || property.Parent.HasParentEntityLink());
         }

+ 15 - 0
InABox.Core/DatabaseSchema/StandardProperty.cs

@@ -13,6 +13,7 @@ namespace InABox.Core
 
         private string _name = "";
 
+        private bool? _calculated;
 
         private Action<object, object> _setter;
 
@@ -98,6 +99,20 @@ namespace InABox.Core
 
         public bool IsEntityLink { get; set; }
 
+        public bool IsCalculated
+        {
+            get
+            {
+                if (!_calculated.HasValue)
+                {
+                    _calculated = Property.GetCustomAttribute<AggregateAttribute>() != null
+                        || Property.GetCustomAttribute<FormulaAttribute>() != null
+                        || Property.GetCustomAttribute<ConditionAttribute>() != null;
+                }
+                return _calculated.Value;
+            }
+        }
+
         public Expression Expression()
         {
             return CoreUtils.CreateMemberExpression(_class, Name);

+ 84 - 0
InABox.Core/Deletion.cs

@@ -7,6 +7,87 @@ using System.Threading.Tasks;
 
 namespace InABox.Core
 {
+    public class SetNullData
+    {
+        public Guid EntityID { get; set; }
+
+        public Guid ParentID { get; set; }
+
+        public string Property { get; set; }
+
+        public SetNullData(Guid entityID, string property, Guid parentID)
+        {
+            EntityID = entityID;
+            ParentID = parentID;
+            Property = property;
+        }
+    }
+
+    public class DeletionData
+    {
+        public Dictionary<string, List<SetNullData>> SetNulls { get; set; } = new Dictionary<string, List<SetNullData>>();
+
+        public Dictionary<string, CoreTable> Cascades { get; set; } = new Dictionary<string, CoreTable>();
+
+        private static bool IsDeletionColumn(IProperty property)
+        {
+            if (property.IsCalculated) return false;
+            if (property.Parent is null) return true;
+            if (property.Parent.IsEntityLink && !property.Name.EndsWith(".ID")) return false;
+            if (property.Parent.HasParentEntityLink()) return false;
+            return true;
+        }
+
+        public static IColumns DeletionColumns(Type T)
+        {
+            var columns = Columns.Create(T);
+            foreach(var property in DatabaseSchema.Properties(T))
+            {
+                if (IsDeletionColumn(property))
+                    columns.Add(property.Name);
+            }
+            return columns;
+        }
+        public static Columns<T> DeletionColumns<T>() => (DeletionColumns(typeof(T)) as Columns<T>)!;
+
+        public void DeleteEntity<T>(T entity)
+            where T : Entity, new()
+        {
+            var entityName = typeof(T).EntityName();
+            if(!Cascades.TryGetValue(entityName, out var table))
+            {
+                table = new CoreTable();
+                table.LoadColumns(DeletionColumns<T>());
+                Cascades.Add(entityName, table);
+            }
+            table.LoadRow(entity);
+        }
+        public void DeleteEntity<T>(CoreRow row)
+            where T : Entity, new()
+        {
+            var entityName = typeof(T).EntityName();
+            if(!Cascades.TryGetValue(entityName, out var table))
+            {
+                table = new CoreTable();
+                table.LoadColumns(DeletionColumns<T>());
+                Cascades.Add(entityName, table);
+            }
+            table.LoadRows(new CoreRow[] { row });
+        }
+
+        public void SetNullEntity<T>(Guid entityID, string property, Guid parentID)
+            where T : Entity, new()
+        {
+            var entityName = typeof(T).EntityName();
+            if (!SetNulls.TryGetValue(entityName, out var list))
+            {
+                list = new List<SetNullData>();
+                SetNulls.Add(entityName, list);
+            }
+            list.Add(new SetNullData(entityID, property, parentID));
+        }
+    }
+
     // A Deletion is not IRemotable; *no one* but the database should know about it.
     public class Deletion : Entity, ILicense<CoreLicense>, IPersistent
     {
@@ -22,5 +103,8 @@ namespace InABox.Core
         [TextBoxEditor(Editable = Editable.Disabled)]
         public string Description { get; set; }
 
+        [NullEditor]
+        public string Data { get; set; }
+
     }
 }

+ 1 - 1
InABox.DynamicGrid/DynamicColumnGrid.cs

@@ -70,7 +70,7 @@ namespace InABox.DynamicGrid
         {
             var result = new List<DynamicGridColumn>();
             var cols = new DynamicGridColumns();
-            cols.ExtractColumns(Type, "");
+            cols.ExtractColumns(Type);
             foreach (var col in cols)
             {
                 if (col.Editor == null)

+ 2 - 2
InABox.DynamicGrid/DynamicGrid.cs

@@ -398,7 +398,7 @@ namespace InABox.DynamicGrid
             LoadColumnsMenu(ColumnsMenu);
 
             MasterColumns = new DynamicGridColumns();
-            MasterColumns.ExtractColumns(typeof(T), "");
+            MasterColumns.ExtractColumns(typeof(T));
 
             foreach(var column in LookupFactory.RequiredColumns<T>().ColumnNames())
             {
@@ -3121,7 +3121,7 @@ namespace InABox.DynamicGrid
                 {
                     var newData = new CoreTable();
                     foreach (var column in columns.Items)
-                        newData.Columns.Add(new CoreColumn { ColumnName = column.Property, DataType = column.ExpressionType()});
+                        newData.Columns.Add(new CoreColumn { ColumnName = column.Property, DataType = column.Type});
 
                     FilterRows(data, newData, filter: (row) =>
                     {

+ 1 - 1
InABox.DynamicGrid/DynamicGridColumns.cs

@@ -14,7 +14,7 @@ namespace InABox.DynamicGrid
 
         private static Dictionary<Type, List<DynamicGridColumn>> _columnscache = new Dictionary<Type, List<DynamicGridColumn>>();
 
-        public void ExtractColumns(Type type, string prefix)
+        public void ExtractColumns(Type type)
         {
             if (!_columnscache.ContainsKey(type))
             {

+ 1 - 1
InABox.DynamicGrid/EmbeddedDynamicEditorForm.xaml.cs

@@ -354,7 +354,7 @@ namespace InABox.DynamicGrid
         {
             var columns = new DynamicGridColumns();
             if (_items != null && _items.Any())
-                columns.ExtractColumns(_items.First().GetType(), "");
+                columns.ExtractColumns(_items.First().GetType());
             if (OnCustomiseColumns != null)
                 columns = OnCustomiseColumns.Invoke(this, columns);
             return columns;

+ 1 - 1
InABox.DynamicGrid/MultiSelectDialog.cs

@@ -125,7 +125,7 @@ namespace InABox.DynamicGrid
 
             //}
             foreach (var column in _columns.Items)
-                result.Columns.Add(new CoreColumn { ColumnName = column.Property, DataType = column.ExpressionType() });
+                result.Columns.Add(new CoreColumn { ColumnName = column.Property, DataType = column.Type });
 
             if (datagrid.Data != null && datagrid.SelectedRows.Any())
                 foreach (var sel in datagrid.SelectedRows)

+ 150 - 81
inabox.database.sqlite/SQLiteProvider.cs

@@ -7,7 +7,7 @@ using System.Reflection;
 using System.Runtime.Serialization.Formatters.Binary;
 using System.Text;
 using InABox.Core;
-
+using Microsoft.CodeAnalysis;
 
 namespace InABox.Database.SQLite
 {
@@ -628,7 +628,7 @@ namespace InABox.Database.SQLite
                 return result;
 
             var actions = new List<string>();
-            var childtypes = Types.Where(x => /* (x != type) && */ x.IsSubclassOf(typeof(Entity)));
+            var childtypes = Types.Where(x => /* (x != type) && */ x.IsSubclassOf(typeof(Entity)) && x.GetCustomAttribute<AutoEntity>() == null);
             foreach (var childtype in childtypes)
             {
                 // Get all registererd types for this entitylink
@@ -1092,10 +1092,11 @@ namespace InABox.Database.SQLite
 
         private void CheckTriggers(SQLiteWriteAccessor access, Type type, Dictionary<string, string> db_triggers)
         {
+            /*
 #if PURGE
             foreach (var trigger in db_triggers.Keys)
                 ExecuteSQL(access, string.Format("DROP TRIGGER {0}", trigger));
-#else
+#else*/
             var type_triggers = LoadTriggers(type);
 
             foreach (var trigger in db_triggers.Keys)
@@ -1104,7 +1105,7 @@ namespace InABox.Database.SQLite
             foreach (var trigger in type_triggers)
                 if (!db_triggers.ContainsValue(trigger))
                     ExecuteSQL(access, trigger);
-#endif
+//#endif
         }
 
         // private void CheckViews(SQLiteWriteAccessor access, Type type, Dictionary<String, String> db_views)
@@ -2148,7 +2149,7 @@ namespace InABox.Database.SQLite
             Dictionary<string, string>? aggregates, Dictionary<string, object?>? constants, int top, bool distinct, bool useparams) where T : Entity
             => PrepareSelectNonGeneric(typeof(T), command, prefix, filter, columns, sort, aggregates, constants, top, distinct, useparams);
 
-        private void PrepareUpsert<T>(SQLiteCommand command, T item, bool addDelete = false) where T : Entity
+        private static void PrepareUpsertNonGeneric(Type T, SQLiteCommand command, Entity item, bool addDelete = false)
         {
             command.CommandText = "";
             command.Parameters.Clear();
@@ -2199,7 +2200,7 @@ namespace InABox.Database.SQLite
 
             var particles = new List<string>();
             particles.Add("INSERT INTO");
-            particles.Add(typeof(T).EntityName().Split('.').Last());
+            particles.Add(T.EntityName().Split('.').Last());
             particles.Add("(");
             particles.Add(string.Join(",", insertfields));
             particles.Add(")");
@@ -2214,6 +2215,9 @@ namespace InABox.Database.SQLite
             command.CommandText = string.Join(" ", particles);
         }
 
+        private void PrepareUpsert<T>(SQLiteCommand command, T item, bool addDelete = false) where T : Entity
+            => PrepareUpsertNonGeneric(typeof(T), command, item, addDelete);
+
         public void PrepareDelete<T>(SQLiteCommand command, T item) where T : Entity
         {
             command.CommandText = string.Format("DELETE FROM {0} WHERE [ID] = @p0", typeof(T).EntityName().Split('.').Last());
@@ -2223,7 +2227,6 @@ namespace InABox.Database.SQLite
 
         #endregion
 
-
         #region Schema Handling
 
         public Dictionary<string, Type> GetSchema()
@@ -2256,6 +2259,8 @@ namespace InABox.Database.SQLite
             return result;
         }
 
+        #region Query
+
         public IEnumerable<object[]> List<T>(Filter<T>? filter = null, Columns<T>? columns = null, SortOrder<T>? sort = null) where T : Entity, new()
         {
             var newFilter = new Filter<T>(x => x.Deleted).IsEqualTo(Guid.Empty);
@@ -2300,34 +2305,33 @@ namespace InABox.Database.SQLite
             return result;
         }
 
-        private CoreTable DoQuery<T>(Filter<T>? filter, Columns<T>? columns, SortOrder<T>? sort, int top, bool log, bool distinct)
-            where T : Entity, new()
+        private CoreTable DoQueryNonGeneric(Type T, IFilter? filter = null, IColumns? columns = null, ISortOrder? sort = null, int top = int.MaxValue, bool log = true, bool distinct = false)
         {
             var start = DateTime.Now;
             //LogReset();
 
             //LogStart();
-            var cols = CoreUtils.GetColumns(columns);
+            var cols = CoreUtils.GetColumns(T, columns);
             //LogStop("GetColumns");
 
             var result = new CoreTable();
-            result.TableName = typeof(T).EntityName();
-            foreach (var col in cols.Items)
-                result.Columns.Add(new CoreColumn { ColumnName = col.Property, DataType = col.ExpressionType() });
+            result.TableName = T.EntityName();
+            foreach (var col in cols.GetColumns())
+                result.Columns.Add(new CoreColumn { ColumnName = col.Property, DataType = col.Type });
             //LogStop("MakeTable");
 
             using (var access = GetReadAccess())
             {
                 using (var command = access.CreateCommand())
                 {
-                    var sortorder = sort == null ? LookupFactory.DefineSort<T>() : sort;
+                    var sortorder = sort ?? LookupFactory.DefineSort(T);
 
                     command.CommandText = "";
                     command.Parameters.Clear();
 
                     //LogStart();
-                    String sql = PrepareSelect(command, 'A', filter, cols, sortorder, null, null, top, distinct, true) + ";";
-   
+                    var sql = PrepareSelectNonGeneric(T, command, 'A', filter, cols, sortorder, null, null, top, distinct, true) + ";";
+
                     command.CommandText = sql;
 
                     try
@@ -2437,12 +2441,15 @@ namespace InABox.Database.SQLite
             {
                 var duration = DateTime.Now - start;
                 Logger.Send(LogType.Information, "",
-                    string.Format("SQLite::Query<{0}>({1} cols) returns {2} rows in {3}ms", typeof(T).Name, cols.Items.Length, result.Rows.Count,
+                    string.Format("SQLite::Query<{0}>({1} cols) returns {2} rows in {3}ms", T.Name, cols.Count, result.Rows.Count,
                         duration.TotalMilliseconds));
             }
             return result;
         }
 
+        private CoreTable DoQuery<T>(Filter<T>? filter, Columns<T>? columns, SortOrder<T>? sort, int top, bool log, bool distinct)
+            where T : Entity, new() => DoQueryNonGeneric(typeof(T), filter, columns, sort, top, log, distinct);
+
         public CoreTable Query<T>(Filter<T>? filter = null, Columns<T>? columns = null, SortOrder<T>? sort = null, int top = int.MaxValue, bool log = true, bool distinct = false) 
             where T : Entity, new()
         {
@@ -2532,7 +2539,11 @@ namespace InABox.Database.SQLite
             return result.ToArray();
         }
 
-        private void OnSave<T>(IEnumerable<T> entities, bool addDelete = false) where T : Entity
+        #endregion
+
+        #region Save
+
+        private void OnSaveNonGeneric(Type T, IEnumerable<Entity> entities, bool addDelete = false)
         {
             if (!entities.Any())
                 return;
@@ -2548,7 +2559,7 @@ namespace InABox.Database.SQLite
                         {
                             foreach (var entity in entities)
                             {
-                                PrepareUpsert(command, entity, addDelete);
+                                PrepareUpsertNonGeneric(T, command, entity, addDelete);
                                 command.ExecuteNonQuery();
                             }
 
@@ -2566,7 +2577,9 @@ namespace InABox.Database.SQLite
                     throw error;
             }
         }
-        
+        private void OnSave<T>(IEnumerable<T> entities, bool addDelete = false) where T : Entity
+            => OnSaveNonGeneric(typeof(T), entities, addDelete);
+
         public static bool CanSave<T>()
         {
             if (DbFactory.IsReadOnly)
@@ -2589,8 +2602,7 @@ namespace InABox.Database.SQLite
             }
             OnSave(entities, false);
         }
-
-        private void OnSave<T>(T entity, bool addDelete = false) where T : Entity
+        private void OnSaveNonGeneric(Type T, Entity entity, bool addDelete = false)
         {
             Exception? error = null;
             using (var access = GetWriteAccess())
@@ -2599,7 +2611,7 @@ namespace InABox.Database.SQLite
                 {
                     try
                     {
-                        PrepareUpsert(command, entity, addDelete);
+                        PrepareUpsertNonGeneric(T, command, entity, addDelete);
                         command.ExecuteNonQuery();
                     }
                     catch (Exception e)
@@ -2613,6 +2625,9 @@ namespace InABox.Database.SQLite
                 throw error;
         }
 
+        private void OnSave<T>(T entity, bool addDelete = false) where T : Entity
+            => OnSaveNonGeneric(typeof(T), entity, addDelete);
+
         public void Save<T>(T entity) where T : Entity
         {
             if (!CanSave<T>())
@@ -2621,7 +2636,11 @@ namespace InABox.Database.SQLite
             }
             OnSave(entity, false);
         }
-        
+
+        #endregion
+
+        #region Delete
+
         public void Purge<T>(T entity) where T : Entity
         {
             using (var access = GetWriteAccess())
@@ -2686,7 +2705,7 @@ namespace InABox.Database.SQLite
             var cascades = new List<Tuple<Type, string>>();
             var setNulls = new List<Tuple<Type, List<string>>>();
 
-            var childtypes = Types.Where(x => x.IsSubclassOf(typeof(Entity)));
+            var childtypes = Types.Where(x => x.IsSubclassOf(typeof(Entity)) && x.GetCustomAttribute<AutoEntity>() == null);
             foreach (var childtype in childtypes)
             {
                 // Get all registered types for this entitylink
@@ -2727,72 +2746,97 @@ namespace InABox.Database.SQLite
 
         private MethodInfo _deleteEntitiesMethod = typeof(SQLiteProvider).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
             .Single(x => x.Name == nameof(DeleteEntity) && x.IsGenericMethod);
-        private void DeleteEntity<T>(Guid parentID, string parentField, Deletion deletion) where T : Entity, new()
+        private void DeleteEntity<T>(Guid parentID, string parentField, DeletionData deletionData) where T : Entity, new()
         {
-            var entities = Query(new Filter<T>(parentField).IsEqualTo(parentID), new Columns<T>(x => x.ID))
-                .Rows.Select(x => x.ToObject<T>()).ToList();
-            foreach(var entity in entities)
+            var columns = DeletionData.DeletionColumns<T>();
+            var entities = Query(new Filter<T>(parentField).IsEqualTo(parentID), columns);
+            foreach(var row in entities.Rows)
             {
-                entity.Deleted = deletion.ID;
+                deletionData.DeleteEntity<T>(row);
+                CascadeDelete(typeof(T), row.Get<T, Guid>(x => x.ID), deletionData);
             }
-            OnSave(entities, true);
-            foreach (var entity in entities)
+        }
+       
+        private void DeleteEntity(Type T, Guid parentID, string parentField, DeletionData deletionData)
+        {
+            _deleteEntitiesMethod.MakeGenericMethod(T).Invoke(this, new object?[] { parentID, parentField, deletionData });
+        }
+
+        private MethodInfo _setNullEntityMethod = typeof(SQLiteProvider).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
+            .Single(x => x.Name == nameof(SetNullEntity) && x.IsGenericMethod);
+        private void SetNullEntity<T>(List<string> properties, Guid parentID, DeletionData deletionData) where T : Entity, new()
+        {
+            foreach(var property in properties)
             {
-                CascadeDelete(typeof(T), entity.ID, deletion);
+                var entities = Query(new Filter<T>(property).IsEqualTo(parentID), new Columns<T>(x => x.ID));
+                foreach (var row in entities.Rows)
+                {
+                    deletionData.SetNullEntity<T>(row.Get<T, Guid>(x => x.ID), property, parentID);
+                }
             }
         }
-       
-        private void DeleteEntity(Type T, Guid parentID, string parentField, Deletion deletion)
+
+        private void SetNullEntity(Type T, List<string> properties, Guid parentID, DeletionData deletionData)
         {
-            _deleteEntitiesMethod.MakeGenericMethod(T).Invoke(this, new object?[] { parentID, parentField, deletion });
+            _setNullEntityMethod.MakeGenericMethod(T).Invoke(this, new object?[] { properties, parentID, deletionData });
         }
 
-        private void CascadeDelete(Type type, Guid parentID, Deletion deletion)
+        private void CascadeDelete(Type type, Guid parentID, DeletionData deletionData)
         {
-            if (!_cascades.TryGetValue(type, out var cascades))
-                return;
-            foreach(var cascade in cascades)
+            if (_cascades.TryGetValue(type, out var cascades))
+            {
+                foreach (var cascade in cascades)
+                {
+                    DeleteEntity(cascade.Item1, parentID, cascade.Item2, deletionData);
+                }
+            }
+            if(_setNulls.TryGetValue(type, out var setNulls))
             {
-                DeleteEntity(cascade.Item1, parentID, cascade.Item2, deletion);
+                foreach(var setNull in setNulls)
+                {
+                    SetNullEntity(setNull.Item1, setNull.Item2, parentID, deletionData);
+                }
             }
         }
 
         public void Delete<T>(T entity, string userID) where T : Entity, new()
         {
-#if PURGE
+//#if PURGE
             if (!CanSave<T>())
             {
                 return;
             }
             entity = DoQuery(
                 new Filter<T>(x => x.ID).IsEqualTo(entity.ID),
-                null,
+                DeletionData.DeletionColumns<T>(),
                 null,
                 int.MaxValue,
                 true,
                 false
             ).Rows.First().ToObject<T>();
 
+            var deletionData = new DeletionData();
+            deletionData.DeleteEntity(entity);
+            CascadeDelete(typeof(T), entity.ID, deletionData);
+
             var tableName = typeof(T).Name;
-            var deletion = new Deletion() { 
-                DeletionDate = DateTime.Now, 
-                HeadTable = tableName, 
+            var deletion = new Deletion()
+            {
+                DeletionDate = DateTime.Now,
+                HeadTable = tableName,
                 Description = entity.ToString() ?? "",
-                DeletedBy = userID
+                DeletedBy = userID,
+                Data = Serialization.Serialize(deletionData)
             };
             OnSave(deletion);
-
-            entity.Deleted = deletion.ID;
-            OnSave(entity, true);
-            CascadeDelete(typeof(T), entity.ID, deletion);
-#else
+            //#else
             Purge(entity);
-#endif
+//#endif
         }
 
         public void Delete<T>(IEnumerable<T> entities, string userID) where T : Entity, new()
         {
-#if PURGE
+//#if PURGE
             if (!CanSave<T>())
             {
                 return;
@@ -2800,28 +2844,32 @@ namespace InABox.Database.SQLite
             if (!entities.Any())
                 return;
             var ids = entities.Select(x => x.ID).ToArray();
-            var entityList = Query(new Filter<T>(x => x.ID).InList(ids)).Rows.Select(x => x.ToObject<T>()).ToList();
+            var entityList = Query(
+                new Filter<T>(x => x.ID).InList(ids),
+                DeletionData.DeletionColumns<T>()).Rows.Select(x => x.ToObject<T>()).ToList();
             if (!entityList.Any())
                 return;
 
+            var deletionData = new DeletionData();
+            foreach (var entity in entityList)
+            {
+                deletionData.DeleteEntity(entity);
+                CascadeDelete(typeof(T), entity.ID, deletionData);
+            }
+
             var tableName = typeof(T).Name;
-            var deletion = new Deletion() { 
+            var deletion = new Deletion()
+            {
                 DeletionDate = DateTime.Now,
                 HeadTable = tableName,
                 Description = $"Deleted {entityList.Count} entries",
-                DeletedBy = userID
+                DeletedBy = userID,
+                Data = Serialization.Serialize(deletionData)
             };
             OnSave(deletion);
-
-            foreach(var entity in entityList)
-            {
-                entity.Deleted = deletion.ID;
-                OnSave(entity, true);
-                CascadeDelete(typeof(T), entity.ID, deletion);
-            }
-#else
+            //#else
             Purge(entities);
-#endif
+//#endif
         }
 
         private void AddDeletionType(Type type, List<Type> deletions)
@@ -2848,7 +2896,7 @@ namespace InABox.Database.SQLite
             return deletionTypes;
         }
 
-        private void DoSetNull<TChild>(string field, Guid[] parentIDs) where TChild : Entity, new()
+        /*private void DoSetNull<TChild>(string field, Guid[] parentIDs) where TChild : Entity, new()
         {
             var columns = new Columns<TChild>(x => x.ID);
             columns.Add(field);
@@ -2865,9 +2913,9 @@ namespace InABox.Database.SQLite
                 CoreUtils.SetPropertyValue(child, field, Guid.Empty);
             }
             OnSave(children);
-        }
+        }*/
 
-        private void PurgeEntityType<T>(Deletion deletion) where T : Entity, new()
+        /*private void PurgeEntityType<T>(Deletion deletion) where T : Entity, new()
         {
             var entities = QueryDeleted(deletion, null, new Columns<T>(x => x.ID)).Rows.Select(x => x.ToObject<T>()).ToList();
             if (_setNulls.TryGetValue(typeof(T), out var setNulls))
@@ -2884,9 +2932,9 @@ namespace InABox.Database.SQLite
                 }
             }
             Purge(entities);
-        }
+        }*/
 
-        private void RecoverEntityType<T>(Deletion deletion) where T : Entity, new()
+        /*private void RecoverEntityType<T>(Deletion deletion) where T : Entity, new()
         {
             var entities = QueryDeleted(deletion, null, new Columns<T>(x => x.ID, x => x.Deleted)).Rows.Select(x => x.ToObject<T>()).ToList();
             foreach (var entity in entities)
@@ -2894,11 +2942,11 @@ namespace InABox.Database.SQLite
                 entity.Deleted = Guid.Empty;
             }
             OnSave(entities, true);
-        }
+        }*/
         
         public void Purge(Deletion deletion)
         {
-            if(deletion.ID == Guid.Empty)
+            /*if(deletion.ID == Guid.Empty)
             {
                 Logger.Send(LogType.Error, "", "Empty Deletion ID; Purge cancelled");
                 return;
@@ -2918,7 +2966,7 @@ namespace InABox.Database.SQLite
             {
                 purgeMethod.MakeGenericMethod(type).Invoke(this, new object[] { deletion });
             }
-
+            */
             Purge<Deletion>(deletion);
         }
 
@@ -2930,18 +2978,37 @@ namespace InABox.Database.SQLite
                 return;
             }
 
-            var entityType = CoreUtils.Entities.FirstOrDefault(x => x.Name == deletion.HeadTable);
-            if (entityType is null)
+            var data = Serialization.Deserialize<DeletionData>(deletion.Data);
+            if (data is null)
             {
-                Logger.Send(LogType.Error, "", $"Entity {deletion.HeadTable} does not exist; Recovery cancelled");
+                Logger.Send(LogType.Error, "", "Deletion Data deserialisation failed; Recovery cancelled");
                 return;
             }
-            var deletionTypes = GetDeletionTypes(entityType);
+            foreach (var (entityName, setNulls) in data.SetNulls)
+            {
+                if (!CoreUtils.TryGetEntity(entityName, out var entityType)) continue;
+
+                var saves = new List<Entity>();
+
+                foreach(var setNull in setNulls)
+                {
+                    var row = DoQueryNonGeneric(entityType,
+                        Filter.Create<Entity>(entityType, x => x.ID).IsEqualTo(setNull.EntityID),
+                        Columns.Create(entityType).Add<Entity>(x => x.ID).Add(setNull.Property)).Rows.FirstOrDefault();
+                    if (row is null) continue;
+
+                    var entity = (row.ToObject(entityType) as Entity)!;
+                    CoreUtils.SetPropertyValue(entity, setNull.Property, setNull.ParentID);
+                    saves.Add(entity);
+                }
 
-            var recoveryMethod = typeof(SQLiteProvider).GetMethod(nameof(RecoverEntityType), BindingFlags.NonPublic | BindingFlags.Instance)!;
-            foreach (var type in deletionTypes)
+                OnSaveNonGeneric(entityType, saves);
+            }
+            foreach(var (entityName, cascade) in data.Cascades)
             {
-                recoveryMethod.MakeGenericMethod(type).Invoke(this, new object[] { deletion });
+                if (!CoreUtils.TryGetEntity(entityName, out var entityType)) continue;
+
+                OnSaveNonGeneric(entityType, cascade.ToObjects(entityType).Cast<Entity>());
             }
 
             Purge<Deletion>(deletion);
@@ -2955,5 +3022,7 @@ namespace InABox.Database.SQLite
 
         #endregion
 
+        #endregion
+
     }
 }