Explorar el Código

Fix to database triggers and deletions

Kenric Nugteren hace 11 meses
padre
commit
7ce86aaedc

+ 7 - 0
InABox.Core/CoreUtils.cs

@@ -113,6 +113,9 @@ namespace InABox.Core
         /// <summary>
         /// Return all <see cref="Type"/> that have been loaded through use of the <see cref="RegisterClasses(Assembly[])"/> function.
         /// </summary>
+        /// <remarks>
+        /// This includes every non-abstract type.
+        /// </remarks>
         public static IEnumerable<Type> Entities => entities.Values;
 
         public static long GenerateSequence()
@@ -303,6 +306,10 @@ namespace InABox.Core
             ImportFactory.Register(typeof(FixedWidthImporter<>), "Fixed Width", "Text Files (*.txt)|*.txt");
         }
 
+        /// <summary>
+        /// Register into <see cref="Entities"/> all types in <paramref name="assemblies"/> that are not abstract.
+        /// </summary>
+        /// <param name="assemblies"></param>
         public static void RegisterClasses(params Assembly[] assemblies)
         {
             foreach(var type in assemblies.SelectMany(x => IterateTypes(x)).Where(x => !x.IsAbstract))

+ 25 - 7
InABox.Core/DatabaseSchema/DatabaseSchema.cs

@@ -215,10 +215,7 @@ namespace InABox.Core
                     {
                         RegisterProperties(master, prop.PropertyType, name + ".", newProperty, newProperties);
                     }
-                    else
-                    {
-                        newProperties.Add(newProperty.Name, newProperty);
-                    }
+                    newProperties.Add(newProperty.Name, newProperty);
                 }
 
                 RegisterSubObjects(type, subObjects);
@@ -318,10 +315,31 @@ namespace InABox.Core
 
         }
 
+        private static IEnumerable<IProperty> PropertiesInternal(Type type)
+            => CheckProperties(type)?.Select(x => x.Value) ?? Enumerable.Empty<IProperty>();
+
+        /// <summary>
+        /// Return the standard property list for <paramref name="type"/>; this includes nested properties.
+        /// </summary>
+        /// <param name="type"></param>
+        /// <returns></returns>
         public static IEnumerable<IProperty> Properties(Type type)
-        {
-            return CheckProperties(type)?.Select(x => x.Value) ?? Enumerable.Empty<IProperty>();
-        }
+            => PropertiesInternal(type).Where(x => !x.IsParent);
+
+        /// <summary>
+        /// Return all properties that are defined directly on <paramref name="type"/>, and does not follow sub objects, but rather includes the
+        /// sub object property itself.
+        /// </summary>
+        /// <param name="type"></param>
+        /// <returns></returns>
+        public static IEnumerable<IProperty> RootProperties(Type type)
+            => PropertiesInternal(type).Where(x => x.Parent is null);
+
+        /// <summary>
+        /// Return the standard property list for <paramref name="type"/>; this includes nested properties.
+        /// </summary>
+        /// <param name="type"></param>
+        /// <returns></returns>
         public static IEnumerable<IProperty> Properties<T>() => Properties(typeof(T));
         
         public static IProperty? Property(Type type, string name)

+ 28 - 0
InABox.Core/Filter.cs

@@ -1909,6 +1909,34 @@ namespace InABox.Core
             }
         }
 
