|
@@ -4,6 +4,7 @@ using System.Data.SQLite;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq.Expressions;
|
|
using System.Linq.Expressions;
|
|
using System.Reflection;
|
|
using System.Reflection;
|
|
|
|
+using System.Resources;
|
|
using System.Runtime.Serialization.Formatters.Binary;
|
|
using System.Runtime.Serialization.Formatters.Binary;
|
|
using System.Text;
|
|
using System.Text;
|
|
using InABox.Core;
|
|
using InABox.Core;
|
|
@@ -93,14 +94,10 @@ namespace InABox.Database.SQLite
|
|
|
|
|
|
private bool RebuildTriggers = false;
|
|
private bool RebuildTriggers = false;
|
|
|
|
|
|
- private SQLiteProvider()
|
|
|
|
- {
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
public SQLiteProvider(string filename)
|
|
public SQLiteProvider(string filename)
|
|
{
|
|
{
|
|
var path = Path.GetDirectoryName(filename);
|
|
var path = Path.GetDirectoryName(filename);
|
|
- if (!Directory.Exists(path))
|
|
|
|
|
|
+ if (!path.IsNullOrWhiteSpace())
|
|
Directory.CreateDirectory(path);
|
|
Directory.CreateDirectory(path);
|
|
URL = filename;
|
|
URL = filename;
|
|
}
|
|
}
|
|
@@ -109,7 +106,10 @@ namespace InABox.Database.SQLite
|
|
|
|
|
|
public event LogEvent? OnLog;
|
|
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()
|
|
public void Start()
|
|
{
|
|
{
|
|
@@ -150,32 +150,35 @@ namespace InABox.Database.SQLite
|
|
|
|
|
|
//ExecuteSQL("PRAGMA foreign_keys = on;");
|
|
//ExecuteSQL("PRAGMA foreign_keys = on;");
|
|
|
|
|
|
|
|
+ //using var profiler = new Profiler(true);
|
|
|
|
+
|
|
// Need to arrange the typelist to ensure that foreign keys
|
|
// Need to arrange the typelist to ensure that foreign keys
|
|
// refer to tables that already exist
|
|
// refer to tables that already exist
|
|
var ordered = new List<Type>();
|
|
var ordered = new List<Type>();
|
|
foreach (var type in Types)
|
|
foreach (var type in Types)
|
|
LoadType(type, ordered);
|
|
LoadType(type, ordered);
|
|
|
|
|
|
|
|
+ //profiler.Log("Ordered");
|
|
|
|
+
|
|
//Load up the metadata
|
|
//Load up the metadata
|
|
var metadata = LoadMetaData();
|
|
var metadata = LoadMetaData();
|
|
|
|
|
|
var table = typeof(CustomProperty).EntityName().Split('.').Last();
|
|
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
|
|
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()))
|
|
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)
|
|
if (type.GetCustomAttribute<AutoEntity>() == null)
|
|
|
|
|
|
{
|
|
{
|
|
- LoadDeletions(type);
|
|
|
|
table = type.EntityName().Split('.').Last();
|
|
table = type.EntityName().Split('.').Last();
|
|
using (var access = GetWriteAccess())
|
|
using (var access = GetWriteAccess())
|
|
{
|
|
{
|
|
@@ -625,65 +627,80 @@ namespace InABox.Database.SQLite
|
|
return result;
|
|
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()
|
|
public void ForceRecreateViews()
|
|
@@ -1134,19 +1151,24 @@ namespace InABox.Database.SQLite
|
|
|
|
|
|
private void CheckTriggers(SQLiteWriteAccessor access, Type type, Dictionary<string, string> db_triggers)
|
|
private void CheckTriggers(SQLiteWriteAccessor access, Type type, Dictionary<string, string> db_triggers)
|
|
{
|
|
{
|
|
|
|
+ LoadDeletions(type);
|
|
|
|
+
|
|
/*
|
|
/*
|
|
#if PURGE
|
|
#if PURGE
|
|
foreach (var trigger in db_triggers.Keys)
|
|
foreach (var trigger in db_triggers.Keys)
|
|
ExecuteSQL(access, string.Format("DROP TRIGGER {0}", trigger));
|
|
ExecuteSQL(access, string.Format("DROP TRIGGER {0}", trigger));
|
|
#else*/
|
|
#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
|
|
//#endif
|
|
}
|
|
}
|
|
|
|
|
|
@@ -2363,29 +2385,6 @@ namespace InABox.Database.SQLite
|
|
|
|
|
|
#endregion
|
|
#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
|
|
#region CRUD Operations
|
|
|
|
|
|
public static object[] GetValues(IDataReader reader, int count)
|
|
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 Dictionary<Type, List<Tuple<Type, List<string>>>> _setNulls = new();
|
|
|
|
|
|
private const int deleteBatchSize = 100;
|
|
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)
|
|
private readonly MethodInfo _deleteEntitiesMethod = typeof(SQLiteProvider).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
|
|
.Single(x => x.Name == nameof(DeleteEntity) && x.IsGenericMethod);
|
|
.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>();
|
|
var columns = DeletionData.DeletionColumns<T>();
|
|
|
|
|
|
@@ -2956,7 +2901,15 @@ namespace InABox.Database.SQLite
|
|
for (int i = 0; i < parentIDs.Length; i += deleteBatchSize)
|
|
for (int i = 0; i < parentIDs.Length; i += deleteBatchSize)
|
|
{
|
|
{
|
|
var items = new ArraySegment<Guid>(parentIDs, i, Math.Min(deleteBatchSize, parentIDs.Length - i));
|
|
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)
|
|
foreach (var row in entities.Rows)
|
|
{
|
|
{
|
|
deletionData.DeleteEntity<T>(row);
|
|
deletionData.DeleteEntity<T>(row);
|
|
@@ -2966,9 +2919,9 @@ namespace InABox.Database.SQLite
|
|
CascadeDelete(typeof(T), entityIDs.ToArray(), deletionData);
|
|
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)
|
|
private readonly MethodInfo _setNullEntityMethod = typeof(SQLiteProvider).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
|