diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/macOS/sqlcipher.bundle.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/macOS/sqlcipher.bundle.meta index 5981ee3..dbe4d26 100644 --- a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/macOS/sqlcipher.bundle.meta +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/macOS/sqlcipher.bundle.meta @@ -20,6 +20,7 @@ PluginImporter: Exclude Editor: 0 Exclude Linux64: 1 Exclude OSXUniversal: 0 + Exclude WebGL: 1 Exclude Win: 1 Exclude Win64: 1 Exclude iOS: 1 @@ -40,7 +41,7 @@ PluginImporter: second: enabled: 1 settings: - CPU: x86_64 + CPU: ARM64 DefaultValueInitialized: true OS: OSX - first: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/libcrypto-1_1-x64.dll b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/libcrypto-1_1-x64.dll deleted file mode 100644 index f746f0e..0000000 Binary files a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/libcrypto-1_1-x64.dll and /dev/null differ diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/libcrypto-1_1-x64.dll.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/libcrypto-1_1-x64.dll.meta deleted file mode 100644 index e0d2f93..0000000 --- a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/libcrypto-1_1-x64.dll.meta +++ /dev/null @@ -1,91 +0,0 @@ -fileFormatVersion: 2 -guid: 4905f87dcd14491469d1286d50ef52c4 -PluginImporter: - externalObjects: {} - serializedVersion: 2 - iconMap: {} - executionOrder: {} - defineConstraints: [] - isPreloaded: 0 - isOverridable: 0 - isExplicitlyReferenced: 0 - validateReferences: 1 - platformData: - - first: - : Any - second: - enabled: 0 - settings: - Exclude Android: 1 - Exclude Editor: 0 - Exclude Linux64: 0 - Exclude OSXUniversal: 0 - Exclude Win: 1 - Exclude Win64: 0 - Exclude WindowsStoreApps: 0 - Exclude iOS: 1 - - first: - Android: Android - second: - enabled: 0 - settings: - CPU: ARMv7 - - first: - Any: - second: - enabled: 0 - settings: {} - - first: - Editor: Editor - second: - enabled: 1 - settings: - CPU: x86_64 - DefaultValueInitialized: true - OS: Windows - - first: - Standalone: Linux64 - second: - enabled: 1 - settings: - CPU: AnyCPU - - first: - Standalone: OSXUniversal - second: - enabled: 1 - settings: - CPU: x86_64 - - first: - Standalone: Win - second: - enabled: 0 - settings: - CPU: None - - first: - Standalone: Win64 - second: - enabled: 1 - settings: - CPU: AnyCPU - - first: - Windows Store Apps: WindowsStoreApps - second: - enabled: 1 - settings: - CPU: X64 - DontProcess: false - PlaceholderPath: - SDK: UWP - ScriptingBackend: AnyScriptingBackend - - first: - iPhone: iOS - second: - enabled: 0 - settings: - AddToEmbeddedBinaries: false - CPU: AnyCPU - CompileFlags: - FrameworkDependencies: - userData: - assetBundleName: - assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/libcrypto.so b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/libcrypto.so deleted file mode 100644 index dd28eec..0000000 Binary files a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/libcrypto.so and /dev/null differ diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/libcrypto.so.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/libcrypto.so.meta deleted file mode 100644 index e5d8761..0000000 --- a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/libcrypto.so.meta +++ /dev/null @@ -1,115 +0,0 @@ -fileFormatVersion: 2 -guid: df3b1f3ce94281c459ecdc390f8ae257 -PluginImporter: - externalObjects: {} - serializedVersion: 2 - iconMap: {} - executionOrder: {} - defineConstraints: [] - isPreloaded: 0 - isOverridable: 0 - isExplicitlyReferenced: 0 - validateReferences: 1 - platformData: - - first: - : Any - second: - enabled: 0 - settings: - Exclude Android: 1 - Exclude Editor: 0 - Exclude Linux: 0 - Exclude Linux64: 0 - Exclude LinuxUniversal: 0 - Exclude OSXUniversal: 0 - Exclude Win: 0 - Exclude Win64: 0 - Exclude iOS: 1 - Exclude tvOS: 1 - - first: - Android: Android - second: - enabled: 0 - settings: - CPU: ARMv7 - - first: - Any: - second: - enabled: 0 - settings: {} - - first: - Editor: Editor - second: - enabled: 1 - settings: - CPU: x86_64 - DefaultValueInitialized: true - OS: Linux - - first: - Facebook: Win - second: - enabled: 0 - settings: - CPU: None - - first: - Facebook: Win64 - second: - enabled: 1 - settings: - CPU: AnyCPU - - first: - Standalone: Linux - second: - enabled: 1 - settings: - CPU: None - - first: - Standalone: Linux64 - second: - enabled: 1 - settings: - CPU: x86_64 - - first: - Standalone: LinuxUniversal - second: - enabled: 1 - settings: - CPU: x86_64 - - first: - Standalone: OSXUniversal - second: - enabled: 1 - settings: - CPU: x86_64 - - first: - Standalone: Win - second: - enabled: 1 - settings: - CPU: None - - first: - Standalone: Win64 - second: - enabled: 1 - settings: - CPU: AnyCPU - - first: - iPhone: iOS - second: - enabled: 0 - settings: - AddToEmbeddedBinaries: false - CPU: AnyCPU - CompileFlags: - FrameworkDependencies: - - first: - tvOS: tvOS - second: - enabled: 0 - settings: - CPU: AnyCPU - CompileFlags: - FrameworkDependencies: - userData: - assetBundleName: - assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/libsqlcipher.so b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/libsqlcipher.so deleted file mode 100644 index ea44784..0000000 Binary files a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/libsqlcipher.so and /dev/null differ diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/libsqlcipher.so.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/libsqlcipher.so.meta deleted file mode 100644 index d7459f2..0000000 --- a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/libsqlcipher.so.meta +++ /dev/null @@ -1,115 +0,0 @@ -fileFormatVersion: 2 -guid: 25342efbb63fb5346b0100dc754b2c91 -PluginImporter: - externalObjects: {} - serializedVersion: 2 - iconMap: {} - executionOrder: {} - defineConstraints: [] - isPreloaded: 0 - isOverridable: 0 - isExplicitlyReferenced: 0 - validateReferences: 1 - platformData: - - first: - : Any - second: - enabled: 0 - settings: - Exclude Android: 1 - Exclude Editor: 0 - Exclude Linux: 0 - Exclude Linux64: 0 - Exclude LinuxUniversal: 0 - Exclude OSXUniversal: 0 - Exclude Win: 0 - Exclude Win64: 0 - Exclude iOS: 1 - Exclude tvOS: 1 - - first: - Android: Android - second: - enabled: 0 - settings: - CPU: ARMv7 - - first: - Any: - second: - enabled: 0 - settings: {} - - first: - Editor: Editor - second: - enabled: 1 - settings: - CPU: x86_64 - DefaultValueInitialized: true - OS: Linux - - first: - Facebook: Win - second: - enabled: 0 - settings: - CPU: None - - first: - Facebook: Win64 - second: - enabled: 1 - settings: - CPU: AnyCPU - - first: - Standalone: Linux - second: - enabled: 1 - settings: - CPU: None - - first: - Standalone: Linux64 - second: - enabled: 1 - settings: - CPU: x86_64 - - first: - Standalone: LinuxUniversal - second: - enabled: 1 - settings: - CPU: x86_64 - - first: - Standalone: OSXUniversal - second: - enabled: 1 - settings: - CPU: x86_64 - - first: - Standalone: Win - second: - enabled: 1 - settings: - CPU: None - - first: - Standalone: Win64 - second: - enabled: 1 - settings: - CPU: AnyCPU - - first: - iPhone: iOS - second: - enabled: 0 - settings: - AddToEmbeddedBinaries: false - CPU: AnyCPU - CompileFlags: - FrameworkDependencies: - - first: - tvOS: tvOS - second: - enabled: 0 - settings: - CPU: AnyCPU - CompileFlags: - FrameworkDependencies: - userData: - assetBundleName: - assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/sqlcipher.dll b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/sqlcipher.dll deleted file mode 100644 index 143413a..0000000 Binary files a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/sqlcipher.dll and /dev/null differ diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/sqlcipher.dll.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/sqlcipher.dll.meta deleted file mode 100644 index 9a5af6a..0000000 --- a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64/sqlcipher.dll.meta +++ /dev/null @@ -1,91 +0,0 @@ -fileFormatVersion: 2 -guid: 553c4748fe7ee42cda362ee103818e60 -PluginImporter: - externalObjects: {} - serializedVersion: 2 - iconMap: {} - executionOrder: {} - defineConstraints: [] - isPreloaded: 0 - isOverridable: 0 - isExplicitlyReferenced: 0 - validateReferences: 1 - platformData: - - first: - : Any - second: - enabled: 0 - settings: - Exclude Android: 1 - Exclude Editor: 0 - Exclude Linux64: 0 - Exclude OSXUniversal: 0 - Exclude Win: 1 - Exclude Win64: 0 - Exclude WindowsStoreApps: 0 - Exclude iOS: 1 - - first: - Android: Android - second: - enabled: 0 - settings: - CPU: ARMv7 - - first: - Any: - second: - enabled: 0 - settings: {} - - first: - Editor: Editor - second: - enabled: 1 - settings: - CPU: x86_64 - DefaultValueInitialized: true - OS: Windows - - first: - Standalone: Linux64 - second: - enabled: 1 - settings: - CPU: AnyCPU - - first: - Standalone: OSXUniversal - second: - enabled: 1 - settings: - CPU: x86_64 - - first: - Standalone: Win - second: - enabled: 0 - settings: - CPU: None - - first: - Standalone: Win64 - second: - enabled: 1 - settings: - CPU: AnyCPU - - first: - Windows Store Apps: WindowsStoreApps - second: - enabled: 1 - settings: - CPU: X64 - DontProcess: false - PlaceholderPath: - SDK: UWP - ScriptingBackend: AnyScriptingBackend - - first: - iPhone: iOS - second: - enabled: 0 - settings: - AddToEmbeddedBinaries: false - CPU: AnyCPU - CompileFlags: - FrameworkDependencies: - userData: - assetBundleName: - assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/SQLiteAsync.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/SQLiteAsync.cs index 2d4254d..a722ff3 100644 --- a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/SQLiteAsync.cs +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/SQLiteAsync.cs @@ -33,6 +33,9 @@ namespace SqlCipher4Unity3D /// /// A pooled asynchronous connection to a SQLite database. /// + #if !SQLITEASYNC_UNITASK + + public partial class SQLiteAsyncConnection { readonly SQLiteConnectionString _connectionString; @@ -1492,7 +1495,7 @@ public void Reset() } } } - + #endif /// /// This is a normal connection except it contains a Lock method that /// can be used to serialize access to the database diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration.meta similarity index 57% rename from SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64.meta rename to SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration.meta index 049d532..b59a2d4 100644 --- a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/Plugins/x64.meta +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration.meta @@ -1,9 +1,8 @@ fileFormatVersion: 2 -guid: d080ad26255213a459eabfbd537e41b5 +guid: fa0c4fa0c60734926b50bc48e9c4deae folderAsset: yes -timeCreated: 1445131378 -licenseType: Free DefaultImporter: + externalObjects: {} userData: assetBundleName: assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Exceptions.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Exceptions.meta new file mode 100644 index 0000000..9a6b33a --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Exceptions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d54b9df3016c409bb2673af7b56d4329 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Exceptions/IncorrectRelationshipExtension.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Exceptions/IncorrectRelationshipExtension.cs new file mode 100644 index 0000000..00df57d --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Exceptions/IncorrectRelationshipExtension.cs @@ -0,0 +1,15 @@ +namespace SqlCipher4Unity3D.UniTaskIntegration.Exceptions +{ + using System; + + public class IncorrectRelationshipException : Exception + { + public string PropertyName { get; set; } + public string TypeName { get; set; } + + public IncorrectRelationshipException(string typeName, string propertyName, string message) + : base(string.Format("{0}.{1}: {2}", typeName, propertyName, message)) + { + } + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Exceptions/IncorrectRelationshipExtension.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Exceptions/IncorrectRelationshipExtension.cs.meta new file mode 100644 index 0000000..dc3bfc6 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Exceptions/IncorrectRelationshipExtension.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f302f7ffa17740e8ad476b0e9490f399 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions.meta new file mode 100644 index 0000000..bc75c37 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8ded37c3131a435f8a66de2ed83494fe +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions.meta new file mode 100644 index 0000000..99e7535 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 05aab3a30e854ae3aa8ec48eabb62ae8 +timeCreated: 1644966093 \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions/ReadOperations.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions/ReadOperations.cs new file mode 100644 index 0000000..2e9c0c4 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions/ReadOperations.cs @@ -0,0 +1,204 @@ +using SQLite.Attributes; + +[assembly: Preserve] +namespace SqlCipher4Unity3D.UniTaskIntegration.Extensions.AsyncExtensions +{ + using System; + using System.Collections.Generic; + using System.Linq.Expressions; + using System.Reflection; + using System.Threading; + using Cysharp.Threading.Tasks; + using SQLite.Attributes; + + + [Preserve] + public static class ReadOperations + { + #region Public API + + /// + /// Fetches all the entities of the specified type with the filter and fetches all the relationship + /// properties of all the returned elements. + /// + /// List of all the elements of the type T that matches the filter with the children already loaded + /// SQLite Net connection object + /// Filter that will be passed to the Where clause when fetching + /// objects from the database. No relationship properties are allowed in this filter as they + /// are loaded afterwards + /// If set to true all the relationships with + /// CascadeOperation.CascadeRead will be loaded recusively. + /// Cancellation token + /// Entity type where the object should be fetched from + public static UniTask> GetAllWithChildrenAsync(this SQLiteAsyncConnection conn, Expression> filter = null, bool recursive = false, CancellationToken cancellationToken = default(CancellationToken)) + where T : new() + { + return UniTask.RunOnThreadPool(() => + { + cancellationToken.ThrowIfCancellationRequested(); + + var connectionWithLock = SqliteAsyncConnectionWrapper.Lock(conn); + using (connectionWithLock.Lock()) + { + cancellationToken.ThrowIfCancellationRequested(); + return connectionWithLock.GetAllWithChildren(filter, recursive); + } + }, false, cancellationToken); + } + + /// + /// Obtains the object from the database and fetches all the properties annotated with + /// any subclass of RelationshipAttribute. If the object with the specified primary key doesn't + /// exist in the database, an exception will be raised. + /// + /// The object with all the children loaded + /// SQLite Net connection object + /// Primary key for the object to search in the database + /// If set to true all the relationships with + /// CascadeOperation.CascadeRead will be loaded recusively. + /// Cancellation token + /// Entity type where the object should be fetched from + public static UniTask GetWithChildrenAsync(this SQLiteAsyncConnection conn, object pk, bool recursive = false, CancellationToken cancellationToken = default(CancellationToken)) + where T : new() + { + return UniTask.RunOnThreadPool(() => + { + cancellationToken.ThrowIfCancellationRequested(); + + var connectionWithLock = SqliteAsyncConnectionWrapper.Lock(conn); + using (connectionWithLock.Lock()) + { + cancellationToken.ThrowIfCancellationRequested(); + return connectionWithLock.GetWithChildren(pk, recursive); + } + },false, cancellationToken); + } + + /// + /// The behavior is the same that GetWithChildren but it returns null if the object doesn't + /// exist in the database instead of throwing an exception + /// Obtains the object from the database and fetch all the properties annotated with + /// any subclass of RelationshipAttribute. If the object with the specified primary key doesn't + /// exist in the database, it will return null + /// + /// The object with all the children loaded or null if it doesn't exist + /// SQLite Net connection object + /// Primary key for the object to search in the database + /// If set to true all the relationships with + /// CascadeOperation.CascadeRead will be loaded recusively. + /// Cancellation token + /// Entity type where the object should be fetched from + public static UniTask FindWithChildrenAsync(this SQLiteAsyncConnection conn, object pk, bool recursive = false, CancellationToken cancellationToken = default(CancellationToken)) + where T : new() + { + return UniTask.RunOnThreadPool(() => + { + cancellationToken.ThrowIfCancellationRequested(); + + var connectionWithLock = SqliteAsyncConnectionWrapper.Lock(conn); + using (connectionWithLock.Lock()) + { + cancellationToken.ThrowIfCancellationRequested(); + return connectionWithLock.FindWithChildren(pk, recursive); + } + }, false , cancellationToken); + } + + /// + /// Fetches all the properties annotated with any subclass of RelationshipAttribute of the current + /// object and keeps fetching recursively if the recursive flag has been set. + /// + /// SQLite Net connection object + /// Element used to load all the relationship properties + /// If set to true all the relationships with + /// CascadeOperation.CascadeRead will be loaded recusively. + /// Cancellation token + /// Entity type where the object should be fetched from + public static UniTask GetChildrenAsync(this SQLiteAsyncConnection conn, T element, bool recursive = false, CancellationToken cancellationToken = default(CancellationToken)) + { + return UniTask.RunOnThreadPool(() => + { + cancellationToken.ThrowIfCancellationRequested(); + + var connectionWithLock = SqliteAsyncConnectionWrapper.Lock(conn); + using (connectionWithLock.Lock()) + { + cancellationToken.ThrowIfCancellationRequested(); + connectionWithLock.GetChildren(element, recursive); + } + }, false, cancellationToken); + } + + /// + /// Fetches a specific property of the current object and keeps fetching recursively if the + /// recursive flag has been set. + /// + /// SQLite Net connection object + /// Element used to load all the relationship properties + /// Name of the property to fetch from the database + /// If set to true all the relationships with + /// CascadeOperation.CascadeRead will be loaded recusively. + /// Cancellation token + /// Entity type where the object should be fetched from + public static UniTask GetChildAsync(this SQLiteAsyncConnection conn, T element, string relationshipProperty, bool recursive = false, CancellationToken cancellationToken = default(CancellationToken)) + { + return UniTask.RunOnThreadPool(() => + { + cancellationToken.ThrowIfCancellationRequested(); + + var connectionWithLock = SqliteAsyncConnectionWrapper.Lock(conn); + using (connectionWithLock.Lock()) + { + cancellationToken.ThrowIfCancellationRequested(); + connectionWithLock.GetChild(element, element.GetType().GetRuntimeProperty(relationshipProperty), recursive); + } + }, false, cancellationToken); + } + + /// + /// Fetches a specific property of the current object and keeps fetching recursively if the + /// recursive flag has been set. + /// + /// SQLite Net connection object + /// Element used to load all the relationship properties + /// Expression that returns the property to be loaded from the database. + /// This variant is useful to avoid spelling mistakes and make the code refactor-safe. + /// If set to true all the relationships with + /// CascadeOperation.CascadeRead will be loaded recusively. + /// Cancellation token + /// Entity type where the object should be fetched from + public static UniTask GetChildAsync(this SQLiteAsyncConnection conn, T element, Expression> propertyExpression, bool recursive = false, CancellationToken cancellationToken = default(CancellationToken)) + { + return conn.GetChildAsync(element, ReflectionExtensions.GetProperty(propertyExpression), recursive, cancellationToken); + } + + /// + /// Fetches a specific property of the current object and keeps fetching recursively if the + /// recursive flag has been set. + /// + /// SQLite Net connection object + /// Element used to load all the relationship properties + /// Property to load from the database + /// If set to true all the relationships with + /// CascadeOperation.CascadeRead will be loaded recusively. + /// Cancellation token + /// Entity type where the object should be fetched from + public static UniTask GetChildAsync(this SQLiteAsyncConnection conn, T element, PropertyInfo relationshipProperty, bool recursive = false, CancellationToken cancellationToken = default(CancellationToken)) + { + return UniTask.RunOnThreadPool(() => + { + cancellationToken.ThrowIfCancellationRequested(); + + var connectionWithLock = SqliteAsyncConnectionWrapper.Lock(conn); + using (connectionWithLock.Lock()) + { + cancellationToken.ThrowIfCancellationRequested(); + connectionWithLock.GetChild(element, relationshipProperty, recursive); + } + }, false, cancellationToken); + } + + #endregion + + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions/ReadOperations.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions/ReadOperations.cs.meta new file mode 100644 index 0000000..0022221 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions/ReadOperations.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4a0eaf3efaf44ae3b0090c1728a45c6f +timeCreated: 1644966093 \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions/SqliteAsyncConnectionWrapper.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions/SqliteAsyncConnectionWrapper.cs new file mode 100644 index 0000000..f164c0c --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions/SqliteAsyncConnectionWrapper.cs @@ -0,0 +1,30 @@ +namespace SqlCipher4Unity3D.UniTaskIntegration.Extensions.AsyncExtensions +{ + using System; + using System.Reflection; + using SqlCipher4Unity3D; + + public static class SqliteAsyncConnectionWrapper + { + private static readonly MethodInfo GetConnectionMethodInfo = typeof(SQLiteAsyncConnection).GetTypeInfo().GetDeclaredMethod("GetConnection"); + + private static SQLiteConnectionWithLock GetConnectionWithLock(SQLiteAsyncConnection asyncConnection) + { + return (SQLiteConnectionWithLock) GetConnectionMethodInfo.Invoke(asyncConnection, null); + } + + public static SQLiteConnectionWithLock Lock(SQLiteAsyncConnection asyncConnection) + { + return GetConnectionWithLock(asyncConnection); + } + } + + public static class SqliteConnectionExtensions + { + public static IDisposable Lock(this SQLiteConnectionWithLock connection) + { + var lockMethod = connection.GetType().GetTypeInfo().GetDeclaredMethod("Lock"); + return (IDisposable)lockMethod.Invoke(connection, null); + } + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions/SqliteAsyncConnectionWrapper.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions/SqliteAsyncConnectionWrapper.cs.meta new file mode 100644 index 0000000..e97e384 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions/SqliteAsyncConnectionWrapper.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 112042533cd14407a281cf58324c697e +timeCreated: 1644966093 \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions/WriteOperations.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions/WriteOperations.cs new file mode 100644 index 0000000..8769df9 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions/WriteOperations.cs @@ -0,0 +1,194 @@ +namespace SqlCipher4Unity3D.UniTaskIntegration.Extensions.AsyncExtensions +{ + using System.Collections; + using System.Collections.Generic; + using Cysharp.Threading.Tasks; + using SqlCipher4Unity3D; + + public static class WriteOperations + { + /// + /// Updates the with foreign keys of the current object and save changes to the database and + /// updates the inverse foreign keys of the defined relationships so the relationships are + /// stored correctly in the database. This operation will create or delete the required intermediate + /// objects for ManyToMany relationships. All related objects must have a primary key assigned in order + /// to work correctly. This also implies that any object with 'AutoIncrement' primary key must've been + /// inserted in the database previous to this call. + /// This method will also update inverse relationships of objects that currently exist in the object tree, + /// but it won't update inverse relationships of objects that are not reachable through this object + /// properties. For example, objects removed from a 'ToMany' relationship won't be updated in memory. + /// + /// SQLite Net connection object + /// Object to be updated. Must already have been inserted in the database + public static UniTask UpdateWithChildrenAsync(this SQLiteAsyncConnection conn, object element) + { + return UniTask.RunOnThreadPool(() => + { + var connectionWithLock = SqliteAsyncConnectionWrapper.Lock(conn); + using (connectionWithLock.Lock()) + { + connectionWithLock.UpdateWithChildren(element); + } + }); + } + + /// + /// Inserts the element and all the relationships that are annotated with CascadeOperation.CascadeInsert + /// into the database. If any element already exists in the database a 'Constraint' exception will be raised. + /// Elements with a primary key that it's not AutoIncrement will need a valid identifier before calling + /// this method. + /// If the recursive flag is set to true, all the relationships annotated with + /// CascadeOperation.CascadeInsert are inserted recursively in the database. This method will handle + /// loops and inverse relationships correctly. ReadOnly properties will be omitted. + /// + /// SQLite Net connection object + /// Object to be inserted. + /// If set to true all the insert-cascade properties will be inserted + public static UniTask InsertWithChildrenAsync(this SQLiteAsyncConnection conn, object element, bool recursive = false) + { + return UniTask.RunOnThreadPool(() => + { + var connectionWithLock = SqliteAsyncConnectionWrapper.Lock(conn); + using (connectionWithLock.Lock()) + { + connectionWithLock.InsertWithChildren(element, recursive); + } + }); + } + + /// + /// Inserts or replace the element and all the relationships that are annotated with + /// CascadeOperation.CascadeInsert into the database. If any element already exists in the database + /// it will be replaced. Elements with AutoIncrement primary keys that haven't been assigned will + /// be always inserted instead of replaced. + /// If the recursive flag is set to true, all the relationships annotated with + /// CascadeOperation.CascadeInsert are inserted recursively in the database. This method will handle + /// loops and inverse relationships correctly. ReadOnly properties will be omitted. + /// + /// SQLite Net connection object + /// Object to be inserted. + /// If set to true all the insert-cascade properties will be inserted + public static UniTask InsertOrReplaceWithChildrenAsync(this SQLiteAsyncConnection conn, object element, bool recursive = false) + { + return UniTask.RunOnThreadPool(() => + { + var connectionWithLock = SqliteAsyncConnectionWrapper.Lock(conn); + using (connectionWithLock.Lock()) + { + connectionWithLock.InsertOrReplaceWithChildren(element, recursive); + } + }); + } + + /// + /// Inserts all the elements and all the relationships that are annotated with CascadeOperation.CascadeInsert + /// into the database. If any element already exists in the database a 'Constraint' exception will be raised. + /// Elements with a primary key that it's not AutoIncrement will need a valid identifier before calling + /// this method. + /// If the recursive flag is set to true, all the relationships annotated with + /// CascadeOperation.CascadeInsert are inserted recursively in the database. This method will handle + /// loops and inverse relationships correctly. ReadOnly properties will be omitted. + /// + /// SQLite Net connection object + /// Objects to be inserted. + /// If set to true all the insert-cascade properties will be inserted + public static UniTask InsertAllWithChildrenAsync(this SQLiteAsyncConnection conn, IEnumerable elements, bool recursive = false) + { + return UniTask.RunOnThreadPool(() => + { + var connectionWithLock = SqliteAsyncConnectionWrapper.Lock(conn); + using (connectionWithLock.Lock()) + { + connectionWithLock.InsertAllWithChildren(elements, recursive); + } + }); + } + + /// + /// Inserts or replace all the elements and all the relationships that are annotated with + /// CascadeOperation.CascadeInsert into the database. If any element already exists in the database + /// it will be replaced. Elements with AutoIncrement primary keys that haven't been assigned will + /// be always inserted instead of replaced. + /// If the recursive flag is set to true, all the relationships annotated with + /// CascadeOperation.CascadeInsert are inserted recursively in the database. This method will handle + /// loops and inverse relationships correctly. ReadOnly properties will be omitted. + /// + /// SQLite Net connection object + /// Objects to be inserted. + /// If set to true all the insert-cascade properties will be inserted + public static UniTask InsertOrReplaceAllWithChildrenAsync(this SQLiteAsyncConnection conn, IEnumerable elements, bool recursive = false) + { + return UniTask.RunOnThreadPool(() => + { + var connectionWithLock = SqliteAsyncConnectionWrapper.Lock(conn); + using (connectionWithLock.Lock()) + { + connectionWithLock.InsertOrReplaceAllWithChildren(elements, recursive); + } + }); + } + + /// + /// Deletes all the objects passed as parameters from the database. + /// If recursive flag is set to true, all relationships marked with 'CascadeDelete' will be + /// deleted from the database recursively. Inverse relationships and closed entity loops are handled + /// correctly to avoid endless loops + /// + /// SQLite Net connection object + /// If set to true all relationships marked with 'CascadeDelete' will be + /// deleted from the database recursively + /// Objects to be deleted from the database + public static UniTask DeleteAllAsync(this SQLiteAsyncConnection conn, IEnumerable objects, bool recursive = false) + { + return UniTask.RunOnThreadPool(() => + { + var connectionWithLock = SqliteAsyncConnectionWrapper.Lock(conn); + using (connectionWithLock.Lock()) + { + connectionWithLock.DeleteAll(objects, recursive); + } + }); + } + + /// + /// Deletes all the objects passed as parameters from the database. + /// If recursive flag is set to true, all relationships marked with 'CascadeDelete' will be + /// deleted from the database recursively. Inverse relationships and closed entity loops are handled + /// correctly to avoid endless loops + /// + /// SQLite Net connection object + /// If set to true all relationships marked with 'CascadeDelete' will be + /// deleted from the database recursively + /// Object to be deleted from the database + public static UniTask DeleteAsync(this SQLiteAsyncConnection conn, object element, bool recursive) + { + return UniTask.RunOnThreadPool(() => + { + var connectionWithLock = SqliteAsyncConnectionWrapper.Lock(conn); + using (connectionWithLock.Lock()) + { + connectionWithLock.Delete(element, recursive); + } + }); + } + + /// + /// Deletes all the objects passed with IDs equal to the passed parameters from the database. + /// Relationships are not taken into account in this method + /// + /// SQLite Net connection object + /// Primary keys of the objects to be deleted from the database + /// The Entity type, it should match de database entity type + public static UniTask DeleteAllIdsAsync(this SQLiteAsyncConnection conn, IEnumerable primaryKeyValues) + { + return UniTask.RunOnThreadPool(() => + { + var connectionWithLock = SqliteAsyncConnectionWrapper.Lock(conn); + using (connectionWithLock.Lock()) + { + connectionWithLock.DeleteAllIds(primaryKeyValues); + } + }); + } + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions/WriteOperations.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions/WriteOperations.cs.meta new file mode 100644 index 0000000..47a16e8 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/AsyncExtensions/WriteOperations.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d3e6bd28399e46b3b8372d2968966e6e +timeCreated: 1644966093 \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes.meta new file mode 100644 index 0000000..c97d653 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1c1cda8f4ffcd43da9355cb6d7449d9f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/CascadeOperation.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/CascadeOperation.cs new file mode 100644 index 0000000..7e18b7a --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/CascadeOperation.cs @@ -0,0 +1,13 @@ +namespace SqlCipher4Unity3D.UniTaskIntegration.Extensions.Attributes +{ + using System; + + [Flags] + public enum CascadeOperation { + None = 0, + CascadeRead = 1 << 1, + CascadeInsert = 1 << 2, + CascadeDelete = 1 << 3, + All = CascadeRead | CascadeInsert | CascadeDelete + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/CascadeOperation.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/CascadeOperation.cs.meta new file mode 100644 index 0000000..50bec37 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/CascadeOperation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 110862a11e154157b2ed8ec0afad4160 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/ForeignKeyAttribute.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/ForeignKeyAttribute.cs new file mode 100644 index 0000000..258c603 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/ForeignKeyAttribute.cs @@ -0,0 +1,16 @@ +namespace SqlCipher4Unity3D.UniTaskIntegration.Extensions.Attributes +{ + using System; + using global::SQLite.Attributes; + + [AttributeUsage(AttributeTargets.Property)] + public class ForeignKeyAttribute : IndexedAttribute + { + public ForeignKeyAttribute(Type foreignType) + { + ForeignType = foreignType; + } + + public Type ForeignType { get; private set; } + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/ForeignKeyAttribute.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/ForeignKeyAttribute.cs.meta new file mode 100644 index 0000000..30bc65c --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/ForeignKeyAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9ef096ad72b346a1bb9a0e7de211fa3c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/ManyToManyAttribute.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/ManyToManyAttribute.cs new file mode 100644 index 0000000..7309c3c --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/ManyToManyAttribute.cs @@ -0,0 +1,15 @@ +namespace SqlCipher4Unity3D.UniTaskIntegration.Extensions.Attributes +{ + using System; + + public class ManyToManyAttribute : RelationshipAttribute + { + public ManyToManyAttribute(Type intermediateType, string inverseForeignKey = null, string inverseProperty = null) + : base(null, inverseForeignKey, inverseProperty) + { + IntermediateType = intermediateType; + } + + public Type IntermediateType { get; private set; } + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/ManyToManyAttribute.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/ManyToManyAttribute.cs.meta new file mode 100644 index 0000000..fd05f39 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/ManyToManyAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e3aab18c045d43249f867f6a28f29004 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/ManyToOneAttribute.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/ManyToOneAttribute.cs new file mode 100644 index 0000000..022d638 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/ManyToOneAttribute.cs @@ -0,0 +1,11 @@ +namespace SqlCipher4Unity3D.UniTaskIntegration.Extensions.Attributes +{ + public class ManyToOneAttribute : RelationshipAttribute + { + public ManyToOneAttribute(string foreignKey = null, string inverseProperty = null) + : base(foreignKey, null, inverseProperty) + { + } + + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/ManyToOneAttribute.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/ManyToOneAttribute.cs.meta new file mode 100644 index 0000000..26d824f --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/ManyToOneAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d0053e0437db4744b43eb596aa477ad2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/OneToManyAttribute.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/OneToManyAttribute.cs new file mode 100644 index 0000000..37be8f1 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/OneToManyAttribute.cs @@ -0,0 +1,10 @@ +namespace SqlCipher4Unity3D.UniTaskIntegration.Extensions.Attributes +{ + public class OneToManyAttribute : RelationshipAttribute + { + public OneToManyAttribute(string inverseForeignKey = null, string inverseProperty = null) + : base(null, inverseForeignKey, inverseProperty) + { + } + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/OneToManyAttribute.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/OneToManyAttribute.cs.meta new file mode 100644 index 0000000..01d71cf --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/OneToManyAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 279764d3f49548568e34922f3db1b2d9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/OneToOneAttribute.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/OneToOneAttribute.cs new file mode 100644 index 0000000..07f9643 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/OneToOneAttribute.cs @@ -0,0 +1,10 @@ +namespace SqlCipher4Unity3D.UniTaskIntegration.Extensions.Attributes +{ + public class OneToOneAttribute : RelationshipAttribute + { + public OneToOneAttribute(string foreignKey = null, string inverseProperty = null) + : base(foreignKey, null, inverseProperty) + { + } + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/OneToOneAttribute.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/OneToOneAttribute.cs.meta new file mode 100644 index 0000000..9e31167 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/OneToOneAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 50cfdc265dca4803ae8b75df3cfecde3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/RelationshipAttribute.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/RelationshipAttribute.cs new file mode 100644 index 0000000..633091b --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/RelationshipAttribute.cs @@ -0,0 +1,26 @@ +namespace SqlCipher4Unity3D.UniTaskIntegration.Extensions.Attributes +{ + using System; + using global::SQLite.Attributes; + + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public abstract class RelationshipAttribute : IgnoreAttribute + { + protected RelationshipAttribute(string foreignKey, string inverseForeignKey, string inverseProperty) + { + InverseForeignKey = inverseForeignKey; + InverseProperty = inverseProperty; + ForeignKey = foreignKey; + } + + public string ForeignKey { get; private set; } + public string InverseProperty { get; private set; } + public string InverseForeignKey { get; private set; } + public virtual CascadeOperation CascadeOperations { get; set; } + public bool ReadOnly { get; set; } + + public bool IsCascadeRead { get { return CascadeOperations.HasFlag(CascadeOperation.CascadeRead); } } + public bool IsCascadeInsert { get { return CascadeOperations.HasFlag(CascadeOperation.CascadeInsert); } } + public bool IsCascadeDelete { get { return CascadeOperations.HasFlag(CascadeOperation.CascadeDelete); } } + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/RelationshipAttribute.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/RelationshipAttribute.cs.meta new file mode 100644 index 0000000..4cab30c --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/RelationshipAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 83de8f69ff0f42d09e4f53594df33d73 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/TextBlobAttribute.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/TextBlobAttribute.cs new file mode 100644 index 0000000..e833e42 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/TextBlobAttribute.cs @@ -0,0 +1,18 @@ +namespace SqlCipher4Unity3D.UniTaskIntegration.Extensions.Attributes +{ + using System; + + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class TextBlobAttribute : RelationshipAttribute + { + public TextBlobAttribute(string textProperty) : base(null, null, null) + { + TextProperty = textProperty; + } + + public string TextProperty { get; private set; } + + // No cascade operations allowed on TextBlob properties + public override CascadeOperation CascadeOperations { get { return CascadeOperation.None; } } + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/TextBlobAttribute.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/TextBlobAttribute.cs.meta new file mode 100644 index 0000000..30c9a71 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/Attributes/TextBlobAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5a8a97dff8f244ca8769fdb7eeb09f58 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/ReadOperations.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/ReadOperations.cs new file mode 100644 index 0000000..142fb8f --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/ReadOperations.cs @@ -0,0 +1,782 @@ +using ObjectCache = + System.Collections.Generic.Dictionary>; + + +namespace SqlCipher4Unity3D.UniTaskIntegration.Extensions +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + using Attributes; + using Exceptions; + using SqlCipher4Unity3D; + using TextBlob; + + public static class ReadOperations + { + #region Public API + + /// + /// Enable to allow descriptive error descriptions on incorrect relationships. Enabled by default. + /// Disable for production environments to remove the checks and reduce performance penalty + /// + public static bool EnableRuntimeAssertions = true; + + /// + /// Fetches all the entities of the specified type with the filter and fetches all the relationship + /// properties of all the returned elements. + /// + /// List of all the elements of the type T that matches the filter with the children already loaded + /// SQLite Net connection object + /// Filter that will be passed to the Where clause when fetching + /// objects from the database. No relationship properties are allowed in this filter as they + /// are loaded afterwards + /// If set to true all the relationships with + /// CascadeOperation.CascadeRead will be loaded recusively. + /// Entity type where the object should be fetched from + public static List GetAllWithChildren(this SQLiteConnection conn, Expression> filter = null, + bool recursive = false) + where T : new() + { + var elements = conn.Table(); + if (filter != null) + { + elements = elements.Where(filter); + } + + var list = elements.ToList(); + + foreach (T element in list) + { + conn.GetChildren(element, recursive); + } + + return list; + } + + /// + /// Obtains the object from the database and fetches all the properties annotated with + /// any subclass of RelationshipAttribute. If the object with the specified primary key doesn't + /// exist in the database, an exception will be raised. + /// + /// The object with all the children loaded + /// SQLite Net connection object + /// Primary key for the object to search in the database + /// If set to true all the relationships with + /// CascadeOperation.CascadeRead will be loaded recusively. + /// Entity type where the object should be fetched from + public static T GetWithChildren(this SQLiteConnection conn, object pk, bool recursive = false) + where T : new() + { + var element = conn.Get(pk); + conn.GetChildren(element, recursive); + return element; + } + + /// + /// The behavior is the same that GetWithChildren but it returns null if the object doesn't + /// exist in the database instead of throwing an exception + /// Obtains the object from the database and fetch all the properties annotated with + /// any subclass of RelationshipAttribute. If the object with the specified primary key doesn't + /// exist in the database, it will return null + /// + /// The object with all the children loaded or null if it doesn't exist + /// SQLite Net connection object + /// Primary key for the object to search in the database + /// If set to true all the relationships with + /// CascadeOperation.CascadeRead will be loaded recusively. + /// Entity type where the object should be fetched from + public static T FindWithChildren(this SQLiteConnection conn, object pk, bool recursive = false) + where T : new() + { + var element = conn.Find(pk); + if (!EqualityComparer.Default.Equals(element, default(T))) + conn.GetChildren(element, recursive); + return element; + } + + /// + /// Fetches all the properties annotated with any subclass of RelationshipAttribute of the current + /// object and keeps fetching recursively if the recursive flag has been set. + /// + /// SQLite Net connection object + /// Element used to load all the relationship properties + /// If set to true all the relationships with + /// CascadeOperation.CascadeRead will be loaded recusively. + /// Entity type where the object should be fetched from + public static void GetChildren(this SQLiteConnection conn, T element, bool recursive = false) + { + GetChildrenRecursive(conn, element, false, recursive); + } + + /// + /// Fetches a specific property of the current object and keeps fetching recursively if the + /// recursive flag has been set. + /// + /// SQLite Net connection object + /// Element used to load all the relationship properties + /// Name of the property to fetch from the database + /// If set to true all the relationships with + /// CascadeOperation.CascadeRead will be loaded recusively. + /// Entity type where the object should be fetched from + public static void GetChild(this SQLiteConnection conn, T element, string relationshipProperty, + bool recursive = false) + { + conn.GetChild(element, element.GetType().GetRuntimeProperty(relationshipProperty), recursive); + } + + /// + /// Fetches a specific property of the current object and keeps fetching recursively if the + /// recursive flag has been set. + /// + /// SQLite Net connection object + /// Element used to load all the relationship properties + /// Expression that returns the property to be loaded from the database. + /// This variant is useful to avoid spelling mistakes and make the code refactor-safe. + /// If set to true all the relationships with + /// CascadeOperation.CascadeRead will be loaded recusively. + /// Entity type where the object should be fetched from + public static void GetChild(this SQLiteConnection conn, T element, + Expression> propertyExpression, bool recursive = false) + { + conn.GetChild(element, ReflectionExtensions.GetProperty(propertyExpression), recursive); + } + + /// + /// Fetches a specific property of the current object and keeps fetching recursively if the + /// recursive flag has been set. + /// + /// SQLite Net connection object + /// Element used to load all the relationship properties + /// Property to load from the database + /// If set to true all the relationships with + /// CascadeOperation.CascadeRead will be loaded recusively. + /// Entity type where the object should be fetched from + public static void GetChild(this SQLiteConnection conn, T element, PropertyInfo relationshipProperty, + bool recursive = false) + { + conn.GetChildRecursive(element, relationshipProperty, recursive, new ObjectCache()); + } + + #endregion + + #region Private methods + + private static void GetChildrenRecursive(this SQLiteConnection conn, object element, bool onlyCascadeChildren, + bool recursive, ObjectCache objectCache = null) + { + objectCache = objectCache ?? new ObjectCache(); + + foreach (var relationshipProperty in element.GetType().GetRelationshipProperties()) + { + var relationshipAttribute = relationshipProperty.GetAttribute(); + if (!onlyCascadeChildren || relationshipAttribute.IsCascadeRead) + { + conn.GetChildRecursive(element, relationshipProperty, recursive, objectCache); + } + else if (relationshipAttribute is TextBlobAttribute) + { + conn.GetChildRecursive(element, relationshipProperty, false, objectCache); + } + } + } + + private static void GetChildRecursive(this SQLiteConnection conn, object element, + PropertyInfo relationshipProperty, bool recursive, ObjectCache objectCache) + { + var relationshipAttribute = relationshipProperty.GetAttribute(); + + switch (relationshipAttribute) + { + case OneToOneAttribute _: + conn.GetOneToOneChildren(new List { element }, relationshipProperty, recursive, objectCache); + break; + case OneToManyAttribute _: + conn.GetOneToManyChildren(element, relationshipProperty, recursive, objectCache); + break; + case ManyToOneAttribute _: + conn.GetManyToOneChildren(new List { element }, relationshipProperty, recursive, + objectCache); + break; + case ManyToManyAttribute _: + conn.GetManyToManyChildren(element, relationshipProperty, recursive, objectCache); + break; + case TextBlobAttribute _: + TextBlobOperations.GetTextBlobChild(element, relationshipProperty); + break; + } + } + + private static object GetOneToOneChildren(this SQLiteConnection conn, IList elements, + PropertyInfo relationshipProperty, + bool recursive, ObjectCache objectCache) + { + var primaryKeys = new Dictionary>(); + var type = elements[0].GetType(); + + var entityType = relationshipProperty.GetEntityType(out var enclosedType); + + Assert(enclosedType == EnclosedType.None, type, relationshipProperty, + "OneToOne relationship cannot be of type List or Array"); + + var currentEntityPrimaryKeyProperty = type.GetPrimaryKey(); + var otherEntityPrimaryKeyProperty = entityType.GetPrimaryKey(); + + Assert(currentEntityPrimaryKeyProperty != null || otherEntityPrimaryKeyProperty != null, type, + relationshipProperty, + "At least one entity in a OneToOne relationship must have Primary Key"); + + var currentEntityForeignKeyProperty = type.GetForeignKeyProperty(relationshipProperty); + var otherEntityForeignKeyProperty = type.GetForeignKeyProperty(relationshipProperty, inverse: true); + + Assert(currentEntityForeignKeyProperty != null || otherEntityForeignKeyProperty != null, type, + relationshipProperty, + "At least one entity in a OneToOne relationship must have Foreign Key"); + + var hasForeignKey = otherEntityPrimaryKeyProperty != null && currentEntityForeignKeyProperty != null; + var hasInverseForeignKey = currentEntityPrimaryKeyProperty != null && otherEntityForeignKeyProperty != null; + + Assert(hasForeignKey || hasInverseForeignKey, type, relationshipProperty, + "Missing either ForeignKey or PrimaryKey for a complete OneToOne relationship"); + + var tableMapping = conn.GetMapping(entityType); + + Assert(tableMapping != null, type, relationshipProperty, + "There's no mapping table for OneToOne relationship"); + + var inverseProperty = type.GetInverseProperty(relationshipProperty); + + foreach (var element in elements) + { + var isLoadedFromCache = false; + var value = default(T); + object keyValue = null; + + if (hasForeignKey) + { + keyValue = currentEntityForeignKeyProperty.GetValue(element, null); + if (keyValue != null) + { + // Try to load from cache when possible + if (recursive) + { + value = (T)GetObjectFromCache(entityType, keyValue, objectCache); + } + + if (value != null) + { + isLoadedFromCache = true; + } + } + } + else + { + keyValue = currentEntityPrimaryKeyProperty?.GetValue(element, null); + // Try to replace the loaded entity with the same object from the cache whenever possible + value = recursive + ? (T)ReplaceWithCacheObjectIfPossible(value, otherEntityPrimaryKeyProperty, objectCache, + out isLoadedFromCache) + : value; + } + + if (isLoadedFromCache) + { + relationshipProperty.SetValue(element, value, null); + if (value != null && inverseProperty != null) + { + inverseProperty.SetValue(value, element, null); + } + } + else + { + if (keyValue != null) + { + AddPrimaryKeyToDictionary(keyValue, element, primaryKeys); + } + } + } + + if (primaryKeys.Count <= 0) return elements[0]; + { + string columnName; + if (otherEntityForeignKeyProperty != null) + { + columnName = otherEntityForeignKeyProperty.GetColumnName(); + } + else + { + columnName = tableMapping?.PK.Name; + } + + var placeHolders = string.Join(",", Enumerable.Repeat("?", primaryKeys.Count)); + + var query = string.Format("select * from [{0}] where [{1}] in ({2})", tableMapping?.TableName, + columnName, placeHolders); + + IList values = conn.Query(tableMapping, query, primaryKeys.Keys.ToArray()); + + if (values.Count <= 0) return elements[0]; + + var keyProperty = otherEntityForeignKeyProperty ?? values[0].GetType().GetPrimaryKey(); + + foreach (var value in values) + { + var keyValue = keyProperty.GetValue(value); + + if (!primaryKeys.TryGetValue(keyValue, out var keyElements)) continue; + + foreach (var keyElement in keyElements) + { + relationshipProperty.SetValue(keyElement, value, null); + if (value != null && inverseProperty != null) + { + inverseProperty.SetValue(value, keyElement, null); + } + + if (value != null && recursive) + { + SaveObjectToCache(value, otherEntityPrimaryKeyProperty.GetValue(value, null), objectCache); + conn.GetChildrenRecursive(value, true, recursive, objectCache); + } + } + } + } + return elements[0]; + } + + + private static object GetManyToOneChildren(this SQLiteConnection conn, IList elements, + PropertyInfo relationshipProperty, + bool recursive, ObjectCache objectCache) + { + var primaryKeys = new Dictionary>(); + var type = elements[0].GetType(); + + var entityType = relationshipProperty.GetEntityType(out var enclosedType); + + Assert(enclosedType == EnclosedType.None, type, relationshipProperty, + "ManyToOne relationship cannot be of type List or Array"); + + var otherEntityPrimaryKeyProperty = entityType.GetPrimaryKey(); + Assert(otherEntityPrimaryKeyProperty != null, type, relationshipProperty, + "ManyToOne relationship destination must have Primary Key"); + + var currentEntityForeignKeyProperty = type.GetForeignKeyProperty(relationshipProperty); + Assert(currentEntityForeignKeyProperty != null, type, relationshipProperty, + "ManyToOne relationship origin must have Foreign Key"); + + var tableMapping = conn.GetMapping(entityType); + Assert(tableMapping != null, type, relationshipProperty, + "There's no mapping table for OneToMany relationship destination"); + + foreach (var element in elements) + { + object value = null; + var isLoadedFromCache = false; + var foreignKeyValue = currentEntityForeignKeyProperty?.GetValue(element, null); + if (foreignKeyValue != null) + { + // Try to load from cache when possible + if (recursive) + { + value = GetObjectFromCache(entityType, foreignKeyValue, objectCache); + } + + if (value == null) + { + AddPrimaryKeyToDictionary(foreignKeyValue, element, primaryKeys); + } + + else + { + isLoadedFromCache = true; + } + } + + if (isLoadedFromCache) + { + relationshipProperty.SetValue(element, value, null); + } + } + + if (primaryKeys.Count <= 0) return elements[0]; + { + var placeHolders = string.Join(",", Enumerable.Repeat("?", primaryKeys.Count)); + + var query = string.Format("select * from [{0}] where [{1}] in ({2})", tableMapping?.TableName, + tableMapping?.PK.Name, placeHolders); + + IList values = conn.Query(tableMapping, query, primaryKeys.Keys.ToArray()); + + if (values.Count <= 0) return elements[0]; + + var keyProperty = values[0].GetType().GetPrimaryKey(); + + foreach (var value in values) + { + var keyValue = keyProperty.GetValue(value); + + if (!primaryKeys.TryGetValue(keyValue, out var keyElements)) continue; + + foreach (var keyElement in keyElements) + { + relationshipProperty.SetValue(keyElement, value, null); + if (value == null || !recursive) continue; + + SaveObjectToCache(value, otherEntityPrimaryKeyProperty?.GetValue(value, null), + objectCache); + + conn.GetChildrenRecursive(value, true, recursive, objectCache); + } + } + } + + return elements[0]; + } + + private static void AddPrimaryKeyToDictionary(object key, T element, + Dictionary> primaryKeys) + { + if (!primaryKeys.TryGetValue(key, out var list)) + { + list = new List() { element }; + primaryKeys.Add(key, list); + } + else + { + list.Add(element); + } + } + + private static IEnumerable GetOneToManyChildren(this SQLiteConnection conn, T element, + PropertyInfo relationshipProperty, + bool recursive, ObjectCache objectCache) + { + var type = element.GetType(); + + var entityType = relationshipProperty.GetEntityType(out var enclosedType); + + Assert(enclosedType != EnclosedType.None, type, relationshipProperty, + "OneToMany relationship must be a List or Array"); + + var currentEntityPrimaryKeyProperty = type.GetPrimaryKey(); + Assert(currentEntityPrimaryKeyProperty != null, type, relationshipProperty, + "OneToMany relationship origin must have Primary Key"); + + var otherEntityForeignKeyProperty = type.GetForeignKeyProperty(relationshipProperty, inverse: true); + Assert(otherEntityForeignKeyProperty != null, type, relationshipProperty, + "OneToMany relationship destination must have Foreign Key to the origin class"); + + var otherEntityPrimaryKeyProperty = entityType.GetPrimaryKey(); + + var tableMapping = conn.GetMapping(entityType); + Assert(tableMapping != null, type, relationshipProperty, + "There's no mapping table for OneToMany relationship destination"); + + var inverseProperty = type.GetInverseProperty(relationshipProperty); + + IList cascadeElements = new List(); + IList values = null; + + var primaryKeyValue = currentEntityPrimaryKeyProperty?.GetValue(element, null); + if (primaryKeyValue != null) + { + var query = string.Format("select * from [{0}] where [{1}] = ?", entityType.GetTableName(), + otherEntityForeignKeyProperty.GetColumnName()); + + var queryResults = conn.Query(tableMapping, query, primaryKeyValue); + + Array array = null; + + values = enclosedType switch + { + // Create a generic list of the expected type + EnclosedType.List => (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(entityType)), + EnclosedType.ObservableCollection => (IList)Activator.CreateInstance( + typeof(ObservableCollection<>).MakeGenericType(entityType)), + _ => array = Array.CreateInstance(entityType, queryResults.Count) + }; + + var i = 0; + + foreach (T result in queryResults) + { + // Replace obtained value with a cached one whenever possible + var loadedFromCache = false; + var value = recursive + ? ReplaceWithCacheObjectIfPossible(result, otherEntityPrimaryKeyProperty, objectCache, + out loadedFromCache) + : result; + + if (array != null) + { + array.SetValue(value, i); + } + else + { + values.Add(value); + } + + if (!loadedFromCache) + { + cascadeElements.Add(result); + } + + i++; + } + } + + relationshipProperty.SetValue(element, values, null); + + if (inverseProperty != null && values != null) + { + // Stablish inverse relationships (we already have that object anyway) + foreach (var value in values) + { + inverseProperty.SetValue(value, element, null); + } + } + + if (recursive) + { + if (cascadeElements.Count > 0) + { + conn.GetChildrenRecursiveBatched(cascadeElements, objectCache); + } + } + + return values; + } + + private static void GetChildrenRecursiveBatched(this SQLiteConnection conn, IList elements, + ObjectCache objectCache) + { + var element = elements[0]; + foreach (var relationshipProperty in element.GetType().GetRelationshipProperties()) + { + var relationshipAttribute = relationshipProperty.GetAttribute(); + if (relationshipAttribute.IsCascadeRead) + { + if (relationshipAttribute is OneToOneAttribute) + { + conn.GetOneToOneChildren(elements, relationshipProperty, true, objectCache); + } + else if (relationshipAttribute is OneToManyAttribute) + { + foreach (var e in elements) + { + conn.GetOneToManyChildren(e, relationshipProperty, true, objectCache); + } + } + else if (relationshipAttribute is ManyToOneAttribute) + { + conn.GetManyToOneChildren(elements, relationshipProperty, true, objectCache); + } + else if (relationshipAttribute is ManyToManyAttribute) + { + foreach (var e in elements) + { + conn.GetManyToManyChildren(e, relationshipProperty, true, objectCache); + } + } + else if (relationshipAttribute is TextBlobAttribute) + { + TextBlobOperations.GetTextBlobChild(element, relationshipProperty); + } + } + else if (relationshipAttribute is TextBlobAttribute) + { + foreach (var e in elements) + { + conn.GetChildRecursive(e, relationshipProperty, false, objectCache); + } + } + } + } + + private static IEnumerable GetManyToManyChildren(this SQLiteConnection conn, T element, + PropertyInfo relationshipProperty, + bool recursive, ObjectCache objectCache) + { + var type = element.GetType(); + EnclosedType enclosedType; + var entityType = relationshipProperty.GetEntityType(out enclosedType); + + var currentEntityPrimaryKeyProperty = type.GetPrimaryKey(); + var otherEntityPrimaryKeyProperty = entityType.GetPrimaryKey(); + var manyToManyMetaInfo = type.GetManyToManyMetaInfo(relationshipProperty); + var currentEntityForeignKeyProperty = manyToManyMetaInfo.OriginProperty; + var otherEntityForeignKeyProperty = manyToManyMetaInfo.DestinationProperty; + var intermediateType = manyToManyMetaInfo.IntermediateType; + var tableMapping = conn.GetMapping(entityType); + + Assert(enclosedType != EnclosedType.None, type, relationshipProperty, + "ManyToMany relationship must be a List or Array"); + Assert(currentEntityPrimaryKeyProperty != null, type, relationshipProperty, + "ManyToMany relationship origin must have Primary Key"); + Assert(otherEntityPrimaryKeyProperty != null, type, relationshipProperty, + "ManyToMany relationship destination must have Primary Key"); + Assert(intermediateType != null, type, relationshipProperty, + "ManyToMany relationship intermediate type cannot be null"); + Assert(currentEntityForeignKeyProperty != null, type, relationshipProperty, + "ManyToMany relationship origin must have a foreign key defined in the intermediate type"); + Assert(otherEntityForeignKeyProperty != null, type, relationshipProperty, + "ManyToMany relationship destination must have a foreign key defined in the intermediate type"); + Assert(tableMapping != null, type, relationshipProperty, + "There's no mapping table defined for ManyToMany relationship origin"); + + IList cascadeElements = new List(); + IList values = null; + + var primaryKeyValue = currentEntityPrimaryKeyProperty?.GetValue(element, null); + if (primaryKeyValue != null) + { + // Obtain the relationship keys + var keysQuery = string.Format("select [{0}] from [{1}] where [{2}] = ?", + otherEntityForeignKeyProperty.GetColumnName(), + intermediateType.GetTableName(), currentEntityForeignKeyProperty.GetColumnName()); + + var query = string.Format("select * from [{0}] where [{1}] in ({2})", entityType.GetTableName(), + otherEntityPrimaryKeyProperty.GetColumnName(), keysQuery); + + var queryResults = conn.Query(tableMapping, query, primaryKeyValue); + + Array array = null; + + values = enclosedType switch + { + // Create a generic list of the expected type + EnclosedType.List => (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(entityType)), + EnclosedType.ObservableCollection => (IList)Activator.CreateInstance( + typeof(ObservableCollection<>).MakeGenericType(entityType)), + _ => array = Array.CreateInstance(entityType, queryResults.Count) + }; + + var i = 0; + foreach (var result in queryResults) + { + // Replace obtained value with a cached one whenever possible + var loadedFromCache = false; + var value = recursive + ? ReplaceWithCacheObjectIfPossible(result, otherEntityPrimaryKeyProperty, objectCache, + out loadedFromCache) + : result; + + if (array != null) + { + array.SetValue(value, i); + } + else + { + values.Add(value); + } + + if (!loadedFromCache) + { + cascadeElements.Add(result); + } + + i++; + } + } + + + relationshipProperty.SetValue(element, values, null); + + if (recursive) + { + foreach (var child in cascadeElements) + { + conn.GetChildrenRecursive(child, true, recursive, objectCache); + } + } + + return values; + } + + static object ReplaceWithCacheObjectIfPossible(object element, PropertyInfo primaryKeyProperty, + ObjectCache objectCache, out bool isLoadedFromCache) + { + isLoadedFromCache = false; + + if (element == null || primaryKeyProperty == null || objectCache == null) + { + return element; + } + + object primaryKey = null; + primaryKey = primaryKeyProperty.GetValue(element, null); + + if (primaryKey == null) + { + return element; + } + + var cachedElement = GetObjectFromCache(element.GetType(), primaryKey, objectCache); + object result; + if (cachedElement != null) + { + result = cachedElement; + isLoadedFromCache = true; + } + else + { + result = element; + SaveObjectToCache(element, primaryKey, objectCache); + } + + return result; + } + + static void Assert(bool assertion, Type type, PropertyInfo property, string message) + { + if (EnableRuntimeAssertions && !assertion) + { + throw new IncorrectRelationshipException(type.Name, property.Name, message); + } + } + + static object GetObjectFromCache(Type objectType, object primaryKey, ObjectCache objectCache) + { + if (objectCache == null) + { + return null; + } + + var typeName = objectType.FullName; + Dictionary typeDict = null; + object value = null; + + if (typeName != null && objectCache.TryGetValue(typeName, out typeDict)) + { + typeDict.TryGetValue(primaryKey, out value); + } + + return value; + } + + static void SaveObjectToCache(object element, object primaryKey, ObjectCache objectCache) + { + if (objectCache == null || primaryKey == null || element == null) + { + return; + } + + var typeName = element.GetType().FullName; + Dictionary typeDict = null; + if (!objectCache.TryGetValue(typeName, out typeDict)) + { + typeDict = new Dictionary(); + objectCache[typeName] = typeDict; + } + + typeDict[primaryKey] = element; + } + + #endregion + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/ReadOperations.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/ReadOperations.cs.meta new file mode 100644 index 0000000..9071645 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/ReadOperations.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f7a3550b60d04dc699c29b95d6f9b501 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/ReflectionExtensions.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/ReflectionExtensions.cs new file mode 100644 index 0000000..d146fd6 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/ReflectionExtensions.cs @@ -0,0 +1,254 @@ +namespace SqlCipher4Unity3D.UniTaskIntegration.Extensions +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + using Attributes; + using SQLite.Attributes; + + public enum EnclosedType + { + None, + Array, + List, + ObservableCollection + } + + public class ManyToManyMetaInfo + { + public Type IntermediateType { get; set; } + public PropertyInfo OriginProperty { get; set; } + public PropertyInfo DestinationProperty { get; set; } + } + + public static class ReflectionExtensions + { + public static T GetAttribute(this Type type) where T : Attribute { + T attribute = null; + var attributes = (T[])type.GetTypeInfo().GetCustomAttributes(typeof(T), true); + if (attributes.Length > 0) + { + attribute = attributes[0]; + } + return attribute; + } + + public static T GetAttribute(this PropertyInfo property) where T : Attribute + { + T attribute = null; + var attributes = (T[])property.GetCustomAttributes(typeof(T), true); + if (attributes.Length > 0) + { + attribute = attributes[0]; + } + return attribute; + } + + public static Type GetEntityType(this PropertyInfo property, out EnclosedType enclosedType) + { + var type = property.PropertyType; + enclosedType = EnclosedType.None; + + var typeInfo = type.GetTypeInfo(); + if (type.IsArray) + { + type = type.GetElementType(); + enclosedType = EnclosedType.Array; + } + else if (typeInfo.IsGenericType && typeof(List<>).GetTypeInfo().IsAssignableFrom(type.GetGenericTypeDefinition().GetTypeInfo())) + { + type = typeInfo.GenericTypeArguments[0]; + enclosedType = EnclosedType.List; + } + else if (typeInfo.IsGenericType && typeof(ObservableCollection<>).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo().GetGenericTypeDefinition().GetTypeInfo())) + { + type = typeInfo.GenericTypeArguments[0]; + enclosedType = EnclosedType.ObservableCollection; + } + return type; + } + + public static object GetDefault(this Type type) + { + return type.GetTypeInfo().IsValueType ? Activator.CreateInstance(type) : null; + } + + private static PropertyInfo GetExplicitForeignKeyProperty(this Type type, Type destinationType) + { + return (from property in type.GetRuntimeProperties() where property.IsPublicInstance() + let foreignKeyAttribute = property.GetAttribute() + where foreignKeyAttribute != null && foreignKeyAttribute.ForeignType.GetTypeInfo().IsAssignableFrom(destinationType.GetTypeInfo()) + select property) + .FirstOrDefault(); + } + + private static PropertyInfo GetConventionForeignKeyProperty(this Type type, string destinationTypeName) + { + var conventionFormats = new List { "{0}Id", "{0}Key", "{0}ForeignKey" }; + + var conventionNames = conventionFormats.Select(format => string.Format(format, destinationTypeName)).ToList(); + + // No explicit declaration, search for convention names + return (from property in type.GetRuntimeProperties() + where property.IsPublicInstance() && conventionNames.Contains(property.Name, StringComparer.OrdinalIgnoreCase) + select property) + .FirstOrDefault(); + } + + public static PropertyInfo GetForeignKeyProperty(this Type type, PropertyInfo relationshipProperty, Type intermediateType = null, bool inverse = false) + { + PropertyInfo result; + var attribute = relationshipProperty.GetAttribute(); + RelationshipAttribute inverseAttribute = null; + + EnclosedType enclosedType; + var propertyType = relationshipProperty.GetEntityType(out enclosedType); + + var originType = intermediateType ?? (inverse ? propertyType : type); + var destinationType = inverse ? type : propertyType; + + // Inverse relationships may have the foreign key declared in the inverse property relationship attribute + var inverseProperty = type.GetInverseProperty(relationshipProperty); + if (inverseProperty != null) + { + inverseAttribute = inverseProperty.GetAttribute(); + } + + if (!inverse && !string.IsNullOrEmpty(attribute.ForeignKey)) + { + // Explicitly declared foreign key name + result = originType.GetRuntimeProperty(attribute.ForeignKey); + } + else if (!inverse && inverseAttribute != null && !string.IsNullOrEmpty(inverseAttribute.InverseForeignKey)) + { + // Explicitly declared inverse foreign key name in inverse property (double inverse refers to current entity foreign key) + result = originType.GetRuntimeProperty(inverseAttribute.InverseForeignKey); + } + else if (inverse && !string.IsNullOrEmpty(attribute.InverseForeignKey)) + { + // Explicitly declared inverse foreign key name + result = originType.GetRuntimeProperty(attribute.InverseForeignKey); + } + else if (inverse && inverseAttribute != null && !string.IsNullOrEmpty(inverseAttribute.ForeignKey)) + { + // Explicitly declared foreign key name in inverse property + result = originType.GetRuntimeProperty(inverseAttribute.ForeignKey); + } + else + { + // Explicitly declared attribute + result = originType.GetExplicitForeignKeyProperty(destinationType) ?? + originType.GetConventionForeignKeyProperty(destinationType.Name); + } + + return result; + } + + + public static PropertyInfo GetInverseProperty(this Type elementType, PropertyInfo property) + { + + var attribute = property.GetAttribute(); + if (attribute == null || (attribute.InverseProperty != null && attribute.InverseProperty.Equals(""))) + { + // Relationship not reversible + return null; + } + + EnclosedType enclosedType; + var propertyType = property.GetEntityType(out enclosedType); + + PropertyInfo result = null; + if (attribute.InverseProperty != null) + { + result = propertyType.GetRuntimeProperty(attribute.InverseProperty); + } + else + { + var properties = (from p in propertyType.GetRuntimeProperties() where p.IsPublicInstance() select p); + foreach (var inverseProperty in properties) + { + var inverseAttribute = inverseProperty.GetAttribute(); + EnclosedType enclosedInverseType; + var inverseType = inverseProperty.GetEntityType(out enclosedInverseType); + if (inverseAttribute != null && inverseType.GetTypeInfo().Equals(elementType.GetTypeInfo())) + { + result = inverseProperty; + break; + } + } + } + + return result; + } + + public static PropertyInfo GetProperty(Expression> expression) { + var type = typeof(T); + var body = expression.Body as MemberExpression; + // Debug.Assert(body != null, "Expression should be a property member expression"); + + var propertyName = body.Member.Name; + return type.GetRuntimeProperty(propertyName); + } + + public static ManyToManyMetaInfo GetManyToManyMetaInfo(this Type type, PropertyInfo relationship) + { + var manyToManyAttribute = relationship.GetAttribute(); + // Debug.Assert(manyToManyAttribute != null, "Unable to find ManyToMany attribute"); + + var intermediateType = manyToManyAttribute.IntermediateType; + var destinationKeyProperty = type.GetForeignKeyProperty(relationship, intermediateType); + var inverseKeyProperty = type.GetForeignKeyProperty(relationship, intermediateType, true); + + return new ManyToManyMetaInfo + { + IntermediateType = intermediateType, + OriginProperty = inverseKeyProperty, + DestinationProperty = destinationKeyProperty + }; + } + + public static List GetRelationshipProperties(this Type type) + { + return (from property in type.GetRuntimeProperties() + where property.IsPublicInstance() && property.GetAttribute() != null + select property).ToList(); + } + + public static PropertyInfo GetPrimaryKey(this Type type) + { + return (from property in type.GetRuntimeProperties() + where property.IsPublicInstance() && property.GetAttribute() != null + select property).FirstOrDefault(); + } + + public static string GetTableName(this Type type) { + var tableName = type.Name; + var tableAttribute = type.GetAttribute(); + if (tableAttribute != null && tableAttribute.Name != null) + tableName = tableAttribute.Name; + + return tableName; + } + + public static string GetColumnName(this PropertyInfo property) { + var column = property.Name; + var columnAttribute = property.GetAttribute(); + if (columnAttribute != null && columnAttribute.Name != null) + column = columnAttribute.Name; + + return column; + } + + // Equivalent to old GetProperties(BindingFlags.Public | BindingFlags.Instance) + private static bool IsPublicInstance(this PropertyInfo propertyInfo) + { + return propertyInfo != null && + ((propertyInfo.GetMethod != null && !propertyInfo.GetMethod.IsStatic && propertyInfo.GetMethod.IsPublic) && + (propertyInfo.SetMethod != null && !propertyInfo.SetMethod.IsStatic && propertyInfo.SetMethod.IsPublic)); + } + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/ReflectionExtensions.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/ReflectionExtensions.cs.meta new file mode 100644 index 0000000..3fa39e3 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/ReflectionExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: eba6e03f0d6f48dc849505805fbddb4d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob.meta new file mode 100644 index 0000000..6f89fb7 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 63e5749fb28d4022b5e591cb6e6f679f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/ITextBlobSerializer.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/ITextBlobSerializer.cs new file mode 100644 index 0000000..289c86a --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/ITextBlobSerializer.cs @@ -0,0 +1,10 @@ +namespace SqlCipher4Unity3D.UniTaskIntegration.Extensions.TextBlob +{ + using System; + + public interface ITextBlobSerializer + { + string Serialize(object element); + object Deserialize(string text, Type type); + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/ITextBlobSerializer.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/ITextBlobSerializer.cs.meta new file mode 100644 index 0000000..b4367d7 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/ITextBlobSerializer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 44158ca43fe441058a86d18033345def +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/Serializers.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/Serializers.meta new file mode 100644 index 0000000..7e9f7bc --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/Serializers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: adce313dae1f4476bc4f8c52454d2372 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/Serializers/JsonBlobSerializer.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/Serializers/JsonBlobSerializer.cs new file mode 100644 index 0000000..005f92f --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/Serializers/JsonBlobSerializer.cs @@ -0,0 +1,18 @@ +namespace SqlCipher4Unity3D.UniTaskIntegration.Extensions.TextBlob.Serializers +{ + using System; + using Newtonsoft.Json; + + public class JsonBlobSerializer : ITextBlobSerializer + { + public string Serialize(object element) + { + return JsonConvert.SerializeObject(element); + } + + public object Deserialize(string text, Type type) + { + return JsonConvert.DeserializeObject(text, type); + } + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/Serializers/JsonBlobSerializer.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/Serializers/JsonBlobSerializer.cs.meta new file mode 100644 index 0000000..d8b0c95 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/Serializers/JsonBlobSerializer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c86db91d41144c2ba600afdd1ec27a5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/TextBlobOperations.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/TextBlobOperations.cs new file mode 100644 index 0000000..24f11d9 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/TextBlobOperations.cs @@ -0,0 +1,56 @@ +namespace SqlCipher4Unity3D.UniTaskIntegration.Extensions.TextBlob +{ + using System.Reflection; + using Attributes; + using Serializers; + + public static class TextBlobOperations + { + private static ITextBlobSerializer _serializer; + + public static void SetTextSerializer(ITextBlobSerializer serializer) + { + _serializer = serializer; + } + + public static ITextBlobSerializer GetTextSerializer() + { + // If not specified, use default JSON serializer + return _serializer ??= new JsonBlobSerializer(); + } + + public static void GetTextBlobChild(object element, PropertyInfo relationshipProperty) + { + var type = element.GetType(); + var relationshipType = relationshipProperty.PropertyType; + + // Debug.Assert(relationshipType != typeof(string), "TextBlob property is already a string"); + + var textblobAttribute = relationshipProperty.GetAttribute(); + var textProperty = type.GetRuntimeProperty(textblobAttribute.TextProperty); + // Debug.Assert(textProperty != null && textProperty.PropertyType == typeof(string), "Text property for TextBlob relationship not found"); + + var textValue = (string)textProperty.GetValue(element, null); + var value = textValue != null ? GetTextSerializer().Deserialize(textValue, relationshipType) : null; + + relationshipProperty.SetValue(element, value, null); + } + + public static void UpdateTextBlobProperty(object element, PropertyInfo relationshipProperty) + { + var type = element.GetType(); + var relationshipType = relationshipProperty.PropertyType; + + // Debug.Assert(relationshipType != typeof(string), "TextBlob property is already a string"); + + var textblobAttribute = relationshipProperty.GetAttribute(); + var textProperty = type.GetRuntimeProperty(textblobAttribute.TextProperty); + // Debug.Assert(textProperty != null && textProperty.PropertyType == typeof(string), "Text property for TextBlob relationship not found"); + + var value = relationshipProperty.GetValue(element, null); + var textValue = value != null ? GetTextSerializer().Serialize(value) : null; + + textProperty.SetValue(element, textValue, null); + } + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/TextBlobOperations.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/TextBlobOperations.cs.meta new file mode 100644 index 0000000..9a5bcf4 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/TextBlob/TextBlobOperations.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cc4b6569787545e5b9f6f3759485f240 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/WriteOperations.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/WriteOperations.cs new file mode 100644 index 0000000..e24de27 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/WriteOperations.cs @@ -0,0 +1,638 @@ +namespace SqlCipher4Unity3D.UniTaskIntegration.Extensions +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using Attributes; + using Exceptions; + using SqlCipher4Unity3D; + using SQLite.Attributes; + using TextBlob; + + public static class WriteOperations + { + const int queryLimit = 990; //Make room for extra keys added by the code + + /// + /// Enable to allow descriptive error descriptions on incorrect relationships. Enabled by default. + /// Disable for production environments to remove the checks and reduce performance penalty + /// + public static bool EnableRuntimeAssertions = true; + + /// + /// Updates the with foreign keys of the current object and save changes to the database and + /// updates the inverse foreign keys of the defined relationships so the relationships are + /// stored correctly in the database. This operation will create or delete the required intermediate + /// objects for ManyToMany relationships. All related objects must have a primary key assigned in order + /// to work correctly. This also implies that any object with 'AutoIncrement' primary key must've been + /// inserted in the database previous to this call. + /// This method will also update inverse relationships of objects that currently exist in the object tree, + /// but it won't update inverse relationships of objects that are not reachable through this object + /// properties. For example, objects removed from a 'ToMany' relationship won't be updated in memory. + /// + /// SQLite Net connection object + /// Object to be updated. Must already have been inserted in the database + public static void UpdateWithChildren(this SQLiteConnection conn, object element) + { + // Update the current element + RefreshForeignKeys(element); + conn.Update(element); + + // Update inverse foreign keys + conn.UpdateInverseForeignKeys(element); + } + + /// + /// Inserts the element and all the relationships that are annotated with CascadeOperation.CascadeInsert + /// into the database. If any element already exists in the database a 'Constraint' exception will be raised. + /// Elements with a primary key that it's not AutoIncrement will need a valid identifier before calling + /// this method. + /// If the recursive flag is set to true, all the relationships annotated with + /// CascadeOperation.CascadeInsert are inserted recursively in the database. This method will handle + /// loops and inverse relationships correctly. ReadOnly properties will be omitted. + /// + /// SQLite Net connection object + /// Object to be inserted. + /// If set to true all the insert-cascade properties will be inserted + public static void InsertWithChildren(this SQLiteConnection conn, object element, bool recursive = false) { + conn.InsertWithChildrenRecursive(element, false, recursive); + } + + /// + /// Inserts or replace the element and all the relationships that are annotated with + /// CascadeOperation.CascadeInsert into the database. If any element already exists in the database + /// it will be replaced. Elements with AutoIncrement primary keys that haven't been assigned will + /// be always inserted instead of replaced. + /// If the recursive flag is set to true, all the relationships annotated with + /// CascadeOperation.CascadeInsert are inserted recursively in the database. This method will handle + /// loops and inverse relationships correctly. ReadOnly properties will be omitted. + /// + /// SQLite Net connection object + /// Object to be inserted. + /// If set to true all the insert-cascade properties will be inserted + public static void InsertOrReplaceWithChildren(this SQLiteConnection conn, object element, bool recursive = false) { + conn.InsertWithChildrenRecursive(element, true, recursive); + } + + /// + /// Inserts all the elements and all the relationships that are annotated with CascadeOperation.CascadeInsert + /// into the database. If any element already exists in the database a 'Constraint' exception will be raised. + /// Elements with a primary key that it's not AutoIncrement will need a valid identifier before calling + /// this method. + /// If the recursive flag is set to true, all the relationships annotated with + /// CascadeOperation.CascadeInsert are inserted recursively in the database. This method will handle + /// loops and inverse relationships correctly. ReadOnly properties will be omitted. + /// + /// SQLite Net connection object + /// Objects to be inserted. + /// If set to true all the insert-cascade properties will be inserted + public static void InsertAllWithChildren(this SQLiteConnection conn, IEnumerable elements, bool recursive = false) { + conn.InsertAllWithChildrenRecursive(elements, false, recursive); + } + + /// + /// Inserts or replace all the elements and all the relationships that are annotated with + /// CascadeOperation.CascadeInsert into the database. If any element already exists in the database + /// it will be replaced. Elements with AutoIncrement primary keys that haven't been assigned will + /// be always inserted instead of replaced. + /// If the recursive flag is set to true, all the relationships annotated with + /// CascadeOperation.CascadeInsert are inserted recursively in the database. This method will handle + /// loops and inverse relationships correctly. ReadOnly properties will be omitted. + /// + /// SQLite Net connection object + /// Objects to be inserted. + /// If set to true all the insert-cascade properties will be inserted + public static void InsertOrReplaceAllWithChildren(this SQLiteConnection conn, IEnumerable elements, bool recursive = false) { + conn.InsertAllWithChildrenRecursive(elements, true, recursive); + } + + /// + /// Deletes all the objects passed as parameters from the database. + /// If recursive flag is set to true, all relationships marked with 'CascadeDelete' will be + /// deleted from the database recursively. Inverse relationships and closed entity loops are handled + /// correctly to avoid endless loops + /// + /// SQLite Net connection object + /// If set to true all relationships marked with 'CascadeDelete' will be + /// deleted from the database recursively + /// Objects to be deleted from the database + public static void DeleteAll(this SQLiteConnection conn, IEnumerable objects, bool recursive = false) { + conn.DeleteAllRecursive(objects, recursive); + } + + /// + /// Deletes all the objects passed as parameters from the database. + /// If recursive flag is set to true, all relationships marked with 'CascadeDelete' will be + /// deleted from the database recursively. Inverse relationships and closed entity loops are handled + /// correctly to avoid endless loops + /// + /// SQLite Net connection object + /// If set to true all relationships marked with 'CascadeDelete' will be + /// deleted from the database recursively + /// Object to be deleted from the database + public static void Delete(this SQLiteConnection conn, object element, bool recursive) { + if (recursive) + conn.DeleteAll(new []{ element }, recursive); + else + conn.Delete(element); + } + + /// + /// Deletes all the objects passed with IDs equal to the passed parameters from the database. + /// Relationships are not taken into account in this method + /// + /// SQLite Net connection object + /// Primary keys of the objects to be deleted from the database + /// The Entity type, it should match de database entity type + public static void DeleteAllIds(this SQLiteConnection conn, IEnumerable primaryKeyValues) { + var type = typeof(T); + var primaryKeyProperty = type.GetPrimaryKey(); + + conn.DeleteAllIds(primaryKeyValues.ToArray(), type.GetTableName(), primaryKeyProperty.GetColumnName()); + } + + + #region Private methods + static void InsertAllWithChildrenRecursive(this SQLiteConnection conn, IEnumerable elements, bool replace, bool recursive, ISet objectCache = null) { + if (elements == null) + return; + + objectCache = objectCache ?? new HashSet(); + var insertedElements = conn.InsertElements(elements, replace, objectCache).Cast().ToList(); + + foreach (var element in insertedElements) { + conn.InsertChildrenRecursive(element, replace, recursive, objectCache); + } + + foreach (var element in insertedElements) { + conn.UpdateWithChildren(element); + } + } + + static void InsertWithChildrenRecursive(this SQLiteConnection conn, object element, bool replace, bool recursive, ISet objectCache = null) { + objectCache = objectCache ?? new HashSet(); + if (objectCache.Contains(element)) + return; + + conn.InsertElement(element, replace, objectCache); + + objectCache.Add(element); + conn.InsertChildrenRecursive(element, replace, recursive, objectCache); + + conn.UpdateWithChildren(element); + } + + static void InsertChildrenRecursive(this SQLiteConnection conn, object element, bool replace, bool recursive, ISet objectCache = null) { + if (element == null) + return; + + objectCache = objectCache ?? new HashSet(); + foreach (var relationshipProperty in element.GetType().GetRelationshipProperties()) + { + var relationshipAttribute = relationshipProperty.GetAttribute(); + + // Ignore read-only attributes and process only 'CascadeInsert' attributes + if (relationshipAttribute.ReadOnly || !relationshipAttribute.IsCascadeInsert) + continue; + + var value = relationshipProperty.GetValue(element, null); + conn.InsertValue(value, replace, recursive, objectCache); + } + } + + static void InsertValue(this SQLiteConnection conn, object value, bool replace, bool recursive, ISet objectCache) { + if (value == null) + return; + + var enumerable = value as IEnumerable; + if (recursive) + { + if (enumerable != null) + conn.InsertAllWithChildrenRecursive(enumerable, replace, recursive, objectCache); + else + conn.InsertWithChildrenRecursive(value, replace, recursive, objectCache); + } + else + { + if (enumerable != null) + conn.InsertElements(enumerable, replace, objectCache); + else + conn.InsertElement(value, replace, objectCache); + } + } + + static IEnumerable InsertElements(this SQLiteConnection conn, IEnumerable elements, bool replace, ISet objectCache) { + if (elements == null) + return Enumerable.Empty(); + + objectCache = objectCache ?? new HashSet(); + var elementsToInsert = elements.Cast().Except(objectCache).ToList(); + if (elementsToInsert.Count == 0) + return Enumerable.Empty(); + + var primaryKeyProperty = elementsToInsert[0].GetType().GetPrimaryKey(); + var isAutoIncrementPrimaryKey = primaryKeyProperty != null && primaryKeyProperty.GetAttribute() != null; + + foreach (var element in elementsToInsert) { + conn.InsertElement(element, replace, primaryKeyProperty, isAutoIncrementPrimaryKey, objectCache); + objectCache.Add(element); + } + + return elementsToInsert; + } + + static void InsertElement(this SQLiteConnection conn, object element, bool replace, ISet objectCache) { + var primaryKeyProperty = element.GetType().GetPrimaryKey(); + var isAutoIncrementPrimaryKey = primaryKeyProperty != null && primaryKeyProperty.GetAttribute() != null; + + conn.InsertElement(element, replace, primaryKeyProperty, isAutoIncrementPrimaryKey, objectCache); + } + + static void InsertElement(this SQLiteConnection conn, object element, bool replace, PropertyInfo primaryKeyProperty, bool isAutoIncrementPrimaryKey, ISet objectCache) { + if (element == null || (objectCache != null && objectCache.Contains(element))) + return; + + bool isPrimaryKeySet = false; + if (replace && isAutoIncrementPrimaryKey) + { + var primaryKeyValue = primaryKeyProperty.GetValue(element, null); + var defaultPrimaryKeyValue = primaryKeyProperty.PropertyType.GetDefault(); + isPrimaryKeySet = primaryKeyValue != null && !primaryKeyValue.Equals(defaultPrimaryKeyValue); + } + + bool shouldReplace = replace && (!isAutoIncrementPrimaryKey || isPrimaryKeySet); + + // Only replace elements that have an assigned primary key + if (shouldReplace) + conn.InsertOrReplace(element); + else + conn.Insert(element); + } + + static void DeleteAllRecursive(this SQLiteConnection conn, IEnumerable elements, bool recursive, ISet objectCache = null) { + if (elements == null) + return; + + var isRootElement = objectCache == null; + objectCache = objectCache ?? new HashSet(); + + var elementList = elements.Cast().Except(objectCache).ToList(); + + // Mark the objects for deletion + foreach (var element in elementList) + objectCache.Add(element); + + if (recursive) + { + foreach (var element in elementList) + { + var type = element.GetType(); + foreach (var relationshipProperty in type.GetRelationshipProperties()) + { + var relationshipAttribute = relationshipProperty.GetAttribute(); + + // Ignore read-only attributes or those that are not marked as CascadeDelete + if (!relationshipAttribute.IsCascadeDelete || relationshipAttribute.ReadOnly) + continue; + + var value = relationshipProperty.GetValue(element, null); + conn.DeleteValueRecursive(value, recursive, objectCache); + } + } + } + + // To improve performance, the root method call will delete all the objects at once + if (isRootElement) { + conn.DeleteAllObjects(objectCache); + } + } + + static void DeleteValueRecursive(this SQLiteConnection conn, object value, bool recursive, ISet objectCache) { + if (value == null) + return; + + var enumerable = value as IEnumerable ?? new [] { value }; + conn.DeleteAllRecursive(enumerable, recursive, objectCache); + } + + static void DeleteAllObjects(this SQLiteConnection conn, IEnumerable elements) { + if (elements == null) + return; + + var groupedElements = elements.Cast().GroupBy(o => o.GetType()); + foreach (var groupElement in groupedElements) + { + var type = groupElement.Key; + var primaryKeyProperty = type.GetPrimaryKey(); + Assert(primaryKeyProperty != null, type, null, "Cannot delete objects without primary key"); + var primaryKeyValues = (from element in groupElement + select primaryKeyProperty.GetValue(element, null)).ToArray(); + conn.DeleteAllIds(primaryKeyValues, type.GetTableName(), primaryKeyProperty.GetColumnName()); + } + } + + private static void RefreshForeignKeys(object element) + { + var type = element.GetType(); + foreach (var relationshipProperty in type.GetRelationshipProperties()) + { + var relationshipAttribute = relationshipProperty.GetAttribute(); + + // Ignore read-only attributes + if (relationshipAttribute.ReadOnly) + continue; + + if (relationshipAttribute is OneToOneAttribute || relationshipAttribute is ManyToOneAttribute) + { + var foreignKeyProperty = type.GetForeignKeyProperty(relationshipProperty); + if (foreignKeyProperty != null) + { + EnclosedType enclosedType; + var entityType = relationshipProperty.GetEntityType(out enclosedType); + var destinationPrimaryKeyProperty = entityType.GetPrimaryKey(); + Assert(enclosedType == EnclosedType.None, type, relationshipProperty, "ToOne relationships cannot be lists or arrays"); + Assert(destinationPrimaryKeyProperty != null, type, relationshipProperty, "Found foreign key but destination Type doesn't have primary key"); + + var relationshipValue = relationshipProperty.GetValue(element, null); + object foreignKeyValue = null; + if (relationshipValue != null) + { + foreignKeyValue = destinationPrimaryKeyProperty.GetValue(relationshipValue, null); + } + foreignKeyProperty.SetValue(element, foreignKeyValue, null); + } + } + else if (relationshipAttribute is TextBlobAttribute) + { + TextBlobOperations.UpdateTextBlobProperty(element, relationshipProperty); + } + } + } + + + private static void UpdateInverseForeignKeys(this SQLiteConnection conn, object element) + { + foreach (var relationshipProperty in element.GetType().GetRelationshipProperties()) + { + var relationshipAttribute = relationshipProperty.GetAttribute(); + + // Ignore read-only attributes + if (relationshipAttribute.ReadOnly) + continue; + + if (relationshipAttribute is OneToManyAttribute) + { + conn.UpdateOneToManyInverseForeignKey(element, relationshipProperty); + } + else if (relationshipAttribute is OneToOneAttribute) + { + conn.UpdateOneToOneInverseForeignKey(element, relationshipProperty); + } + else if (relationshipAttribute is ManyToManyAttribute) + { + conn.UpdateManyToManyForeignKeys(element, relationshipProperty); + } + } + } + + private static void UpdateOneToManyInverseForeignKey(this SQLiteConnection conn, object element, PropertyInfo relationshipProperty) + { + var type = element.GetType(); + + EnclosedType enclosedType; + var entityType = relationshipProperty.GetEntityType(out enclosedType); + + var originPrimaryKeyProperty = type.GetPrimaryKey(); + var inversePrimaryKeyProperty = entityType.GetPrimaryKey(); + var inverseForeignKeyProperty = type.GetForeignKeyProperty(relationshipProperty, inverse: true); + + Assert(enclosedType != EnclosedType.None, type, relationshipProperty, "OneToMany relationships must be List or Array of entities"); + Assert(originPrimaryKeyProperty != null, type, relationshipProperty, "OneToMany relationships require Primary Key in the origin entity"); + Assert(inversePrimaryKeyProperty != null, type, relationshipProperty, "OneToMany relationships require Primary Key in the destination entity"); + Assert(inverseForeignKeyProperty != null, type, relationshipProperty, "Unable to find foreign key for OneToMany relationship"); + + var inverseProperty = type.GetInverseProperty(relationshipProperty); + if (inverseProperty != null) + { + EnclosedType inverseEnclosedType; + var inverseEntityType = inverseProperty.GetEntityType(out inverseEnclosedType); + Assert(inverseEnclosedType == EnclosedType.None, type, relationshipProperty, "OneToMany inverse relationship shouldn't be List or Array"); + Assert(inverseEntityType == type, type, relationshipProperty, "OneToMany inverse relationship is not the expected type"); + } + + var keyValue = originPrimaryKeyProperty.GetValue(element, null); + var children = (IEnumerable)relationshipProperty.GetValue(element, null); + var childrenKeyList = new List(); + if (children != null) + { + foreach (var child in children) + { + var childKey = inversePrimaryKeyProperty.GetValue(child, null); + childrenKeyList.Add(childKey); + + inverseForeignKeyProperty.SetValue(child, keyValue, null); + if (inverseProperty != null) + { + inverseProperty.SetValue(child, element, null); + } + } + } + + + // Delete previous relationships + var deleteQuery = string.Format("update [{0}] set [{1}] = NULL where [{1}] == ?", + entityType.GetTableName(), inverseForeignKeyProperty.GetColumnName()); + var deleteParamaters = new List { keyValue }; + conn.Execute(deleteQuery, deleteParamaters.ToArray()); + + var chunks = Split(childrenKeyList, queryLimit); + var loopTo = chunks.Count == 0 ? 1 : chunks.Count; + for (int i = 0; i < loopTo; i++) { + var chunk = chunks.Count > i ? chunks[i] :new List(); + // Objects already updated, now change the database + var childrenPlaceHolders = string.Join(",", Enumerable.Repeat("?", chunk.Count)); + var query = string.Format("update [{0}] set [{1}] = ? where [{2}] in ({3})", + entityType.GetTableName(), inverseForeignKeyProperty.GetColumnName(), inversePrimaryKeyProperty.GetColumnName(), childrenPlaceHolders); + + var parameters = new List { keyValue }; + parameters.AddRange(chunk); + conn.Execute(query, parameters.ToArray()); + } + } + + private static void UpdateOneToOneInverseForeignKey(this SQLiteConnection conn, object element, PropertyInfo relationshipProperty) + { + var type = element.GetType(); + + EnclosedType enclosedType; + var entityType = relationshipProperty.GetEntityType(out enclosedType); + + var originPrimaryKeyProperty = type.GetPrimaryKey(); + var inversePrimaryKeyProperty = entityType.GetPrimaryKey(); + var inverseForeignKeyProperty = type.GetForeignKeyProperty(relationshipProperty, inverse: true); + + Assert(enclosedType == EnclosedType.None, type, relationshipProperty, "OneToOne relationships cannot be List or Array of entities"); + + var inverseProperty = type.GetInverseProperty(relationshipProperty); + if (inverseProperty != null) + { + EnclosedType inverseEnclosedType; + var inverseEntityType = inverseProperty.GetEntityType(out inverseEnclosedType); + Assert(inverseEnclosedType == EnclosedType.None, type, relationshipProperty, "OneToOne inverse relationship shouldn't be List or Array"); + Assert(inverseEntityType == type, type, relationshipProperty, "OneToOne inverse relationship is not the expected type"); + } + + object keyValue = null; + if (originPrimaryKeyProperty != null && inverseForeignKeyProperty != null) + { + keyValue = originPrimaryKeyProperty.GetValue(element, null); + } + + object childKey = null; + var child = relationshipProperty.GetValue(element, null); + if (child != null) + { + if (inverseForeignKeyProperty != null && keyValue != null) + { + inverseForeignKeyProperty.SetValue(child, keyValue, null); + } + if (inverseProperty != null) + { + inverseProperty.SetValue(child, element, null); + } + if (inversePrimaryKeyProperty != null) + { + childKey = inversePrimaryKeyProperty.GetValue(child, null); + } + } + + + // Objects already updated, now change the database + if (inverseForeignKeyProperty != null && inversePrimaryKeyProperty != null) + { + var query = string.Format("update [{0}] set [{1}] = ? where [{2}] == ?", + entityType.GetTableName(), inverseForeignKeyProperty.GetColumnName(), inversePrimaryKeyProperty.GetColumnName()); + conn.Execute(query, keyValue, childKey); + + // Delete previous relationships + var deleteQuery = string.Format("update [{0}] set [{1}] = NULL where [{1}] == ? and [{2}] not in (?)", + entityType.GetTableName(), inverseForeignKeyProperty.GetColumnName(), inversePrimaryKeyProperty.GetColumnName()); + conn.Execute(deleteQuery, keyValue, childKey ?? ""); + } + } + + private static void UpdateManyToManyForeignKeys(this SQLiteConnection conn, object element, PropertyInfo relationshipProperty) + { + var type = element.GetType(); + + EnclosedType enclosedType; + var entityType = relationshipProperty.GetEntityType(out enclosedType); + + var currentEntityPrimaryKeyProperty = type.GetPrimaryKey(); + var otherEntityPrimaryKeyProperty = entityType.GetPrimaryKey(); + var manyToManyMetaInfo = type.GetManyToManyMetaInfo(relationshipProperty); + var currentEntityForeignKeyProperty = manyToManyMetaInfo.OriginProperty; + var otherEntityForeignKeyProperty = manyToManyMetaInfo.DestinationProperty; + var intermediateType = manyToManyMetaInfo.IntermediateType; + + Assert(enclosedType != EnclosedType.None, type, relationshipProperty, "ManyToMany relationship must be a List or Array"); + Assert(currentEntityPrimaryKeyProperty != null, type, relationshipProperty, "ManyToMany relationship origin must have Primary Key"); + Assert(otherEntityPrimaryKeyProperty != null, type, relationshipProperty, "ManyToMany relationship destination must have Primary Key"); + Assert(intermediateType != null, type, relationshipProperty, "ManyToMany relationship intermediate type cannot be null"); + Assert(currentEntityForeignKeyProperty != null, type, relationshipProperty, "ManyToMany relationship origin must have a foreign key defined in the intermediate type"); + Assert(otherEntityForeignKeyProperty != null, type, relationshipProperty, "ManyToMany relationship destination must have a foreign key defined in the intermediate type"); + + var primaryKey = currentEntityPrimaryKeyProperty.GetValue(element, null); + + // Obtain the list of children keys + var childList = (IEnumerable)relationshipProperty.GetValue(element, null); + var childKeyList = (from object child in childList ?? new List() + select otherEntityPrimaryKeyProperty.GetValue(child, null)).ToList(); + + List currentChildKeyList = new List(); + var chunks = Split(childKeyList, queryLimit); + var loopTo = chunks.Count == 0 ? 1 : chunks.Count; + for (int i = 0; i < loopTo; i++) { + var chunk = chunks.Count > i ? chunks[i] : new List(); + // Check for already existing relationships + var childrenPlaceHolders = string.Join(",", Enumerable.Repeat("?", chunk.Count)); + var currentChildrenQuery = string.Format("select [{0}] from [{1}] where [{2}] == ? and [{0}] in ({3})", + otherEntityForeignKeyProperty.GetColumnName(), intermediateType.GetTableName(), currentEntityForeignKeyProperty.GetColumnName(), childrenPlaceHolders); + + var parameters = new List { primaryKey }; + parameters.AddRange(chunk); + currentChildKeyList.AddRange( + from object child in + conn.Query(conn.GetMapping(intermediateType), currentChildrenQuery, parameters.ToArray()) + select otherEntityForeignKeyProperty.GetValue(child, null)); + } + + // Insert missing relationships in the intermediate table + var missingChildKeyList = childKeyList.Where(o => !currentChildKeyList.Contains(o)).ToList(); + var missingIntermediateObjects = new List(missingChildKeyList.Count); + foreach (var missingChildKey in missingChildKeyList) + { + var intermediateObject = Activator.CreateInstance(intermediateType); + currentEntityForeignKeyProperty.SetValue(intermediateObject, primaryKey, null); + otherEntityForeignKeyProperty.SetValue(intermediateObject, missingChildKey, null); + + missingIntermediateObjects.Add(intermediateObject); + } + + conn.InsertAll(missingIntermediateObjects); + + + + for (int i = 0; i < loopTo; i++) + { + var chunk = chunks.Count > i ? chunks[i] : new List(); + var childrenPlaceHolders = string.Join(",", Enumerable.Repeat("?", chunk.Count)); + + // Delete any other pending relationship + var deleteQuery = string.Format("delete from [{0}] where [{1}] == ? and [{2}] not in ({3})", + intermediateType.GetTableName(), currentEntityForeignKeyProperty.GetColumnName(), + otherEntityForeignKeyProperty.GetColumnName(), childrenPlaceHolders); + + var parameters = new List { primaryKey }; + parameters.AddRange(chunk); + conn.Execute(deleteQuery, parameters.ToArray()); + } + + } + + private static void DeleteAllIds(this SQLiteConnection conn, object[] primaryKeyValues, string entityName, string primaryKeyName) { + if (primaryKeyValues == null || primaryKeyValues.Length == 0) + return; + + if (primaryKeyValues.Length <= queryLimit) + { + var placeholdersString = string.Join(",", Enumerable.Repeat("?", primaryKeyValues.Length)); + var deleteQuery = string.Format("delete from [{0}] where [{1}] in ({2})", entityName, primaryKeyName, placeholdersString); + + conn.Execute(deleteQuery, primaryKeyValues); + } + else { + foreach (var primaryKeys in Split(primaryKeyValues.ToList(), queryLimit)) { + conn.DeleteAllIds(primaryKeys.ToArray(), entityName, primaryKeyName); + } + + } + } + + static List> Split(List items, int sliceSize = 30) + { + List> list = new List>(); + for (int i = 0; i < items.Count; i += sliceSize) + list.Add(items.GetRange(i, Math.Min(sliceSize, items.Count - i))); + return list; + } + + static void Assert(bool assertion, Type type, PropertyInfo property, string message) { + if (EnableRuntimeAssertions && !assertion) + throw new IncorrectRelationshipException(type.Name, property != null ? property.Name : string.Empty , message); + } + #endregion + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/WriteOperations.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/WriteOperations.cs.meta new file mode 100644 index 0000000..44f57d2 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Extensions/WriteOperations.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 62ef0acd4a6442679d5fa4c4e602157c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/SQLiteAsyncUnitask.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/SQLiteAsyncUnitask.cs new file mode 100644 index 0000000..b76414b --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/SQLiteAsyncUnitask.cs @@ -0,0 +1 @@ +#if SQLITEASYNC_UNITASK namespace SqlCipher4Unity3D.UniTaskIntegration { using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading; using Cysharp.Threading.Tasks; /// /// A pooled asynchronous connection to a SQLite database. /// public partial class SQLiteAsyncConnection { readonly SQLiteConnectionString _connectionString; // NOTE(pyoung): added for SqlCipher4Unity3D public SQLiteAsyncConnection(string databasePath, string password, bool storeDateTimeAsTicks = true) : this(new SQLiteConnectionString(databasePath, SQLiteOpenFlags.Create | SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.FullMutex, storeDateTimeAsTicks, password)) { } /// /// Constructs a new SQLiteAsyncConnection and opens a pooled SQLite database specified by databasePath. /// /// /// Specifies the path to the database file. /// /// /// Specifies whether to store DateTime properties as ticks (true) or strings (false). You /// absolutely do want to store them as Ticks in all new projects. The value of false is /// only here for backwards compatibility. There is a *significant* speed advantage, with no /// down sides, when setting storeDateTimeAsTicks = true. /// If you use DateTimeOffset properties, it will be always stored as ticks regardingless /// the storeDateTimeAsTicks parameter. /// public SQLiteAsyncConnection(string databasePath, bool storeDateTimeAsTicks = true) : this(new SQLiteConnectionString(databasePath, SQLiteOpenFlags.Create | SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.FullMutex, storeDateTimeAsTicks)) { } /// /// Constructs a new SQLiteAsyncConnection and opens a pooled SQLite database specified by databasePath. /// /// /// Specifies the path to the database file. /// /// /// Flags controlling how the connection should be opened. /// Async connections should have the FullMutex flag set to provide best performance. /// /// /// Specifies whether to store DateTime properties as ticks (true) or strings (false). You /// absolutely do want to store them as Ticks in all new projects. The value of false is /// only here for backwards compatibility. There is a *significant* speed advantage, with no /// down sides, when setting storeDateTimeAsTicks = true. /// If you use DateTimeOffset properties, it will be always stored as ticks regardingless /// the storeDateTimeAsTicks parameter. /// public SQLiteAsyncConnection(string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks = true) : this(new SQLiteConnectionString(databasePath, openFlags, storeDateTimeAsTicks)) { } /// /// Constructs a new SQLiteAsyncConnection and opens a pooled SQLite database /// using the given connection string. /// /// /// Details on how to find and open the database. /// public SQLiteAsyncConnection(SQLiteConnectionString connectionString) { _connectionString = connectionString; } /// /// Gets the database path used by this connection. /// public string DatabasePath => GetConnection().DatabasePath; /// /// Gets the SQLite library version number. 3007014 would be v3.7.14 /// public int LibVersionNumber => GetConnection().LibVersionNumber; /// /// The format to use when storing DateTime properties as strings. Ignored if StoreDateTimeAsTicks is true. /// /// The date time string format. public string DateTimeStringFormat => GetConnection().DateTimeStringFormat; /// /// The amount of time to wait for a table to become unlocked. /// public TimeSpan GetBusyTimeout() { return GetConnection().BusyTimeout; } /// /// Sets the amount of time to wait for a table to become unlocked. /// public UniTask SetBusyTimeoutAsync(TimeSpan value) { return ReadAsync(conn => { conn.BusyTimeout = value; return null; }); } /// /// Enables the write ahead logging. WAL is significantly faster in most scenarios /// by providing better concurrency and better disk IO performance than the normal /// journal mode. You only need to call this function once in the lifetime of the database. /// public UniTask EnableWriteAheadLoggingAsync() { return WriteAsync(conn => { conn.EnableWriteAheadLogging(); return null; }); } /// /// Whether to store DateTime properties as ticks (true) or strings (false). /// public bool StoreDateTimeAsTicks => GetConnection().StoreDateTimeAsTicks; /// /// Whether to store TimeSpan properties as ticks (true) or strings (false). /// public bool StoreTimeSpanAsTicks => GetConnection().StoreTimeSpanAsTicks; /// /// Whether to writer queries to during execution. /// /// The tracer. public bool Trace { get { return GetConnection().Trace; } set { GetConnection().Trace = value; } } /// /// The delegate responsible for writing trace lines. /// /// The tracer. public Action Tracer { get { return GetConnection().Tracer; } set { GetConnection().Tracer = value; } } /// /// Whether Trace lines should be written that show the execution time of queries. /// public bool TimeExecution { get { return GetConnection().TimeExecution; } set { GetConnection().TimeExecution = value; } } /// /// Returns the mappings from types to tables that the connection /// currently understands. /// public IEnumerable TableMappings => GetConnection().TableMappings; /// /// Closes all connections to all async databases. /// You should *never* need to do this. /// This is a blocking operation that will return when all connections /// have been closed. /// public static void ResetPool() { SQLiteConnectionPool.Shared.Reset(); } /// /// Gets the pooled lockable connection used by this async connection. /// You should never need to use this. This is provided only to add additional /// functionality to SQLite-net. If you use this connection, you must use /// the Lock method on it while using it. /// public SQLiteConnectionWithLock GetConnection() { return SQLiteConnectionPool.Shared.GetConnection(_connectionString); } SQLiteConnectionWithLock GetConnectionAndTransactionLock(out object transactionLock) { return SQLiteConnectionPool.Shared.GetConnectionAndTransactionLock(_connectionString, out transactionLock); } /// /// Closes any pooled connections used by the database. /// public UniTask CloseAsync() { return UniTask.RunOnThreadPool(() => SQLiteConnectionPool.Shared.CloseConnection (_connectionString), true, CancellationToken.None); } UniTask ReadAsync(Func read) { return UniTask.RunOnThreadPool(() => { var conn = GetConnection(); using (conn.Lock()) { return read(conn); } }, false, CancellationToken.None); } UniTask WriteAsync(Func write) { return UniTask.RunOnThreadPool(() => { var conn = GetConnection(); using (conn.Lock()) { return write(conn); } },false, CancellationToken.None); } UniTask TransactAsync(Func transact) { return UniTask.RunOnThreadPool(() => { var conn = GetConnectionAndTransactionLock(out var transactionLock); lock (transactionLock) { using (conn.Lock()) { return transact(conn); } } },false, CancellationToken.None); } /// /// Enable or disable extension loading. /// public UniTask EnableLoadExtensionAsync(bool enabled) { return WriteAsync(conn => { conn.EnableLoadExtension(enabled); return null; }); } /// /// Executes a "create table if not exists" on the database. It also /// creates any specified indexes on the columns of the table. It uses /// a schema automatically generated from the specified type. You can /// later access this schema by calling GetMapping. /// /// /// Whether the table was created or migrated. /// public UniTask CreateTableAsync(CreateFlags createFlags = CreateFlags.None) where T : new() { return WriteAsync(conn => conn.CreateTable(createFlags)); } /// /// Executes a "create table if not exists" on the database. It also /// creates any specified indexes on the columns of the table. It uses /// a schema automatically generated from the specified type. You can /// later access this schema by calling GetMapping. /// /// Type to reflect to a database table. /// Optional flags allowing implicit PK and indexes based on naming conventions. /// /// Whether the table was created or migrated. /// public UniTask CreateTableAsync(Type ty, CreateFlags createFlags = CreateFlags.None) { return WriteAsync(conn => conn.CreateTable(ty, createFlags)); } /// /// Executes a "create table if not exists" on the database for each type. It also /// creates any specified indexes on the columns of the table. It uses /// a schema automatically generated from the specified type. You can /// later access this schema by calling GetMapping. /// /// /// Whether the table was created or migrated for each type. /// public UniTask CreateTablesAsync(CreateFlags createFlags = CreateFlags.None) where T : new() where T2 : new() { return CreateTablesAsync(createFlags, typeof(T), typeof(T2)); } /// /// Executes a "create table if not exists" on the database for each type. It also /// creates any specified indexes on the columns of the table. It uses /// a schema automatically generated from the specified type. You can /// later access this schema by calling GetMapping. /// /// /// Whether the table was created or migrated for each type. /// public UniTask CreateTablesAsync(CreateFlags createFlags = CreateFlags.None) where T : new() where T2 : new() where T3 : new() { return CreateTablesAsync(createFlags, typeof(T), typeof(T2), typeof(T3)); } /// /// Executes a "create table if not exists" on the database for each type. It also /// creates any specified indexes on the columns of the table. It uses /// a schema automatically generated from the specified type. You can /// later access this schema by calling GetMapping. /// /// /// Whether the table was created or migrated for each type. /// public UniTask CreateTablesAsync(CreateFlags createFlags = CreateFlags.None) where T : new() where T2 : new() where T3 : new() where T4 : new() { return CreateTablesAsync(createFlags, typeof(T), typeof(T2), typeof(T3), typeof(T4)); } /// /// Executes a "create table if not exists" on the database for each type. It also /// creates any specified indexes on the columns of the table. It uses /// a schema automatically generated from the specified type. You can /// later access this schema by calling GetMapping. /// /// /// Whether the table was created or migrated for each type. /// public UniTask CreateTablesAsync(CreateFlags createFlags = CreateFlags.None) where T : new() where T2 : new() where T3 : new() where T4 : new() where T5 : new() { return CreateTablesAsync(createFlags, typeof(T), typeof(T2), typeof(T3), typeof(T4), typeof(T5)); } /// /// Executes a "create table if not exists" on the database for each type. It also /// creates any specified indexes on the columns of the table. It uses /// a schema automatically generated from the specified type. You can /// later access this schema by calling GetMapping. /// /// /// Whether the table was created or migrated for each type. /// public UniTask CreateTablesAsync(CreateFlags createFlags = CreateFlags.None, params Type[] types) { return WriteAsync(conn => conn.CreateTables(createFlags, types)); } /// /// Executes a "drop table" on the database. This is non-recoverable. /// public UniTask DropTableAsync() where T : new() { return WriteAsync(conn => conn.DropTable()); } /// /// Executes a "drop table" on the database. This is non-recoverable. /// /// /// The TableMapping used to identify the table. /// public UniTask DropTableAsync(TableMapping map) { return WriteAsync(conn => conn.DropTable(map)); } /// /// Creates an index for the specified table and column. /// /// Name of the database table /// Name of the column to index /// Whether the index should be unique /// Zero on success. public UniTask CreateIndexAsync(string tableName, string columnName, bool unique = false) { return WriteAsync(conn => conn.CreateIndex(tableName, columnName, unique)); } /// /// Creates an index for the specified table and column. /// /// Name of the index to create /// Name of the database table /// Name of the column to index /// Whether the index should be unique /// Zero on success. public UniTask CreateIndexAsync(string indexName, string tableName, string columnName, bool unique = false) { return WriteAsync(conn => conn.CreateIndex(indexName, tableName, columnName, unique)); } /// /// Creates an index for the specified table and columns. /// /// Name of the database table /// An array of column names to index /// Whether the index should be unique /// Zero on success. public UniTask CreateIndexAsync(string tableName, string[] columnNames, bool unique = false) { return WriteAsync(conn => conn.CreateIndex(tableName, columnNames, unique)); } /// /// Creates an index for the specified table and columns. /// /// Name of the index to create /// Name of the database table /// An array of column names to index /// Whether the index should be unique /// Zero on success. public UniTask CreateIndexAsync(string indexName, string tableName, string[] columnNames, bool unique = false) { return WriteAsync(conn => conn.CreateIndex(indexName, tableName, columnNames, unique)); } /// /// Creates an index for the specified object property. /// e.g. CreateIndex<Client>(c => c.Name); /// /// Type to reflect to a database table. /// Property to index /// Whether the index should be unique /// Zero on success. public UniTask CreateIndexAsync(Expression> property, bool unique = false) { return WriteAsync(conn => conn.CreateIndex(property, unique)); } /// /// Inserts the given object and retrieves its /// auto incremented primary key if it has one. /// /// /// The object to insert. /// /// /// The number of rows added to the table. /// public UniTask InsertAsync(object obj) { return WriteAsync(conn => conn.Insert(obj)); } /// /// Inserts the given object (and updates its /// auto incremented primary key if it has one). /// The return value is the number of rows added to the table. /// /// /// The object to insert. /// /// /// The type of object to insert. /// /// /// The number of rows added to the table. /// public UniTask InsertAsync(object obj, Type objType) { return WriteAsync(conn => conn.Insert(obj, objType)); } /// /// Inserts the given object (and updates its /// auto incremented primary key if it has one). /// The return value is the number of rows added to the table. /// /// /// The object to insert. /// /// /// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... /// /// /// The number of rows added to the table. /// public UniTask InsertAsync(object obj, string extra) { return WriteAsync(conn => conn.Insert(obj, extra)); } /// /// Inserts the given object (and updates its /// auto incremented primary key if it has one). /// The return value is the number of rows added to the table. /// /// /// The object to insert. /// /// /// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... /// /// /// The type of object to insert. /// /// /// The number of rows added to the table. /// public UniTask InsertAsync(object obj, string extra, Type objType) { return WriteAsync(conn => conn.Insert(obj, extra, objType)); } /// /// Inserts the given object (and updates its /// auto incremented primary key if it has one). /// The return value is the number of rows added to the table. /// If a UNIQUE constraint violation occurs with /// some pre-existing object, this function deletes /// the old object. /// /// /// The object to insert. /// /// /// The number of rows modified. /// public UniTask InsertOrReplaceAsync(object obj) { return WriteAsync(conn => conn.InsertOrReplace(obj)); } /// /// Inserts the given object (and updates its /// auto incremented primary key if it has one). /// The return value is the number of rows added to the table. /// If a UNIQUE constraint violation occurs with /// some pre-existing object, this function deletes /// the old object. /// /// /// The object to insert. /// /// /// The type of object to insert. /// /// /// The number of rows modified. /// public UniTask InsertOrReplaceAsync(object obj, Type objType) { return WriteAsync(conn => conn.InsertOrReplace(obj, objType)); } /// /// Updates all of the columns of a table using the specified object /// except for its primary key. /// The object is required to have a primary key. /// /// /// The object to update. It must have a primary key designated using the PrimaryKeyAttribute. /// /// /// The number of rows updated. /// public UniTask UpdateAsync(object obj) { return WriteAsync(conn => conn.Update(obj)); } /// /// Updates all of the columns of a table using the specified object /// except for its primary key. /// The object is required to have a primary key. /// /// /// The object to update. It must have a primary key designated using the PrimaryKeyAttribute. /// /// /// The type of object to insert. /// /// /// The number of rows updated. /// public UniTask UpdateAsync(object obj, Type objType) { return WriteAsync(conn => conn.Update(obj, objType)); } /// /// Updates all specified objects. /// /// /// An of the objects to insert. /// /// /// A boolean indicating if the inserts should be wrapped in a transaction /// /// /// The number of rows modified. /// public UniTask UpdateAllAsync(IEnumerable objects, bool runInTransaction = true) { return WriteAsync(conn => conn.UpdateAll(objects, runInTransaction)); } /// /// Deletes the given object from the database using its primary key. /// /// /// The object to delete. It must have a primary key designated using the PrimaryKeyAttribute. /// /// /// The number of rows deleted. /// public UniTask DeleteAsync(object objectToDelete) { return WriteAsync(conn => conn.Delete(objectToDelete)); } /// /// Deletes the object with the specified primary key. /// /// /// The primary key of the object to delete. /// /// /// The number of objects deleted. /// /// /// The type of object. /// public UniTask DeleteAsync(object primaryKey) { return WriteAsync(conn => conn.Delete(primaryKey)); } /// /// Deletes the object with the specified primary key. /// /// /// The primary key of the object to delete. /// /// /// The TableMapping used to identify the table. /// /// /// The number of objects deleted. /// public UniTask DeleteAsync(object primaryKey, TableMapping map) { return WriteAsync(conn => conn.Delete(primaryKey, map)); } /// /// Deletes all the objects from the specified table. /// WARNING WARNING: Let me repeat. It deletes ALL the objects from the /// specified table. Do you really want to do that? /// /// /// The number of objects deleted. /// /// /// The type of objects to delete. /// public UniTask DeleteAllAsync() { return WriteAsync(conn => conn.DeleteAll()); } /// /// Deletes all the objects from the specified table. /// WARNING WARNING: Let me repeat. It deletes ALL the objects from the /// specified table. Do you really want to do that? /// /// /// The TableMapping used to identify the table. /// /// /// The number of objects deleted. /// public UniTask DeleteAllAsync(TableMapping map) { return WriteAsync(conn => conn.DeleteAll(map)); } /// /// Backup the entire database to the specified path. /// /// Path to backup file. /// The name of the database to backup (usually "main"). public UniTask BackupAsync(string destinationDatabasePath, string databaseName = "main") { return WriteAsync(conn => { conn.Backup(destinationDatabasePath, databaseName); return 0; }); } /// /// Attempts to retrieve an object with the given primary key from the table /// associated with the specified type. Use of this method requires that /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). /// /// /// The primary key. /// /// /// The object with the given primary key. Throws a not found exception /// if the object is not found. /// public UniTask GetAsync(object pk) where T : new() { return ReadAsync(conn => conn.Get(pk)); } /// /// Attempts to retrieve an object with the given primary key from the table /// associated with the specified type. Use of this method requires that /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). /// /// /// The primary key. /// /// /// The TableMapping used to identify the table. /// /// /// The object with the given primary key. Throws a not found exception /// if the object is not found. /// public UniTask GetAsync(object pk, TableMapping map) { return ReadAsync(conn => conn.Get(pk, map)); } /// /// Attempts to retrieve the first object that matches the predicate from the table /// associated with the specified type. /// /// /// A predicate for which object to find. /// /// /// The object that matches the given predicate. Throws a not found exception /// if the object is not found. /// public UniTask GetAsync(Expression> predicate) where T : new() { return ReadAsync(conn => conn.Get(predicate)); } /// /// Attempts to retrieve an object with the given primary key from the table /// associated with the specified type. Use of this method requires that /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). /// /// /// The primary key. /// /// /// The object with the given primary key or null /// if the object is not found. /// public UniTask FindAsync(object pk) where T : new() { return ReadAsync(conn => conn.Find(pk)); } /// /// Attempts to retrieve an object with the given primary key from the table /// associated with the specified type. Use of this method requires that /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). /// /// /// The primary key. /// /// /// The TableMapping used to identify the table. /// /// /// The object with the given primary key or null /// if the object is not found. /// public UniTask FindAsync(object pk, TableMapping map) { return ReadAsync(conn => conn.Find(pk, map)); } /// /// Attempts to retrieve the first object that matches the predicate from the table /// associated with the specified type. /// /// /// A predicate for which object to find. /// /// /// The object that matches the given predicate or null /// if the object is not found. /// public UniTask FindAsync(Expression> predicate) where T : new() { return ReadAsync(conn => conn.Find(predicate)); } /// /// Attempts to retrieve the first object that matches the query from the table /// associated with the specified type. /// /// /// The fully escaped SQL. /// /// /// Arguments to substitute for the occurences of '?' in the query. /// /// /// The object that matches the given predicate or null /// if the object is not found. /// public UniTask FindWithQueryAsync(string query, params object[] args) where T : new() { return ReadAsync(conn => conn.FindWithQuery(query, args)); } /// /// Attempts to retrieve the first object that matches the query from the table /// associated with the specified type. /// /// /// The TableMapping used to identify the table. /// /// /// The fully escaped SQL. /// /// /// Arguments to substitute for the occurences of '?' in the query. /// /// /// The object that matches the given predicate or null /// if the object is not found. /// public UniTask FindWithQueryAsync(TableMapping map, string query, params object[] args) { return ReadAsync(conn => conn.FindWithQuery(map, query, args)); } /// /// Retrieves the mapping that is automatically generated for the given type. /// /// /// The type whose mapping to the database is returned. /// /// /// Optional flags allowing implicit PK and indexes based on naming conventions /// /// /// The mapping represents the schema of the columns of the database and contains /// methods to set and get properties of objects. /// public UniTask GetMappingAsync(Type type, CreateFlags createFlags = CreateFlags.None) { return ReadAsync(conn => conn.GetMapping(type, createFlags)); } /// /// Retrieves the mapping that is automatically generated for the given type. /// /// /// Optional flags allowing implicit PK and indexes based on naming conventions /// /// /// The mapping represents the schema of the columns of the database and contains /// methods to set and get properties of objects. /// public UniTask GetMappingAsync(CreateFlags createFlags = CreateFlags.None) where T : new() { return ReadAsync(conn => conn.GetMapping(createFlags)); } /// /// Query the built-in sqlite table_info table for a specific tables columns. /// /// The columns contains in the table. /// Table name. public UniTask> GetTableInfoAsync(string tableName) { return ReadAsync(conn => conn.GetTableInfo(tableName)); } /// /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' /// in the command text for each of the arguments and then executes that command. /// Use this method instead of Query when you don't expect rows back. Such cases include /// INSERTs, UPDATEs, and DELETEs. /// You can set the Trace or TimeExecution properties of the connection /// to profile execution. /// /// /// The fully escaped SQL. /// /// /// Arguments to substitute for the occurences of '?' in the query. /// /// /// The number of rows modified in the database as a result of this execution. /// public UniTask ExecuteAsync(string query, params object[] args) { return WriteAsync(conn => conn.Execute(query, args)); } /// /// Inserts all specified objects. /// /// /// An of the objects to insert. /// /// A boolean indicating if the inserts should be wrapped in a transaction. /// /// /// The number of rows added to the table. /// public UniTask InsertAllAsync(IEnumerable objects, bool runInTransaction = true) { return WriteAsync(conn => conn.InsertAll(objects, runInTransaction)); } /// /// Inserts all specified objects. /// /// /// An of the objects to insert. /// /// /// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... /// /// /// A boolean indicating if the inserts should be wrapped in a transaction. /// /// /// The number of rows added to the table. /// public UniTask InsertAllAsync(IEnumerable objects, string extra, bool runInTransaction = true) { return WriteAsync(conn => conn.InsertAll(objects, extra, runInTransaction)); } /// /// Inserts all specified objects. /// /// /// An of the objects to insert. /// /// /// The type of object to insert. /// /// /// A boolean indicating if the inserts should be wrapped in a transaction. /// /// /// The number of rows added to the table. /// public UniTask InsertAllAsync(IEnumerable objects, Type objType, bool runInTransaction = true) { return WriteAsync(conn => conn.InsertAll(objects, objType, runInTransaction)); } /// /// Executes within a (possibly nested) transaction by wrapping it in a SAVEPOINT. If an /// exception occurs the whole transaction is rolled back, not just the current savepoint. The exception /// is rethrown. /// /// /// The to perform within a transaction. can contain any number /// of operations on the connection but should never call or /// . /// public UniTask RunInTransactionAsync(Action action) { return TransactAsync(conn => { conn.BeginTransaction(); try { action(conn); conn.Commit(); return null; } catch (Exception) { conn.Rollback(); throw; } }); } /// /// Returns a queryable interface to the table represented by the given type. /// /// /// A queryable object that is able to translate Where, OrderBy, and Take /// queries into native SQL. /// public AsyncTableQuery Table() where T : new() { // // This isn't async as the underlying connection doesn't go out to the database // until the query is performed. The Async methods are on the query itself. // var conn = GetConnection(); return new AsyncTableQuery(conn.Table()); } /// /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' /// in the command text for each of the arguments and then executes that command. /// Use this method when return primitive values. /// You can set the Trace or TimeExecution properties of the connection /// to profile execution. /// /// /// The fully escaped SQL. /// /// /// Arguments to substitute for the occurences of '?' in the query. /// /// /// The number of rows modified in the database as a result of this execution. /// public UniTask ExecuteScalarAsync(string query, params object[] args) { return WriteAsync(conn => { var command = conn.CreateCommand(query, args); return command.ExecuteScalar(); }); } /// /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' /// in the command text for each of the arguments and then executes that command. /// It returns each row of the result using the mapping automatically generated for /// the given type. /// /// /// The fully escaped SQL. /// /// /// Arguments to substitute for the occurences of '?' in the query. /// /// /// A list with one result for each row returned by the query. /// public UniTask> QueryAsync(string query, params object[] args) where T : new() { return ReadAsync(conn => conn.Query(query, args)); } /// /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' /// in the command text for each of the arguments and then executes that command. /// It returns the first column of each row of the result. /// /// /// The fully escaped SQL. /// /// /// Arguments to substitute for the occurences of '?' in the query. /// /// /// A list with one result for the first column of each row returned by the query. /// public UniTask> QueryScalarsAsync(string query, params object[] args) { return ReadAsync(conn => conn.QueryScalars(query, args)); } /// /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' /// in the command text for each of the arguments and then executes that command. /// It returns each row of the result using the specified mapping. This function is /// only used by libraries in order to query the database via introspection. It is /// normally not used. /// /// /// A to use to convert the resulting rows /// into objects. /// /// /// The fully escaped SQL. /// /// /// Arguments to substitute for the occurences of '?' in the query. /// /// /// An enumerable with one result for each row returned by the query. /// public UniTask> QueryAsync(TableMapping map, string query, params object[] args) { return ReadAsync(conn => conn.Query(map, query, args)); } /// /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' /// in the command text for each of the arguments and then executes that command. /// It returns each row of the result using the mapping automatically generated for /// the given type. /// /// /// The fully escaped SQL. /// /// /// Arguments to substitute for the occurences of '?' in the query. /// /// /// An enumerable with one result for each row returned by the query. /// The enumerator will call sqlite3_step on each call to MoveNext, so the database /// connection must remain open for the lifetime of the enumerator. /// public UniTask> DeferredQueryAsync(string query, params object[] args) where T : new() { return ReadAsync(conn => (IEnumerable)conn.DeferredQuery(query, args).ToList()); } /// /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' /// in the command text for each of the arguments and then executes that command. /// It returns each row of the result using the specified mapping. This function is /// only used by libraries in order to query the database via introspection. It is /// normally not used. /// /// /// A to use to convert the resulting rows /// into objects. /// /// /// The fully escaped SQL. /// /// /// Arguments to substitute for the occurences of '?' in the query. /// /// /// An enumerable with one result for each row returned by the query. /// The enumerator will call sqlite3_step on each call to MoveNext, so the database /// connection must remain open for the lifetime of the enumerator. /// public UniTask> DeferredQueryAsync(TableMapping map, string query, params object[] args) { return ReadAsync(conn => (IEnumerable)conn.DeferredQuery(map, query, args).ToList()); } } /// /// Query to an asynchronous database connection. /// public class AsyncTableQuery where T : new() { TableQuery _innerQuery; /// /// Creates a new async query that uses given the synchronous query. /// public AsyncTableQuery(TableQuery innerQuery) { _innerQuery = innerQuery; } UniTask ReadAsync(Func read) { return UniTask.RunOnThreadPool(() => { var conn = (SQLiteConnectionWithLock)_innerQuery.Connection; using (conn.Lock()) { return read(conn); } }, false, CancellationToken.None); } UniTask WriteAsync(Func write) { return UniTask.RunOnThreadPool(() => { var conn = (SQLiteConnectionWithLock)_innerQuery.Connection; using (conn.Lock()) { return write(conn); } }, false, CancellationToken.None); } /// /// Filters the query based on a predicate. /// public AsyncTableQuery Where(Expression> predExpr) { return new AsyncTableQuery(_innerQuery.Where(predExpr)); } /// /// Skips a given number of elements from the query and then yields the remainder. /// public AsyncTableQuery Skip(int n) { return new AsyncTableQuery(_innerQuery.Skip(n)); } /// /// Yields a given number of elements from the query and then skips the remainder. /// public AsyncTableQuery Take(int n) { return new AsyncTableQuery(_innerQuery.Take(n)); } /// /// Order the query results according to a key. /// public AsyncTableQuery OrderBy(Expression> orderExpr) { return new AsyncTableQuery(_innerQuery.OrderBy(orderExpr)); } /// /// Order the query results according to a key. /// public AsyncTableQuery OrderByDescending(Expression> orderExpr) { return new AsyncTableQuery(_innerQuery.OrderByDescending(orderExpr)); } /// /// Order the query results according to a key. /// public AsyncTableQuery ThenBy(Expression> orderExpr) { return new AsyncTableQuery(_innerQuery.ThenBy(orderExpr)); } /// /// Order the query results according to a key. /// public AsyncTableQuery ThenByDescending(Expression> orderExpr) { return new AsyncTableQuery(_innerQuery.ThenByDescending(orderExpr)); } /// /// Queries the database and returns the results as a List. /// public UniTask> ToListAsync() { return ReadAsync(conn => _innerQuery.ToList()); } /// /// Queries the database and returns the results as an array. /// public UniTask ToArrayAsync() { return ReadAsync(conn => _innerQuery.ToArray()); } /// /// Execute SELECT COUNT(*) on the query /// public UniTask CountAsync() { return ReadAsync(conn => _innerQuery.Count()); } /// /// Execute SELECT COUNT(*) on the query with an additional WHERE clause. /// public UniTask CountAsync(Expression> predExpr) { return ReadAsync(conn => _innerQuery.Count(predExpr)); } /// /// Returns the element at a given index /// public UniTask ElementAtAsync(int index) { return ReadAsync(conn => _innerQuery.ElementAt(index)); } /// /// Returns the first element of this query. /// public UniTask FirstAsync() { return ReadAsync(conn => _innerQuery.First()); } /// /// Returns the first element of this query, or null if no element is found. /// public UniTask FirstOrDefaultAsync() { return ReadAsync(conn => _innerQuery.FirstOrDefault()); } /// /// Returns the first element of this query that matches the predicate. /// public UniTask FirstAsync(Expression> predExpr) { return ReadAsync(conn => _innerQuery.First(predExpr)); } /// /// Returns the first element of this query that matches the predicate. /// public UniTask FirstOrDefaultAsync(Expression> predExpr) { return ReadAsync(conn => _innerQuery.FirstOrDefault(predExpr)); } /// /// Delete all the rows that match this query and the given predicate. /// public UniTask DeleteAsync(Expression> predExpr) { return WriteAsync(conn => _innerQuery.Delete(predExpr)); } /// /// Delete all the rows that match this query. /// public UniTask DeleteAsync() { return WriteAsync(conn => _innerQuery.Delete()); } } class SQLiteConnectionPool { class Entry { public SQLiteConnectionWithLock Connection { get; private set; } public SQLiteConnectionString ConnectionString { get; } public object TransactionLock { get; } = new object(); public Entry(SQLiteConnectionString connectionString) { ConnectionString = connectionString; Connection = new SQLiteConnectionWithLock(ConnectionString); // If the database is FullMutex, then we don't need to bother locking if (ConnectionString.OpenFlags.HasFlag(SQLiteOpenFlags.FullMutex)) { Connection.SkipLock = true; } } public void Close() { var wc = Connection; Connection = null; if (wc != null) { wc.Close(); } } } readonly Dictionary _entries = new Dictionary(); readonly object _entriesLock = new object(); static readonly SQLiteConnectionPool _shared = new SQLiteConnectionPool(); /// /// Gets the singleton instance of the connection tool. /// public static SQLiteConnectionPool Shared { get { return _shared; } } public SQLiteConnectionWithLock GetConnection(SQLiteConnectionString connectionString) { return GetConnectionAndTransactionLock(connectionString, out var _); } public SQLiteConnectionWithLock GetConnectionAndTransactionLock(SQLiteConnectionString connectionString, out object transactionLock) { var key = connectionString.UniqueKey; Entry entry; lock (_entriesLock) { if (!_entries.TryGetValue(key, out entry)) { // The opens the database while we're locked // This is to ensure another thread doesn't get an unopened database entry = new Entry(connectionString); _entries[key] = entry; } transactionLock = entry.TransactionLock; return entry.Connection; } } public void CloseConnection(SQLiteConnectionString connectionString) { var key = connectionString.UniqueKey; Entry entry; lock (_entriesLock) { if (_entries.TryGetValue(key, out entry)) { _entries.Remove(key); } } entry?.Close(); } /// /// Closes all connections managed by this pool. /// public void Reset() { List entries; lock (_entriesLock) { entries = new List(_entries.Values); _entries.Clear(); } foreach (var e in entries) { e.Close(); } } } #endif } \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/SQLiteAsyncUnitask.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/SQLiteAsyncUnitask.cs.meta new file mode 100644 index 0000000..81664e1 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/SQLiteAsyncUnitask.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2700c4f7c8f94c36b1ea2bed408c687d +timeCreated: 1644952632 \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests.meta new file mode 100644 index 0000000..f93231b --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3b4720d7f80a4fb9bd6ce8b7f2aaa62d +timeCreated: 1644966648 \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests/AsyncTests.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests/AsyncTests.cs new file mode 100644 index 0000000..b5fc338 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests/AsyncTests.cs @@ -0,0 +1,862 @@ +namespace SqlCipher4Unity3D.UniTaskIntegration.Tests +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using Cysharp.Threading.Tasks; + using NUnit.Framework; + using SqlCipher4Unity3D; + using SQLite.Attributes; + using UnityEngine.TestTools; + + [Serializable] + public class Customer + { + [AutoIncrement, PrimaryKey] + public int Id { get; set; } + + [MaxLength (64)] + public string FirstName { get; set; } + + [MaxLength (64)] + public string LastName { get; set; } + + [MaxLength (64), Indexed] + public string Email { get; set; } + } + + /// + /// Defines tests that exercise async behaviour. + /// + [TestFixture] + public class AsyncTests + { + private const string DatabaseName = "async.db"; + + [UnityTest] + public IEnumerator EnableWalAsync () => UniTask.ToCoroutine(async () => + { + var path = TestPath.GetTempFileName (); + var connection = new SQLiteAsyncConnection (path); + + await connection.EnableWriteAheadLoggingAsync (); + }); + + [UnityTest] + public IEnumerator QueryAsync () => UniTask.ToCoroutine(async () => + { + var connection = GetConnection (); + await connection.CreateTableAsync (); + + var customer = new Customer { + FirstName = "Joe" + }; + + await connection.InsertAsync (customer); + + await connection.QueryAsync ("select * from Customer"); + }); + + [UnityTest] + public IEnumerator MemoryQueryAsync () => UniTask.ToCoroutine(async () => + { + var connection = new SQLiteAsyncConnection (":memory:", false); + await connection.CreateTableAsync (); + + var customer = new Customer { + FirstName = "Joe" + }; + + await connection.InsertAsync (customer); + + await connection.QueryAsync ("select * from Customer"); + }); + + [UnityTest] + public IEnumerator StressAsync () => UniTask.ToCoroutine(async () => + { + string path = null; + var globalConn = GetConnection (ref path); + + await globalConn.CreateTableAsync (); + + var n = 100; + var errors = new List (); + for (var i = 0; i < n; i++) { + var ii = i; + try { + var conn = GetConnection (); + var obj = new Customer { + FirstName = ii.ToString (), + }; + await conn.InsertAsync (obj); + if (obj.Id == 0) { + lock (errors) { + errors.Add ("Bad Id"); + } + } + var obj2 = (await (from c in conn.Table () where c.Id == obj.Id select c).ToListAsync()).FirstOrDefault(); + if (obj2 == null) { + lock (errors) { + errors.Add ("Failed query"); + } + } + } + catch (Exception ex) { + lock (errors) { + errors.Add (ex.Message); + } + } + + } + + var count = await globalConn.Table().CountAsync(); + + foreach (var e in errors) { + Console.WriteLine ("ERROR " + e); + } + + Assert.AreEqual (0, errors.Count); + Assert.AreEqual (n, count); + }); + + [UnityTest] + public IEnumerator TestCreateTableAsync () => UniTask.ToCoroutine(async () => + { + string path = null; + var conn = GetConnection (ref path); + + // drop the customer table... + await conn.ExecuteAsync("drop table if exists Customer"); + + // run... + await conn.CreateTableAsync(); + + // check... + using (SQLiteConnection check = new SQLiteConnection (new SQLiteConnectionString(path))) { + // run it - if it's missing we'll get a failure... + check.Execute ("select * from Customer"); + } + }); + + SQLiteAsyncConnection GetConnection () + { + string path = null; + return GetConnection (ref path); + } + + string _path; + string _connectionString; + + [SetUp] + public void SetUp() + { + _connectionString = TestPath.GetTempFileName(); + _path = _connectionString; + System.IO.File.Delete (_path); + } + + SQLiteAsyncConnection GetConnection (ref string path) + { + path = _path; + return new SQLiteAsyncConnection (_connectionString); + } + + [UnityTest] + public IEnumerator TestDropTableAsync () => UniTask.ToCoroutine(async () => + { + string path = null; + var conn = GetConnection (ref path); + await conn.CreateTableAsync (); + + // drop it... + await conn.DropTableAsync (); + + // check... + using (SQLiteConnection check = new SQLiteConnection (new SQLiteConnectionString(path))) { + // load it back and check - should be missing + var command = check.CreateCommand ("select name from sqlite_master where type='table' and name='customer'"); + Assert.IsNull (command.ExecuteScalar ()); + } + }); + + private Customer CreateCustomer () + { + Customer customer = new Customer () { + FirstName = "foo", + LastName = "bar", + Email = Guid.NewGuid ().ToString () + }; + return customer; + } + + [UnityTest] + public IEnumerator TestInsertAsync () => UniTask.ToCoroutine(async () => + { + // create... + Customer customer = this.CreateCustomer (); + + // connect... + string path = null; + var conn = GetConnection (ref path); + await conn.CreateTableAsync (); + + // run... + await conn.InsertAsync (customer); + + // check that we got an id... + Assert.AreNotEqual (0, customer.Id); + + // check... + using (SQLiteConnection check = new SQLiteConnection (new SQLiteConnectionString(path))) { + // load it back... + Customer loaded = check.Get (customer.Id); + Assert.AreEqual (loaded.Id, customer.Id); + } + }); + + [UnityTest] + public IEnumerator TestUpdateAsync () => UniTask.ToCoroutine(async () => + { + // create... + Customer customer = CreateCustomer (); + + // connect... + string path = null; + var conn = GetConnection (ref path); + await conn.CreateTableAsync (); + + // run... + await conn.InsertAsync (customer); + + // change it... + string newEmail = Guid.NewGuid ().ToString (); + customer.Email = newEmail; + + // save it... + await conn.UpdateAsync (customer); + + // check... + using (SQLiteConnection check = new SQLiteConnection (new SQLiteConnectionString(path))) { + // load it back - should be changed... + Customer loaded = check.Get (customer.Id); + Assert.AreEqual (newEmail, loaded.Email); + } + }); + + [UnityTest] + public IEnumerator TestDeleteAsync () => UniTask.ToCoroutine(async () => + { + // create... + Customer customer = CreateCustomer (); + + // connect... + string path = null; + var conn = GetConnection (ref path); + await conn.CreateTableAsync (); + + // run... + await conn.InsertAsync (customer); + + // delete it... + await conn.DeleteAsync (customer); + + // check... + using (SQLiteConnection check = new SQLiteConnection (new SQLiteConnectionString(path))) { + // load it back - should be null... + var loaded = check.Table ().Where (v => v.Id == customer.Id).ToList (); + Assert.AreEqual (0, loaded.Count); + } + }); + + [UnityTest] + public IEnumerator GetAsync () => UniTask.ToCoroutine(async () => + { + // create... + Customer customer = new Customer (); + customer.FirstName = "foo"; + customer.LastName = "bar"; + customer.Email = Guid.NewGuid ().ToString (); + + // connect and insert... + string path = null; + var conn = GetConnection (ref path); + await conn.CreateTableAsync (); + await conn.InsertAsync (customer); + + // check... + Assert.AreNotEqual (0, customer.Id); + + // get it back... + var task = conn.GetAsync (customer.Id); + + Customer loaded = await task; + + // check... + Assert.AreEqual (customer.Id, loaded.Id); + }); + + [UnityTest] + public IEnumerator FindAsyncWithExpression () => UniTask.ToCoroutine(async () => + { + // create... + Customer customer = new Customer (); + customer.FirstName = "foo"; + customer.LastName = "bar"; + customer.Email = Guid.NewGuid ().ToString (); + + // connect and insert... + string path = null; + var conn = GetConnection (ref path); + await conn.CreateTableAsync (); + await conn.InsertAsync (customer); + + // check... + Assert.AreNotEqual (0, customer.Id); + + // get it back... + var task = conn.FindAsync (x => x.Id == customer.Id); + + Customer loaded = await task; + + // check... + Assert.AreEqual (customer.Id, loaded.Id); + }); + + [UnityTest] + public IEnumerator FindAsyncWithExpressionNull () => UniTask.ToCoroutine(async () => + { + // connect and insert... + var conn = GetConnection (); + await conn.CreateTableAsync (); + + // get it back... + var task = conn.FindAsync (x => x.Id == 1); + + var loaded = await task; + + // check... + Assert.IsNull (loaded); + }); + + [UnityTest] + public IEnumerator TestFindAsyncItemPresent () => UniTask.ToCoroutine(async () => + { + // create... + Customer customer = CreateCustomer (); + + // connect and insert... + string path = null; + var conn = GetConnection (ref path); + await conn.CreateTableAsync (); + await conn.InsertAsync (customer); + + // check... + Assert.AreNotEqual (0, customer.Id); + + // get it back... + var task = conn.FindAsync(customer.Id); + Customer loaded = await task; + + // check... + Assert.AreEqual (customer.Id, loaded.Id); + }); + + [UnityTest] + public IEnumerator TestFindAsyncItemMissing () => UniTask.ToCoroutine(async () => + { + // connect and insert... + var conn = GetConnection (); + await conn.CreateTableAsync (); + + // now get one that doesn't exist... + var task = conn.FindAsync (-1); + + // check... + Assert.IsNull (await task); + }); + + [UnityTest] + public IEnumerator TestQueryAsync () => UniTask.ToCoroutine(async () => + { + // connect... + var conn = GetConnection (); + await conn.CreateTableAsync (); + + // insert some... + List customers = new List (); + for (int index = 0; index < 5; index++) { + Customer customer = CreateCustomer (); + + // insert... + await conn.InsertAsync (customer); + + // add... + customers.Add (customer); + } + + // return the third one... + var task = conn.QueryAsync ("select * from customer where id=?", customers[2].Id); + var loaded = await task; + + // check... + Assert.AreEqual (1, loaded.Count); + Assert.AreEqual (customers[2].Email, loaded[0].Email); + }); + + [UnityTest] + public IEnumerator TestTableAsync () => UniTask.ToCoroutine(async () => + { + // connect... + var conn = GetConnection (); + await conn.CreateTableAsync (); + await conn.ExecuteAsync ("delete from customer"); + + // insert some... + List customers = new List (); + for (int index = 0; index < 5; index++) { + Customer customer = new Customer (); + customer.FirstName = "foo"; + customer.LastName = "bar"; + customer.Email = Guid.NewGuid ().ToString (); + + // insert... + await conn.InsertAsync (customer); + + // add... + customers.Add (customer); + } + + // run the table operation... + var query = conn.Table (); + var loaded = await query.ToListAsync (); + + // check that we got them all back... + Assert.AreEqual (5, loaded.Count); + Assert.IsNotNull (loaded.Where (v => v.Id == customers[0].Id)); + Assert.IsNotNull (loaded.Where (v => v.Id == customers[1].Id)); + Assert.IsNotNull (loaded.Where (v => v.Id == customers[2].Id)); + Assert.IsNotNull (loaded.Where (v => v.Id == customers[3].Id)); + Assert.IsNotNull (loaded.Where (v => v.Id == customers[4].Id)); + }); + + [UnityTest] + public IEnumerator TestExecuteAsync () => UniTask.ToCoroutine(async () => + { + // connect... + string path = null; + var conn = GetConnection (ref path); + await conn.CreateTableAsync (); + + // do a manual insert... + string email = Guid.NewGuid ().ToString (); + await conn.ExecuteAsync ($"insert into customer (firstname, lastname, email) values (?, ?, ?)", + "foo", "bar", email); + + // check... + using (SQLiteConnection check = new SQLiteConnection (new SQLiteConnectionString(path))) { + // load it back - should be null... + var result = check.Table ().Where (v => v.Email == email); + Assert.IsNotNull (result); + } + }); + + [UnityTest] + public IEnumerator TestInsertAllAsync () => UniTask.ToCoroutine(async () => + { + // create a bunch of customers... + List customers = new List (); + for (int index = 0; index < 100; index++) { + Customer customer = new Customer (); + customer.FirstName = "foo"; + customer.LastName = "bar"; + customer.Email = Guid.NewGuid ().ToString (); + customers.Add (customer); + } + + // connect... + string path = null; + var conn = GetConnection (ref path); + await conn.CreateTableAsync (); + + // insert them all... + await conn.InsertAllAsync (customers); + + // check... + using (SQLiteConnection check = new SQLiteConnection (new SQLiteConnectionString(path))) { + for (int index = 0; index < customers.Count; index++) { + // load it back and check... + Customer loaded = check.Get (customers[index].Id); + Assert.AreEqual (loaded.Email, customers[index].Email); + } + } + }); + + [UnityTest] + public IEnumerator TestRunInTransactionAsync () => UniTask.ToCoroutine(async () => + { + // connect... + string path = null; + var conn = GetConnection (ref path); + await conn.CreateTableAsync (); + bool transactionCompleted = false; + + // run... + Customer customer = new Customer (); + await conn.RunInTransactionAsync((c) => + { + // insert... + customer.FirstName = "foo"; + customer.LastName = "bar"; + customer.Email = Guid.NewGuid().ToString(); + c.Insert(customer); + + // delete it again... + c.Execute("delete from customer where id=?", customer.Id); + + // set completion flag + transactionCompleted = true; + }); + + // check... + Assert.IsTrue(transactionCompleted); + using (SQLiteConnection check = new SQLiteConnection (new SQLiteConnectionString(path))) { + // load it back and check - should be deleted... + var loaded = check.Table ().Where (v => v.Id == customer.Id).ToList (); + Assert.AreEqual (0, loaded.Count); + } + }); + + [UnityTest] + public IEnumerator TestExecuteScalar () => UniTask.ToCoroutine(async () => + { + // connect... + var conn = GetConnection (); + await conn.CreateTableAsync (); + + // check... + var task = conn.ExecuteScalarAsync ("select name from sqlite_master where type='table' and name='customer'"); + object name = await task; + Assert.AreNotEqual ("Customer", name); + }); + + [UnityTest] + public IEnumerator TestAsyncTableQueryToListAsync () => UniTask.ToCoroutine(async () => + { + var conn = GetConnection (); + await conn.CreateTableAsync (); + + // create... + Customer customer = this.CreateCustomer (); + await conn.InsertAsync (customer); + + // query... + var query = conn.Table (); + var task = query.ToListAsync (); + var items = await task; + + // check... + var loaded = items.Where (v => v.Id == customer.Id).First (); + Assert.AreEqual (customer.Email, loaded.Email); + }); + + [UnityTest] + public IEnumerator TestAsyncTableQueryToFirstAsyncFound () => UniTask.ToCoroutine(async () => + { + var conn = GetConnection (); + await conn.CreateTableAsync(); + + // create... + Customer customer = this.CreateCustomer(); + await conn.InsertAsync(customer); + + // query... + var query = conn.Table ().Where(v => v.Id == customer.Id); + var task = query.FirstAsync (); + + var loaded = await task; + + // check... + Assert.AreEqual(customer.Email, loaded.Email); + }); + + [UnityTest] + public IEnumerator TestAsyncTableQueryToFirstAsyncMissing () => UniTask.ToCoroutine(async () => + { + var conn = GetConnection(); + await conn.CreateTableAsync(); + + // create... + Customer customer = this.CreateCustomer(); + await conn.InsertAsync(customer); + + // query... + var query = conn.Table().Where(v => v.Id == -1); + try + { + var task = await query.FirstAsync(); + + //if Exception not occurs assert + Assert.Fail("InvalidOperationException Exception expected"); + } + catch (Exception e) + { + Assert.IsTrue(e is InvalidOperationException); + } + // can't use ExceptionAssert with async function + //ExceptionAssert.Throws(async () => await query.FirstAsync()); + }); + + [UnityTest] + public IEnumerator TestAsyncTableQueryToFirstOrDefaultAsyncFound () => UniTask.ToCoroutine(async () => + { + var conn = GetConnection(); + await conn.CreateTableAsync(); + + // create... + Customer customer = this.CreateCustomer(); + await conn.InsertAsync(customer); + + // query... + var query = conn.Table().Where(v => v.Id == customer.Id); + var task = query.FirstOrDefaultAsync(); + var loaded = await task; + + // check... + Assert.AreEqual(customer.Email, loaded.Email); + }); + + [UnityTest] + public IEnumerator TestAsyncTableQueryToFirstOrDefaultAsyncMissing () => UniTask.ToCoroutine(async () => + { + var conn = GetConnection(); + await conn.CreateTableAsync(); + + // create... + Customer customer = this.CreateCustomer(); + await conn.InsertAsync(customer); + + // query... + var query = conn.Table().Where(v => v.Id == -1); + var task = query.FirstOrDefaultAsync(); + var loaded = await task; + + // check... + Assert.IsNull(loaded); + }); + + [UnityTest] + public IEnumerator TestAsyncTableQueryWhereOperation () => UniTask.ToCoroutine(async () => + { + var conn = GetConnection (); + await conn.CreateTableAsync(); + + // create... + Customer customer = this.CreateCustomer (); + await conn.InsertAsync(customer); + + // query... + var query = conn.Table (); + var task = query.ToListAsync (); + var items = await task; + + // check... + var loaded = items.Where (v => v.Id == customer.Id).First (); + Assert.AreEqual (customer.Email, loaded.Email); + }); + + [UnityTest] + public IEnumerator TestAsyncTableQueryCountAsync () => UniTask.ToCoroutine(async () => + { + var conn = GetConnection (); + await conn.CreateTableAsync(); + await conn.ExecuteAsync("delete from customer"); + + // create... + for (int index = 0; index < 10; index++) + await conn.InsertAsync(this.CreateCustomer()); + + // load... + var query = conn.Table (); + var task = query.CountAsync (); + + // check... + Assert.AreEqual (10, await task); + }); + + [UnityTest] + public IEnumerator TestAsyncTableOrderBy () => UniTask.ToCoroutine(async () => + { + var conn = GetConnection (); + await conn.CreateTableAsync(); + await conn.ExecuteAsync("delete from customer"); + + // create... + for (int index = 0; index < 10; index++) + await conn.InsertAsync(this.CreateCustomer()); + + // query... + var query = conn.Table ().OrderBy (v => v.Email); + var task = query.ToListAsync (); + var items = await task; + + // check... + Assert.AreEqual (-1, string.Compare (items[0].Email, items[9].Email)); + }); + + [UnityTest] + public IEnumerator TestAsyncTableOrderByDescending () => UniTask.ToCoroutine(async () => + { + var conn = GetConnection (); + await conn.CreateTableAsync(); + await conn.ExecuteAsync("delete from customer"); + + // create... + for (int index = 0; index < 10; index++) + await conn.InsertAsync(this.CreateCustomer()); + + // query... + var query = conn.Table ().OrderByDescending (v => v.Email); + var task = query.ToListAsync (); + var items = await task; + + // check... + Assert.AreEqual (1, string.Compare (items[0].Email, items[9].Email)); + }); + + [UnityTest] + public IEnumerator TestAsyncTableQueryTake () => UniTask.ToCoroutine(async () => + { + var conn = GetConnection (); + await conn.CreateTableAsync(); + await conn.ExecuteAsync("delete from customer"); + + // create... + for (int index = 0; index < 10; index++) { + var customer = this.CreateCustomer (); + customer.FirstName = index.ToString (); + await conn.InsertAsync(customer); + } + + // query... + var query = conn.Table ().OrderBy (v => v.FirstName).Take (1); + var task = query.ToListAsync (); + var items = await task; + + // check... + Assert.AreEqual (1, items.Count); + Assert.AreEqual ("0", items[0].FirstName); + }); + + [UnityTest] + public IEnumerator TestAsyncTableQuerySkip () => UniTask.ToCoroutine(async () => + { + var conn = GetConnection (); + await conn.CreateTableAsync(); + await conn.ExecuteAsync("delete from customer"); + + // create... + for (int index = 0; index < 10; index++) { + var customer = this.CreateCustomer (); + customer.FirstName = index.ToString (); + await conn.InsertAsync(customer); + } + + // query... + var query = conn.Table ().OrderBy (v => v.FirstName).Skip (5); + var task = query.ToListAsync (); + var items = await task; + + // check... + Assert.AreEqual (5, items.Count); + Assert.AreEqual ("5", items[0].FirstName); + }); + + [UnityTest] + public IEnumerator TestAsyncTableElementAtAsync () => UniTask.ToCoroutine(async () => + { + var conn = GetConnection (); + await conn.CreateTableAsync(); + await conn.ExecuteAsync("delete from customer"); + + // create... + for (int index = 0; index < 10; index++) { + var customer = this.CreateCustomer (); + customer.FirstName = index.ToString (); + await conn.InsertAsync(customer); + } + + // query... + var query = conn.Table ().OrderBy (v => v.FirstName); + var task = query.ElementAtAsync (7); + + var loaded = await task; + + // check... + Assert.AreEqual ("7", loaded.FirstName); + }); + + + [UnityTest] + public IEnumerator TestAsyncGetWithExpression() => UniTask.ToCoroutine(async () => + { + var conn = GetConnection(); + await conn.CreateTableAsync(); + await conn.ExecuteAsync("delete from customer"); + + // create... + for (int index = 0; index < 10; index++) + { + var customer = this.CreateCustomer(); + customer.FirstName = index.ToString(); + await conn.InsertAsync(customer); + } + + // get... + var result = conn.GetAsync(x => x.FirstName == "7"); + + var loaded = await result; + // check... + Assert.AreEqual("7", loaded.FirstName); + }); + + [UnityTest] + public IEnumerator CreateTable () => UniTask.ToCoroutine(async () => + { + var conn = GetConnection (); + + var trace = new List (); + conn.Tracer = trace.Add; + conn.Trace = true; + + var r0 = await conn.CreateTableAsync(); + + Assert.AreEqual (CreateTableResult.Created, r0); + + var r1 = await conn.CreateTableAsync (); + + Assert.AreEqual (CreateTableResult.Migrated, r1); + + var r2 = await conn.CreateTableAsync (); + + Assert.AreEqual (CreateTableResult.Migrated, r1); + + Assert.AreEqual (7, trace.Count); + }); + + [UnityTest] + public IEnumerator CloseAsync () => UniTask.ToCoroutine(async () => + { + var conn = GetConnection (); + + var r0 = await conn.CreateTableAsync (); + + Assert.AreEqual (CreateTableResult.Created, r0); + + await conn.CloseAsync(); + }); + + + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests/AsyncTests.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests/AsyncTests.cs.meta new file mode 100644 index 0000000..79e544d --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests/AsyncTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e59e109891fe4a8e82dcbf1cb6d0874d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests/Database Tests.asmdef b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests/Database Tests.asmdef new file mode 100644 index 0000000..95784e1 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests/Database Tests.asmdef @@ -0,0 +1,25 @@ +{ + "name": "UniTaskIntegrationTests", + "rootNamespace": "", + "references": [ + "UnityEngine.TestRunner", + "UnityEditor.TestRunner", + "UniTask", + "SqlCipher4Unity3D", + "UniTaskIntegration" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll", + "Newtonsoft.Json.dll" + ], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests/Database Tests.asmdef.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests/Database Tests.asmdef.meta new file mode 100644 index 0000000..f4aae98 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests/Database Tests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3030cfec1a5e4d6984b417db1c1f5735 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests/TestDb.cs b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests/TestDb.cs new file mode 100644 index 0000000..3689ba0 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests/TestDb.cs @@ -0,0 +1,87 @@ +namespace SqlCipher4Unity3D.UniTaskIntegration.Tests +{ + using System; + using System.IO; + using SqlCipher4Unity3D; + using SQLite.Attributes; + using UnityEngine; + + public class Product + { + [AutoIncrement, PrimaryKey] + public int Id { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + + public uint TotalSales { get; set; } + } + + public class Order + { + [AutoIncrement, PrimaryKey] + public int Id { get; set; } + public DateTime PlacedTime { get; set; } + } + + public class OrderHistory + { + [AutoIncrement, PrimaryKey] + public int Id { get; set; } + public int OrderId { get; set; } + public DateTime Time { get; set; } + public string Comment { get; set; } + } + + public class OrderLine + { + [AutoIncrement, PrimaryKey] + public int Id { get; set; } + [Indexed("IX_OrderProduct", 1)] + public int OrderId { get; set; } + [Indexed("IX_OrderProduct", 2)] + public int ProductId { get; set; } + public int Quantity { get; set; } + public decimal UnitPrice { get; set; } + public OrderLineStatus Status { get; set; } + } + + public enum OrderLineStatus + { + Placed = 1, + Shipped = 100 + } + + public class TestDb : SQLiteConnection + { + public TestDb (bool storeDateTimeAsTicks = true, object key = null, bool wal = true) : base (new SQLiteConnectionString (TestPath.GetTempFileName (), storeDateTimeAsTicks, key: key)) + { + Trace = true; + if (wal) + EnableWriteAheadLogging (); + } + + public TestDb (SQLiteConnectionString connectionString, bool wal = true) : base (connectionString) + { + Trace = true; + if (wal) + EnableWriteAheadLogging (); + } + + public TestDb (string path, bool storeDateTimeAsTicks = true, object key = null, bool wal = true) : base (new SQLiteConnectionString (path, storeDateTimeAsTicks, key: key)) + { + Trace = true; + if (wal) + EnableWriteAheadLogging (); + } + + + } + + public class TestPath + { + public static string GetTempFileName () + { + return Path.Combine(Application.persistentDataPath, $"{Guid.NewGuid()}.sqlite"); + } + } +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests/TestDb.cs.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests/TestDb.cs.meta new file mode 100644 index 0000000..4c4ce22 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/Tests/TestDb.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0575da99e10f4c469cde0aa3dbb54108 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/UniTaskIntegration.asmdef b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/UniTaskIntegration.asmdef new file mode 100644 index 0000000..8fcfd16 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/UniTaskIntegration.asmdef @@ -0,0 +1,22 @@ +{ + "name": "UniTaskIntegration", + "rootNamespace": "", + "references": [ + "GUID:5dcaa7c28ba9de042a11ef79db487161", + "GUID:f51ebe6a0ceec4240a699833d6309b23", + "GUID:5c01796d064528144a599661eaab93a6" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "Newtonsoft.Json.dll" + ], + "autoReferenced": true, + "defineConstraints": [ + "SQLITEASYNC_UNITASK" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/UniTaskIntegration.asmdef.meta b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/UniTaskIntegration.asmdef.meta new file mode 100644 index 0000000..c561d12 --- /dev/null +++ b/SqlCipher4Unity3D/Assets/SqlCipher4Unity3D/UniTaskIntegration/UniTaskIntegration.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7de2a96fe63894fbdbe68ce953e46662 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SqlCipher4Unity3D/Assets/example.async/DataServiceAsync.cs b/SqlCipher4Unity3D/Assets/example.async/DataServiceAsync.cs index 83e0ccb..73681b0 100644 --- a/SqlCipher4Unity3D/Assets/example.async/DataServiceAsync.cs +++ b/SqlCipher4Unity3D/Assets/example.async/DataServiceAsync.cs @@ -8,6 +8,11 @@ #endif namespace example.async { + using Cysharp.Threading.Tasks; + #if SQLITEASYNC_UNITASK + using SqlCipher4Unity3D.UniTaskIntegration; + + #endif public class DataServiceAsync { private readonly SQLiteAsyncConnection _connection; @@ -69,7 +74,12 @@ public DataServiceAsync(string DatabaseName) } ~DataServiceAsync() { + #if SQLITEASYNC_UNITASK + _connection.CloseAsync(); + #else _connection.CloseAsync().Wait(); + #endif + } public async Task CreateDB() @@ -119,12 +129,20 @@ public AsyncTableQuery GetPersonsNamedRoberto() { return _connection.Table().Where(x => x.Name == "Roberto"); } + + #if SQLITEASYNC_UNITASK + public UniTask GetJohnny() + { + return _connection.Table().Where(person => person.Name == "Johnny").FirstOrDefaultAsync(); + } + #else public Task GetJohnny() { return _connection.Table().Where(x => x.Name == "Johnny").FirstOrDefaultAsync(); } + #endif public async Task CreatePerson() { Person p = new Person diff --git a/SqlCipher4Unity3D/Assets/test/test_update_async/test_update_async.cs b/SqlCipher4Unity3D/Assets/test/test_update_async/test_update_async.cs index 603ca39..10f7cd1 100644 --- a/SqlCipher4Unity3D/Assets/test/test_update_async/test_update_async.cs +++ b/SqlCipher4Unity3D/Assets/test/test_update_async/test_update_async.cs @@ -9,6 +9,10 @@ namespace test.test_update_async { + #if SQLITEASYNC_UNITASK + using SqlCipher4Unity3D.UniTaskIntegration; + #endif + [SQLite.Attributes.Preserve] public class player_profile { diff --git a/SqlCipher4Unity3D/Packages/manifest.json b/SqlCipher4Unity3D/Packages/manifest.json index 7e5db1a..2b8636b 100644 --- a/SqlCipher4Unity3D/Packages/manifest.json +++ b/SqlCipher4Unity3D/Packages/manifest.json @@ -1,9 +1,11 @@ { "dependencies": { - "com.unity.ide.rider": "2.0.7", - "com.unity.ide.visualstudio": "2.0.12", - "com.unity.ide.vscode": "1.2.4", + "com.cysharp.unitask": "2.3.1", + "com.unity.ide.rider": "3.0.12", + "com.unity.ide.visualstudio": "2.0.14", + "com.unity.ide.vscode": "1.2.5", "com.unity.mobile.android-logcat": "1.2.3", + "com.unity.nuget.newtonsoft-json": "2.0.2", "com.unity.test-framework": "1.1.30", "com.unity.textmeshpro": "3.0.6", "com.unity.ugui": "1.0.0", @@ -35,5 +37,14 @@ "com.unity.modules.video": "1.0.0", "com.unity.modules.vr": "1.0.0", "com.unity.modules.xr": "1.0.0" - } + }, + "scopedRegistries": [ + { + "name": "OpenUpm", + "url": "https://package.openupm.com", + "scopes": [ + "com.cysharp" + ] + } + ] } diff --git a/SqlCipher4Unity3D/Packages/packages-lock.json b/SqlCipher4Unity3D/Packages/packages-lock.json index 6e8b3b6..b1554be 100644 --- a/SqlCipher4Unity3D/Packages/packages-lock.json +++ b/SqlCipher4Unity3D/Packages/packages-lock.json @@ -1,5 +1,12 @@ { "dependencies": { + "com.cysharp.unitask": { + "version": "2.3.1", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://package.openupm.com" + }, "com.unity.ext.nunit": { "version": "1.0.6", "depth": 1, @@ -8,16 +15,16 @@ "url": "https://packages.unity.com" }, "com.unity.ide.rider": { - "version": "2.0.7", + "version": "3.0.12", "depth": 0, "source": "registry", "dependencies": { - "com.unity.test-framework": "1.1.1" + "com.unity.ext.nunit": "1.0.6" }, "url": "https://packages.unity.com" }, "com.unity.ide.visualstudio": { - "version": "2.0.12", + "version": "2.0.14", "depth": 0, "source": "registry", "dependencies": { @@ -26,7 +33,7 @@ "url": "https://packages.unity.com" }, "com.unity.ide.vscode": { - "version": "1.2.4", + "version": "1.2.5", "depth": 0, "source": "registry", "dependencies": {}, @@ -39,6 +46,13 @@ "dependencies": {}, "url": "https://packages.unity.com" }, + "com.unity.nuget.newtonsoft-json": { + "version": "2.0.2", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, "com.unity.test-framework": { "version": "1.1.30", "depth": 0, diff --git a/SqlCipher4Unity3D/ProjectSettings/PackageManagerSettings 2.asset b/SqlCipher4Unity3D/ProjectSettings/PackageManagerSettings 2.asset new file mode 100644 index 0000000..be4a797 --- /dev/null +++ b/SqlCipher4Unity3D/ProjectSettings/PackageManagerSettings 2.asset @@ -0,0 +1,43 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &1 +MonoBehaviour: + m_ObjectHideFlags: 61 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 13964, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_EnablePreviewPackages: 0 + m_EnablePackageDependencies: 0 + m_AdvancedSettingsExpanded: 1 + m_ScopedRegistriesSettingsExpanded: 1 + oneTimeWarningShown: 0 + m_Registries: + - m_Id: main + m_Name: + m_Url: https://packages.unity.com + m_Scopes: [] + m_IsDefault: 1 + m_Capabilities: 7 + m_UserSelectedRegistryName: + m_UserAddingNewScopedRegistry: 0 + m_RegistryInfoDraft: + m_ErrorMessage: + m_Original: + m_Id: + m_Name: + m_Url: + m_Scopes: [] + m_IsDefault: 0 + m_Capabilities: 0 + m_Modified: 0 + m_Name: + m_Url: + m_Scopes: + - + m_SelectedScopeIndex: 0 diff --git a/SqlCipher4Unity3D/ProjectSettings/PackageManagerSettings.asset b/SqlCipher4Unity3D/ProjectSettings/PackageManagerSettings.asset index be4a797..d24ffac 100644 --- a/SqlCipher4Unity3D/ProjectSettings/PackageManagerSettings.asset +++ b/SqlCipher4Unity3D/ProjectSettings/PackageManagerSettings.asset @@ -12,10 +12,11 @@ MonoBehaviour: m_Script: {fileID: 13964, guid: 0000000000000000e000000000000000, type: 0} m_Name: m_EditorClassIdentifier: - m_EnablePreviewPackages: 0 + m_EnablePreReleasePackages: 0 m_EnablePackageDependencies: 0 m_AdvancedSettingsExpanded: 1 m_ScopedRegistriesSettingsExpanded: 1 + m_SeeAllPackageVersions: 0 oneTimeWarningShown: 0 m_Registries: - m_Id: main @@ -24,20 +25,18 @@ MonoBehaviour: m_Scopes: [] m_IsDefault: 1 m_Capabilities: 7 - m_UserSelectedRegistryName: + - m_Id: scoped:OpenUpm + m_Name: OpenUpm + m_Url: https://package.openupm.com + m_Scopes: + - com.cysharp + m_IsDefault: 0 + m_Capabilities: 0 + m_UserSelectedRegistryName: OpenUpm m_UserAddingNewScopedRegistry: 0 m_RegistryInfoDraft: - m_ErrorMessage: - m_Original: - m_Id: - m_Name: - m_Url: - m_Scopes: [] - m_IsDefault: 0 - m_Capabilities: 0 m_Modified: 0 - m_Name: - m_Url: - m_Scopes: - - - m_SelectedScopeIndex: 0 + m_ErrorMessage: + m_UserModificationsInstanceId: -826 + m_OriginalInstanceId: -830 + m_LoadAssets: 0 diff --git a/SqlCipher4Unity3D/ProjectSettings/ProjectSettings.asset b/SqlCipher4Unity3D/ProjectSettings/ProjectSettings.asset index 3aba730..9d606a3 100644 --- a/SqlCipher4Unity3D/ProjectSettings/ProjectSettings.asset +++ b/SqlCipher4Unity3D/ProjectSettings/ProjectSettings.asset @@ -3,7 +3,7 @@ --- !u!129 &1 PlayerSettings: m_ObjectHideFlags: 0 - serializedVersion: 22 + serializedVersion: 23 productGUID: 48e513c1f0b13404eb309ddd9ba471bd AndroidProfiler: 0 AndroidFilterTouchesWhenObscured: 0 @@ -217,6 +217,7 @@ PlayerSettings: iOSLaunchScreeniPadCustomStoryboardPath: iOSDeviceRequirements: [] iOSURLSchemes: [] + macOSURLSchemes: [] iOSBackgroundModes: 0 iOSMetalForceHardShadows: 0 metalEditorSupport: 1 @@ -495,6 +496,9 @@ PlayerSettings: - m_BuildTarget: iOSSupport m_APIs: 10000000 m_Automatic: 1 + - m_BuildTarget: AndroidPlayer + m_APIs: 0b00000008000000 + m_Automatic: 0 m_BuildTargetVRSettings: [] openGLRequireES31: 0 openGLRequireES31AEP: 0 @@ -507,6 +511,7 @@ PlayerSettings: m_BuildTargetGroupLightmapEncodingQuality: [] m_BuildTargetGroupLightmapSettings: [] m_BuildTargetNormalMapEncoding: [] + m_BuildTargetDefaultTextureCompressionFormat: [] playModeTestRunnerEnabled: 0 runPlayModeTestAsEditModeTest: 0 actionOnDotNetUnhandledException: 1 @@ -525,6 +530,7 @@ PlayerSettings: switchScreenResolutionBehavior: 2 switchUseCPUProfiler: 0 switchUseGOLDLinker: 0 + switchLTOSetting: 0 switchApplicationID: 0x01004b9000490000 switchNSODependencies: switchTitleNames_0: @@ -655,7 +661,9 @@ PlayerSettings: switchPlayerConnectionEnabled: 1 switchUseNewStyleFilepaths: 0 switchUseMicroSleepForYield: 1 + switchEnableRamDiskSupport: 0 switchMicroSleepForYieldTime: 25 + switchRamDiskSpaceSize: 12 ps4NPAgeRating: 12 ps4NPTitleSecret: ps4NPTrophyPackPath: @@ -726,7 +734,6 @@ PlayerSettings: ps4videoRecordingFeaturesUsed: 0 ps4contentSearchFeaturesUsed: 0 ps4CompatibilityPS5: 0 - ps4AllowPS5Detection: 0 ps4GPU800MHz: 1 ps4attribEyeToEyeDistanceSettingVR: 0 ps4IncludedModules: [] @@ -751,7 +758,8 @@ PlayerSettings: webGLLinkerTarget: 1 webGLThreadsSupport: 0 webGLDecompressionFallback: 0 - scriptingDefineSymbols: {} + scriptingDefineSymbols: + Standalone: SQLITEASYNC_UNITASK additionalCompilerArguments: {} platformArchitecture: {} scriptingBackend: @@ -762,7 +770,6 @@ PlayerSettings: suppressCommonWarnings: 1 allowUnsafeCode: 0 useDeterministicCompilation: 1 - useReferenceAssemblies: 1 enableRoslynAnalyzers: 1 additionalIl2CppArgs: scriptingRuntimeVersion: 1 @@ -850,4 +857,6 @@ PlayerSettings: organizationId: cloudEnabled: 0 legacyClampBlendShapeWeights: 1 + playerDataPath: + forceSRGBBlit: 1 virtualTexturingSupportEnabled: 0 diff --git a/SqlCipher4Unity3D/SqlCipher4Unity3D.unitypackage b/SqlCipher4Unity3D/SqlCipher4Unity3D.unitypackage new file mode 100644 index 0000000..07ee082 Binary files /dev/null and b/SqlCipher4Unity3D/SqlCipher4Unity3D.unitypackage differ diff --git a/SqlCipher4Unity3D/UserSettings/Search.settings b/SqlCipher4Unity3D/UserSettings/Search.settings new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/SqlCipher4Unity3D/UserSettings/Search.settings @@ -0,0 +1 @@ +{} \ No newline at end of file