+        public Filter<T>? CombineOr()
+        {
+            if(filters.Count == 0)
+            {
+                return new Filter<T>().None();
+            }
+            else if(filters.Count == 1)
+            {
+                return filters[0];
+            }
+            else
+            {
+                var result = new Filter<T>().None();
+                foreach (var filter in filters)
+                {
+                    if (filter != null)
+                    {
+                        if (result is null)
+                            result = filter;
+                        else
+                            result.Ors.Add(filter);
+                    }
+                }
+
+                return result;
+            }
+        }
+
         public static Filter<T>? Combine(params Filter<T>[] filters)
         {
             return new Filters<T>()

+ 13 - 96
InABox.Database/DbFactory.cs

@@ -49,12 +49,11 @@ public static class DbFactory
     // to be removed
     public static int RestPort { get; set; }
     public static int RPCPort { get; set; }
-    
-    //public static Type[] Entities { get { return entities; } set { SetEntityTypes(value); } }
-    public static IEnumerable<Type> Entities
-    {
-        get { return CoreUtils.Entities.Where(x => x.GetInterfaces().Contains(typeof(IPersistent))); }
-    }
+
+    /// <summary>
+    /// Return every <see cref="IPersistent"/> entity in <see cref="CoreUtils.Entities"/>.
+    /// </summary>
+    public static IEnumerable<Type> Entities => CoreUtils.Entities.Where(x => x.HasInterface<IPersistent>());
 
     public static Type[] Stores
     {
@@ -68,31 +67,12 @@ public static class DbFactory
     {
         CoreUtils.CheckLicensing();
         
-        var status = ValidateSchema();
-
-        if (status.Equals(SchemaStatus.New))
-            try
-            {
-                Provider.CreateSchema(ConsolidatedObjectModel().ToArray());
-                SaveSchema();
-            }
-            catch (Exception err)
-            {
-                throw new Exception(string.Format("Unable to Create Schema\n\n{0}", err.Message));
-            }
-        else if (status.Equals(SchemaStatus.Changed))
-            try
-            {
-                Provider.UpgradeSchema(ConsolidatedObjectModel().ToArray());
-                SaveSchema();
-            }
-            catch (Exception err)
-            {
-                throw new Exception(string.Format("Unable to Update Schema\n\n{0}", err.Message));
-            }
-
         // Start the provider
-        Provider.Types = ConsolidatedObjectModel();
+        Provider.Types = Entities.Where(x =>
+            x.GetTypeInfo().IsClass
+            && !x.GetTypeInfo().IsGenericType
+            && x.GetTypeInfo().IsSubclassOf(typeof(Entity))
+        ).ToArray();
 
         Provider.OnLog += LogMessage;
 
@@ -110,7 +90,7 @@ public static class DbFactory
         // Can't use clients (b/c we're inside the database layer already
         // but we can simply access the store directly :-)
         //CustomProperty[] props = FindStore<CustomProperty>("", "", "", "").Load(new Filter<CustomProperty>(x=>x.ID).IsNotEqualTo(Guid.Empty),null);
-        var props = Provider.Query<CustomProperty>().Rows.Select(x => x.ToObject<CustomProperty>()).ToArray();
+        var props = Provider.Query<CustomProperty>().ToArray<CustomProperty>();
         DatabaseSchema.Load(props);
 
         AssertLicense();
@@ -409,9 +389,9 @@ public static class DbFactory
         foreach (var type in Entities)
         {
             var key = type.EntityName();
-            if (config.ContainsKey(key))
+            if (config.TryGetValue(key, out bool value))
             {
-                if (config[key])
+                if (value)
                     //Logger.Send(LogType.Information, "", String.Format("{0} is enabled", key));
                     result.Add(type);
                 else
@@ -511,68 +491,5 @@ public static class DbFactory
         stores = types;
     }
 
-    private static Type[] ConsolidatedObjectModel()
-    {
-        // Add the core types from InABox.Core
-        var types = new List<Type>();
-        //var coreTypes = CoreUtils.TypeList(
-        //	new Assembly[] { typeof(Entity).Assembly },
-        //	myType =>
-        //	myType.IsClass
-        //	&& !myType.IsAbstract
-        //	&& !myType.IsGenericType
-        //	&& myType.IsSubclassOf(typeof(Entity))
-        //	&& myType.GetInterfaces().Contains(typeof(IRemotable))
-        //);
-        //types.AddRange(coreTypes);
-
-        // Now add the end-user object model
-        types.AddRange(Entities.Where(x =>
-            x.GetTypeInfo().IsClass
-            && !x.GetTypeInfo().IsGenericType
-            && x.GetTypeInfo().IsSubclassOf(typeof(Entity))
-        ));
-
-        return types.ToArray();
-    }
-
-    private enum SchemaStatus
-    {
-        New,
-        Changed,
-        Validated
-    }
-
-    private static Dictionary<string, Type> GetSchema()
-    {
-        var model = new Dictionary<string, Type>();
-        var objectmodel = ConsolidatedObjectModel();
-        foreach (var type in objectmodel)
-        {
-            Dictionary<string, Type> thismodel = CoreUtils.PropertyList(type, x => true, true);
-            foreach (var key in thismodel.Keys)
-                model[type.Name + "." + key] = thismodel[key];
-        }
-
-        return model;
-        //return Serialization.Serialize(model, Formatting.Indented);
-    }
-
-    private static SchemaStatus ValidateSchema()
-    {
-        var db_schema = Provider.GetSchema();
-        if (db_schema.Count() == 0)
-            return SchemaStatus.New;
-
-        var mdl_json = Serialization.Serialize(GetSchema());
-        var db_json = Serialization.Serialize(db_schema);
-        return mdl_json.Equals(db_json) ? SchemaStatus.Validated : SchemaStatus.Changed;
-    }
-
-    private static void SaveSchema()
-    {
-        Provider.SaveSchema(GetSchema());
-    }
-
     #endregion
 }

+ 52 - 57
InABox.Database/IProvider.cs

@@ -1,61 +1,56 @@
 using InABox.Core;
 
-namespace InABox.Database
+namespace InABox.Database;
+
+public delegate void LogEvent(LogType type, string message);
+
+public interface IProvider
 {
-    public delegate void LogEvent(LogType type, string message);
-
-    public interface IProvider
-    {
-        string URL { get; set; }
-        Type[] Types { get; set; }
-
-        Dictionary<string, Type> GetSchema();
-        void CreateSchema(params Type[] types);
-        void UpgradeSchema(params Type[] types);
-        void SaveSchema(Dictionary<string, Type> schema);
-        void ForceRecreateViews();
-
-        void Start();
-
-        IEnumerable<object[]> List<T>(Filter<T>? filter = null, Columns<T>? columns = null, SortOrder<T>? sort = null) where T : Entity, new();
-        
-        CoreTable Query<T>(Filter<T>? filter = null, Columns<T>? columns = null, SortOrder<T>? sort = null, 
-            int top = int.MaxValue, bool log = false, bool distinct = false) where T : Entity, new();
-        
-        CoreTable Query(Type type, IFilter? filter = null, IColumns? columns = null, ISortOrder? sort = null, 
-            int top = int.MaxValue, bool log = false, bool distinct = false);
-        
-        /// <summary>
-        /// Same as <see cref="Query{T}(Filter{T}, Columns{T}, SortOrder{T}, int, bool)"/>, but only for deleted items
-        /// </summary>
-        /// <typeparam name="T"></typeparam>
-        /// <param name="deletion"></param>
-        /// <param name="filter"></param>
-        /// <param name="columns"></param>
-        /// <param name="sort"></param>
-        /// <param name="top"></param>
-        /// <returns></returns>
-        CoreTable QueryDeleted<T>(Deletion deletion, Filter<T>? filter = null, Columns<T>? columns = null, SortOrder<T>? sort = null, int top = int.MaxValue, bool deleted = false) where T : Entity, new();
-
-        T[] Load<T>(Filter<T>? filter = null, SortOrder<T>? sort = null) where T : Entity, new();
-
-        void Save<T>(T entity) where T : Entity;
-        void Save<T>(IEnumerable<T> entities) where T : Entity;
-
-        void Save(Type type, Entity entity);
-        void Save(Type type, IEnumerable<Entity> entities);
-
-        void Delete<T>(T entity, string userID) where T : Entity, new();
-        void Delete<T>(IEnumerable<T> entities, string userID) where T : Entity, new();
-
-        void Purge<T>(T entity) where T : Entity;
-        void Purge<T>(IEnumerable<T> entities) where T : Entity;
-
-        void Purge(Deletion deletion);
-        void Recover(Deletion deletion);
-
-        List<Type> GetDeletionTypes(Deletion deletion);
-
-        event LogEvent OnLog;
-    }
+    string URL { get; set; }
+    Type[] Types { get; set; }
+
+    void ForceRecreateViews();
+
+    void Start();
+
+    IEnumerable<object[]> List<T>(Filter<T>? filter = null, Columns<T>? columns = null, SortOrder<T>? sort = null) where T : Entity, new();
+    
+    CoreTable Query<T>(Filter<T>? filter = null, Columns<T>? columns = null, SortOrder<T>? sort = null, 
+        int top = int.MaxValue, bool log = false, bool distinct = false) where T : Entity, new();
+    
+    CoreTable Query(Type type, IFilter? filter = null, IColumns? columns = null, ISortOrder? sort = null, 
+        int top = int.MaxValue, bool log = false, bool distinct = false);
+    
+    /// <summary>
+    /// Same as <see cref="Query{T}(Filter{T}, Columns{T}, SortOrder{T}, int, bool)"/>, but only for deleted items
+    /// </summary>
+    /// <typeparam name="T"></typeparam>
+    /// <param name="deletion"></param>
+    /// <param name="filter"></param>
+    /// <param name="columns"></param>
+    /// <param name="sort"></param>
+    /// <param name="top"></param>
+    /// <returns></returns>
+    CoreTable QueryDeleted<T>(Deletion deletion, Filter<T>? filter = null, Columns<T>? columns = null, SortOrder<T>? sort = null, int top = int.MaxValue, bool deleted = false) where T : Entity, new();
+
+    T[] Load<T>(Filter<T>? filter = null, SortOrder<T>? sort = null) where T : Entity, new();
+
+    void Save<T>(T entity) where T : Entity;
+    void Save<T>(IEnumerable<T> entities) where T : Entity;
+
+    void Save(Type type, Entity entity);
+    void Save(Type type, IEnumerable<Entity> entities);
+
+    void Delete<T>(T entity, string userID) where T : Entity, new();
+    void Delete<T>(IEnumerable<T> entities, string userID) where T : Entity, new();
+
+    void Purge<T>(T entity) where T : Entity;
+    void Purge<T>(IEnumerable<T> entities) where T : Entity;
+
+    void Purge(Deletion deletion);
+    void Recover(Deletion deletion);
+
+    List<Type> GetDeletionTypes(Deletion deletion);
+
+    event LogEvent OnLog;
 }

+ 104 - 151
inabox.database.sqlite/SQLiteProvider.cs

@@ -4,6 +4,7 @@ using System.Data.SQLite;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq.Expressions;
 using System.Reflection;
+using System.Resources;
 using System.Runtime.Serialization.Formatters.Binary;
 using System.Text;
 using InABox.Core;
@@ -93,14 +94,10 @@ namespace InABox.Database.SQLite
 
         private bool RebuildTriggers = false;
 
-        private SQLiteProvider()
-        {
-        }
-
         public SQLiteProvider(string filename)
         {
             var path = Path.GetDirectoryName(filename);
-            if (!Directory.Exists(path))
+            if (!path.IsNullOrWhiteSpace())
                 Directory.CreateDirectory(path);
             URL = filename;
         }
@@ -109,7 +106,10 @@ namespace InABox.Database.SQLite
 
         public event LogEvent? OnLog;
 
-        public Type[] Types { get; set; }
+        /// <summary>
+        /// An array containing every <see cref="Entity"/> type in the database.
+        /// </summary>
+        public Type[] Types { get; set; } = [];
 
         public void Start()
         {
@@ -150,32 +150,35 @@ namespace InABox.Database.SQLite
 
             //ExecuteSQL("PRAGMA foreign_keys = on;");
 
+            //using var profiler = new Profiler(true);
+
             // Need to arrange the typelist to ensure that foreign keys
             // refer to tables that already exist
             var ordered = new List<Type>();
             foreach (var type in Types)
                 LoadType(type, ordered);
 
+            //profiler.Log("Ordered");
+
             //Load up the metadata
             var metadata = LoadMetaData();
             
             var table = typeof(CustomProperty).EntityName().Split('.').Last();
-            if (!metadata.ContainsKey(table))
+            if (!metadata.TryGetValue(table, out var value))
             {
-                OnLog?.Invoke(LogType.Information, "Creating Table: " + typeof(CustomProperty).EntityName().Split('.').Last());
-                using (var access = GetWriteAccess())
-                {
-                    CreateTable(access, typeof(CustomProperty), true, new CustomProperty[] { });
-                }
+                OnLog?.Invoke(LogType.Information, $"Creating Table: {nameof(CustomProperty)}");
+
+                using var access = GetWriteAccess();
+                CreateTable(access, typeof(CustomProperty), true, []);
             }
             else
             {
-                using (var access = GetWriteAccess())
-                {
-                    CheckFields(access, typeof(CustomProperty), metadata[table].Item1, new CustomProperty[] { });
-                }
+                using var access = GetWriteAccess();
+                CheckFields(access, typeof(CustomProperty), value.Item1, []);
             }
 
+            //profiler.Log("Ordered");
+
             var customproperties = Load<CustomProperty>(); // new Filter<CustomProperty>(x => x.Class).IsEqualTo(type.EntityName()))
 
 
@@ -218,7 +221,6 @@ namespace InABox.Database.SQLite
                 if (type.GetCustomAttribute<AutoEntity>() == null)
 
                 {
-                    LoadDeletions(type);
                     table = type.EntityName().Split('.').Last();
                     using (var access = GetWriteAccess())
                     {
@@ -625,65 +627,80 @@ namespace InABox.Database.SQLite
             return result;
         }
 
-        private List<string> LoadTriggers(Type type)
+        private void LoadDeletions(Type type)
         {
-            var result = new List<string>();
-
-            // Get the EntityLink that is associated with this class
-            var linkclass = CoreUtils.TypeList(
-                new[] { type.Assembly },
-                x => typeof(IEntityLink).GetTypeInfo().IsAssignableFrom(x) && x.GetInheritedGenericTypeArguments().FirstOrDefault() == type
-            ).FirstOrDefault();
-
-            // if The entitylink does not exist, we don't need to do anything
-            if (linkclass == null)
-                return result;
+            var cascades = new List<Tuple<Type, List<string>>>();
+            var setNulls = new List<Tuple<Type, List<string>>>();
 
-            var actions = new List<string>();
-            var childtypes = Types.Where(x => /* (x != type) && */ x.IsSubclassOf(typeof(Entity)) && x.GetCustomAttribute<AutoEntity>() == null);
-            foreach (var childtype in childtypes)
+            foreach(var otherType in Types.Where(x => x.GetCustomAttribute<AutoEntity>() is null))
             {
-                // Get all registererd types for this entitylink
-                var fields = new List<string>();
-                var bDelete = false;
-
-                var tablename = childtype.EntityName().Split('.').Last();
-                // Find any IEntityLink<> properties that refer back to this class
-                var childprops = CoreUtils.PropertyList(childtype, x => x.PropertyType == linkclass);
+                var setNullFields = new List<string>();
+                var cascadeFields = new List<string>();
 
-                foreach (var childprop in childprops)
+                var props = DatabaseSchema.RootProperties(otherType)
+                    .Where(x => x.IsEntityLink && x.PropertyType.GetInterfaceDefinition(typeof(IEntityLink<>))?.GenericTypeArguments[0] == type);
+                foreach(var prop in props)
                 {
-                    var fieldname = string.Format("[{0}.ID]", childprop.Name);
-                    var attr = childprop.GetCustomAttributes(typeof(EntityRelationshipAttribute), true).FirstOrDefault();
-                    if (attr != null && ((EntityRelationshipAttribute)attr).Action.Equals(DeleteAction.Cascade))
+                    var fieldname = $"{prop.Name}.ID";
+                    if(prop.GetAttribute<EntityRelationshipAttribute>() is EntityRelationshipAttribute attr
+                        && attr.Action == DeleteAction.Cascade)
                     {
-                        fields.Clear();
-                        bDelete = true;
-                        fields.Add(fieldname);
-                        break;
+                        cascadeFields.Add(fieldname);
                     }
+                    else
+                    {
+                        setNullFields.Add(fieldname);
+                    }
+                }
+                cascadeFields.Sort();
+                setNullFields.Sort();
 
-                    fields.Add(fieldname);
+                cascades.Add(new(otherType, cascadeFields));
+                setNulls.Add(new(otherType, setNullFields));
+            }
 
-                    //actions[childtype] = String.Format("UPDATE {1} SET [{0}.ID] = NULL WHERE [id] = old.[ID];", tablename)
-                }
+            if(cascades.Count > 0)
+            {
+                _cascades[type] = cascades;
+            }
+            if(setNulls.Count > 0)
+            {
+                _setNulls[type] = setNulls;
+            }
+        }
+        private string? LoadTrigger(Type type)
+        {
+            var actions = new List<string>();
 
-                if (fields.Any())
+            if(_setNulls.TryGetValue(type, out var setNulls))
+            {
+                foreach(var (otherType, fields) in setNulls)
                 {
-                    if (bDelete)
-                        actions.Add(string.Format("DELETE FROM {0} WHERE {1} = old.ID;", tablename, fields.First()));
-                    else
-                        foreach (var field in fields)
-                            actions.Add(string.Format("UPDATE {0} SET {1} = NULL WHERE {1} = old.ID;", tablename, field));
+                    foreach(var field in fields)
+                    {
+                        actions.Add($"UPDATE {otherType.Name} SET [{field}] = NULL WHERE [{field}] = old.ID;");
+                    }
+                }
+            }
+            if(_cascades.TryGetValue(type, out var cascades))
+            {
+                foreach(var (otherType, fields) in cascades)
+                {
+                    foreach(var field in fields)
+                    {
+                        actions.Add($"DELETE FROM {otherType.Name} WHERE [{field}] = old.ID;");
+                    }
                 }
             }
 
-            if (actions.Any())
-                result.Add(string.Format("CREATE TRIGGER {0}_BEFOREDELETE BEFORE DELETE ON {0} FOR EACH ROW BEGIN {1} END",
-                    type.EntityName().Split('.').Last(), string.Join(" ", actions)));
-
-
-            return result;
+            if (actions.Count != 0)
+            {
+                return $"CREATE TRIGGER {type.Name}_BEFOREDELETE BEFORE DELETE ON {type.Name} FOR EACH ROW BEGIN {string.Join(' ', actions)} END";
+            }
+            else
+            {
+                return null;
+            }
         }
 
         public void ForceRecreateViews()
@@ -1134,19 +1151,24 @@ namespace InABox.Database.SQLite
 
         private void CheckTriggers(SQLiteWriteAccessor access, Type type, Dictionary<string, string> db_triggers)
         {
+            LoadDeletions(type);
+
             /*
 #if PURGE
             foreach (var trigger in db_triggers.Keys)
                 ExecuteSQL(access, string.Format("DROP TRIGGER {0}", trigger));
 #else*/
-            var type_triggers = LoadTriggers(type);
+            var type_trigger = LoadTrigger(type);
 
-            foreach (var trigger in db_triggers.Keys)
-                if (!type_triggers.Contains(db_triggers[trigger]))
-                    ExecuteSQL(access, string.Format("DROP TRIGGER {0}", trigger));
-            foreach (var trigger in type_triggers)
-                if (!db_triggers.ContainsValue(trigger))
-                    ExecuteSQL(access, trigger);
+            foreach (var (key, trigger) in db_triggers)
+                if (!Equals(type_trigger, trigger))
+                    ExecuteSQL(access, $"DROP TRIGGER {key}");
+
+            if(type_trigger is not null)
+            {
+                if (!db_triggers.ContainsValue(type_trigger))
+                    ExecuteSQL(access, type_trigger);
+            }
 //#endif
         }
 
@@ -2363,29 +2385,6 @@ namespace InABox.Database.SQLite
 
         #endregion
 
-        #region Schema Handling
-
-        public Dictionary<string, Type> GetSchema()
-        {
-            var result = new Dictionary<string, Type>();
-            return result;
-        }
-
-
-        public void CreateSchema(params Type[] types)
-        {
-        }
-
-        public void SaveSchema(Dictionary<string, Type> schema)
-        {
-        }
-
-        public void UpgradeSchema(params Type[] types)
-        {
-        }
-
-        #endregion
-
         #region CRUD Operations
         
         public static object[] GetValues(IDataReader reader, int count)
@@ -2886,68 +2885,14 @@ namespace InABox.Database.SQLite
             }
         }
 
-        private Dictionary<Type, List<Tuple<Type, string>>> _cascades = new();
+        private Dictionary<Type, List<Tuple<Type, List<string>>>> _cascades = new();
         private Dictionary<Type, List<Tuple<Type, List<string>>>> _setNulls = new();
 
         private const int deleteBatchSize = 100;
 
-        private void LoadDeletions(Type type)
-        {
-            // Get the EntityLink that is associated with this class
-            var linkclass = CoreUtils.TypeList(
-                new[] { type.Assembly },
-                x => typeof(IEntityLink).GetTypeInfo().IsAssignableFrom(x) && x.GetInheritedGenericTypeArguments().FirstOrDefault() == type
-            ).FirstOrDefault();
-
-            // if The entitylink does not exist, we don't need to do anything
-            if (linkclass == null)
-                return;
-
-            var cascades = new List<Tuple<Type, string>>();
-            var setNulls = new List<Tuple<Type, List<string>>>();
-
-            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
-                var fields = new List<string>();
-                var bDelete = false;
-
-                // Find any IEntityLink<> properties that refer back to this class
-                var childprops = CoreUtils.PropertyList(childtype, x => x.PropertyType == linkclass);
-
-                foreach (var childprop in childprops)
-                {
-                    var fieldname = string.Format("{0}.ID", childprop.Name);
-                    var attr = childprop.GetCustomAttributes(typeof(EntityRelationshipAttribute), true).FirstOrDefault();
-                    if (attr != null && ((EntityRelationshipAttribute)attr).Action.Equals(DeleteAction.Cascade))
-                    {
-                        cascades.Add(new(childtype, fieldname));
-                        bDelete = true;
-                        break;
-                    }
-
-                    fields.Add(fieldname);
-                }
-                if(!bDelete && fields.Any())
-                {
-                    setNulls.Add(new(childtype, fields));
-                }
-            }
-
-            if(cascades.Count > 0)
-            {
-                _cascades[type] = cascades;
-            }
-            if(setNulls.Count > 0)
-            {
-                _setNulls[type] = setNulls;
-            }
-        }
-
         private readonly MethodInfo _deleteEntitiesMethod = typeof(SQLiteProvider).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
             .Single(x => x.Name == nameof(DeleteEntity) && x.IsGenericMethod);
-        private void DeleteEntity<T>(Guid[] parentIDs, string parentField, DeletionData deletionData) where T : Entity, new()
+        private void DeleteEntity<T>(Guid[] parentIDs, List<string> parentFields, DeletionData deletionData) where T : Entity, new()
         {
             var columns = DeletionData.DeletionColumns<T>();
 
@@ -2956,7 +2901,15 @@ namespace InABox.Database.SQLite
             for (int i = 0; i < parentIDs.Length; i += deleteBatchSize)
             {
                 var items = new ArraySegment<Guid>(parentIDs, i, Math.Min(deleteBatchSize, parentIDs.Length - i));
-                var entities = Query(new Filter<T>(parentField).InList(items.ToArray()), columns);
+                var ids = items.ToArray();
+
+                var filter = new Filters<T>();
+                foreach(var field in parentFields)
+                {
+                    filter.Add(new Filter<T>(field).InList(ids));
+                }
+
+                var entities = Query(filter.CombineOr(), columns);
                 foreach (var row in entities.Rows)
                 {
                     deletionData.DeleteEntity<T>(row);
@@ -2966,9 +2919,9 @@ namespace InABox.Database.SQLite
             CascadeDelete(typeof(T), entityIDs.ToArray(), deletionData);
         }
        
-        private void DeleteEntity(Type T, Guid[] parentIDs, string parentField, DeletionData deletionData)
+        private void DeleteEntity(Type T, Guid[] parentIDs, List<string> parentFields, DeletionData deletionData)
         {
-            _deleteEntitiesMethod.MakeGenericMethod(T).Invoke(this, new object?[] { parentIDs, parentField, deletionData });
+            _deleteEntitiesMethod.MakeGenericMethod(T).Invoke(this, new object?[] { parentIDs, parentFields, deletionData });
         }
 
         private readonly MethodInfo _setNullEntityMethod = typeof(SQLiteProvider).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)