From bc536776a4390a7916807e2efcfbd063be8e28e6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 13 Apr 2024 23:47:09 -0700 Subject: [PATCH 01/14] Add .NET 8 TFM --- global.json | 3 ++- nuget/SQLite-net-base/SQLite-net-base.csproj | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/global.json b/global.json index 8a1056e1..5c4e7459 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,6 @@ { "sdk": { - "version": "8.0.100" + "version": "8.0.100", + "rollForward": "latestFeature" } } \ No newline at end of file diff --git a/nuget/SQLite-net-base/SQLite-net-base.csproj b/nuget/SQLite-net-base/SQLite-net-base.csproj index 6f04bcb7..a1ffaa22 100644 --- a/nuget/SQLite-net-base/SQLite-net-base.csproj +++ b/nuget/SQLite-net-base/SQLite-net-base.csproj @@ -1,7 +1,7 @@ - netstandard2.0 + netstandard2.0;net8.0 SQLite-net sqlite-net-base SQLite-net .NET Standard Base Library From c8bdd4e64bfb878e3baf84755217f17b9db1bf66 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 13 Apr 2024 23:47:52 -0700 Subject: [PATCH 02/14] Make EnumCacheInfo AOT-safe --- src/SQLite.cs | 9638 +++++++++++++++++++++++++------------------------ 1 file changed, 4822 insertions(+), 4816 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index 1697fe9f..7d3999ed 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -59,4934 +59,4940 @@ namespace SQLite { - public class SQLiteException : Exception - { - public SQLite3.Result Result { get; private set; } - - protected SQLiteException (SQLite3.Result r, string message) : base (message) - { - Result = r; - } - - public static SQLiteException New (SQLite3.Result r, string message) - { - return new SQLiteException (r, message); - } - } - - public class NotNullConstraintViolationException : SQLiteException - { - public IEnumerable Columns { get; protected set; } - - protected NotNullConstraintViolationException (SQLite3.Result r, string message) - : this (r, message, null, null) - { - - } - - protected NotNullConstraintViolationException (SQLite3.Result r, string message, TableMapping mapping, object obj) - : base (r, message) - { - if (mapping != null && obj != null) { - this.Columns = from c in mapping.Columns - where c.IsNullable == false && c.GetValue (obj) == null - select c; - } - } - - public static new NotNullConstraintViolationException New (SQLite3.Result r, string message) - { - return new NotNullConstraintViolationException (r, message); - } - - public static NotNullConstraintViolationException New (SQLite3.Result r, string message, TableMapping mapping, object obj) - { - return new NotNullConstraintViolationException (r, message, mapping, obj); - } - - public static NotNullConstraintViolationException New (SQLiteException exception, TableMapping mapping, object obj) - { - return new NotNullConstraintViolationException (exception.Result, exception.Message, mapping, obj); - } - } - - [Flags] - public enum SQLiteOpenFlags - { - ReadOnly = 1, ReadWrite = 2, Create = 4, - Uri = 0x40, Memory = 0x80, - NoMutex = 0x8000, FullMutex = 0x10000, - SharedCache = 0x20000, PrivateCache = 0x40000, - ProtectionComplete = 0x00100000, - ProtectionCompleteUnlessOpen = 0x00200000, - ProtectionCompleteUntilFirstUserAuthentication = 0x00300000, - ProtectionNone = 0x00400000 - } - - [Flags] - public enum CreateFlags - { - /// - /// Use the default creation options - /// - None = 0x000, - /// - /// Create a primary key index for a property called 'Id' (case-insensitive). - /// This avoids the need for the [PrimaryKey] attribute. - /// - ImplicitPK = 0x001, - /// - /// Create indices for properties ending in 'Id' (case-insensitive). - /// - ImplicitIndex = 0x002, - /// - /// Create a primary key for a property called 'Id' and - /// create an indices for properties ending in 'Id' (case-insensitive). - /// - AllImplicit = 0x003, - /// - /// Force the primary key property to be auto incrementing. - /// This avoids the need for the [AutoIncrement] attribute. - /// The primary key property on the class should have type int or long. - /// - AutoIncPK = 0x004, - /// - /// Create virtual table using FTS3 - /// - FullTextSearch3 = 0x100, - /// - /// Create virtual table using FTS4 - /// - FullTextSearch4 = 0x200 - } - - public interface ISQLiteConnection : IDisposable - { - Sqlite3DatabaseHandle Handle { get; } - string DatabasePath { get; } - int LibVersionNumber { get; } - bool TimeExecution { get; set; } - bool Trace { get; set; } - Action Tracer { get; set; } - bool StoreDateTimeAsTicks { get; } - bool StoreTimeSpanAsTicks { get; } - string DateTimeStringFormat { get; } - TimeSpan BusyTimeout { get; set; } - IEnumerable TableMappings { get; } - bool IsInTransaction { get; } - - event EventHandler TableChanged; - - void Backup (string destinationDatabasePath, string databaseName = "main"); - void BeginTransaction (); - void Close (); - void Commit (); - SQLiteCommand CreateCommand (string cmdText, params object[] ps); - SQLiteCommand CreateCommand (string cmdText, Dictionary args); - int CreateIndex (string indexName, string tableName, string[] columnNames, bool unique = false); - int CreateIndex (string indexName, string tableName, string columnName, bool unique = false); - int CreateIndex (string tableName, string columnName, bool unique = false); - int CreateIndex (string tableName, string[] columnNames, bool unique = false); - int CreateIndex (Expression> property, bool unique = false); - CreateTableResult CreateTable (CreateFlags createFlags = CreateFlags.None); - CreateTableResult CreateTable (Type ty, CreateFlags createFlags = CreateFlags.None); - CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) - where T : new() - where T2 : new(); - CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) - where T : new() - where T2 : new() - where T3 : new(); - CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) - where T : new() - where T2 : new() - where T3 : new() - where T4 : new(); - CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) - where T : new() - where T2 : new() - where T3 : new() - where T4 : new() - where T5 : new(); - CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None, params Type[] types); - IEnumerable DeferredQuery (string query, params object[] args) where T : new(); - IEnumerable DeferredQuery (TableMapping map, string query, params object[] args); - int Delete (object objectToDelete); - int Delete (object primaryKey); - int Delete (object primaryKey, TableMapping map); - int DeleteAll (); - int DeleteAll (TableMapping map); - int DropTable (); - int DropTable (TableMapping map); - void EnableLoadExtension (bool enabled); - void EnableWriteAheadLogging (); - int Execute (string query, params object[] args); - T ExecuteScalar (string query, params object[] args); - T Find (object pk) where T : new(); - object Find (object pk, TableMapping map); - T Find (Expression> predicate) where T : new(); - T FindWithQuery (string query, params object[] args) where T : new(); - object FindWithQuery (TableMapping map, string query, params object[] args); - T Get (object pk) where T : new(); - object Get (object pk, TableMapping map); - T Get (Expression> predicate) where T : new(); - TableMapping GetMapping (Type type, CreateFlags createFlags = CreateFlags.None); - TableMapping GetMapping (CreateFlags createFlags = CreateFlags.None); - List GetTableInfo (string tableName); - int Insert (object obj); - int Insert (object obj, Type objType); - int Insert (object obj, string extra); - int Insert (object obj, string extra, Type objType); - int InsertAll (IEnumerable objects, bool runInTransaction = true); - int InsertAll (IEnumerable objects, string extra, bool runInTransaction = true); - int InsertAll (IEnumerable objects, Type objType, bool runInTransaction = true); - int InsertOrReplace (object obj); - int InsertOrReplace (object obj, Type objType); - List Query (string query, params object[] args) where T : new(); - List Query (TableMapping map, string query, params object[] args); - List QueryScalars (string query, params object[] args); - void ReKey (string key); - void ReKey (byte[] key); - void Release (string savepoint); - void Rollback (); - void RollbackTo (string savepoint); - void RunInTransaction (Action action); - string SaveTransactionPoint (); - TableQuery Table () where T : new(); - int Update (object obj); - int Update (object obj, Type objType); - int UpdateAll (IEnumerable objects, bool runInTransaction = true); - } - - /// - /// An open connection to a SQLite database. - /// - [Preserve (AllMembers = true)] - public partial class SQLiteConnection : ISQLiteConnection - { - private bool _open; - private TimeSpan _busyTimeout; - readonly static Dictionary _mappings = new Dictionary (); - private System.Diagnostics.Stopwatch _sw; - private long _elapsedMilliseconds = 0; - - private int _transactionDepth = 0; - private Random _rand = new Random (); - - public Sqlite3DatabaseHandle Handle { get; private set; } - static readonly Sqlite3DatabaseHandle NullHandle = default (Sqlite3DatabaseHandle); - static readonly Sqlite3BackupHandle NullBackupHandle = default (Sqlite3BackupHandle); - - /// - /// Gets the database path used by this connection. - /// - public string DatabasePath { get; private set; } - - /// - /// Gets the SQLite library version number. 3007014 would be v3.7.14 - /// - public int LibVersionNumber { get; private set; } - - /// - /// Whether Trace lines should be written that show the execution time of queries. - /// - public bool TimeExecution { get; set; } - - /// - /// Whether to write queries to during execution. - /// - public bool Trace { get; set; } - - /// - /// The delegate responsible for writing trace lines. - /// - /// The tracer. - public Action Tracer { get; set; } - - /// - /// Whether to store DateTime properties as ticks (true) or strings (false). - /// - public bool StoreDateTimeAsTicks { get; private set; } - - /// - /// Whether to store TimeSpan properties as ticks (true) or strings (false). - /// - public bool StoreTimeSpanAsTicks { get; private set; } - - /// - /// The format to use when storing DateTime properties as strings. Ignored if StoreDateTimeAsTicks is true. - /// - /// The date time string format. - public string DateTimeStringFormat { get; private set; } - - /// - /// The DateTimeStyles value to use when parsing a DateTime property string. - /// - /// The date time style. - internal System.Globalization.DateTimeStyles DateTimeStyle { get; private set; } + public class SQLiteException : Exception + { + public SQLite3.Result Result { get; private set; } + + protected SQLiteException (SQLite3.Result r, string message) : base (message) + { + Result = r; + } + + public static SQLiteException New (SQLite3.Result r, string message) + { + return new SQLiteException (r, message); + } + } + + public class NotNullConstraintViolationException : SQLiteException + { + public IEnumerable Columns { get; protected set; } + + protected NotNullConstraintViolationException (SQLite3.Result r, string message) + : this (r, message, null, null) + { + + } + + protected NotNullConstraintViolationException (SQLite3.Result r, string message, TableMapping mapping, object obj) + : base (r, message) + { + if (mapping != null && obj != null) { + this.Columns = from c in mapping.Columns + where c.IsNullable == false && c.GetValue (obj) == null + select c; + } + } + + public static new NotNullConstraintViolationException New (SQLite3.Result r, string message) + { + return new NotNullConstraintViolationException (r, message); + } + + public static NotNullConstraintViolationException New (SQLite3.Result r, string message, TableMapping mapping, object obj) + { + return new NotNullConstraintViolationException (r, message, mapping, obj); + } + + public static NotNullConstraintViolationException New (SQLiteException exception, TableMapping mapping, object obj) + { + return new NotNullConstraintViolationException (exception.Result, exception.Message, mapping, obj); + } + } + + [Flags] + public enum SQLiteOpenFlags + { + ReadOnly = 1, ReadWrite = 2, Create = 4, + Uri = 0x40, Memory = 0x80, + NoMutex = 0x8000, FullMutex = 0x10000, + SharedCache = 0x20000, PrivateCache = 0x40000, + ProtectionComplete = 0x00100000, + ProtectionCompleteUnlessOpen = 0x00200000, + ProtectionCompleteUntilFirstUserAuthentication = 0x00300000, + ProtectionNone = 0x00400000 + } + + [Flags] + public enum CreateFlags + { + /// + /// Use the default creation options + /// + None = 0x000, + /// + /// Create a primary key index for a property called 'Id' (case-insensitive). + /// This avoids the need for the [PrimaryKey] attribute. + /// + ImplicitPK = 0x001, + /// + /// Create indices for properties ending in 'Id' (case-insensitive). + /// + ImplicitIndex = 0x002, + /// + /// Create a primary key for a property called 'Id' and + /// create an indices for properties ending in 'Id' (case-insensitive). + /// + AllImplicit = 0x003, + /// + /// Force the primary key property to be auto incrementing. + /// This avoids the need for the [AutoIncrement] attribute. + /// The primary key property on the class should have type int or long. + /// + AutoIncPK = 0x004, + /// + /// Create virtual table using FTS3 + /// + FullTextSearch3 = 0x100, + /// + /// Create virtual table using FTS4 + /// + FullTextSearch4 = 0x200 + } + + public interface ISQLiteConnection : IDisposable + { + Sqlite3DatabaseHandle Handle { get; } + string DatabasePath { get; } + int LibVersionNumber { get; } + bool TimeExecution { get; set; } + bool Trace { get; set; } + Action Tracer { get; set; } + bool StoreDateTimeAsTicks { get; } + bool StoreTimeSpanAsTicks { get; } + string DateTimeStringFormat { get; } + TimeSpan BusyTimeout { get; set; } + IEnumerable TableMappings { get; } + bool IsInTransaction { get; } + + event EventHandler TableChanged; + + void Backup (string destinationDatabasePath, string databaseName = "main"); + void BeginTransaction (); + void Close (); + void Commit (); + SQLiteCommand CreateCommand (string cmdText, params object[] ps); + SQLiteCommand CreateCommand (string cmdText, Dictionary args); + int CreateIndex (string indexName, string tableName, string[] columnNames, bool unique = false); + int CreateIndex (string indexName, string tableName, string columnName, bool unique = false); + int CreateIndex (string tableName, string columnName, bool unique = false); + int CreateIndex (string tableName, string[] columnNames, bool unique = false); + int CreateIndex (Expression> property, bool unique = false); + CreateTableResult CreateTable (CreateFlags createFlags = CreateFlags.None); + CreateTableResult CreateTable (Type ty, CreateFlags createFlags = CreateFlags.None); + CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) + where T : new() + where T2 : new(); + CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) + where T : new() + where T2 : new() + where T3 : new(); + CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) + where T : new() + where T2 : new() + where T3 : new() + where T4 : new(); + CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) + where T : new() + where T2 : new() + where T3 : new() + where T4 : new() + where T5 : new(); + CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None, params Type[] types); + IEnumerable DeferredQuery (string query, params object[] args) where T : new(); + IEnumerable DeferredQuery (TableMapping map, string query, params object[] args); + int Delete (object objectToDelete); + int Delete (object primaryKey); + int Delete (object primaryKey, TableMapping map); + int DeleteAll (); + int DeleteAll (TableMapping map); + int DropTable (); + int DropTable (TableMapping map); + void EnableLoadExtension (bool enabled); + void EnableWriteAheadLogging (); + int Execute (string query, params object[] args); + T ExecuteScalar (string query, params object[] args); + T Find (object pk) where T : new(); + object Find (object pk, TableMapping map); + T Find (Expression> predicate) where T : new(); + T FindWithQuery (string query, params object[] args) where T : new(); + object FindWithQuery (TableMapping map, string query, params object[] args); + T Get (object pk) where T : new(); + object Get (object pk, TableMapping map); + T Get (Expression> predicate) where T : new(); + TableMapping GetMapping (Type type, CreateFlags createFlags = CreateFlags.None); + TableMapping GetMapping (CreateFlags createFlags = CreateFlags.None); + List GetTableInfo (string tableName); + int Insert (object obj); + int Insert (object obj, Type objType); + int Insert (object obj, string extra); + int Insert (object obj, string extra, Type objType); + int InsertAll (IEnumerable objects, bool runInTransaction = true); + int InsertAll (IEnumerable objects, string extra, bool runInTransaction = true); + int InsertAll (IEnumerable objects, Type objType, bool runInTransaction = true); + int InsertOrReplace (object obj); + int InsertOrReplace (object obj, Type objType); + List Query (string query, params object[] args) where T : new(); + List Query (TableMapping map, string query, params object[] args); + List QueryScalars (string query, params object[] args); + void ReKey (string key); + void ReKey (byte[] key); + void Release (string savepoint); + void Rollback (); + void RollbackTo (string savepoint); + void RunInTransaction (Action action); + string SaveTransactionPoint (); + TableQuery Table () where T : new(); + int Update (object obj); + int Update (object obj, Type objType); + int UpdateAll (IEnumerable objects, bool runInTransaction = true); + } + + /// + /// An open connection to a SQLite database. + /// + [Preserve (AllMembers = true)] + public partial class SQLiteConnection : ISQLiteConnection + { + private bool _open; + private TimeSpan _busyTimeout; + readonly static Dictionary _mappings = new Dictionary (); + private System.Diagnostics.Stopwatch _sw; + private long _elapsedMilliseconds = 0; + + private int _transactionDepth = 0; + private Random _rand = new Random (); + + public Sqlite3DatabaseHandle Handle { get; private set; } + static readonly Sqlite3DatabaseHandle NullHandle = default (Sqlite3DatabaseHandle); + static readonly Sqlite3BackupHandle NullBackupHandle = default (Sqlite3BackupHandle); + + /// + /// Gets the database path used by this connection. + /// + public string DatabasePath { get; private set; } + + /// + /// Gets the SQLite library version number. 3007014 would be v3.7.14 + /// + public int LibVersionNumber { get; private set; } + + /// + /// Whether Trace lines should be written that show the execution time of queries. + /// + public bool TimeExecution { get; set; } + + /// + /// Whether to write queries to during execution. + /// + public bool Trace { get; set; } + + /// + /// The delegate responsible for writing trace lines. + /// + /// The tracer. + public Action Tracer { get; set; } + + /// + /// Whether to store DateTime properties as ticks (true) or strings (false). + /// + public bool StoreDateTimeAsTicks { get; private set; } + + /// + /// Whether to store TimeSpan properties as ticks (true) or strings (false). + /// + public bool StoreTimeSpanAsTicks { get; private set; } + + /// + /// The format to use when storing DateTime properties as strings. Ignored if StoreDateTimeAsTicks is true. + /// + /// The date time string format. + public string DateTimeStringFormat { get; private set; } + + /// + /// The DateTimeStyles value to use when parsing a DateTime property string. + /// + /// The date time style. + internal System.Globalization.DateTimeStyles DateTimeStyle { get; private set; } #if USE_SQLITEPCL_RAW && !NO_SQLITEPCL_RAW_BATTERIES - static SQLiteConnection () - { - SQLitePCL.Batteries_V2.Init (); - } + static SQLiteConnection () + { + SQLitePCL.Batteries_V2.Init (); + } +#endif + + /// + /// Constructs a new SQLiteConnection and opens a 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 SQLiteConnection (string databasePath, bool storeDateTimeAsTicks = true) + : this (new SQLiteConnectionString (databasePath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create, storeDateTimeAsTicks)) + { + } + + /// + /// Constructs a new SQLiteConnection and opens a SQLite database specified by databasePath. + /// + /// + /// Specifies the path to the database file. + /// + /// + /// Flags controlling how the connection should be opened. + /// + /// + /// 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 SQLiteConnection (string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks = true) + : this (new SQLiteConnectionString (databasePath, openFlags, storeDateTimeAsTicks)) + { + } + + /// + /// Constructs a new SQLiteConnection and opens a SQLite database specified by databasePath. + /// + /// + /// Details on how to find and open the database. + /// + public SQLiteConnection (SQLiteConnectionString connectionString) + { + if (connectionString == null) + throw new ArgumentNullException (nameof (connectionString)); + if (connectionString.DatabasePath == null) + throw new InvalidOperationException ("DatabasePath must be specified"); + + DatabasePath = connectionString.DatabasePath; + + LibVersionNumber = SQLite3.LibVersionNumber (); + +#if NETFX_CORE + SQLite3.SetDirectory(/*temp directory type*/2, Windows.Storage.ApplicationData.Current.TemporaryFolder.Path); +#endif + + Sqlite3DatabaseHandle handle; + +#if SILVERLIGHT || USE_CSHARP_SQLITE || USE_SQLITEPCL_RAW + var r = SQLite3.Open (connectionString.DatabasePath, out handle, (int)connectionString.OpenFlags, connectionString.VfsName); +#else + // open using the byte[] + // in the case where the path may include Unicode + // force open to using UTF-8 using sqlite3_open_v2 + var databasePathAsBytes = GetNullTerminatedUtf8 (connectionString.DatabasePath); + var r = SQLite3.Open (databasePathAsBytes, out handle, (int)connectionString.OpenFlags, connectionString.VfsName); +#endif + + Handle = handle; + if (r != SQLite3.Result.OK) { + throw SQLiteException.New (r, String.Format ("Could not open database file: {0} ({1})", DatabasePath, r)); + } + _open = true; + + StoreDateTimeAsTicks = connectionString.StoreDateTimeAsTicks; + StoreTimeSpanAsTicks = connectionString.StoreTimeSpanAsTicks; + DateTimeStringFormat = connectionString.DateTimeStringFormat; + DateTimeStyle = connectionString.DateTimeStyle; + + BusyTimeout = TimeSpan.FromSeconds (1.0); + Tracer = line => Debug.WriteLine (line); + + connectionString.PreKeyAction?.Invoke (this); + if (connectionString.Key is string stringKey) { + SetKey (stringKey); + } + else if (connectionString.Key is byte[] bytesKey) { + SetKey (bytesKey); + } + else if (connectionString.Key != null) { + throw new InvalidOperationException ("Encryption keys must be strings or byte arrays"); + } + connectionString.PostKeyAction?.Invoke (this); + } + + /// + /// 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 void EnableWriteAheadLogging () + { + ExecuteScalar ("PRAGMA journal_mode=WAL"); + } + + /// + /// Convert an input string to a quoted SQL string that can be safely used in queries. + /// + /// The quoted string. + /// The unsafe string to quote. + static string Quote (string unsafeString) + { + // TODO: Doesn't call sqlite3_mprintf("%Q", u) because we're waiting on https://github.com/ericsink/SQLitePCL.raw/issues/153 + if (unsafeString == null) + return "NULL"; + var safe = unsafeString.Replace ("'", "''"); + return "'" + safe + "'"; + } + + /// + /// Sets the key used to encrypt/decrypt the database with "pragma key = ...". + /// This must be the first thing you call before doing anything else with this connection + /// if your database is encrypted. + /// This only has an effect if you are using the SQLCipher nuget package. + /// + /// Encryption key plain text that is converted to the real encryption key using PBKDF2 key derivation + void SetKey (string key) + { + if (key == null) + throw new ArgumentNullException (nameof (key)); + var q = Quote (key); + ExecuteScalar ("pragma key = " + q); + } + + /// + /// Sets the key used to encrypt/decrypt the database. + /// This must be the first thing you call before doing anything else with this connection + /// if your database is encrypted. + /// This only has an effect if you are using the SQLCipher nuget package. + /// + /// 256-bit (32 byte) encryption key data + void SetKey (byte[] key) + { + if (key == null) + throw new ArgumentNullException (nameof (key)); + if (key.Length != 32 && key.Length != 48) + throw new ArgumentException ("Key must be 32 bytes (256-bit) or 48 bytes (384-bit)", nameof (key)); + var s = String.Join ("", key.Select (x => x.ToString ("X2"))); + ExecuteScalar ("pragma key = \"x'" + s + "'\""); + } + + /// + /// Change the encryption key for a SQLCipher database with "pragma rekey = ...". + /// + /// Encryption key plain text that is converted to the real encryption key using PBKDF2 key derivation + public void ReKey (string key) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + var q = Quote(key); + ExecuteScalar("pragma rekey = " + q); + } + + /// + /// Change the encryption key for a SQLCipher database. + /// + /// 256-bit (32 byte) or 384-bit (48 bytes) encryption key data + public void ReKey (byte[] key) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (key.Length != 32 && key.Length != 48) + throw new ArgumentException ("Key must be 32 bytes (256-bit) or 48 bytes (384-bit)", nameof (key)); + var s = String.Join("", key.Select(x => x.ToString("X2"))); + ExecuteScalar("pragma rekey = \"x'" + s + "'\""); + } + + /// + /// Enable or disable extension loading. + /// + public void EnableLoadExtension (bool enabled) + { + SQLite3.Result r = SQLite3.EnableLoadExtension (Handle, enabled ? 1 : 0); + if (r != SQLite3.Result.OK) { + string msg = SQLite3.GetErrmsg (Handle); + throw SQLiteException.New (r, msg); + } + } + +#if !USE_SQLITEPCL_RAW + static byte[] GetNullTerminatedUtf8 (string s) + { + var utf8Length = System.Text.Encoding.UTF8.GetByteCount (s); + var bytes = new byte [utf8Length + 1]; + utf8Length = System.Text.Encoding.UTF8.GetBytes(s, 0, s.Length, bytes, 0); + return bytes; + } +#endif + + /// + /// Sets a busy handler to sleep the specified amount of time when a table is locked. + /// The handler will sleep multiple times until a total time of has accumulated. + /// + public TimeSpan BusyTimeout { + get { return _busyTimeout; } + set { + _busyTimeout = value; + if (Handle != NullHandle) { + SQLite3.BusyTimeout (Handle, (int)_busyTimeout.TotalMilliseconds); + } + } + } + + /// + /// Returns the mappings from types to tables that the connection + /// currently understands. + /// + public IEnumerable TableMappings { + get { + lock (_mappings) { + return new List (_mappings.Values); + } + } + } + + /// + /// 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 TableMapping GetMapping (Type type, CreateFlags createFlags = CreateFlags.None) + { + TableMapping map; + var key = type.FullName; + lock (_mappings) { + if (_mappings.TryGetValue (key, out map)) { + if (createFlags != CreateFlags.None && createFlags != map.CreateFlags) { + map = new TableMapping (type, createFlags); + _mappings[key] = map; + } + } + else { + map = new TableMapping (type, createFlags); + _mappings.Add (key, map); + } + } + return map; + } + + /// + /// 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 TableMapping GetMapping (CreateFlags createFlags = CreateFlags.None) + { + return GetMapping (typeof (T), createFlags); + } + + private struct IndexedColumn + { + public int Order; + public string ColumnName; + } + + private struct IndexInfo + { + public string IndexName; + public string TableName; + public bool Unique; + public List Columns; + } + + /// + /// Executes a "drop table" on the database. This is non-recoverable. + /// + public int DropTable () + { + return DropTable (GetMapping (typeof (T))); + } + + /// + /// Executes a "drop table" on the database. This is non-recoverable. + /// + /// + /// The TableMapping used to identify the table. + /// + public int DropTable (TableMapping map) + { + var query = string.Format ("drop table if exists \"{0}\"", map.TableName); + return Execute (query); + } + + /// + /// 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 CreateTableResult CreateTable (CreateFlags createFlags = CreateFlags.None) + { + return CreateTable (typeof (T), 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 CreateTableResult CreateTable (Type ty, CreateFlags createFlags = CreateFlags.None) + { + var map = GetMapping (ty, createFlags); + + // Present a nice error if no columns specified + if (map.Columns.Length == 0) { + throw new Exception (string.Format ("Cannot create a table without columns (does '{0}' have public properties?)", ty.FullName)); + } + + // Check if the table exists + var result = CreateTableResult.Created; + var existingCols = GetTableInfo (map.TableName); + + // Create or migrate it + if (existingCols.Count == 0) { + + // Facilitate virtual tables a.k.a. full-text search. + bool fts3 = (createFlags & CreateFlags.FullTextSearch3) != 0; + bool fts4 = (createFlags & CreateFlags.FullTextSearch4) != 0; + bool fts = fts3 || fts4; + var @virtual = fts ? "virtual " : string.Empty; + var @using = fts3 ? "using fts3 " : fts4 ? "using fts4 " : string.Empty; + + // Build query. + var query = "create " + @virtual + "table if not exists \"" + map.TableName + "\" " + @using + "(\n"; + var decls = map.Columns.Select (p => Orm.SqlDecl (p, StoreDateTimeAsTicks, StoreTimeSpanAsTicks)); + var decl = string.Join (",\n", decls.ToArray ()); + query += decl; + query += ")"; + if (map.WithoutRowId) { + query += " without rowid"; + } + + Execute (query); + } + else { + result = CreateTableResult.Migrated; + MigrateTable (map, existingCols); + } + + var indexes = new Dictionary (); + foreach (var c in map.Columns) { + foreach (var i in c.Indices) { + var iname = i.Name ?? map.TableName + "_" + c.Name; + IndexInfo iinfo; + if (!indexes.TryGetValue (iname, out iinfo)) { + iinfo = new IndexInfo { + IndexName = iname, + TableName = map.TableName, + Unique = i.Unique, + Columns = new List () + }; + indexes.Add (iname, iinfo); + } + + if (i.Unique != iinfo.Unique) + throw new Exception ("All the columns in an index must have the same value for their Unique property"); + + iinfo.Columns.Add (new IndexedColumn { + Order = i.Order, + ColumnName = c.Name + }); + } + } + + foreach (var indexName in indexes.Keys) { + var index = indexes[indexName]; + var columns = index.Columns.OrderBy (i => i.Order).Select (i => i.ColumnName).ToArray (); + CreateIndex (indexName, index.TableName, columns, index.Unique); + } + + return result; + } + + /// + /// 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 CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) + where T : new() + where T2 : new() + { + return CreateTables (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 CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) + where T : new() + where T2 : new() + where T3 : new() + { + return CreateTables (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 CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) + where T : new() + where T2 : new() + where T3 : new() + where T4 : new() + { + return CreateTables (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 CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) + where T : new() + where T2 : new() + where T3 : new() + where T4 : new() + where T5 : new() + { + return CreateTables (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 CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None, params Type[] types) + { + var result = new CreateTablesResult (); + foreach (Type type in types) { + var aResult = CreateTable (type, createFlags); + result.Results[type] = aResult; + } + return result; + } + + /// + /// 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 int CreateIndex (string indexName, string tableName, string[] columnNames, bool unique = false) + { + const string sqlFormat = "create {2} index if not exists \"{3}\" on \"{0}\"(\"{1}\")"; + var sql = String.Format (sqlFormat, tableName, string.Join ("\", \"", columnNames), unique ? "unique" : "", indexName); + return Execute (sql); + } + + /// + /// 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 int CreateIndex (string indexName, string tableName, string columnName, bool unique = false) + { + return CreateIndex (indexName, tableName, new string[] { columnName }, unique); + } + + /// + /// 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 int CreateIndex (string tableName, string columnName, bool unique = false) + { + return CreateIndex (tableName + "_" + columnName, 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 int CreateIndex (string tableName, string[] columnNames, bool unique = false) + { + return CreateIndex (tableName + "_" + string.Join ("_", columnNames), 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 int CreateIndex (Expression> property, bool unique = false) + { + MemberExpression mx; + if (property.Body.NodeType == ExpressionType.Convert) { + mx = ((UnaryExpression)property.Body).Operand as MemberExpression; + } + else { + mx = (property.Body as MemberExpression); + } + var propertyInfo = mx.Member as PropertyInfo; + if (propertyInfo == null) { + throw new ArgumentException ("The lambda expression 'property' should point to a valid Property"); + } + + var propName = propertyInfo.Name; + + var map = GetMapping (); + var colName = map.FindColumnWithPropertyName (propName).Name; + + return CreateIndex (map.TableName, colName, unique); + } + + [Preserve (AllMembers = true)] + public class ColumnInfo + { + // public int cid { get; set; } + + [Column ("name")] + public string Name { get; set; } + + // [Column ("type")] + // public string ColumnType { get; set; } + + public int notnull { get; set; } + + // public string dflt_value { get; set; } + + // public int pk { get; set; } + + public override string ToString () + { + return Name; + } + } + + /// + /// Query the built-in sqlite table_info table for a specific tables columns. + /// + /// The columns contains in the table. + /// Table name. + public List GetTableInfo (string tableName) + { + var query = "pragma table_info(\"" + tableName + "\")"; + return Query (query); + } + + void MigrateTable (TableMapping map, List existingCols) + { + var toBeAdded = new List (); + + foreach (var p in map.Columns) { + var found = false; + foreach (var c in existingCols) { + found = (string.Compare (p.Name, c.Name, StringComparison.OrdinalIgnoreCase) == 0); + if (found) + break; + } + if (!found) { + toBeAdded.Add (p); + } + } + + foreach (var p in toBeAdded) { + var addCol = "alter table \"" + map.TableName + "\" add column " + Orm.SqlDecl (p, StoreDateTimeAsTicks, StoreTimeSpanAsTicks); + Execute (addCol); + } + } + + /// + /// Creates a new SQLiteCommand. Can be overridden to provide a sub-class. + /// + /// + protected virtual SQLiteCommand NewCommand () + { + return new SQLiteCommand (this); + } + + /// + /// Creates a new SQLiteCommand given the command text with arguments. Place a '?' + /// in the command text for each of the arguments. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the command text. + /// + /// + /// A + /// + public SQLiteCommand CreateCommand (string cmdText, params object[] ps) + { + if (!_open) + throw SQLiteException.New (SQLite3.Result.Error, "Cannot create commands from unopened database"); + + var cmd = NewCommand (); + cmd.CommandText = cmdText; + foreach (var o in ps) { + cmd.Bind (o); + } + return cmd; + } + + /// + /// Creates a new SQLiteCommand given the command text with named arguments. Place a "[@:$]VVV" + /// in the command text for each of the arguments. VVV represents an alphanumeric identifier. + /// For example, @name :name and $name can all be used in the query. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of "[@:$]VVV" in the command text. + /// + /// + /// A + /// + public SQLiteCommand CreateCommand (string cmdText, Dictionary args) + { + if (!_open) + throw SQLiteException.New (SQLite3.Result.Error, "Cannot create commands from unopened database"); + + SQLiteCommand cmd = NewCommand (); + cmd.CommandText = cmdText; + foreach (var kv in args) { + cmd.Bind (kv.Key, kv.Value); + } + return cmd; + } + + /// + /// 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 int Execute (string query, params object[] args) + { + var cmd = CreateCommand (query, args); + + if (TimeExecution) { + if (_sw == null) { + _sw = new Stopwatch (); + } + _sw.Reset (); + _sw.Start (); + } + + var r = cmd.ExecuteNonQuery (); + + if (TimeExecution) { + _sw.Stop (); + _elapsedMilliseconds += _sw.ElapsedMilliseconds; + Tracer?.Invoke (string.Format ("Finished in {0} ms ({1:0.0} s total)", _sw.ElapsedMilliseconds, _elapsedMilliseconds / 1000.0)); + } + + return r; + } + + /// + /// 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 T ExecuteScalar (string query, params object[] args) + { + var cmd = CreateCommand (query, args); + + if (TimeExecution) { + if (_sw == null) { + _sw = new Stopwatch (); + } + _sw.Reset (); + _sw.Start (); + } + + var r = cmd.ExecuteScalar (); + + if (TimeExecution) { + _sw.Stop (); + _elapsedMilliseconds += _sw.ElapsedMilliseconds; + Tracer?.Invoke (string.Format ("Finished in {0} ms ({1:0.0} s total)", _sw.ElapsedMilliseconds, _elapsedMilliseconds / 1000.0)); + } + + return r; + } + + /// + /// 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. + /// + public List Query (string query, params object[] args) where T : new() + { + var cmd = CreateCommand (query, args); + return cmd.ExecuteQuery (); + } + + /// + /// 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. + /// + /// + /// An enumerable with one result for the first column of each row returned by the query. + /// + public List QueryScalars (string query, params object[] args) + { + var cmd = CreateCommand (query, args); + return cmd.ExecuteQueryScalars ().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 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 (retrieved by calling GetEnumerator() on the result of this method) + /// will call sqlite3_step on each call to MoveNext, so the database + /// connection must remain open for the lifetime of the enumerator. + /// + public IEnumerable DeferredQuery (string query, params object[] args) where T : new() + { + var cmd = CreateCommand (query, args); + return cmd.ExecuteDeferredQuery (); + } + + /// + /// 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 List Query (TableMapping map, string query, params object[] args) + { + var cmd = CreateCommand (query, args); + return cmd.ExecuteQuery (map); + } + + /// + /// 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 (retrieved by calling GetEnumerator() on the result of this method) + /// will call sqlite3_step on each call to MoveNext, so the database + /// connection must remain open for the lifetime of the enumerator. + /// + public IEnumerable DeferredQuery (TableMapping map, string query, params object[] args) + { + var cmd = CreateCommand (query, args); + return cmd.ExecuteDeferredQuery (map); + } + + /// + /// 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 TableQuery Table () where T : new() + { + return new TableQuery (this); + } + + /// + /// 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 T Get (object pk) where T : new() + { + var map = GetMapping (typeof (T)); + return Query (map.GetByPrimaryKeySql, pk).First (); + } + + /// + /// 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 object Get (object pk, TableMapping map) + { + return Query (map, map.GetByPrimaryKeySql, pk).First (); + } + + /// + /// 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 T Get (Expression> predicate) where T : new() + { + return Table ().Where (predicate).First (); + } + + /// + /// 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 T Find (object pk) where T : new() + { + var map = GetMapping (typeof (T)); + return Query (map.GetByPrimaryKeySql, pk).FirstOrDefault (); + } + + /// + /// 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 object Find (object pk, TableMapping map) + { + return Query (map, map.GetByPrimaryKeySql, pk).FirstOrDefault (); + } + + /// + /// 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 T Find (Expression> predicate) where T : new() + { + return Table ().Where (predicate).FirstOrDefault (); + } + + /// + /// 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 T FindWithQuery (string query, params object[] args) where T : new() + { + return Query (query, args).FirstOrDefault (); + } + + /// + /// 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 object FindWithQuery (TableMapping map, string query, params object[] args) + { + return Query (map, query, args).FirstOrDefault (); + } + + /// + /// Whether has been called and the database is waiting for a . + /// + public bool IsInTransaction { + get { return _transactionDepth > 0; } + } + + /// + /// Begins a new transaction. Call to end the transaction. + /// + /// Throws if a transaction has already begun. + public void BeginTransaction () + { + // The BEGIN command only works if the transaction stack is empty, + // or in other words if there are no pending transactions. + // If the transaction stack is not empty when the BEGIN command is invoked, + // then the command fails with an error. + // Rather than crash with an error, we will just ignore calls to BeginTransaction + // that would result in an error. + if (Interlocked.CompareExchange (ref _transactionDepth, 1, 0) == 0) { + try { + Execute ("begin transaction"); + } + catch (Exception ex) { + var sqlExp = ex as SQLiteException; + if (sqlExp != null) { + // It is recommended that applications respond to the errors listed below + // by explicitly issuing a ROLLBACK command. + // TODO: This rollback failsafe should be localized to all throw sites. + switch (sqlExp.Result) { + case SQLite3.Result.IOError: + case SQLite3.Result.Full: + case SQLite3.Result.Busy: + case SQLite3.Result.NoMem: + case SQLite3.Result.Interrupt: + RollbackTo (null, true); + break; + } + } + else { + // Call decrement and not VolatileWrite in case we've already + // created a transaction point in SaveTransactionPoint since the catch. + Interlocked.Decrement (ref _transactionDepth); + } + + throw; + } + } + else { + // Calling BeginTransaction on an already open transaction is invalid + throw new InvalidOperationException ("Cannot begin a transaction while already in a transaction."); + } + } + + /// + /// Creates a savepoint in the database at the current point in the transaction timeline. + /// Begins a new transaction if one is not in progress. + /// + /// Call to undo transactions since the returned savepoint. + /// Call to commit transactions after the savepoint returned here. + /// Call to end the transaction, committing all changes. + /// + /// A string naming the savepoint. + public string SaveTransactionPoint () + { + int depth = Interlocked.Increment (ref _transactionDepth) - 1; + string retVal = "S" + _rand.Next (short.MaxValue) + "D" + depth; + + try { + Execute ("savepoint " + retVal); + } + catch (Exception ex) { + var sqlExp = ex as SQLiteException; + if (sqlExp != null) { + // It is recommended that applications respond to the errors listed below + // by explicitly issuing a ROLLBACK command. + // TODO: This rollback failsafe should be localized to all throw sites. + switch (sqlExp.Result) { + case SQLite3.Result.IOError: + case SQLite3.Result.Full: + case SQLite3.Result.Busy: + case SQLite3.Result.NoMem: + case SQLite3.Result.Interrupt: + RollbackTo (null, true); + break; + } + } + else { + Interlocked.Decrement (ref _transactionDepth); + } + + throw; + } + + return retVal; + } + + /// + /// Rolls back the transaction that was begun by or . + /// + public void Rollback () + { + RollbackTo (null, false); + } + + /// + /// Rolls back the savepoint created by or SaveTransactionPoint. + /// + /// The name of the savepoint to roll back to, as returned by . If savepoint is null or empty, this method is equivalent to a call to + public void RollbackTo (string savepoint) + { + RollbackTo (savepoint, false); + } + + /// + /// Rolls back the transaction that was begun by . + /// + /// The name of the savepoint to roll back to, as returned by . If savepoint is null or empty, this method is equivalent to a call to + /// true to avoid throwing exceptions, false otherwise + void RollbackTo (string savepoint, bool noThrow) + { + // Rolling back without a TO clause rolls backs all transactions + // and leaves the transaction stack empty. + try { + if (String.IsNullOrEmpty (savepoint)) { + if (Interlocked.Exchange (ref _transactionDepth, 0) > 0) { + Execute ("rollback"); + } + } + else { + DoSavePointExecute (savepoint, "rollback to "); + } + } + catch (SQLiteException) { + if (!noThrow) + throw; + + } + // No need to rollback if there are no transactions open. + } + + /// + /// Releases a savepoint returned from . Releasing a savepoint + /// makes changes since that savepoint permanent if the savepoint began the transaction, + /// or otherwise the changes are permanent pending a call to . + /// + /// The RELEASE command is like a COMMIT for a SAVEPOINT. + /// + /// The name of the savepoint to release. The string should be the result of a call to + public void Release (string savepoint) + { + try { + DoSavePointExecute (savepoint, "release "); + } + catch (SQLiteException ex) { + if (ex.Result == SQLite3.Result.Busy) { + // Force a rollback since most people don't know this function can fail + // Don't call Rollback() since the _transactionDepth is 0 and it won't try + // Calling rollback makes our _transactionDepth variable correct. + // Writes to the database only happen at depth=0, so this failure will only happen then. + try { + Execute ("rollback"); + } + catch { + // rollback can fail in all sorts of wonderful version-dependent ways. Let's just hope for the best + } + } + throw; + } + } + + void DoSavePointExecute (string savepoint, string cmd) + { + // Validate the savepoint + int firstLen = savepoint.IndexOf ('D'); + if (firstLen >= 2 && savepoint.Length > firstLen + 1) { + int depth; + if (Int32.TryParse (savepoint.Substring (firstLen + 1), out depth)) { + // TODO: Mild race here, but inescapable without locking almost everywhere. + if (0 <= depth && depth < _transactionDepth) { +#if NETFX_CORE || USE_SQLITEPCL_RAW || NETCORE + Volatile.Write (ref _transactionDepth, depth); +#elif SILVERLIGHT + _transactionDepth = depth; +#else + Thread.VolatileWrite (ref _transactionDepth, depth); #endif + Execute (cmd + savepoint); + return; + } + } + } + + throw new ArgumentException ("savePoint is not valid, and should be the result of a call to SaveTransactionPoint.", "savePoint"); + } + + /// + /// Commits the transaction that was begun by . + /// + public void Commit () + { + if (Interlocked.Exchange (ref _transactionDepth, 0) != 0) { + try { + Execute ("commit"); + } + catch { + // Force a rollback since most people don't know this function can fail + // Don't call Rollback() since the _transactionDepth is 0 and it won't try + // Calling rollback makes our _transactionDepth variable correct. + try { + Execute ("rollback"); + } + catch { + // rollback can fail in all sorts of wonderful version-dependent ways. Let's just hope for the best + } + throw; + } + } + // Do nothing on a commit with no open transaction + } + + /// + /// 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 void RunInTransaction (Action action) + { + try { + var savePoint = SaveTransactionPoint (); + action (); + Release (savePoint); + } + catch (Exception) { + Rollback (); + throw; + } + } + + /// + /// 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 int InsertAll (System.Collections.IEnumerable objects, bool runInTransaction = true) + { + var c = 0; + if (runInTransaction) { + RunInTransaction (() => { + foreach (var r in objects) { + c += Insert (r); + } + }); + } + else { + foreach (var r in objects) { + c += Insert (r); + } + } + return c; + } + + /// + /// 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 int InsertAll (System.Collections.IEnumerable objects, string extra, bool runInTransaction = true) + { + var c = 0; + if (runInTransaction) { + RunInTransaction (() => { + foreach (var r in objects) { + c += Insert (r, extra); + } + }); + } + else { + foreach (var r in objects) { + c += Insert (r, extra); + } + } + return c; + } + + /// + /// 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 int InsertAll (System.Collections.IEnumerable objects, Type objType, bool runInTransaction = true) + { + var c = 0; + if (runInTransaction) { + RunInTransaction (() => { + foreach (var r in objects) { + c += Insert (r, objType); + } + }); + } + else { + foreach (var r in objects) { + c += Insert (r, objType); + } + } + return c; + } + + /// + /// 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 number of rows added to the table. + /// + public int Insert (object obj) + { + if (obj == null) { + return 0; + } + return Insert (obj, "", Orm.GetType (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 number of rows modified. + /// + public int InsertOrReplace (object obj) + { + if (obj == null) { + return 0; + } + return Insert (obj, "OR REPLACE", Orm.GetType (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 int Insert (object obj, Type objType) + { + return 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. + /// 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 int InsertOrReplace (object obj, Type objType) + { + return Insert (obj, "OR REPLACE", 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 int Insert (object obj, string extra) + { + if (obj == null) { + return 0; + } + return Insert (obj, extra, Orm.GetType (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. + /// + /// + /// 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 int Insert (object obj, string extra, Type objType) + { + if (obj == null || objType == null) { + return 0; + } + + var map = GetMapping (objType); + + if (map.PK != null && map.PK.IsAutoGuid) { + if (map.PK.GetValue (obj).Equals (Guid.Empty)) { + map.PK.SetValue (obj, Guid.NewGuid ()); + } + } + + var replacing = string.Compare (extra, "OR REPLACE", StringComparison.OrdinalIgnoreCase) == 0; + + var cols = replacing ? map.InsertOrReplaceColumns : map.InsertColumns; + var vals = new object[cols.Length]; + for (var i = 0; i < vals.Length; i++) { + vals[i] = cols[i].GetValue (obj); + } + + var insertCmd = GetInsertCommand (map, extra); + int count; + + lock (insertCmd) { + // We lock here to protect the prepared statement returned via GetInsertCommand. + // A SQLite prepared statement can be bound for only one operation at a time. + try { + count = insertCmd.ExecuteNonQuery (vals); + } + catch (SQLiteException ex) { + if (SQLite3.ExtendedErrCode (this.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { + throw NotNullConstraintViolationException.New (ex.Result, ex.Message, map, obj); + } + throw; + } + + if (map.HasAutoIncPK) { + var id = SQLite3.LastInsertRowid (Handle); + map.SetAutoIncPK (obj, id); + } + } + if (count > 0) + OnTableChanged (map, NotifyTableChangedAction.Insert); + + return count; + } + + readonly Dictionary, PreparedSqlLiteInsertCommand> _insertCommandMap = new Dictionary, PreparedSqlLiteInsertCommand> (); + + PreparedSqlLiteInsertCommand GetInsertCommand (TableMapping map, string extra) + { + PreparedSqlLiteInsertCommand prepCmd; + + var key = Tuple.Create (map.MappedType.FullName, extra); + + lock (_insertCommandMap) { + if (_insertCommandMap.TryGetValue (key, out prepCmd)) { + return prepCmd; + } + } + + prepCmd = CreateInsertCommand (map, extra); + + lock (_insertCommandMap) { + if (_insertCommandMap.TryGetValue (key, out var existing)) { + prepCmd.Dispose (); + return existing; + } + + _insertCommandMap.Add (key, prepCmd); + } + + return prepCmd; + } + + PreparedSqlLiteInsertCommand CreateInsertCommand (TableMapping map, string extra) + { + var cols = map.InsertColumns; + string insertSql; + if (cols.Length == 0 && map.Columns.Length == 1 && map.Columns[0].IsAutoInc) { + insertSql = string.Format ("insert {1} into \"{0}\" default values", map.TableName, extra); + } + else { + var replacing = string.Compare (extra, "OR REPLACE", StringComparison.OrdinalIgnoreCase) == 0; + + if (replacing) { + cols = map.InsertOrReplaceColumns; + } + + insertSql = string.Format ("insert {3} into \"{0}\"({1}) values ({2})", map.TableName, + string.Join (",", (from c in cols + select "\"" + c.Name + "\"").ToArray ()), + string.Join (",", (from c in cols + select "?").ToArray ()), extra); + + } + + var insertCommand = new PreparedSqlLiteInsertCommand (this, insertSql); + return insertCommand; + } + + /// + /// 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 int Update (object obj) + { + if (obj == null) { + return 0; + } + return Update (obj, Orm.GetType (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 int Update (object obj, Type objType) + { + int rowsAffected = 0; + if (obj == null || objType == null) { + return 0; + } + + var map = GetMapping (objType); + + var pk = map.PK; + + if (pk == null) { + throw new NotSupportedException ("Cannot update " + map.TableName + ": it has no PK"); + } + + var cols = from p in map.Columns + where p != pk + select p; + var vals = from c in cols + select c.GetValue (obj); + var ps = new List (vals); + if (ps.Count == 0) { + // There is a PK but no accompanying data, + // so reset the PK to make the UPDATE work. + cols = map.Columns; + vals = from c in cols + select c.GetValue (obj); + ps = new List (vals); + } + ps.Add (pk.GetValue (obj)); + var q = string.Format ("update \"{0}\" set {1} where \"{2}\" = ? ", map.TableName, string.Join (",", (from c in cols + select "\"" + c.Name + "\" = ? ").ToArray ()), pk.Name); + + try { + rowsAffected = Execute (q, ps.ToArray ()); + } + catch (SQLiteException ex) { + + if (ex.Result == SQLite3.Result.Constraint && SQLite3.ExtendedErrCode (this.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { + throw NotNullConstraintViolationException.New (ex, map, obj); + } + + throw; + } + + if (rowsAffected > 0) + OnTableChanged (map, NotifyTableChangedAction.Update); + + return rowsAffected; + } + + /// + /// 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 int UpdateAll (System.Collections.IEnumerable objects, bool runInTransaction = true) + { + var c = 0; + if (runInTransaction) { + RunInTransaction (() => { + foreach (var r in objects) { + c += Update (r); + } + }); + } + else { + foreach (var r in objects) { + c += Update (r); + } + } + return c; + } + + /// + /// 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 int Delete (object objectToDelete) + { + var map = GetMapping (Orm.GetType (objectToDelete)); + var pk = map.PK; + if (pk == null) { + throw new NotSupportedException ("Cannot delete " + map.TableName + ": it has no PK"); + } + var q = string.Format ("delete from \"{0}\" where \"{1}\" = ?", map.TableName, pk.Name); + var count = Execute (q, pk.GetValue (objectToDelete)); + if (count > 0) + OnTableChanged (map, NotifyTableChangedAction.Delete); + return count; + } + + /// + /// 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 int Delete (object primaryKey) + { + return Delete (primaryKey, GetMapping (typeof (T))); + } + + /// + /// 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 int Delete (object primaryKey, TableMapping map) + { + var pk = map.PK; + if (pk == null) { + throw new NotSupportedException ("Cannot delete " + map.TableName + ": it has no PK"); + } + var q = string.Format ("delete from \"{0}\" where \"{1}\" = ?", map.TableName, pk.Name); + var count = Execute (q, primaryKey); + if (count > 0) + OnTableChanged (map, NotifyTableChangedAction.Delete); + return count; + } + + /// + /// 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 int DeleteAll () + { + var map = GetMapping (typeof (T)); + return DeleteAll (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 TableMapping used to identify the table. + /// + /// + /// The number of objects deleted. + /// + public int DeleteAll (TableMapping map) + { + var query = string.Format ("delete from \"{0}\"", map.TableName); + var count = Execute (query); + if (count > 0) + OnTableChanged (map, NotifyTableChangedAction.Delete); + return count; + } + + /// + /// Backup the entire database to the specified path. + /// + /// Path to backup file. + /// The name of the database to backup (usually "main"). + public void Backup (string destinationDatabasePath, string databaseName = "main") + { + // Open the destination + var r = SQLite3.Open (destinationDatabasePath, out var destHandle); + if (r != SQLite3.Result.OK) { + throw SQLiteException.New (r, "Failed to open destination database"); + } + + // Init the backup + var backup = SQLite3.BackupInit (destHandle, databaseName, Handle, databaseName); + if (backup == NullBackupHandle) { + SQLite3.Close (destHandle); + throw new Exception ("Failed to create backup"); + } + + // Perform it + SQLite3.BackupStep (backup, -1); + SQLite3.BackupFinish (backup); + + // Check for errors + r = SQLite3.GetResult (destHandle); + string msg = ""; + if (r != SQLite3.Result.OK) { + msg = SQLite3.GetErrmsg (destHandle); + } + + // Close everything and report errors + SQLite3.Close (destHandle); + if (r != SQLite3.Result.OK) { + throw SQLiteException.New (r, msg); + } + } + + ~SQLiteConnection () + { + Dispose (false); + } + + public void Dispose () + { + Dispose (true); + GC.SuppressFinalize (this); + } + + public void Close () + { + Dispose (true); + } + + protected virtual void Dispose (bool disposing) + { + var useClose2 = LibVersionNumber >= 3007014; + + if (_open && Handle != NullHandle) { + try { + if (disposing) { + lock (_insertCommandMap) { + foreach (var sqlInsertCommand in _insertCommandMap.Values) { + sqlInsertCommand.Dispose (); + } + _insertCommandMap.Clear (); + } + + var r = useClose2 ? SQLite3.Close2 (Handle) : SQLite3.Close (Handle); + if (r != SQLite3.Result.OK) { + string msg = SQLite3.GetErrmsg (Handle); + throw SQLiteException.New (r, msg); + } + } + else { + var r = useClose2 ? SQLite3.Close2 (Handle) : SQLite3.Close (Handle); + } + } + finally { + Handle = NullHandle; + _open = false; + } + } + } + + void OnTableChanged (TableMapping table, NotifyTableChangedAction action) + { + var ev = TableChanged; + if (ev != null) + ev (this, new NotifyTableChangedEventArgs (table, action)); + } + + public event EventHandler TableChanged; + } + + public class NotifyTableChangedEventArgs : EventArgs + { + public TableMapping Table { get; private set; } + public NotifyTableChangedAction Action { get; private set; } + + public NotifyTableChangedEventArgs (TableMapping table, NotifyTableChangedAction action) + { + Table = table; + Action = action; + } + } + + public enum NotifyTableChangedAction + { + Insert, + Update, + Delete, + } + + /// + /// Represents a parsed connection string. + /// + public class SQLiteConnectionString + { + const string DateTimeSqliteDefaultFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff"; + + public string UniqueKey { get; } + public string DatabasePath { get; } + public bool StoreDateTimeAsTicks { get; } + public bool StoreTimeSpanAsTicks { get; } + public string DateTimeStringFormat { get; } + public System.Globalization.DateTimeStyles DateTimeStyle { get; } + public object Key { get; } + public SQLiteOpenFlags OpenFlags { get; } + public Action PreKeyAction { get; } + public Action PostKeyAction { get; } + public string VfsName { get; } + +#if NETFX_CORE + static readonly string MetroStyleDataPath = Windows.Storage.ApplicationData.Current.LocalFolder.Path; + + public static readonly string[] InMemoryDbPaths = new[] + { + ":memory:", + "file::memory:" + }; + + public static bool IsInMemoryPath(string databasePath) + { + return InMemoryDbPaths.Any(i => i.Equals(databasePath, StringComparison.OrdinalIgnoreCase)); + } + +#endif + + /// + /// Constructs a new SQLiteConnectionString with all the data needed to open an SQLiteConnection. + /// + /// + /// 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 SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks = true) + : this (databasePath, SQLiteOpenFlags.Create | SQLiteOpenFlags.ReadWrite, storeDateTimeAsTicks) + { + } + + /// + /// Constructs a new SQLiteConnectionString with all the data needed to open an SQLiteConnection. + /// + /// + /// 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. + /// + /// + /// Specifies the encryption key to use on the database. Should be a string or a byte[]. + /// + /// + /// Executes prior to setting key for SQLCipher databases + /// + /// + /// Executes after setting key for SQLCipher databases + /// + /// + /// Specifies the Virtual File System to use on the database. + /// + public SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks, object key = null, Action preKeyAction = null, Action postKeyAction = null, string vfsName = null) + : this (databasePath, SQLiteOpenFlags.Create | SQLiteOpenFlags.ReadWrite, storeDateTimeAsTicks, key, preKeyAction, postKeyAction, vfsName) + { + } + + /// + /// Constructs a new SQLiteConnectionString with all the data needed to open an SQLiteConnection. + /// + /// + /// Specifies the path to the database file. + /// + /// + /// Flags controlling how the connection should be opened. + /// + /// + /// 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. + /// + /// + /// Specifies the encryption key to use on the database. Should be a string or a byte[]. + /// + /// + /// Executes prior to setting key for SQLCipher databases + /// + /// + /// Executes after setting key for SQLCipher databases + /// + /// + /// Specifies the Virtual File System to use on the database. + /// + /// + /// Specifies the format to use when storing DateTime properties as strings. + /// + /// + /// Specifies whether to store TimeSpan 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 storeTimeSpanAsTicks = true. + /// + public SQLiteConnectionString (string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks, object key = null, Action preKeyAction = null, Action postKeyAction = null, string vfsName = null, string dateTimeStringFormat = DateTimeSqliteDefaultFormat, bool storeTimeSpanAsTicks = true) + { + if (key != null && !((key is byte[]) || (key is string))) + throw new ArgumentException ("Encryption keys must be strings or byte arrays", nameof (key)); + + UniqueKey = string.Format ("{0}_{1:X8}", databasePath, (uint)openFlags); + StoreDateTimeAsTicks = storeDateTimeAsTicks; + StoreTimeSpanAsTicks = storeTimeSpanAsTicks; + DateTimeStringFormat = dateTimeStringFormat; + DateTimeStyle = "o".Equals (DateTimeStringFormat, StringComparison.OrdinalIgnoreCase) || "r".Equals (DateTimeStringFormat, StringComparison.OrdinalIgnoreCase) ? System.Globalization.DateTimeStyles.RoundtripKind : System.Globalization.DateTimeStyles.None; + Key = key; + PreKeyAction = preKeyAction; + PostKeyAction = postKeyAction; + OpenFlags = openFlags; + VfsName = vfsName; + +#if NETFX_CORE + DatabasePath = IsInMemoryPath(databasePath) + ? databasePath + : System.IO.Path.Combine(MetroStyleDataPath, databasePath); + +#else + DatabasePath = databasePath; +#endif + } + } + + [AttributeUsage (AttributeTargets.Class)] + public class TableAttribute : Attribute + { + public string Name { get; set; } + + /// + /// Flag whether to create the table without rowid (see https://sqlite.org/withoutrowid.html) + /// + /// The default is false so that sqlite adds an implicit rowid to every table created. + /// + public bool WithoutRowId { get; set; } + + public TableAttribute (string name) + { + Name = name; + } + } + + [AttributeUsage (AttributeTargets.Property)] + public class ColumnAttribute : Attribute + { + public string Name { get; set; } + + public ColumnAttribute (string name) + { + Name = name; + } + } + + [AttributeUsage (AttributeTargets.Property)] + public class PrimaryKeyAttribute : Attribute + { + } + + [AttributeUsage (AttributeTargets.Property)] + public class AutoIncrementAttribute : Attribute + { + } + + [AttributeUsage (AttributeTargets.Property, AllowMultiple = true)] + public class IndexedAttribute : Attribute + { + public string Name { get; set; } + public int Order { get; set; } + public virtual bool Unique { get; set; } + + public IndexedAttribute () + { + } + + public IndexedAttribute (string name, int order) + { + Name = name; + Order = order; + } + } + + [AttributeUsage (AttributeTargets.Property)] + public class IgnoreAttribute : Attribute + { + } + + [AttributeUsage (AttributeTargets.Property)] + public class UniqueAttribute : IndexedAttribute + { + public override bool Unique { + get { return true; } + set { /* throw? */ } + } + } + + [AttributeUsage (AttributeTargets.Property)] + public class MaxLengthAttribute : Attribute + { + public int Value { get; private set; } + + public MaxLengthAttribute (int length) + { + Value = length; + } + } + + public sealed class PreserveAttribute : System.Attribute + { + public bool AllMembers; + public bool Conditional; + } + + /// + /// Select the collating sequence to use on a column. + /// "BINARY", "NOCASE", and "RTRIM" are supported. + /// "BINARY" is the default. + /// + [AttributeUsage (AttributeTargets.Property)] + public class CollationAttribute : Attribute + { + public string Value { get; private set; } + + public CollationAttribute (string collation) + { + Value = collation; + } + } + + [AttributeUsage (AttributeTargets.Property)] + public class NotNullAttribute : Attribute + { + } + + [AttributeUsage (AttributeTargets.Enum)] + public class StoreAsTextAttribute : Attribute + { + } + + public class TableMapping + { + public Type MappedType { get; private set; } + + public string TableName { get; private set; } + + public bool WithoutRowId { get; private set; } + + public Column[] Columns { get; private set; } + + public Column PK { get; private set; } + + public string GetByPrimaryKeySql { get; private set; } + + public CreateFlags CreateFlags { get; private set; } + + internal MapMethod Method { get; private set; } = MapMethod.ByName; + + readonly Column _autoPk; + readonly Column[] _insertColumns; + readonly Column[] _insertOrReplaceColumns; + + public TableMapping (Type type, CreateFlags createFlags = CreateFlags.None) + { + MappedType = type; + CreateFlags = createFlags; + + var typeInfo = type.GetTypeInfo (); +#if ENABLE_IL2CPP + var tableAttr = typeInfo.GetCustomAttribute (); +#else + var tableAttr = + typeInfo.CustomAttributes + .Where (x => x.AttributeType == typeof (TableAttribute)) + .Select (x => (TableAttribute)Orm.InflateAttribute (x)) + .FirstOrDefault (); +#endif + + TableName = (tableAttr != null && !string.IsNullOrEmpty (tableAttr.Name)) ? tableAttr.Name : MappedType.Name; + WithoutRowId = tableAttr != null ? tableAttr.WithoutRowId : false; + + var members = GetPublicMembers(type); + var cols = new List(members.Count); + foreach(var m in members) + { + var ignore = m.IsDefined(typeof(IgnoreAttribute), true); + if(!ignore) + cols.Add(new Column(m, createFlags)); + } + Columns = cols.ToArray (); + foreach (var c in Columns) { + if (c.IsAutoInc && c.IsPK) { + _autoPk = c; + } + if (c.IsPK) { + PK = c; + } + } + + HasAutoIncPK = _autoPk != null; + + if (PK != null) { + GetByPrimaryKeySql = string.Format ("select * from \"{0}\" where \"{1}\" = ?", TableName, PK.Name); + } + else { + // People should not be calling Get/Find without a PK + GetByPrimaryKeySql = string.Format ("select * from \"{0}\" limit 1", TableName); + } + + _insertColumns = Columns.Where (c => !c.IsAutoInc).ToArray (); + _insertOrReplaceColumns = Columns.ToArray (); + } + + private IReadOnlyCollection GetPublicMembers(Type type) + { + if(type.Name.StartsWith("ValueTuple`")) + return GetFieldsFromValueTuple(type); + + var members = new List(); + var memberNames = new HashSet(); + var newMembers = new List(); + do + { + var ti = type.GetTypeInfo(); + newMembers.Clear(); + + newMembers.AddRange( + from p in ti.DeclaredProperties + where !memberNames.Contains(p.Name) && + p.CanRead && p.CanWrite && + p.GetMethod != null && p.SetMethod != null && + p.GetMethod.IsPublic && p.SetMethod.IsPublic && + !p.GetMethod.IsStatic && !p.SetMethod.IsStatic + select p); + + members.AddRange(newMembers); + foreach(var m in newMembers) + memberNames.Add(m.Name); + + type = ti.BaseType; + } + while(type != typeof(object)); + + return members; + } + + private IReadOnlyCollection GetFieldsFromValueTuple(Type type) + { + Method = MapMethod.ByPosition; + var fields = type.GetFields(); + + // https://docs.microsoft.com/en-us/dotnet/api/system.valuetuple-8.rest + if(fields.Length >= 8) + throw new NotSupportedException("ValueTuple with more than 7 members not supported due to nesting; see https://docs.microsoft.com/en-us/dotnet/api/system.valuetuple-8.rest"); + + return fields; + } + + public bool HasAutoIncPK { get; private set; } + + public void SetAutoIncPK (object obj, long id) + { + if (_autoPk != null) { + _autoPk.SetValue (obj, Convert.ChangeType (id, _autoPk.ColumnType, null)); + } + } + + public Column[] InsertColumns { + get { + return _insertColumns; + } + } + + public Column[] InsertOrReplaceColumns { + get { + return _insertOrReplaceColumns; + } + } + + public Column FindColumnWithPropertyName (string propertyName) + { + var exact = Columns.FirstOrDefault (c => c.PropertyName == propertyName); + return exact; + } + + public Column FindColumn (string columnName) + { + if(Method != MapMethod.ByName) + throw new InvalidOperationException($"This {nameof(TableMapping)} is not mapped by name, but {Method}."); + + var exact = Columns.FirstOrDefault (c => c.Name.ToLower () == columnName.ToLower ()); + return exact; + } + + public class Column + { + MemberInfo _member; + + public string Name { get; private set; } + + public PropertyInfo PropertyInfo => _member as PropertyInfo; + + public string PropertyName { get { return _member.Name; } } + + public Type ColumnType { get; private set; } + + public string Collation { get; private set; } + + public bool IsAutoInc { get; private set; } + public bool IsAutoGuid { get; private set; } + + public bool IsPK { get; private set; } + + public IEnumerable Indices { get; set; } + + public bool IsNullable { get; private set; } + + public int? MaxStringLength { get; private set; } + + public bool StoreAsText { get; private set; } + + public Column (MemberInfo member, CreateFlags createFlags = CreateFlags.None) + { + _member = member; + var memberType = GetMemberType(member); + + var colAttr = member.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (ColumnAttribute)); +#if ENABLE_IL2CPP + var ca = member.GetCustomAttribute(typeof(ColumnAttribute)) as ColumnAttribute; + Name = ca == null ? member.Name : ca.Name; +#else + Name = (colAttr != null && colAttr.ConstructorArguments.Count > 0) ? + colAttr.ConstructorArguments[0].Value?.ToString () : + member.Name; +#endif + //If this type is Nullable then Nullable.GetUnderlyingType returns the T, otherwise it returns null, so get the actual type instead + ColumnType = Nullable.GetUnderlyingType (memberType) ?? memberType; + Collation = Orm.Collation (member); + + IsPK = Orm.IsPK (member) || + (((createFlags & CreateFlags.ImplicitPK) == CreateFlags.ImplicitPK) && + string.Compare (member.Name, Orm.ImplicitPkName, StringComparison.OrdinalIgnoreCase) == 0); + + var isAuto = Orm.IsAutoInc (member) || (IsPK && ((createFlags & CreateFlags.AutoIncPK) == CreateFlags.AutoIncPK)); + IsAutoGuid = isAuto && ColumnType == typeof (Guid); + IsAutoInc = isAuto && !IsAutoGuid; + + Indices = Orm.GetIndices (member); + if (!Indices.Any () + && !IsPK + && ((createFlags & CreateFlags.ImplicitIndex) == CreateFlags.ImplicitIndex) + && Name.EndsWith (Orm.ImplicitIndexSuffix, StringComparison.OrdinalIgnoreCase) + ) { + Indices = new IndexedAttribute[] { new IndexedAttribute () }; + } + IsNullable = !(IsPK || Orm.IsMarkedNotNull (member)); + MaxStringLength = Orm.MaxStringLength (member); + + StoreAsText = memberType.GetTypeInfo ().CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute)); + } + + public Column (PropertyInfo member, CreateFlags createFlags = CreateFlags.None) + : this((MemberInfo)member, createFlags) + { } + + public void SetValue (object obj, object val) + { + if(_member is PropertyInfo propy) + { + if (val != null && ColumnType.GetTypeInfo ().IsEnum) + propy.SetValue (obj, Enum.ToObject (ColumnType, val)); + else + propy.SetValue (obj, val); + } + else if(_member is FieldInfo field) + { + if (val != null && ColumnType.GetTypeInfo ().IsEnum) + field.SetValue (obj, Enum.ToObject (ColumnType, val)); + else + field.SetValue (obj, val); + } + else + throw new InvalidProgramException("unreachable condition"); + } + + public object GetValue (object obj) + { + if(_member is PropertyInfo propy) + return propy.GetValue(obj); + else if(_member is FieldInfo field) + return field.GetValue(obj); + else + throw new InvalidProgramException("unreachable condition"); + } + + private static Type GetMemberType(MemberInfo m) + { + switch(m.MemberType) + { + case MemberTypes.Property: return ((PropertyInfo)m).PropertyType; + case MemberTypes.Field: return ((FieldInfo)m).FieldType; + default: throw new InvalidProgramException($"{nameof(TableMapping)} supports properties or fields only."); + } + } + } + + internal enum MapMethod + { + ByName, + ByPosition + } + } + + class EnumCacheInfo + { + public EnumCacheInfo (Type type) + { + var typeInfo = type.GetTypeInfo (); + + IsEnum = typeInfo.IsEnum; + + if (IsEnum) { + StoreAsText = typeInfo.CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute)); + + if (StoreAsText) { + EnumValues = new Dictionary (); +#if NET8_0_OR_GREATER + foreach (object e in Enum.GetValuesAsUnderlyingType (type)) { + EnumValues[Convert.ToInt32 (e)] = Enum.ToObject(type, e).ToString (); + } +#else + foreach (object e in Enum.GetValues (type)) { + EnumValues[Convert.ToInt32 (e)] = e.ToString (); + } +#endif + } + } + } + + public bool IsEnum { get; private set; } + + public bool StoreAsText { get; private set; } + + public Dictionary EnumValues { get; private set; } + } + + static class EnumCache + { + static readonly Dictionary Cache = new Dictionary (); + + public static EnumCacheInfo GetInfo () + { + return GetInfo (typeof (T)); + } + + public static EnumCacheInfo GetInfo (Type type) + { + lock (Cache) { + EnumCacheInfo info = null; + if (!Cache.TryGetValue (type, out info)) { + info = new EnumCacheInfo (type); + Cache[type] = info; + } + + return info; + } + } + } + + public static class Orm + { + public const int DefaultMaxStringLength = 140; + public const string ImplicitPkName = "Id"; + public const string ImplicitIndexSuffix = "Id"; + + public static Type GetType (object obj) + { + if (obj == null) + return typeof (object); + var rt = obj as IReflectableType; + if (rt != null) + return rt.GetTypeInfo ().AsType (); + return obj.GetType (); + } + + public static string SqlDecl (TableMapping.Column p, bool storeDateTimeAsTicks, bool storeTimeSpanAsTicks) + { + string decl = "\"" + p.Name + "\" " + SqlType (p, storeDateTimeAsTicks, storeTimeSpanAsTicks) + " "; + + if (p.IsPK) { + decl += "primary key "; + } + if (p.IsAutoInc) { + decl += "autoincrement "; + } + if (!p.IsNullable) { + decl += "not null "; + } + if (!string.IsNullOrEmpty (p.Collation)) { + decl += "collate " + p.Collation + " "; + } + + return decl; + } + + public static string SqlType (TableMapping.Column p, bool storeDateTimeAsTicks, bool storeTimeSpanAsTicks) + { + var clrType = p.ColumnType; + if (clrType == typeof (Boolean) || clrType == typeof (Byte) || clrType == typeof (UInt16) || clrType == typeof (SByte) || clrType == typeof (Int16) || clrType == typeof (Int32) || clrType == typeof (UInt32) || clrType == typeof (Int64) || clrType == typeof (UInt64)) { + return "integer"; + } + else if (clrType == typeof (Single) || clrType == typeof (Double) || clrType == typeof (Decimal)) { + return "float"; + } + else if (clrType == typeof (String) || clrType == typeof (StringBuilder) || clrType == typeof (Uri) || clrType == typeof (UriBuilder)) { + int? len = p.MaxStringLength; + + if (len.HasValue) + return "varchar(" + len.Value + ")"; + + return "varchar"; + } + else if (clrType == typeof (TimeSpan)) { + return storeTimeSpanAsTicks ? "bigint" : "time"; + } + else if (clrType == typeof (DateTime)) { + return storeDateTimeAsTicks ? "bigint" : "datetime"; + } + else if (clrType == typeof (DateTimeOffset)) { + return "bigint"; + } + else if (clrType.GetTypeInfo ().IsEnum) { + if (p.StoreAsText) + return "varchar"; + else + return "integer"; + } + else if (clrType == typeof (byte[])) { + return "blob"; + } + else if (clrType == typeof (Guid)) { + return "varchar(36)"; + } + else { + throw new NotSupportedException ("Don't know about " + clrType); + } + } + + public static bool IsPK (MemberInfo p) + { + return p.CustomAttributes.Any (x => x.AttributeType == typeof (PrimaryKeyAttribute)); + } + + public static string Collation (MemberInfo p) + { +#if ENABLE_IL2CPP + return (p.GetCustomAttribute ()?.Value) ?? ""; +#else + return + (p.CustomAttributes + .Where (x => typeof (CollationAttribute) == x.AttributeType) + .Select (x => { + var args = x.ConstructorArguments; + return args.Count > 0 ? ((args[0].Value as string) ?? "") : ""; + }) + .FirstOrDefault ()) ?? ""; +#endif + } + + public static bool IsAutoInc (MemberInfo p) + { + return p.CustomAttributes.Any (x => x.AttributeType == typeof (AutoIncrementAttribute)); + } + + public static FieldInfo GetField (TypeInfo t, string name) + { + var f = t.GetDeclaredField (name); + if (f != null) + return f; + return GetField (t.BaseType.GetTypeInfo (), name); + } + + public static PropertyInfo GetProperty (TypeInfo t, string name) + { + var f = t.GetDeclaredProperty (name); + if (f != null) + return f; + return GetProperty (t.BaseType.GetTypeInfo (), name); + } + + public static object InflateAttribute (CustomAttributeData x) + { + var atype = x.AttributeType; + var typeInfo = atype.GetTypeInfo (); +#if ENABLE_IL2CPP + var r = Activator.CreateInstance (x.AttributeType); +#else + var args = x.ConstructorArguments.Select (a => a.Value).ToArray (); + var r = Activator.CreateInstance (x.AttributeType, args); + foreach (var arg in x.NamedArguments) { + if (arg.IsField) { + GetField (typeInfo, arg.MemberName).SetValue (r, arg.TypedValue.Value); + } + else { + GetProperty (typeInfo, arg.MemberName).SetValue (r, arg.TypedValue.Value); + } + } +#endif + return r; + } + + public static IEnumerable GetIndices (MemberInfo p) + { +#if ENABLE_IL2CPP + return p.GetCustomAttributes (); +#else + var indexedInfo = typeof (IndexedAttribute).GetTypeInfo (); + return + p.CustomAttributes + .Where (x => indexedInfo.IsAssignableFrom (x.AttributeType.GetTypeInfo ())) + .Select (x => (IndexedAttribute)InflateAttribute (x)); +#endif + } + + public static int? MaxStringLength (MemberInfo p) + { +#if ENABLE_IL2CPP + return p.GetCustomAttribute ()?.Value; +#else + var attr = p.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (MaxLengthAttribute)); + if (attr != null) { + var attrv = (MaxLengthAttribute)InflateAttribute (attr); + return attrv.Value; + } + return null; +#endif + } + + public static int? MaxStringLength (PropertyInfo p) => MaxStringLength((MemberInfo)p); + + public static bool IsMarkedNotNull (MemberInfo p) + { + return p.CustomAttributes.Any (x => x.AttributeType == typeof (NotNullAttribute)); + } + } + + public partial class SQLiteCommand + { + SQLiteConnection _conn; + private List _bindings; + + public string CommandText { get; set; } + + public SQLiteCommand (SQLiteConnection conn) + { + _conn = conn; + _bindings = new List (); + CommandText = ""; + } + + public int ExecuteNonQuery () + { + if (_conn.Trace) { + _conn.Tracer?.Invoke ("Executing: " + this); + } + + var r = SQLite3.Result.OK; + var stmt = Prepare (); + r = SQLite3.Step (stmt); + Finalize (stmt); + if (r == SQLite3.Result.Done) { + int rowsAffected = SQLite3.Changes (_conn.Handle); + return rowsAffected; + } + else if (r == SQLite3.Result.Error) { + string msg = SQLite3.GetErrmsg (_conn.Handle); + throw SQLiteException.New (r, msg); + } + else if (r == SQLite3.Result.Constraint) { + if (SQLite3.ExtendedErrCode (_conn.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { + throw NotNullConstraintViolationException.New (r, SQLite3.GetErrmsg (_conn.Handle)); + } + } + + throw SQLiteException.New (r, SQLite3.GetErrmsg (_conn.Handle)); + } + + public IEnumerable ExecuteDeferredQuery () + { + return ExecuteDeferredQuery (_conn.GetMapping (typeof (T))); + } + + public List ExecuteQuery () + { + return ExecuteDeferredQuery (_conn.GetMapping (typeof (T))).ToList (); + } + + public List ExecuteQuery (TableMapping map) + { + return ExecuteDeferredQuery (map).ToList (); + } + + /// + /// Invoked every time an instance is loaded from the database. + /// + /// + /// The newly created object. + /// + /// + /// This can be overridden in combination with the + /// method to hook into the life-cycle of objects. + /// + protected virtual void OnInstanceCreated (object obj) + { + // Can be overridden. + } + + public IEnumerable ExecuteDeferredQuery (TableMapping map) + { + if (_conn.Trace) { + _conn.Tracer?.Invoke ("Executing Query: " + this); + } + + var stmt = Prepare (); + try { + var cols = new TableMapping.Column[SQLite3.ColumnCount (stmt)]; + var fastColumnSetters = new Action[SQLite3.ColumnCount (stmt)]; + + if (map.Method == TableMapping.MapMethod.ByPosition) + { + Array.Copy(map.Columns, cols, Math.Min(cols.Length, map.Columns.Length)); + } + else if (map.Method == TableMapping.MapMethod.ByName) { + MethodInfo getSetter = null; + if (typeof(T) != map.MappedType) { + getSetter = typeof(FastColumnSetter) + .GetMethod (nameof(FastColumnSetter.GetFastSetter), + BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod (map.MappedType); + } + + for (int i = 0; i < cols.Length; i++) { + var name = SQLite3.ColumnName16 (stmt, i); + cols[i] = map.FindColumn (name); + if (cols[i] != null) + if (getSetter != null) { + fastColumnSetters[i] = (Action)getSetter.Invoke(null, new object[]{ _conn, cols[i]}); + } + else { + fastColumnSetters[i] = FastColumnSetter.GetFastSetter(_conn, cols[i]); + } + } + } + + while (SQLite3.Step (stmt) == SQLite3.Result.Row) { + var obj = Activator.CreateInstance (map.MappedType); + for (int i = 0; i < cols.Length; i++) { + if (cols[i] == null) + continue; + + if (fastColumnSetters[i] != null) { + fastColumnSetters[i].Invoke (obj, stmt, i); + } + else { + var colType = SQLite3.ColumnType (stmt, i); + var val = ReadCol (stmt, i, colType, cols[i].ColumnType); + cols[i].SetValue (obj, val); + } + } + OnInstanceCreated (obj); + yield return (T)obj; + } + } + finally { + SQLite3.Finalize (stmt); + } + } + + public T ExecuteScalar () + { + if (_conn.Trace) { + _conn.Tracer?.Invoke ("Executing Query: " + this); + } + + T val = default (T); + + var stmt = Prepare (); + + try { + var r = SQLite3.Step (stmt); + if (r == SQLite3.Result.Row) { + var colType = SQLite3.ColumnType (stmt, 0); + var colval = ReadCol (stmt, 0, colType, typeof (T)); + if (colval != null) { + val = (T)colval; + } + } + else if (r == SQLite3.Result.Done) { + } + else { + throw SQLiteException.New (r, SQLite3.GetErrmsg (_conn.Handle)); + } + } + finally { + Finalize (stmt); + } + + return val; + } + + public IEnumerable ExecuteQueryScalars () + { + if (_conn.Trace) { + _conn.Tracer?.Invoke ("Executing Query: " + this); + } + var stmt = Prepare (); + try { + if (SQLite3.ColumnCount (stmt) < 1) { + throw new InvalidOperationException ("QueryScalars should return at least one column"); + } + while (SQLite3.Step (stmt) == SQLite3.Result.Row) { + var colType = SQLite3.ColumnType (stmt, 0); + var val = ReadCol (stmt, 0, colType, typeof (T)); + if (val == null) { + yield return default (T); + } + else { + yield return (T)val; + } + } + } + finally { + Finalize (stmt); + } + } + + public void Bind (string name, object val) + { + _bindings.Add (new Binding { + Name = name, + Value = val + }); + } + + public void Bind (object val) + { + Bind (null, val); + } + + public override string ToString () + { + var parts = new string[1 + _bindings.Count]; + parts[0] = CommandText; + var i = 1; + foreach (var b in _bindings) { + parts[i] = string.Format (" {0}: {1}", i - 1, b.Value); + i++; + } + return string.Join (Environment.NewLine, parts); + } + + Sqlite3Statement Prepare () + { + var stmt = SQLite3.Prepare2 (_conn.Handle, CommandText); + BindAll (stmt); + return stmt; + } + + void Finalize (Sqlite3Statement stmt) + { + SQLite3.Finalize (stmt); + } + + void BindAll (Sqlite3Statement stmt) + { + int nextIdx = 1; + foreach (var b in _bindings) { + if (b.Name != null) { + b.Index = SQLite3.BindParameterIndex (stmt, b.Name); + } + else { + b.Index = nextIdx++; + } + + BindParameter (stmt, b.Index, b.Value, _conn.StoreDateTimeAsTicks, _conn.DateTimeStringFormat, _conn.StoreTimeSpanAsTicks); + } + } + + static IntPtr NegativePointer = new IntPtr (-1); + + internal static void BindParameter (Sqlite3Statement stmt, int index, object value, bool storeDateTimeAsTicks, string dateTimeStringFormat, bool storeTimeSpanAsTicks) + { + if (value == null) { + SQLite3.BindNull (stmt, index); + } + else { + if (value is Int32) { + SQLite3.BindInt (stmt, index, (int)value); + } + else if (value is String) { + SQLite3.BindText (stmt, index, (string)value, -1, NegativePointer); + } + else if (value is Byte || value is UInt16 || value is SByte || value is Int16) { + SQLite3.BindInt (stmt, index, Convert.ToInt32 (value)); + } + else if (value is Boolean) { + SQLite3.BindInt (stmt, index, (bool)value ? 1 : 0); + } + else if (value is UInt32 || value is Int64 || value is UInt64) { + SQLite3.BindInt64 (stmt, index, Convert.ToInt64 (value)); + } + else if (value is Single || value is Double || value is Decimal) { + SQLite3.BindDouble (stmt, index, Convert.ToDouble (value)); + } + else if (value is TimeSpan) { + if (storeTimeSpanAsTicks) { + SQLite3.BindInt64 (stmt, index, ((TimeSpan)value).Ticks); + } + else { + SQLite3.BindText (stmt, index, ((TimeSpan)value).ToString (), -1, NegativePointer); + } + } + else if (value is DateTime) { + if (storeDateTimeAsTicks) { + SQLite3.BindInt64 (stmt, index, ((DateTime)value).Ticks); + } + else { + SQLite3.BindText (stmt, index, ((DateTime)value).ToString (dateTimeStringFormat, System.Globalization.CultureInfo.InvariantCulture), -1, NegativePointer); + } + } + else if (value is DateTimeOffset) { + SQLite3.BindInt64 (stmt, index, ((DateTimeOffset)value).UtcTicks); + } + else if (value is byte[]) { + SQLite3.BindBlob (stmt, index, (byte[])value, ((byte[])value).Length, NegativePointer); + } + else if (value is Guid) { + SQLite3.BindText (stmt, index, ((Guid)value).ToString (), 72, NegativePointer); + } + else if (value is Uri) { + SQLite3.BindText (stmt, index, ((Uri)value).ToString (), -1, NegativePointer); + } + else if (value is StringBuilder) { + SQLite3.BindText (stmt, index, ((StringBuilder)value).ToString (), -1, NegativePointer); + } + else if (value is UriBuilder) { + SQLite3.BindText (stmt, index, ((UriBuilder)value).ToString (), -1, NegativePointer); + } + else { + // Now we could possibly get an enum, retrieve cached info + var valueType = value.GetType (); + var enumInfo = EnumCache.GetInfo (valueType); + if (enumInfo.IsEnum) { + var enumIntValue = Convert.ToInt32 (value); + if (enumInfo.StoreAsText) + SQLite3.BindText (stmt, index, enumInfo.EnumValues[enumIntValue], -1, NegativePointer); + else + SQLite3.BindInt (stmt, index, enumIntValue); + } + else { + throw new NotSupportedException ("Cannot store type: " + Orm.GetType (value)); + } + } + } + } + + class Binding + { + public string Name { get; set; } + + public object Value { get; set; } + + public int Index { get; set; } + } + + object ReadCol (Sqlite3Statement stmt, int index, SQLite3.ColType type, Type clrType) + { + if (type == SQLite3.ColType.Null) { + return null; + } + else { + var clrTypeInfo = clrType.GetTypeInfo (); + if (clrTypeInfo.IsGenericType && clrTypeInfo.GetGenericTypeDefinition () == typeof (Nullable<>)) { + clrType = clrTypeInfo.GenericTypeArguments[0]; + clrTypeInfo = clrType.GetTypeInfo (); + } + + if (clrType == typeof (String)) { + return SQLite3.ColumnString (stmt, index); + } + else if (clrType == typeof (Int32)) { + return (int)SQLite3.ColumnInt (stmt, index); + } + else if (clrType == typeof (Boolean)) { + return SQLite3.ColumnInt (stmt, index) == 1; + } + else if (clrType == typeof (double)) { + return SQLite3.ColumnDouble (stmt, index); + } + else if (clrType == typeof (float)) { + return (float)SQLite3.ColumnDouble (stmt, index); + } + else if (clrType == typeof (TimeSpan)) { + if (_conn.StoreTimeSpanAsTicks) { + return new TimeSpan (SQLite3.ColumnInt64 (stmt, index)); + } + else { + var text = SQLite3.ColumnString (stmt, index); + TimeSpan resultTime; + if (!TimeSpan.TryParseExact (text, "c", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.TimeSpanStyles.None, out resultTime)) { + resultTime = TimeSpan.Parse (text); + } + return resultTime; + } + } + else if (clrType == typeof (DateTime)) { + if (_conn.StoreDateTimeAsTicks) { + return new DateTime (SQLite3.ColumnInt64 (stmt, index)); + } + else { + var text = SQLite3.ColumnString (stmt, index); + DateTime resultDate; + if (!DateTime.TryParseExact (text, _conn.DateTimeStringFormat, System.Globalization.CultureInfo.InvariantCulture, _conn.DateTimeStyle, out resultDate)) { + resultDate = DateTime.Parse (text); + } + return resultDate; + } + } + else if (clrType == typeof (DateTimeOffset)) { + return new DateTimeOffset (SQLite3.ColumnInt64 (stmt, index), TimeSpan.Zero); + } + else if (clrTypeInfo.IsEnum) { + if (type == SQLite3.ColType.Text) { + var value = SQLite3.ColumnString (stmt, index); + return Enum.Parse (clrType, value.ToString (), true); + } + else + return SQLite3.ColumnInt (stmt, index); + } + else if (clrType == typeof (Int64)) { + return SQLite3.ColumnInt64 (stmt, index); + } + else if (clrType == typeof (UInt64)) { + return (ulong)SQLite3.ColumnInt64 (stmt, index); + } + else if (clrType == typeof (UInt32)) { + return (uint)SQLite3.ColumnInt64 (stmt, index); + } + else if (clrType == typeof (decimal)) { + return (decimal)SQLite3.ColumnDouble (stmt, index); + } + else if (clrType == typeof (Byte)) { + return (byte)SQLite3.ColumnInt (stmt, index); + } + else if (clrType == typeof (UInt16)) { + return (ushort)SQLite3.ColumnInt (stmt, index); + } + else if (clrType == typeof (Int16)) { + return (short)SQLite3.ColumnInt (stmt, index); + } + else if (clrType == typeof (sbyte)) { + return (sbyte)SQLite3.ColumnInt (stmt, index); + } + else if (clrType == typeof (byte[])) { + return SQLite3.ColumnByteArray (stmt, index); + } + else if (clrType == typeof (Guid)) { + var text = SQLite3.ColumnString (stmt, index); + return new Guid (text); + } + else if (clrType == typeof (Uri)) { + var text = SQLite3.ColumnString (stmt, index); + return new Uri (text); + } + else if (clrType == typeof (StringBuilder)) { + var text = SQLite3.ColumnString (stmt, index); + return new StringBuilder (text); + } + else if (clrType == typeof (UriBuilder)) { + var text = SQLite3.ColumnString (stmt, index); + return new UriBuilder (text); + } + else { + throw new NotSupportedException ("Don't know how to read " + clrType); + } + } + } + } + + internal class FastColumnSetter + { + /// + /// Creates a delegate that can be used to quickly set object members from query columns. + /// + /// Note that this frontloads the slow reflection-based type checking for columns to only happen once at the beginning of a query, + /// and then afterwards each row of the query can invoke the delegate returned by this function to get much better performance (up to 10x speed boost, depending on query size and platform). + /// + /// The type of the destination object that the query will read into + /// The active connection. Note that this is primarily needed in order to read preferences regarding how certain data types (such as TimeSpan / DateTime) should be encoded in the database. + /// The table mapping used to map the statement column to a member of the destination object type + /// + /// A delegate for fast-setting of object members from statement columns. + /// + /// If no fast setter is available for the requested column (enums in particular cause headache), then this function returns null. + /// + internal static Action GetFastSetter (SQLiteConnection conn, TableMapping.Column column) + { + Action fastSetter = null; + + Type clrType = column.PropertyInfo.PropertyType; + + var clrTypeInfo = clrType.GetTypeInfo (); + if (clrTypeInfo.IsGenericType && clrTypeInfo.GetGenericTypeDefinition () == typeof (Nullable<>)) { + clrType = clrTypeInfo.GenericTypeArguments[0]; + clrTypeInfo = clrType.GetTypeInfo (); + } + + if (clrType == typeof (String)) { + fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { + return SQLite3.ColumnString (stmt, index); + }); + } + else if (clrType == typeof (Int32)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index)=>{ + return SQLite3.ColumnInt (stmt, index); + }); + } + else if (clrType == typeof (Boolean)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return SQLite3.ColumnInt (stmt, index) == 1; + }); + } + else if (clrType == typeof (double)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return SQLite3.ColumnDouble (stmt, index); + }); + } + else if (clrType == typeof (float)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return (float) SQLite3.ColumnDouble (stmt, index); + }); + } + else if (clrType == typeof (TimeSpan)) { + if (conn.StoreTimeSpanAsTicks) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return new TimeSpan (SQLite3.ColumnInt64 (stmt, index)); + }); + } + else { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + var text = SQLite3.ColumnString (stmt, index); + TimeSpan resultTime; + if (!TimeSpan.TryParseExact (text, "c", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.TimeSpanStyles.None, out resultTime)) { + resultTime = TimeSpan.Parse (text); + } + return resultTime; + }); + } + } + else if (clrType == typeof (DateTime)) { + if (conn.StoreDateTimeAsTicks) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return new DateTime (SQLite3.ColumnInt64 (stmt, index)); + }); + } + else { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + var text = SQLite3.ColumnString (stmt, index); + DateTime resultDate; + if (!DateTime.TryParseExact (text, conn.DateTimeStringFormat, System.Globalization.CultureInfo.InvariantCulture, conn.DateTimeStyle, out resultDate)) { + resultDate = DateTime.Parse (text); + } + return resultDate; + }); + } + } + else if (clrType == typeof (DateTimeOffset)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return new DateTimeOffset (SQLite3.ColumnInt64 (stmt, index), TimeSpan.Zero); + }); + } + else if (clrTypeInfo.IsEnum) { + // NOTE: Not sure of a good way (if any?) to do a strongly-typed fast setter like this for enumerated types -- for now, return null and column sets will revert back to the safe (but slow) Reflection-based method of column prop.Set() + } + else if (clrType == typeof (Int64)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return SQLite3.ColumnInt64 (stmt, index); + }); + } + else if (clrType == typeof(UInt64)) + { + fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { + return (ulong)SQLite3.ColumnInt64(stmt, index); + }); + } + else if (clrType == typeof (UInt32)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return (uint)SQLite3.ColumnInt64 (stmt, index); + }); + } + else if (clrType == typeof (decimal)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return (decimal)SQLite3.ColumnDouble (stmt, index); + }); + } + else if (clrType == typeof (Byte)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return (byte)SQLite3.ColumnInt (stmt, index); + }); + } + else if (clrType == typeof (UInt16)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return (ushort)SQLite3.ColumnInt (stmt, index); + }); + } + else if (clrType == typeof (Int16)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return (short)SQLite3.ColumnInt (stmt, index); + }); + } + else if (clrType == typeof (sbyte)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return (sbyte)SQLite3.ColumnInt (stmt, index); + }); + } + else if (clrType == typeof (byte[])) { + fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { + return SQLite3.ColumnByteArray (stmt, index); + }); + } + else if (clrType == typeof (Guid)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + var text = SQLite3.ColumnString (stmt, index); + return new Guid (text); + }); + } + else if (clrType == typeof (Uri)) { + fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { + var text = SQLite3.ColumnString (stmt, index); + return new Uri (text); + }); + } + else if (clrType == typeof (StringBuilder)) { + fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { + var text = SQLite3.ColumnString (stmt, index); + return new StringBuilder (text); + }); + } + else if (clrType == typeof (UriBuilder)) { + fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { + var text = SQLite3.ColumnString (stmt, index); + return new UriBuilder (text); + }); + } + else { + // NOTE: Will fall back to the slow setter method in the event that we are unable to create a fast setter delegate for a particular column type + } + return fastSetter; + } + + /// + /// This creates a strongly typed delegate that will permit fast setting of column values given a Sqlite3Statement and a column index. + /// + /// Note that this is identical to CreateTypedSetterDelegate(), but has an extra check to see if it should create a nullable version of the delegate. + /// + /// The type of the object whose member column is being set + /// The CLR type of the member in the object which corresponds to the given SQLite columnn + /// The column mapping that identifies the target member of the destination object + /// A lambda that can be used to retrieve the column value at query-time + /// A strongly-typed delegate + private static Action CreateNullableTypedSetterDelegate (TableMapping.Column column, Func getColumnValue) where ColumnMemberType : struct + { + var clrTypeInfo = column.PropertyInfo.PropertyType.GetTypeInfo(); + bool isNullable = false; + + if (clrTypeInfo.IsGenericType && clrTypeInfo.GetGenericTypeDefinition () == typeof (Nullable<>)) { + isNullable = true; + } + + if (isNullable) { + var setProperty = (Action)Delegate.CreateDelegate ( + typeof (Action), null, + column.PropertyInfo.GetSetMethod ()); + + return (o, stmt, i) => { + var colType = SQLite3.ColumnType (stmt, i); + if (colType != SQLite3.ColType.Null) + setProperty.Invoke ((ObjectType)o, getColumnValue.Invoke (stmt, i)); + }; + } + + return CreateTypedSetterDelegate (column, getColumnValue); + } + + /// + /// This creates a strongly typed delegate that will permit fast setting of column values given a Sqlite3Statement and a column index. + /// + /// The type of the object whose member column is being set + /// The CLR type of the member in the object which corresponds to the given SQLite columnn + /// The column mapping that identifies the target member of the destination object + /// A lambda that can be used to retrieve the column value at query-time + /// A strongly-typed delegate + private static Action CreateTypedSetterDelegate (TableMapping.Column column, Func getColumnValue) + { + var setProperty = (Action)Delegate.CreateDelegate ( + typeof (Action), null, + column.PropertyInfo.GetSetMethod ()); + + return (o, stmt, i) => { + var colType = SQLite3.ColumnType (stmt, i); + if (colType != SQLite3.ColType.Null) + setProperty.Invoke ((ObjectType)o, getColumnValue.Invoke (stmt, i)); + }; + } + } + + /// + /// Since the insert never changed, we only need to prepare once. + /// + class PreparedSqlLiteInsertCommand : IDisposable + { + bool Initialized; + + SQLiteConnection Connection; + + string CommandText; + + Sqlite3Statement Statement; + static readonly Sqlite3Statement NullStatement = default (Sqlite3Statement); + + public PreparedSqlLiteInsertCommand (SQLiteConnection conn, string commandText) + { + Connection = conn; + CommandText = commandText; + } + + public int ExecuteNonQuery (object[] source) + { + if (Initialized && Statement == NullStatement) { + throw new ObjectDisposedException (nameof (PreparedSqlLiteInsertCommand)); + } + + if (Connection.Trace) { + Connection.Tracer?.Invoke ("Executing: " + CommandText); + } + + var r = SQLite3.Result.OK; + + if (!Initialized) { + Statement = SQLite3.Prepare2 (Connection.Handle, CommandText); + Initialized = true; + } + + //bind the values. + if (source != null) { + for (int i = 0; i < source.Length; i++) { + SQLiteCommand.BindParameter (Statement, i + 1, source[i], Connection.StoreDateTimeAsTicks, Connection.DateTimeStringFormat, Connection.StoreTimeSpanAsTicks); + } + } + r = SQLite3.Step (Statement); + + if (r == SQLite3.Result.Done) { + int rowsAffected = SQLite3.Changes (Connection.Handle); + SQLite3.Reset (Statement); + return rowsAffected; + } + else if (r == SQLite3.Result.Error) { + string msg = SQLite3.GetErrmsg (Connection.Handle); + SQLite3.Reset (Statement); + throw SQLiteException.New (r, msg); + } + else if (r == SQLite3.Result.Constraint && SQLite3.ExtendedErrCode (Connection.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { + SQLite3.Reset (Statement); + throw NotNullConstraintViolationException.New (r, SQLite3.GetErrmsg (Connection.Handle)); + } + else { + SQLite3.Reset (Statement); + throw SQLiteException.New (r, SQLite3.GetErrmsg (Connection.Handle)); + } + } + + public void Dispose () + { + Dispose (true); + GC.SuppressFinalize (this); + } + + void Dispose (bool disposing) + { + var s = Statement; + Statement = NullStatement; + Connection = null; + if (s != NullStatement) { + SQLite3.Finalize (s); + } + } + + ~PreparedSqlLiteInsertCommand () + { + Dispose (false); + } + } + + public enum CreateTableResult + { + Created, + Migrated, + } + + public class CreateTablesResult + { + public Dictionary Results { get; private set; } + + public CreateTablesResult () + { + Results = new Dictionary (); + } + } + + public abstract class BaseTableQuery + { + protected class Ordering + { + public string ColumnName { get; set; } + public bool Ascending { get; set; } + } + } - /// - /// Constructs a new SQLiteConnection and opens a 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 SQLiteConnection (string databasePath, bool storeDateTimeAsTicks = true) - : this (new SQLiteConnectionString (databasePath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create, storeDateTimeAsTicks)) - { - } - - /// - /// Constructs a new SQLiteConnection and opens a SQLite database specified by databasePath. - /// - /// - /// Specifies the path to the database file. - /// - /// - /// Flags controlling how the connection should be opened. - /// - /// - /// 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 SQLiteConnection (string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks = true) - : this (new SQLiteConnectionString (databasePath, openFlags, storeDateTimeAsTicks)) - { - } - - /// - /// Constructs a new SQLiteConnection and opens a SQLite database specified by databasePath. - /// - /// - /// Details on how to find and open the database. - /// - public SQLiteConnection (SQLiteConnectionString connectionString) - { - if (connectionString == null) - throw new ArgumentNullException (nameof (connectionString)); - if (connectionString.DatabasePath == null) - throw new InvalidOperationException ("DatabasePath must be specified"); - - DatabasePath = connectionString.DatabasePath; - - LibVersionNumber = SQLite3.LibVersionNumber (); + public class TableQuery : BaseTableQuery, IEnumerable + { + public SQLiteConnection Connection { get; private set; } -#if NETFX_CORE - SQLite3.SetDirectory(/*temp directory type*/2, Windows.Storage.ApplicationData.Current.TemporaryFolder.Path); -#endif + public TableMapping Table { get; private set; } - Sqlite3DatabaseHandle handle; + Expression _where; + List _orderBys; + int? _limit; + int? _offset; -#if SILVERLIGHT || USE_CSHARP_SQLITE || USE_SQLITEPCL_RAW - var r = SQLite3.Open (connectionString.DatabasePath, out handle, (int)connectionString.OpenFlags, connectionString.VfsName); -#else - // open using the byte[] - // in the case where the path may include Unicode - // force open to using UTF-8 using sqlite3_open_v2 - var databasePathAsBytes = GetNullTerminatedUtf8 (connectionString.DatabasePath); - var r = SQLite3.Open (databasePathAsBytes, out handle, (int)connectionString.OpenFlags, connectionString.VfsName); -#endif + BaseTableQuery _joinInner; + Expression _joinInnerKeySelector; + BaseTableQuery _joinOuter; + Expression _joinOuterKeySelector; + Expression _joinSelector; - Handle = handle; - if (r != SQLite3.Result.OK) { - throw SQLiteException.New (r, String.Format ("Could not open database file: {0} ({1})", DatabasePath, r)); - } - _open = true; - - StoreDateTimeAsTicks = connectionString.StoreDateTimeAsTicks; - StoreTimeSpanAsTicks = connectionString.StoreTimeSpanAsTicks; - DateTimeStringFormat = connectionString.DateTimeStringFormat; - DateTimeStyle = connectionString.DateTimeStyle; - - BusyTimeout = TimeSpan.FromSeconds (1.0); - Tracer = line => Debug.WriteLine (line); - - connectionString.PreKeyAction?.Invoke (this); - if (connectionString.Key is string stringKey) { - SetKey (stringKey); - } - else if (connectionString.Key is byte[] bytesKey) { - SetKey (bytesKey); - } - else if (connectionString.Key != null) { - throw new InvalidOperationException ("Encryption keys must be strings or byte arrays"); - } - connectionString.PostKeyAction?.Invoke (this); - } - - /// - /// 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 void EnableWriteAheadLogging () - { - ExecuteScalar ("PRAGMA journal_mode=WAL"); - } - - /// - /// Convert an input string to a quoted SQL string that can be safely used in queries. - /// - /// The quoted string. - /// The unsafe string to quote. - static string Quote (string unsafeString) - { - // TODO: Doesn't call sqlite3_mprintf("%Q", u) because we're waiting on https://github.com/ericsink/SQLitePCL.raw/issues/153 - if (unsafeString == null) - return "NULL"; - var safe = unsafeString.Replace ("'", "''"); - return "'" + safe + "'"; - } - - /// - /// Sets the key used to encrypt/decrypt the database with "pragma key = ...". - /// This must be the first thing you call before doing anything else with this connection - /// if your database is encrypted. - /// This only has an effect if you are using the SQLCipher nuget package. - /// - /// Encryption key plain text that is converted to the real encryption key using PBKDF2 key derivation - void SetKey (string key) - { - if (key == null) - throw new ArgumentNullException (nameof (key)); - var q = Quote (key); - ExecuteScalar ("pragma key = " + q); - } - - /// - /// Sets the key used to encrypt/decrypt the database. - /// This must be the first thing you call before doing anything else with this connection - /// if your database is encrypted. - /// This only has an effect if you are using the SQLCipher nuget package. - /// - /// 256-bit (32 byte) encryption key data - void SetKey (byte[] key) - { - if (key == null) - throw new ArgumentNullException (nameof (key)); - if (key.Length != 32 && key.Length != 48) - throw new ArgumentException ("Key must be 32 bytes (256-bit) or 48 bytes (384-bit)", nameof (key)); - var s = String.Join ("", key.Select (x => x.ToString ("X2"))); - ExecuteScalar ("pragma key = \"x'" + s + "'\""); - } - - /// - /// Change the encryption key for a SQLCipher database with "pragma rekey = ...". - /// - /// Encryption key plain text that is converted to the real encryption key using PBKDF2 key derivation - public void ReKey (string key) - { - if (key == null) - throw new ArgumentNullException(nameof(key)); - var q = Quote(key); - ExecuteScalar("pragma rekey = " + q); - } - - /// - /// Change the encryption key for a SQLCipher database. - /// - /// 256-bit (32 byte) or 384-bit (48 bytes) encryption key data - public void ReKey (byte[] key) - { - if (key == null) - throw new ArgumentNullException(nameof(key)); - if (key.Length != 32 && key.Length != 48) - throw new ArgumentException ("Key must be 32 bytes (256-bit) or 48 bytes (384-bit)", nameof (key)); - var s = String.Join("", key.Select(x => x.ToString("X2"))); - ExecuteScalar("pragma rekey = \"x'" + s + "'\""); - } - - /// - /// Enable or disable extension loading. - /// - public void EnableLoadExtension (bool enabled) - { - SQLite3.Result r = SQLite3.EnableLoadExtension (Handle, enabled ? 1 : 0); - if (r != SQLite3.Result.OK) { - string msg = SQLite3.GetErrmsg (Handle); - throw SQLiteException.New (r, msg); - } - } + Expression _selector; -#if !USE_SQLITEPCL_RAW - static byte[] GetNullTerminatedUtf8 (string s) - { - var utf8Length = System.Text.Encoding.UTF8.GetByteCount (s); - var bytes = new byte [utf8Length + 1]; - utf8Length = System.Text.Encoding.UTF8.GetBytes(s, 0, s.Length, bytes, 0); - return bytes; - } -#endif + TableQuery (SQLiteConnection conn, TableMapping table) + { + Connection = conn; + Table = table; + } - /// - /// Sets a busy handler to sleep the specified amount of time when a table is locked. - /// The handler will sleep multiple times until a total time of has accumulated. - /// - public TimeSpan BusyTimeout { - get { return _busyTimeout; } - set { - _busyTimeout = value; - if (Handle != NullHandle) { - SQLite3.BusyTimeout (Handle, (int)_busyTimeout.TotalMilliseconds); - } - } - } - - /// - /// Returns the mappings from types to tables that the connection - /// currently understands. - /// - public IEnumerable TableMappings { - get { - lock (_mappings) { - return new List (_mappings.Values); - } - } - } - - /// - /// 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 TableMapping GetMapping (Type type, CreateFlags createFlags = CreateFlags.None) - { - TableMapping map; - var key = type.FullName; - lock (_mappings) { - if (_mappings.TryGetValue (key, out map)) { - if (createFlags != CreateFlags.None && createFlags != map.CreateFlags) { - map = new TableMapping (type, createFlags); - _mappings[key] = map; - } - } - else { - map = new TableMapping (type, createFlags); - _mappings.Add (key, map); - } - } - return map; - } - - /// - /// 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 TableMapping GetMapping (CreateFlags createFlags = CreateFlags.None) - { - return GetMapping (typeof (T), createFlags); - } - - private struct IndexedColumn - { - public int Order; - public string ColumnName; - } - - private struct IndexInfo - { - public string IndexName; - public string TableName; - public bool Unique; - public List Columns; - } - - /// - /// Executes a "drop table" on the database. This is non-recoverable. - /// - public int DropTable () - { - return DropTable (GetMapping (typeof (T))); - } - - /// - /// Executes a "drop table" on the database. This is non-recoverable. - /// - /// - /// The TableMapping used to identify the table. - /// - public int DropTable (TableMapping map) - { - var query = string.Format ("drop table if exists \"{0}\"", map.TableName); - return Execute (query); - } - - /// - /// 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 CreateTableResult CreateTable (CreateFlags createFlags = CreateFlags.None) - { - return CreateTable (typeof (T), 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 CreateTableResult CreateTable (Type ty, CreateFlags createFlags = CreateFlags.None) - { - var map = GetMapping (ty, createFlags); - - // Present a nice error if no columns specified - if (map.Columns.Length == 0) { - throw new Exception (string.Format ("Cannot create a table without columns (does '{0}' have public properties?)", ty.FullName)); - } - - // Check if the table exists - var result = CreateTableResult.Created; - var existingCols = GetTableInfo (map.TableName); - - // Create or migrate it - if (existingCols.Count == 0) { - - // Facilitate virtual tables a.k.a. full-text search. - bool fts3 = (createFlags & CreateFlags.FullTextSearch3) != 0; - bool fts4 = (createFlags & CreateFlags.FullTextSearch4) != 0; - bool fts = fts3 || fts4; - var @virtual = fts ? "virtual " : string.Empty; - var @using = fts3 ? "using fts3 " : fts4 ? "using fts4 " : string.Empty; - - // Build query. - var query = "create " + @virtual + "table if not exists \"" + map.TableName + "\" " + @using + "(\n"; - var decls = map.Columns.Select (p => Orm.SqlDecl (p, StoreDateTimeAsTicks, StoreTimeSpanAsTicks)); - var decl = string.Join (",\n", decls.ToArray ()); - query += decl; - query += ")"; - if (map.WithoutRowId) { - query += " without rowid"; - } - - Execute (query); - } - else { - result = CreateTableResult.Migrated; - MigrateTable (map, existingCols); - } - - var indexes = new Dictionary (); - foreach (var c in map.Columns) { - foreach (var i in c.Indices) { - var iname = i.Name ?? map.TableName + "_" + c.Name; - IndexInfo iinfo; - if (!indexes.TryGetValue (iname, out iinfo)) { - iinfo = new IndexInfo { - IndexName = iname, - TableName = map.TableName, - Unique = i.Unique, - Columns = new List () - }; - indexes.Add (iname, iinfo); - } - - if (i.Unique != iinfo.Unique) - throw new Exception ("All the columns in an index must have the same value for their Unique property"); - - iinfo.Columns.Add (new IndexedColumn { - Order = i.Order, - ColumnName = c.Name - }); - } - } - - foreach (var indexName in indexes.Keys) { - var index = indexes[indexName]; - var columns = index.Columns.OrderBy (i => i.Order).Select (i => i.ColumnName).ToArray (); - CreateIndex (indexName, index.TableName, columns, index.Unique); - } - - return result; - } - - /// - /// 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 CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) - where T : new() - where T2 : new() - { - return CreateTables (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 CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) - where T : new() - where T2 : new() - where T3 : new() - { - return CreateTables (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 CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) - where T : new() - where T2 : new() - where T3 : new() - where T4 : new() - { - return CreateTables (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 CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) - where T : new() - where T2 : new() - where T3 : new() - where T4 : new() - where T5 : new() - { - return CreateTables (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 CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None, params Type[] types) - { - var result = new CreateTablesResult (); - foreach (Type type in types) { - var aResult = CreateTable (type, createFlags); - result.Results[type] = aResult; - } - return result; - } - - /// - /// 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 int CreateIndex (string indexName, string tableName, string[] columnNames, bool unique = false) - { - const string sqlFormat = "create {2} index if not exists \"{3}\" on \"{0}\"(\"{1}\")"; - var sql = String.Format (sqlFormat, tableName, string.Join ("\", \"", columnNames), unique ? "unique" : "", indexName); - return Execute (sql); - } - - /// - /// 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 int CreateIndex (string indexName, string tableName, string columnName, bool unique = false) - { - return CreateIndex (indexName, tableName, new string[] { columnName }, unique); - } - - /// - /// 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 int CreateIndex (string tableName, string columnName, bool unique = false) - { - return CreateIndex (tableName + "_" + columnName, 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 int CreateIndex (string tableName, string[] columnNames, bool unique = false) - { - return CreateIndex (tableName + "_" + string.Join ("_", columnNames), 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 int CreateIndex (Expression> property, bool unique = false) - { - MemberExpression mx; - if (property.Body.NodeType == ExpressionType.Convert) { - mx = ((UnaryExpression)property.Body).Operand as MemberExpression; - } - else { - mx = (property.Body as MemberExpression); - } - var propertyInfo = mx.Member as PropertyInfo; - if (propertyInfo == null) { - throw new ArgumentException ("The lambda expression 'property' should point to a valid Property"); - } - - var propName = propertyInfo.Name; - - var map = GetMapping (); - var colName = map.FindColumnWithPropertyName (propName).Name; - - return CreateIndex (map.TableName, colName, unique); - } - - [Preserve (AllMembers = true)] - public class ColumnInfo - { - // public int cid { get; set; } - - [Column ("name")] - public string Name { get; set; } - - // [Column ("type")] - // public string ColumnType { get; set; } - - public int notnull { get; set; } - - // public string dflt_value { get; set; } - - // public int pk { get; set; } - - public override string ToString () - { - return Name; - } - } - - /// - /// Query the built-in sqlite table_info table for a specific tables columns. - /// - /// The columns contains in the table. - /// Table name. - public List GetTableInfo (string tableName) - { - var query = "pragma table_info(\"" + tableName + "\")"; - return Query (query); - } - - void MigrateTable (TableMapping map, List existingCols) - { - var toBeAdded = new List (); - - foreach (var p in map.Columns) { - var found = false; - foreach (var c in existingCols) { - found = (string.Compare (p.Name, c.Name, StringComparison.OrdinalIgnoreCase) == 0); - if (found) - break; - } - if (!found) { - toBeAdded.Add (p); - } - } - - foreach (var p in toBeAdded) { - var addCol = "alter table \"" + map.TableName + "\" add column " + Orm.SqlDecl (p, StoreDateTimeAsTicks, StoreTimeSpanAsTicks); - Execute (addCol); - } - } - - /// - /// Creates a new SQLiteCommand. Can be overridden to provide a sub-class. - /// - /// - protected virtual SQLiteCommand NewCommand () - { - return new SQLiteCommand (this); - } - - /// - /// Creates a new SQLiteCommand given the command text with arguments. Place a '?' - /// in the command text for each of the arguments. - /// - /// - /// The fully escaped SQL. - /// - /// - /// Arguments to substitute for the occurences of '?' in the command text. - /// - /// - /// A - /// - public SQLiteCommand CreateCommand (string cmdText, params object[] ps) - { - if (!_open) - throw SQLiteException.New (SQLite3.Result.Error, "Cannot create commands from unopened database"); - - var cmd = NewCommand (); - cmd.CommandText = cmdText; - foreach (var o in ps) { - cmd.Bind (o); - } - return cmd; - } - - /// - /// Creates a new SQLiteCommand given the command text with named arguments. Place a "[@:$]VVV" - /// in the command text for each of the arguments. VVV represents an alphanumeric identifier. - /// For example, @name :name and $name can all be used in the query. - /// - /// - /// The fully escaped SQL. - /// - /// - /// Arguments to substitute for the occurences of "[@:$]VVV" in the command text. - /// - /// - /// A - /// - public SQLiteCommand CreateCommand (string cmdText, Dictionary args) - { - if (!_open) - throw SQLiteException.New (SQLite3.Result.Error, "Cannot create commands from unopened database"); - - SQLiteCommand cmd = NewCommand (); - cmd.CommandText = cmdText; - foreach (var kv in args) { - cmd.Bind (kv.Key, kv.Value); - } - return cmd; - } - - /// - /// 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 int Execute (string query, params object[] args) - { - var cmd = CreateCommand (query, args); - - if (TimeExecution) { - if (_sw == null) { - _sw = new Stopwatch (); - } - _sw.Reset (); - _sw.Start (); - } - - var r = cmd.ExecuteNonQuery (); - - if (TimeExecution) { - _sw.Stop (); - _elapsedMilliseconds += _sw.ElapsedMilliseconds; - Tracer?.Invoke (string.Format ("Finished in {0} ms ({1:0.0} s total)", _sw.ElapsedMilliseconds, _elapsedMilliseconds / 1000.0)); - } - - return r; - } - - /// - /// 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 T ExecuteScalar (string query, params object[] args) - { - var cmd = CreateCommand (query, args); - - if (TimeExecution) { - if (_sw == null) { - _sw = new Stopwatch (); - } - _sw.Reset (); - _sw.Start (); - } - - var r = cmd.ExecuteScalar (); - - if (TimeExecution) { - _sw.Stop (); - _elapsedMilliseconds += _sw.ElapsedMilliseconds; - Tracer?.Invoke (string.Format ("Finished in {0} ms ({1:0.0} s total)", _sw.ElapsedMilliseconds, _elapsedMilliseconds / 1000.0)); - } - - return r; - } - - /// - /// 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. - /// - public List Query (string query, params object[] args) where T : new() - { - var cmd = CreateCommand (query, args); - return cmd.ExecuteQuery (); - } - - /// - /// 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. - /// - /// - /// An enumerable with one result for the first column of each row returned by the query. - /// - public List QueryScalars (string query, params object[] args) - { - var cmd = CreateCommand (query, args); - return cmd.ExecuteQueryScalars ().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 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 (retrieved by calling GetEnumerator() on the result of this method) - /// will call sqlite3_step on each call to MoveNext, so the database - /// connection must remain open for the lifetime of the enumerator. - /// - public IEnumerable DeferredQuery (string query, params object[] args) where T : new() - { - var cmd = CreateCommand (query, args); - return cmd.ExecuteDeferredQuery (); - } - - /// - /// 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 List Query (TableMapping map, string query, params object[] args) - { - var cmd = CreateCommand (query, args); - return cmd.ExecuteQuery (map); - } - - /// - /// 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 (retrieved by calling GetEnumerator() on the result of this method) - /// will call sqlite3_step on each call to MoveNext, so the database - /// connection must remain open for the lifetime of the enumerator. - /// - public IEnumerable DeferredQuery (TableMapping map, string query, params object[] args) - { - var cmd = CreateCommand (query, args); - return cmd.ExecuteDeferredQuery (map); - } - - /// - /// 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 TableQuery Table () where T : new() - { - return new TableQuery (this); - } - - /// - /// 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 T Get (object pk) where T : new() - { - var map = GetMapping (typeof (T)); - return Query (map.GetByPrimaryKeySql, pk).First (); - } - - /// - /// 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 object Get (object pk, TableMapping map) - { - return Query (map, map.GetByPrimaryKeySql, pk).First (); - } - - /// - /// 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 T Get (Expression> predicate) where T : new() - { - return Table ().Where (predicate).First (); - } - - /// - /// 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 T Find (object pk) where T : new() - { - var map = GetMapping (typeof (T)); - return Query (map.GetByPrimaryKeySql, pk).FirstOrDefault (); - } - - /// - /// 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 object Find (object pk, TableMapping map) - { - return Query (map, map.GetByPrimaryKeySql, pk).FirstOrDefault (); - } - - /// - /// 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 T Find (Expression> predicate) where T : new() - { - return Table ().Where (predicate).FirstOrDefault (); - } - - /// - /// 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 T FindWithQuery (string query, params object[] args) where T : new() - { - return Query (query, args).FirstOrDefault (); - } - - /// - /// 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 object FindWithQuery (TableMapping map, string query, params object[] args) - { - return Query (map, query, args).FirstOrDefault (); - } - - /// - /// Whether has been called and the database is waiting for a . - /// - public bool IsInTransaction { - get { return _transactionDepth > 0; } - } - - /// - /// Begins a new transaction. Call to end the transaction. - /// - /// Throws if a transaction has already begun. - public void BeginTransaction () - { - // The BEGIN command only works if the transaction stack is empty, - // or in other words if there are no pending transactions. - // If the transaction stack is not empty when the BEGIN command is invoked, - // then the command fails with an error. - // Rather than crash with an error, we will just ignore calls to BeginTransaction - // that would result in an error. - if (Interlocked.CompareExchange (ref _transactionDepth, 1, 0) == 0) { - try { - Execute ("begin transaction"); - } - catch (Exception ex) { - var sqlExp = ex as SQLiteException; - if (sqlExp != null) { - // It is recommended that applications respond to the errors listed below - // by explicitly issuing a ROLLBACK command. - // TODO: This rollback failsafe should be localized to all throw sites. - switch (sqlExp.Result) { - case SQLite3.Result.IOError: - case SQLite3.Result.Full: - case SQLite3.Result.Busy: - case SQLite3.Result.NoMem: - case SQLite3.Result.Interrupt: - RollbackTo (null, true); - break; - } - } - else { - // Call decrement and not VolatileWrite in case we've already - // created a transaction point in SaveTransactionPoint since the catch. - Interlocked.Decrement (ref _transactionDepth); - } - - throw; - } - } - else { - // Calling BeginTransaction on an already open transaction is invalid - throw new InvalidOperationException ("Cannot begin a transaction while already in a transaction."); - } - } - - /// - /// Creates a savepoint in the database at the current point in the transaction timeline. - /// Begins a new transaction if one is not in progress. - /// - /// Call to undo transactions since the returned savepoint. - /// Call to commit transactions after the savepoint returned here. - /// Call to end the transaction, committing all changes. - /// - /// A string naming the savepoint. - public string SaveTransactionPoint () - { - int depth = Interlocked.Increment (ref _transactionDepth) - 1; - string retVal = "S" + _rand.Next (short.MaxValue) + "D" + depth; - - try { - Execute ("savepoint " + retVal); - } - catch (Exception ex) { - var sqlExp = ex as SQLiteException; - if (sqlExp != null) { - // It is recommended that applications respond to the errors listed below - // by explicitly issuing a ROLLBACK command. - // TODO: This rollback failsafe should be localized to all throw sites. - switch (sqlExp.Result) { - case SQLite3.Result.IOError: - case SQLite3.Result.Full: - case SQLite3.Result.Busy: - case SQLite3.Result.NoMem: - case SQLite3.Result.Interrupt: - RollbackTo (null, true); - break; - } - } - else { - Interlocked.Decrement (ref _transactionDepth); - } - - throw; - } - - return retVal; - } - - /// - /// Rolls back the transaction that was begun by or . - /// - public void Rollback () - { - RollbackTo (null, false); - } - - /// - /// Rolls back the savepoint created by or SaveTransactionPoint. - /// - /// The name of the savepoint to roll back to, as returned by . If savepoint is null or empty, this method is equivalent to a call to - public void RollbackTo (string savepoint) - { - RollbackTo (savepoint, false); - } - - /// - /// Rolls back the transaction that was begun by . - /// - /// The name of the savepoint to roll back to, as returned by . If savepoint is null or empty, this method is equivalent to a call to - /// true to avoid throwing exceptions, false otherwise - void RollbackTo (string savepoint, bool noThrow) - { - // Rolling back without a TO clause rolls backs all transactions - // and leaves the transaction stack empty. - try { - if (String.IsNullOrEmpty (savepoint)) { - if (Interlocked.Exchange (ref _transactionDepth, 0) > 0) { - Execute ("rollback"); - } - } - else { - DoSavePointExecute (savepoint, "rollback to "); - } - } - catch (SQLiteException) { - if (!noThrow) - throw; - - } - // No need to rollback if there are no transactions open. - } - - /// - /// Releases a savepoint returned from . Releasing a savepoint - /// makes changes since that savepoint permanent if the savepoint began the transaction, - /// or otherwise the changes are permanent pending a call to . - /// - /// The RELEASE command is like a COMMIT for a SAVEPOINT. - /// - /// The name of the savepoint to release. The string should be the result of a call to - public void Release (string savepoint) - { - try { - DoSavePointExecute (savepoint, "release "); - } - catch (SQLiteException ex) { - if (ex.Result == SQLite3.Result.Busy) { - // Force a rollback since most people don't know this function can fail - // Don't call Rollback() since the _transactionDepth is 0 and it won't try - // Calling rollback makes our _transactionDepth variable correct. - // Writes to the database only happen at depth=0, so this failure will only happen then. - try { - Execute ("rollback"); - } - catch { - // rollback can fail in all sorts of wonderful version-dependent ways. Let's just hope for the best - } - } - throw; - } - } - - void DoSavePointExecute (string savepoint, string cmd) - { - // Validate the savepoint - int firstLen = savepoint.IndexOf ('D'); - if (firstLen >= 2 && savepoint.Length > firstLen + 1) { - int depth; - if (Int32.TryParse (savepoint.Substring (firstLen + 1), out depth)) { - // TODO: Mild race here, but inescapable without locking almost everywhere. - if (0 <= depth && depth < _transactionDepth) { -#if NETFX_CORE || USE_SQLITEPCL_RAW || NETCORE - Volatile.Write (ref _transactionDepth, depth); -#elif SILVERLIGHT - _transactionDepth = depth; -#else - Thread.VolatileWrite (ref _transactionDepth, depth); -#endif - Execute (cmd + savepoint); - return; - } - } - } - - throw new ArgumentException ("savePoint is not valid, and should be the result of a call to SaveTransactionPoint.", "savePoint"); - } - - /// - /// Commits the transaction that was begun by . - /// - public void Commit () - { - if (Interlocked.Exchange (ref _transactionDepth, 0) != 0) { - try { - Execute ("commit"); - } - catch { - // Force a rollback since most people don't know this function can fail - // Don't call Rollback() since the _transactionDepth is 0 and it won't try - // Calling rollback makes our _transactionDepth variable correct. - try { - Execute ("rollback"); - } - catch { - // rollback can fail in all sorts of wonderful version-dependent ways. Let's just hope for the best - } - throw; - } - } - // Do nothing on a commit with no open transaction - } - - /// - /// 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 void RunInTransaction (Action action) - { - try { - var savePoint = SaveTransactionPoint (); - action (); - Release (savePoint); - } - catch (Exception) { - Rollback (); - throw; - } - } - - /// - /// 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 int InsertAll (System.Collections.IEnumerable objects, bool runInTransaction = true) - { - var c = 0; - if (runInTransaction) { - RunInTransaction (() => { - foreach (var r in objects) { - c += Insert (r); - } - }); - } - else { - foreach (var r in objects) { - c += Insert (r); - } - } - return c; - } - - /// - /// 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 int InsertAll (System.Collections.IEnumerable objects, string extra, bool runInTransaction = true) - { - var c = 0; - if (runInTransaction) { - RunInTransaction (() => { - foreach (var r in objects) { - c += Insert (r, extra); - } - }); - } - else { - foreach (var r in objects) { - c += Insert (r, extra); - } - } - return c; - } - - /// - /// 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 int InsertAll (System.Collections.IEnumerable objects, Type objType, bool runInTransaction = true) - { - var c = 0; - if (runInTransaction) { - RunInTransaction (() => { - foreach (var r in objects) { - c += Insert (r, objType); - } - }); - } - else { - foreach (var r in objects) { - c += Insert (r, objType); - } - } - return c; - } - - /// - /// 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 number of rows added to the table. - /// - public int Insert (object obj) - { - if (obj == null) { - return 0; - } - return Insert (obj, "", Orm.GetType (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 number of rows modified. - /// - public int InsertOrReplace (object obj) - { - if (obj == null) { - return 0; - } - return Insert (obj, "OR REPLACE", Orm.GetType (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 int Insert (object obj, Type objType) - { - return 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. - /// 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 int InsertOrReplace (object obj, Type objType) - { - return Insert (obj, "OR REPLACE", 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 int Insert (object obj, string extra) - { - if (obj == null) { - return 0; - } - return Insert (obj, extra, Orm.GetType (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. - /// - /// - /// 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 int Insert (object obj, string extra, Type objType) - { - if (obj == null || objType == null) { - return 0; - } - - var map = GetMapping (objType); - - if (map.PK != null && map.PK.IsAutoGuid) { - if (map.PK.GetValue (obj).Equals (Guid.Empty)) { - map.PK.SetValue (obj, Guid.NewGuid ()); - } - } - - var replacing = string.Compare (extra, "OR REPLACE", StringComparison.OrdinalIgnoreCase) == 0; - - var cols = replacing ? map.InsertOrReplaceColumns : map.InsertColumns; - var vals = new object[cols.Length]; - for (var i = 0; i < vals.Length; i++) { - vals[i] = cols[i].GetValue (obj); - } - - var insertCmd = GetInsertCommand (map, extra); - int count; - - lock (insertCmd) { - // We lock here to protect the prepared statement returned via GetInsertCommand. - // A SQLite prepared statement can be bound for only one operation at a time. - try { - count = insertCmd.ExecuteNonQuery (vals); - } - catch (SQLiteException ex) { - if (SQLite3.ExtendedErrCode (this.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { - throw NotNullConstraintViolationException.New (ex.Result, ex.Message, map, obj); - } - throw; - } - - if (map.HasAutoIncPK) { - var id = SQLite3.LastInsertRowid (Handle); - map.SetAutoIncPK (obj, id); - } - } - if (count > 0) - OnTableChanged (map, NotifyTableChangedAction.Insert); - - return count; - } - - readonly Dictionary, PreparedSqlLiteInsertCommand> _insertCommandMap = new Dictionary, PreparedSqlLiteInsertCommand> (); - - PreparedSqlLiteInsertCommand GetInsertCommand (TableMapping map, string extra) - { - PreparedSqlLiteInsertCommand prepCmd; - - var key = Tuple.Create (map.MappedType.FullName, extra); - - lock (_insertCommandMap) { - if (_insertCommandMap.TryGetValue (key, out prepCmd)) { - return prepCmd; - } - } - - prepCmd = CreateInsertCommand (map, extra); - - lock (_insertCommandMap) { - if (_insertCommandMap.TryGetValue (key, out var existing)) { - prepCmd.Dispose (); - return existing; - } - - _insertCommandMap.Add (key, prepCmd); - } - - return prepCmd; - } - - PreparedSqlLiteInsertCommand CreateInsertCommand (TableMapping map, string extra) - { - var cols = map.InsertColumns; - string insertSql; - if (cols.Length == 0 && map.Columns.Length == 1 && map.Columns[0].IsAutoInc) { - insertSql = string.Format ("insert {1} into \"{0}\" default values", map.TableName, extra); - } - else { - var replacing = string.Compare (extra, "OR REPLACE", StringComparison.OrdinalIgnoreCase) == 0; - - if (replacing) { - cols = map.InsertOrReplaceColumns; - } - - insertSql = string.Format ("insert {3} into \"{0}\"({1}) values ({2})", map.TableName, - string.Join (",", (from c in cols - select "\"" + c.Name + "\"").ToArray ()), - string.Join (",", (from c in cols - select "?").ToArray ()), extra); - - } - - var insertCommand = new PreparedSqlLiteInsertCommand (this, insertSql); - return insertCommand; - } - - /// - /// 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 int Update (object obj) - { - if (obj == null) { - return 0; - } - return Update (obj, Orm.GetType (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 int Update (object obj, Type objType) - { - int rowsAffected = 0; - if (obj == null || objType == null) { - return 0; - } - - var map = GetMapping (objType); - - var pk = map.PK; - - if (pk == null) { - throw new NotSupportedException ("Cannot update " + map.TableName + ": it has no PK"); - } - - var cols = from p in map.Columns - where p != pk - select p; - var vals = from c in cols - select c.GetValue (obj); - var ps = new List (vals); - if (ps.Count == 0) { - // There is a PK but no accompanying data, - // so reset the PK to make the UPDATE work. - cols = map.Columns; - vals = from c in cols - select c.GetValue (obj); - ps = new List (vals); - } - ps.Add (pk.GetValue (obj)); - var q = string.Format ("update \"{0}\" set {1} where \"{2}\" = ? ", map.TableName, string.Join (",", (from c in cols - select "\"" + c.Name + "\" = ? ").ToArray ()), pk.Name); - - try { - rowsAffected = Execute (q, ps.ToArray ()); - } - catch (SQLiteException ex) { - - if (ex.Result == SQLite3.Result.Constraint && SQLite3.ExtendedErrCode (this.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { - throw NotNullConstraintViolationException.New (ex, map, obj); - } - - throw; - } - - if (rowsAffected > 0) - OnTableChanged (map, NotifyTableChangedAction.Update); - - return rowsAffected; - } - - /// - /// 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 int UpdateAll (System.Collections.IEnumerable objects, bool runInTransaction = true) - { - var c = 0; - if (runInTransaction) { - RunInTransaction (() => { - foreach (var r in objects) { - c += Update (r); - } - }); - } - else { - foreach (var r in objects) { - c += Update (r); - } - } - return c; - } - - /// - /// 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 int Delete (object objectToDelete) - { - var map = GetMapping (Orm.GetType (objectToDelete)); - var pk = map.PK; - if (pk == null) { - throw new NotSupportedException ("Cannot delete " + map.TableName + ": it has no PK"); - } - var q = string.Format ("delete from \"{0}\" where \"{1}\" = ?", map.TableName, pk.Name); - var count = Execute (q, pk.GetValue (objectToDelete)); - if (count > 0) - OnTableChanged (map, NotifyTableChangedAction.Delete); - return count; - } - - /// - /// 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 int Delete (object primaryKey) - { - return Delete (primaryKey, GetMapping (typeof (T))); - } - - /// - /// 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 int Delete (object primaryKey, TableMapping map) - { - var pk = map.PK; - if (pk == null) { - throw new NotSupportedException ("Cannot delete " + map.TableName + ": it has no PK"); - } - var q = string.Format ("delete from \"{0}\" where \"{1}\" = ?", map.TableName, pk.Name); - var count = Execute (q, primaryKey); - if (count > 0) - OnTableChanged (map, NotifyTableChangedAction.Delete); - return count; - } - - /// - /// 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 int DeleteAll () - { - var map = GetMapping (typeof (T)); - return DeleteAll (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 TableMapping used to identify the table. - /// - /// - /// The number of objects deleted. - /// - public int DeleteAll (TableMapping map) - { - var query = string.Format ("delete from \"{0}\"", map.TableName); - var count = Execute (query); - if (count > 0) - OnTableChanged (map, NotifyTableChangedAction.Delete); - return count; - } - - /// - /// Backup the entire database to the specified path. - /// - /// Path to backup file. - /// The name of the database to backup (usually "main"). - public void Backup (string destinationDatabasePath, string databaseName = "main") - { - // Open the destination - var r = SQLite3.Open (destinationDatabasePath, out var destHandle); - if (r != SQLite3.Result.OK) { - throw SQLiteException.New (r, "Failed to open destination database"); - } - - // Init the backup - var backup = SQLite3.BackupInit (destHandle, databaseName, Handle, databaseName); - if (backup == NullBackupHandle) { - SQLite3.Close (destHandle); - throw new Exception ("Failed to create backup"); - } - - // Perform it - SQLite3.BackupStep (backup, -1); - SQLite3.BackupFinish (backup); - - // Check for errors - r = SQLite3.GetResult (destHandle); - string msg = ""; - if (r != SQLite3.Result.OK) { - msg = SQLite3.GetErrmsg (destHandle); - } - - // Close everything and report errors - SQLite3.Close (destHandle); - if (r != SQLite3.Result.OK) { - throw SQLiteException.New (r, msg); - } - } - - ~SQLiteConnection () - { - Dispose (false); - } - - public void Dispose () - { - Dispose (true); - GC.SuppressFinalize (this); - } - - public void Close () - { - Dispose (true); - } - - protected virtual void Dispose (bool disposing) - { - var useClose2 = LibVersionNumber >= 3007014; - - if (_open && Handle != NullHandle) { - try { - if (disposing) { - lock (_insertCommandMap) { - foreach (var sqlInsertCommand in _insertCommandMap.Values) { - sqlInsertCommand.Dispose (); - } - _insertCommandMap.Clear (); - } - - var r = useClose2 ? SQLite3.Close2 (Handle) : SQLite3.Close (Handle); - if (r != SQLite3.Result.OK) { - string msg = SQLite3.GetErrmsg (Handle); - throw SQLiteException.New (r, msg); - } - } - else { - var r = useClose2 ? SQLite3.Close2 (Handle) : SQLite3.Close (Handle); - } - } - finally { - Handle = NullHandle; - _open = false; - } - } - } - - void OnTableChanged (TableMapping table, NotifyTableChangedAction action) - { - var ev = TableChanged; - if (ev != null) - ev (this, new NotifyTableChangedEventArgs (table, action)); - } - - public event EventHandler TableChanged; - } - - public class NotifyTableChangedEventArgs : EventArgs - { - public TableMapping Table { get; private set; } - public NotifyTableChangedAction Action { get; private set; } - - public NotifyTableChangedEventArgs (TableMapping table, NotifyTableChangedAction action) - { - Table = table; - Action = action; - } - } - - public enum NotifyTableChangedAction - { - Insert, - Update, - Delete, - } - - /// - /// Represents a parsed connection string. - /// - public class SQLiteConnectionString - { - const string DateTimeSqliteDefaultFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff"; - - public string UniqueKey { get; } - public string DatabasePath { get; } - public bool StoreDateTimeAsTicks { get; } - public bool StoreTimeSpanAsTicks { get; } - public string DateTimeStringFormat { get; } - public System.Globalization.DateTimeStyles DateTimeStyle { get; } - public object Key { get; } - public SQLiteOpenFlags OpenFlags { get; } - public Action PreKeyAction { get; } - public Action PostKeyAction { get; } - public string VfsName { get; } + public TableQuery (SQLiteConnection conn) + { + Connection = conn; + Table = Connection.GetMapping (typeof (T)); + } -#if NETFX_CORE - static readonly string MetroStyleDataPath = Windows.Storage.ApplicationData.Current.LocalFolder.Path; + public TableQuery Clone () + { + var q = new TableQuery (Connection, Table); + q._where = _where; + q._deferred = _deferred; + if (_orderBys != null) { + q._orderBys = new List (_orderBys); + } + q._limit = _limit; + q._offset = _offset; + q._joinInner = _joinInner; + q._joinInnerKeySelector = _joinInnerKeySelector; + q._joinOuter = _joinOuter; + q._joinOuterKeySelector = _joinOuterKeySelector; + q._joinSelector = _joinSelector; + q._selector = _selector; + return q; + } - public static readonly string[] InMemoryDbPaths = new[] + /// + /// Filters the query based on a predicate. + /// + public TableQuery Where (Expression> predExpr) { - ":memory:", - "file::memory:" - }; + if (predExpr.NodeType == ExpressionType.Lambda) { + var lambda = (LambdaExpression)predExpr; + var pred = lambda.Body; + var q = Clone (); + q.AddWhere (pred); + return q; + } + else { + throw new NotSupportedException ("Must be a predicate"); + } + } - public static bool IsInMemoryPath(string databasePath) + /// + /// Delete all the rows that match this query. + /// + public int Delete () { - return InMemoryDbPaths.Any(i => i.Equals(databasePath, StringComparison.OrdinalIgnoreCase)); + return Delete (null); } -#endif + /// + /// Delete all the rows that match this query and the given predicate. + /// + public int Delete (Expression> predExpr) + { + if (_limit.HasValue || _offset.HasValue) + throw new InvalidOperationException ("Cannot delete with limits or offsets"); - /// - /// Constructs a new SQLiteConnectionString with all the data needed to open an SQLiteConnection. - /// - /// - /// 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 SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks = true) - : this (databasePath, SQLiteOpenFlags.Create | SQLiteOpenFlags.ReadWrite, storeDateTimeAsTicks) - { - } - - /// - /// Constructs a new SQLiteConnectionString with all the data needed to open an SQLiteConnection. - /// - /// - /// 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. - /// - /// - /// Specifies the encryption key to use on the database. Should be a string or a byte[]. - /// - /// - /// Executes prior to setting key for SQLCipher databases - /// - /// - /// Executes after setting key for SQLCipher databases - /// - /// - /// Specifies the Virtual File System to use on the database. - /// - public SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks, object key = null, Action preKeyAction = null, Action postKeyAction = null, string vfsName = null) - : this (databasePath, SQLiteOpenFlags.Create | SQLiteOpenFlags.ReadWrite, storeDateTimeAsTicks, key, preKeyAction, postKeyAction, vfsName) - { - } - - /// - /// Constructs a new SQLiteConnectionString with all the data needed to open an SQLiteConnection. - /// - /// - /// Specifies the path to the database file. - /// - /// - /// Flags controlling how the connection should be opened. - /// - /// - /// 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. - /// - /// - /// Specifies the encryption key to use on the database. Should be a string or a byte[]. - /// - /// - /// Executes prior to setting key for SQLCipher databases - /// - /// - /// Executes after setting key for SQLCipher databases - /// - /// - /// Specifies the Virtual File System to use on the database. - /// - /// - /// Specifies the format to use when storing DateTime properties as strings. - /// - /// - /// Specifies whether to store TimeSpan 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 storeTimeSpanAsTicks = true. - /// - public SQLiteConnectionString (string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks, object key = null, Action preKeyAction = null, Action postKeyAction = null, string vfsName = null, string dateTimeStringFormat = DateTimeSqliteDefaultFormat, bool storeTimeSpanAsTicks = true) - { - if (key != null && !((key is byte[]) || (key is string))) - throw new ArgumentException ("Encryption keys must be strings or byte arrays", nameof (key)); - - UniqueKey = string.Format ("{0}_{1:X8}", databasePath, (uint)openFlags); - StoreDateTimeAsTicks = storeDateTimeAsTicks; - StoreTimeSpanAsTicks = storeTimeSpanAsTicks; - DateTimeStringFormat = dateTimeStringFormat; - DateTimeStyle = "o".Equals (DateTimeStringFormat, StringComparison.OrdinalIgnoreCase) || "r".Equals (DateTimeStringFormat, StringComparison.OrdinalIgnoreCase) ? System.Globalization.DateTimeStyles.RoundtripKind : System.Globalization.DateTimeStyles.None; - Key = key; - PreKeyAction = preKeyAction; - PostKeyAction = postKeyAction; - OpenFlags = openFlags; - VfsName = vfsName; + if (_where == null && predExpr == null) + throw new InvalidOperationException ("No condition specified"); -#if NETFX_CORE - DatabasePath = IsInMemoryPath(databasePath) - ? databasePath - : System.IO.Path.Combine(MetroStyleDataPath, databasePath); + var pred = _where; -#else - DatabasePath = databasePath; -#endif - } - } - - [AttributeUsage (AttributeTargets.Class)] - public class TableAttribute : Attribute - { - public string Name { get; set; } - - /// - /// Flag whether to create the table without rowid (see https://sqlite.org/withoutrowid.html) - /// - /// The default is false so that sqlite adds an implicit rowid to every table created. - /// - public bool WithoutRowId { get; set; } - - public TableAttribute (string name) - { - Name = name; - } - } - - [AttributeUsage (AttributeTargets.Property)] - public class ColumnAttribute : Attribute - { - public string Name { get; set; } - - public ColumnAttribute (string name) - { - Name = name; - } - } - - [AttributeUsage (AttributeTargets.Property)] - public class PrimaryKeyAttribute : Attribute - { - } - - [AttributeUsage (AttributeTargets.Property)] - public class AutoIncrementAttribute : Attribute - { - } - - [AttributeUsage (AttributeTargets.Property, AllowMultiple = true)] - public class IndexedAttribute : Attribute - { - public string Name { get; set; } - public int Order { get; set; } - public virtual bool Unique { get; set; } - - public IndexedAttribute () - { - } - - public IndexedAttribute (string name, int order) - { - Name = name; - Order = order; - } - } - - [AttributeUsage (AttributeTargets.Property)] - public class IgnoreAttribute : Attribute - { - } - - [AttributeUsage (AttributeTargets.Property)] - public class UniqueAttribute : IndexedAttribute - { - public override bool Unique { - get { return true; } - set { /* throw? */ } - } - } - - [AttributeUsage (AttributeTargets.Property)] - public class MaxLengthAttribute : Attribute - { - public int Value { get; private set; } - - public MaxLengthAttribute (int length) - { - Value = length; - } - } - - public sealed class PreserveAttribute : System.Attribute - { - public bool AllMembers; - public bool Conditional; - } - - /// - /// Select the collating sequence to use on a column. - /// "BINARY", "NOCASE", and "RTRIM" are supported. - /// "BINARY" is the default. - /// - [AttributeUsage (AttributeTargets.Property)] - public class CollationAttribute : Attribute - { - public string Value { get; private set; } - - public CollationAttribute (string collation) - { - Value = collation; - } - } - - [AttributeUsage (AttributeTargets.Property)] - public class NotNullAttribute : Attribute - { - } - - [AttributeUsage (AttributeTargets.Enum)] - public class StoreAsTextAttribute : Attribute - { - } - - public class TableMapping - { - public Type MappedType { get; private set; } - - public string TableName { get; private set; } - - public bool WithoutRowId { get; private set; } - - public Column[] Columns { get; private set; } - - public Column PK { get; private set; } - - public string GetByPrimaryKeySql { get; private set; } - - public CreateFlags CreateFlags { get; private set; } - - internal MapMethod Method { get; private set; } = MapMethod.ByName; - - readonly Column _autoPk; - readonly Column[] _insertColumns; - readonly Column[] _insertOrReplaceColumns; - - public TableMapping (Type type, CreateFlags createFlags = CreateFlags.None) - { - MappedType = type; - CreateFlags = createFlags; - - var typeInfo = type.GetTypeInfo (); -#if ENABLE_IL2CPP - var tableAttr = typeInfo.GetCustomAttribute (); -#else - var tableAttr = - typeInfo.CustomAttributes - .Where (x => x.AttributeType == typeof (TableAttribute)) - .Select (x => (TableAttribute)Orm.InflateAttribute (x)) - .FirstOrDefault (); -#endif + if (predExpr != null && predExpr.NodeType == ExpressionType.Lambda) { + var lambda = (LambdaExpression)predExpr; + pred = pred != null ? Expression.AndAlso (pred, lambda.Body) : lambda.Body; + } - TableName = (tableAttr != null && !string.IsNullOrEmpty (tableAttr.Name)) ? tableAttr.Name : MappedType.Name; - WithoutRowId = tableAttr != null ? tableAttr.WithoutRowId : false; - - var members = GetPublicMembers(type); - var cols = new List(members.Count); - foreach(var m in members) - { - var ignore = m.IsDefined(typeof(IgnoreAttribute), true); - if(!ignore) - cols.Add(new Column(m, createFlags)); - } - Columns = cols.ToArray (); - foreach (var c in Columns) { - if (c.IsAutoInc && c.IsPK) { - _autoPk = c; - } - if (c.IsPK) { - PK = c; - } - } - - HasAutoIncPK = _autoPk != null; - - if (PK != null) { - GetByPrimaryKeySql = string.Format ("select * from \"{0}\" where \"{1}\" = ?", TableName, PK.Name); - } - else { - // People should not be calling Get/Find without a PK - GetByPrimaryKeySql = string.Format ("select * from \"{0}\" limit 1", TableName); - } - - _insertColumns = Columns.Where (c => !c.IsAutoInc).ToArray (); - _insertOrReplaceColumns = Columns.ToArray (); - } - - private IReadOnlyCollection GetPublicMembers(Type type) - { - if(type.Name.StartsWith("ValueTuple`")) - return GetFieldsFromValueTuple(type); - - var members = new List(); - var memberNames = new HashSet(); - var newMembers = new List(); - do - { - var ti = type.GetTypeInfo(); - newMembers.Clear(); - - newMembers.AddRange( - from p in ti.DeclaredProperties - where !memberNames.Contains(p.Name) && - p.CanRead && p.CanWrite && - p.GetMethod != null && p.SetMethod != null && - p.GetMethod.IsPublic && p.SetMethod.IsPublic && - !p.GetMethod.IsStatic && !p.SetMethod.IsStatic - select p); - - members.AddRange(newMembers); - foreach(var m in newMembers) - memberNames.Add(m.Name); - - type = ti.BaseType; - } - while(type != typeof(object)); - - return members; - } - - private IReadOnlyCollection GetFieldsFromValueTuple(Type type) - { - Method = MapMethod.ByPosition; - var fields = type.GetFields(); - - // https://docs.microsoft.com/en-us/dotnet/api/system.valuetuple-8.rest - if(fields.Length >= 8) - throw new NotSupportedException("ValueTuple with more than 7 members not supported due to nesting; see https://docs.microsoft.com/en-us/dotnet/api/system.valuetuple-8.rest"); - - return fields; - } - - public bool HasAutoIncPK { get; private set; } - - public void SetAutoIncPK (object obj, long id) - { - if (_autoPk != null) { - _autoPk.SetValue (obj, Convert.ChangeType (id, _autoPk.ColumnType, null)); - } - } - - public Column[] InsertColumns { - get { - return _insertColumns; - } - } - - public Column[] InsertOrReplaceColumns { - get { - return _insertOrReplaceColumns; - } - } - - public Column FindColumnWithPropertyName (string propertyName) - { - var exact = Columns.FirstOrDefault (c => c.PropertyName == propertyName); - return exact; - } - - public Column FindColumn (string columnName) - { - if(Method != MapMethod.ByName) - throw new InvalidOperationException($"This {nameof(TableMapping)} is not mapped by name, but {Method}."); - - var exact = Columns.FirstOrDefault (c => c.Name.ToLower () == columnName.ToLower ()); - return exact; - } + var args = new List (); + var cmdText = "delete from \"" + Table.TableName + "\""; + var w = CompileExpr (pred, args); + cmdText += " where " + w.CommandText; - public class Column - { - MemberInfo _member; + var command = Connection.CreateCommand (cmdText, args.ToArray ()); - public string Name { get; private set; } + int result = command.ExecuteNonQuery (); + return result; + } - public PropertyInfo PropertyInfo => _member as PropertyInfo; + /// + /// Yields a given number of elements from the query and then skips the remainder. + /// + public TableQuery Take (int n) + { + var q = Clone (); + q._limit = n; + return q; + } - public string PropertyName { get { return _member.Name; } } + /// + /// Skips a given number of elements from the query and then yields the remainder. + /// + public TableQuery Skip (int n) + { + var q = Clone (); + q._offset = n; + return q; + } - public Type ColumnType { get; private set; } + /// + /// Returns the element at a given index + /// + public T ElementAt (int index) + { + return Skip (index).Take (1).First (); + } - public string Collation { get; private set; } + bool _deferred; + public TableQuery Deferred () + { + var q = Clone (); + q._deferred = true; + return q; + } - public bool IsAutoInc { get; private set; } - public bool IsAutoGuid { get; private set; } + /// + /// Order the query results according to a key. + /// + public TableQuery OrderBy (Expression> orderExpr) + { + return AddOrderBy (orderExpr, true); + } - public bool IsPK { get; private set; } + /// + /// Order the query results according to a key. + /// + public TableQuery OrderByDescending (Expression> orderExpr) + { + return AddOrderBy (orderExpr, false); + } - public IEnumerable Indices { get; set; } + /// + /// Order the query results according to a key. + /// + public TableQuery ThenBy (Expression> orderExpr) + { + return AddOrderBy (orderExpr, true); + } - public bool IsNullable { get; private set; } + /// + /// Order the query results according to a key. + /// + public TableQuery ThenByDescending (Expression> orderExpr) + { + return AddOrderBy (orderExpr, false); + } - public int? MaxStringLength { get; private set; } + TableQuery AddOrderBy (Expression> orderExpr, bool asc) + { + if (orderExpr.NodeType == ExpressionType.Lambda) { + var lambda = (LambdaExpression)orderExpr; + + MemberExpression mem = null; + + var unary = lambda.Body as UnaryExpression; + if (unary != null && unary.NodeType == ExpressionType.Convert) { + mem = unary.Operand as MemberExpression; + } + else { + mem = lambda.Body as MemberExpression; + } + + if (mem != null && (mem.Expression.NodeType == ExpressionType.Parameter)) { + var q = Clone (); + if (q._orderBys == null) { + q._orderBys = new List (); + } + q._orderBys.Add (new Ordering { + ColumnName = Table.FindColumnWithPropertyName (mem.Member.Name).Name, + Ascending = asc + }); + return q; + } + else { + throw new NotSupportedException ("Order By does not support: " + orderExpr); + } + } + else { + throw new NotSupportedException ("Must be a predicate"); + } + } - public bool StoreAsText { get; private set; } + private void AddWhere (Expression pred) + { + if (_where == null) { + _where = pred; + } + else { + _where = Expression.AndAlso (_where, pred); + } + } - public Column (MemberInfo member, CreateFlags createFlags = CreateFlags.None) - { - _member = member; - var memberType = GetMemberType(member); + ///// + ///// Performs an inner join of two queries based on matching keys extracted from the elements. + ///// + //public TableQuery Join ( + // TableQuery inner, + // Expression> outerKeySelector, + // Expression> innerKeySelector, + // Expression> resultSelector) + //{ + // var q = new TableQuery (Connection, Connection.GetMapping (typeof (TResult))) { + // _joinOuter = this, + // _joinOuterKeySelector = outerKeySelector, + // _joinInner = inner, + // _joinInnerKeySelector = innerKeySelector, + // _joinSelector = resultSelector, + // }; + // return q; + //} + + // Not needed until Joins are supported + // Keeping this commented out forces the default Linq to objects processor to run + //public TableQuery Select (Expression> selector) + //{ + // var q = Clone (); + // q._selector = selector; + // return q; + //} + + private SQLiteCommand GenerateCommand (string selectionList) + { + if (_joinInner != null && _joinOuter != null) { + throw new NotSupportedException ("Joins are not supported."); + } + else { + var cmdText = "select " + selectionList + " from \"" + Table.TableName + "\""; + var args = new List (); + if (_where != null) { + var w = CompileExpr (_where, args); + cmdText += " where " + w.CommandText; + } + if ((_orderBys != null) && (_orderBys.Count > 0)) { + var t = string.Join (", ", _orderBys.Select (o => "\"" + o.ColumnName + "\"" + (o.Ascending ? "" : " desc")).ToArray ()); + cmdText += " order by " + t; + } + if (_limit.HasValue) { + cmdText += " limit " + _limit.Value; + } + if (_offset.HasValue) { + if (!_limit.HasValue) { + cmdText += " limit -1 "; + } + cmdText += " offset " + _offset.Value; + } + return Connection.CreateCommand (cmdText, args.ToArray ()); + } + } - var colAttr = member.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (ColumnAttribute)); -#if ENABLE_IL2CPP - var ca = member.GetCustomAttribute(typeof(ColumnAttribute)) as ColumnAttribute; - Name = ca == null ? member.Name : ca.Name; -#else - Name = (colAttr != null && colAttr.ConstructorArguments.Count > 0) ? - colAttr.ConstructorArguments[0].Value?.ToString () : - member.Name; -#endif - //If this type is Nullable then Nullable.GetUnderlyingType returns the T, otherwise it returns null, so get the actual type instead - ColumnType = Nullable.GetUnderlyingType (memberType) ?? memberType; - Collation = Orm.Collation (member); - - IsPK = Orm.IsPK (member) || - (((createFlags & CreateFlags.ImplicitPK) == CreateFlags.ImplicitPK) && - string.Compare (member.Name, Orm.ImplicitPkName, StringComparison.OrdinalIgnoreCase) == 0); - - var isAuto = Orm.IsAutoInc (member) || (IsPK && ((createFlags & CreateFlags.AutoIncPK) == CreateFlags.AutoIncPK)); - IsAutoGuid = isAuto && ColumnType == typeof (Guid); - IsAutoInc = isAuto && !IsAutoGuid; - - Indices = Orm.GetIndices (member); - if (!Indices.Any () - && !IsPK - && ((createFlags & CreateFlags.ImplicitIndex) == CreateFlags.ImplicitIndex) - && Name.EndsWith (Orm.ImplicitIndexSuffix, StringComparison.OrdinalIgnoreCase) - ) { - Indices = new IndexedAttribute[] { new IndexedAttribute () }; - } - IsNullable = !(IsPK || Orm.IsMarkedNotNull (member)); - MaxStringLength = Orm.MaxStringLength (member); - - StoreAsText = memberType.GetTypeInfo ().CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute)); - } - - public Column (PropertyInfo member, CreateFlags createFlags = CreateFlags.None) - : this((MemberInfo)member, createFlags) - { } - - public void SetValue (object obj, object val) - { - if(_member is PropertyInfo propy) - { - if (val != null && ColumnType.GetTypeInfo ().IsEnum) - propy.SetValue (obj, Enum.ToObject (ColumnType, val)); - else - propy.SetValue (obj, val); - } - else if(_member is FieldInfo field) - { - if (val != null && ColumnType.GetTypeInfo ().IsEnum) - field.SetValue (obj, Enum.ToObject (ColumnType, val)); - else - field.SetValue (obj, val); - } - else - throw new InvalidProgramException("unreachable condition"); - } - - public object GetValue (object obj) - { - if(_member is PropertyInfo propy) - return propy.GetValue(obj); - else if(_member is FieldInfo field) - return field.GetValue(obj); - else - throw new InvalidProgramException("unreachable condition"); - } - - private static Type GetMemberType(MemberInfo m) - { - switch(m.MemberType) - { - case MemberTypes.Property: return ((PropertyInfo)m).PropertyType; - case MemberTypes.Field: return ((FieldInfo)m).FieldType; - default: throw new InvalidProgramException($"{nameof(TableMapping)} supports properties or fields only."); - } - } - } - - internal enum MapMethod - { - ByName, - ByPosition - } - } - - class EnumCacheInfo - { - public EnumCacheInfo (Type type) - { - var typeInfo = type.GetTypeInfo (); - - IsEnum = typeInfo.IsEnum; - - if (IsEnum) { - StoreAsText = typeInfo.CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute)); - - if (StoreAsText) { - EnumValues = new Dictionary (); - foreach (object e in Enum.GetValues (type)) { - EnumValues[Convert.ToInt32 (e)] = e.ToString (); - } - } - } - } - - public bool IsEnum { get; private set; } - - public bool StoreAsText { get; private set; } - - public Dictionary EnumValues { get; private set; } - } - - static class EnumCache - { - static readonly Dictionary Cache = new Dictionary (); - - public static EnumCacheInfo GetInfo () - { - return GetInfo (typeof (T)); - } - - public static EnumCacheInfo GetInfo (Type type) - { - lock (Cache) { - EnumCacheInfo info = null; - if (!Cache.TryGetValue (type, out info)) { - info = new EnumCacheInfo (type); - Cache[type] = info; - } - - return info; - } - } - } - - public static class Orm - { - public const int DefaultMaxStringLength = 140; - public const string ImplicitPkName = "Id"; - public const string ImplicitIndexSuffix = "Id"; - - public static Type GetType (object obj) - { - if (obj == null) - return typeof (object); - var rt = obj as IReflectableType; - if (rt != null) - return rt.GetTypeInfo ().AsType (); - return obj.GetType (); - } - - public static string SqlDecl (TableMapping.Column p, bool storeDateTimeAsTicks, bool storeTimeSpanAsTicks) - { - string decl = "\"" + p.Name + "\" " + SqlType (p, storeDateTimeAsTicks, storeTimeSpanAsTicks) + " "; - - if (p.IsPK) { - decl += "primary key "; - } - if (p.IsAutoInc) { - decl += "autoincrement "; - } - if (!p.IsNullable) { - decl += "not null "; - } - if (!string.IsNullOrEmpty (p.Collation)) { - decl += "collate " + p.Collation + " "; - } - - return decl; - } - - public static string SqlType (TableMapping.Column p, bool storeDateTimeAsTicks, bool storeTimeSpanAsTicks) - { - var clrType = p.ColumnType; - if (clrType == typeof (Boolean) || clrType == typeof (Byte) || clrType == typeof (UInt16) || clrType == typeof (SByte) || clrType == typeof (Int16) || clrType == typeof (Int32) || clrType == typeof (UInt32) || clrType == typeof (Int64) || clrType == typeof (UInt64)) { - return "integer"; - } - else if (clrType == typeof (Single) || clrType == typeof (Double) || clrType == typeof (Decimal)) { - return "float"; - } - else if (clrType == typeof (String) || clrType == typeof (StringBuilder) || clrType == typeof (Uri) || clrType == typeof (UriBuilder)) { - int? len = p.MaxStringLength; - - if (len.HasValue) - return "varchar(" + len.Value + ")"; - - return "varchar"; - } - else if (clrType == typeof (TimeSpan)) { - return storeTimeSpanAsTicks ? "bigint" : "time"; - } - else if (clrType == typeof (DateTime)) { - return storeDateTimeAsTicks ? "bigint" : "datetime"; - } - else if (clrType == typeof (DateTimeOffset)) { - return "bigint"; - } - else if (clrType.GetTypeInfo ().IsEnum) { - if (p.StoreAsText) - return "varchar"; - else - return "integer"; - } - else if (clrType == typeof (byte[])) { - return "blob"; - } - else if (clrType == typeof (Guid)) { - return "varchar(36)"; - } - else { - throw new NotSupportedException ("Don't know about " + clrType); - } - } - - public static bool IsPK (MemberInfo p) - { - return p.CustomAttributes.Any (x => x.AttributeType == typeof (PrimaryKeyAttribute)); - } - - public static string Collation (MemberInfo p) - { -#if ENABLE_IL2CPP - return (p.GetCustomAttribute ()?.Value) ?? ""; -#else - return - (p.CustomAttributes - .Where (x => typeof (CollationAttribute) == x.AttributeType) - .Select (x => { - var args = x.ConstructorArguments; - return args.Count > 0 ? ((args[0].Value as string) ?? "") : ""; - }) - .FirstOrDefault ()) ?? ""; -#endif - } - - public static bool IsAutoInc (MemberInfo p) - { - return p.CustomAttributes.Any (x => x.AttributeType == typeof (AutoIncrementAttribute)); - } - - public static FieldInfo GetField (TypeInfo t, string name) - { - var f = t.GetDeclaredField (name); - if (f != null) - return f; - return GetField (t.BaseType.GetTypeInfo (), name); - } - - public static PropertyInfo GetProperty (TypeInfo t, string name) - { - var f = t.GetDeclaredProperty (name); - if (f != null) - return f; - return GetProperty (t.BaseType.GetTypeInfo (), name); - } - - public static object InflateAttribute (CustomAttributeData x) - { - var atype = x.AttributeType; - var typeInfo = atype.GetTypeInfo (); -#if ENABLE_IL2CPP - var r = Activator.CreateInstance (x.AttributeType); -#else - var args = x.ConstructorArguments.Select (a => a.Value).ToArray (); - var r = Activator.CreateInstance (x.AttributeType, args); - foreach (var arg in x.NamedArguments) { - if (arg.IsField) { - GetField (typeInfo, arg.MemberName).SetValue (r, arg.TypedValue.Value); - } - else { - GetProperty (typeInfo, arg.MemberName).SetValue (r, arg.TypedValue.Value); - } - } -#endif - return r; - } + class CompileResult + { + public string CommandText { get; set; } - public static IEnumerable GetIndices (MemberInfo p) - { -#if ENABLE_IL2CPP - return p.GetCustomAttributes (); -#else - var indexedInfo = typeof (IndexedAttribute).GetTypeInfo (); - return - p.CustomAttributes - .Where (x => indexedInfo.IsAssignableFrom (x.AttributeType.GetTypeInfo ())) - .Select (x => (IndexedAttribute)InflateAttribute (x)); -#endif - } + public object Value { get; set; } + } - public static int? MaxStringLength (MemberInfo p) - { -#if ENABLE_IL2CPP - return p.GetCustomAttribute ()?.Value; -#else - var attr = p.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (MaxLengthAttribute)); - if (attr != null) { - var attrv = (MaxLengthAttribute)InflateAttribute (attr); - return attrv.Value; - } - return null; -#endif - } - - public static int? MaxStringLength (PropertyInfo p) => MaxStringLength((MemberInfo)p); - - public static bool IsMarkedNotNull (MemberInfo p) - { - return p.CustomAttributes.Any (x => x.AttributeType == typeof (NotNullAttribute)); - } - } - - public partial class SQLiteCommand - { - SQLiteConnection _conn; - private List _bindings; - - public string CommandText { get; set; } - - public SQLiteCommand (SQLiteConnection conn) - { - _conn = conn; - _bindings = new List (); - CommandText = ""; - } - - public int ExecuteNonQuery () - { - if (_conn.Trace) { - _conn.Tracer?.Invoke ("Executing: " + this); - } - - var r = SQLite3.Result.OK; - var stmt = Prepare (); - r = SQLite3.Step (stmt); - Finalize (stmt); - if (r == SQLite3.Result.Done) { - int rowsAffected = SQLite3.Changes (_conn.Handle); - return rowsAffected; - } - else if (r == SQLite3.Result.Error) { - string msg = SQLite3.GetErrmsg (_conn.Handle); - throw SQLiteException.New (r, msg); - } - else if (r == SQLite3.Result.Constraint) { - if (SQLite3.ExtendedErrCode (_conn.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { - throw NotNullConstraintViolationException.New (r, SQLite3.GetErrmsg (_conn.Handle)); - } - } - - throw SQLiteException.New (r, SQLite3.GetErrmsg (_conn.Handle)); - } - - public IEnumerable ExecuteDeferredQuery () - { - return ExecuteDeferredQuery (_conn.GetMapping (typeof (T))); - } - - public List ExecuteQuery () - { - return ExecuteDeferredQuery (_conn.GetMapping (typeof (T))).ToList (); - } - - public List ExecuteQuery (TableMapping map) - { - return ExecuteDeferredQuery (map).ToList (); - } - - /// - /// Invoked every time an instance is loaded from the database. - /// - /// - /// The newly created object. - /// - /// - /// This can be overridden in combination with the - /// method to hook into the life-cycle of objects. - /// - protected virtual void OnInstanceCreated (object obj) - { - // Can be overridden. - } - - public IEnumerable ExecuteDeferredQuery (TableMapping map) - { - if (_conn.Trace) { - _conn.Tracer?.Invoke ("Executing Query: " + this); - } - - var stmt = Prepare (); - try { - var cols = new TableMapping.Column[SQLite3.ColumnCount (stmt)]; - var fastColumnSetters = new Action[SQLite3.ColumnCount (stmt)]; - - if (map.Method == TableMapping.MapMethod.ByPosition) - { - Array.Copy(map.Columns, cols, Math.Min(cols.Length, map.Columns.Length)); - } - else if (map.Method == TableMapping.MapMethod.ByName) { - MethodInfo getSetter = null; - if (typeof(T) != map.MappedType) { - getSetter = typeof(FastColumnSetter) - .GetMethod (nameof(FastColumnSetter.GetFastSetter), - BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod (map.MappedType); - } - - for (int i = 0; i < cols.Length; i++) { - var name = SQLite3.ColumnName16 (stmt, i); - cols[i] = map.FindColumn (name); - if (cols[i] != null) - if (getSetter != null) { - fastColumnSetters[i] = (Action)getSetter.Invoke(null, new object[]{ _conn, cols[i]}); - } - else { - fastColumnSetters[i] = FastColumnSetter.GetFastSetter(_conn, cols[i]); - } - } - } - - while (SQLite3.Step (stmt) == SQLite3.Result.Row) { - var obj = Activator.CreateInstance (map.MappedType); - for (int i = 0; i < cols.Length; i++) { - if (cols[i] == null) - continue; - - if (fastColumnSetters[i] != null) { - fastColumnSetters[i].Invoke (obj, stmt, i); - } - else { - var colType = SQLite3.ColumnType (stmt, i); - var val = ReadCol (stmt, i, colType, cols[i].ColumnType); - cols[i].SetValue (obj, val); - } - } - OnInstanceCreated (obj); - yield return (T)obj; - } - } - finally { - SQLite3.Finalize (stmt); - } - } - - public T ExecuteScalar () - { - if (_conn.Trace) { - _conn.Tracer?.Invoke ("Executing Query: " + this); - } - - T val = default (T); - - var stmt = Prepare (); - - try { - var r = SQLite3.Step (stmt); - if (r == SQLite3.Result.Row) { - var colType = SQLite3.ColumnType (stmt, 0); - var colval = ReadCol (stmt, 0, colType, typeof (T)); - if (colval != null) { - val = (T)colval; - } - } - else if (r == SQLite3.Result.Done) { - } - else { - throw SQLiteException.New (r, SQLite3.GetErrmsg (_conn.Handle)); - } - } - finally { - Finalize (stmt); - } - - return val; - } - - public IEnumerable ExecuteQueryScalars () - { - if (_conn.Trace) { - _conn.Tracer?.Invoke ("Executing Query: " + this); - } - var stmt = Prepare (); - try { - if (SQLite3.ColumnCount (stmt) < 1) { - throw new InvalidOperationException ("QueryScalars should return at least one column"); - } - while (SQLite3.Step (stmt) == SQLite3.Result.Row) { - var colType = SQLite3.ColumnType (stmt, 0); - var val = ReadCol (stmt, 0, colType, typeof (T)); - if (val == null) { - yield return default (T); - } - else { - yield return (T)val; - } - } - } - finally { - Finalize (stmt); - } - } - - public void Bind (string name, object val) - { - _bindings.Add (new Binding { - Name = name, - Value = val - }); - } - - public void Bind (object val) - { - Bind (null, val); - } - - public override string ToString () - { - var parts = new string[1 + _bindings.Count]; - parts[0] = CommandText; - var i = 1; - foreach (var b in _bindings) { - parts[i] = string.Format (" {0}: {1}", i - 1, b.Value); - i++; - } - return string.Join (Environment.NewLine, parts); - } - - Sqlite3Statement Prepare () - { - var stmt = SQLite3.Prepare2 (_conn.Handle, CommandText); - BindAll (stmt); - return stmt; - } - - void Finalize (Sqlite3Statement stmt) - { - SQLite3.Finalize (stmt); - } - - void BindAll (Sqlite3Statement stmt) - { - int nextIdx = 1; - foreach (var b in _bindings) { - if (b.Name != null) { - b.Index = SQLite3.BindParameterIndex (stmt, b.Name); - } - else { - b.Index = nextIdx++; - } - - BindParameter (stmt, b.Index, b.Value, _conn.StoreDateTimeAsTicks, _conn.DateTimeStringFormat, _conn.StoreTimeSpanAsTicks); - } - } - - static IntPtr NegativePointer = new IntPtr (-1); - - internal static void BindParameter (Sqlite3Statement stmt, int index, object value, bool storeDateTimeAsTicks, string dateTimeStringFormat, bool storeTimeSpanAsTicks) - { - if (value == null) { - SQLite3.BindNull (stmt, index); - } - else { - if (value is Int32) { - SQLite3.BindInt (stmt, index, (int)value); - } - else if (value is String) { - SQLite3.BindText (stmt, index, (string)value, -1, NegativePointer); - } - else if (value is Byte || value is UInt16 || value is SByte || value is Int16) { - SQLite3.BindInt (stmt, index, Convert.ToInt32 (value)); - } - else if (value is Boolean) { - SQLite3.BindInt (stmt, index, (bool)value ? 1 : 0); - } - else if (value is UInt32 || value is Int64 || value is UInt64) { - SQLite3.BindInt64 (stmt, index, Convert.ToInt64 (value)); - } - else if (value is Single || value is Double || value is Decimal) { - SQLite3.BindDouble (stmt, index, Convert.ToDouble (value)); - } - else if (value is TimeSpan) { - if (storeTimeSpanAsTicks) { - SQLite3.BindInt64 (stmt, index, ((TimeSpan)value).Ticks); - } - else { - SQLite3.BindText (stmt, index, ((TimeSpan)value).ToString (), -1, NegativePointer); - } - } - else if (value is DateTime) { - if (storeDateTimeAsTicks) { - SQLite3.BindInt64 (stmt, index, ((DateTime)value).Ticks); - } - else { - SQLite3.BindText (stmt, index, ((DateTime)value).ToString (dateTimeStringFormat, System.Globalization.CultureInfo.InvariantCulture), -1, NegativePointer); - } - } - else if (value is DateTimeOffset) { - SQLite3.BindInt64 (stmt, index, ((DateTimeOffset)value).UtcTicks); - } - else if (value is byte[]) { - SQLite3.BindBlob (stmt, index, (byte[])value, ((byte[])value).Length, NegativePointer); - } - else if (value is Guid) { - SQLite3.BindText (stmt, index, ((Guid)value).ToString (), 72, NegativePointer); - } - else if (value is Uri) { - SQLite3.BindText (stmt, index, ((Uri)value).ToString (), -1, NegativePointer); - } - else if (value is StringBuilder) { - SQLite3.BindText (stmt, index, ((StringBuilder)value).ToString (), -1, NegativePointer); - } - else if (value is UriBuilder) { - SQLite3.BindText (stmt, index, ((UriBuilder)value).ToString (), -1, NegativePointer); - } - else { - // Now we could possibly get an enum, retrieve cached info - var valueType = value.GetType (); - var enumInfo = EnumCache.GetInfo (valueType); - if (enumInfo.IsEnum) { - var enumIntValue = Convert.ToInt32 (value); - if (enumInfo.StoreAsText) - SQLite3.BindText (stmt, index, enumInfo.EnumValues[enumIntValue], -1, NegativePointer); - else - SQLite3.BindInt (stmt, index, enumIntValue); - } - else { - throw new NotSupportedException ("Cannot store type: " + Orm.GetType (value)); - } - } - } - } - - class Binding - { - public string Name { get; set; } - - public object Value { get; set; } - - public int Index { get; set; } - } - - object ReadCol (Sqlite3Statement stmt, int index, SQLite3.ColType type, Type clrType) - { - if (type == SQLite3.ColType.Null) { - return null; - } - else { - var clrTypeInfo = clrType.GetTypeInfo (); - if (clrTypeInfo.IsGenericType && clrTypeInfo.GetGenericTypeDefinition () == typeof (Nullable<>)) { - clrType = clrTypeInfo.GenericTypeArguments[0]; - clrTypeInfo = clrType.GetTypeInfo (); - } - - if (clrType == typeof (String)) { - return SQLite3.ColumnString (stmt, index); - } - else if (clrType == typeof (Int32)) { - return (int)SQLite3.ColumnInt (stmt, index); - } - else if (clrType == typeof (Boolean)) { - return SQLite3.ColumnInt (stmt, index) == 1; - } - else if (clrType == typeof (double)) { - return SQLite3.ColumnDouble (stmt, index); - } - else if (clrType == typeof (float)) { - return (float)SQLite3.ColumnDouble (stmt, index); - } - else if (clrType == typeof (TimeSpan)) { - if (_conn.StoreTimeSpanAsTicks) { - return new TimeSpan (SQLite3.ColumnInt64 (stmt, index)); - } - else { - var text = SQLite3.ColumnString (stmt, index); - TimeSpan resultTime; - if (!TimeSpan.TryParseExact (text, "c", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.TimeSpanStyles.None, out resultTime)) { - resultTime = TimeSpan.Parse (text); - } - return resultTime; - } - } - else if (clrType == typeof (DateTime)) { - if (_conn.StoreDateTimeAsTicks) { - return new DateTime (SQLite3.ColumnInt64 (stmt, index)); - } - else { - var text = SQLite3.ColumnString (stmt, index); - DateTime resultDate; - if (!DateTime.TryParseExact (text, _conn.DateTimeStringFormat, System.Globalization.CultureInfo.InvariantCulture, _conn.DateTimeStyle, out resultDate)) { - resultDate = DateTime.Parse (text); - } - return resultDate; - } - } - else if (clrType == typeof (DateTimeOffset)) { - return new DateTimeOffset (SQLite3.ColumnInt64 (stmt, index), TimeSpan.Zero); - } - else if (clrTypeInfo.IsEnum) { - if (type == SQLite3.ColType.Text) { - var value = SQLite3.ColumnString (stmt, index); - return Enum.Parse (clrType, value.ToString (), true); - } - else - return SQLite3.ColumnInt (stmt, index); - } - else if (clrType == typeof (Int64)) { - return SQLite3.ColumnInt64 (stmt, index); - } - else if (clrType == typeof (UInt64)) { - return (ulong)SQLite3.ColumnInt64 (stmt, index); - } - else if (clrType == typeof (UInt32)) { - return (uint)SQLite3.ColumnInt64 (stmt, index); - } - else if (clrType == typeof (decimal)) { - return (decimal)SQLite3.ColumnDouble (stmt, index); - } - else if (clrType == typeof (Byte)) { - return (byte)SQLite3.ColumnInt (stmt, index); - } - else if (clrType == typeof (UInt16)) { - return (ushort)SQLite3.ColumnInt (stmt, index); - } - else if (clrType == typeof (Int16)) { - return (short)SQLite3.ColumnInt (stmt, index); - } - else if (clrType == typeof (sbyte)) { - return (sbyte)SQLite3.ColumnInt (stmt, index); - } - else if (clrType == typeof (byte[])) { - return SQLite3.ColumnByteArray (stmt, index); - } - else if (clrType == typeof (Guid)) { - var text = SQLite3.ColumnString (stmt, index); - return new Guid (text); - } - else if (clrType == typeof (Uri)) { - var text = SQLite3.ColumnString (stmt, index); - return new Uri (text); - } - else if (clrType == typeof (StringBuilder)) { - var text = SQLite3.ColumnString (stmt, index); - return new StringBuilder (text); - } - else if (clrType == typeof (UriBuilder)) { - var text = SQLite3.ColumnString (stmt, index); - return new UriBuilder (text); - } - else { - throw new NotSupportedException ("Don't know how to read " + clrType); - } - } - } - } - - internal class FastColumnSetter - { - /// - /// Creates a delegate that can be used to quickly set object members from query columns. - /// - /// Note that this frontloads the slow reflection-based type checking for columns to only happen once at the beginning of a query, - /// and then afterwards each row of the query can invoke the delegate returned by this function to get much better performance (up to 10x speed boost, depending on query size and platform). - /// - /// The type of the destination object that the query will read into - /// The active connection. Note that this is primarily needed in order to read preferences regarding how certain data types (such as TimeSpan / DateTime) should be encoded in the database. - /// The table mapping used to map the statement column to a member of the destination object type - /// - /// A delegate for fast-setting of object members from statement columns. - /// - /// If no fast setter is available for the requested column (enums in particular cause headache), then this function returns null. - /// - internal static Action GetFastSetter (SQLiteConnection conn, TableMapping.Column column) - { - Action fastSetter = null; - - Type clrType = column.PropertyInfo.PropertyType; - - var clrTypeInfo = clrType.GetTypeInfo (); - if (clrTypeInfo.IsGenericType && clrTypeInfo.GetGenericTypeDefinition () == typeof (Nullable<>)) { - clrType = clrTypeInfo.GenericTypeArguments[0]; - clrTypeInfo = clrType.GetTypeInfo (); - } - - if (clrType == typeof (String)) { - fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { - return SQLite3.ColumnString (stmt, index); - }); - } - else if (clrType == typeof (Int32)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index)=>{ - return SQLite3.ColumnInt (stmt, index); - }); - } - else if (clrType == typeof (Boolean)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return SQLite3.ColumnInt (stmt, index) == 1; - }); - } - else if (clrType == typeof (double)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return SQLite3.ColumnDouble (stmt, index); - }); - } - else if (clrType == typeof (float)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return (float) SQLite3.ColumnDouble (stmt, index); - }); - } - else if (clrType == typeof (TimeSpan)) { - if (conn.StoreTimeSpanAsTicks) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return new TimeSpan (SQLite3.ColumnInt64 (stmt, index)); - }); - } - else { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - var text = SQLite3.ColumnString (stmt, index); - TimeSpan resultTime; - if (!TimeSpan.TryParseExact (text, "c", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.TimeSpanStyles.None, out resultTime)) { - resultTime = TimeSpan.Parse (text); - } - return resultTime; - }); - } - } - else if (clrType == typeof (DateTime)) { - if (conn.StoreDateTimeAsTicks) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return new DateTime (SQLite3.ColumnInt64 (stmt, index)); - }); - } - else { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - var text = SQLite3.ColumnString (stmt, index); - DateTime resultDate; - if (!DateTime.TryParseExact (text, conn.DateTimeStringFormat, System.Globalization.CultureInfo.InvariantCulture, conn.DateTimeStyle, out resultDate)) { - resultDate = DateTime.Parse (text); - } - return resultDate; - }); - } - } - else if (clrType == typeof (DateTimeOffset)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return new DateTimeOffset (SQLite3.ColumnInt64 (stmt, index), TimeSpan.Zero); - }); - } - else if (clrTypeInfo.IsEnum) { - // NOTE: Not sure of a good way (if any?) to do a strongly-typed fast setter like this for enumerated types -- for now, return null and column sets will revert back to the safe (but slow) Reflection-based method of column prop.Set() - } - else if (clrType == typeof (Int64)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return SQLite3.ColumnInt64 (stmt, index); - }); - } - else if (clrType == typeof(UInt64)) - { - fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { - return (ulong)SQLite3.ColumnInt64(stmt, index); - }); - } - else if (clrType == typeof (UInt32)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return (uint)SQLite3.ColumnInt64 (stmt, index); - }); - } - else if (clrType == typeof (decimal)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return (decimal)SQLite3.ColumnDouble (stmt, index); - }); - } - else if (clrType == typeof (Byte)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return (byte)SQLite3.ColumnInt (stmt, index); - }); - } - else if (clrType == typeof (UInt16)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return (ushort)SQLite3.ColumnInt (stmt, index); - }); - } - else if (clrType == typeof (Int16)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return (short)SQLite3.ColumnInt (stmt, index); - }); - } - else if (clrType == typeof (sbyte)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return (sbyte)SQLite3.ColumnInt (stmt, index); - }); - } - else if (clrType == typeof (byte[])) { - fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { - return SQLite3.ColumnByteArray (stmt, index); - }); - } - else if (clrType == typeof (Guid)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - var text = SQLite3.ColumnString (stmt, index); - return new Guid (text); - }); - } - else if (clrType == typeof (Uri)) { - fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { - var text = SQLite3.ColumnString (stmt, index); - return new Uri (text); - }); - } - else if (clrType == typeof (StringBuilder)) { - fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { - var text = SQLite3.ColumnString (stmt, index); - return new StringBuilder (text); - }); - } - else if (clrType == typeof (UriBuilder)) { - fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { - var text = SQLite3.ColumnString (stmt, index); - return new UriBuilder (text); - }); - } - else { - // NOTE: Will fall back to the slow setter method in the event that we are unable to create a fast setter delegate for a particular column type - } - return fastSetter; - } - - /// - /// This creates a strongly typed delegate that will permit fast setting of column values given a Sqlite3Statement and a column index. - /// - /// Note that this is identical to CreateTypedSetterDelegate(), but has an extra check to see if it should create a nullable version of the delegate. - /// - /// The type of the object whose member column is being set - /// The CLR type of the member in the object which corresponds to the given SQLite columnn - /// The column mapping that identifies the target member of the destination object - /// A lambda that can be used to retrieve the column value at query-time - /// A strongly-typed delegate - private static Action CreateNullableTypedSetterDelegate (TableMapping.Column column, Func getColumnValue) where ColumnMemberType : struct - { - var clrTypeInfo = column.PropertyInfo.PropertyType.GetTypeInfo(); - bool isNullable = false; - - if (clrTypeInfo.IsGenericType && clrTypeInfo.GetGenericTypeDefinition () == typeof (Nullable<>)) { - isNullable = true; - } - - if (isNullable) { - var setProperty = (Action)Delegate.CreateDelegate ( - typeof (Action), null, - column.PropertyInfo.GetSetMethod ()); - - return (o, stmt, i) => { - var colType = SQLite3.ColumnType (stmt, i); - if (colType != SQLite3.ColType.Null) - setProperty.Invoke ((ObjectType)o, getColumnValue.Invoke (stmt, i)); - }; - } - - return CreateTypedSetterDelegate (column, getColumnValue); - } - - /// - /// This creates a strongly typed delegate that will permit fast setting of column values given a Sqlite3Statement and a column index. - /// - /// The type of the object whose member column is being set - /// The CLR type of the member in the object which corresponds to the given SQLite columnn - /// The column mapping that identifies the target member of the destination object - /// A lambda that can be used to retrieve the column value at query-time - /// A strongly-typed delegate - private static Action CreateTypedSetterDelegate (TableMapping.Column column, Func getColumnValue) - { - var setProperty = (Action)Delegate.CreateDelegate ( - typeof (Action), null, - column.PropertyInfo.GetSetMethod ()); - - return (o, stmt, i) => { - var colType = SQLite3.ColumnType (stmt, i); - if (colType != SQLite3.ColType.Null) - setProperty.Invoke ((ObjectType)o, getColumnValue.Invoke (stmt, i)); - }; - } - } - - /// - /// Since the insert never changed, we only need to prepare once. - /// - class PreparedSqlLiteInsertCommand : IDisposable - { - bool Initialized; - - SQLiteConnection Connection; - - string CommandText; - - Sqlite3Statement Statement; - static readonly Sqlite3Statement NullStatement = default (Sqlite3Statement); - - public PreparedSqlLiteInsertCommand (SQLiteConnection conn, string commandText) - { - Connection = conn; - CommandText = commandText; - } - - public int ExecuteNonQuery (object[] source) - { - if (Initialized && Statement == NullStatement) { - throw new ObjectDisposedException (nameof (PreparedSqlLiteInsertCommand)); - } - - if (Connection.Trace) { - Connection.Tracer?.Invoke ("Executing: " + CommandText); - } - - var r = SQLite3.Result.OK; - - if (!Initialized) { - Statement = SQLite3.Prepare2 (Connection.Handle, CommandText); - Initialized = true; - } - - //bind the values. - if (source != null) { - for (int i = 0; i < source.Length; i++) { - SQLiteCommand.BindParameter (Statement, i + 1, source[i], Connection.StoreDateTimeAsTicks, Connection.DateTimeStringFormat, Connection.StoreTimeSpanAsTicks); - } - } - r = SQLite3.Step (Statement); - - if (r == SQLite3.Result.Done) { - int rowsAffected = SQLite3.Changes (Connection.Handle); - SQLite3.Reset (Statement); - return rowsAffected; - } - else if (r == SQLite3.Result.Error) { - string msg = SQLite3.GetErrmsg (Connection.Handle); - SQLite3.Reset (Statement); - throw SQLiteException.New (r, msg); - } - else if (r == SQLite3.Result.Constraint && SQLite3.ExtendedErrCode (Connection.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { - SQLite3.Reset (Statement); - throw NotNullConstraintViolationException.New (r, SQLite3.GetErrmsg (Connection.Handle)); - } - else { - SQLite3.Reset (Statement); - throw SQLiteException.New (r, SQLite3.GetErrmsg (Connection.Handle)); - } - } - - public void Dispose () - { - Dispose (true); - GC.SuppressFinalize (this); - } - - void Dispose (bool disposing) - { - var s = Statement; - Statement = NullStatement; - Connection = null; - if (s != NullStatement) { - SQLite3.Finalize (s); - } - } - - ~PreparedSqlLiteInsertCommand () - { - Dispose (false); - } - } - - public enum CreateTableResult - { - Created, - Migrated, - } - - public class CreateTablesResult - { - public Dictionary Results { get; private set; } - - public CreateTablesResult () - { - Results = new Dictionary (); - } - } - - public abstract class BaseTableQuery - { - protected class Ordering - { - public string ColumnName { get; set; } - public bool Ascending { get; set; } - } - } - - public class TableQuery : BaseTableQuery, IEnumerable - { - public SQLiteConnection Connection { get; private set; } - - public TableMapping Table { get; private set; } - - Expression _where; - List _orderBys; - int? _limit; - int? _offset; - - BaseTableQuery _joinInner; - Expression _joinInnerKeySelector; - BaseTableQuery _joinOuter; - Expression _joinOuterKeySelector; - Expression _joinSelector; - - Expression _selector; - - TableQuery (SQLiteConnection conn, TableMapping table) - { - Connection = conn; - Table = table; - } - - public TableQuery (SQLiteConnection conn) - { - Connection = conn; - Table = Connection.GetMapping (typeof (T)); - } - - public TableQuery Clone () - { - var q = new TableQuery (Connection, Table); - q._where = _where; - q._deferred = _deferred; - if (_orderBys != null) { - q._orderBys = new List (_orderBys); - } - q._limit = _limit; - q._offset = _offset; - q._joinInner = _joinInner; - q._joinInnerKeySelector = _joinInnerKeySelector; - q._joinOuter = _joinOuter; - q._joinOuterKeySelector = _joinOuterKeySelector; - q._joinSelector = _joinSelector; - q._selector = _selector; - return q; - } - - /// - /// Filters the query based on a predicate. - /// - public TableQuery Where (Expression> predExpr) - { - if (predExpr.NodeType == ExpressionType.Lambda) { - var lambda = (LambdaExpression)predExpr; - var pred = lambda.Body; - var q = Clone (); - q.AddWhere (pred); - return q; - } - else { - throw new NotSupportedException ("Must be a predicate"); - } - } - - /// - /// Delete all the rows that match this query. - /// - public int Delete () - { - return Delete (null); - } - - /// - /// Delete all the rows that match this query and the given predicate. - /// - public int Delete (Expression> predExpr) - { - if (_limit.HasValue || _offset.HasValue) - throw new InvalidOperationException ("Cannot delete with limits or offsets"); - - if (_where == null && predExpr == null) - throw new InvalidOperationException ("No condition specified"); - - var pred = _where; - - if (predExpr != null && predExpr.NodeType == ExpressionType.Lambda) { - var lambda = (LambdaExpression)predExpr; - pred = pred != null ? Expression.AndAlso (pred, lambda.Body) : lambda.Body; - } - - var args = new List (); - var cmdText = "delete from \"" + Table.TableName + "\""; - var w = CompileExpr (pred, args); - cmdText += " where " + w.CommandText; - - var command = Connection.CreateCommand (cmdText, args.ToArray ()); - - int result = command.ExecuteNonQuery (); - return result; - } - - /// - /// Yields a given number of elements from the query and then skips the remainder. - /// - public TableQuery Take (int n) - { - var q = Clone (); - q._limit = n; - return q; - } - - /// - /// Skips a given number of elements from the query and then yields the remainder. - /// - public TableQuery Skip (int n) - { - var q = Clone (); - q._offset = n; - return q; - } - - /// - /// Returns the element at a given index - /// - public T ElementAt (int index) - { - return Skip (index).Take (1).First (); - } - - bool _deferred; - public TableQuery Deferred () - { - var q = Clone (); - q._deferred = true; - return q; - } - - /// - /// Order the query results according to a key. - /// - public TableQuery OrderBy (Expression> orderExpr) - { - return AddOrderBy (orderExpr, true); - } - - /// - /// Order the query results according to a key. - /// - public TableQuery OrderByDescending (Expression> orderExpr) - { - return AddOrderBy (orderExpr, false); - } - - /// - /// Order the query results according to a key. - /// - public TableQuery ThenBy (Expression> orderExpr) - { - return AddOrderBy (orderExpr, true); - } - - /// - /// Order the query results according to a key. - /// - public TableQuery ThenByDescending (Expression> orderExpr) - { - return AddOrderBy (orderExpr, false); - } - - TableQuery AddOrderBy (Expression> orderExpr, bool asc) - { - if (orderExpr.NodeType == ExpressionType.Lambda) { - var lambda = (LambdaExpression)orderExpr; - - MemberExpression mem = null; - - var unary = lambda.Body as UnaryExpression; - if (unary != null && unary.NodeType == ExpressionType.Convert) { - mem = unary.Operand as MemberExpression; - } - else { - mem = lambda.Body as MemberExpression; - } - - if (mem != null && (mem.Expression.NodeType == ExpressionType.Parameter)) { - var q = Clone (); - if (q._orderBys == null) { - q._orderBys = new List (); - } - q._orderBys.Add (new Ordering { - ColumnName = Table.FindColumnWithPropertyName (mem.Member.Name).Name, - Ascending = asc - }); - return q; - } - else { - throw new NotSupportedException ("Order By does not support: " + orderExpr); - } - } - else { - throw new NotSupportedException ("Must be a predicate"); - } - } - - private void AddWhere (Expression pred) - { - if (_where == null) { - _where = pred; - } - else { - _where = Expression.AndAlso (_where, pred); - } - } - - ///// - ///// Performs an inner join of two queries based on matching keys extracted from the elements. - ///// - //public TableQuery Join ( - // TableQuery inner, - // Expression> outerKeySelector, - // Expression> innerKeySelector, - // Expression> resultSelector) - //{ - // var q = new TableQuery (Connection, Connection.GetMapping (typeof (TResult))) { - // _joinOuter = this, - // _joinOuterKeySelector = outerKeySelector, - // _joinInner = inner, - // _joinInnerKeySelector = innerKeySelector, - // _joinSelector = resultSelector, - // }; - // return q; - //} - - // Not needed until Joins are supported - // Keeping this commented out forces the default Linq to objects processor to run - //public TableQuery Select (Expression> selector) - //{ - // var q = Clone (); - // q._selector = selector; - // return q; - //} - - private SQLiteCommand GenerateCommand (string selectionList) - { - if (_joinInner != null && _joinOuter != null) { - throw new NotSupportedException ("Joins are not supported."); - } - else { - var cmdText = "select " + selectionList + " from \"" + Table.TableName + "\""; - var args = new List (); - if (_where != null) { - var w = CompileExpr (_where, args); - cmdText += " where " + w.CommandText; - } - if ((_orderBys != null) && (_orderBys.Count > 0)) { - var t = string.Join (", ", _orderBys.Select (o => "\"" + o.ColumnName + "\"" + (o.Ascending ? "" : " desc")).ToArray ()); - cmdText += " order by " + t; - } - if (_limit.HasValue) { - cmdText += " limit " + _limit.Value; - } - if (_offset.HasValue) { - if (!_limit.HasValue) { - cmdText += " limit -1 "; - } - cmdText += " offset " + _offset.Value; - } - return Connection.CreateCommand (cmdText, args.ToArray ()); - } - } - - class CompileResult - { - public string CommandText { get; set; } - - public object Value { get; set; } - } - - private CompileResult CompileExpr (Expression expr, List queryArgs) - { - if (expr == null) { - throw new NotSupportedException ("Expression is NULL"); - } - else if (expr is BinaryExpression) { - var bin = (BinaryExpression)expr; - - // VB turns 'x=="foo"' into 'CompareString(x,"foo",true/false)==0', so we need to unwrap it - // http://blogs.msdn.com/b/vbteam/archive/2007/09/18/vb-expression-trees-string-comparisons.aspx - if (bin.Left.NodeType == ExpressionType.Call) { - var call = (MethodCallExpression)bin.Left; - if (call.Method.DeclaringType.FullName == "Microsoft.VisualBasic.CompilerServices.Operators" - && call.Method.Name == "CompareString") - bin = Expression.MakeBinary (bin.NodeType, call.Arguments[0], call.Arguments[1]); - } - - - var leftr = CompileExpr (bin.Left, queryArgs); - var rightr = CompileExpr (bin.Right, queryArgs); - - //If either side is a parameter and is null, then handle the other side specially (for "is null"/"is not null") - string text; - if (leftr.CommandText == "?" && leftr.Value == null) - text = CompileNullBinaryExpression (bin, rightr); - else if (rightr.CommandText == "?" && rightr.Value == null) - text = CompileNullBinaryExpression (bin, leftr); - else - text = "(" + leftr.CommandText + " " + GetSqlName (bin) + " " + rightr.CommandText + ")"; - return new CompileResult { CommandText = text }; - } - else if (expr.NodeType == ExpressionType.Not) { - var operandExpr = ((UnaryExpression)expr).Operand; - var opr = CompileExpr (operandExpr, queryArgs); - object val = opr.Value; - if (val is bool) - val = !((bool)val); - return new CompileResult { - CommandText = "NOT(" + opr.CommandText + ")", - Value = val - }; - } - else if (expr.NodeType == ExpressionType.Call) { - - var call = (MethodCallExpression)expr; - var args = new CompileResult[call.Arguments.Count]; - var obj = call.Object != null ? CompileExpr (call.Object, queryArgs) : null; - - for (var i = 0; i < args.Length; i++) { - args[i] = CompileExpr (call.Arguments[i], queryArgs); - } - - var sqlCall = ""; - - if (call.Method.Name == "Like" && args.Length == 2) { - sqlCall = "(" + args[0].CommandText + " like " + args[1].CommandText + ")"; - } - else if (call.Method.Name == "Contains" && args.Length == 2) { - sqlCall = "(" + args[1].CommandText + " in " + args[0].CommandText + ")"; - } - else if (call.Method.Name == "Contains" && args.Length == 1) { - if (call.Object != null && call.Object.Type == typeof (string)) { - sqlCall = "( instr(" + obj.CommandText + "," + args[0].CommandText + ") >0 )"; - } - else { - sqlCall = "(" + args[0].CommandText + " in " + obj.CommandText + ")"; - } - } - else if (call.Method.Name == "StartsWith" && args.Length >= 1) { - var startsWithCmpOp = StringComparison.CurrentCulture; - if (args.Length == 2) { - startsWithCmpOp = (StringComparison)args[1].Value; - } - switch (startsWithCmpOp) { - case StringComparison.Ordinal: - case StringComparison.CurrentCulture: - sqlCall = "( substr(" + obj.CommandText + ", 1, " + args[0].Value.ToString ().Length + ") = " + args[0].CommandText + ")"; - break; - case StringComparison.OrdinalIgnoreCase: - case StringComparison.CurrentCultureIgnoreCase: - sqlCall = "(" + obj.CommandText + " like (" + args[0].CommandText + " || '%'))"; - break; - } - - } - else if (call.Method.Name == "EndsWith" && args.Length >= 1) { - var endsWithCmpOp = StringComparison.CurrentCulture; - if (args.Length == 2) { - endsWithCmpOp = (StringComparison)args[1].Value; - } - switch (endsWithCmpOp) { - case StringComparison.Ordinal: - case StringComparison.CurrentCulture: - sqlCall = "( substr(" + obj.CommandText + ", length(" + obj.CommandText + ") - " + args[0].Value.ToString ().Length + "+1, " + args[0].Value.ToString ().Length + ") = " + args[0].CommandText + ")"; - break; - case StringComparison.OrdinalIgnoreCase: - case StringComparison.CurrentCultureIgnoreCase: - sqlCall = "(" + obj.CommandText + " like ('%' || " + args[0].CommandText + "))"; - break; - } - } - else if (call.Method.Name == "Equals" && args.Length == 1) { - sqlCall = "(" + obj.CommandText + " = (" + args[0].CommandText + "))"; - } - else if (call.Method.Name == "ToLower") { - sqlCall = "(lower(" + obj.CommandText + "))"; - } - else if (call.Method.Name == "ToUpper") { - sqlCall = "(upper(" + obj.CommandText + "))"; - } - else if (call.Method.Name == "Replace" && args.Length == 2) { - sqlCall = "(replace(" + obj.CommandText + "," + args[0].CommandText + "," + args[1].CommandText + "))"; - } - else if (call.Method.Name == "IsNullOrEmpty" && args.Length == 1) { - sqlCall = "(" + args[0].CommandText + " is null or" + args[0].CommandText + " ='' )"; - } - else { - sqlCall = call.Method.Name.ToLower () + "(" + string.Join (",", args.Select (a => a.CommandText).ToArray ()) + ")"; - } - return new CompileResult { CommandText = sqlCall }; - - } - else if (expr.NodeType == ExpressionType.Constant) { - var c = (ConstantExpression)expr; - queryArgs.Add (c.Value); - return new CompileResult { - CommandText = "?", - Value = c.Value - }; - } - else if (expr.NodeType == ExpressionType.Convert) { - var u = (UnaryExpression)expr; - var ty = u.Type; - var valr = CompileExpr (u.Operand, queryArgs); - return new CompileResult { - CommandText = valr.CommandText, - Value = valr.Value != null ? ConvertTo (valr.Value, ty) : null - }; - } - else if (expr.NodeType == ExpressionType.MemberAccess) { - var mem = (MemberExpression)expr; - - var paramExpr = mem.Expression as ParameterExpression; - if (paramExpr == null) { - var convert = mem.Expression as UnaryExpression; - if (convert != null && convert.NodeType == ExpressionType.Convert) { - paramExpr = convert.Operand as ParameterExpression; - } - } - - if (paramExpr != null) { - // - // This is a column of our table, output just the column name - // Need to translate it if that column name is mapped - // - var columnName = Table.FindColumnWithPropertyName (mem.Member.Name).Name; - return new CompileResult { CommandText = "\"" + columnName + "\"" }; - } - else { - object obj = null; - if (mem.Expression != null) { - var r = CompileExpr (mem.Expression, queryArgs); - if (r.Value == null) { - throw new NotSupportedException ("Member access failed to compile expression"); - } - if (r.CommandText == "?") { - queryArgs.RemoveAt (queryArgs.Count - 1); - } - obj = r.Value; - } - - // - // Get the member value - // - object val = null; - - if (mem.Member is PropertyInfo) { - var m = (PropertyInfo)mem.Member; - val = m.GetValue (obj, null); - } - else if (mem.Member is FieldInfo) { - var m = (FieldInfo)mem.Member; - val = m.GetValue (obj); - } - else { - throw new NotSupportedException ("MemberExpr: " + mem.Member.GetType ()); - } - - // - // Work special magic for enumerables - // - if (val != null && val is System.Collections.IEnumerable && !(val is string) && !(val is System.Collections.Generic.IEnumerable)) { - var sb = new System.Text.StringBuilder (); - sb.Append ("("); - var head = ""; - foreach (var a in (System.Collections.IEnumerable)val) { - queryArgs.Add (a); - sb.Append (head); - sb.Append ("?"); - head = ","; - } - sb.Append (")"); - return new CompileResult { - CommandText = sb.ToString (), - Value = val - }; - } - else { - queryArgs.Add (val); - return new CompileResult { - CommandText = "?", - Value = val - }; - } - } - } - throw new NotSupportedException ("Cannot compile: " + expr.NodeType.ToString ()); - } - - static object ConvertTo (object obj, Type t) - { - Type nut = Nullable.GetUnderlyingType (t); - - if (nut != null) { - if (obj == null) - return null; - return Convert.ChangeType (obj, nut); - } - else { - return Convert.ChangeType (obj, t); - } - } - - /// - /// Compiles a BinaryExpression where one of the parameters is null. - /// - /// The expression to compile - /// The non-null parameter - private string CompileNullBinaryExpression (BinaryExpression expression, CompileResult parameter) - { - if (expression.NodeType == ExpressionType.Equal) - return "(" + parameter.CommandText + " is ?)"; - else if (expression.NodeType == ExpressionType.NotEqual) - return "(" + parameter.CommandText + " is not ?)"; - else if (expression.NodeType == ExpressionType.GreaterThan - || expression.NodeType == ExpressionType.GreaterThanOrEqual - || expression.NodeType == ExpressionType.LessThan - || expression.NodeType == ExpressionType.LessThanOrEqual) - return "(" + parameter.CommandText + " < ?)"; // always false - else - throw new NotSupportedException ("Cannot compile Null-BinaryExpression with type " + expression.NodeType.ToString ()); - } - - string GetSqlName (Expression expr) - { - var n = expr.NodeType; - if (n == ExpressionType.GreaterThan) - return ">"; - else if (n == ExpressionType.GreaterThanOrEqual) { - return ">="; - } - else if (n == ExpressionType.LessThan) { - return "<"; - } - else if (n == ExpressionType.LessThanOrEqual) { - return "<="; - } - else if (n == ExpressionType.And) { - return "&"; - } - else if (n == ExpressionType.AndAlso) { - return "and"; - } - else if (n == ExpressionType.Or) { - return "|"; - } - else if (n == ExpressionType.OrElse) { - return "or"; - } - else if (n == ExpressionType.Equal) { - return "="; - } - else if (n == ExpressionType.NotEqual) { - return "!="; - } - else { - throw new NotSupportedException ("Cannot get SQL for: " + n); - } - } - - /// - /// Execute SELECT COUNT(*) on the query - /// - public int Count () - { - return GenerateCommand ("count(*)").ExecuteScalar (); - } - - /// - /// Execute SELECT COUNT(*) on the query with an additional WHERE clause. - /// - public int Count (Expression> predExpr) - { - return Where (predExpr).Count (); - } - - public IEnumerator GetEnumerator () - { - if (!_deferred) - return GenerateCommand ("*").ExecuteQuery ().GetEnumerator (); - - return GenerateCommand ("*").ExecuteDeferredQuery ().GetEnumerator (); - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () - { - return GetEnumerator (); - } - - /// - /// Queries the database and returns the results as a List. - /// - public List ToList () - { - return GenerateCommand ("*").ExecuteQuery (); - } - - /// - /// Queries the database and returns the results as an array. - /// - public T[] ToArray () - { - return GenerateCommand ("*").ExecuteQuery ().ToArray (); - } - - /// - /// Returns the first element of this query. - /// - public T First () - { - var query = Take (1); - return query.ToList ().First (); - } - - /// - /// Returns the first element of this query, or null if no element is found. - /// - public T FirstOrDefault () - { - var query = Take (1); - return query.ToList ().FirstOrDefault (); - } - - /// - /// Returns the first element of this query that matches the predicate. - /// - public T First (Expression> predExpr) - { - return Where (predExpr).First (); - } - - /// - /// Returns the first element of this query that matches the predicate, or null - /// if no element is found. - /// - public T FirstOrDefault (Expression> predExpr) - { - return Where (predExpr).FirstOrDefault (); - } - } - - public static class SQLite3 - { - public enum Result : int - { - OK = 0, - Error = 1, - Internal = 2, - Perm = 3, - Abort = 4, - Busy = 5, - Locked = 6, - NoMem = 7, - ReadOnly = 8, - Interrupt = 9, - IOError = 10, - Corrupt = 11, - NotFound = 12, - Full = 13, - CannotOpen = 14, - LockErr = 15, - Empty = 16, - SchemaChngd = 17, - TooBig = 18, - Constraint = 19, - Mismatch = 20, - Misuse = 21, - NotImplementedLFS = 22, - AccessDenied = 23, - Format = 24, - Range = 25, - NonDBFile = 26, - Notice = 27, - Warning = 28, - Row = 100, - Done = 101 - } - - public enum ExtendedResult : int - { - IOErrorRead = (Result.IOError | (1 << 8)), - IOErrorShortRead = (Result.IOError | (2 << 8)), - IOErrorWrite = (Result.IOError | (3 << 8)), - IOErrorFsync = (Result.IOError | (4 << 8)), - IOErrorDirFSync = (Result.IOError | (5 << 8)), - IOErrorTruncate = (Result.IOError | (6 << 8)), - IOErrorFStat = (Result.IOError | (7 << 8)), - IOErrorUnlock = (Result.IOError | (8 << 8)), - IOErrorRdlock = (Result.IOError | (9 << 8)), - IOErrorDelete = (Result.IOError | (10 << 8)), - IOErrorBlocked = (Result.IOError | (11 << 8)), - IOErrorNoMem = (Result.IOError | (12 << 8)), - IOErrorAccess = (Result.IOError | (13 << 8)), - IOErrorCheckReservedLock = (Result.IOError | (14 << 8)), - IOErrorLock = (Result.IOError | (15 << 8)), - IOErrorClose = (Result.IOError | (16 << 8)), - IOErrorDirClose = (Result.IOError | (17 << 8)), - IOErrorSHMOpen = (Result.IOError | (18 << 8)), - IOErrorSHMSize = (Result.IOError | (19 << 8)), - IOErrorSHMLock = (Result.IOError | (20 << 8)), - IOErrorSHMMap = (Result.IOError | (21 << 8)), - IOErrorSeek = (Result.IOError | (22 << 8)), - IOErrorDeleteNoEnt = (Result.IOError | (23 << 8)), - IOErrorMMap = (Result.IOError | (24 << 8)), - LockedSharedcache = (Result.Locked | (1 << 8)), - BusyRecovery = (Result.Busy | (1 << 8)), - CannottOpenNoTempDir = (Result.CannotOpen | (1 << 8)), - CannotOpenIsDir = (Result.CannotOpen | (2 << 8)), - CannotOpenFullPath = (Result.CannotOpen | (3 << 8)), - CorruptVTab = (Result.Corrupt | (1 << 8)), - ReadonlyRecovery = (Result.ReadOnly | (1 << 8)), - ReadonlyCannotLock = (Result.ReadOnly | (2 << 8)), - ReadonlyRollback = (Result.ReadOnly | (3 << 8)), - AbortRollback = (Result.Abort | (2 << 8)), - ConstraintCheck = (Result.Constraint | (1 << 8)), - ConstraintCommitHook = (Result.Constraint | (2 << 8)), - ConstraintForeignKey = (Result.Constraint | (3 << 8)), - ConstraintFunction = (Result.Constraint | (4 << 8)), - ConstraintNotNull = (Result.Constraint | (5 << 8)), - ConstraintPrimaryKey = (Result.Constraint | (6 << 8)), - ConstraintTrigger = (Result.Constraint | (7 << 8)), - ConstraintUnique = (Result.Constraint | (8 << 8)), - ConstraintVTab = (Result.Constraint | (9 << 8)), - NoticeRecoverWAL = (Result.Notice | (1 << 8)), - NoticeRecoverRollback = (Result.Notice | (2 << 8)) - } - - - public enum ConfigOption : int - { - SingleThread = 1, - MultiThread = 2, - Serialized = 3 - } - - const string LibraryPath = "sqlite3"; + private CompileResult CompileExpr (Expression expr, List queryArgs) + { + if (expr == null) { + throw new NotSupportedException ("Expression is NULL"); + } + else if (expr is BinaryExpression) { + var bin = (BinaryExpression)expr; + + // VB turns 'x=="foo"' into 'CompareString(x,"foo",true/false)==0', so we need to unwrap it + // http://blogs.msdn.com/b/vbteam/archive/2007/09/18/vb-expression-trees-string-comparisons.aspx + if (bin.Left.NodeType == ExpressionType.Call) { + var call = (MethodCallExpression)bin.Left; + if (call.Method.DeclaringType.FullName == "Microsoft.VisualBasic.CompilerServices.Operators" + && call.Method.Name == "CompareString") + bin = Expression.MakeBinary (bin.NodeType, call.Arguments[0], call.Arguments[1]); + } + + + var leftr = CompileExpr (bin.Left, queryArgs); + var rightr = CompileExpr (bin.Right, queryArgs); + + //If either side is a parameter and is null, then handle the other side specially (for "is null"/"is not null") + string text; + if (leftr.CommandText == "?" && leftr.Value == null) + text = CompileNullBinaryExpression (bin, rightr); + else if (rightr.CommandText == "?" && rightr.Value == null) + text = CompileNullBinaryExpression (bin, leftr); + else + text = "(" + leftr.CommandText + " " + GetSqlName (bin) + " " + rightr.CommandText + ")"; + return new CompileResult { CommandText = text }; + } + else if (expr.NodeType == ExpressionType.Not) { + var operandExpr = ((UnaryExpression)expr).Operand; + var opr = CompileExpr (operandExpr, queryArgs); + object val = opr.Value; + if (val is bool) + val = !((bool)val); + return new CompileResult { + CommandText = "NOT(" + opr.CommandText + ")", + Value = val + }; + } + else if (expr.NodeType == ExpressionType.Call) { + + var call = (MethodCallExpression)expr; + var args = new CompileResult[call.Arguments.Count]; + var obj = call.Object != null ? CompileExpr (call.Object, queryArgs) : null; + + for (var i = 0; i < args.Length; i++) { + args[i] = CompileExpr (call.Arguments[i], queryArgs); + } + + var sqlCall = ""; + + if (call.Method.Name == "Like" && args.Length == 2) { + sqlCall = "(" + args[0].CommandText + " like " + args[1].CommandText + ")"; + } + else if (call.Method.Name == "Contains" && args.Length == 2) { + sqlCall = "(" + args[1].CommandText + " in " + args[0].CommandText + ")"; + } + else if (call.Method.Name == "Contains" && args.Length == 1) { + if (call.Object != null && call.Object.Type == typeof (string)) { + sqlCall = "( instr(" + obj.CommandText + "," + args[0].CommandText + ") >0 )"; + } + else { + sqlCall = "(" + args[0].CommandText + " in " + obj.CommandText + ")"; + } + } + else if (call.Method.Name == "StartsWith" && args.Length >= 1) { + var startsWithCmpOp = StringComparison.CurrentCulture; + if (args.Length == 2) { + startsWithCmpOp = (StringComparison)args[1].Value; + } + switch (startsWithCmpOp) { + case StringComparison.Ordinal: + case StringComparison.CurrentCulture: + sqlCall = "( substr(" + obj.CommandText + ", 1, " + args[0].Value.ToString ().Length + ") = " + args[0].CommandText + ")"; + break; + case StringComparison.OrdinalIgnoreCase: + case StringComparison.CurrentCultureIgnoreCase: + sqlCall = "(" + obj.CommandText + " like (" + args[0].CommandText + " || '%'))"; + break; + } + + } + else if (call.Method.Name == "EndsWith" && args.Length >= 1) { + var endsWithCmpOp = StringComparison.CurrentCulture; + if (args.Length == 2) { + endsWithCmpOp = (StringComparison)args[1].Value; + } + switch (endsWithCmpOp) { + case StringComparison.Ordinal: + case StringComparison.CurrentCulture: + sqlCall = "( substr(" + obj.CommandText + ", length(" + obj.CommandText + ") - " + args[0].Value.ToString ().Length + "+1, " + args[0].Value.ToString ().Length + ") = " + args[0].CommandText + ")"; + break; + case StringComparison.OrdinalIgnoreCase: + case StringComparison.CurrentCultureIgnoreCase: + sqlCall = "(" + obj.CommandText + " like ('%' || " + args[0].CommandText + "))"; + break; + } + } + else if (call.Method.Name == "Equals" && args.Length == 1) { + sqlCall = "(" + obj.CommandText + " = (" + args[0].CommandText + "))"; + } + else if (call.Method.Name == "ToLower") { + sqlCall = "(lower(" + obj.CommandText + "))"; + } + else if (call.Method.Name == "ToUpper") { + sqlCall = "(upper(" + obj.CommandText + "))"; + } + else if (call.Method.Name == "Replace" && args.Length == 2) { + sqlCall = "(replace(" + obj.CommandText + "," + args[0].CommandText + "," + args[1].CommandText + "))"; + } + else if (call.Method.Name == "IsNullOrEmpty" && args.Length == 1) { + sqlCall = "(" + args[0].CommandText + " is null or" + args[0].CommandText + " ='' )"; + } + else { + sqlCall = call.Method.Name.ToLower () + "(" + string.Join (",", args.Select (a => a.CommandText).ToArray ()) + ")"; + } + return new CompileResult { CommandText = sqlCall }; + + } + else if (expr.NodeType == ExpressionType.Constant) { + var c = (ConstantExpression)expr; + queryArgs.Add (c.Value); + return new CompileResult { + CommandText = "?", + Value = c.Value + }; + } + else if (expr.NodeType == ExpressionType.Convert) { + var u = (UnaryExpression)expr; + var ty = u.Type; + var valr = CompileExpr (u.Operand, queryArgs); + return new CompileResult { + CommandText = valr.CommandText, + Value = valr.Value != null ? ConvertTo (valr.Value, ty) : null + }; + } + else if (expr.NodeType == ExpressionType.MemberAccess) { + var mem = (MemberExpression)expr; + + var paramExpr = mem.Expression as ParameterExpression; + if (paramExpr == null) { + var convert = mem.Expression as UnaryExpression; + if (convert != null && convert.NodeType == ExpressionType.Convert) { + paramExpr = convert.Operand as ParameterExpression; + } + } + + if (paramExpr != null) { + // + // This is a column of our table, output just the column name + // Need to translate it if that column name is mapped + // + var columnName = Table.FindColumnWithPropertyName (mem.Member.Name).Name; + return new CompileResult { CommandText = "\"" + columnName + "\"" }; + } + else { + object obj = null; + if (mem.Expression != null) { + var r = CompileExpr (mem.Expression, queryArgs); + if (r.Value == null) { + throw new NotSupportedException ("Member access failed to compile expression"); + } + if (r.CommandText == "?") { + queryArgs.RemoveAt (queryArgs.Count - 1); + } + obj = r.Value; + } + + // + // Get the member value + // + object val = null; + + if (mem.Member is PropertyInfo) { + var m = (PropertyInfo)mem.Member; + val = m.GetValue (obj, null); + } + else if (mem.Member is FieldInfo) { + var m = (FieldInfo)mem.Member; + val = m.GetValue (obj); + } + else { + throw new NotSupportedException ("MemberExpr: " + mem.Member.GetType ()); + } + + // + // Work special magic for enumerables + // + if (val != null && val is System.Collections.IEnumerable && !(val is string) && !(val is System.Collections.Generic.IEnumerable)) { + var sb = new System.Text.StringBuilder (); + sb.Append ("("); + var head = ""; + foreach (var a in (System.Collections.IEnumerable)val) { + queryArgs.Add (a); + sb.Append (head); + sb.Append ("?"); + head = ","; + } + sb.Append (")"); + return new CompileResult { + CommandText = sb.ToString (), + Value = val + }; + } + else { + queryArgs.Add (val); + return new CompileResult { + CommandText = "?", + Value = val + }; + } + } + } + throw new NotSupportedException ("Cannot compile: " + expr.NodeType.ToString ()); + } + + static object ConvertTo (object obj, Type t) + { + Type nut = Nullable.GetUnderlyingType (t); + + if (nut != null) { + if (obj == null) + return null; + return Convert.ChangeType (obj, nut); + } + else { + return Convert.ChangeType (obj, t); + } + } + + /// + /// Compiles a BinaryExpression where one of the parameters is null. + /// + /// The expression to compile + /// The non-null parameter + private string CompileNullBinaryExpression (BinaryExpression expression, CompileResult parameter) + { + if (expression.NodeType == ExpressionType.Equal) + return "(" + parameter.CommandText + " is ?)"; + else if (expression.NodeType == ExpressionType.NotEqual) + return "(" + parameter.CommandText + " is not ?)"; + else if (expression.NodeType == ExpressionType.GreaterThan + || expression.NodeType == ExpressionType.GreaterThanOrEqual + || expression.NodeType == ExpressionType.LessThan + || expression.NodeType == ExpressionType.LessThanOrEqual) + return "(" + parameter.CommandText + " < ?)"; // always false + else + throw new NotSupportedException ("Cannot compile Null-BinaryExpression with type " + expression.NodeType.ToString ()); + } + + string GetSqlName (Expression expr) + { + var n = expr.NodeType; + if (n == ExpressionType.GreaterThan) + return ">"; + else if (n == ExpressionType.GreaterThanOrEqual) { + return ">="; + } + else if (n == ExpressionType.LessThan) { + return "<"; + } + else if (n == ExpressionType.LessThanOrEqual) { + return "<="; + } + else if (n == ExpressionType.And) { + return "&"; + } + else if (n == ExpressionType.AndAlso) { + return "and"; + } + else if (n == ExpressionType.Or) { + return "|"; + } + else if (n == ExpressionType.OrElse) { + return "or"; + } + else if (n == ExpressionType.Equal) { + return "="; + } + else if (n == ExpressionType.NotEqual) { + return "!="; + } + else { + throw new NotSupportedException ("Cannot get SQL for: " + n); + } + } + + /// + /// Execute SELECT COUNT(*) on the query + /// + public int Count () + { + return GenerateCommand ("count(*)").ExecuteScalar (); + } + + /// + /// Execute SELECT COUNT(*) on the query with an additional WHERE clause. + /// + public int Count (Expression> predExpr) + { + return Where (predExpr).Count (); + } + + public IEnumerator GetEnumerator () + { + if (!_deferred) + return GenerateCommand ("*").ExecuteQuery ().GetEnumerator (); + + return GenerateCommand ("*").ExecuteDeferredQuery ().GetEnumerator (); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + + /// + /// Queries the database and returns the results as a List. + /// + public List ToList () + { + return GenerateCommand ("*").ExecuteQuery (); + } + + /// + /// Queries the database and returns the results as an array. + /// + public T[] ToArray () + { + return GenerateCommand ("*").ExecuteQuery ().ToArray (); + } + + /// + /// Returns the first element of this query. + /// + public T First () + { + var query = Take (1); + return query.ToList ().First (); + } + + /// + /// Returns the first element of this query, or null if no element is found. + /// + public T FirstOrDefault () + { + var query = Take (1); + return query.ToList ().FirstOrDefault (); + } + + /// + /// Returns the first element of this query that matches the predicate. + /// + public T First (Expression> predExpr) + { + return Where (predExpr).First (); + } + + /// + /// Returns the first element of this query that matches the predicate, or null + /// if no element is found. + /// + public T FirstOrDefault (Expression> predExpr) + { + return Where (predExpr).FirstOrDefault (); + } + } + + public static class SQLite3 + { + public enum Result : int + { + OK = 0, + Error = 1, + Internal = 2, + Perm = 3, + Abort = 4, + Busy = 5, + Locked = 6, + NoMem = 7, + ReadOnly = 8, + Interrupt = 9, + IOError = 10, + Corrupt = 11, + NotFound = 12, + Full = 13, + CannotOpen = 14, + LockErr = 15, + Empty = 16, + SchemaChngd = 17, + TooBig = 18, + Constraint = 19, + Mismatch = 20, + Misuse = 21, + NotImplementedLFS = 22, + AccessDenied = 23, + Format = 24, + Range = 25, + NonDBFile = 26, + Notice = 27, + Warning = 28, + Row = 100, + Done = 101 + } + + public enum ExtendedResult : int + { + IOErrorRead = (Result.IOError | (1 << 8)), + IOErrorShortRead = (Result.IOError | (2 << 8)), + IOErrorWrite = (Result.IOError | (3 << 8)), + IOErrorFsync = (Result.IOError | (4 << 8)), + IOErrorDirFSync = (Result.IOError | (5 << 8)), + IOErrorTruncate = (Result.IOError | (6 << 8)), + IOErrorFStat = (Result.IOError | (7 << 8)), + IOErrorUnlock = (Result.IOError | (8 << 8)), + IOErrorRdlock = (Result.IOError | (9 << 8)), + IOErrorDelete = (Result.IOError | (10 << 8)), + IOErrorBlocked = (Result.IOError | (11 << 8)), + IOErrorNoMem = (Result.IOError | (12 << 8)), + IOErrorAccess = (Result.IOError | (13 << 8)), + IOErrorCheckReservedLock = (Result.IOError | (14 << 8)), + IOErrorLock = (Result.IOError | (15 << 8)), + IOErrorClose = (Result.IOError | (16 << 8)), + IOErrorDirClose = (Result.IOError | (17 << 8)), + IOErrorSHMOpen = (Result.IOError | (18 << 8)), + IOErrorSHMSize = (Result.IOError | (19 << 8)), + IOErrorSHMLock = (Result.IOError | (20 << 8)), + IOErrorSHMMap = (Result.IOError | (21 << 8)), + IOErrorSeek = (Result.IOError | (22 << 8)), + IOErrorDeleteNoEnt = (Result.IOError | (23 << 8)), + IOErrorMMap = (Result.IOError | (24 << 8)), + LockedSharedcache = (Result.Locked | (1 << 8)), + BusyRecovery = (Result.Busy | (1 << 8)), + CannottOpenNoTempDir = (Result.CannotOpen | (1 << 8)), + CannotOpenIsDir = (Result.CannotOpen | (2 << 8)), + CannotOpenFullPath = (Result.CannotOpen | (3 << 8)), + CorruptVTab = (Result.Corrupt | (1 << 8)), + ReadonlyRecovery = (Result.ReadOnly | (1 << 8)), + ReadonlyCannotLock = (Result.ReadOnly | (2 << 8)), + ReadonlyRollback = (Result.ReadOnly | (3 << 8)), + AbortRollback = (Result.Abort | (2 << 8)), + ConstraintCheck = (Result.Constraint | (1 << 8)), + ConstraintCommitHook = (Result.Constraint | (2 << 8)), + ConstraintForeignKey = (Result.Constraint | (3 << 8)), + ConstraintFunction = (Result.Constraint | (4 << 8)), + ConstraintNotNull = (Result.Constraint | (5 << 8)), + ConstraintPrimaryKey = (Result.Constraint | (6 << 8)), + ConstraintTrigger = (Result.Constraint | (7 << 8)), + ConstraintUnique = (Result.Constraint | (8 << 8)), + ConstraintVTab = (Result.Constraint | (9 << 8)), + NoticeRecoverWAL = (Result.Notice | (1 << 8)), + NoticeRecoverRollback = (Result.Notice | (2 << 8)) + } + + + public enum ConfigOption : int + { + SingleThread = 1, + MultiThread = 2, + Serialized = 3 + } + + const string LibraryPath = "sqlite3"; #if !USE_CSHARP_SQLITE && !USE_WP8_NATIVE_SQLITE && !USE_SQLITEPCL_RAW - [DllImport(LibraryPath, EntryPoint = "sqlite3_threadsafe", CallingConvention=CallingConvention.Cdecl)] - public static extern int Threadsafe (); + [DllImport(LibraryPath, EntryPoint = "sqlite3_threadsafe", CallingConvention=CallingConvention.Cdecl)] + public static extern int Threadsafe (); - [DllImport(LibraryPath, EntryPoint = "sqlite3_open", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Open ([MarshalAs(UnmanagedType.LPStr)] string filename, out IntPtr db); + [DllImport(LibraryPath, EntryPoint = "sqlite3_open", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Open ([MarshalAs(UnmanagedType.LPStr)] string filename, out IntPtr db); - [DllImport(LibraryPath, EntryPoint = "sqlite3_open_v2", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Open ([MarshalAs(UnmanagedType.LPStr)] string filename, out IntPtr db, int flags, [MarshalAs (UnmanagedType.LPStr)] string zvfs); + [DllImport(LibraryPath, EntryPoint = "sqlite3_open_v2", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Open ([MarshalAs(UnmanagedType.LPStr)] string filename, out IntPtr db, int flags, [MarshalAs (UnmanagedType.LPStr)] string zvfs); - [DllImport(LibraryPath, EntryPoint = "sqlite3_open_v2", CallingConvention = CallingConvention.Cdecl)] - public static extern Result Open(byte[] filename, out IntPtr db, int flags, [MarshalAs (UnmanagedType.LPStr)] string zvfs); + [DllImport(LibraryPath, EntryPoint = "sqlite3_open_v2", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Open(byte[] filename, out IntPtr db, int flags, [MarshalAs (UnmanagedType.LPStr)] string zvfs); - [DllImport(LibraryPath, EntryPoint = "sqlite3_open16", CallingConvention = CallingConvention.Cdecl)] - public static extern Result Open16([MarshalAs(UnmanagedType.LPWStr)] string filename, out IntPtr db); + [DllImport(LibraryPath, EntryPoint = "sqlite3_open16", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Open16([MarshalAs(UnmanagedType.LPWStr)] string filename, out IntPtr db); - [DllImport(LibraryPath, EntryPoint = "sqlite3_enable_load_extension", CallingConvention=CallingConvention.Cdecl)] - public static extern Result EnableLoadExtension (IntPtr db, int onoff); + [DllImport(LibraryPath, EntryPoint = "sqlite3_enable_load_extension", CallingConvention=CallingConvention.Cdecl)] + public static extern Result EnableLoadExtension (IntPtr db, int onoff); - [DllImport(LibraryPath, EntryPoint = "sqlite3_close", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Close (IntPtr db); + [DllImport(LibraryPath, EntryPoint = "sqlite3_close", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Close (IntPtr db); - [DllImport(LibraryPath, EntryPoint = "sqlite3_close_v2", CallingConvention = CallingConvention.Cdecl)] - public static extern Result Close2(IntPtr db); + [DllImport(LibraryPath, EntryPoint = "sqlite3_close_v2", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Close2(IntPtr db); - [DllImport(LibraryPath, EntryPoint = "sqlite3_initialize", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Initialize(); + [DllImport(LibraryPath, EntryPoint = "sqlite3_initialize", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Initialize(); - [DllImport(LibraryPath, EntryPoint = "sqlite3_shutdown", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Shutdown(); + [DllImport(LibraryPath, EntryPoint = "sqlite3_shutdown", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Shutdown(); - [DllImport(LibraryPath, EntryPoint = "sqlite3_config", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Config (ConfigOption option); + [DllImport(LibraryPath, EntryPoint = "sqlite3_config", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Config (ConfigOption option); - [DllImport(LibraryPath, EntryPoint = "sqlite3_win32_set_directory", CallingConvention=CallingConvention.Cdecl, CharSet=CharSet.Unicode)] - public static extern int SetDirectory (uint directoryType, string directoryPath); + [DllImport(LibraryPath, EntryPoint = "sqlite3_win32_set_directory", CallingConvention=CallingConvention.Cdecl, CharSet=CharSet.Unicode)] + public static extern int SetDirectory (uint directoryType, string directoryPath); - [DllImport(LibraryPath, EntryPoint = "sqlite3_busy_timeout", CallingConvention=CallingConvention.Cdecl)] - public static extern Result BusyTimeout (IntPtr db, int milliseconds); + [DllImport(LibraryPath, EntryPoint = "sqlite3_busy_timeout", CallingConvention=CallingConvention.Cdecl)] + public static extern Result BusyTimeout (IntPtr db, int milliseconds); - [DllImport(LibraryPath, EntryPoint = "sqlite3_changes", CallingConvention=CallingConvention.Cdecl)] - public static extern int Changes (IntPtr db); + [DllImport(LibraryPath, EntryPoint = "sqlite3_changes", CallingConvention=CallingConvention.Cdecl)] + public static extern int Changes (IntPtr db); - [DllImport(LibraryPath, EntryPoint = "sqlite3_prepare_v2", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Prepare2 (IntPtr db, [MarshalAs(UnmanagedType.LPStr)] string sql, int numBytes, out IntPtr stmt, IntPtr pzTail); + [DllImport(LibraryPath, EntryPoint = "sqlite3_prepare_v2", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Prepare2 (IntPtr db, [MarshalAs(UnmanagedType.LPStr)] string sql, int numBytes, out IntPtr stmt, IntPtr pzTail); #if NETFX_CORE - [DllImport (LibraryPath, EntryPoint = "sqlite3_prepare_v2", CallingConvention = CallingConvention.Cdecl)] - public static extern Result Prepare2 (IntPtr db, byte[] queryBytes, int numBytes, out IntPtr stmt, IntPtr pzTail); + [DllImport (LibraryPath, EntryPoint = "sqlite3_prepare_v2", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Prepare2 (IntPtr db, byte[] queryBytes, int numBytes, out IntPtr stmt, IntPtr pzTail); #endif - public static IntPtr Prepare2 (IntPtr db, string query) - { - IntPtr stmt; + public static IntPtr Prepare2 (IntPtr db, string query) + { + IntPtr stmt; #if NETFX_CORE byte[] queryBytes = System.Text.UTF8Encoding.UTF8.GetBytes (query); var r = Prepare2 (db, queryBytes, queryBytes.Length, out stmt, IntPtr.Zero); #else var r = Prepare2 (db, query, System.Text.UTF8Encoding.UTF8.GetByteCount (query), out stmt, IntPtr.Zero); #endif - if (r != Result.OK) { - throw SQLiteException.New (r, GetErrmsg (db)); - } - return stmt; - } + if (r != Result.OK) { + throw SQLiteException.New (r, GetErrmsg (db)); + } + return stmt; + } - [DllImport(LibraryPath, EntryPoint = "sqlite3_step", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Step (IntPtr stmt); + [DllImport(LibraryPath, EntryPoint = "sqlite3_step", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Step (IntPtr stmt); - [DllImport(LibraryPath, EntryPoint = "sqlite3_reset", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Reset (IntPtr stmt); + [DllImport(LibraryPath, EntryPoint = "sqlite3_reset", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Reset (IntPtr stmt); - [DllImport(LibraryPath, EntryPoint = "sqlite3_finalize", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Finalize (IntPtr stmt); + [DllImport(LibraryPath, EntryPoint = "sqlite3_finalize", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Finalize (IntPtr stmt); - [DllImport(LibraryPath, EntryPoint = "sqlite3_last_insert_rowid", CallingConvention=CallingConvention.Cdecl)] - public static extern long LastInsertRowid (IntPtr db); + [DllImport(LibraryPath, EntryPoint = "sqlite3_last_insert_rowid", CallingConvention=CallingConvention.Cdecl)] + public static extern long LastInsertRowid (IntPtr db); - [DllImport(LibraryPath, EntryPoint = "sqlite3_errmsg16", CallingConvention=CallingConvention.Cdecl)] - public static extern IntPtr Errmsg (IntPtr db); + [DllImport(LibraryPath, EntryPoint = "sqlite3_errmsg16", CallingConvention=CallingConvention.Cdecl)] + public static extern IntPtr Errmsg (IntPtr db); - public static string GetErrmsg (IntPtr db) - { - return Marshal.PtrToStringUni (Errmsg (db)); - } + public static string GetErrmsg (IntPtr db) + { + return Marshal.PtrToStringUni (Errmsg (db)); + } - [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_parameter_index", CallingConvention=CallingConvention.Cdecl)] - public static extern int BindParameterIndex (IntPtr stmt, [MarshalAs(UnmanagedType.LPStr)] string name); + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_parameter_index", CallingConvention=CallingConvention.Cdecl)] + public static extern int BindParameterIndex (IntPtr stmt, [MarshalAs(UnmanagedType.LPStr)] string name); - [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_null", CallingConvention=CallingConvention.Cdecl)] - public static extern int BindNull (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_null", CallingConvention=CallingConvention.Cdecl)] + public static extern int BindNull (IntPtr stmt, int index); - [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_int", CallingConvention=CallingConvention.Cdecl)] - public static extern int BindInt (IntPtr stmt, int index, int val); + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_int", CallingConvention=CallingConvention.Cdecl)] + public static extern int BindInt (IntPtr stmt, int index, int val); - [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_int64", CallingConvention=CallingConvention.Cdecl)] - public static extern int BindInt64 (IntPtr stmt, int index, long val); + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_int64", CallingConvention=CallingConvention.Cdecl)] + public static extern int BindInt64 (IntPtr stmt, int index, long val); - [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_double", CallingConvention=CallingConvention.Cdecl)] - public static extern int BindDouble (IntPtr stmt, int index, double val); + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_double", CallingConvention=CallingConvention.Cdecl)] + public static extern int BindDouble (IntPtr stmt, int index, double val); - [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_text16", CallingConvention=CallingConvention.Cdecl, CharSet = CharSet.Unicode)] - public static extern int BindText (IntPtr stmt, int index, [MarshalAs(UnmanagedType.LPWStr)] string val, int n, IntPtr free); + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_text16", CallingConvention=CallingConvention.Cdecl, CharSet = CharSet.Unicode)] + public static extern int BindText (IntPtr stmt, int index, [MarshalAs(UnmanagedType.LPWStr)] string val, int n, IntPtr free); - [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_blob", CallingConvention=CallingConvention.Cdecl)] - public static extern int BindBlob (IntPtr stmt, int index, byte[] val, int n, IntPtr free); + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_blob", CallingConvention=CallingConvention.Cdecl)] + public static extern int BindBlob (IntPtr stmt, int index, byte[] val, int n, IntPtr free); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_count", CallingConvention=CallingConvention.Cdecl)] - public static extern int ColumnCount (IntPtr stmt); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_count", CallingConvention=CallingConvention.Cdecl)] + public static extern int ColumnCount (IntPtr stmt); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_name", CallingConvention=CallingConvention.Cdecl)] - public static extern IntPtr ColumnName (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_name", CallingConvention=CallingConvention.Cdecl)] + public static extern IntPtr ColumnName (IntPtr stmt, int index); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_name16", CallingConvention=CallingConvention.Cdecl)] - static extern IntPtr ColumnName16Internal (IntPtr stmt, int index); - public static string ColumnName16(IntPtr stmt, int index) - { - return Marshal.PtrToStringUni(ColumnName16Internal(stmt, index)); - } + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_name16", CallingConvention=CallingConvention.Cdecl)] + static extern IntPtr ColumnName16Internal (IntPtr stmt, int index); + public static string ColumnName16(IntPtr stmt, int index) + { + return Marshal.PtrToStringUni(ColumnName16Internal(stmt, index)); + } - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_type", CallingConvention=CallingConvention.Cdecl)] - public static extern ColType ColumnType (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_type", CallingConvention=CallingConvention.Cdecl)] + public static extern ColType ColumnType (IntPtr stmt, int index); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_int", CallingConvention=CallingConvention.Cdecl)] - public static extern int ColumnInt (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_int", CallingConvention=CallingConvention.Cdecl)] + public static extern int ColumnInt (IntPtr stmt, int index); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_int64", CallingConvention=CallingConvention.Cdecl)] - public static extern long ColumnInt64 (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_int64", CallingConvention=CallingConvention.Cdecl)] + public static extern long ColumnInt64 (IntPtr stmt, int index); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_double", CallingConvention=CallingConvention.Cdecl)] - public static extern double ColumnDouble (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_double", CallingConvention=CallingConvention.Cdecl)] + public static extern double ColumnDouble (IntPtr stmt, int index); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_text", CallingConvention=CallingConvention.Cdecl)] - public static extern IntPtr ColumnText (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_text", CallingConvention=CallingConvention.Cdecl)] + public static extern IntPtr ColumnText (IntPtr stmt, int index); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_text16", CallingConvention=CallingConvention.Cdecl)] - public static extern IntPtr ColumnText16 (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_text16", CallingConvention=CallingConvention.Cdecl)] + public static extern IntPtr ColumnText16 (IntPtr stmt, int index); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_blob", CallingConvention=CallingConvention.Cdecl)] - public static extern IntPtr ColumnBlob (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_blob", CallingConvention=CallingConvention.Cdecl)] + public static extern IntPtr ColumnBlob (IntPtr stmt, int index); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_bytes", CallingConvention=CallingConvention.Cdecl)] - public static extern int ColumnBytes (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_bytes", CallingConvention=CallingConvention.Cdecl)] + public static extern int ColumnBytes (IntPtr stmt, int index); - public static string ColumnString (IntPtr stmt, int index) - { - return Marshal.PtrToStringUni (SQLite3.ColumnText16 (stmt, index)); - } + public static string ColumnString (IntPtr stmt, int index) + { + return Marshal.PtrToStringUni (SQLite3.ColumnText16 (stmt, index)); + } - public static byte[] ColumnByteArray (IntPtr stmt, int index) - { - int length = ColumnBytes (stmt, index); - var result = new byte[length]; - if (length > 0) - Marshal.Copy (ColumnBlob (stmt, index), result, 0, length); - return result; - } + public static byte[] ColumnByteArray (IntPtr stmt, int index) + { + int length = ColumnBytes (stmt, index); + var result = new byte[length]; + if (length > 0) + Marshal.Copy (ColumnBlob (stmt, index), result, 0, length); + return result; + } - [DllImport (LibraryPath, EntryPoint = "sqlite3_errcode", CallingConvention = CallingConvention.Cdecl)] - public static extern Result GetResult (Sqlite3DatabaseHandle db); + [DllImport (LibraryPath, EntryPoint = "sqlite3_errcode", CallingConvention = CallingConvention.Cdecl)] + public static extern Result GetResult (Sqlite3DatabaseHandle db); - [DllImport (LibraryPath, EntryPoint = "sqlite3_extended_errcode", CallingConvention = CallingConvention.Cdecl)] - public static extern ExtendedResult ExtendedErrCode (IntPtr db); + [DllImport (LibraryPath, EntryPoint = "sqlite3_extended_errcode", CallingConvention = CallingConvention.Cdecl)] + public static extern ExtendedResult ExtendedErrCode (IntPtr db); - [DllImport (LibraryPath, EntryPoint = "sqlite3_libversion_number", CallingConvention = CallingConvention.Cdecl)] - public static extern int LibVersionNumber (); + [DllImport (LibraryPath, EntryPoint = "sqlite3_libversion_number", CallingConvention = CallingConvention.Cdecl)] + public static extern int LibVersionNumber (); - [DllImport (LibraryPath, EntryPoint = "sqlite3_backup_init", CallingConvention = CallingConvention.Cdecl)] - public static extern Sqlite3BackupHandle BackupInit (Sqlite3DatabaseHandle destDb, [MarshalAs (UnmanagedType.LPStr)] string destName, Sqlite3DatabaseHandle sourceDb, [MarshalAs (UnmanagedType.LPStr)] string sourceName); + [DllImport (LibraryPath, EntryPoint = "sqlite3_backup_init", CallingConvention = CallingConvention.Cdecl)] + public static extern Sqlite3BackupHandle BackupInit (Sqlite3DatabaseHandle destDb, [MarshalAs (UnmanagedType.LPStr)] string destName, Sqlite3DatabaseHandle sourceDb, [MarshalAs (UnmanagedType.LPStr)] string sourceName); - [DllImport (LibraryPath, EntryPoint = "sqlite3_backup_step", CallingConvention = CallingConvention.Cdecl)] - public static extern Result BackupStep (Sqlite3BackupHandle backup, int numPages); + [DllImport (LibraryPath, EntryPoint = "sqlite3_backup_step", CallingConvention = CallingConvention.Cdecl)] + public static extern Result BackupStep (Sqlite3BackupHandle backup, int numPages); - [DllImport (LibraryPath, EntryPoint = "sqlite3_backup_finish", CallingConvention = CallingConvention.Cdecl)] - public static extern Result BackupFinish (Sqlite3BackupHandle backup); + [DllImport (LibraryPath, EntryPoint = "sqlite3_backup_finish", CallingConvention = CallingConvention.Cdecl)] + public static extern Result BackupFinish (Sqlite3BackupHandle backup); #else - public static Result Open (string filename, out Sqlite3DatabaseHandle db) - { - return (Result)Sqlite3.sqlite3_open (filename, out db); - } + public static Result Open (string filename, out Sqlite3DatabaseHandle db) + { + return (Result)Sqlite3.sqlite3_open (filename, out db); + } - public static Result Open (string filename, out Sqlite3DatabaseHandle db, int flags, string vfsName) - { + public static Result Open (string filename, out Sqlite3DatabaseHandle db, int flags, string vfsName) + { #if USE_WP8_NATIVE_SQLITE - return (Result)Sqlite3.sqlite3_open_v2(filename, out db, flags, vfsName ?? ""); + return (Result)Sqlite3.sqlite3_open_v2(filename, out db, flags, vfsName ?? ""); #else - return (Result)Sqlite3.sqlite3_open_v2 (filename, out db, flags, vfsName); + return (Result)Sqlite3.sqlite3_open_v2 (filename, out db, flags, vfsName); #endif - } - - public static Result Close (Sqlite3DatabaseHandle db) - { - return (Result)Sqlite3.sqlite3_close (db); - } - - public static Result Close2 (Sqlite3DatabaseHandle db) - { - return (Result)Sqlite3.sqlite3_close_v2 (db); - } - - public static Result BusyTimeout (Sqlite3DatabaseHandle db, int milliseconds) - { - return (Result)Sqlite3.sqlite3_busy_timeout (db, milliseconds); - } - - public static int Changes (Sqlite3DatabaseHandle db) - { - return Sqlite3.sqlite3_changes (db); - } - - public static Sqlite3Statement Prepare2 (Sqlite3DatabaseHandle db, string query) - { - Sqlite3Statement stmt = default (Sqlite3Statement); + } + + public static Result Close (Sqlite3DatabaseHandle db) + { + return (Result)Sqlite3.sqlite3_close (db); + } + + public static Result Close2 (Sqlite3DatabaseHandle db) + { + return (Result)Sqlite3.sqlite3_close_v2 (db); + } + + public static Result BusyTimeout (Sqlite3DatabaseHandle db, int milliseconds) + { + return (Result)Sqlite3.sqlite3_busy_timeout (db, milliseconds); + } + + public static int Changes (Sqlite3DatabaseHandle db) + { + return Sqlite3.sqlite3_changes (db); + } + + public static Sqlite3Statement Prepare2 (Sqlite3DatabaseHandle db, string query) + { + Sqlite3Statement stmt = default (Sqlite3Statement); #if USE_WP8_NATIVE_SQLITE || USE_SQLITEPCL_RAW - var r = Sqlite3.sqlite3_prepare_v2 (db, query, out stmt); + var r = Sqlite3.sqlite3_prepare_v2 (db, query, out stmt); #else - stmt = new Sqlite3Statement(); - var r = Sqlite3.sqlite3_prepare_v2(db, query, -1, ref stmt, 0); + stmt = new Sqlite3Statement(); + var r = Sqlite3.sqlite3_prepare_v2(db, query, -1, ref stmt, 0); #endif - if (r != 0) { - throw SQLiteException.New ((Result)r, GetErrmsg (db)); - } - return stmt; - } - - public static Result Step (Sqlite3Statement stmt) - { - return (Result)Sqlite3.sqlite3_step (stmt); - } - - public static Result Reset (Sqlite3Statement stmt) - { - return (Result)Sqlite3.sqlite3_reset (stmt); - } - - public static Result Finalize (Sqlite3Statement stmt) - { - return (Result)Sqlite3.sqlite3_finalize (stmt); - } - - public static long LastInsertRowid (Sqlite3DatabaseHandle db) - { - return Sqlite3.sqlite3_last_insert_rowid (db); - } - - public static string GetErrmsg (Sqlite3DatabaseHandle db) - { - return Sqlite3.sqlite3_errmsg (db).utf8_to_string (); - } - - public static int BindParameterIndex (Sqlite3Statement stmt, string name) - { - return Sqlite3.sqlite3_bind_parameter_index (stmt, name); - } - - public static int BindNull (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_bind_null (stmt, index); - } - - public static int BindInt (Sqlite3Statement stmt, int index, int val) - { - return Sqlite3.sqlite3_bind_int (stmt, index, val); - } - - public static int BindInt64 (Sqlite3Statement stmt, int index, long val) - { - return Sqlite3.sqlite3_bind_int64 (stmt, index, val); - } - - public static int BindDouble (Sqlite3Statement stmt, int index, double val) - { - return Sqlite3.sqlite3_bind_double (stmt, index, val); - } - - public static int BindText (Sqlite3Statement stmt, int index, string val, int n, IntPtr free) - { + if (r != 0) { + throw SQLiteException.New ((Result)r, GetErrmsg (db)); + } + return stmt; + } + + public static Result Step (Sqlite3Statement stmt) + { + return (Result)Sqlite3.sqlite3_step (stmt); + } + + public static Result Reset (Sqlite3Statement stmt) + { + return (Result)Sqlite3.sqlite3_reset (stmt); + } + + public static Result Finalize (Sqlite3Statement stmt) + { + return (Result)Sqlite3.sqlite3_finalize (stmt); + } + + public static long LastInsertRowid (Sqlite3DatabaseHandle db) + { + return Sqlite3.sqlite3_last_insert_rowid (db); + } + + public static string GetErrmsg (Sqlite3DatabaseHandle db) + { + return Sqlite3.sqlite3_errmsg (db).utf8_to_string (); + } + + public static int BindParameterIndex (Sqlite3Statement stmt, string name) + { + return Sqlite3.sqlite3_bind_parameter_index (stmt, name); + } + + public static int BindNull (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_bind_null (stmt, index); + } + + public static int BindInt (Sqlite3Statement stmt, int index, int val) + { + return Sqlite3.sqlite3_bind_int (stmt, index, val); + } + + public static int BindInt64 (Sqlite3Statement stmt, int index, long val) + { + return Sqlite3.sqlite3_bind_int64 (stmt, index, val); + } + + public static int BindDouble (Sqlite3Statement stmt, int index, double val) + { + return Sqlite3.sqlite3_bind_double (stmt, index, val); + } + + public static int BindText (Sqlite3Statement stmt, int index, string val, int n, IntPtr free) + { #if USE_WP8_NATIVE_SQLITE - return Sqlite3.sqlite3_bind_text(stmt, index, val, n); + return Sqlite3.sqlite3_bind_text(stmt, index, val, n); #elif USE_SQLITEPCL_RAW - return Sqlite3.sqlite3_bind_text (stmt, index, val); + return Sqlite3.sqlite3_bind_text (stmt, index, val); #else - return Sqlite3.sqlite3_bind_text(stmt, index, val, n, null); + return Sqlite3.sqlite3_bind_text(stmt, index, val, n, null); #endif - } + } - public static int BindBlob (Sqlite3Statement stmt, int index, byte[] val, int n, IntPtr free) - { + public static int BindBlob (Sqlite3Statement stmt, int index, byte[] val, int n, IntPtr free) + { #if USE_WP8_NATIVE_SQLITE - return Sqlite3.sqlite3_bind_blob(stmt, index, val, n); + return Sqlite3.sqlite3_bind_blob(stmt, index, val, n); #elif USE_SQLITEPCL_RAW - return Sqlite3.sqlite3_bind_blob (stmt, index, val); + return Sqlite3.sqlite3_bind_blob (stmt, index, val); #else - return Sqlite3.sqlite3_bind_blob(stmt, index, val, n, null); + return Sqlite3.sqlite3_bind_blob(stmt, index, val, n, null); #endif - } - - public static int ColumnCount (Sqlite3Statement stmt) - { - return Sqlite3.sqlite3_column_count (stmt); - } - - public static string ColumnName (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_name (stmt, index).utf8_to_string (); - } - - public static string ColumnName16 (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_name (stmt, index).utf8_to_string (); - } - - public static ColType ColumnType (Sqlite3Statement stmt, int index) - { - return (ColType)Sqlite3.sqlite3_column_type (stmt, index); - } - - public static int ColumnInt (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_int (stmt, index); - } - - public static long ColumnInt64 (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_int64 (stmt, index); - } - - public static double ColumnDouble (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_double (stmt, index); - } - - public static string ColumnText (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_text (stmt, index).utf8_to_string (); - } - - public static string ColumnText16 (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_text (stmt, index).utf8_to_string (); - } - - public static byte[] ColumnBlob (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_blob (stmt, index).ToArray (); - } - - public static int ColumnBytes (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_bytes (stmt, index); - } - - public static string ColumnString (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_text (stmt, index).utf8_to_string (); - } - - public static byte[] ColumnByteArray (Sqlite3Statement stmt, int index) - { - int length = ColumnBytes (stmt, index); - if (length > 0) { - return ColumnBlob (stmt, index); - } - return new byte[0]; - } - - public static Result EnableLoadExtension (Sqlite3DatabaseHandle db, int onoff) - { - return (Result)Sqlite3.sqlite3_enable_load_extension (db, onoff); - } - - public static int LibVersionNumber () - { - return Sqlite3.sqlite3_libversion_number (); - } - - public static Result GetResult (Sqlite3DatabaseHandle db) - { - return (Result)Sqlite3.sqlite3_errcode (db); - } - - public static ExtendedResult ExtendedErrCode (Sqlite3DatabaseHandle db) - { - return (ExtendedResult)Sqlite3.sqlite3_extended_errcode (db); - } - - public static Sqlite3BackupHandle BackupInit (Sqlite3DatabaseHandle destDb, string destName, Sqlite3DatabaseHandle sourceDb, string sourceName) - { - return Sqlite3.sqlite3_backup_init (destDb, destName, sourceDb, sourceName); - } - - public static Result BackupStep (Sqlite3BackupHandle backup, int numPages) - { - return (Result)Sqlite3.sqlite3_backup_step (backup, numPages); - } - - public static Result BackupFinish (Sqlite3BackupHandle backup) - { - return (Result)Sqlite3.sqlite3_backup_finish (backup); - } + } + + public static int ColumnCount (Sqlite3Statement stmt) + { + return Sqlite3.sqlite3_column_count (stmt); + } + + public static string ColumnName (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_name (stmt, index).utf8_to_string (); + } + + public static string ColumnName16 (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_name (stmt, index).utf8_to_string (); + } + + public static ColType ColumnType (Sqlite3Statement stmt, int index) + { + return (ColType)Sqlite3.sqlite3_column_type (stmt, index); + } + + public static int ColumnInt (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_int (stmt, index); + } + + public static long ColumnInt64 (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_int64 (stmt, index); + } + + public static double ColumnDouble (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_double (stmt, index); + } + + public static string ColumnText (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_text (stmt, index).utf8_to_string (); + } + + public static string ColumnText16 (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_text (stmt, index).utf8_to_string (); + } + + public static byte[] ColumnBlob (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_blob (stmt, index).ToArray (); + } + + public static int ColumnBytes (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_bytes (stmt, index); + } + + public static string ColumnString (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_text (stmt, index).utf8_to_string (); + } + + public static byte[] ColumnByteArray (Sqlite3Statement stmt, int index) + { + int length = ColumnBytes (stmt, index); + if (length > 0) { + return ColumnBlob (stmt, index); + } + return new byte[0]; + } + + public static Result EnableLoadExtension (Sqlite3DatabaseHandle db, int onoff) + { + return (Result)Sqlite3.sqlite3_enable_load_extension (db, onoff); + } + + public static int LibVersionNumber () + { + return Sqlite3.sqlite3_libversion_number (); + } + + public static Result GetResult (Sqlite3DatabaseHandle db) + { + return (Result)Sqlite3.sqlite3_errcode (db); + } + + public static ExtendedResult ExtendedErrCode (Sqlite3DatabaseHandle db) + { + return (ExtendedResult)Sqlite3.sqlite3_extended_errcode (db); + } + + public static Sqlite3BackupHandle BackupInit (Sqlite3DatabaseHandle destDb, string destName, Sqlite3DatabaseHandle sourceDb, string sourceName) + { + return Sqlite3.sqlite3_backup_init (destDb, destName, sourceDb, sourceName); + } + + public static Result BackupStep (Sqlite3BackupHandle backup, int numPages) + { + return (Result)Sqlite3.sqlite3_backup_step (backup, numPages); + } + + public static Result BackupFinish (Sqlite3BackupHandle backup) + { + return (Result)Sqlite3.sqlite3_backup_finish (backup); + } #endif - public enum ColType : int - { - Integer = 1, - Float = 2, - Text = 3, - Blob = 4, - Null = 5 - } - } + public enum ColType : int + { + Integer = 1, + Float = 2, + Text = 3, + Blob = 4, + Null = 5 + } + } } From 4baa9c52ac244f439c60b8bd94e21bdd27e15da6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 14 Apr 2024 00:23:53 -0700 Subject: [PATCH 03/14] Add 'DAMT.All' annotations --- src/SQLite.cs | 562 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 454 insertions(+), 108 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index 7d3999ed..a99eb1e9 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -26,6 +26,9 @@ using System; using System.Collections; using System.Diagnostics; +#if NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif #if !USE_SQLITEPCL_RAW using System.Runtime.InteropServices; #endif @@ -187,62 +190,199 @@ public interface ISQLiteConnection : IDisposable int CreateIndex (string indexName, string tableName, string columnName, bool unique = false); int CreateIndex (string tableName, string columnName, bool unique = false); int CreateIndex (string tableName, string[] columnNames, bool unique = false); - int CreateIndex (Expression> property, bool unique = false); - CreateTableResult CreateTable (CreateFlags createFlags = CreateFlags.None); - CreateTableResult CreateTable (Type ty, CreateFlags createFlags = CreateFlags.None); - CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) + int CreateIndex< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (Expression> property, bool unique = false); + CreateTableResult CreateTable< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (CreateFlags createFlags = CreateFlags.None); + CreateTableResult CreateTable ( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type ty, CreateFlags createFlags = CreateFlags.None); + CreateTablesResult CreateTables< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T2> (CreateFlags createFlags = CreateFlags.None) where T : new() where T2 : new(); - CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) + CreateTablesResult CreateTables< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T2, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T3> (CreateFlags createFlags = CreateFlags.None) where T : new() where T2 : new() where T3 : new(); - CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) + CreateTablesResult CreateTables< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T2, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T3, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T4> (CreateFlags createFlags = CreateFlags.None) where T : new() where T2 : new() where T3 : new() where T4 : new(); - CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) + CreateTablesResult CreateTables< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T2, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T3, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T4, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T5> (CreateFlags createFlags = CreateFlags.None) where T : new() where T2 : new() where T3 : new() where T4 : new() where T5 : new(); - CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None, params Type[] types); - IEnumerable DeferredQuery (string query, params object[] args) where T : new(); +#if NET + [RequiresUnreferencedCode ("This method requires 'DynamicallyAccessedMemberTypes.All' on each input 'Type' instance.")] +#endif + CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None, params Type[] types); + IEnumerable DeferredQuery< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (string query, params object[] args) where T : new(); IEnumerable DeferredQuery (TableMapping map, string query, params object[] args); int Delete (object objectToDelete); - int Delete (object primaryKey); + int Delete< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (object primaryKey); int Delete (object primaryKey, TableMapping map); int DeleteAll (); int DeleteAll (TableMapping map); - int DropTable (); + int DropTable< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (); int DropTable (TableMapping map); void EnableLoadExtension (bool enabled); void EnableWriteAheadLogging (); int Execute (string query, params object[] args); T ExecuteScalar (string query, params object[] args); - T Find (object pk) where T : new(); + T Find< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (object pk) where T : new(); object Find (object pk, TableMapping map); - T Find (Expression> predicate) where T : new(); - T FindWithQuery (string query, params object[] args) where T : new(); + T Find< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (Expression> predicate) where T : new(); + T FindWithQuery< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (string query, params object[] args) where T : new(); object FindWithQuery (TableMapping map, string query, params object[] args); - T Get (object pk) where T : new(); + T Get< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (object pk) where T : new(); object Get (object pk, TableMapping map); - T Get (Expression> predicate) where T : new(); - TableMapping GetMapping (Type type, CreateFlags createFlags = CreateFlags.None); - TableMapping GetMapping (CreateFlags createFlags = CreateFlags.None); + T Get< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (Expression> predicate) where T : new(); + TableMapping GetMapping ( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type type, CreateFlags createFlags = CreateFlags.None); + TableMapping GetMapping< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (CreateFlags createFlags = CreateFlags.None); List GetTableInfo (string tableName); int Insert (object obj); - int Insert (object obj, Type objType); + int Insert ( + object obj, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType); int Insert (object obj, string extra); - int Insert (object obj, string extra, Type objType); + int Insert ( + object obj, + string extra, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType); int InsertAll (IEnumerable objects, bool runInTransaction = true); int InsertAll (IEnumerable objects, string extra, bool runInTransaction = true); - int InsertAll (IEnumerable objects, Type objType, bool runInTransaction = true); + int InsertAll ( + IEnumerable objects, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType, + bool runInTransaction = true); int InsertOrReplace (object obj); - int InsertOrReplace (object obj, Type objType); - List Query (string query, params object[] args) where T : new(); + int InsertOrReplace ( + object obj, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType); + List Query< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (string query, params object[] args) where T : new(); List Query (TableMapping map, string query, params object[] args); List QueryScalars (string query, params object[] args); void ReKey (string key); @@ -252,9 +392,18 @@ CreateTablesResult CreateTables (CreateFlags createFlags = Cr void RollbackTo (string savepoint); void RunInTransaction (Action action); string SaveTransactionPoint (); - TableQuery Table () where T : new(); + TableQuery Table< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> () where T : new(); int Update (object obj); - int Update (object obj, Type objType); + int Update ( + object obj, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType); int UpdateAll (IEnumerable objects, bool runInTransaction = true); } @@ -576,7 +725,12 @@ public IEnumerable TableMappings { /// The mapping represents the schema of the columns of the database and contains /// methods to set and get properties of objects. /// - public TableMapping GetMapping (Type type, CreateFlags createFlags = CreateFlags.None) + public TableMapping GetMapping ( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type type, + CreateFlags createFlags = CreateFlags.None) { TableMapping map; var key = type.FullName; @@ -605,7 +759,11 @@ public TableMapping GetMapping (Type type, CreateFlags createFlags = CreateFlags /// The mapping represents the schema of the columns of the database and contains /// methods to set and get properties of objects. /// - public TableMapping GetMapping (CreateFlags createFlags = CreateFlags.None) + public TableMapping GetMapping< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (CreateFlags createFlags = CreateFlags.None) { return GetMapping (typeof (T), createFlags); } @@ -627,7 +785,11 @@ private struct IndexInfo /// /// Executes a "drop table" on the database. This is non-recoverable. /// - public int DropTable () + public int DropTable< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> () { return DropTable (GetMapping (typeof (T))); } @@ -653,7 +815,11 @@ public int DropTable (TableMapping map) /// /// Whether the table was created or migrated. /// - public CreateTableResult CreateTable (CreateFlags createFlags = CreateFlags.None) + public CreateTableResult CreateTable< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (CreateFlags createFlags = CreateFlags.None) { return CreateTable (typeof (T), createFlags); } @@ -669,7 +835,11 @@ public CreateTableResult CreateTable (CreateFlags createFlags = CreateFlags.N /// /// Whether the table was created or migrated. /// - public CreateTableResult CreateTable (Type ty, CreateFlags createFlags = CreateFlags.None) + public CreateTableResult CreateTable ( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type ty, CreateFlags createFlags = CreateFlags.None) { var map = GetMapping (ty, createFlags); @@ -743,50 +913,95 @@ public CreateTableResult CreateTable (Type ty, CreateFlags createFlags = CreateF return result; } - /// - /// 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 CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) + /// + /// 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. + /// +#if NET8_0_OR_GREATER + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This method preserves metata for all type arguments.")] +#endif + public CreateTablesResult CreateTables< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T2> (CreateFlags createFlags = CreateFlags.None) where T : new() where T2 : new() { return CreateTables (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 CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) - where T : new() + /// + /// 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. + /// +#if NET8_0_OR_GREATER + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This method preserves metata for all type arguments.")] +#endif + public CreateTablesResult CreateTables< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +# endif + T2, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T3> (CreateFlags createFlags = CreateFlags.None) + where T : new() where T2 : new() where T3 : new() { return CreateTables (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 CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) - where T : new() + /// + /// 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. + /// +#if NET8_0_OR_GREATER + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This method preserves metata for all type arguments.")] +#endif + public CreateTablesResult CreateTables< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +# endif + T, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +# endif + T2, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +# endif + T3, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +# endif + T4> (CreateFlags createFlags = CreateFlags.None) + where T : new() where T2 : new() where T3 : new() where T4 : new() @@ -794,17 +1009,40 @@ public CreateTablesResult CreateTables (CreateFlags createFlags = return CreateTables (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 CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None) - where T : new() + /// + /// 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. + /// +#if NET8_0_OR_GREATER + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This method preserves metata for all type arguments.")] +#endif + public CreateTablesResult CreateTables< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +# endif + T, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +# endif + T2, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +# endif + T3, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +# endif + T4, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +# endif + T5> (CreateFlags createFlags = CreateFlags.None) + where T : new() where T2 : new() where T3 : new() where T4 : new() @@ -813,16 +1051,19 @@ public CreateTablesResult CreateTables (CreateFlags createFla return CreateTables (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 CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None, params Type[] types) + /// + /// 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. + /// +#if NET + [RequiresUnreferencedCode("This method requires 'DynamicallyAccessedMemberTypes.All' on each input 'Type' instance.")] +#endif + public CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None, params Type[] types) { var result = new CreateTablesResult (); foreach (Type type in types) { @@ -892,7 +1133,11 @@ public int CreateIndex (string tableName, string[] columnNames, bool unique = fa /// Property to index /// Whether the index should be unique /// Zero on success. - public int CreateIndex (Expression> property, bool unique = false) + public int CreateIndex< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (Expression> property, bool unique = false) { MemberExpression mx; if (property.Body.NodeType == ExpressionType.Convert) { @@ -1126,7 +1371,11 @@ public T ExecuteScalar (string query, params object[] args) /// /// An enumerable with one result for each row returned by the query. /// - public List Query (string query, params object[] args) where T : new() + public List Query< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (string query, params object[] args) where T : new() { var cmd = CreateCommand (query, args); return cmd.ExecuteQuery (); @@ -1170,7 +1419,11 @@ public List QueryScalars (string query, params object[] args) /// will call sqlite3_step on each call to MoveNext, so the database /// connection must remain open for the lifetime of the enumerator. /// - public IEnumerable DeferredQuery (string query, params object[] args) where T : new() + public IEnumerable DeferredQuery< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (string query, params object[] args) where T : new() { var cmd = CreateCommand (query, args); return cmd.ExecuteDeferredQuery (); @@ -1238,7 +1491,11 @@ public IEnumerable DeferredQuery (TableMapping map, string query, params /// A queryable object that is able to translate Where, OrderBy, and Take /// queries into native SQL. /// - public TableQuery Table () where T : new() + public TableQuery Table< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> () where T : new() { return new TableQuery (this); } @@ -1255,7 +1512,11 @@ public IEnumerable DeferredQuery (TableMapping map, string query, params /// The object with the given primary key. Throws a not found exception /// if the object is not found. /// - public T Get (object pk) where T : new() + public T Get< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (object pk) where T : new() { var map = GetMapping (typeof (T)); return Query (map.GetByPrimaryKeySql, pk).First (); @@ -1292,7 +1553,11 @@ public object Get (object pk, TableMapping map) /// The object that matches the given predicate. Throws a not found exception /// if the object is not found. /// - public T Get (Expression> predicate) where T : new() + public T Get< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (Expression> predicate) where T : new() { return Table ().Where (predicate).First (); } @@ -1309,7 +1574,11 @@ public object Get (object pk, TableMapping map) /// The object with the given primary key or null /// if the object is not found. /// - public T Find (object pk) where T : new() + public T Find< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (object pk) where T : new() { var map = GetMapping (typeof (T)); return Query (map.GetByPrimaryKeySql, pk).FirstOrDefault (); @@ -1346,7 +1615,11 @@ public object Find (object pk, TableMapping map) /// The object that matches the given predicate or null /// if the object is not found. /// - public T Find (Expression> predicate) where T : new() + public T Find< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (Expression> predicate) where T : new() { return Table ().Where (predicate).FirstOrDefault (); } @@ -1365,7 +1638,11 @@ public object Find (object pk, TableMapping map) /// The object that matches the given predicate or null /// if the object is not found. /// - public T FindWithQuery (string query, params object[] args) where T : new() + public T FindWithQuery< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (string query, params object[] args) where T : new() { return Query (query, args).FirstOrDefault (); } @@ -1713,7 +1990,13 @@ public int InsertAll (System.Collections.IEnumerable objects, string extra, bool /// /// The number of rows added to the table. /// - public int InsertAll (System.Collections.IEnumerable objects, Type objType, bool runInTransaction = true) + public int InsertAll ( + System.Collections.IEnumerable objects, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType, + bool runInTransaction = true) { var c = 0; if (runInTransaction) { @@ -1786,7 +2069,12 @@ public int InsertOrReplace (object obj) /// /// The number of rows added to the table. /// - public int Insert (object obj, Type objType) + public int Insert ( + object obj, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType) { return Insert (obj, "", objType); } @@ -1808,7 +2096,12 @@ public int Insert (object obj, Type objType) /// /// The number of rows modified. /// - public int InsertOrReplace (object obj, Type objType) + public int InsertOrReplace ( + object obj, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType) { return Insert (obj, "OR REPLACE", objType); } @@ -1852,7 +2145,13 @@ public int Insert (object obj, string extra) /// /// The number of rows added to the table. /// - public int Insert (object obj, string extra, Type objType) + public int Insert ( + object obj, + string extra, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType) { if (obj == null || objType == null) { return 0; @@ -1988,7 +2287,12 @@ public int Update (object obj) /// /// The number of rows updated. /// - public int Update (object obj, Type objType) + public int Update ( + object obj, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType) { int rowsAffected = 0; if (obj == null || objType == null) { @@ -2104,7 +2408,11 @@ public int Delete (object objectToDelete) /// /// The type of object. /// - public int Delete (object primaryKey) + public int Delete< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (object primaryKey) { return Delete (primaryKey, GetMapping (typeof (T))); } @@ -2571,7 +2879,11 @@ public class TableMapping readonly Column[] _insertColumns; readonly Column[] _insertOrReplaceColumns; - public TableMapping (Type type, CreateFlags createFlags = CreateFlags.None) + public TableMapping ( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type type, CreateFlags createFlags = CreateFlags.None) { MappedType = type; CreateFlags = createFlags; @@ -2622,7 +2934,11 @@ public TableMapping (Type type, CreateFlags createFlags = CreateFlags.None) _insertOrReplaceColumns = Columns.ToArray (); } - private IReadOnlyCollection GetPublicMembers(Type type) + private IReadOnlyCollection GetPublicMembers( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type type) { if(type.Name.StartsWith("ValueTuple`")) return GetFieldsFromValueTuple(type); @@ -2655,7 +2971,11 @@ from p in ti.DeclaredProperties return members; } - private IReadOnlyCollection GetFieldsFromValueTuple(Type type) + private IReadOnlyCollection GetFieldsFromValueTuple( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields)] +#endif + Type type) { Method = MapMethod.ByPosition; var fields = type.GetFields(); @@ -2983,15 +3303,25 @@ public static bool IsAutoInc (MemberInfo p) return p.CustomAttributes.Any (x => x.AttributeType == typeof (AutoIncrementAttribute)); } - public static FieldInfo GetField (TypeInfo t, string name) + public static FieldInfo GetField ( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + TypeInfo t, + string name) { - var f = t.GetDeclaredField (name); + var f = t.GetDeclaredField (name); if (f != null) return f; return GetField (t.BaseType.GetTypeInfo (), name); } - public static PropertyInfo GetProperty (TypeInfo t, string name) + public static PropertyInfo GetProperty ( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + TypeInfo t, + string name) { var f = t.GetDeclaredProperty (name); if (f != null) @@ -3096,12 +3426,20 @@ public int ExecuteNonQuery () throw SQLiteException.New (r, SQLite3.GetErrmsg (_conn.Handle)); } - public IEnumerable ExecuteDeferredQuery () + public IEnumerable ExecuteDeferredQuery< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> () { return ExecuteDeferredQuery (_conn.GetMapping (typeof (T))); } - public List ExecuteQuery () + public List ExecuteQuery< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> () { return ExecuteDeferredQuery (_conn.GetMapping (typeof (T))).ToList (); } @@ -3836,7 +4174,11 @@ protected class Ordering } } - public class TableQuery : BaseTableQuery, IEnumerable + public class TableQuery< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> : BaseTableQuery, IEnumerable { public SQLiteConnection Connection { get; private set; } @@ -3867,7 +4209,11 @@ public TableQuery (SQLiteConnection conn) Table = Connection.GetMapping (typeof (T)); } - public TableQuery Clone () + public TableQuery Clone< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + U> () { var q = new TableQuery (Connection, Table); q._where = _where; From 21a9ca43efee54c8a017f6cdd92d6f9255a01fd0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 14 Apr 2024 00:33:29 -0700 Subject: [PATCH 04/14] Remove InflateAttribute on .NET 8 --- src/SQLite.cs | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index a99eb1e9..ce748c29 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -2883,23 +2883,27 @@ public TableMapping ( #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif - Type type, CreateFlags createFlags = CreateFlags.None) + Type type, + CreateFlags createFlags = CreateFlags.None) { MappedType = type; CreateFlags = createFlags; - var typeInfo = type.GetTypeInfo (); #if ENABLE_IL2CPP + var typeInfo = type.GetTypeInfo (); var tableAttr = typeInfo.GetCustomAttribute (); +#elif NET8_0_OR_GREATER + var tableAttr = type.GetCustomAttributes ().FirstOrDefault (); #else - var tableAttr = + var typeInfo = type.GetTypeInfo (); + var tableAttr = typeInfo.CustomAttributes .Where (x => x.AttributeType == typeof (TableAttribute)) .Select (x => (TableAttribute)Orm.InflateAttribute (x)) .FirstOrDefault (); #endif - TableName = (tableAttr != null && !string.IsNullOrEmpty (tableAttr.Name)) ? tableAttr.Name : MappedType.Name; + TableName = (tableAttr != null && !string.IsNullOrEmpty (tableAttr.Name)) ? tableAttr.Name : MappedType.Name; WithoutRowId = tableAttr != null ? tableAttr.WithoutRowId : false; var members = GetPublicMembers(type); @@ -3329,7 +3333,8 @@ public static PropertyInfo GetProperty ( return GetProperty (t.BaseType.GetTypeInfo (), name); } - public static object InflateAttribute (CustomAttributeData x) +#if !NET8_0_OR_GREATER + public static object InflateAttribute (CustomAttributeData x) { var atype = x.AttributeType; var typeInfo = atype.GetTypeInfo (); @@ -3349,13 +3354,14 @@ public static object InflateAttribute (CustomAttributeData x) #endif return r; } +#endif public static IEnumerable GetIndices (MemberInfo p) { -#if ENABLE_IL2CPP - return p.GetCustomAttributes (); +#if ENABLE_IL2CPP || NET8_0_OR_GREATER + return p.GetCustomAttributes (); #else - var indexedInfo = typeof (IndexedAttribute).GetTypeInfo (); + var indexedInfo = typeof (IndexedAttribute).GetTypeInfo (); return p.CustomAttributes .Where (x => indexedInfo.IsAssignableFrom (x.AttributeType.GetTypeInfo ())) @@ -3367,15 +3373,17 @@ public static IEnumerable GetIndices (MemberInfo p) { #if ENABLE_IL2CPP return p.GetCustomAttribute ()?.Value; +#elif NET8_0_OR_GREATER + return p.GetCustomAttributes ().FirstOrDefault ()?.Value; #else - var attr = p.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (MaxLengthAttribute)); + var attr = p.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (MaxLengthAttribute)); if (attr != null) { var attrv = (MaxLengthAttribute)InflateAttribute (attr); return attrv.Value; } return null; #endif - } + } public static int? MaxStringLength (PropertyInfo p) => MaxStringLength((MemberInfo)p); From ff9018f6880f598b2926d1ac6e445f214c751bdc Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 14 Apr 2024 00:39:21 -0700 Subject: [PATCH 05/14] Fix warnings on all common CRUD methods --- src/SQLite.cs | 286 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 171 insertions(+), 115 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index ce748c29..77f801b7 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -289,14 +289,21 @@ IEnumerable DeferredQuery< #endif T> (string query, params object[] args) where T : new(); IEnumerable DeferredQuery (TableMapping map, string query, params object[] args); - int Delete (object objectToDelete); +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'objectToDelete'.")] +#endif + int Delete (object objectToDelete); int Delete< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T> (object primaryKey); int Delete (object primaryKey, TableMapping map); - int DeleteAll (); + int DeleteAll< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (); int DeleteAll (TableMapping map); int DropTable< #if NET8_0_OR_GREATER @@ -347,14 +354,20 @@ TableMapping GetMapping< #endif T> (CreateFlags createFlags = CreateFlags.None); List GetTableInfo (string tableName); - int Insert (object obj); +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif + int Insert (object obj); int Insert ( object obj, #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif Type objType); - int Insert (object obj, string extra); +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif + int Insert (object obj, string extra); int Insert ( object obj, string extra, @@ -362,8 +375,14 @@ int Insert ( [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif Type objType); - int InsertAll (IEnumerable objects, bool runInTransaction = true); - int InsertAll (IEnumerable objects, string extra, bool runInTransaction = true); +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] +#endif + int InsertAll (IEnumerable objects, bool runInTransaction = true); +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] +#endif + int InsertAll (IEnumerable objects, string extra, bool runInTransaction = true); int InsertAll ( IEnumerable objects, #if NET8_0_OR_GREATER @@ -371,7 +390,10 @@ int InsertAll ( #endif Type objType, bool runInTransaction = true); - int InsertOrReplace (object obj); +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif + int InsertOrReplace (object obj); int InsertOrReplace ( object obj, #if NET8_0_OR_GREATER @@ -397,14 +419,20 @@ TableQuery Table< [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T> () where T : new(); - int Update (object obj); +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif + int Update (object obj); int Update ( object obj, #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif Type objType); - int UpdateAll (IEnumerable objects, bool runInTransaction = true); +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] +#endif + int UpdateAll (IEnumerable objects, bool runInTransaction = true); } /// @@ -1913,18 +1941,21 @@ public void RunInTransaction (Action action) } } - /// - /// 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 int InsertAll (System.Collections.IEnumerable objects, bool runInTransaction = true) + /// + /// 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. + /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] +#endif + public int InsertAll (System.Collections.IEnumerable objects, bool runInTransaction = true) { var c = 0; if (runInTransaction) { @@ -1942,22 +1973,25 @@ public int InsertAll (System.Collections.IEnumerable objects, bool runInTransact return c; } - /// - /// 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 int InsertAll (System.Collections.IEnumerable objects, string extra, bool runInTransaction = true) + /// + /// 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. + /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] +#endif + public int InsertAll (System.Collections.IEnumerable objects, string extra, bool runInTransaction = true) { var c = 0; if (runInTransaction) { @@ -2014,18 +2048,21 @@ public int InsertAll ( return c; } - /// - /// 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 number of rows added to the table. - /// - public int Insert (object 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 number of rows added to the table. + /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif + public int Insert (object obj) { if (obj == null) { return 0; @@ -2033,21 +2070,24 @@ public int Insert (object obj) return Insert (obj, "", Orm.GetType (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 number of rows modified. - /// - public int InsertOrReplace (object 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 number of rows modified. + /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif + public int InsertOrReplace (object obj) { if (obj == null) { return 0; @@ -2106,21 +2146,24 @@ public int InsertOrReplace ( return Insert (obj, "OR REPLACE", 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 int Insert (object obj, string 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 number of rows added to the table. + /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif + public int Insert (object obj, string extra) { if (obj == null) { return 0; @@ -2254,18 +2297,21 @@ PreparedSqlLiteInsertCommand CreateInsertCommand (TableMapping map, string extra return insertCommand; } - /// - /// 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 int Update (object 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 number of rows updated. + /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif + public int Update (object obj) { if (obj == null) { return 0; @@ -2343,19 +2389,22 @@ public int Update ( return rowsAffected; } - /// - /// 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 int UpdateAll (System.Collections.IEnumerable objects, bool runInTransaction = true) + /// + /// 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. + /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] +#endif + public int UpdateAll (System.Collections.IEnumerable objects, bool runInTransaction = true) { var c = 0; if (runInTransaction) { @@ -2373,16 +2422,19 @@ public int UpdateAll (System.Collections.IEnumerable objects, bool runInTransact return c; } - /// - /// 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 int Delete (object objectToDelete) + /// + /// 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. + /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'objectToDelete'.")] +#endif + public int Delete (object objectToDelete) { var map = GetMapping (Orm.GetType (objectToDelete)); var pk = map.PK; @@ -2453,7 +2505,11 @@ public int Delete (object primaryKey, TableMapping map) /// /// The type of objects to delete. /// - public int DeleteAll () + public int DeleteAll< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> () { var map = GetMapping (typeof (T)); return DeleteAll (map); From d4bcd5f1db3e69cf6c9ef8dab3bed6b2b14d6eb6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 14 Apr 2024 00:40:14 -0700 Subject: [PATCH 06/14] Add annotations for Activator call --- src/SQLite.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index 77f801b7..ddfbdfe0 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -2915,7 +2915,10 @@ public class StoreAsTextAttribute : Attribute public class TableMapping { - public Type MappedType { get; private set; } +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] +#endif + public Type MappedType { get; private set; } public string TableName { get; private set; } From dff8edbe4bb26407e0752e54f26131bc57ea372a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 14 Apr 2024 10:08:08 -0700 Subject: [PATCH 07/14] Fix AOT warning in 'ExecuteDeferredQuery' --- src/SQLite.cs | 69 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index ddfbdfe0..ed5b84ca 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -25,17 +25,20 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Diagnostics; #if NET8_0_OR_GREATER using System.Diagnostics.CodeAnalysis; #endif +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +#if NET8_0_OR_GREATER +using System.Runtime.CompilerServices; +#endif #if !USE_SQLITEPCL_RAW using System.Runtime.InteropServices; #endif -using System.Collections.Generic; -using System.Reflection; -using System.Linq; -using System.Linq.Expressions; using System.Text; using System.Threading; @@ -3549,9 +3552,15 @@ public IEnumerable ExecuteDeferredQuery (TableMapping map) else if (map.Method == TableMapping.MapMethod.ByName) { MethodInfo getSetter = null; if (typeof(T) != map.MappedType) { - getSetter = typeof(FastColumnSetter) - .GetMethod (nameof(FastColumnSetter.GetFastSetter), - BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod (map.MappedType); +#if NET8_0_OR_GREATER + if (!RuntimeFeature.IsDynamicCodeSupported) { + if (map.MappedType.IsValueType) { + throw new NotSupportedException ( + $"Executing a query with a value type mapped type is not supported in AOT environments (The mapped type is '{map.MappedType}')."); + } + } +#endif + getSetter = FastColumnSetter.GetFastSetterMethodInfoUnsafe (map.MappedType); } for (int i = 0; i < cols.Length; i++) { @@ -3904,21 +3913,37 @@ object ReadCol (Sqlite3Statement stmt, int index, SQLite3.ColType type, Type clr internal class FastColumnSetter { - /// - /// Creates a delegate that can be used to quickly set object members from query columns. - /// - /// Note that this frontloads the slow reflection-based type checking for columns to only happen once at the beginning of a query, - /// and then afterwards each row of the query can invoke the delegate returned by this function to get much better performance (up to 10x speed boost, depending on query size and platform). - /// - /// The type of the destination object that the query will read into - /// The active connection. Note that this is primarily needed in order to read preferences regarding how certain data types (such as TimeSpan / DateTime) should be encoded in the database. - /// The table mapping used to map the statement column to a member of the destination object type - /// - /// A delegate for fast-setting of object members from statement columns. - /// - /// If no fast setter is available for the requested column (enums in particular cause headache), then this function returns null. - /// - internal static Action GetFastSetter (SQLiteConnection conn, TableMapping.Column column) + /// + /// Gets a for a generic method, suppressing AOT warnings. + /// + /// The type of the destination object that the query will read into. + /// The generic instance. + /// This should only be called when is a reference type. +#if NET8_0_OR_GREATER + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "This method is only ever called when 'mappedType' is a reference type.")] +#endif + internal static MethodInfo GetFastSetterMethodInfoUnsafe (Type mappedType) + { + return typeof (FastColumnSetter) + .GetMethod (nameof (GetFastSetter), + BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod (mappedType); + } + + /// + /// Creates a delegate that can be used to quickly set object members from query columns. + /// + /// Note that this frontloads the slow reflection-based type checking for columns to only happen once at the beginning of a query, + /// and then afterwards each row of the query can invoke the delegate returned by this function to get much better performance (up to 10x speed boost, depending on query size and platform). + /// + /// The type of the destination object that the query will read into + /// The active connection. Note that this is primarily needed in order to read preferences regarding how certain data types (such as TimeSpan / DateTime) should be encoded in the database. + /// The table mapping used to map the statement column to a member of the destination object type + /// + /// A delegate for fast-setting of object members from statement columns. + /// + /// If no fast setter is available for the requested column (enums in particular cause headache), then this function returns null. + /// + internal static Action GetFastSetter (SQLiteConnection conn, TableMapping.Column column) { Action fastSetter = null; From bf150687bfd944381901f43dc6d3ded15bae7dea Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 14 Apr 2024 10:22:00 -0700 Subject: [PATCH 08/14] Fix trim warnings in SQLiteAsync.cs --- src/SQLiteAsync.cs | 461 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 412 insertions(+), 49 deletions(-) diff --git a/src/SQLiteAsync.cs b/src/SQLiteAsync.cs index a33f80f4..5883bea6 100644 --- a/src/SQLiteAsync.cs +++ b/src/SQLiteAsync.cs @@ -23,6 +23,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Threading; @@ -50,74 +51,247 @@ public interface ISQLiteAsyncConnection Task CreateIndexAsync (string indexName, string tableName, string columnName, bool unique = false); Task CreateIndexAsync (string tableName, string[] columnNames, bool unique = false); Task CreateIndexAsync (string indexName, string tableName, string[] columnNames, bool unique = false); - Task CreateIndexAsync (Expression> property, bool unique = false); - Task CreateTableAsync (CreateFlags createFlags = CreateFlags.None) where T : new(); - Task CreateTableAsync (Type ty, CreateFlags createFlags = CreateFlags.None); - Task CreateTablesAsync (CreateFlags createFlags = CreateFlags.None) + Task CreateIndexAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (Expression> property, bool unique = false); + Task CreateTableAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (CreateFlags createFlags = CreateFlags.None) where T : new(); + Task CreateTableAsync ( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type ty, + CreateFlags createFlags = CreateFlags.None); + Task CreateTablesAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T2> (CreateFlags createFlags = CreateFlags.None) where T : new() where T2 : new(); - Task CreateTablesAsync (CreateFlags createFlags = CreateFlags.None) + Task CreateTablesAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T2, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T3> (CreateFlags createFlags = CreateFlags.None) where T : new() where T2 : new() where T3 : new(); - Task CreateTablesAsync (CreateFlags createFlags = CreateFlags.None) + Task CreateTablesAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T2, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T3, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T4> (CreateFlags createFlags = CreateFlags.None) where T : new() where T2 : new() where T3 : new() where T4 : new(); - Task CreateTablesAsync (CreateFlags createFlags = CreateFlags.None) + Task CreateTablesAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T2, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T3, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T4, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T5> (CreateFlags createFlags = CreateFlags.None) where T : new() where T2 : new() where T3 : new() where T4 : new() where T5 : new(); +#if NET + [RequiresUnreferencedCode ("This method requires 'DynamicallyAccessedMemberTypes.All' on each input 'Type' instance.")] +#endif Task CreateTablesAsync (CreateFlags createFlags = CreateFlags.None, params Type[] types); - Task> DeferredQueryAsync (string query, params object[] args) where T : new(); + Task> DeferredQueryAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (string query, params object[] args) where T : new(); Task> DeferredQueryAsync (TableMapping map, string query, params object[] args); - Task DeleteAllAsync (); + Task DeleteAllAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (); Task DeleteAllAsync (TableMapping map); +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'objectToDelete'.")] +#endif Task DeleteAsync (object objectToDelete); - Task DeleteAsync (object primaryKey); + Task DeleteAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (object primaryKey); Task DeleteAsync (object primaryKey, TableMapping map); - Task DropTableAsync () where T : new(); + Task DropTableAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> () where T : new(); Task DropTableAsync (TableMapping map); Task EnableLoadExtensionAsync (bool enabled); Task EnableWriteAheadLoggingAsync (); Task ExecuteAsync (string query, params object[] args); Task ExecuteScalarAsync (string query, params object[] args); - Task FindAsync (object pk) where T : new(); + Task FindAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (object pk) where T : new(); Task FindAsync (object pk, TableMapping map); - Task FindAsync (Expression> predicate) where T : new(); - Task FindWithQueryAsync (string query, params object[] args) where T : new(); + Task FindAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (Expression> predicate) where T : new(); + Task FindWithQueryAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (string query, params object[] args) where T : new(); Task FindWithQueryAsync (TableMapping map, string query, params object[] args); - Task GetAsync (object pk) where T : new(); + Task GetAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (object pk) where T : new(); Task GetAsync (object pk, TableMapping map); - Task GetAsync (Expression> predicate) where T : new(); + Task GetAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (Expression> predicate) where T : new(); TimeSpan GetBusyTimeout (); SQLiteConnectionWithLock GetConnection (); - Task GetMappingAsync (Type type, CreateFlags createFlags = CreateFlags.None); - Task GetMappingAsync (CreateFlags createFlags = CreateFlags.None) where T : new(); + Task GetMappingAsync ( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type type, + CreateFlags createFlags = CreateFlags.None); + Task GetMappingAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (CreateFlags createFlags = CreateFlags.None) where T : new(); Task> GetTableInfoAsync (string tableName); +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] +#endif Task InsertAllAsync (IEnumerable objects, bool runInTransaction = true); +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] +#endif Task InsertAllAsync (IEnumerable objects, string extra, bool runInTransaction = true); +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] +#endif Task InsertAllAsync (IEnumerable objects, Type objType, bool runInTransaction = true); +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif Task InsertAsync (object obj); - Task InsertAsync (object obj, Type objType); + Task InsertAsync ( + object obj, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType); +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif Task InsertAsync (object obj, string extra); - Task InsertAsync (object obj, string extra, Type objType); + Task InsertAsync ( + object obj, + string extra, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType); +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif Task InsertOrReplaceAsync (object obj); - Task InsertOrReplaceAsync (object obj, Type objType); - Task> QueryAsync (string query, params object[] args) where T : new(); + Task InsertOrReplaceAsync ( + object obj, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType); + Task> QueryAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (string query, params object[] args) where T : new(); Task> QueryAsync (TableMapping map, string query, params object[] args); Task> QueryScalarsAsync (string query, params object[] args); Task ReKeyAsync (string key); Task ReKeyAsync (byte[] key); Task RunInTransactionAsync (Action action); Task SetBusyTimeoutAsync (TimeSpan value); - AsyncTableQuery Table () where T : new(); + AsyncTableQuery Table< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> () where T : new(); +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] +#endif Task UpdateAllAsync (IEnumerable objects, bool runInTransaction = true); +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif Task UpdateAsync (object obj); - Task UpdateAsync (object obj, Type objType); + Task UpdateAsync ( + object obj, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType); } /// @@ -360,7 +534,11 @@ public Task EnableLoadExtensionAsync (bool enabled) /// /// Whether the table was created or migrated. /// - public Task CreateTableAsync (CreateFlags createFlags = CreateFlags.None) + public Task CreateTableAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (CreateFlags createFlags = CreateFlags.None) where T : new() { return WriteAsync (conn => conn.CreateTable (createFlags)); @@ -377,7 +555,12 @@ public Task CreateTableAsync (CreateFlags createFlags = Cr /// /// Whether the table was created or migrated. /// - public Task CreateTableAsync (Type ty, CreateFlags createFlags = CreateFlags.None) + public Task CreateTableAsync ( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type ty, + CreateFlags createFlags = CreateFlags.None) { return WriteAsync (conn => conn.CreateTable (ty, createFlags)); } @@ -391,7 +574,18 @@ public Task CreateTableAsync (Type ty, CreateFlags createFlag /// /// Whether the table was created or migrated for each type. /// - public Task CreateTablesAsync (CreateFlags createFlags = CreateFlags.None) +#if NET8_0_OR_GREATER + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This method preserves metata for all type arguments.")] +#endif + public Task CreateTablesAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T2> (CreateFlags createFlags = CreateFlags.None) where T : new() where T2 : new() { @@ -407,7 +601,22 @@ public Task CreateTablesAsync (CreateFlags createFlag /// /// Whether the table was created or migrated for each type. /// - public Task CreateTablesAsync (CreateFlags createFlags = CreateFlags.None) +#if NET8_0_OR_GREATER + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This method preserves metata for all type arguments.")] +#endif + public Task CreateTablesAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T2, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T3> (CreateFlags createFlags = CreateFlags.None) where T : new() where T2 : new() where T3 : new() @@ -424,7 +633,26 @@ public Task CreateTablesAsync (CreateFlags create /// /// Whether the table was created or migrated for each type. /// - public Task CreateTablesAsync (CreateFlags createFlags = CreateFlags.None) +#if NET8_0_OR_GREATER + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This method preserves metata for all type arguments.")] +#endif + public Task CreateTablesAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T2, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T3, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T4> (CreateFlags createFlags = CreateFlags.None) where T : new() where T2 : new() where T3 : new() @@ -442,7 +670,30 @@ public Task CreateTablesAsync (CreateFlags cr /// /// Whether the table was created or migrated for each type. /// - public Task CreateTablesAsync (CreateFlags createFlags = CreateFlags.None) +#if NET8_0_OR_GREATER + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This method preserves metata for all type arguments.")] +#endif + public Task CreateTablesAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T2, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T3, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T4, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T5> (CreateFlags createFlags = CreateFlags.None) where T : new() where T2 : new() where T3 : new() @@ -461,6 +712,9 @@ public Task CreateTablesAsync (CreateFlag /// /// Whether the table was created or migrated for each type. /// +#if NET + [RequiresUnreferencedCode ("This method requires 'DynamicallyAccessedMemberTypes.All' on each input 'Type' instance.")] +#endif public Task CreateTablesAsync (CreateFlags createFlags = CreateFlags.None, params Type[] types) { return WriteAsync (conn => conn.CreateTables (createFlags, types)); @@ -469,7 +723,11 @@ public Task CreateTablesAsync (CreateFlags createFlags = Cre /// /// Executes a "drop table" on the database. This is non-recoverable. /// - public Task DropTableAsync () + public Task DropTableAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> () where T : new() { return WriteAsync (conn => conn.DropTable ()); @@ -544,7 +802,11 @@ public Task CreateIndexAsync (string indexName, string tableName, string[] /// Property to index /// Whether the index should be unique /// Zero on success. - public Task CreateIndexAsync (Expression> property, bool unique = false) + public Task CreateIndexAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (Expression> property, bool unique = false) { return WriteAsync (conn => conn.CreateIndex (property, unique)); } @@ -559,6 +821,9 @@ public Task CreateIndexAsync (Expression> property, bool /// /// The number of rows added to the table. /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif public Task InsertAsync (object obj) { return WriteAsync (conn => conn.Insert (obj)); @@ -578,7 +843,12 @@ public Task InsertAsync (object obj) /// /// The number of rows added to the table. /// - public Task InsertAsync (object obj, Type objType) + public Task InsertAsync ( + object obj, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType) { return WriteAsync (conn => conn.Insert (obj, objType)); } @@ -597,6 +867,9 @@ public Task InsertAsync (object obj, Type objType) /// /// The number of rows added to the table. /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif public Task InsertAsync (object obj, string extra) { return WriteAsync (conn => conn.Insert (obj, extra)); @@ -619,7 +892,13 @@ public Task InsertAsync (object obj, string extra) /// /// The number of rows added to the table. /// - public Task InsertAsync (object obj, string extra, Type objType) + public Task InsertAsync ( + object obj, + string extra, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType) { return WriteAsync (conn => conn.Insert (obj, extra, objType)); } @@ -638,6 +917,9 @@ public Task InsertAsync (object obj, string extra, Type objType) /// /// The number of rows modified. /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif public Task InsertOrReplaceAsync (object obj) { return WriteAsync (conn => conn.InsertOrReplace (obj)); @@ -660,7 +942,12 @@ public Task InsertOrReplaceAsync (object obj) /// /// The number of rows modified. /// - public Task InsertOrReplaceAsync (object obj, Type objType) + public Task InsertOrReplaceAsync ( + object obj, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType) { return WriteAsync (conn => conn.InsertOrReplace (obj, objType)); } @@ -676,6 +963,9 @@ public Task InsertOrReplaceAsync (object obj, Type objType) /// /// The number of rows updated. /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif public Task UpdateAsync (object obj) { return WriteAsync (conn => conn.Update (obj)); @@ -695,7 +985,12 @@ public Task UpdateAsync (object obj) /// /// The number of rows updated. /// - public Task UpdateAsync (object obj, Type objType) + public Task UpdateAsync ( + object obj, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType) { return WriteAsync (conn => conn.Update (obj, objType)); } @@ -712,6 +1007,9 @@ public Task UpdateAsync (object obj, Type objType) /// /// The number of rows modified. /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] +#endif public Task UpdateAllAsync (IEnumerable objects, bool runInTransaction = true) { return WriteAsync (conn => conn.UpdateAll (objects, runInTransaction)); @@ -726,6 +1024,9 @@ public Task UpdateAllAsync (IEnumerable objects, bool runInTransaction = tr /// /// The number of rows deleted. /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'objectToDelete'.")] +#endif public Task DeleteAsync (object objectToDelete) { return WriteAsync (conn => conn.Delete (objectToDelete)); @@ -743,7 +1044,11 @@ public Task DeleteAsync (object objectToDelete) /// /// The type of object. /// - public Task DeleteAsync (object primaryKey) + public Task DeleteAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (object primaryKey) { return WriteAsync (conn => conn.Delete (primaryKey)); } @@ -776,7 +1081,11 @@ public Task DeleteAsync (object primaryKey, TableMapping map) /// /// The type of objects to delete. /// - public Task DeleteAllAsync () + public Task DeleteAllAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> () { return WriteAsync (conn => conn.DeleteAll ()); } @@ -822,7 +1131,11 @@ public Task BackupAsync (string destinationDatabasePath, string databaseName = " /// The object with the given primary key. Throws a not found exception /// if the object is not found. /// - public Task GetAsync (object pk) + public Task GetAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (object pk) where T : new() { return ReadAsync (conn => conn.Get (pk)); @@ -859,7 +1172,11 @@ public Task GetAsync (object pk, TableMapping map) /// The object that matches the given predicate. Throws a not found exception /// if the object is not found. /// - public Task GetAsync (Expression> predicate) + public Task GetAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +# endif + T> (Expression> predicate) where T : new() { return ReadAsync (conn => conn.Get (predicate)); @@ -877,7 +1194,11 @@ public Task GetAsync (Expression> predicate) /// The object with the given primary key or null /// if the object is not found. /// - public Task FindAsync (object pk) + public Task FindAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (object pk) where T : new() { return ReadAsync (conn => conn.Find (pk)); @@ -914,7 +1235,11 @@ public Task FindAsync (object pk, TableMapping map) /// The object that matches the given predicate or null /// if the object is not found. /// - public Task FindAsync (Expression> predicate) + public Task FindAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (Expression> predicate) where T : new() { return ReadAsync (conn => conn.Find (predicate)); @@ -934,7 +1259,11 @@ public Task FindAsync (Expression> predicate) /// The object that matches the given predicate or null /// if the object is not found. /// - public Task FindWithQueryAsync (string query, params object[] args) + public Task FindWithQueryAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (string query, params object[] args) where T : new() { return ReadAsync (conn => conn.FindWithQuery (query, args)); @@ -975,7 +1304,12 @@ public Task FindWithQueryAsync (TableMapping map, string query, params o /// The mapping represents the schema of the columns of the database and contains /// methods to set and get properties of objects. /// - public Task GetMappingAsync (Type type, CreateFlags createFlags = CreateFlags.None) + public Task GetMappingAsync ( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type type, + CreateFlags createFlags = CreateFlags.None) { return ReadAsync (conn => conn.GetMapping (type, createFlags)); } @@ -990,7 +1324,11 @@ public Task GetMappingAsync (Type type, CreateFlags createFlags = /// The mapping represents the schema of the columns of the database and contains /// methods to set and get properties of objects. /// - public Task GetMappingAsync (CreateFlags createFlags = CreateFlags.None) + public Task GetMappingAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (CreateFlags createFlags = CreateFlags.None) where T : new() { return ReadAsync (conn => conn.GetMapping (createFlags)); @@ -1039,6 +1377,9 @@ public Task ExecuteAsync (string query, params object[] args) /// /// The number of rows added to the table. /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] +#endif public Task InsertAllAsync (IEnumerable objects, bool runInTransaction = true) { return WriteAsync (conn => conn.InsertAll (objects, runInTransaction)); @@ -1059,6 +1400,9 @@ public Task InsertAllAsync (IEnumerable objects, bool runInTransaction = tr /// /// The number of rows added to the table. /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] +#endif public Task InsertAllAsync (IEnumerable objects, string extra, bool runInTransaction = true) { return WriteAsync (conn => conn.InsertAll (objects, extra, runInTransaction)); @@ -1079,6 +1423,9 @@ public Task InsertAllAsync (IEnumerable objects, string extra, bool runInTr /// /// The number of rows added to the table. /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] +#endif public Task InsertAllAsync (IEnumerable objects, Type objType, bool runInTransaction = true) { return WriteAsync (conn => conn.InsertAll (objects, objType, runInTransaction)); @@ -1117,7 +1464,11 @@ public Task RunInTransactionAsync (Action action) /// A queryable object that is able to translate Where, OrderBy, and Take /// queries into native SQL. /// - public AsyncTableQuery Table () + public AsyncTableQuery Table< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> () where T : new() { // @@ -1167,7 +1518,11 @@ public Task ExecuteScalarAsync (string query, params object[] args) /// /// A list with one result for each row returned by the query. /// - public Task> QueryAsync (string query, params object[] args) + public Task> QueryAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (string query, params object[] args) where T : new() { return ReadAsync (conn => conn.Query (query, args)); @@ -1234,7 +1589,11 @@ public Task> QueryAsync (TableMapping map, string query, params obj /// 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 Task> DeferredQueryAsync (string query, params object[] args) + public Task> DeferredQueryAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (string query, params object[] args) where T : new() { return ReadAsync (conn => (IEnumerable)conn.DeferredQuery (query, args).ToList ()); @@ -1295,7 +1654,11 @@ public Task ReKeyAsync (byte[] key) /// /// Query to an asynchronous database connection. /// - public class AsyncTableQuery + public class AsyncTableQuery< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> where T : new() { TableQuery _innerQuery; From 7df8f10a267ec0974be7cb5453fd6828c375d390 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 14 Apr 2024 10:23:23 -0700 Subject: [PATCH 09/14] Add multi-targeting to all projects --- nuget/SQLite-net-base/SQLite-net-base.csproj | 1 + nuget/SQLite-net-sqlcipher/SQLite-net-sqlcipher.csproj | 3 ++- nuget/SQLite-net-static/SQLite-net-static.csproj | 3 ++- nuget/SQLite-net-std/SQLite-net-std.csproj | 3 ++- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/nuget/SQLite-net-base/SQLite-net-base.csproj b/nuget/SQLite-net-base/SQLite-net-base.csproj index a1ffaa22..b576eb89 100644 --- a/nuget/SQLite-net-base/SQLite-net-base.csproj +++ b/nuget/SQLite-net-base/SQLite-net-base.csproj @@ -10,6 +10,7 @@ It is meant to give you all the power of SQLite-net but with the freedom to choose your own provider. Please use the package sqlite-net-pcl if you have no idea what any of this means. + true diff --git a/nuget/SQLite-net-sqlcipher/SQLite-net-sqlcipher.csproj b/nuget/SQLite-net-sqlcipher/SQLite-net-sqlcipher.csproj index dd1c02f4..0e693df1 100644 --- a/nuget/SQLite-net-sqlcipher/SQLite-net-sqlcipher.csproj +++ b/nuget/SQLite-net-sqlcipher/SQLite-net-sqlcipher.csproj @@ -1,7 +1,7 @@ - netstandard2.0 + netstandard2.0;net8.0 SQLite-net sqlite-net-sqlcipher SQLite-net SQLCipher .NET Standard Library @@ -11,6 +11,7 @@ This enables secure access to the database with password (key) access. sqlite-net;sqlite;database;orm;encryption;sqlcipher + true diff --git a/nuget/SQLite-net-static/SQLite-net-static.csproj b/nuget/SQLite-net-static/SQLite-net-static.csproj index fe63605d..a8ecb5a9 100644 --- a/nuget/SQLite-net-static/SQLite-net-static.csproj +++ b/nuget/SQLite-net-static/SQLite-net-static.csproj @@ -1,7 +1,7 @@ - netstandard2.0 + netstandard2.0;net8.0 SQLite-net sqlite-net-static SQLite-net .NET Standard P/Invoke Library @@ -10,6 +10,7 @@ This version uses P/Invokes to the "sqlite3" native library provided by the operating system. This works on Xamarin.iOS, Xamarin.Mac, Wilderness Labs' Meadow, and any other platform that has a "sqlite3" library in the path. + true diff --git a/nuget/SQLite-net-std/SQLite-net-std.csproj b/nuget/SQLite-net-std/SQLite-net-std.csproj index 4b7fc253..b72b68f7 100644 --- a/nuget/SQLite-net-std/SQLite-net-std.csproj +++ b/nuget/SQLite-net-std/SQLite-net-std.csproj @@ -1,7 +1,7 @@ - netstandard2.0 + netstandard2.0;net8.0 SQLite-net sqlite-net-pcl SQLite-net Official .NET Standard Library @@ -9,6 +9,7 @@ SQLite-net is an open source and light weight library providing easy SQLite database storage for .NET, Mono, and Xamarin applications. This version uses SQLitePCLRaw to provide platform independent versions of SQLite. + true From 8f4cb6050b612d0822245613aac42d54ef1d6d9f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 14 Apr 2024 10:26:43 -0700 Subject: [PATCH 10/14] Fix TFM for XML docs paths --- nuget/SQLite-net-base/SQLite-net-base.csproj | 4 ++-- nuget/SQLite-net-sqlcipher/SQLite-net-sqlcipher.csproj | 4 ++-- nuget/SQLite-net-static/SQLite-net-static.csproj | 4 ++-- nuget/SQLite-net-std/SQLite-net-std.csproj | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/nuget/SQLite-net-base/SQLite-net-base.csproj b/nuget/SQLite-net-base/SQLite-net-base.csproj index b576eb89..04f19cda 100644 --- a/nuget/SQLite-net-base/SQLite-net-base.csproj +++ b/nuget/SQLite-net-base/SQLite-net-base.csproj @@ -15,11 +15,11 @@ USE_SQLITEPCL_RAW;NO_SQLITEPCL_RAW_BATTERIES;RELEASE - bin\Release\netstandard2.0\SQLite-net.xml + bin\Release\$(TargetFramework)\SQLite-net.xml USE_SQLITEPCL_RAW;NO_SQLITEPCL_RAW_BATTERIES;DEBUG - bin\Debug\netstandard2.0\SQLite-net.xml + bin\Debug\$(TargetFramework)\SQLite-net.xml diff --git a/nuget/SQLite-net-sqlcipher/SQLite-net-sqlcipher.csproj b/nuget/SQLite-net-sqlcipher/SQLite-net-sqlcipher.csproj index 0e693df1..683b5fcc 100644 --- a/nuget/SQLite-net-sqlcipher/SQLite-net-sqlcipher.csproj +++ b/nuget/SQLite-net-sqlcipher/SQLite-net-sqlcipher.csproj @@ -16,11 +16,11 @@ USE_SQLITEPCL_RAW;RELEASE - bin\Release\netstandard2.0\SQLite-net.xml + bin\Release\$(TargetFramework)\SQLite-net.xml USE_SQLITEPCL_RAW;DEBUG - bin\Debug\netstandard2.0\SQLite-net.xml + bin\Debug\$(TargetFramework)\SQLite-net.xml diff --git a/nuget/SQLite-net-static/SQLite-net-static.csproj b/nuget/SQLite-net-static/SQLite-net-static.csproj index a8ecb5a9..f18b40eb 100644 --- a/nuget/SQLite-net-static/SQLite-net-static.csproj +++ b/nuget/SQLite-net-static/SQLite-net-static.csproj @@ -15,11 +15,11 @@ RELEASE - bin\Release\netstandard2.0\SQLite-net.xml + bin\Release\$(TargetFramework)\SQLite-net.xml DEBUG - bin\Debug\netstandard2.0\SQLite-net.xml + bin\Debug\$(TargetFramework)\SQLite-net.xml diff --git a/nuget/SQLite-net-std/SQLite-net-std.csproj b/nuget/SQLite-net-std/SQLite-net-std.csproj index b72b68f7..c1685598 100644 --- a/nuget/SQLite-net-std/SQLite-net-std.csproj +++ b/nuget/SQLite-net-std/SQLite-net-std.csproj @@ -14,11 +14,11 @@ USE_SQLITEPCL_RAW;RELEASE - bin\Release\netstandard2.0\SQLite-net.xml + bin\Release\$(TargetFramework)\SQLite-net.xml USE_SQLITEPCL_RAW;DEBUG - bin\Debug\netstandard2.0\SQLite-net.xml + bin\Debug\$(TargetFramework)\SQLite-net.xml From dddec8e3367c4ba285fa9e17b21e3ca21fc4640b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 14 Apr 2024 10:35:43 -0700 Subject: [PATCH 11/14] Fix whitespace to follow codebase conventions --- src/SQLite.cs | 9600 ++++++++++++++++++++++++------------------------- 1 file changed, 4800 insertions(+), 4800 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index ed5b84ca..33d712d7 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -65,150 +65,150 @@ namespace SQLite { - public class SQLiteException : Exception - { - public SQLite3.Result Result { get; private set; } - - protected SQLiteException (SQLite3.Result r, string message) : base (message) - { - Result = r; - } - - public static SQLiteException New (SQLite3.Result r, string message) - { - return new SQLiteException (r, message); - } - } - - public class NotNullConstraintViolationException : SQLiteException - { - public IEnumerable Columns { get; protected set; } - - protected NotNullConstraintViolationException (SQLite3.Result r, string message) - : this (r, message, null, null) - { - - } - - protected NotNullConstraintViolationException (SQLite3.Result r, string message, TableMapping mapping, object obj) - : base (r, message) - { - if (mapping != null && obj != null) { - this.Columns = from c in mapping.Columns - where c.IsNullable == false && c.GetValue (obj) == null - select c; - } - } - - public static new NotNullConstraintViolationException New (SQLite3.Result r, string message) - { - return new NotNullConstraintViolationException (r, message); - } - - public static NotNullConstraintViolationException New (SQLite3.Result r, string message, TableMapping mapping, object obj) - { - return new NotNullConstraintViolationException (r, message, mapping, obj); - } - - public static NotNullConstraintViolationException New (SQLiteException exception, TableMapping mapping, object obj) - { - return new NotNullConstraintViolationException (exception.Result, exception.Message, mapping, obj); - } - } - - [Flags] - public enum SQLiteOpenFlags - { - ReadOnly = 1, ReadWrite = 2, Create = 4, - Uri = 0x40, Memory = 0x80, - NoMutex = 0x8000, FullMutex = 0x10000, - SharedCache = 0x20000, PrivateCache = 0x40000, - ProtectionComplete = 0x00100000, - ProtectionCompleteUnlessOpen = 0x00200000, - ProtectionCompleteUntilFirstUserAuthentication = 0x00300000, - ProtectionNone = 0x00400000 - } - - [Flags] - public enum CreateFlags - { - /// - /// Use the default creation options - /// - None = 0x000, - /// - /// Create a primary key index for a property called 'Id' (case-insensitive). - /// This avoids the need for the [PrimaryKey] attribute. - /// - ImplicitPK = 0x001, - /// - /// Create indices for properties ending in 'Id' (case-insensitive). - /// - ImplicitIndex = 0x002, - /// - /// Create a primary key for a property called 'Id' and - /// create an indices for properties ending in 'Id' (case-insensitive). - /// - AllImplicit = 0x003, - /// - /// Force the primary key property to be auto incrementing. - /// This avoids the need for the [AutoIncrement] attribute. - /// The primary key property on the class should have type int or long. - /// - AutoIncPK = 0x004, - /// - /// Create virtual table using FTS3 - /// - FullTextSearch3 = 0x100, - /// - /// Create virtual table using FTS4 - /// - FullTextSearch4 = 0x200 - } - - public interface ISQLiteConnection : IDisposable - { - Sqlite3DatabaseHandle Handle { get; } - string DatabasePath { get; } - int LibVersionNumber { get; } - bool TimeExecution { get; set; } - bool Trace { get; set; } - Action Tracer { get; set; } - bool StoreDateTimeAsTicks { get; } - bool StoreTimeSpanAsTicks { get; } - string DateTimeStringFormat { get; } - TimeSpan BusyTimeout { get; set; } - IEnumerable TableMappings { get; } - bool IsInTransaction { get; } - - event EventHandler TableChanged; - - void Backup (string destinationDatabasePath, string databaseName = "main"); - void BeginTransaction (); - void Close (); - void Commit (); - SQLiteCommand CreateCommand (string cmdText, params object[] ps); - SQLiteCommand CreateCommand (string cmdText, Dictionary args); - int CreateIndex (string indexName, string tableName, string[] columnNames, bool unique = false); - int CreateIndex (string indexName, string tableName, string columnName, bool unique = false); - int CreateIndex (string tableName, string columnName, bool unique = false); - int CreateIndex (string tableName, string[] columnNames, bool unique = false); - int CreateIndex< + public class SQLiteException : Exception + { + public SQLite3.Result Result { get; private set; } + + protected SQLiteException (SQLite3.Result r, string message) : base (message) + { + Result = r; + } + + public static SQLiteException New (SQLite3.Result r, string message) + { + return new SQLiteException (r, message); + } + } + + public class NotNullConstraintViolationException : SQLiteException + { + public IEnumerable Columns { get; protected set; } + + protected NotNullConstraintViolationException (SQLite3.Result r, string message) + : this (r, message, null, null) + { + + } + + protected NotNullConstraintViolationException (SQLite3.Result r, string message, TableMapping mapping, object obj) + : base (r, message) + { + if (mapping != null && obj != null) { + this.Columns = from c in mapping.Columns + where c.IsNullable == false && c.GetValue (obj) == null + select c; + } + } + + public static new NotNullConstraintViolationException New (SQLite3.Result r, string message) + { + return new NotNullConstraintViolationException (r, message); + } + + public static NotNullConstraintViolationException New (SQLite3.Result r, string message, TableMapping mapping, object obj) + { + return new NotNullConstraintViolationException (r, message, mapping, obj); + } + + public static NotNullConstraintViolationException New (SQLiteException exception, TableMapping mapping, object obj) + { + return new NotNullConstraintViolationException (exception.Result, exception.Message, mapping, obj); + } + } + + [Flags] + public enum SQLiteOpenFlags + { + ReadOnly = 1, ReadWrite = 2, Create = 4, + Uri = 0x40, Memory = 0x80, + NoMutex = 0x8000, FullMutex = 0x10000, + SharedCache = 0x20000, PrivateCache = 0x40000, + ProtectionComplete = 0x00100000, + ProtectionCompleteUnlessOpen = 0x00200000, + ProtectionCompleteUntilFirstUserAuthentication = 0x00300000, + ProtectionNone = 0x00400000 + } + + [Flags] + public enum CreateFlags + { + /// + /// Use the default creation options + /// + None = 0x000, + /// + /// Create a primary key index for a property called 'Id' (case-insensitive). + /// This avoids the need for the [PrimaryKey] attribute. + /// + ImplicitPK = 0x001, + /// + /// Create indices for properties ending in 'Id' (case-insensitive). + /// + ImplicitIndex = 0x002, + /// + /// Create a primary key for a property called 'Id' and + /// create an indices for properties ending in 'Id' (case-insensitive). + /// + AllImplicit = 0x003, + /// + /// Force the primary key property to be auto incrementing. + /// This avoids the need for the [AutoIncrement] attribute. + /// The primary key property on the class should have type int or long. + /// + AutoIncPK = 0x004, + /// + /// Create virtual table using FTS3 + /// + FullTextSearch3 = 0x100, + /// + /// Create virtual table using FTS4 + /// + FullTextSearch4 = 0x200 + } + + public interface ISQLiteConnection : IDisposable + { + Sqlite3DatabaseHandle Handle { get; } + string DatabasePath { get; } + int LibVersionNumber { get; } + bool TimeExecution { get; set; } + bool Trace { get; set; } + Action Tracer { get; set; } + bool StoreDateTimeAsTicks { get; } + bool StoreTimeSpanAsTicks { get; } + string DateTimeStringFormat { get; } + TimeSpan BusyTimeout { get; set; } + IEnumerable TableMappings { get; } + bool IsInTransaction { get; } + + event EventHandler TableChanged; + + void Backup (string destinationDatabasePath, string databaseName = "main"); + void BeginTransaction (); + void Close (); + void Commit (); + SQLiteCommand CreateCommand (string cmdText, params object[] ps); + SQLiteCommand CreateCommand (string cmdText, Dictionary args); + int CreateIndex (string indexName, string tableName, string[] columnNames, bool unique = false); + int CreateIndex (string indexName, string tableName, string columnName, bool unique = false); + int CreateIndex (string tableName, string columnName, bool unique = false); + int CreateIndex (string tableName, string[] columnNames, bool unique = false); + int CreateIndex< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T> (Expression> property, bool unique = false); - CreateTableResult CreateTable< + CreateTableResult CreateTable< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T> (CreateFlags createFlags = CreateFlags.None); - CreateTableResult CreateTable ( + CreateTableResult CreateTable ( #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif Type ty, CreateFlags createFlags = CreateFlags.None); - CreateTablesResult CreateTables< + CreateTablesResult CreateTables< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif @@ -217,9 +217,9 @@ CreateTablesResult CreateTables< [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T2> (CreateFlags createFlags = CreateFlags.None) - where T : new() - where T2 : new(); - CreateTablesResult CreateTables< + where T : new() + where T2 : new(); + CreateTablesResult CreateTables< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif @@ -232,10 +232,10 @@ CreateTablesResult CreateTables< [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T3> (CreateFlags createFlags = CreateFlags.None) - where T : new() - where T2 : new() - where T3 : new(); - CreateTablesResult CreateTables< + where T : new() + where T2 : new() + where T3 : new(); + CreateTablesResult CreateTables< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif @@ -252,11 +252,11 @@ CreateTablesResult CreateTables< [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T4> (CreateFlags createFlags = CreateFlags.None) - where T : new() - where T2 : new() - where T3 : new() - where T4 : new(); - CreateTablesResult CreateTables< + where T : new() + where T2 : new() + where T3 : new() + where T4 : new(); + CreateTablesResult CreateTables< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif @@ -277,91 +277,91 @@ CreateTablesResult CreateTables< [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T5> (CreateFlags createFlags = CreateFlags.None) - where T : new() - where T2 : new() - where T3 : new() - where T4 : new() - where T5 : new(); + where T : new() + where T2 : new() + where T3 : new() + where T4 : new() + where T5 : new(); #if NET [RequiresUnreferencedCode ("This method requires 'DynamicallyAccessedMemberTypes.All' on each input 'Type' instance.")] #endif CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None, params Type[] types); - IEnumerable DeferredQuery< + IEnumerable DeferredQuery< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T> (string query, params object[] args) where T : new(); - IEnumerable DeferredQuery (TableMapping map, string query, params object[] args); + IEnumerable DeferredQuery (TableMapping map, string query, params object[] args); #if NET8_0_OR_GREATER [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'objectToDelete'.")] #endif int Delete (object objectToDelete); - int Delete< + int Delete< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T> (object primaryKey); - int Delete (object primaryKey, TableMapping map); - int DeleteAll< + int Delete (object primaryKey, TableMapping map); + int DeleteAll< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T> (); - int DeleteAll (TableMapping map); - int DropTable< + int DeleteAll (TableMapping map); + int DropTable< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T> (); - int DropTable (TableMapping map); - void EnableLoadExtension (bool enabled); - void EnableWriteAheadLogging (); - int Execute (string query, params object[] args); - T ExecuteScalar (string query, params object[] args); - T Find< + int DropTable (TableMapping map); + void EnableLoadExtension (bool enabled); + void EnableWriteAheadLogging (); + int Execute (string query, params object[] args); + T ExecuteScalar (string query, params object[] args); + T Find< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T> (object pk) where T : new(); - object Find (object pk, TableMapping map); - T Find< + object Find (object pk, TableMapping map); + T Find< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T> (Expression> predicate) where T : new(); - T FindWithQuery< + T FindWithQuery< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T> (string query, params object[] args) where T : new(); - object FindWithQuery (TableMapping map, string query, params object[] args); - T Get< + object FindWithQuery (TableMapping map, string query, params object[] args); + T Get< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T> (object pk) where T : new(); - object Get (object pk, TableMapping map); - T Get< + object Get (object pk, TableMapping map); + T Get< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T> (Expression> predicate) where T : new(); - TableMapping GetMapping ( + TableMapping GetMapping ( #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif Type type, CreateFlags createFlags = CreateFlags.None); - TableMapping GetMapping< + TableMapping GetMapping< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T> (CreateFlags createFlags = CreateFlags.None); - List GetTableInfo (string tableName); + List GetTableInfo (string tableName); #if NET8_0_OR_GREATER [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] #endif int Insert (object obj); - int Insert ( + int Insert ( object obj, #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] @@ -371,7 +371,7 @@ int Insert ( [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] #endif int Insert (object obj, string extra); - int Insert ( + int Insert ( object obj, string extra, #if NET8_0_OR_GREATER @@ -386,7 +386,7 @@ int Insert ( [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] #endif int InsertAll (IEnumerable objects, string extra, bool runInTransaction = true); - int InsertAll ( + int InsertAll ( IEnumerable objects, #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] @@ -397,27 +397,27 @@ int InsertAll ( [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] #endif int InsertOrReplace (object obj); - int InsertOrReplace ( + int InsertOrReplace ( object obj, #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif Type objType); - List Query< + List Query< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T> (string query, params object[] args) where T : new(); - List Query (TableMapping map, string query, params object[] args); - List QueryScalars (string query, params object[] args); - void ReKey (string key); - void ReKey (byte[] key); - void Release (string savepoint); - void Rollback (); - void RollbackTo (string savepoint); - void RunInTransaction (Action action); - string SaveTransactionPoint (); - TableQuery Table< + List Query (TableMapping map, string query, params object[] args); + List QueryScalars (string query, params object[] args); + void ReKey (string key); + void ReKey (byte[] key); + void Release (string savepoint); + void Rollback (); + void RollbackTo (string savepoint); + void RunInTransaction (Action action); + string SaveTransactionPoint (); + TableQuery Table< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif @@ -426,7 +426,7 @@ TableQuery Table< [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] #endif int Update (object obj); - int Update ( + int Update ( object obj, #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] @@ -436,513 +436,513 @@ int Update ( [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] #endif int UpdateAll (IEnumerable objects, bool runInTransaction = true); - } - - /// - /// An open connection to a SQLite database. - /// - [Preserve (AllMembers = true)] - public partial class SQLiteConnection : ISQLiteConnection - { - private bool _open; - private TimeSpan _busyTimeout; - readonly static Dictionary _mappings = new Dictionary (); - private System.Diagnostics.Stopwatch _sw; - private long _elapsedMilliseconds = 0; - - private int _transactionDepth = 0; - private Random _rand = new Random (); - - public Sqlite3DatabaseHandle Handle { get; private set; } - static readonly Sqlite3DatabaseHandle NullHandle = default (Sqlite3DatabaseHandle); - static readonly Sqlite3BackupHandle NullBackupHandle = default (Sqlite3BackupHandle); - - /// - /// Gets the database path used by this connection. - /// - public string DatabasePath { get; private set; } - - /// - /// Gets the SQLite library version number. 3007014 would be v3.7.14 - /// - public int LibVersionNumber { get; private set; } - - /// - /// Whether Trace lines should be written that show the execution time of queries. - /// - public bool TimeExecution { get; set; } - - /// - /// Whether to write queries to during execution. - /// - public bool Trace { get; set; } - - /// - /// The delegate responsible for writing trace lines. - /// - /// The tracer. - public Action Tracer { get; set; } - - /// - /// Whether to store DateTime properties as ticks (true) or strings (false). - /// - public bool StoreDateTimeAsTicks { get; private set; } - - /// - /// Whether to store TimeSpan properties as ticks (true) or strings (false). - /// - public bool StoreTimeSpanAsTicks { get; private set; } - - /// - /// The format to use when storing DateTime properties as strings. Ignored if StoreDateTimeAsTicks is true. - /// - /// The date time string format. - public string DateTimeStringFormat { get; private set; } - - /// - /// The DateTimeStyles value to use when parsing a DateTime property string. - /// - /// The date time style. - internal System.Globalization.DateTimeStyles DateTimeStyle { get; private set; } + } + + /// + /// An open connection to a SQLite database. + /// + [Preserve (AllMembers = true)] + public partial class SQLiteConnection : ISQLiteConnection + { + private bool _open; + private TimeSpan _busyTimeout; + readonly static Dictionary _mappings = new Dictionary (); + private System.Diagnostics.Stopwatch _sw; + private long _elapsedMilliseconds = 0; + + private int _transactionDepth = 0; + private Random _rand = new Random (); + + public Sqlite3DatabaseHandle Handle { get; private set; } + static readonly Sqlite3DatabaseHandle NullHandle = default (Sqlite3DatabaseHandle); + static readonly Sqlite3BackupHandle NullBackupHandle = default (Sqlite3BackupHandle); + + /// + /// Gets the database path used by this connection. + /// + public string DatabasePath { get; private set; } + + /// + /// Gets the SQLite library version number. 3007014 would be v3.7.14 + /// + public int LibVersionNumber { get; private set; } + + /// + /// Whether Trace lines should be written that show the execution time of queries. + /// + public bool TimeExecution { get; set; } + + /// + /// Whether to write queries to during execution. + /// + public bool Trace { get; set; } + + /// + /// The delegate responsible for writing trace lines. + /// + /// The tracer. + public Action Tracer { get; set; } + + /// + /// Whether to store DateTime properties as ticks (true) or strings (false). + /// + public bool StoreDateTimeAsTicks { get; private set; } + + /// + /// Whether to store TimeSpan properties as ticks (true) or strings (false). + /// + public bool StoreTimeSpanAsTicks { get; private set; } + + /// + /// The format to use when storing DateTime properties as strings. Ignored if StoreDateTimeAsTicks is true. + /// + /// The date time string format. + public string DateTimeStringFormat { get; private set; } + + /// + /// The DateTimeStyles value to use when parsing a DateTime property string. + /// + /// The date time style. + internal System.Globalization.DateTimeStyles DateTimeStyle { get; private set; } #if USE_SQLITEPCL_RAW && !NO_SQLITEPCL_RAW_BATTERIES - static SQLiteConnection () - { - SQLitePCL.Batteries_V2.Init (); - } + static SQLiteConnection () + { + SQLitePCL.Batteries_V2.Init (); + } #endif - /// - /// Constructs a new SQLiteConnection and opens a 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 SQLiteConnection (string databasePath, bool storeDateTimeAsTicks = true) - : this (new SQLiteConnectionString (databasePath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create, storeDateTimeAsTicks)) - { - } - - /// - /// Constructs a new SQLiteConnection and opens a SQLite database specified by databasePath. - /// - /// - /// Specifies the path to the database file. - /// - /// - /// Flags controlling how the connection should be opened. - /// - /// - /// 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 SQLiteConnection (string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks = true) - : this (new SQLiteConnectionString (databasePath, openFlags, storeDateTimeAsTicks)) - { - } - - /// - /// Constructs a new SQLiteConnection and opens a SQLite database specified by databasePath. - /// - /// - /// Details on how to find and open the database. - /// - public SQLiteConnection (SQLiteConnectionString connectionString) - { - if (connectionString == null) - throw new ArgumentNullException (nameof (connectionString)); - if (connectionString.DatabasePath == null) - throw new InvalidOperationException ("DatabasePath must be specified"); - - DatabasePath = connectionString.DatabasePath; - - LibVersionNumber = SQLite3.LibVersionNumber (); + /// + /// Constructs a new SQLiteConnection and opens a 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 SQLiteConnection (string databasePath, bool storeDateTimeAsTicks = true) + : this (new SQLiteConnectionString (databasePath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create, storeDateTimeAsTicks)) + { + } + + /// + /// Constructs a new SQLiteConnection and opens a SQLite database specified by databasePath. + /// + /// + /// Specifies the path to the database file. + /// + /// + /// Flags controlling how the connection should be opened. + /// + /// + /// 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 SQLiteConnection (string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks = true) + : this (new SQLiteConnectionString (databasePath, openFlags, storeDateTimeAsTicks)) + { + } + + /// + /// Constructs a new SQLiteConnection and opens a SQLite database specified by databasePath. + /// + /// + /// Details on how to find and open the database. + /// + public SQLiteConnection (SQLiteConnectionString connectionString) + { + if (connectionString == null) + throw new ArgumentNullException (nameof (connectionString)); + if (connectionString.DatabasePath == null) + throw new InvalidOperationException ("DatabasePath must be specified"); + + DatabasePath = connectionString.DatabasePath; + + LibVersionNumber = SQLite3.LibVersionNumber (); #if NETFX_CORE - SQLite3.SetDirectory(/*temp directory type*/2, Windows.Storage.ApplicationData.Current.TemporaryFolder.Path); + SQLite3.SetDirectory(/*temp directory type*/2, Windows.Storage.ApplicationData.Current.TemporaryFolder.Path); #endif - Sqlite3DatabaseHandle handle; + Sqlite3DatabaseHandle handle; #if SILVERLIGHT || USE_CSHARP_SQLITE || USE_SQLITEPCL_RAW - var r = SQLite3.Open (connectionString.DatabasePath, out handle, (int)connectionString.OpenFlags, connectionString.VfsName); + var r = SQLite3.Open (connectionString.DatabasePath, out handle, (int)connectionString.OpenFlags, connectionString.VfsName); #else - // open using the byte[] - // in the case where the path may include Unicode - // force open to using UTF-8 using sqlite3_open_v2 - var databasePathAsBytes = GetNullTerminatedUtf8 (connectionString.DatabasePath); - var r = SQLite3.Open (databasePathAsBytes, out handle, (int)connectionString.OpenFlags, connectionString.VfsName); + // open using the byte[] + // in the case where the path may include Unicode + // force open to using UTF-8 using sqlite3_open_v2 + var databasePathAsBytes = GetNullTerminatedUtf8 (connectionString.DatabasePath); + var r = SQLite3.Open (databasePathAsBytes, out handle, (int)connectionString.OpenFlags, connectionString.VfsName); #endif - Handle = handle; - if (r != SQLite3.Result.OK) { - throw SQLiteException.New (r, String.Format ("Could not open database file: {0} ({1})", DatabasePath, r)); - } - _open = true; - - StoreDateTimeAsTicks = connectionString.StoreDateTimeAsTicks; - StoreTimeSpanAsTicks = connectionString.StoreTimeSpanAsTicks; - DateTimeStringFormat = connectionString.DateTimeStringFormat; - DateTimeStyle = connectionString.DateTimeStyle; - - BusyTimeout = TimeSpan.FromSeconds (1.0); - Tracer = line => Debug.WriteLine (line); - - connectionString.PreKeyAction?.Invoke (this); - if (connectionString.Key is string stringKey) { - SetKey (stringKey); - } - else if (connectionString.Key is byte[] bytesKey) { - SetKey (bytesKey); - } - else if (connectionString.Key != null) { - throw new InvalidOperationException ("Encryption keys must be strings or byte arrays"); - } - connectionString.PostKeyAction?.Invoke (this); - } - - /// - /// 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 void EnableWriteAheadLogging () - { - ExecuteScalar ("PRAGMA journal_mode=WAL"); - } - - /// - /// Convert an input string to a quoted SQL string that can be safely used in queries. - /// - /// The quoted string. - /// The unsafe string to quote. - static string Quote (string unsafeString) - { - // TODO: Doesn't call sqlite3_mprintf("%Q", u) because we're waiting on https://github.com/ericsink/SQLitePCL.raw/issues/153 - if (unsafeString == null) - return "NULL"; - var safe = unsafeString.Replace ("'", "''"); - return "'" + safe + "'"; - } - - /// - /// Sets the key used to encrypt/decrypt the database with "pragma key = ...". - /// This must be the first thing you call before doing anything else with this connection - /// if your database is encrypted. - /// This only has an effect if you are using the SQLCipher nuget package. - /// - /// Encryption key plain text that is converted to the real encryption key using PBKDF2 key derivation - void SetKey (string key) - { - if (key == null) - throw new ArgumentNullException (nameof (key)); - var q = Quote (key); - ExecuteScalar ("pragma key = " + q); - } - - /// - /// Sets the key used to encrypt/decrypt the database. - /// This must be the first thing you call before doing anything else with this connection - /// if your database is encrypted. - /// This only has an effect if you are using the SQLCipher nuget package. - /// - /// 256-bit (32 byte) encryption key data - void SetKey (byte[] key) - { - if (key == null) - throw new ArgumentNullException (nameof (key)); - if (key.Length != 32 && key.Length != 48) - throw new ArgumentException ("Key must be 32 bytes (256-bit) or 48 bytes (384-bit)", nameof (key)); - var s = String.Join ("", key.Select (x => x.ToString ("X2"))); - ExecuteScalar ("pragma key = \"x'" + s + "'\""); - } - - /// - /// Change the encryption key for a SQLCipher database with "pragma rekey = ...". - /// - /// Encryption key plain text that is converted to the real encryption key using PBKDF2 key derivation - public void ReKey (string key) - { - if (key == null) - throw new ArgumentNullException(nameof(key)); - var q = Quote(key); - ExecuteScalar("pragma rekey = " + q); - } - - /// - /// Change the encryption key for a SQLCipher database. - /// - /// 256-bit (32 byte) or 384-bit (48 bytes) encryption key data - public void ReKey (byte[] key) - { - if (key == null) - throw new ArgumentNullException(nameof(key)); - if (key.Length != 32 && key.Length != 48) - throw new ArgumentException ("Key must be 32 bytes (256-bit) or 48 bytes (384-bit)", nameof (key)); - var s = String.Join("", key.Select(x => x.ToString("X2"))); - ExecuteScalar("pragma rekey = \"x'" + s + "'\""); - } - - /// - /// Enable or disable extension loading. - /// - public void EnableLoadExtension (bool enabled) - { - SQLite3.Result r = SQLite3.EnableLoadExtension (Handle, enabled ? 1 : 0); - if (r != SQLite3.Result.OK) { - string msg = SQLite3.GetErrmsg (Handle); - throw SQLiteException.New (r, msg); - } - } + Handle = handle; + if (r != SQLite3.Result.OK) { + throw SQLiteException.New (r, String.Format ("Could not open database file: {0} ({1})", DatabasePath, r)); + } + _open = true; + + StoreDateTimeAsTicks = connectionString.StoreDateTimeAsTicks; + StoreTimeSpanAsTicks = connectionString.StoreTimeSpanAsTicks; + DateTimeStringFormat = connectionString.DateTimeStringFormat; + DateTimeStyle = connectionString.DateTimeStyle; + + BusyTimeout = TimeSpan.FromSeconds (1.0); + Tracer = line => Debug.WriteLine (line); + + connectionString.PreKeyAction?.Invoke (this); + if (connectionString.Key is string stringKey) { + SetKey (stringKey); + } + else if (connectionString.Key is byte[] bytesKey) { + SetKey (bytesKey); + } + else if (connectionString.Key != null) { + throw new InvalidOperationException ("Encryption keys must be strings or byte arrays"); + } + connectionString.PostKeyAction?.Invoke (this); + } + + /// + /// 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 void EnableWriteAheadLogging () + { + ExecuteScalar ("PRAGMA journal_mode=WAL"); + } + + /// + /// Convert an input string to a quoted SQL string that can be safely used in queries. + /// + /// The quoted string. + /// The unsafe string to quote. + static string Quote (string unsafeString) + { + // TODO: Doesn't call sqlite3_mprintf("%Q", u) because we're waiting on https://github.com/ericsink/SQLitePCL.raw/issues/153 + if (unsafeString == null) + return "NULL"; + var safe = unsafeString.Replace ("'", "''"); + return "'" + safe + "'"; + } + + /// + /// Sets the key used to encrypt/decrypt the database with "pragma key = ...". + /// This must be the first thing you call before doing anything else with this connection + /// if your database is encrypted. + /// This only has an effect if you are using the SQLCipher nuget package. + /// + /// Encryption key plain text that is converted to the real encryption key using PBKDF2 key derivation + void SetKey (string key) + { + if (key == null) + throw new ArgumentNullException (nameof (key)); + var q = Quote (key); + ExecuteScalar ("pragma key = " + q); + } + + /// + /// Sets the key used to encrypt/decrypt the database. + /// This must be the first thing you call before doing anything else with this connection + /// if your database is encrypted. + /// This only has an effect if you are using the SQLCipher nuget package. + /// + /// 256-bit (32 byte) encryption key data + void SetKey (byte[] key) + { + if (key == null) + throw new ArgumentNullException (nameof (key)); + if (key.Length != 32 && key.Length != 48) + throw new ArgumentException ("Key must be 32 bytes (256-bit) or 48 bytes (384-bit)", nameof (key)); + var s = String.Join ("", key.Select (x => x.ToString ("X2"))); + ExecuteScalar ("pragma key = \"x'" + s + "'\""); + } + + /// + /// Change the encryption key for a SQLCipher database with "pragma rekey = ...". + /// + /// Encryption key plain text that is converted to the real encryption key using PBKDF2 key derivation + public void ReKey (string key) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + var q = Quote(key); + ExecuteScalar("pragma rekey = " + q); + } + + /// + /// Change the encryption key for a SQLCipher database. + /// + /// 256-bit (32 byte) or 384-bit (48 bytes) encryption key data + public void ReKey (byte[] key) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (key.Length != 32 && key.Length != 48) + throw new ArgumentException ("Key must be 32 bytes (256-bit) or 48 bytes (384-bit)", nameof (key)); + var s = String.Join("", key.Select(x => x.ToString("X2"))); + ExecuteScalar("pragma rekey = \"x'" + s + "'\""); + } + + /// + /// Enable or disable extension loading. + /// + public void EnableLoadExtension (bool enabled) + { + SQLite3.Result r = SQLite3.EnableLoadExtension (Handle, enabled ? 1 : 0); + if (r != SQLite3.Result.OK) { + string msg = SQLite3.GetErrmsg (Handle); + throw SQLiteException.New (r, msg); + } + } #if !USE_SQLITEPCL_RAW - static byte[] GetNullTerminatedUtf8 (string s) - { - var utf8Length = System.Text.Encoding.UTF8.GetByteCount (s); - var bytes = new byte [utf8Length + 1]; - utf8Length = System.Text.Encoding.UTF8.GetBytes(s, 0, s.Length, bytes, 0); - return bytes; - } + static byte[] GetNullTerminatedUtf8 (string s) + { + var utf8Length = System.Text.Encoding.UTF8.GetByteCount (s); + var bytes = new byte [utf8Length + 1]; + utf8Length = System.Text.Encoding.UTF8.GetBytes(s, 0, s.Length, bytes, 0); + return bytes; + } #endif - /// - /// Sets a busy handler to sleep the specified amount of time when a table is locked. - /// The handler will sleep multiple times until a total time of has accumulated. - /// - public TimeSpan BusyTimeout { - get { return _busyTimeout; } - set { - _busyTimeout = value; - if (Handle != NullHandle) { - SQLite3.BusyTimeout (Handle, (int)_busyTimeout.TotalMilliseconds); - } - } - } - - /// - /// Returns the mappings from types to tables that the connection - /// currently understands. - /// - public IEnumerable TableMappings { - get { - lock (_mappings) { - return new List (_mappings.Values); - } - } - } - - /// - /// 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 TableMapping GetMapping ( + /// + /// Sets a busy handler to sleep the specified amount of time when a table is locked. + /// The handler will sleep multiple times until a total time of has accumulated. + /// + public TimeSpan BusyTimeout { + get { return _busyTimeout; } + set { + _busyTimeout = value; + if (Handle != NullHandle) { + SQLite3.BusyTimeout (Handle, (int)_busyTimeout.TotalMilliseconds); + } + } + } + + /// + /// Returns the mappings from types to tables that the connection + /// currently understands. + /// + public IEnumerable TableMappings { + get { + lock (_mappings) { + return new List (_mappings.Values); + } + } + } + + /// + /// 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 TableMapping GetMapping ( #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif Type type, CreateFlags createFlags = CreateFlags.None) - { - TableMapping map; - var key = type.FullName; - lock (_mappings) { - if (_mappings.TryGetValue (key, out map)) { - if (createFlags != CreateFlags.None && createFlags != map.CreateFlags) { - map = new TableMapping (type, createFlags); - _mappings[key] = map; - } - } - else { - map = new TableMapping (type, createFlags); - _mappings.Add (key, map); - } - } - return map; - } - - /// - /// 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 TableMapping GetMapping< + { + TableMapping map; + var key = type.FullName; + lock (_mappings) { + if (_mappings.TryGetValue (key, out map)) { + if (createFlags != CreateFlags.None && createFlags != map.CreateFlags) { + map = new TableMapping (type, createFlags); + _mappings[key] = map; + } + } + else { + map = new TableMapping (type, createFlags); + _mappings.Add (key, map); + } + } + return map; + } + + /// + /// 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 TableMapping GetMapping< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T> (CreateFlags createFlags = CreateFlags.None) - { - return GetMapping (typeof (T), createFlags); - } - - private struct IndexedColumn - { - public int Order; - public string ColumnName; - } - - private struct IndexInfo - { - public string IndexName; - public string TableName; - public bool Unique; - public List Columns; - } - - /// - /// Executes a "drop table" on the database. This is non-recoverable. - /// - public int DropTable< + { + return GetMapping (typeof (T), createFlags); + } + + private struct IndexedColumn + { + public int Order; + public string ColumnName; + } + + private struct IndexInfo + { + public string IndexName; + public string TableName; + public bool Unique; + public List Columns; + } + + /// + /// Executes a "drop table" on the database. This is non-recoverable. + /// + public int DropTable< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T> () - { - return DropTable (GetMapping (typeof (T))); - } - - /// - /// Executes a "drop table" on the database. This is non-recoverable. - /// - /// - /// The TableMapping used to identify the table. - /// - public int DropTable (TableMapping map) - { - var query = string.Format ("drop table if exists \"{0}\"", map.TableName); - return Execute (query); - } - - /// - /// 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 CreateTableResult CreateTable< + { + return DropTable (GetMapping (typeof (T))); + } + + /// + /// Executes a "drop table" on the database. This is non-recoverable. + /// + /// + /// The TableMapping used to identify the table. + /// + public int DropTable (TableMapping map) + { + var query = string.Format ("drop table if exists \"{0}\"", map.TableName); + return Execute (query); + } + + /// + /// 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 CreateTableResult CreateTable< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T> (CreateFlags createFlags = CreateFlags.None) - { - return CreateTable (typeof (T), 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 CreateTableResult CreateTable ( + { + return CreateTable (typeof (T), 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 CreateTableResult CreateTable ( #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif Type ty, CreateFlags createFlags = CreateFlags.None) - { - var map = GetMapping (ty, createFlags); - - // Present a nice error if no columns specified - if (map.Columns.Length == 0) { - throw new Exception (string.Format ("Cannot create a table without columns (does '{0}' have public properties?)", ty.FullName)); - } - - // Check if the table exists - var result = CreateTableResult.Created; - var existingCols = GetTableInfo (map.TableName); - - // Create or migrate it - if (existingCols.Count == 0) { - - // Facilitate virtual tables a.k.a. full-text search. - bool fts3 = (createFlags & CreateFlags.FullTextSearch3) != 0; - bool fts4 = (createFlags & CreateFlags.FullTextSearch4) != 0; - bool fts = fts3 || fts4; - var @virtual = fts ? "virtual " : string.Empty; - var @using = fts3 ? "using fts3 " : fts4 ? "using fts4 " : string.Empty; - - // Build query. - var query = "create " + @virtual + "table if not exists \"" + map.TableName + "\" " + @using + "(\n"; - var decls = map.Columns.Select (p => Orm.SqlDecl (p, StoreDateTimeAsTicks, StoreTimeSpanAsTicks)); - var decl = string.Join (",\n", decls.ToArray ()); - query += decl; - query += ")"; - if (map.WithoutRowId) { - query += " without rowid"; - } - - Execute (query); - } - else { - result = CreateTableResult.Migrated; - MigrateTable (map, existingCols); - } - - var indexes = new Dictionary (); - foreach (var c in map.Columns) { - foreach (var i in c.Indices) { - var iname = i.Name ?? map.TableName + "_" + c.Name; - IndexInfo iinfo; - if (!indexes.TryGetValue (iname, out iinfo)) { - iinfo = new IndexInfo { - IndexName = iname, - TableName = map.TableName, - Unique = i.Unique, - Columns = new List () - }; - indexes.Add (iname, iinfo); - } - - if (i.Unique != iinfo.Unique) - throw new Exception ("All the columns in an index must have the same value for their Unique property"); - - iinfo.Columns.Add (new IndexedColumn { - Order = i.Order, - ColumnName = c.Name - }); - } - } - - foreach (var indexName in indexes.Keys) { - var index = indexes[indexName]; - var columns = index.Columns.OrderBy (i => i.Order).Select (i => i.ColumnName).ToArray (); - CreateIndex (indexName, index.TableName, columns, index.Unique); - } - - return result; - } + { + var map = GetMapping (ty, createFlags); + + // Present a nice error if no columns specified + if (map.Columns.Length == 0) { + throw new Exception (string.Format ("Cannot create a table without columns (does '{0}' have public properties?)", ty.FullName)); + } + + // Check if the table exists + var result = CreateTableResult.Created; + var existingCols = GetTableInfo (map.TableName); + + // Create or migrate it + if (existingCols.Count == 0) { + + // Facilitate virtual tables a.k.a. full-text search. + bool fts3 = (createFlags & CreateFlags.FullTextSearch3) != 0; + bool fts4 = (createFlags & CreateFlags.FullTextSearch4) != 0; + bool fts = fts3 || fts4; + var @virtual = fts ? "virtual " : string.Empty; + var @using = fts3 ? "using fts3 " : fts4 ? "using fts4 " : string.Empty; + + // Build query. + var query = "create " + @virtual + "table if not exists \"" + map.TableName + "\" " + @using + "(\n"; + var decls = map.Columns.Select (p => Orm.SqlDecl (p, StoreDateTimeAsTicks, StoreTimeSpanAsTicks)); + var decl = string.Join (",\n", decls.ToArray ()); + query += decl; + query += ")"; + if (map.WithoutRowId) { + query += " without rowid"; + } + + Execute (query); + } + else { + result = CreateTableResult.Migrated; + MigrateTable (map, existingCols); + } + + var indexes = new Dictionary (); + foreach (var c in map.Columns) { + foreach (var i in c.Indices) { + var iname = i.Name ?? map.TableName + "_" + c.Name; + IndexInfo iinfo; + if (!indexes.TryGetValue (iname, out iinfo)) { + iinfo = new IndexInfo { + IndexName = iname, + TableName = map.TableName, + Unique = i.Unique, + Columns = new List () + }; + indexes.Add (iname, iinfo); + } + + if (i.Unique != iinfo.Unique) + throw new Exception ("All the columns in an index must have the same value for their Unique property"); + + iinfo.Columns.Add (new IndexedColumn { + Order = i.Order, + ColumnName = c.Name + }); + } + } + + foreach (var indexName in indexes.Keys) { + var index = indexes[indexName]; + var columns = index.Columns.OrderBy (i => i.Order).Select (i => i.ColumnName).ToArray (); + CreateIndex (indexName, index.TableName, columns, index.Unique); + } + + return result; + } /// /// Executes a "create table if not exists" on the database for each type. It also @@ -965,11 +965,11 @@ public CreateTablesResult CreateTables< [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif T2> (CreateFlags createFlags = CreateFlags.None) - where T : new() - where T2 : new() - { - return CreateTables (createFlags, typeof (T), typeof (T2)); - } + where T : new() + where T2 : new() + { + return CreateTables (createFlags, typeof (T), typeof (T2)); + } /// /// Executes a "create table if not exists" on the database for each type. It also @@ -997,11 +997,11 @@ public CreateTablesResult CreateTables< #endif T3> (CreateFlags createFlags = CreateFlags.None) where T : new() - where T2 : new() - where T3 : new() - { - return CreateTables (createFlags, typeof (T), typeof (T2), typeof (T3)); - } + where T2 : new() + where T3 : new() + { + return CreateTables (createFlags, typeof (T), typeof (T2), typeof (T3)); + } /// /// Executes a "create table if not exists" on the database for each type. It also @@ -1033,12 +1033,12 @@ public CreateTablesResult CreateTables< # endif T4> (CreateFlags createFlags = CreateFlags.None) where T : new() - where T2 : new() - where T3 : new() - where T4 : new() - { - return CreateTables (createFlags, typeof (T), typeof (T2), typeof (T3), typeof (T4)); - } + where T2 : new() + where T3 : new() + where T4 : new() + { + return CreateTables (createFlags, typeof (T), typeof (T2), typeof (T3), typeof (T4)); + } /// /// Executes a "create table if not exists" on the database for each type. It also @@ -1074,13 +1074,13 @@ public CreateTablesResult CreateTables< # endif T5> (CreateFlags createFlags = CreateFlags.None) where T : new() - where T2 : new() - where T3 : new() - where T4 : new() - where T5 : new() - { - return CreateTables (createFlags, typeof (T), typeof (T2), typeof (T3), typeof (T4), typeof (T5)); - } + where T2 : new() + where T3 : new() + where T4 : new() + where T5 : new() + { + return CreateTables (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 @@ -1095,4342 +1095,4342 @@ public CreateTablesResult CreateTables< [RequiresUnreferencedCode("This method requires 'DynamicallyAccessedMemberTypes.All' on each input 'Type' instance.")] #endif public CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None, params Type[] types) - { - var result = new CreateTablesResult (); - foreach (Type type in types) { - var aResult = CreateTable (type, createFlags); - result.Results[type] = aResult; - } - return result; - } - - /// - /// 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 int CreateIndex (string indexName, string tableName, string[] columnNames, bool unique = false) - { - const string sqlFormat = "create {2} index if not exists \"{3}\" on \"{0}\"(\"{1}\")"; - var sql = String.Format (sqlFormat, tableName, string.Join ("\", \"", columnNames), unique ? "unique" : "", indexName); - return Execute (sql); - } - - /// - /// 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 int CreateIndex (string indexName, string tableName, string columnName, bool unique = false) - { - return CreateIndex (indexName, tableName, new string[] { columnName }, unique); - } - - /// - /// 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 int CreateIndex (string tableName, string columnName, bool unique = false) - { - return CreateIndex (tableName + "_" + columnName, 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 int CreateIndex (string tableName, string[] columnNames, bool unique = false) - { - return CreateIndex (tableName + "_" + string.Join ("_", columnNames), 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 int CreateIndex< -#if NET8_0_OR_GREATER - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] -#endif - T> (Expression> property, bool unique = false) - { - MemberExpression mx; - if (property.Body.NodeType == ExpressionType.Convert) { - mx = ((UnaryExpression)property.Body).Operand as MemberExpression; - } - else { - mx = (property.Body as MemberExpression); - } - var propertyInfo = mx.Member as PropertyInfo; - if (propertyInfo == null) { - throw new ArgumentException ("The lambda expression 'property' should point to a valid Property"); - } - - var propName = propertyInfo.Name; - - var map = GetMapping (); - var colName = map.FindColumnWithPropertyName (propName).Name; - - return CreateIndex (map.TableName, colName, unique); - } - - [Preserve (AllMembers = true)] - public class ColumnInfo - { - // public int cid { get; set; } - - [Column ("name")] - public string Name { get; set; } - - // [Column ("type")] - // public string ColumnType { get; set; } - - public int notnull { get; set; } - - // public string dflt_value { get; set; } - - // public int pk { get; set; } - - public override string ToString () - { - return Name; - } - } - - /// - /// Query the built-in sqlite table_info table for a specific tables columns. - /// - /// The columns contains in the table. - /// Table name. - public List GetTableInfo (string tableName) - { - var query = "pragma table_info(\"" + tableName + "\")"; - return Query (query); - } - - void MigrateTable (TableMapping map, List existingCols) - { - var toBeAdded = new List (); - - foreach (var p in map.Columns) { - var found = false; - foreach (var c in existingCols) { - found = (string.Compare (p.Name, c.Name, StringComparison.OrdinalIgnoreCase) == 0); - if (found) - break; - } - if (!found) { - toBeAdded.Add (p); - } - } - - foreach (var p in toBeAdded) { - var addCol = "alter table \"" + map.TableName + "\" add column " + Orm.SqlDecl (p, StoreDateTimeAsTicks, StoreTimeSpanAsTicks); - Execute (addCol); - } - } - - /// - /// Creates a new SQLiteCommand. Can be overridden to provide a sub-class. - /// - /// - protected virtual SQLiteCommand NewCommand () - { - return new SQLiteCommand (this); - } - - /// - /// Creates a new SQLiteCommand given the command text with arguments. Place a '?' - /// in the command text for each of the arguments. - /// - /// - /// The fully escaped SQL. - /// - /// - /// Arguments to substitute for the occurences of '?' in the command text. - /// - /// - /// A - /// - public SQLiteCommand CreateCommand (string cmdText, params object[] ps) - { - if (!_open) - throw SQLiteException.New (SQLite3.Result.Error, "Cannot create commands from unopened database"); - - var cmd = NewCommand (); - cmd.CommandText = cmdText; - foreach (var o in ps) { - cmd.Bind (o); - } - return cmd; - } - - /// - /// Creates a new SQLiteCommand given the command text with named arguments. Place a "[@:$]VVV" - /// in the command text for each of the arguments. VVV represents an alphanumeric identifier. - /// For example, @name :name and $name can all be used in the query. - /// - /// - /// The fully escaped SQL. - /// - /// - /// Arguments to substitute for the occurences of "[@:$]VVV" in the command text. - /// - /// - /// A - /// - public SQLiteCommand CreateCommand (string cmdText, Dictionary args) - { - if (!_open) - throw SQLiteException.New (SQLite3.Result.Error, "Cannot create commands from unopened database"); - - SQLiteCommand cmd = NewCommand (); - cmd.CommandText = cmdText; - foreach (var kv in args) { - cmd.Bind (kv.Key, kv.Value); - } - return cmd; - } - - /// - /// 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 int Execute (string query, params object[] args) - { - var cmd = CreateCommand (query, args); - - if (TimeExecution) { - if (_sw == null) { - _sw = new Stopwatch (); - } - _sw.Reset (); - _sw.Start (); - } - - var r = cmd.ExecuteNonQuery (); - - if (TimeExecution) { - _sw.Stop (); - _elapsedMilliseconds += _sw.ElapsedMilliseconds; - Tracer?.Invoke (string.Format ("Finished in {0} ms ({1:0.0} s total)", _sw.ElapsedMilliseconds, _elapsedMilliseconds / 1000.0)); - } - - return r; - } - - /// - /// 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 T ExecuteScalar (string query, params object[] args) - { - var cmd = CreateCommand (query, args); - - if (TimeExecution) { - if (_sw == null) { - _sw = new Stopwatch (); - } - _sw.Reset (); - _sw.Start (); - } - - var r = cmd.ExecuteScalar (); - - if (TimeExecution) { - _sw.Stop (); - _elapsedMilliseconds += _sw.ElapsedMilliseconds; - Tracer?.Invoke (string.Format ("Finished in {0} ms ({1:0.0} s total)", _sw.ElapsedMilliseconds, _elapsedMilliseconds / 1000.0)); - } - - return r; - } - - /// - /// 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. - /// - public List Query< -#if NET8_0_OR_GREATER - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] -#endif - T> (string query, params object[] args) where T : new() - { - var cmd = CreateCommand (query, args); - return cmd.ExecuteQuery (); - } - - /// - /// 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. - /// - /// - /// An enumerable with one result for the first column of each row returned by the query. - /// - public List QueryScalars (string query, params object[] args) - { - var cmd = CreateCommand (query, args); - return cmd.ExecuteQueryScalars ().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 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 (retrieved by calling GetEnumerator() on the result of this method) - /// will call sqlite3_step on each call to MoveNext, so the database - /// connection must remain open for the lifetime of the enumerator. - /// - public IEnumerable DeferredQuery< -#if NET8_0_OR_GREATER - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] -#endif - T> (string query, params object[] args) where T : new() - { - var cmd = CreateCommand (query, args); - return cmd.ExecuteDeferredQuery (); - } - - /// - /// 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 List Query (TableMapping map, string query, params object[] args) - { - var cmd = CreateCommand (query, args); - return cmd.ExecuteQuery (map); - } - - /// - /// 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 (retrieved by calling GetEnumerator() on the result of this method) - /// will call sqlite3_step on each call to MoveNext, so the database - /// connection must remain open for the lifetime of the enumerator. - /// - public IEnumerable DeferredQuery (TableMapping map, string query, params object[] args) - { - var cmd = CreateCommand (query, args); - return cmd.ExecuteDeferredQuery (map); - } - - /// - /// 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 TableQuery Table< -#if NET8_0_OR_GREATER - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] -#endif - T> () where T : new() - { - return new TableQuery (this); - } - - /// - /// 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 T Get< -#if NET8_0_OR_GREATER - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] -#endif - T> (object pk) where T : new() - { - var map = GetMapping (typeof (T)); - return Query (map.GetByPrimaryKeySql, pk).First (); - } - - /// - /// 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 object Get (object pk, TableMapping map) - { - return Query (map, map.GetByPrimaryKeySql, pk).First (); - } - - /// - /// 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 T Get< -#if NET8_0_OR_GREATER - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] -#endif - T> (Expression> predicate) where T : new() - { - return Table ().Where (predicate).First (); - } - - /// - /// 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 T Find< -#if NET8_0_OR_GREATER - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] -#endif - T> (object pk) where T : new() - { - var map = GetMapping (typeof (T)); - return Query (map.GetByPrimaryKeySql, pk).FirstOrDefault (); - } - - /// - /// 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 object Find (object pk, TableMapping map) - { - return Query (map, map.GetByPrimaryKeySql, pk).FirstOrDefault (); - } - - /// - /// 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 T Find< -#if NET8_0_OR_GREATER - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] -#endif - T> (Expression> predicate) where T : new() - { - return Table ().Where (predicate).FirstOrDefault (); - } - - /// - /// 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 T FindWithQuery< -#if NET8_0_OR_GREATER - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] -#endif - T> (string query, params object[] args) where T : new() - { - return Query (query, args).FirstOrDefault (); - } - - /// - /// 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 object FindWithQuery (TableMapping map, string query, params object[] args) - { - return Query (map, query, args).FirstOrDefault (); - } - - /// - /// Whether has been called and the database is waiting for a . - /// - public bool IsInTransaction { - get { return _transactionDepth > 0; } - } - - /// - /// Begins a new transaction. Call to end the transaction. - /// - /// Throws if a transaction has already begun. - public void BeginTransaction () - { - // The BEGIN command only works if the transaction stack is empty, - // or in other words if there are no pending transactions. - // If the transaction stack is not empty when the BEGIN command is invoked, - // then the command fails with an error. - // Rather than crash with an error, we will just ignore calls to BeginTransaction - // that would result in an error. - if (Interlocked.CompareExchange (ref _transactionDepth, 1, 0) == 0) { - try { - Execute ("begin transaction"); - } - catch (Exception ex) { - var sqlExp = ex as SQLiteException; - if (sqlExp != null) { - // It is recommended that applications respond to the errors listed below - // by explicitly issuing a ROLLBACK command. - // TODO: This rollback failsafe should be localized to all throw sites. - switch (sqlExp.Result) { - case SQLite3.Result.IOError: - case SQLite3.Result.Full: - case SQLite3.Result.Busy: - case SQLite3.Result.NoMem: - case SQLite3.Result.Interrupt: - RollbackTo (null, true); - break; - } - } - else { - // Call decrement and not VolatileWrite in case we've already - // created a transaction point in SaveTransactionPoint since the catch. - Interlocked.Decrement (ref _transactionDepth); - } - - throw; - } - } - else { - // Calling BeginTransaction on an already open transaction is invalid - throw new InvalidOperationException ("Cannot begin a transaction while already in a transaction."); - } - } - - /// - /// Creates a savepoint in the database at the current point in the transaction timeline. - /// Begins a new transaction if one is not in progress. - /// - /// Call to undo transactions since the returned savepoint. - /// Call to commit transactions after the savepoint returned here. - /// Call to end the transaction, committing all changes. - /// - /// A string naming the savepoint. - public string SaveTransactionPoint () - { - int depth = Interlocked.Increment (ref _transactionDepth) - 1; - string retVal = "S" + _rand.Next (short.MaxValue) + "D" + depth; - - try { - Execute ("savepoint " + retVal); - } - catch (Exception ex) { - var sqlExp = ex as SQLiteException; - if (sqlExp != null) { - // It is recommended that applications respond to the errors listed below - // by explicitly issuing a ROLLBACK command. - // TODO: This rollback failsafe should be localized to all throw sites. - switch (sqlExp.Result) { - case SQLite3.Result.IOError: - case SQLite3.Result.Full: - case SQLite3.Result.Busy: - case SQLite3.Result.NoMem: - case SQLite3.Result.Interrupt: - RollbackTo (null, true); - break; - } - } - else { - Interlocked.Decrement (ref _transactionDepth); - } - - throw; - } - - return retVal; - } - - /// - /// Rolls back the transaction that was begun by or . - /// - public void Rollback () - { - RollbackTo (null, false); - } - - /// - /// Rolls back the savepoint created by or SaveTransactionPoint. - /// - /// The name of the savepoint to roll back to, as returned by . If savepoint is null or empty, this method is equivalent to a call to - public void RollbackTo (string savepoint) - { - RollbackTo (savepoint, false); - } - - /// - /// Rolls back the transaction that was begun by . - /// - /// The name of the savepoint to roll back to, as returned by . If savepoint is null or empty, this method is equivalent to a call to - /// true to avoid throwing exceptions, false otherwise - void RollbackTo (string savepoint, bool noThrow) - { - // Rolling back without a TO clause rolls backs all transactions - // and leaves the transaction stack empty. - try { - if (String.IsNullOrEmpty (savepoint)) { - if (Interlocked.Exchange (ref _transactionDepth, 0) > 0) { - Execute ("rollback"); - } - } - else { - DoSavePointExecute (savepoint, "rollback to "); - } - } - catch (SQLiteException) { - if (!noThrow) - throw; - - } - // No need to rollback if there are no transactions open. - } - - /// - /// Releases a savepoint returned from . Releasing a savepoint - /// makes changes since that savepoint permanent if the savepoint began the transaction, - /// or otherwise the changes are permanent pending a call to . - /// - /// The RELEASE command is like a COMMIT for a SAVEPOINT. - /// - /// The name of the savepoint to release. The string should be the result of a call to - public void Release (string savepoint) - { - try { - DoSavePointExecute (savepoint, "release "); - } - catch (SQLiteException ex) { - if (ex.Result == SQLite3.Result.Busy) { - // Force a rollback since most people don't know this function can fail - // Don't call Rollback() since the _transactionDepth is 0 and it won't try - // Calling rollback makes our _transactionDepth variable correct. - // Writes to the database only happen at depth=0, so this failure will only happen then. - try { - Execute ("rollback"); - } - catch { - // rollback can fail in all sorts of wonderful version-dependent ways. Let's just hope for the best - } - } - throw; - } - } - - void DoSavePointExecute (string savepoint, string cmd) - { - // Validate the savepoint - int firstLen = savepoint.IndexOf ('D'); - if (firstLen >= 2 && savepoint.Length > firstLen + 1) { - int depth; - if (Int32.TryParse (savepoint.Substring (firstLen + 1), out depth)) { - // TODO: Mild race here, but inescapable without locking almost everywhere. - if (0 <= depth && depth < _transactionDepth) { -#if NETFX_CORE || USE_SQLITEPCL_RAW || NETCORE - Volatile.Write (ref _transactionDepth, depth); -#elif SILVERLIGHT - _transactionDepth = depth; -#else - Thread.VolatileWrite (ref _transactionDepth, depth); -#endif - Execute (cmd + savepoint); - return; - } - } - } - - throw new ArgumentException ("savePoint is not valid, and should be the result of a call to SaveTransactionPoint.", "savePoint"); - } - - /// - /// Commits the transaction that was begun by . - /// - public void Commit () - { - if (Interlocked.Exchange (ref _transactionDepth, 0) != 0) { - try { - Execute ("commit"); - } - catch { - // Force a rollback since most people don't know this function can fail - // Don't call Rollback() since the _transactionDepth is 0 and it won't try - // Calling rollback makes our _transactionDepth variable correct. - try { - Execute ("rollback"); - } - catch { - // rollback can fail in all sorts of wonderful version-dependent ways. Let's just hope for the best - } - throw; - } - } - // Do nothing on a commit with no open transaction - } - - /// - /// 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 void RunInTransaction (Action action) - { - try { - var savePoint = SaveTransactionPoint (); - action (); - Release (savePoint); - } - catch (Exception) { - Rollback (); - throw; - } - } + { + var result = new CreateTablesResult (); + foreach (Type type in types) { + var aResult = CreateTable (type, createFlags); + result.Results[type] = aResult; + } + return result; + } /// - /// Inserts all specified objects. + /// Creates an index for the specified table and columns. /// - /// - /// 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. - /// -#if NET8_0_OR_GREATER - [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] -#endif - public int InsertAll (System.Collections.IEnumerable objects, bool runInTransaction = true) - { - var c = 0; - if (runInTransaction) { - RunInTransaction (() => { - foreach (var r in objects) { - c += Insert (r); - } - }); - } - else { - foreach (var r in objects) { - c += Insert (r); - } - } - return c; - } + /// 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 int CreateIndex (string indexName, string tableName, string[] columnNames, bool unique = false) + { + const string sqlFormat = "create {2} index if not exists \"{3}\" on \"{0}\"(\"{1}\")"; + var sql = String.Format (sqlFormat, tableName, string.Join ("\", \"", columnNames), unique ? "unique" : "", indexName); + return Execute (sql); + } /// - /// Inserts all specified objects. + /// Creates an index for the specified table and column. /// - /// - /// An of the objects to insert. + /// 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 int CreateIndex (string indexName, string tableName, string columnName, bool unique = false) + { + return CreateIndex (indexName, tableName, new string[] { columnName }, unique); + } + + /// + /// 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 int CreateIndex (string tableName, string columnName, bool unique = false) + { + return CreateIndex (tableName + "_" + columnName, 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 int CreateIndex (string tableName, string[] columnNames, bool unique = false) + { + return CreateIndex (tableName + "_" + string.Join ("_", columnNames), 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 int CreateIndex< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (Expression> property, bool unique = false) + { + MemberExpression mx; + if (property.Body.NodeType == ExpressionType.Convert) { + mx = ((UnaryExpression)property.Body).Operand as MemberExpression; + } + else { + mx = (property.Body as MemberExpression); + } + var propertyInfo = mx.Member as PropertyInfo; + if (propertyInfo == null) { + throw new ArgumentException ("The lambda expression 'property' should point to a valid Property"); + } + + var propName = propertyInfo.Name; + + var map = GetMapping (); + var colName = map.FindColumnWithPropertyName (propName).Name; + + return CreateIndex (map.TableName, colName, unique); + } + + [Preserve (AllMembers = true)] + public class ColumnInfo + { + // public int cid { get; set; } + + [Column ("name")] + public string Name { get; set; } + + // [Column ("type")] + // public string ColumnType { get; set; } + + public int notnull { get; set; } + + // public string dflt_value { get; set; } + + // public int pk { get; set; } + + public override string ToString () + { + return Name; + } + } + + /// + /// Query the built-in sqlite table_info table for a specific tables columns. + /// + /// The columns contains in the table. + /// Table name. + public List GetTableInfo (string tableName) + { + var query = "pragma table_info(\"" + tableName + "\")"; + return Query (query); + } + + void MigrateTable (TableMapping map, List existingCols) + { + var toBeAdded = new List (); + + foreach (var p in map.Columns) { + var found = false; + foreach (var c in existingCols) { + found = (string.Compare (p.Name, c.Name, StringComparison.OrdinalIgnoreCase) == 0); + if (found) + break; + } + if (!found) { + toBeAdded.Add (p); + } + } + + foreach (var p in toBeAdded) { + var addCol = "alter table \"" + map.TableName + "\" add column " + Orm.SqlDecl (p, StoreDateTimeAsTicks, StoreTimeSpanAsTicks); + Execute (addCol); + } + } + + /// + /// Creates a new SQLiteCommand. Can be overridden to provide a sub-class. + /// + /// + protected virtual SQLiteCommand NewCommand () + { + return new SQLiteCommand (this); + } + + /// + /// Creates a new SQLiteCommand given the command text with arguments. Place a '?' + /// in the command text for each of the arguments. + /// + /// + /// The fully escaped SQL. /// - /// - /// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... + /// + /// Arguments to substitute for the occurences of '?' in the command text. /// - /// - /// A boolean indicating if the inserts should be wrapped in a transaction. + /// + /// A + /// + public SQLiteCommand CreateCommand (string cmdText, params object[] ps) + { + if (!_open) + throw SQLiteException.New (SQLite3.Result.Error, "Cannot create commands from unopened database"); + + var cmd = NewCommand (); + cmd.CommandText = cmdText; + foreach (var o in ps) { + cmd.Bind (o); + } + return cmd; + } + + /// + /// Creates a new SQLiteCommand given the command text with named arguments. Place a "[@:$]VVV" + /// in the command text for each of the arguments. VVV represents an alphanumeric identifier. + /// For example, @name :name and $name can all be used in the query. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of "[@:$]VVV" in the command text. /// /// - /// The number of rows added to the table. + /// A /// -#if NET8_0_OR_GREATER - [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] -#endif - public int InsertAll (System.Collections.IEnumerable objects, string extra, bool runInTransaction = true) - { - var c = 0; - if (runInTransaction) { - RunInTransaction (() => { - foreach (var r in objects) { - c += Insert (r, extra); - } - }); - } - else { - foreach (var r in objects) { - c += Insert (r, extra); - } - } - return c; - } - - /// - /// 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 int InsertAll ( - System.Collections.IEnumerable objects, + public SQLiteCommand CreateCommand (string cmdText, Dictionary args) + { + if (!_open) + throw SQLiteException.New (SQLite3.Result.Error, "Cannot create commands from unopened database"); + + SQLiteCommand cmd = NewCommand (); + cmd.CommandText = cmdText; + foreach (var kv in args) { + cmd.Bind (kv.Key, kv.Value); + } + return cmd; + } + + /// + /// 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 int Execute (string query, params object[] args) + { + var cmd = CreateCommand (query, args); + + if (TimeExecution) { + if (_sw == null) { + _sw = new Stopwatch (); + } + _sw.Reset (); + _sw.Start (); + } + + var r = cmd.ExecuteNonQuery (); + + if (TimeExecution) { + _sw.Stop (); + _elapsedMilliseconds += _sw.ElapsedMilliseconds; + Tracer?.Invoke (string.Format ("Finished in {0} ms ({1:0.0} s total)", _sw.ElapsedMilliseconds, _elapsedMilliseconds / 1000.0)); + } + + return r; + } + + /// + /// 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 T ExecuteScalar (string query, params object[] args) + { + var cmd = CreateCommand (query, args); + + if (TimeExecution) { + if (_sw == null) { + _sw = new Stopwatch (); + } + _sw.Reset (); + _sw.Start (); + } + + var r = cmd.ExecuteScalar (); + + if (TimeExecution) { + _sw.Stop (); + _elapsedMilliseconds += _sw.ElapsedMilliseconds; + Tracer?.Invoke (string.Format ("Finished in {0} ms ({1:0.0} s total)", _sw.ElapsedMilliseconds, _elapsedMilliseconds / 1000.0)); + } + + return r; + } + + /// + /// 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. + /// + public List Query< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif - Type objType, - bool runInTransaction = true) - { - var c = 0; - if (runInTransaction) { - RunInTransaction (() => { - foreach (var r in objects) { - c += Insert (r, objType); - } - }); - } - else { - foreach (var r in objects) { - c += Insert (r, objType); - } - } - return c; - } + T> (string query, params object[] args) where T : new() + { + var cmd = CreateCommand (query, args); + return cmd.ExecuteQuery (); + } /// - /// 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. + /// 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 object to insert. + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. /// /// - /// The number of rows added to the table. + /// An enumerable with one result for the first column of each row returned by the query. /// -#if NET8_0_OR_GREATER - [RequiresUnreferencedCode("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] -#endif - public int Insert (object obj) - { - if (obj == null) { - return 0; - } - return Insert (obj, "", Orm.GetType (obj)); - } + public List QueryScalars (string query, params object[] args) + { + var cmd = CreateCommand (query, args); + return cmd.ExecuteQueryScalars ().ToList (); + } /// - /// 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. + /// 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 object to insert. + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. /// /// - /// The number of rows modified. + /// An enumerable with one result for each row returned by the query. + /// The enumerator (retrieved by calling GetEnumerator() on the result of this method) + /// will call sqlite3_step on each call to MoveNext, so the database + /// connection must remain open for the lifetime of the enumerator. /// + public IEnumerable DeferredQuery< #if NET8_0_OR_GREATER - [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif - public int InsertOrReplace (object obj) - { - if (obj == null) { - return 0; - } - return Insert (obj, "OR REPLACE", Orm.GetType (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 int Insert ( - object obj, + T> (string query, params object[] args) where T : new() + { + var cmd = CreateCommand (query, args); + return cmd.ExecuteDeferredQuery (); + } + + /// + /// 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 List Query (TableMapping map, string query, params object[] args) + { + var cmd = CreateCommand (query, args); + return cmd.ExecuteQuery (map); + } + + /// + /// 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 (retrieved by calling GetEnumerator() on the result of this method) + /// will call sqlite3_step on each call to MoveNext, so the database + /// connection must remain open for the lifetime of the enumerator. + /// + public IEnumerable DeferredQuery (TableMapping map, string query, params object[] args) + { + var cmd = CreateCommand (query, args); + return cmd.ExecuteDeferredQuery (map); + } + + /// + /// 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 TableQuery Table< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif - Type objType) - { - return 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. - /// 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 int InsertOrReplace ( - object obj, + T> () where T : new() + { + return new TableQuery (this); + } + + /// + /// 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 T Get< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif - Type objType) - { - return Insert (obj, "OR REPLACE", objType); - } + T> (object pk) where T : new() + { + var map = GetMapping (typeof (T)); + return Query (map.GetByPrimaryKeySql, pk).First (); + } /// - /// 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. + /// 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 object to insert. + /// + /// The primary key. /// - /// - /// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... + /// + /// The TableMapping used to identify the table. /// /// - /// The number of rows added to the table. + /// The object with the given primary key. Throws a not found exception + /// if the object is not found. /// -#if NET8_0_OR_GREATER - [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] -#endif - public int Insert (object obj, string extra) - { - if (obj == null) { - return 0; - } - return Insert (obj, extra, Orm.GetType (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. - /// - /// - /// 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 int Insert ( - object obj, - string extra, + public object Get (object pk, TableMapping map) + { + return Query (map, map.GetByPrimaryKeySql, pk).First (); + } + + /// + /// 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 T Get< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif - Type objType) - { - if (obj == null || objType == null) { - return 0; - } - - var map = GetMapping (objType); - - if (map.PK != null && map.PK.IsAutoGuid) { - if (map.PK.GetValue (obj).Equals (Guid.Empty)) { - map.PK.SetValue (obj, Guid.NewGuid ()); - } - } - - var replacing = string.Compare (extra, "OR REPLACE", StringComparison.OrdinalIgnoreCase) == 0; - - var cols = replacing ? map.InsertOrReplaceColumns : map.InsertColumns; - var vals = new object[cols.Length]; - for (var i = 0; i < vals.Length; i++) { - vals[i] = cols[i].GetValue (obj); - } - - var insertCmd = GetInsertCommand (map, extra); - int count; - - lock (insertCmd) { - // We lock here to protect the prepared statement returned via GetInsertCommand. - // A SQLite prepared statement can be bound for only one operation at a time. - try { - count = insertCmd.ExecuteNonQuery (vals); - } - catch (SQLiteException ex) { - if (SQLite3.ExtendedErrCode (this.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { - throw NotNullConstraintViolationException.New (ex.Result, ex.Message, map, obj); - } - throw; - } - - if (map.HasAutoIncPK) { - var id = SQLite3.LastInsertRowid (Handle); - map.SetAutoIncPK (obj, id); - } - } - if (count > 0) - OnTableChanged (map, NotifyTableChangedAction.Insert); - - return count; - } - - readonly Dictionary, PreparedSqlLiteInsertCommand> _insertCommandMap = new Dictionary, PreparedSqlLiteInsertCommand> (); - - PreparedSqlLiteInsertCommand GetInsertCommand (TableMapping map, string extra) - { - PreparedSqlLiteInsertCommand prepCmd; - - var key = Tuple.Create (map.MappedType.FullName, extra); - - lock (_insertCommandMap) { - if (_insertCommandMap.TryGetValue (key, out prepCmd)) { - return prepCmd; - } - } - - prepCmd = CreateInsertCommand (map, extra); - - lock (_insertCommandMap) { - if (_insertCommandMap.TryGetValue (key, out var existing)) { - prepCmd.Dispose (); - return existing; - } - - _insertCommandMap.Add (key, prepCmd); - } - - return prepCmd; - } - - PreparedSqlLiteInsertCommand CreateInsertCommand (TableMapping map, string extra) - { - var cols = map.InsertColumns; - string insertSql; - if (cols.Length == 0 && map.Columns.Length == 1 && map.Columns[0].IsAutoInc) { - insertSql = string.Format ("insert {1} into \"{0}\" default values", map.TableName, extra); - } - else { - var replacing = string.Compare (extra, "OR REPLACE", StringComparison.OrdinalIgnoreCase) == 0; - - if (replacing) { - cols = map.InsertOrReplaceColumns; - } - - insertSql = string.Format ("insert {3} into \"{0}\"({1}) values ({2})", map.TableName, - string.Join (",", (from c in cols - select "\"" + c.Name + "\"").ToArray ()), - string.Join (",", (from c in cols - select "?").ToArray ()), extra); - - } - - var insertCommand = new PreparedSqlLiteInsertCommand (this, insertSql); - return insertCommand; - } + T> (Expression> predicate) where T : new() + { + return Table ().Where (predicate).First (); + } /// - /// 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. + /// 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 object to update. It must have a primary key designated using the PrimaryKeyAttribute. + /// + /// The primary key. /// /// - /// The number of rows updated. + /// The object with the given primary key or null + /// if the object is not found. /// -#if NET8_0_OR_GREATER - [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] -#endif - public int Update (object obj) - { - if (obj == null) { - return 0; - } - return Update (obj, Orm.GetType (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 int Update ( - object obj, + public T Find< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif - Type objType) - { - int rowsAffected = 0; - if (obj == null || objType == null) { - return 0; - } - - var map = GetMapping (objType); - - var pk = map.PK; - - if (pk == null) { - throw new NotSupportedException ("Cannot update " + map.TableName + ": it has no PK"); - } - - var cols = from p in map.Columns - where p != pk - select p; - var vals = from c in cols - select c.GetValue (obj); - var ps = new List (vals); - if (ps.Count == 0) { - // There is a PK but no accompanying data, - // so reset the PK to make the UPDATE work. - cols = map.Columns; - vals = from c in cols - select c.GetValue (obj); - ps = new List (vals); - } - ps.Add (pk.GetValue (obj)); - var q = string.Format ("update \"{0}\" set {1} where \"{2}\" = ? ", map.TableName, string.Join (",", (from c in cols - select "\"" + c.Name + "\" = ? ").ToArray ()), pk.Name); - - try { - rowsAffected = Execute (q, ps.ToArray ()); - } - catch (SQLiteException ex) { - - if (ex.Result == SQLite3.Result.Constraint && SQLite3.ExtendedErrCode (this.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { - throw NotNullConstraintViolationException.New (ex, map, obj); - } - - throw; - } - - if (rowsAffected > 0) - OnTableChanged (map, NotifyTableChangedAction.Update); - - return rowsAffected; - } + T> (object pk) where T : new() + { + var map = GetMapping (typeof (T)); + return Query (map.GetByPrimaryKeySql, pk).FirstOrDefault (); + } /// - /// Updates all specified objects. + /// 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). /// - /// - /// An of the objects to insert. + /// + /// The primary key. /// - /// - /// A boolean indicating if the inserts should be wrapped in a transaction + /// + /// The TableMapping used to identify the table. /// /// - /// The number of rows modified. + /// The object with the given primary key or null + /// if the object is not found. /// -#if NET8_0_OR_GREATER - [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] -#endif - public int UpdateAll (System.Collections.IEnumerable objects, bool runInTransaction = true) - { - var c = 0; - if (runInTransaction) { - RunInTransaction (() => { - foreach (var r in objects) { - c += Update (r); - } - }); - } - else { - foreach (var r in objects) { - c += Update (r); - } - } - return c; - } + public object Find (object pk, TableMapping map) + { + return Query (map, map.GetByPrimaryKeySql, pk).FirstOrDefault (); + } /// - /// Deletes the given object from the database using its primary key. + /// Attempts to retrieve the first object that matches the predicate from the table + /// associated with the specified type. /// - /// - /// The object to delete. It must have a primary key designated using the PrimaryKeyAttribute. + /// + /// A predicate for which object to find. /// /// - /// The number of rows deleted. + /// The object that matches the given predicate or null + /// if the object is not found. /// -#if NET8_0_OR_GREATER - [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'objectToDelete'.")] -#endif - public int Delete (object objectToDelete) - { - var map = GetMapping (Orm.GetType (objectToDelete)); - var pk = map.PK; - if (pk == null) { - throw new NotSupportedException ("Cannot delete " + map.TableName + ": it has no PK"); - } - var q = string.Format ("delete from \"{0}\" where \"{1}\" = ?", map.TableName, pk.Name); - var count = Execute (q, pk.GetValue (objectToDelete)); - if (count > 0) - OnTableChanged (map, NotifyTableChangedAction.Delete); - return count; - } - - /// - /// 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 int Delete< + public T Find< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif - T> (object primaryKey) - { - return Delete (primaryKey, GetMapping (typeof (T))); - } - - /// - /// 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 int Delete (object primaryKey, TableMapping map) - { - var pk = map.PK; - if (pk == null) { - throw new NotSupportedException ("Cannot delete " + map.TableName + ": it has no PK"); - } - var q = string.Format ("delete from \"{0}\" where \"{1}\" = ?", map.TableName, pk.Name); - var count = Execute (q, primaryKey); - if (count > 0) - OnTableChanged (map, NotifyTableChangedAction.Delete); - return count; - } - - /// - /// 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 int DeleteAll< + T> (Expression> predicate) where T : new() + { + return Table ().Where (predicate).FirstOrDefault (); + } + + /// + /// 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 T FindWithQuery< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif - T> () - { - var map = GetMapping (typeof (T)); - return DeleteAll (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 TableMapping used to identify the table. - /// - /// - /// The number of objects deleted. - /// - public int DeleteAll (TableMapping map) - { - var query = string.Format ("delete from \"{0}\"", map.TableName); - var count = Execute (query); - if (count > 0) - OnTableChanged (map, NotifyTableChangedAction.Delete); - return count; - } - - /// - /// Backup the entire database to the specified path. - /// - /// Path to backup file. - /// The name of the database to backup (usually "main"). - public void Backup (string destinationDatabasePath, string databaseName = "main") - { - // Open the destination - var r = SQLite3.Open (destinationDatabasePath, out var destHandle); - if (r != SQLite3.Result.OK) { - throw SQLiteException.New (r, "Failed to open destination database"); - } - - // Init the backup - var backup = SQLite3.BackupInit (destHandle, databaseName, Handle, databaseName); - if (backup == NullBackupHandle) { - SQLite3.Close (destHandle); - throw new Exception ("Failed to create backup"); - } - - // Perform it - SQLite3.BackupStep (backup, -1); - SQLite3.BackupFinish (backup); - - // Check for errors - r = SQLite3.GetResult (destHandle); - string msg = ""; - if (r != SQLite3.Result.OK) { - msg = SQLite3.GetErrmsg (destHandle); - } - - // Close everything and report errors - SQLite3.Close (destHandle); - if (r != SQLite3.Result.OK) { - throw SQLiteException.New (r, msg); - } - } - - ~SQLiteConnection () - { - Dispose (false); - } - - public void Dispose () - { - Dispose (true); - GC.SuppressFinalize (this); - } - - public void Close () - { - Dispose (true); - } - - protected virtual void Dispose (bool disposing) - { - var useClose2 = LibVersionNumber >= 3007014; - - if (_open && Handle != NullHandle) { - try { - if (disposing) { - lock (_insertCommandMap) { - foreach (var sqlInsertCommand in _insertCommandMap.Values) { - sqlInsertCommand.Dispose (); - } - _insertCommandMap.Clear (); - } - - var r = useClose2 ? SQLite3.Close2 (Handle) : SQLite3.Close (Handle); - if (r != SQLite3.Result.OK) { - string msg = SQLite3.GetErrmsg (Handle); - throw SQLiteException.New (r, msg); - } - } - else { - var r = useClose2 ? SQLite3.Close2 (Handle) : SQLite3.Close (Handle); - } - } - finally { - Handle = NullHandle; - _open = false; - } - } - } - - void OnTableChanged (TableMapping table, NotifyTableChangedAction action) - { - var ev = TableChanged; - if (ev != null) - ev (this, new NotifyTableChangedEventArgs (table, action)); - } - - public event EventHandler TableChanged; - } - - public class NotifyTableChangedEventArgs : EventArgs - { - public TableMapping Table { get; private set; } - public NotifyTableChangedAction Action { get; private set; } - - public NotifyTableChangedEventArgs (TableMapping table, NotifyTableChangedAction action) - { - Table = table; - Action = action; - } - } - - public enum NotifyTableChangedAction - { - Insert, - Update, - Delete, - } - - /// - /// Represents a parsed connection string. - /// - public class SQLiteConnectionString - { - const string DateTimeSqliteDefaultFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff"; - - public string UniqueKey { get; } - public string DatabasePath { get; } - public bool StoreDateTimeAsTicks { get; } - public bool StoreTimeSpanAsTicks { get; } - public string DateTimeStringFormat { get; } - public System.Globalization.DateTimeStyles DateTimeStyle { get; } - public object Key { get; } - public SQLiteOpenFlags OpenFlags { get; } - public Action PreKeyAction { get; } - public Action PostKeyAction { get; } - public string VfsName { get; } - -#if NETFX_CORE - static readonly string MetroStyleDataPath = Windows.Storage.ApplicationData.Current.LocalFolder.Path; - - public static readonly string[] InMemoryDbPaths = new[] - { - ":memory:", - "file::memory:" - }; + T> (string query, params object[] args) where T : new() + { + return Query (query, args).FirstOrDefault (); + } - public static bool IsInMemoryPath(string databasePath) - { - return InMemoryDbPaths.Any(i => i.Equals(databasePath, StringComparison.OrdinalIgnoreCase)); - } + /// + /// 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 object FindWithQuery (TableMapping map, string query, params object[] args) + { + return Query (map, query, args).FirstOrDefault (); + } + + /// + /// Whether has been called and the database is waiting for a . + /// + public bool IsInTransaction { + get { return _transactionDepth > 0; } + } + + /// + /// Begins a new transaction. Call to end the transaction. + /// + /// Throws if a transaction has already begun. + public void BeginTransaction () + { + // The BEGIN command only works if the transaction stack is empty, + // or in other words if there are no pending transactions. + // If the transaction stack is not empty when the BEGIN command is invoked, + // then the command fails with an error. + // Rather than crash with an error, we will just ignore calls to BeginTransaction + // that would result in an error. + if (Interlocked.CompareExchange (ref _transactionDepth, 1, 0) == 0) { + try { + Execute ("begin transaction"); + } + catch (Exception ex) { + var sqlExp = ex as SQLiteException; + if (sqlExp != null) { + // It is recommended that applications respond to the errors listed below + // by explicitly issuing a ROLLBACK command. + // TODO: This rollback failsafe should be localized to all throw sites. + switch (sqlExp.Result) { + case SQLite3.Result.IOError: + case SQLite3.Result.Full: + case SQLite3.Result.Busy: + case SQLite3.Result.NoMem: + case SQLite3.Result.Interrupt: + RollbackTo (null, true); + break; + } + } + else { + // Call decrement and not VolatileWrite in case we've already + // created a transaction point in SaveTransactionPoint since the catch. + Interlocked.Decrement (ref _transactionDepth); + } + + throw; + } + } + else { + // Calling BeginTransaction on an already open transaction is invalid + throw new InvalidOperationException ("Cannot begin a transaction while already in a transaction."); + } + } + + /// + /// Creates a savepoint in the database at the current point in the transaction timeline. + /// Begins a new transaction if one is not in progress. + /// + /// Call to undo transactions since the returned savepoint. + /// Call to commit transactions after the savepoint returned here. + /// Call to end the transaction, committing all changes. + /// + /// A string naming the savepoint. + public string SaveTransactionPoint () + { + int depth = Interlocked.Increment (ref _transactionDepth) - 1; + string retVal = "S" + _rand.Next (short.MaxValue) + "D" + depth; + + try { + Execute ("savepoint " + retVal); + } + catch (Exception ex) { + var sqlExp = ex as SQLiteException; + if (sqlExp != null) { + // It is recommended that applications respond to the errors listed below + // by explicitly issuing a ROLLBACK command. + // TODO: This rollback failsafe should be localized to all throw sites. + switch (sqlExp.Result) { + case SQLite3.Result.IOError: + case SQLite3.Result.Full: + case SQLite3.Result.Busy: + case SQLite3.Result.NoMem: + case SQLite3.Result.Interrupt: + RollbackTo (null, true); + break; + } + } + else { + Interlocked.Decrement (ref _transactionDepth); + } + + throw; + } + + return retVal; + } + + /// + /// Rolls back the transaction that was begun by or . + /// + public void Rollback () + { + RollbackTo (null, false); + } + + /// + /// Rolls back the savepoint created by or SaveTransactionPoint. + /// + /// The name of the savepoint to roll back to, as returned by . If savepoint is null or empty, this method is equivalent to a call to + public void RollbackTo (string savepoint) + { + RollbackTo (savepoint, false); + } + + /// + /// Rolls back the transaction that was begun by . + /// + /// The name of the savepoint to roll back to, as returned by . If savepoint is null or empty, this method is equivalent to a call to + /// true to avoid throwing exceptions, false otherwise + void RollbackTo (string savepoint, bool noThrow) + { + // Rolling back without a TO clause rolls backs all transactions + // and leaves the transaction stack empty. + try { + if (String.IsNullOrEmpty (savepoint)) { + if (Interlocked.Exchange (ref _transactionDepth, 0) > 0) { + Execute ("rollback"); + } + } + else { + DoSavePointExecute (savepoint, "rollback to "); + } + } + catch (SQLiteException) { + if (!noThrow) + throw; + + } + // No need to rollback if there are no transactions open. + } + + /// + /// Releases a savepoint returned from . Releasing a savepoint + /// makes changes since that savepoint permanent if the savepoint began the transaction, + /// or otherwise the changes are permanent pending a call to . + /// + /// The RELEASE command is like a COMMIT for a SAVEPOINT. + /// + /// The name of the savepoint to release. The string should be the result of a call to + public void Release (string savepoint) + { + try { + DoSavePointExecute (savepoint, "release "); + } + catch (SQLiteException ex) { + if (ex.Result == SQLite3.Result.Busy) { + // Force a rollback since most people don't know this function can fail + // Don't call Rollback() since the _transactionDepth is 0 and it won't try + // Calling rollback makes our _transactionDepth variable correct. + // Writes to the database only happen at depth=0, so this failure will only happen then. + try { + Execute ("rollback"); + } + catch { + // rollback can fail in all sorts of wonderful version-dependent ways. Let's just hope for the best + } + } + throw; + } + } + + void DoSavePointExecute (string savepoint, string cmd) + { + // Validate the savepoint + int firstLen = savepoint.IndexOf ('D'); + if (firstLen >= 2 && savepoint.Length > firstLen + 1) { + int depth; + if (Int32.TryParse (savepoint.Substring (firstLen + 1), out depth)) { + // TODO: Mild race here, but inescapable without locking almost everywhere. + if (0 <= depth && depth < _transactionDepth) { +#if NETFX_CORE || USE_SQLITEPCL_RAW || NETCORE + Volatile.Write (ref _transactionDepth, depth); +#elif SILVERLIGHT + _transactionDepth = depth; +#else + Thread.VolatileWrite (ref _transactionDepth, depth); +#endif + Execute (cmd + savepoint); + return; + } + } + } + + throw new ArgumentException ("savePoint is not valid, and should be the result of a call to SaveTransactionPoint.", "savePoint"); + } + + /// + /// Commits the transaction that was begun by . + /// + public void Commit () + { + if (Interlocked.Exchange (ref _transactionDepth, 0) != 0) { + try { + Execute ("commit"); + } + catch { + // Force a rollback since most people don't know this function can fail + // Don't call Rollback() since the _transactionDepth is 0 and it won't try + // Calling rollback makes our _transactionDepth variable correct. + try { + Execute ("rollback"); + } + catch { + // rollback can fail in all sorts of wonderful version-dependent ways. Let's just hope for the best + } + throw; + } + } + // Do nothing on a commit with no open transaction + } + + /// + /// 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 void RunInTransaction (Action action) + { + try { + var savePoint = SaveTransactionPoint (); + action (); + Release (savePoint); + } + catch (Exception) { + Rollback (); + throw; + } + } + + /// + /// 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. + /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] +#endif + public int InsertAll (System.Collections.IEnumerable objects, bool runInTransaction = true) + { + var c = 0; + if (runInTransaction) { + RunInTransaction (() => { + foreach (var r in objects) { + c += Insert (r); + } + }); + } + else { + foreach (var r in objects) { + c += Insert (r); + } + } + return c; + } + + /// + /// 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. + /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] +#endif + public int InsertAll (System.Collections.IEnumerable objects, string extra, bool runInTransaction = true) + { + var c = 0; + if (runInTransaction) { + RunInTransaction (() => { + foreach (var r in objects) { + c += Insert (r, extra); + } + }); + } + else { + foreach (var r in objects) { + c += Insert (r, extra); + } + } + return c; + } + + /// + /// 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 int InsertAll ( + System.Collections.IEnumerable objects, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType, + bool runInTransaction = true) + { + var c = 0; + if (runInTransaction) { + RunInTransaction (() => { + foreach (var r in objects) { + c += Insert (r, objType); + } + }); + } + else { + foreach (var r in objects) { + c += Insert (r, objType); + } + } + return c; + } + + /// + /// 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 number of rows added to the table. + /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif + public int Insert (object obj) + { + if (obj == null) { + return 0; + } + return Insert (obj, "", Orm.GetType (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 number of rows modified. + /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif + public int InsertOrReplace (object obj) + { + if (obj == null) { + return 0; + } + return Insert (obj, "OR REPLACE", Orm.GetType (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 int Insert ( + object obj, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType) + { + return 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. + /// 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 int InsertOrReplace ( + object obj, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType) + { + return Insert (obj, "OR REPLACE", 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. + /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif + public int Insert (object obj, string extra) + { + if (obj == null) { + return 0; + } + return Insert (obj, extra, Orm.GetType (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. + /// + /// + /// 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 int Insert ( + object obj, + string extra, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType) + { + if (obj == null || objType == null) { + return 0; + } + + var map = GetMapping (objType); + + if (map.PK != null && map.PK.IsAutoGuid) { + if (map.PK.GetValue (obj).Equals (Guid.Empty)) { + map.PK.SetValue (obj, Guid.NewGuid ()); + } + } + + var replacing = string.Compare (extra, "OR REPLACE", StringComparison.OrdinalIgnoreCase) == 0; + + var cols = replacing ? map.InsertOrReplaceColumns : map.InsertColumns; + var vals = new object[cols.Length]; + for (var i = 0; i < vals.Length; i++) { + vals[i] = cols[i].GetValue (obj); + } + + var insertCmd = GetInsertCommand (map, extra); + int count; + + lock (insertCmd) { + // We lock here to protect the prepared statement returned via GetInsertCommand. + // A SQLite prepared statement can be bound for only one operation at a time. + try { + count = insertCmd.ExecuteNonQuery (vals); + } + catch (SQLiteException ex) { + if (SQLite3.ExtendedErrCode (this.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { + throw NotNullConstraintViolationException.New (ex.Result, ex.Message, map, obj); + } + throw; + } + + if (map.HasAutoIncPK) { + var id = SQLite3.LastInsertRowid (Handle); + map.SetAutoIncPK (obj, id); + } + } + if (count > 0) + OnTableChanged (map, NotifyTableChangedAction.Insert); + + return count; + } + + readonly Dictionary, PreparedSqlLiteInsertCommand> _insertCommandMap = new Dictionary, PreparedSqlLiteInsertCommand> (); + + PreparedSqlLiteInsertCommand GetInsertCommand (TableMapping map, string extra) + { + PreparedSqlLiteInsertCommand prepCmd; + + var key = Tuple.Create (map.MappedType.FullName, extra); + + lock (_insertCommandMap) { + if (_insertCommandMap.TryGetValue (key, out prepCmd)) { + return prepCmd; + } + } + + prepCmd = CreateInsertCommand (map, extra); + + lock (_insertCommandMap) { + if (_insertCommandMap.TryGetValue (key, out var existing)) { + prepCmd.Dispose (); + return existing; + } + + _insertCommandMap.Add (key, prepCmd); + } + + return prepCmd; + } + + PreparedSqlLiteInsertCommand CreateInsertCommand (TableMapping map, string extra) + { + var cols = map.InsertColumns; + string insertSql; + if (cols.Length == 0 && map.Columns.Length == 1 && map.Columns[0].IsAutoInc) { + insertSql = string.Format ("insert {1} into \"{0}\" default values", map.TableName, extra); + } + else { + var replacing = string.Compare (extra, "OR REPLACE", StringComparison.OrdinalIgnoreCase) == 0; + + if (replacing) { + cols = map.InsertOrReplaceColumns; + } + + insertSql = string.Format ("insert {3} into \"{0}\"({1}) values ({2})", map.TableName, + string.Join (",", (from c in cols + select "\"" + c.Name + "\"").ToArray ()), + string.Join (",", (from c in cols + select "?").ToArray ()), extra); + + } + + var insertCommand = new PreparedSqlLiteInsertCommand (this, insertSql); + return insertCommand; + } + + /// + /// 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. + /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'obj'.")] +#endif + public int Update (object obj) + { + if (obj == null) { + return 0; + } + return Update (obj, Orm.GetType (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 int Update ( + object obj, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type objType) + { + int rowsAffected = 0; + if (obj == null || objType == null) { + return 0; + } + + var map = GetMapping (objType); + + var pk = map.PK; + + if (pk == null) { + throw new NotSupportedException ("Cannot update " + map.TableName + ": it has no PK"); + } + + var cols = from p in map.Columns + where p != pk + select p; + var vals = from c in cols + select c.GetValue (obj); + var ps = new List (vals); + if (ps.Count == 0) { + // There is a PK but no accompanying data, + // so reset the PK to make the UPDATE work. + cols = map.Columns; + vals = from c in cols + select c.GetValue (obj); + ps = new List (vals); + } + ps.Add (pk.GetValue (obj)); + var q = string.Format ("update \"{0}\" set {1} where \"{2}\" = ? ", map.TableName, string.Join (",", (from c in cols + select "\"" + c.Name + "\" = ? ").ToArray ()), pk.Name); + + try { + rowsAffected = Execute (q, ps.ToArray ()); + } + catch (SQLiteException ex) { + + if (ex.Result == SQLite3.Result.Constraint && SQLite3.ExtendedErrCode (this.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { + throw NotNullConstraintViolationException.New (ex, map, obj); + } + + throw; + } + + if (rowsAffected > 0) + OnTableChanged (map, NotifyTableChangedAction.Update); + + return rowsAffected; + } + + /// + /// 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. + /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of all objects in 'objects'.")] +#endif + public int UpdateAll (System.Collections.IEnumerable objects, bool runInTransaction = true) + { + var c = 0; + if (runInTransaction) { + RunInTransaction (() => { + foreach (var r in objects) { + c += Update (r); + } + }); + } + else { + foreach (var r in objects) { + c += Update (r); + } + } + return c; + } + + /// + /// 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. + /// +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode ("This method requires ''DynamicallyAccessedMemberTypes.All' on the runtime type of 'objectToDelete'.")] +#endif + public int Delete (object objectToDelete) + { + var map = GetMapping (Orm.GetType (objectToDelete)); + var pk = map.PK; + if (pk == null) { + throw new NotSupportedException ("Cannot delete " + map.TableName + ": it has no PK"); + } + var q = string.Format ("delete from \"{0}\" where \"{1}\" = ?", map.TableName, pk.Name); + var count = Execute (q, pk.GetValue (objectToDelete)); + if (count > 0) + OnTableChanged (map, NotifyTableChangedAction.Delete); + return count; + } + + /// + /// 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 int Delete< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> (object primaryKey) + { + return Delete (primaryKey, GetMapping (typeof (T))); + } + + /// + /// 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 int Delete (object primaryKey, TableMapping map) + { + var pk = map.PK; + if (pk == null) { + throw new NotSupportedException ("Cannot delete " + map.TableName + ": it has no PK"); + } + var q = string.Format ("delete from \"{0}\" where \"{1}\" = ?", map.TableName, pk.Name); + var count = Execute (q, primaryKey); + if (count > 0) + OnTableChanged (map, NotifyTableChangedAction.Delete); + return count; + } + + /// + /// 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 int DeleteAll< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> () + { + var map = GetMapping (typeof (T)); + return DeleteAll (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 TableMapping used to identify the table. + /// + /// + /// The number of objects deleted. + /// + public int DeleteAll (TableMapping map) + { + var query = string.Format ("delete from \"{0}\"", map.TableName); + var count = Execute (query); + if (count > 0) + OnTableChanged (map, NotifyTableChangedAction.Delete); + return count; + } + + /// + /// Backup the entire database to the specified path. + /// + /// Path to backup file. + /// The name of the database to backup (usually "main"). + public void Backup (string destinationDatabasePath, string databaseName = "main") + { + // Open the destination + var r = SQLite3.Open (destinationDatabasePath, out var destHandle); + if (r != SQLite3.Result.OK) { + throw SQLiteException.New (r, "Failed to open destination database"); + } + + // Init the backup + var backup = SQLite3.BackupInit (destHandle, databaseName, Handle, databaseName); + if (backup == NullBackupHandle) { + SQLite3.Close (destHandle); + throw new Exception ("Failed to create backup"); + } + + // Perform it + SQLite3.BackupStep (backup, -1); + SQLite3.BackupFinish (backup); + + // Check for errors + r = SQLite3.GetResult (destHandle); + string msg = ""; + if (r != SQLite3.Result.OK) { + msg = SQLite3.GetErrmsg (destHandle); + } + + // Close everything and report errors + SQLite3.Close (destHandle); + if (r != SQLite3.Result.OK) { + throw SQLiteException.New (r, msg); + } + } + + ~SQLiteConnection () + { + Dispose (false); + } + + public void Dispose () + { + Dispose (true); + GC.SuppressFinalize (this); + } + + public void Close () + { + Dispose (true); + } + + protected virtual void Dispose (bool disposing) + { + var useClose2 = LibVersionNumber >= 3007014; + + if (_open && Handle != NullHandle) { + try { + if (disposing) { + lock (_insertCommandMap) { + foreach (var sqlInsertCommand in _insertCommandMap.Values) { + sqlInsertCommand.Dispose (); + } + _insertCommandMap.Clear (); + } + + var r = useClose2 ? SQLite3.Close2 (Handle) : SQLite3.Close (Handle); + if (r != SQLite3.Result.OK) { + string msg = SQLite3.GetErrmsg (Handle); + throw SQLiteException.New (r, msg); + } + } + else { + var r = useClose2 ? SQLite3.Close2 (Handle) : SQLite3.Close (Handle); + } + } + finally { + Handle = NullHandle; + _open = false; + } + } + } + + void OnTableChanged (TableMapping table, NotifyTableChangedAction action) + { + var ev = TableChanged; + if (ev != null) + ev (this, new NotifyTableChangedEventArgs (table, action)); + } + + public event EventHandler TableChanged; + } + + public class NotifyTableChangedEventArgs : EventArgs + { + public TableMapping Table { get; private set; } + public NotifyTableChangedAction Action { get; private set; } + + public NotifyTableChangedEventArgs (TableMapping table, NotifyTableChangedAction action) + { + Table = table; + Action = action; + } + } + + public enum NotifyTableChangedAction + { + Insert, + Update, + Delete, + } + + /// + /// Represents a parsed connection string. + /// + public class SQLiteConnectionString + { + const string DateTimeSqliteDefaultFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff"; + + public string UniqueKey { get; } + public string DatabasePath { get; } + public bool StoreDateTimeAsTicks { get; } + public bool StoreTimeSpanAsTicks { get; } + public string DateTimeStringFormat { get; } + public System.Globalization.DateTimeStyles DateTimeStyle { get; } + public object Key { get; } + public SQLiteOpenFlags OpenFlags { get; } + public Action PreKeyAction { get; } + public Action PostKeyAction { get; } + public string VfsName { get; } + +#if NETFX_CORE + static readonly string MetroStyleDataPath = Windows.Storage.ApplicationData.Current.LocalFolder.Path; + + public static readonly string[] InMemoryDbPaths = new[] + { + ":memory:", + "file::memory:" + }; + + public static bool IsInMemoryPath(string databasePath) + { + return InMemoryDbPaths.Any(i => i.Equals(databasePath, StringComparison.OrdinalIgnoreCase)); + } + +#endif + + /// + /// Constructs a new SQLiteConnectionString with all the data needed to open an SQLiteConnection. + /// + /// + /// 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 SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks = true) + : this (databasePath, SQLiteOpenFlags.Create | SQLiteOpenFlags.ReadWrite, storeDateTimeAsTicks) + { + } + + /// + /// Constructs a new SQLiteConnectionString with all the data needed to open an SQLiteConnection. + /// + /// + /// 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. + /// + /// + /// Specifies the encryption key to use on the database. Should be a string or a byte[]. + /// + /// + /// Executes prior to setting key for SQLCipher databases + /// + /// + /// Executes after setting key for SQLCipher databases + /// + /// + /// Specifies the Virtual File System to use on the database. + /// + public SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks, object key = null, Action preKeyAction = null, Action postKeyAction = null, string vfsName = null) + : this (databasePath, SQLiteOpenFlags.Create | SQLiteOpenFlags.ReadWrite, storeDateTimeAsTicks, key, preKeyAction, postKeyAction, vfsName) + { + } + + /// + /// Constructs a new SQLiteConnectionString with all the data needed to open an SQLiteConnection. + /// + /// + /// Specifies the path to the database file. + /// + /// + /// Flags controlling how the connection should be opened. + /// + /// + /// 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. + /// + /// + /// Specifies the encryption key to use on the database. Should be a string or a byte[]. + /// + /// + /// Executes prior to setting key for SQLCipher databases + /// + /// + /// Executes after setting key for SQLCipher databases + /// + /// + /// Specifies the Virtual File System to use on the database. + /// + /// + /// Specifies the format to use when storing DateTime properties as strings. + /// + /// + /// Specifies whether to store TimeSpan 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 storeTimeSpanAsTicks = true. + /// + public SQLiteConnectionString (string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks, object key = null, Action preKeyAction = null, Action postKeyAction = null, string vfsName = null, string dateTimeStringFormat = DateTimeSqliteDefaultFormat, bool storeTimeSpanAsTicks = true) + { + if (key != null && !((key is byte[]) || (key is string))) + throw new ArgumentException ("Encryption keys must be strings or byte arrays", nameof (key)); + + UniqueKey = string.Format ("{0}_{1:X8}", databasePath, (uint)openFlags); + StoreDateTimeAsTicks = storeDateTimeAsTicks; + StoreTimeSpanAsTicks = storeTimeSpanAsTicks; + DateTimeStringFormat = dateTimeStringFormat; + DateTimeStyle = "o".Equals (DateTimeStringFormat, StringComparison.OrdinalIgnoreCase) || "r".Equals (DateTimeStringFormat, StringComparison.OrdinalIgnoreCase) ? System.Globalization.DateTimeStyles.RoundtripKind : System.Globalization.DateTimeStyles.None; + Key = key; + PreKeyAction = preKeyAction; + PostKeyAction = postKeyAction; + OpenFlags = openFlags; + VfsName = vfsName; + +#if NETFX_CORE + DatabasePath = IsInMemoryPath(databasePath) + ? databasePath + : System.IO.Path.Combine(MetroStyleDataPath, databasePath); + +#else + DatabasePath = databasePath; +#endif + } + } + + [AttributeUsage (AttributeTargets.Class)] + public class TableAttribute : Attribute + { + public string Name { get; set; } + + /// + /// Flag whether to create the table without rowid (see https://sqlite.org/withoutrowid.html) + /// + /// The default is false so that sqlite adds an implicit rowid to every table created. + /// + public bool WithoutRowId { get; set; } + + public TableAttribute (string name) + { + Name = name; + } + } + + [AttributeUsage (AttributeTargets.Property)] + public class ColumnAttribute : Attribute + { + public string Name { get; set; } + + public ColumnAttribute (string name) + { + Name = name; + } + } + + [AttributeUsage (AttributeTargets.Property)] + public class PrimaryKeyAttribute : Attribute + { + } + + [AttributeUsage (AttributeTargets.Property)] + public class AutoIncrementAttribute : Attribute + { + } + + [AttributeUsage (AttributeTargets.Property, AllowMultiple = true)] + public class IndexedAttribute : Attribute + { + public string Name { get; set; } + public int Order { get; set; } + public virtual bool Unique { get; set; } + + public IndexedAttribute () + { + } + + public IndexedAttribute (string name, int order) + { + Name = name; + Order = order; + } + } + + [AttributeUsage (AttributeTargets.Property)] + public class IgnoreAttribute : Attribute + { + } + + [AttributeUsage (AttributeTargets.Property)] + public class UniqueAttribute : IndexedAttribute + { + public override bool Unique { + get { return true; } + set { /* throw? */ } + } + } + + [AttributeUsage (AttributeTargets.Property)] + public class MaxLengthAttribute : Attribute + { + public int Value { get; private set; } + + public MaxLengthAttribute (int length) + { + Value = length; + } + } + + public sealed class PreserveAttribute : System.Attribute + { + public bool AllMembers; + public bool Conditional; + } + + /// + /// Select the collating sequence to use on a column. + /// "BINARY", "NOCASE", and "RTRIM" are supported. + /// "BINARY" is the default. + /// + [AttributeUsage (AttributeTargets.Property)] + public class CollationAttribute : Attribute + { + public string Value { get; private set; } + + public CollationAttribute (string collation) + { + Value = collation; + } + } + + [AttributeUsage (AttributeTargets.Property)] + public class NotNullAttribute : Attribute + { + } + + [AttributeUsage (AttributeTargets.Enum)] + public class StoreAsTextAttribute : Attribute + { + } + + public class TableMapping + { +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] +#endif + public Type MappedType { get; private set; } + + public string TableName { get; private set; } + + public bool WithoutRowId { get; private set; } + + public Column[] Columns { get; private set; } + + public Column PK { get; private set; } + + public string GetByPrimaryKeySql { get; private set; } + + public CreateFlags CreateFlags { get; private set; } + + internal MapMethod Method { get; private set; } = MapMethod.ByName; + + readonly Column _autoPk; + readonly Column[] _insertColumns; + readonly Column[] _insertOrReplaceColumns; + + public TableMapping ( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type type, + CreateFlags createFlags = CreateFlags.None) + { + MappedType = type; + CreateFlags = createFlags; + +#if ENABLE_IL2CPP + var typeInfo = type.GetTypeInfo (); + var tableAttr = typeInfo.GetCustomAttribute (); +#elif NET8_0_OR_GREATER + var tableAttr = type.GetCustomAttributes ().FirstOrDefault (); +#else + var typeInfo = type.GetTypeInfo (); + var tableAttr = + typeInfo.CustomAttributes + .Where (x => x.AttributeType == typeof (TableAttribute)) + .Select (x => (TableAttribute)Orm.InflateAttribute (x)) + .FirstOrDefault (); +#endif + + TableName = (tableAttr != null && !string.IsNullOrEmpty (tableAttr.Name)) ? tableAttr.Name : MappedType.Name; + WithoutRowId = tableAttr != null ? tableAttr.WithoutRowId : false; + + var members = GetPublicMembers(type); + var cols = new List(members.Count); + foreach(var m in members) + { + var ignore = m.IsDefined(typeof(IgnoreAttribute), true); + if(!ignore) + cols.Add(new Column(m, createFlags)); + } + Columns = cols.ToArray (); + foreach (var c in Columns) { + if (c.IsAutoInc && c.IsPK) { + _autoPk = c; + } + if (c.IsPK) { + PK = c; + } + } + + HasAutoIncPK = _autoPk != null; + + if (PK != null) { + GetByPrimaryKeySql = string.Format ("select * from \"{0}\" where \"{1}\" = ?", TableName, PK.Name); + } + else { + // People should not be calling Get/Find without a PK + GetByPrimaryKeySql = string.Format ("select * from \"{0}\" limit 1", TableName); + } + + _insertColumns = Columns.Where (c => !c.IsAutoInc).ToArray (); + _insertOrReplaceColumns = Columns.ToArray (); + } + + private IReadOnlyCollection GetPublicMembers( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + Type type) + { + if(type.Name.StartsWith("ValueTuple`")) + return GetFieldsFromValueTuple(type); + + var members = new List(); + var memberNames = new HashSet(); + var newMembers = new List(); + do + { + var ti = type.GetTypeInfo(); + newMembers.Clear(); + + newMembers.AddRange( + from p in ti.DeclaredProperties + where !memberNames.Contains(p.Name) && + p.CanRead && p.CanWrite && + p.GetMethod != null && p.SetMethod != null && + p.GetMethod.IsPublic && p.SetMethod.IsPublic && + !p.GetMethod.IsStatic && !p.SetMethod.IsStatic + select p); + + members.AddRange(newMembers); + foreach(var m in newMembers) + memberNames.Add(m.Name); + + type = ti.BaseType; + } + while(type != typeof(object)); + + return members; + } + + private IReadOnlyCollection GetFieldsFromValueTuple( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields)] +#endif + Type type) + { + Method = MapMethod.ByPosition; + var fields = type.GetFields(); + + // https://docs.microsoft.com/en-us/dotnet/api/system.valuetuple-8.rest + if(fields.Length >= 8) + throw new NotSupportedException("ValueTuple with more than 7 members not supported due to nesting; see https://docs.microsoft.com/en-us/dotnet/api/system.valuetuple-8.rest"); + + return fields; + } + + public bool HasAutoIncPK { get; private set; } + + public void SetAutoIncPK (object obj, long id) + { + if (_autoPk != null) { + _autoPk.SetValue (obj, Convert.ChangeType (id, _autoPk.ColumnType, null)); + } + } + + public Column[] InsertColumns { + get { + return _insertColumns; + } + } + + public Column[] InsertOrReplaceColumns { + get { + return _insertOrReplaceColumns; + } + } + + public Column FindColumnWithPropertyName (string propertyName) + { + var exact = Columns.FirstOrDefault (c => c.PropertyName == propertyName); + return exact; + } + + public Column FindColumn (string columnName) + { + if(Method != MapMethod.ByName) + throw new InvalidOperationException($"This {nameof(TableMapping)} is not mapped by name, but {Method}."); + + var exact = Columns.FirstOrDefault (c => c.Name.ToLower () == columnName.ToLower ()); + return exact; + } + + public class Column + { + MemberInfo _member; + + public string Name { get; private set; } + + public PropertyInfo PropertyInfo => _member as PropertyInfo; + + public string PropertyName { get { return _member.Name; } } + + public Type ColumnType { get; private set; } + + public string Collation { get; private set; } + + public bool IsAutoInc { get; private set; } + public bool IsAutoGuid { get; private set; } + + public bool IsPK { get; private set; } + + public IEnumerable Indices { get; set; } + + public bool IsNullable { get; private set; } + + public int? MaxStringLength { get; private set; } + + public bool StoreAsText { get; private set; } + + public Column (MemberInfo member, CreateFlags createFlags = CreateFlags.None) + { + _member = member; + var memberType = GetMemberType(member); + + var colAttr = member.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (ColumnAttribute)); +#if ENABLE_IL2CPP + var ca = member.GetCustomAttribute(typeof(ColumnAttribute)) as ColumnAttribute; + Name = ca == null ? member.Name : ca.Name; +#else + Name = (colAttr != null && colAttr.ConstructorArguments.Count > 0) ? + colAttr.ConstructorArguments[0].Value?.ToString () : + member.Name; +#endif + //If this type is Nullable then Nullable.GetUnderlyingType returns the T, otherwise it returns null, so get the actual type instead + ColumnType = Nullable.GetUnderlyingType (memberType) ?? memberType; + Collation = Orm.Collation (member); + + IsPK = Orm.IsPK (member) || + (((createFlags & CreateFlags.ImplicitPK) == CreateFlags.ImplicitPK) && + string.Compare (member.Name, Orm.ImplicitPkName, StringComparison.OrdinalIgnoreCase) == 0); + + var isAuto = Orm.IsAutoInc (member) || (IsPK && ((createFlags & CreateFlags.AutoIncPK) == CreateFlags.AutoIncPK)); + IsAutoGuid = isAuto && ColumnType == typeof (Guid); + IsAutoInc = isAuto && !IsAutoGuid; + + Indices = Orm.GetIndices (member); + if (!Indices.Any () + && !IsPK + && ((createFlags & CreateFlags.ImplicitIndex) == CreateFlags.ImplicitIndex) + && Name.EndsWith (Orm.ImplicitIndexSuffix, StringComparison.OrdinalIgnoreCase) + ) { + Indices = new IndexedAttribute[] { new IndexedAttribute () }; + } + IsNullable = !(IsPK || Orm.IsMarkedNotNull (member)); + MaxStringLength = Orm.MaxStringLength (member); + + StoreAsText = memberType.GetTypeInfo ().CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute)); + } + + public Column (PropertyInfo member, CreateFlags createFlags = CreateFlags.None) + : this((MemberInfo)member, createFlags) + { } + + public void SetValue (object obj, object val) + { + if(_member is PropertyInfo propy) + { + if (val != null && ColumnType.GetTypeInfo ().IsEnum) + propy.SetValue (obj, Enum.ToObject (ColumnType, val)); + else + propy.SetValue (obj, val); + } + else if(_member is FieldInfo field) + { + if (val != null && ColumnType.GetTypeInfo ().IsEnum) + field.SetValue (obj, Enum.ToObject (ColumnType, val)); + else + field.SetValue (obj, val); + } + else + throw new InvalidProgramException("unreachable condition"); + } + + public object GetValue (object obj) + { + if(_member is PropertyInfo propy) + return propy.GetValue(obj); + else if(_member is FieldInfo field) + return field.GetValue(obj); + else + throw new InvalidProgramException("unreachable condition"); + } + + private static Type GetMemberType(MemberInfo m) + { + switch(m.MemberType) + { + case MemberTypes.Property: return ((PropertyInfo)m).PropertyType; + case MemberTypes.Field: return ((FieldInfo)m).FieldType; + default: throw new InvalidProgramException($"{nameof(TableMapping)} supports properties or fields only."); + } + } + } + + internal enum MapMethod + { + ByName, + ByPosition + } + } + + class EnumCacheInfo + { + public EnumCacheInfo (Type type) + { + var typeInfo = type.GetTypeInfo (); + + IsEnum = typeInfo.IsEnum; + + if (IsEnum) { + StoreAsText = typeInfo.CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute)); + + if (StoreAsText) { + EnumValues = new Dictionary (); +#if NET8_0_OR_GREATER + foreach (object e in Enum.GetValuesAsUnderlyingType (type)) { + EnumValues[Convert.ToInt32 (e)] = Enum.ToObject(type, e).ToString (); + } +#else + foreach (object e in Enum.GetValues (type)) { + EnumValues[Convert.ToInt32 (e)] = e.ToString (); + } +#endif + } + } + } + + public bool IsEnum { get; private set; } + + public bool StoreAsText { get; private set; } + + public Dictionary EnumValues { get; private set; } + } + + static class EnumCache + { + static readonly Dictionary Cache = new Dictionary (); + + public static EnumCacheInfo GetInfo () + { + return GetInfo (typeof (T)); + } + + public static EnumCacheInfo GetInfo (Type type) + { + lock (Cache) { + EnumCacheInfo info = null; + if (!Cache.TryGetValue (type, out info)) { + info = new EnumCacheInfo (type); + Cache[type] = info; + } + + return info; + } + } + } + + public static class Orm + { + public const int DefaultMaxStringLength = 140; + public const string ImplicitPkName = "Id"; + public const string ImplicitIndexSuffix = "Id"; + + public static Type GetType (object obj) + { + if (obj == null) + return typeof (object); + var rt = obj as IReflectableType; + if (rt != null) + return rt.GetTypeInfo ().AsType (); + return obj.GetType (); + } + + public static string SqlDecl (TableMapping.Column p, bool storeDateTimeAsTicks, bool storeTimeSpanAsTicks) + { + string decl = "\"" + p.Name + "\" " + SqlType (p, storeDateTimeAsTicks, storeTimeSpanAsTicks) + " "; + + if (p.IsPK) { + decl += "primary key "; + } + if (p.IsAutoInc) { + decl += "autoincrement "; + } + if (!p.IsNullable) { + decl += "not null "; + } + if (!string.IsNullOrEmpty (p.Collation)) { + decl += "collate " + p.Collation + " "; + } + + return decl; + } + + public static string SqlType (TableMapping.Column p, bool storeDateTimeAsTicks, bool storeTimeSpanAsTicks) + { + var clrType = p.ColumnType; + if (clrType == typeof (Boolean) || clrType == typeof (Byte) || clrType == typeof (UInt16) || clrType == typeof (SByte) || clrType == typeof (Int16) || clrType == typeof (Int32) || clrType == typeof (UInt32) || clrType == typeof (Int64) || clrType == typeof (UInt64)) { + return "integer"; + } + else if (clrType == typeof (Single) || clrType == typeof (Double) || clrType == typeof (Decimal)) { + return "float"; + } + else if (clrType == typeof (String) || clrType == typeof (StringBuilder) || clrType == typeof (Uri) || clrType == typeof (UriBuilder)) { + int? len = p.MaxStringLength; + + if (len.HasValue) + return "varchar(" + len.Value + ")"; + + return "varchar"; + } + else if (clrType == typeof (TimeSpan)) { + return storeTimeSpanAsTicks ? "bigint" : "time"; + } + else if (clrType == typeof (DateTime)) { + return storeDateTimeAsTicks ? "bigint" : "datetime"; + } + else if (clrType == typeof (DateTimeOffset)) { + return "bigint"; + } + else if (clrType.GetTypeInfo ().IsEnum) { + if (p.StoreAsText) + return "varchar"; + else + return "integer"; + } + else if (clrType == typeof (byte[])) { + return "blob"; + } + else if (clrType == typeof (Guid)) { + return "varchar(36)"; + } + else { + throw new NotSupportedException ("Don't know about " + clrType); + } + } + + public static bool IsPK (MemberInfo p) + { + return p.CustomAttributes.Any (x => x.AttributeType == typeof (PrimaryKeyAttribute)); + } + + public static string Collation (MemberInfo p) + { +#if ENABLE_IL2CPP + return (p.GetCustomAttribute ()?.Value) ?? ""; +#else + return + (p.CustomAttributes + .Where (x => typeof (CollationAttribute) == x.AttributeType) + .Select (x => { + var args = x.ConstructorArguments; + return args.Count > 0 ? ((args[0].Value as string) ?? "") : ""; + }) + .FirstOrDefault ()) ?? ""; +#endif + } + + public static bool IsAutoInc (MemberInfo p) + { + return p.CustomAttributes.Any (x => x.AttributeType == typeof (AutoIncrementAttribute)); + } + + public static FieldInfo GetField ( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + TypeInfo t, + string name) + { + var f = t.GetDeclaredField (name); + if (f != null) + return f; + return GetField (t.BaseType.GetTypeInfo (), name); + } + + public static PropertyInfo GetProperty ( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + TypeInfo t, + string name) + { + var f = t.GetDeclaredProperty (name); + if (f != null) + return f; + return GetProperty (t.BaseType.GetTypeInfo (), name); + } + +#if !NET8_0_OR_GREATER + public static object InflateAttribute (CustomAttributeData x) + { + var atype = x.AttributeType; + var typeInfo = atype.GetTypeInfo (); +#if ENABLE_IL2CPP + var r = Activator.CreateInstance (x.AttributeType); +#else + var args = x.ConstructorArguments.Select (a => a.Value).ToArray (); + var r = Activator.CreateInstance (x.AttributeType, args); + foreach (var arg in x.NamedArguments) { + if (arg.IsField) { + GetField (typeInfo, arg.MemberName).SetValue (r, arg.TypedValue.Value); + } + else { + GetProperty (typeInfo, arg.MemberName).SetValue (r, arg.TypedValue.Value); + } + } +#endif + return r; + } +#endif + public static IEnumerable GetIndices (MemberInfo p) + { +#if ENABLE_IL2CPP || NET8_0_OR_GREATER + return p.GetCustomAttributes (); +#else + var indexedInfo = typeof (IndexedAttribute).GetTypeInfo (); + return + p.CustomAttributes + .Where (x => indexedInfo.IsAssignableFrom (x.AttributeType.GetTypeInfo ())) + .Select (x => (IndexedAttribute)InflateAttribute (x)); #endif + } - /// - /// Constructs a new SQLiteConnectionString with all the data needed to open an SQLiteConnection. - /// - /// - /// 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 SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks = true) - : this (databasePath, SQLiteOpenFlags.Create | SQLiteOpenFlags.ReadWrite, storeDateTimeAsTicks) - { - } - - /// - /// Constructs a new SQLiteConnectionString with all the data needed to open an SQLiteConnection. - /// - /// - /// 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. - /// - /// - /// Specifies the encryption key to use on the database. Should be a string or a byte[]. - /// - /// - /// Executes prior to setting key for SQLCipher databases - /// - /// - /// Executes after setting key for SQLCipher databases - /// - /// - /// Specifies the Virtual File System to use on the database. - /// - public SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks, object key = null, Action preKeyAction = null, Action postKeyAction = null, string vfsName = null) - : this (databasePath, SQLiteOpenFlags.Create | SQLiteOpenFlags.ReadWrite, storeDateTimeAsTicks, key, preKeyAction, postKeyAction, vfsName) - { - } - - /// - /// Constructs a new SQLiteConnectionString with all the data needed to open an SQLiteConnection. - /// - /// - /// Specifies the path to the database file. - /// - /// - /// Flags controlling how the connection should be opened. - /// - /// - /// 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. - /// - /// - /// Specifies the encryption key to use on the database. Should be a string or a byte[]. - /// - /// - /// Executes prior to setting key for SQLCipher databases - /// - /// - /// Executes after setting key for SQLCipher databases - /// - /// - /// Specifies the Virtual File System to use on the database. - /// - /// - /// Specifies the format to use when storing DateTime properties as strings. - /// - /// - /// Specifies whether to store TimeSpan 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 storeTimeSpanAsTicks = true. - /// - public SQLiteConnectionString (string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks, object key = null, Action preKeyAction = null, Action postKeyAction = null, string vfsName = null, string dateTimeStringFormat = DateTimeSqliteDefaultFormat, bool storeTimeSpanAsTicks = true) - { - if (key != null && !((key is byte[]) || (key is string))) - throw new ArgumentException ("Encryption keys must be strings or byte arrays", nameof (key)); - - UniqueKey = string.Format ("{0}_{1:X8}", databasePath, (uint)openFlags); - StoreDateTimeAsTicks = storeDateTimeAsTicks; - StoreTimeSpanAsTicks = storeTimeSpanAsTicks; - DateTimeStringFormat = dateTimeStringFormat; - DateTimeStyle = "o".Equals (DateTimeStringFormat, StringComparison.OrdinalIgnoreCase) || "r".Equals (DateTimeStringFormat, StringComparison.OrdinalIgnoreCase) ? System.Globalization.DateTimeStyles.RoundtripKind : System.Globalization.DateTimeStyles.None; - Key = key; - PreKeyAction = preKeyAction; - PostKeyAction = postKeyAction; - OpenFlags = openFlags; - VfsName = vfsName; + public static int? MaxStringLength (MemberInfo p) + { +#if ENABLE_IL2CPP + return p.GetCustomAttribute ()?.Value; +#elif NET8_0_OR_GREATER + return p.GetCustomAttributes ().FirstOrDefault ()?.Value; +#else + var attr = p.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (MaxLengthAttribute)); + if (attr != null) { + var attrv = (MaxLengthAttribute)InflateAttribute (attr); + return attrv.Value; + } + return null; +#endif + } -#if NETFX_CORE - DatabasePath = IsInMemoryPath(databasePath) - ? databasePath - : System.IO.Path.Combine(MetroStyleDataPath, databasePath); + public static int? MaxStringLength (PropertyInfo p) => MaxStringLength((MemberInfo)p); -#else - DatabasePath = databasePath; + public static bool IsMarkedNotNull (MemberInfo p) + { + return p.CustomAttributes.Any (x => x.AttributeType == typeof (NotNullAttribute)); + } + } + + public partial class SQLiteCommand + { + SQLiteConnection _conn; + private List _bindings; + + public string CommandText { get; set; } + + public SQLiteCommand (SQLiteConnection conn) + { + _conn = conn; + _bindings = new List (); + CommandText = ""; + } + + public int ExecuteNonQuery () + { + if (_conn.Trace) { + _conn.Tracer?.Invoke ("Executing: " + this); + } + + var r = SQLite3.Result.OK; + var stmt = Prepare (); + r = SQLite3.Step (stmt); + Finalize (stmt); + if (r == SQLite3.Result.Done) { + int rowsAffected = SQLite3.Changes (_conn.Handle); + return rowsAffected; + } + else if (r == SQLite3.Result.Error) { + string msg = SQLite3.GetErrmsg (_conn.Handle); + throw SQLiteException.New (r, msg); + } + else if (r == SQLite3.Result.Constraint) { + if (SQLite3.ExtendedErrCode (_conn.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { + throw NotNullConstraintViolationException.New (r, SQLite3.GetErrmsg (_conn.Handle)); + } + } + + throw SQLiteException.New (r, SQLite3.GetErrmsg (_conn.Handle)); + } + + public IEnumerable ExecuteDeferredQuery< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] +#endif + T> () + { + return ExecuteDeferredQuery (_conn.GetMapping (typeof (T))); + } + + public List ExecuteQuery< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif - } - } - - [AttributeUsage (AttributeTargets.Class)] - public class TableAttribute : Attribute - { - public string Name { get; set; } - - /// - /// Flag whether to create the table without rowid (see https://sqlite.org/withoutrowid.html) - /// - /// The default is false so that sqlite adds an implicit rowid to every table created. - /// - public bool WithoutRowId { get; set; } - - public TableAttribute (string name) - { - Name = name; - } - } - - [AttributeUsage (AttributeTargets.Property)] - public class ColumnAttribute : Attribute - { - public string Name { get; set; } - - public ColumnAttribute (string name) - { - Name = name; - } - } - - [AttributeUsage (AttributeTargets.Property)] - public class PrimaryKeyAttribute : Attribute - { - } - - [AttributeUsage (AttributeTargets.Property)] - public class AutoIncrementAttribute : Attribute - { - } - - [AttributeUsage (AttributeTargets.Property, AllowMultiple = true)] - public class IndexedAttribute : Attribute - { - public string Name { get; set; } - public int Order { get; set; } - public virtual bool Unique { get; set; } - - public IndexedAttribute () - { - } - - public IndexedAttribute (string name, int order) - { - Name = name; - Order = order; - } - } - - [AttributeUsage (AttributeTargets.Property)] - public class IgnoreAttribute : Attribute - { - } - - [AttributeUsage (AttributeTargets.Property)] - public class UniqueAttribute : IndexedAttribute - { - public override bool Unique { - get { return true; } - set { /* throw? */ } - } - } - - [AttributeUsage (AttributeTargets.Property)] - public class MaxLengthAttribute : Attribute - { - public int Value { get; private set; } - - public MaxLengthAttribute (int length) - { - Value = length; - } - } - - public sealed class PreserveAttribute : System.Attribute - { - public bool AllMembers; - public bool Conditional; - } - - /// - /// Select the collating sequence to use on a column. - /// "BINARY", "NOCASE", and "RTRIM" are supported. - /// "BINARY" is the default. - /// - [AttributeUsage (AttributeTargets.Property)] - public class CollationAttribute : Attribute - { - public string Value { get; private set; } - - public CollationAttribute (string collation) - { - Value = collation; - } - } - - [AttributeUsage (AttributeTargets.Property)] - public class NotNullAttribute : Attribute - { - } - - [AttributeUsage (AttributeTargets.Enum)] - public class StoreAsTextAttribute : Attribute - { - } - - public class TableMapping - { + T> () + { + return ExecuteDeferredQuery (_conn.GetMapping (typeof (T))).ToList (); + } + + public List ExecuteQuery (TableMapping map) + { + return ExecuteDeferredQuery (map).ToList (); + } + + /// + /// Invoked every time an instance is loaded from the database. + /// + /// + /// The newly created object. + /// + /// + /// This can be overridden in combination with the + /// method to hook into the life-cycle of objects. + /// + protected virtual void OnInstanceCreated (object obj) + { + // Can be overridden. + } + + public IEnumerable ExecuteDeferredQuery (TableMapping map) + { + if (_conn.Trace) { + _conn.Tracer?.Invoke ("Executing Query: " + this); + } + + var stmt = Prepare (); + try { + var cols = new TableMapping.Column[SQLite3.ColumnCount (stmt)]; + var fastColumnSetters = new Action[SQLite3.ColumnCount (stmt)]; + + if (map.Method == TableMapping.MapMethod.ByPosition) + { + Array.Copy(map.Columns, cols, Math.Min(cols.Length, map.Columns.Length)); + } + else if (map.Method == TableMapping.MapMethod.ByName) { + MethodInfo getSetter = null; + if (typeof(T) != map.MappedType) { #if NET8_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + if (!RuntimeFeature.IsDynamicCodeSupported) { + if (map.MappedType.IsValueType) { + throw new NotSupportedException ( + $"Executing a query with a value type mapped type is not supported in AOT environments (The mapped type is '{map.MappedType}')."); + } + } #endif - public Type MappedType { get; private set; } + getSetter = FastColumnSetter.GetFastSetterMethodInfoUnsafe (map.MappedType); + } + + for (int i = 0; i < cols.Length; i++) { + var name = SQLite3.ColumnName16 (stmt, i); + cols[i] = map.FindColumn (name); + if (cols[i] != null) + if (getSetter != null) { + fastColumnSetters[i] = (Action)getSetter.Invoke(null, new object[]{ _conn, cols[i]}); + } + else { + fastColumnSetters[i] = FastColumnSetter.GetFastSetter(_conn, cols[i]); + } + } + } + + while (SQLite3.Step (stmt) == SQLite3.Result.Row) { + var obj = Activator.CreateInstance (map.MappedType); + for (int i = 0; i < cols.Length; i++) { + if (cols[i] == null) + continue; + + if (fastColumnSetters[i] != null) { + fastColumnSetters[i].Invoke (obj, stmt, i); + } + else { + var colType = SQLite3.ColumnType (stmt, i); + var val = ReadCol (stmt, i, colType, cols[i].ColumnType); + cols[i].SetValue (obj, val); + } + } + OnInstanceCreated (obj); + yield return (T)obj; + } + } + finally { + SQLite3.Finalize (stmt); + } + } + + public T ExecuteScalar () + { + if (_conn.Trace) { + _conn.Tracer?.Invoke ("Executing Query: " + this); + } + + T val = default (T); + + var stmt = Prepare (); + + try { + var r = SQLite3.Step (stmt); + if (r == SQLite3.Result.Row) { + var colType = SQLite3.ColumnType (stmt, 0); + var colval = ReadCol (stmt, 0, colType, typeof (T)); + if (colval != null) { + val = (T)colval; + } + } + else if (r == SQLite3.Result.Done) { + } + else { + throw SQLiteException.New (r, SQLite3.GetErrmsg (_conn.Handle)); + } + } + finally { + Finalize (stmt); + } + + return val; + } + + public IEnumerable ExecuteQueryScalars () + { + if (_conn.Trace) { + _conn.Tracer?.Invoke ("Executing Query: " + this); + } + var stmt = Prepare (); + try { + if (SQLite3.ColumnCount (stmt) < 1) { + throw new InvalidOperationException ("QueryScalars should return at least one column"); + } + while (SQLite3.Step (stmt) == SQLite3.Result.Row) { + var colType = SQLite3.ColumnType (stmt, 0); + var val = ReadCol (stmt, 0, colType, typeof (T)); + if (val == null) { + yield return default (T); + } + else { + yield return (T)val; + } + } + } + finally { + Finalize (stmt); + } + } + + public void Bind (string name, object val) + { + _bindings.Add (new Binding { + Name = name, + Value = val + }); + } + + public void Bind (object val) + { + Bind (null, val); + } + + public override string ToString () + { + var parts = new string[1 + _bindings.Count]; + parts[0] = CommandText; + var i = 1; + foreach (var b in _bindings) { + parts[i] = string.Format (" {0}: {1}", i - 1, b.Value); + i++; + } + return string.Join (Environment.NewLine, parts); + } + + Sqlite3Statement Prepare () + { + var stmt = SQLite3.Prepare2 (_conn.Handle, CommandText); + BindAll (stmt); + return stmt; + } - public string TableName { get; private set; } + void Finalize (Sqlite3Statement stmt) + { + SQLite3.Finalize (stmt); + } - public bool WithoutRowId { get; private set; } + void BindAll (Sqlite3Statement stmt) + { + int nextIdx = 1; + foreach (var b in _bindings) { + if (b.Name != null) { + b.Index = SQLite3.BindParameterIndex (stmt, b.Name); + } + else { + b.Index = nextIdx++; + } + + BindParameter (stmt, b.Index, b.Value, _conn.StoreDateTimeAsTicks, _conn.DateTimeStringFormat, _conn.StoreTimeSpanAsTicks); + } + } - public Column[] Columns { get; private set; } + static IntPtr NegativePointer = new IntPtr (-1); - public Column PK { get; private set; } + internal static void BindParameter (Sqlite3Statement stmt, int index, object value, bool storeDateTimeAsTicks, string dateTimeStringFormat, bool storeTimeSpanAsTicks) + { + if (value == null) { + SQLite3.BindNull (stmt, index); + } + else { + if (value is Int32) { + SQLite3.BindInt (stmt, index, (int)value); + } + else if (value is String) { + SQLite3.BindText (stmt, index, (string)value, -1, NegativePointer); + } + else if (value is Byte || value is UInt16 || value is SByte || value is Int16) { + SQLite3.BindInt (stmt, index, Convert.ToInt32 (value)); + } + else if (value is Boolean) { + SQLite3.BindInt (stmt, index, (bool)value ? 1 : 0); + } + else if (value is UInt32 || value is Int64 || value is UInt64) { + SQLite3.BindInt64 (stmt, index, Convert.ToInt64 (value)); + } + else if (value is Single || value is Double || value is Decimal) { + SQLite3.BindDouble (stmt, index, Convert.ToDouble (value)); + } + else if (value is TimeSpan) { + if (storeTimeSpanAsTicks) { + SQLite3.BindInt64 (stmt, index, ((TimeSpan)value).Ticks); + } + else { + SQLite3.BindText (stmt, index, ((TimeSpan)value).ToString (), -1, NegativePointer); + } + } + else if (value is DateTime) { + if (storeDateTimeAsTicks) { + SQLite3.BindInt64 (stmt, index, ((DateTime)value).Ticks); + } + else { + SQLite3.BindText (stmt, index, ((DateTime)value).ToString (dateTimeStringFormat, System.Globalization.CultureInfo.InvariantCulture), -1, NegativePointer); + } + } + else if (value is DateTimeOffset) { + SQLite3.BindInt64 (stmt, index, ((DateTimeOffset)value).UtcTicks); + } + else if (value is byte[]) { + SQLite3.BindBlob (stmt, index, (byte[])value, ((byte[])value).Length, NegativePointer); + } + else if (value is Guid) { + SQLite3.BindText (stmt, index, ((Guid)value).ToString (), 72, NegativePointer); + } + else if (value is Uri) { + SQLite3.BindText (stmt, index, ((Uri)value).ToString (), -1, NegativePointer); + } + else if (value is StringBuilder) { + SQLite3.BindText (stmt, index, ((StringBuilder)value).ToString (), -1, NegativePointer); + } + else if (value is UriBuilder) { + SQLite3.BindText (stmt, index, ((UriBuilder)value).ToString (), -1, NegativePointer); + } + else { + // Now we could possibly get an enum, retrieve cached info + var valueType = value.GetType (); + var enumInfo = EnumCache.GetInfo (valueType); + if (enumInfo.IsEnum) { + var enumIntValue = Convert.ToInt32 (value); + if (enumInfo.StoreAsText) + SQLite3.BindText (stmt, index, enumInfo.EnumValues[enumIntValue], -1, NegativePointer); + else + SQLite3.BindInt (stmt, index, enumIntValue); + } + else { + throw new NotSupportedException ("Cannot store type: " + Orm.GetType (value)); + } + } + } + } - public string GetByPrimaryKeySql { get; private set; } + class Binding + { + public string Name { get; set; } - public CreateFlags CreateFlags { get; private set; } + public object Value { get; set; } - internal MapMethod Method { get; private set; } = MapMethod.ByName; + public int Index { get; set; } + } - readonly Column _autoPk; - readonly Column[] _insertColumns; - readonly Column[] _insertOrReplaceColumns; + object ReadCol (Sqlite3Statement stmt, int index, SQLite3.ColType type, Type clrType) + { + if (type == SQLite3.ColType.Null) { + return null; + } + else { + var clrTypeInfo = clrType.GetTypeInfo (); + if (clrTypeInfo.IsGenericType && clrTypeInfo.GetGenericTypeDefinition () == typeof (Nullable<>)) { + clrType = clrTypeInfo.GenericTypeArguments[0]; + clrTypeInfo = clrType.GetTypeInfo (); + } + + if (clrType == typeof (String)) { + return SQLite3.ColumnString (stmt, index); + } + else if (clrType == typeof (Int32)) { + return (int)SQLite3.ColumnInt (stmt, index); + } + else if (clrType == typeof (Boolean)) { + return SQLite3.ColumnInt (stmt, index) == 1; + } + else if (clrType == typeof (double)) { + return SQLite3.ColumnDouble (stmt, index); + } + else if (clrType == typeof (float)) { + return (float)SQLite3.ColumnDouble (stmt, index); + } + else if (clrType == typeof (TimeSpan)) { + if (_conn.StoreTimeSpanAsTicks) { + return new TimeSpan (SQLite3.ColumnInt64 (stmt, index)); + } + else { + var text = SQLite3.ColumnString (stmt, index); + TimeSpan resultTime; + if (!TimeSpan.TryParseExact (text, "c", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.TimeSpanStyles.None, out resultTime)) { + resultTime = TimeSpan.Parse (text); + } + return resultTime; + } + } + else if (clrType == typeof (DateTime)) { + if (_conn.StoreDateTimeAsTicks) { + return new DateTime (SQLite3.ColumnInt64 (stmt, index)); + } + else { + var text = SQLite3.ColumnString (stmt, index); + DateTime resultDate; + if (!DateTime.TryParseExact (text, _conn.DateTimeStringFormat, System.Globalization.CultureInfo.InvariantCulture, _conn.DateTimeStyle, out resultDate)) { + resultDate = DateTime.Parse (text); + } + return resultDate; + } + } + else if (clrType == typeof (DateTimeOffset)) { + return new DateTimeOffset (SQLite3.ColumnInt64 (stmt, index), TimeSpan.Zero); + } + else if (clrTypeInfo.IsEnum) { + if (type == SQLite3.ColType.Text) { + var value = SQLite3.ColumnString (stmt, index); + return Enum.Parse (clrType, value.ToString (), true); + } + else + return SQLite3.ColumnInt (stmt, index); + } + else if (clrType == typeof (Int64)) { + return SQLite3.ColumnInt64 (stmt, index); + } + else if (clrType == typeof (UInt64)) { + return (ulong)SQLite3.ColumnInt64 (stmt, index); + } + else if (clrType == typeof (UInt32)) { + return (uint)SQLite3.ColumnInt64 (stmt, index); + } + else if (clrType == typeof (decimal)) { + return (decimal)SQLite3.ColumnDouble (stmt, index); + } + else if (clrType == typeof (Byte)) { + return (byte)SQLite3.ColumnInt (stmt, index); + } + else if (clrType == typeof (UInt16)) { + return (ushort)SQLite3.ColumnInt (stmt, index); + } + else if (clrType == typeof (Int16)) { + return (short)SQLite3.ColumnInt (stmt, index); + } + else if (clrType == typeof (sbyte)) { + return (sbyte)SQLite3.ColumnInt (stmt, index); + } + else if (clrType == typeof (byte[])) { + return SQLite3.ColumnByteArray (stmt, index); + } + else if (clrType == typeof (Guid)) { + var text = SQLite3.ColumnString (stmt, index); + return new Guid (text); + } + else if (clrType == typeof (Uri)) { + var text = SQLite3.ColumnString (stmt, index); + return new Uri (text); + } + else if (clrType == typeof (StringBuilder)) { + var text = SQLite3.ColumnString (stmt, index); + return new StringBuilder (text); + } + else if (clrType == typeof (UriBuilder)) { + var text = SQLite3.ColumnString (stmt, index); + return new UriBuilder (text); + } + else { + throw new NotSupportedException ("Don't know how to read " + clrType); + } + } + } + } - public TableMapping ( + internal class FastColumnSetter + { + /// + /// Gets a for a generic method, suppressing AOT warnings. + /// + /// The type of the destination object that the query will read into. + /// The generic instance. + /// This should only be called when is a reference type. #if NET8_0_OR_GREATER - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "This method is only ever called when 'mappedType' is a reference type.")] #endif - Type type, - CreateFlags createFlags = CreateFlags.None) - { - MappedType = type; - CreateFlags = createFlags; + internal static MethodInfo GetFastSetterMethodInfoUnsafe (Type mappedType) + { + return typeof (FastColumnSetter) + .GetMethod (nameof (GetFastSetter), + BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod (mappedType); + } -#if ENABLE_IL2CPP - var typeInfo = type.GetTypeInfo (); - var tableAttr = typeInfo.GetCustomAttribute (); -#elif NET8_0_OR_GREATER - var tableAttr = type.GetCustomAttributes ().FirstOrDefault (); -#else - var typeInfo = type.GetTypeInfo (); - var tableAttr = - typeInfo.CustomAttributes - .Where (x => x.AttributeType == typeof (TableAttribute)) - .Select (x => (TableAttribute)Orm.InflateAttribute (x)) - .FirstOrDefault (); -#endif + /// + /// Creates a delegate that can be used to quickly set object members from query columns. + /// + /// Note that this frontloads the slow reflection-based type checking for columns to only happen once at the beginning of a query, + /// and then afterwards each row of the query can invoke the delegate returned by this function to get much better performance (up to 10x speed boost, depending on query size and platform). + /// + /// The type of the destination object that the query will read into + /// The active connection. Note that this is primarily needed in order to read preferences regarding how certain data types (such as TimeSpan / DateTime) should be encoded in the database. + /// The table mapping used to map the statement column to a member of the destination object type + /// + /// A delegate for fast-setting of object members from statement columns. + /// + /// If no fast setter is available for the requested column (enums in particular cause headache), then this function returns null. + /// + internal static Action GetFastSetter (SQLiteConnection conn, TableMapping.Column column) + { + Action fastSetter = null; + + Type clrType = column.PropertyInfo.PropertyType; + + var clrTypeInfo = clrType.GetTypeInfo (); + if (clrTypeInfo.IsGenericType && clrTypeInfo.GetGenericTypeDefinition () == typeof (Nullable<>)) { + clrType = clrTypeInfo.GenericTypeArguments[0]; + clrTypeInfo = clrType.GetTypeInfo (); + } + + if (clrType == typeof (String)) { + fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { + return SQLite3.ColumnString (stmt, index); + }); + } + else if (clrType == typeof (Int32)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index)=>{ + return SQLite3.ColumnInt (stmt, index); + }); + } + else if (clrType == typeof (Boolean)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return SQLite3.ColumnInt (stmt, index) == 1; + }); + } + else if (clrType == typeof (double)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return SQLite3.ColumnDouble (stmt, index); + }); + } + else if (clrType == typeof (float)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return (float) SQLite3.ColumnDouble (stmt, index); + }); + } + else if (clrType == typeof (TimeSpan)) { + if (conn.StoreTimeSpanAsTicks) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return new TimeSpan (SQLite3.ColumnInt64 (stmt, index)); + }); + } + else { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + var text = SQLite3.ColumnString (stmt, index); + TimeSpan resultTime; + if (!TimeSpan.TryParseExact (text, "c", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.TimeSpanStyles.None, out resultTime)) { + resultTime = TimeSpan.Parse (text); + } + return resultTime; + }); + } + } + else if (clrType == typeof (DateTime)) { + if (conn.StoreDateTimeAsTicks) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return new DateTime (SQLite3.ColumnInt64 (stmt, index)); + }); + } + else { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + var text = SQLite3.ColumnString (stmt, index); + DateTime resultDate; + if (!DateTime.TryParseExact (text, conn.DateTimeStringFormat, System.Globalization.CultureInfo.InvariantCulture, conn.DateTimeStyle, out resultDate)) { + resultDate = DateTime.Parse (text); + } + return resultDate; + }); + } + } + else if (clrType == typeof (DateTimeOffset)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return new DateTimeOffset (SQLite3.ColumnInt64 (stmt, index), TimeSpan.Zero); + }); + } + else if (clrTypeInfo.IsEnum) { + // NOTE: Not sure of a good way (if any?) to do a strongly-typed fast setter like this for enumerated types -- for now, return null and column sets will revert back to the safe (but slow) Reflection-based method of column prop.Set() + } + else if (clrType == typeof (Int64)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return SQLite3.ColumnInt64 (stmt, index); + }); + } + else if (clrType == typeof(UInt64)) + { + fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { + return (ulong)SQLite3.ColumnInt64(stmt, index); + }); + } + else if (clrType == typeof (UInt32)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return (uint)SQLite3.ColumnInt64 (stmt, index); + }); + } + else if (clrType == typeof (decimal)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return (decimal)SQLite3.ColumnDouble (stmt, index); + }); + } + else if (clrType == typeof (Byte)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return (byte)SQLite3.ColumnInt (stmt, index); + }); + } + else if (clrType == typeof (UInt16)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return (ushort)SQLite3.ColumnInt (stmt, index); + }); + } + else if (clrType == typeof (Int16)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return (short)SQLite3.ColumnInt (stmt, index); + }); + } + else if (clrType == typeof (sbyte)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + return (sbyte)SQLite3.ColumnInt (stmt, index); + }); + } + else if (clrType == typeof (byte[])) { + fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { + return SQLite3.ColumnByteArray (stmt, index); + }); + } + else if (clrType == typeof (Guid)) { + fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { + var text = SQLite3.ColumnString (stmt, index); + return new Guid (text); + }); + } + else if (clrType == typeof (Uri)) { + fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { + var text = SQLite3.ColumnString (stmt, index); + return new Uri (text); + }); + } + else if (clrType == typeof (StringBuilder)) { + fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { + var text = SQLite3.ColumnString (stmt, index); + return new StringBuilder (text); + }); + } + else if (clrType == typeof (UriBuilder)) { + fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { + var text = SQLite3.ColumnString (stmt, index); + return new UriBuilder (text); + }); + } + else { + // NOTE: Will fall back to the slow setter method in the event that we are unable to create a fast setter delegate for a particular column type + } + return fastSetter; + } - TableName = (tableAttr != null && !string.IsNullOrEmpty (tableAttr.Name)) ? tableAttr.Name : MappedType.Name; - WithoutRowId = tableAttr != null ? tableAttr.WithoutRowId : false; - - var members = GetPublicMembers(type); - var cols = new List(members.Count); - foreach(var m in members) - { - var ignore = m.IsDefined(typeof(IgnoreAttribute), true); - if(!ignore) - cols.Add(new Column(m, createFlags)); - } - Columns = cols.ToArray (); - foreach (var c in Columns) { - if (c.IsAutoInc && c.IsPK) { - _autoPk = c; - } - if (c.IsPK) { - PK = c; - } - } - - HasAutoIncPK = _autoPk != null; - - if (PK != null) { - GetByPrimaryKeySql = string.Format ("select * from \"{0}\" where \"{1}\" = ?", TableName, PK.Name); - } - else { - // People should not be calling Get/Find without a PK - GetByPrimaryKeySql = string.Format ("select * from \"{0}\" limit 1", TableName); - } - - _insertColumns = Columns.Where (c => !c.IsAutoInc).ToArray (); - _insertOrReplaceColumns = Columns.ToArray (); - } - - private IReadOnlyCollection GetPublicMembers( + /// + /// This creates a strongly typed delegate that will permit fast setting of column values given a Sqlite3Statement and a column index. + /// + /// Note that this is identical to CreateTypedSetterDelegate(), but has an extra check to see if it should create a nullable version of the delegate. + /// + /// The type of the object whose member column is being set + /// The CLR type of the member in the object which corresponds to the given SQLite columnn + /// The column mapping that identifies the target member of the destination object + /// A lambda that can be used to retrieve the column value at query-time + /// A strongly-typed delegate + private static Action CreateNullableTypedSetterDelegate (TableMapping.Column column, Func getColumnValue) where ColumnMemberType : struct + { + var clrTypeInfo = column.PropertyInfo.PropertyType.GetTypeInfo(); + bool isNullable = false; + + if (clrTypeInfo.IsGenericType && clrTypeInfo.GetGenericTypeDefinition () == typeof (Nullable<>)) { + isNullable = true; + } + + if (isNullable) { + var setProperty = (Action)Delegate.CreateDelegate ( + typeof (Action), null, + column.PropertyInfo.GetSetMethod ()); + + return (o, stmt, i) => { + var colType = SQLite3.ColumnType (stmt, i); + if (colType != SQLite3.ColType.Null) + setProperty.Invoke ((ObjectType)o, getColumnValue.Invoke (stmt, i)); + }; + } + + return CreateTypedSetterDelegate (column, getColumnValue); + } + + /// + /// This creates a strongly typed delegate that will permit fast setting of column values given a Sqlite3Statement and a column index. + /// + /// The type of the object whose member column is being set + /// The CLR type of the member in the object which corresponds to the given SQLite columnn + /// The column mapping that identifies the target member of the destination object + /// A lambda that can be used to retrieve the column value at query-time + /// A strongly-typed delegate + private static Action CreateTypedSetterDelegate (TableMapping.Column column, Func getColumnValue) + { + var setProperty = (Action)Delegate.CreateDelegate ( + typeof (Action), null, + column.PropertyInfo.GetSetMethod ()); + + return (o, stmt, i) => { + var colType = SQLite3.ColumnType (stmt, i); + if (colType != SQLite3.ColType.Null) + setProperty.Invoke ((ObjectType)o, getColumnValue.Invoke (stmt, i)); + }; + } + } + + /// + /// Since the insert never changed, we only need to prepare once. + /// + class PreparedSqlLiteInsertCommand : IDisposable + { + bool Initialized; + + SQLiteConnection Connection; + + string CommandText; + + Sqlite3Statement Statement; + static readonly Sqlite3Statement NullStatement = default (Sqlite3Statement); + + public PreparedSqlLiteInsertCommand (SQLiteConnection conn, string commandText) + { + Connection = conn; + CommandText = commandText; + } + + public int ExecuteNonQuery (object[] source) + { + if (Initialized && Statement == NullStatement) { + throw new ObjectDisposedException (nameof (PreparedSqlLiteInsertCommand)); + } + + if (Connection.Trace) { + Connection.Tracer?.Invoke ("Executing: " + CommandText); + } + + var r = SQLite3.Result.OK; + + if (!Initialized) { + Statement = SQLite3.Prepare2 (Connection.Handle, CommandText); + Initialized = true; + } + + //bind the values. + if (source != null) { + for (int i = 0; i < source.Length; i++) { + SQLiteCommand.BindParameter (Statement, i + 1, source[i], Connection.StoreDateTimeAsTicks, Connection.DateTimeStringFormat, Connection.StoreTimeSpanAsTicks); + } + } + r = SQLite3.Step (Statement); + + if (r == SQLite3.Result.Done) { + int rowsAffected = SQLite3.Changes (Connection.Handle); + SQLite3.Reset (Statement); + return rowsAffected; + } + else if (r == SQLite3.Result.Error) { + string msg = SQLite3.GetErrmsg (Connection.Handle); + SQLite3.Reset (Statement); + throw SQLiteException.New (r, msg); + } + else if (r == SQLite3.Result.Constraint && SQLite3.ExtendedErrCode (Connection.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { + SQLite3.Reset (Statement); + throw NotNullConstraintViolationException.New (r, SQLite3.GetErrmsg (Connection.Handle)); + } + else { + SQLite3.Reset (Statement); + throw SQLiteException.New (r, SQLite3.GetErrmsg (Connection.Handle)); + } + } + + public void Dispose () + { + Dispose (true); + GC.SuppressFinalize (this); + } + + void Dispose (bool disposing) + { + var s = Statement; + Statement = NullStatement; + Connection = null; + if (s != NullStatement) { + SQLite3.Finalize (s); + } + } + + ~PreparedSqlLiteInsertCommand () + { + Dispose (false); + } + } + + public enum CreateTableResult + { + Created, + Migrated, + } + + public class CreateTablesResult + { + public Dictionary Results { get; private set; } + + public CreateTablesResult () + { + Results = new Dictionary (); + } + } + + public abstract class BaseTableQuery + { + protected class Ordering + { + public string ColumnName { get; set; } + public bool Ascending { get; set; } + } + } + + public class TableQuery< #if NET8_0_OR_GREATER - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif - Type type) - { - if(type.Name.StartsWith("ValueTuple`")) - return GetFieldsFromValueTuple(type); - - var members = new List(); - var memberNames = new HashSet(); - var newMembers = new List(); - do - { - var ti = type.GetTypeInfo(); - newMembers.Clear(); - - newMembers.AddRange( - from p in ti.DeclaredProperties - where !memberNames.Contains(p.Name) && - p.CanRead && p.CanWrite && - p.GetMethod != null && p.SetMethod != null && - p.GetMethod.IsPublic && p.SetMethod.IsPublic && - !p.GetMethod.IsStatic && !p.SetMethod.IsStatic - select p); - - members.AddRange(newMembers); - foreach(var m in newMembers) - memberNames.Add(m.Name); - - type = ti.BaseType; - } - while(type != typeof(object)); - - return members; - } - - private IReadOnlyCollection GetFieldsFromValueTuple( + T> : BaseTableQuery, IEnumerable + { + public SQLiteConnection Connection { get; private set; } + + public TableMapping Table { get; private set; } + + Expression _where; + List _orderBys; + int? _limit; + int? _offset; + + BaseTableQuery _joinInner; + Expression _joinInnerKeySelector; + BaseTableQuery _joinOuter; + Expression _joinOuterKeySelector; + Expression _joinSelector; + + Expression _selector; + + TableQuery (SQLiteConnection conn, TableMapping table) + { + Connection = conn; + Table = table; + } + + public TableQuery (SQLiteConnection conn) + { + Connection = conn; + Table = Connection.GetMapping (typeof (T)); + } + + public TableQuery Clone< #if NET8_0_OR_GREATER - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields)] + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] #endif - Type type) - { - Method = MapMethod.ByPosition; - var fields = type.GetFields(); + U> () + { + var q = new TableQuery (Connection, Table); + q._where = _where; + q._deferred = _deferred; + if (_orderBys != null) { + q._orderBys = new List (_orderBys); + } + q._limit = _limit; + q._offset = _offset; + q._joinInner = _joinInner; + q._joinInnerKeySelector = _joinInnerKeySelector; + q._joinOuter = _joinOuter; + q._joinOuterKeySelector = _joinOuterKeySelector; + q._joinSelector = _joinSelector; + q._selector = _selector; + return q; + } + + /// + /// Filters the query based on a predicate. + /// + public TableQuery Where (Expression> predExpr) + { + if (predExpr.NodeType == ExpressionType.Lambda) { + var lambda = (LambdaExpression)predExpr; + var pred = lambda.Body; + var q = Clone (); + q.AddWhere (pred); + return q; + } + else { + throw new NotSupportedException ("Must be a predicate"); + } + } + + /// + /// Delete all the rows that match this query. + /// + public int Delete () + { + return Delete (null); + } + + /// + /// Delete all the rows that match this query and the given predicate. + /// + public int Delete (Expression> predExpr) + { + if (_limit.HasValue || _offset.HasValue) + throw new InvalidOperationException ("Cannot delete with limits or offsets"); - // https://docs.microsoft.com/en-us/dotnet/api/system.valuetuple-8.rest - if(fields.Length >= 8) - throw new NotSupportedException("ValueTuple with more than 7 members not supported due to nesting; see https://docs.microsoft.com/en-us/dotnet/api/system.valuetuple-8.rest"); + if (_where == null && predExpr == null) + throw new InvalidOperationException ("No condition specified"); - return fields; - } + var pred = _where; - public bool HasAutoIncPK { get; private set; } + if (predExpr != null && predExpr.NodeType == ExpressionType.Lambda) { + var lambda = (LambdaExpression)predExpr; + pred = pred != null ? Expression.AndAlso (pred, lambda.Body) : lambda.Body; + } - public void SetAutoIncPK (object obj, long id) - { - if (_autoPk != null) { - _autoPk.SetValue (obj, Convert.ChangeType (id, _autoPk.ColumnType, null)); - } - } + var args = new List (); + var cmdText = "delete from \"" + Table.TableName + "\""; + var w = CompileExpr (pred, args); + cmdText += " where " + w.CommandText; - public Column[] InsertColumns { - get { - return _insertColumns; - } - } + var command = Connection.CreateCommand (cmdText, args.ToArray ()); - public Column[] InsertOrReplaceColumns { - get { - return _insertOrReplaceColumns; - } - } + int result = command.ExecuteNonQuery (); + return result; + } - public Column FindColumnWithPropertyName (string propertyName) - { - var exact = Columns.FirstOrDefault (c => c.PropertyName == propertyName); - return exact; - } + /// + /// Yields a given number of elements from the query and then skips the remainder. + /// + public TableQuery Take (int n) + { + var q = Clone (); + q._limit = n; + return q; + } - public Column FindColumn (string columnName) - { - if(Method != MapMethod.ByName) - throw new InvalidOperationException($"This {nameof(TableMapping)} is not mapped by name, but {Method}."); + /// + /// Skips a given number of elements from the query and then yields the remainder. + /// + public TableQuery Skip (int n) + { + var q = Clone (); + q._offset = n; + return q; + } - var exact = Columns.FirstOrDefault (c => c.Name.ToLower () == columnName.ToLower ()); - return exact; - } + /// + /// Returns the element at a given index + /// + public T ElementAt (int index) + { + return Skip (index).Take (1).First (); + } - public class Column - { - MemberInfo _member; + bool _deferred; + public TableQuery Deferred () + { + var q = Clone (); + q._deferred = true; + return q; + } - public string Name { get; private set; } + /// + /// Order the query results according to a key. + /// + public TableQuery OrderBy (Expression> orderExpr) + { + return AddOrderBy (orderExpr, true); + } - public PropertyInfo PropertyInfo => _member as PropertyInfo; + /// + /// Order the query results according to a key. + /// + public TableQuery OrderByDescending (Expression> orderExpr) + { + return AddOrderBy (orderExpr, false); + } - public string PropertyName { get { return _member.Name; } } + /// + /// Order the query results according to a key. + /// + public TableQuery ThenBy (Expression> orderExpr) + { + return AddOrderBy (orderExpr, true); + } - public Type ColumnType { get; private set; } + /// + /// Order the query results according to a key. + /// + public TableQuery ThenByDescending (Expression> orderExpr) + { + return AddOrderBy (orderExpr, false); + } - public string Collation { get; private set; } + TableQuery AddOrderBy (Expression> orderExpr, bool asc) + { + if (orderExpr.NodeType == ExpressionType.Lambda) { + var lambda = (LambdaExpression)orderExpr; + + MemberExpression mem = null; + + var unary = lambda.Body as UnaryExpression; + if (unary != null && unary.NodeType == ExpressionType.Convert) { + mem = unary.Operand as MemberExpression; + } + else { + mem = lambda.Body as MemberExpression; + } + + if (mem != null && (mem.Expression.NodeType == ExpressionType.Parameter)) { + var q = Clone (); + if (q._orderBys == null) { + q._orderBys = new List (); + } + q._orderBys.Add (new Ordering { + ColumnName = Table.FindColumnWithPropertyName (mem.Member.Name).Name, + Ascending = asc + }); + return q; + } + else { + throw new NotSupportedException ("Order By does not support: " + orderExpr); + } + } + else { + throw new NotSupportedException ("Must be a predicate"); + } + } - public bool IsAutoInc { get; private set; } - public bool IsAutoGuid { get; private set; } + private void AddWhere (Expression pred) + { + if (_where == null) { + _where = pred; + } + else { + _where = Expression.AndAlso (_where, pred); + } + } - public bool IsPK { get; private set; } + ///// + ///// Performs an inner join of two queries based on matching keys extracted from the elements. + ///// + //public TableQuery Join ( + // TableQuery inner, + // Expression> outerKeySelector, + // Expression> innerKeySelector, + // Expression> resultSelector) + //{ + // var q = new TableQuery (Connection, Connection.GetMapping (typeof (TResult))) { + // _joinOuter = this, + // _joinOuterKeySelector = outerKeySelector, + // _joinInner = inner, + // _joinInnerKeySelector = innerKeySelector, + // _joinSelector = resultSelector, + // }; + // return q; + //} + + // Not needed until Joins are supported + // Keeping this commented out forces the default Linq to objects processor to run + //public TableQuery Select (Expression> selector) + //{ + // var q = Clone (); + // q._selector = selector; + // return q; + //} + + private SQLiteCommand GenerateCommand (string selectionList) + { + if (_joinInner != null && _joinOuter != null) { + throw new NotSupportedException ("Joins are not supported."); + } + else { + var cmdText = "select " + selectionList + " from \"" + Table.TableName + "\""; + var args = new List (); + if (_where != null) { + var w = CompileExpr (_where, args); + cmdText += " where " + w.CommandText; + } + if ((_orderBys != null) && (_orderBys.Count > 0)) { + var t = string.Join (", ", _orderBys.Select (o => "\"" + o.ColumnName + "\"" + (o.Ascending ? "" : " desc")).ToArray ()); + cmdText += " order by " + t; + } + if (_limit.HasValue) { + cmdText += " limit " + _limit.Value; + } + if (_offset.HasValue) { + if (!_limit.HasValue) { + cmdText += " limit -1 "; + } + cmdText += " offset " + _offset.Value; + } + return Connection.CreateCommand (cmdText, args.ToArray ()); + } + } - public IEnumerable Indices { get; set; } + class CompileResult + { + public string CommandText { get; set; } - public bool IsNullable { get; private set; } + public object Value { get; set; } + } - public int? MaxStringLength { get; private set; } + private CompileResult CompileExpr (Expression expr, List queryArgs) + { + if (expr == null) { + throw new NotSupportedException ("Expression is NULL"); + } + else if (expr is BinaryExpression) { + var bin = (BinaryExpression)expr; + + // VB turns 'x=="foo"' into 'CompareString(x,"foo",true/false)==0', so we need to unwrap it + // http://blogs.msdn.com/b/vbteam/archive/2007/09/18/vb-expression-trees-string-comparisons.aspx + if (bin.Left.NodeType == ExpressionType.Call) { + var call = (MethodCallExpression)bin.Left; + if (call.Method.DeclaringType.FullName == "Microsoft.VisualBasic.CompilerServices.Operators" + && call.Method.Name == "CompareString") + bin = Expression.MakeBinary (bin.NodeType, call.Arguments[0], call.Arguments[1]); + } + + + var leftr = CompileExpr (bin.Left, queryArgs); + var rightr = CompileExpr (bin.Right, queryArgs); + + //If either side is a parameter and is null, then handle the other side specially (for "is null"/"is not null") + string text; + if (leftr.CommandText == "?" && leftr.Value == null) + text = CompileNullBinaryExpression (bin, rightr); + else if (rightr.CommandText == "?" && rightr.Value == null) + text = CompileNullBinaryExpression (bin, leftr); + else + text = "(" + leftr.CommandText + " " + GetSqlName (bin) + " " + rightr.CommandText + ")"; + return new CompileResult { CommandText = text }; + } + else if (expr.NodeType == ExpressionType.Not) { + var operandExpr = ((UnaryExpression)expr).Operand; + var opr = CompileExpr (operandExpr, queryArgs); + object val = opr.Value; + if (val is bool) + val = !((bool)val); + return new CompileResult { + CommandText = "NOT(" + opr.CommandText + ")", + Value = val + }; + } + else if (expr.NodeType == ExpressionType.Call) { + + var call = (MethodCallExpression)expr; + var args = new CompileResult[call.Arguments.Count]; + var obj = call.Object != null ? CompileExpr (call.Object, queryArgs) : null; + + for (var i = 0; i < args.Length; i++) { + args[i] = CompileExpr (call.Arguments[i], queryArgs); + } + + var sqlCall = ""; + + if (call.Method.Name == "Like" && args.Length == 2) { + sqlCall = "(" + args[0].CommandText + " like " + args[1].CommandText + ")"; + } + else if (call.Method.Name == "Contains" && args.Length == 2) { + sqlCall = "(" + args[1].CommandText + " in " + args[0].CommandText + ")"; + } + else if (call.Method.Name == "Contains" && args.Length == 1) { + if (call.Object != null && call.Object.Type == typeof (string)) { + sqlCall = "( instr(" + obj.CommandText + "," + args[0].CommandText + ") >0 )"; + } + else { + sqlCall = "(" + args[0].CommandText + " in " + obj.CommandText + ")"; + } + } + else if (call.Method.Name == "StartsWith" && args.Length >= 1) { + var startsWithCmpOp = StringComparison.CurrentCulture; + if (args.Length == 2) { + startsWithCmpOp = (StringComparison)args[1].Value; + } + switch (startsWithCmpOp) { + case StringComparison.Ordinal: + case StringComparison.CurrentCulture: + sqlCall = "( substr(" + obj.CommandText + ", 1, " + args[0].Value.ToString ().Length + ") = " + args[0].CommandText + ")"; + break; + case StringComparison.OrdinalIgnoreCase: + case StringComparison.CurrentCultureIgnoreCase: + sqlCall = "(" + obj.CommandText + " like (" + args[0].CommandText + " || '%'))"; + break; + } + + } + else if (call.Method.Name == "EndsWith" && args.Length >= 1) { + var endsWithCmpOp = StringComparison.CurrentCulture; + if (args.Length == 2) { + endsWithCmpOp = (StringComparison)args[1].Value; + } + switch (endsWithCmpOp) { + case StringComparison.Ordinal: + case StringComparison.CurrentCulture: + sqlCall = "( substr(" + obj.CommandText + ", length(" + obj.CommandText + ") - " + args[0].Value.ToString ().Length + "+1, " + args[0].Value.ToString ().Length + ") = " + args[0].CommandText + ")"; + break; + case StringComparison.OrdinalIgnoreCase: + case StringComparison.CurrentCultureIgnoreCase: + sqlCall = "(" + obj.CommandText + " like ('%' || " + args[0].CommandText + "))"; + break; + } + } + else if (call.Method.Name == "Equals" && args.Length == 1) { + sqlCall = "(" + obj.CommandText + " = (" + args[0].CommandText + "))"; + } + else if (call.Method.Name == "ToLower") { + sqlCall = "(lower(" + obj.CommandText + "))"; + } + else if (call.Method.Name == "ToUpper") { + sqlCall = "(upper(" + obj.CommandText + "))"; + } + else if (call.Method.Name == "Replace" && args.Length == 2) { + sqlCall = "(replace(" + obj.CommandText + "," + args[0].CommandText + "," + args[1].CommandText + "))"; + } + else if (call.Method.Name == "IsNullOrEmpty" && args.Length == 1) { + sqlCall = "(" + args[0].CommandText + " is null or" + args[0].CommandText + " ='' )"; + } + else { + sqlCall = call.Method.Name.ToLower () + "(" + string.Join (",", args.Select (a => a.CommandText).ToArray ()) + ")"; + } + return new CompileResult { CommandText = sqlCall }; + + } + else if (expr.NodeType == ExpressionType.Constant) { + var c = (ConstantExpression)expr; + queryArgs.Add (c.Value); + return new CompileResult { + CommandText = "?", + Value = c.Value + }; + } + else if (expr.NodeType == ExpressionType.Convert) { + var u = (UnaryExpression)expr; + var ty = u.Type; + var valr = CompileExpr (u.Operand, queryArgs); + return new CompileResult { + CommandText = valr.CommandText, + Value = valr.Value != null ? ConvertTo (valr.Value, ty) : null + }; + } + else if (expr.NodeType == ExpressionType.MemberAccess) { + var mem = (MemberExpression)expr; + + var paramExpr = mem.Expression as ParameterExpression; + if (paramExpr == null) { + var convert = mem.Expression as UnaryExpression; + if (convert != null && convert.NodeType == ExpressionType.Convert) { + paramExpr = convert.Operand as ParameterExpression; + } + } + + if (paramExpr != null) { + // + // This is a column of our table, output just the column name + // Need to translate it if that column name is mapped + // + var columnName = Table.FindColumnWithPropertyName (mem.Member.Name).Name; + return new CompileResult { CommandText = "\"" + columnName + "\"" }; + } + else { + object obj = null; + if (mem.Expression != null) { + var r = CompileExpr (mem.Expression, queryArgs); + if (r.Value == null) { + throw new NotSupportedException ("Member access failed to compile expression"); + } + if (r.CommandText == "?") { + queryArgs.RemoveAt (queryArgs.Count - 1); + } + obj = r.Value; + } + + // + // Get the member value + // + object val = null; + + if (mem.Member is PropertyInfo) { + var m = (PropertyInfo)mem.Member; + val = m.GetValue (obj, null); + } + else if (mem.Member is FieldInfo) { + var m = (FieldInfo)mem.Member; + val = m.GetValue (obj); + } + else { + throw new NotSupportedException ("MemberExpr: " + mem.Member.GetType ()); + } + + // + // Work special magic for enumerables + // + if (val != null && val is System.Collections.IEnumerable && !(val is string) && !(val is System.Collections.Generic.IEnumerable)) { + var sb = new System.Text.StringBuilder (); + sb.Append ("("); + var head = ""; + foreach (var a in (System.Collections.IEnumerable)val) { + queryArgs.Add (a); + sb.Append (head); + sb.Append ("?"); + head = ","; + } + sb.Append (")"); + return new CompileResult { + CommandText = sb.ToString (), + Value = val + }; + } + else { + queryArgs.Add (val); + return new CompileResult { + CommandText = "?", + Value = val + }; + } + } + } + throw new NotSupportedException ("Cannot compile: " + expr.NodeType.ToString ()); + } - public bool StoreAsText { get; private set; } + static object ConvertTo (object obj, Type t) + { + Type nut = Nullable.GetUnderlyingType (t); + + if (nut != null) { + if (obj == null) + return null; + return Convert.ChangeType (obj, nut); + } + else { + return Convert.ChangeType (obj, t); + } + } - public Column (MemberInfo member, CreateFlags createFlags = CreateFlags.None) - { - _member = member; - var memberType = GetMemberType(member); + /// + /// Compiles a BinaryExpression where one of the parameters is null. + /// + /// The expression to compile + /// The non-null parameter + private string CompileNullBinaryExpression (BinaryExpression expression, CompileResult parameter) + { + if (expression.NodeType == ExpressionType.Equal) + return "(" + parameter.CommandText + " is ?)"; + else if (expression.NodeType == ExpressionType.NotEqual) + return "(" + parameter.CommandText + " is not ?)"; + else if (expression.NodeType == ExpressionType.GreaterThan + || expression.NodeType == ExpressionType.GreaterThanOrEqual + || expression.NodeType == ExpressionType.LessThan + || expression.NodeType == ExpressionType.LessThanOrEqual) + return "(" + parameter.CommandText + " < ?)"; // always false + else + throw new NotSupportedException ("Cannot compile Null-BinaryExpression with type " + expression.NodeType.ToString ()); + } - var colAttr = member.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (ColumnAttribute)); -#if ENABLE_IL2CPP - var ca = member.GetCustomAttribute(typeof(ColumnAttribute)) as ColumnAttribute; - Name = ca == null ? member.Name : ca.Name; -#else - Name = (colAttr != null && colAttr.ConstructorArguments.Count > 0) ? - colAttr.ConstructorArguments[0].Value?.ToString () : - member.Name; -#endif - //If this type is Nullable then Nullable.GetUnderlyingType returns the T, otherwise it returns null, so get the actual type instead - ColumnType = Nullable.GetUnderlyingType (memberType) ?? memberType; - Collation = Orm.Collation (member); - - IsPK = Orm.IsPK (member) || - (((createFlags & CreateFlags.ImplicitPK) == CreateFlags.ImplicitPK) && - string.Compare (member.Name, Orm.ImplicitPkName, StringComparison.OrdinalIgnoreCase) == 0); - - var isAuto = Orm.IsAutoInc (member) || (IsPK && ((createFlags & CreateFlags.AutoIncPK) == CreateFlags.AutoIncPK)); - IsAutoGuid = isAuto && ColumnType == typeof (Guid); - IsAutoInc = isAuto && !IsAutoGuid; - - Indices = Orm.GetIndices (member); - if (!Indices.Any () - && !IsPK - && ((createFlags & CreateFlags.ImplicitIndex) == CreateFlags.ImplicitIndex) - && Name.EndsWith (Orm.ImplicitIndexSuffix, StringComparison.OrdinalIgnoreCase) - ) { - Indices = new IndexedAttribute[] { new IndexedAttribute () }; - } - IsNullable = !(IsPK || Orm.IsMarkedNotNull (member)); - MaxStringLength = Orm.MaxStringLength (member); - - StoreAsText = memberType.GetTypeInfo ().CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute)); - } - - public Column (PropertyInfo member, CreateFlags createFlags = CreateFlags.None) - : this((MemberInfo)member, createFlags) - { } - - public void SetValue (object obj, object val) - { - if(_member is PropertyInfo propy) - { - if (val != null && ColumnType.GetTypeInfo ().IsEnum) - propy.SetValue (obj, Enum.ToObject (ColumnType, val)); - else - propy.SetValue (obj, val); - } - else if(_member is FieldInfo field) - { - if (val != null && ColumnType.GetTypeInfo ().IsEnum) - field.SetValue (obj, Enum.ToObject (ColumnType, val)); - else - field.SetValue (obj, val); - } - else - throw new InvalidProgramException("unreachable condition"); - } - - public object GetValue (object obj) - { - if(_member is PropertyInfo propy) - return propy.GetValue(obj); - else if(_member is FieldInfo field) - return field.GetValue(obj); - else - throw new InvalidProgramException("unreachable condition"); - } - - private static Type GetMemberType(MemberInfo m) - { - switch(m.MemberType) - { - case MemberTypes.Property: return ((PropertyInfo)m).PropertyType; - case MemberTypes.Field: return ((FieldInfo)m).FieldType; - default: throw new InvalidProgramException($"{nameof(TableMapping)} supports properties or fields only."); - } - } - } - - internal enum MapMethod - { - ByName, - ByPosition - } - } - - class EnumCacheInfo - { - public EnumCacheInfo (Type type) - { - var typeInfo = type.GetTypeInfo (); - - IsEnum = typeInfo.IsEnum; - - if (IsEnum) { - StoreAsText = typeInfo.CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute)); - - if (StoreAsText) { - EnumValues = new Dictionary (); -#if NET8_0_OR_GREATER - foreach (object e in Enum.GetValuesAsUnderlyingType (type)) { - EnumValues[Convert.ToInt32 (e)] = Enum.ToObject(type, e).ToString (); - } -#else - foreach (object e in Enum.GetValues (type)) { - EnumValues[Convert.ToInt32 (e)] = e.ToString (); - } -#endif - } - } - } - - public bool IsEnum { get; private set; } - - public bool StoreAsText { get; private set; } - - public Dictionary EnumValues { get; private set; } - } - - static class EnumCache - { - static readonly Dictionary Cache = new Dictionary (); - - public static EnumCacheInfo GetInfo () - { - return GetInfo (typeof (T)); - } - - public static EnumCacheInfo GetInfo (Type type) - { - lock (Cache) { - EnumCacheInfo info = null; - if (!Cache.TryGetValue (type, out info)) { - info = new EnumCacheInfo (type); - Cache[type] = info; - } - - return info; - } - } - } - - public static class Orm - { - public const int DefaultMaxStringLength = 140; - public const string ImplicitPkName = "Id"; - public const string ImplicitIndexSuffix = "Id"; - - public static Type GetType (object obj) - { - if (obj == null) - return typeof (object); - var rt = obj as IReflectableType; - if (rt != null) - return rt.GetTypeInfo ().AsType (); - return obj.GetType (); - } - - public static string SqlDecl (TableMapping.Column p, bool storeDateTimeAsTicks, bool storeTimeSpanAsTicks) - { - string decl = "\"" + p.Name + "\" " + SqlType (p, storeDateTimeAsTicks, storeTimeSpanAsTicks) + " "; - - if (p.IsPK) { - decl += "primary key "; - } - if (p.IsAutoInc) { - decl += "autoincrement "; - } - if (!p.IsNullable) { - decl += "not null "; - } - if (!string.IsNullOrEmpty (p.Collation)) { - decl += "collate " + p.Collation + " "; - } - - return decl; - } - - public static string SqlType (TableMapping.Column p, bool storeDateTimeAsTicks, bool storeTimeSpanAsTicks) - { - var clrType = p.ColumnType; - if (clrType == typeof (Boolean) || clrType == typeof (Byte) || clrType == typeof (UInt16) || clrType == typeof (SByte) || clrType == typeof (Int16) || clrType == typeof (Int32) || clrType == typeof (UInt32) || clrType == typeof (Int64) || clrType == typeof (UInt64)) { - return "integer"; - } - else if (clrType == typeof (Single) || clrType == typeof (Double) || clrType == typeof (Decimal)) { - return "float"; - } - else if (clrType == typeof (String) || clrType == typeof (StringBuilder) || clrType == typeof (Uri) || clrType == typeof (UriBuilder)) { - int? len = p.MaxStringLength; - - if (len.HasValue) - return "varchar(" + len.Value + ")"; - - return "varchar"; - } - else if (clrType == typeof (TimeSpan)) { - return storeTimeSpanAsTicks ? "bigint" : "time"; - } - else if (clrType == typeof (DateTime)) { - return storeDateTimeAsTicks ? "bigint" : "datetime"; - } - else if (clrType == typeof (DateTimeOffset)) { - return "bigint"; - } - else if (clrType.GetTypeInfo ().IsEnum) { - if (p.StoreAsText) - return "varchar"; - else - return "integer"; - } - else if (clrType == typeof (byte[])) { - return "blob"; - } - else if (clrType == typeof (Guid)) { - return "varchar(36)"; - } - else { - throw new NotSupportedException ("Don't know about " + clrType); - } - } - - public static bool IsPK (MemberInfo p) - { - return p.CustomAttributes.Any (x => x.AttributeType == typeof (PrimaryKeyAttribute)); - } - - public static string Collation (MemberInfo p) - { -#if ENABLE_IL2CPP - return (p.GetCustomAttribute ()?.Value) ?? ""; -#else - return - (p.CustomAttributes - .Where (x => typeof (CollationAttribute) == x.AttributeType) - .Select (x => { - var args = x.ConstructorArguments; - return args.Count > 0 ? ((args[0].Value as string) ?? "") : ""; - }) - .FirstOrDefault ()) ?? ""; -#endif - } + string GetSqlName (Expression expr) + { + var n = expr.NodeType; + if (n == ExpressionType.GreaterThan) + return ">"; + else if (n == ExpressionType.GreaterThanOrEqual) { + return ">="; + } + else if (n == ExpressionType.LessThan) { + return "<"; + } + else if (n == ExpressionType.LessThanOrEqual) { + return "<="; + } + else if (n == ExpressionType.And) { + return "&"; + } + else if (n == ExpressionType.AndAlso) { + return "and"; + } + else if (n == ExpressionType.Or) { + return "|"; + } + else if (n == ExpressionType.OrElse) { + return "or"; + } + else if (n == ExpressionType.Equal) { + return "="; + } + else if (n == ExpressionType.NotEqual) { + return "!="; + } + else { + throw new NotSupportedException ("Cannot get SQL for: " + n); + } + } - public static bool IsAutoInc (MemberInfo p) - { - return p.CustomAttributes.Any (x => x.AttributeType == typeof (AutoIncrementAttribute)); - } + /// + /// Execute SELECT COUNT(*) on the query + /// + public int Count () + { + return GenerateCommand ("count(*)").ExecuteScalar (); + } - public static FieldInfo GetField ( -#if NET8_0_OR_GREATER - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] -#endif - TypeInfo t, - string name) - { - var f = t.GetDeclaredField (name); - if (f != null) - return f; - return GetField (t.BaseType.GetTypeInfo (), name); - } + /// + /// Execute SELECT COUNT(*) on the query with an additional WHERE clause. + /// + public int Count (Expression> predExpr) + { + return Where (predExpr).Count (); + } - public static PropertyInfo GetProperty ( -#if NET8_0_OR_GREATER - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] -#endif - TypeInfo t, - string name) - { - var f = t.GetDeclaredProperty (name); - if (f != null) - return f; - return GetProperty (t.BaseType.GetTypeInfo (), name); - } + public IEnumerator GetEnumerator () + { + if (!_deferred) + return GenerateCommand ("*").ExecuteQuery ().GetEnumerator (); -#if !NET8_0_OR_GREATER - public static object InflateAttribute (CustomAttributeData x) - { - var atype = x.AttributeType; - var typeInfo = atype.GetTypeInfo (); -#if ENABLE_IL2CPP - var r = Activator.CreateInstance (x.AttributeType); -#else - var args = x.ConstructorArguments.Select (a => a.Value).ToArray (); - var r = Activator.CreateInstance (x.AttributeType, args); - foreach (var arg in x.NamedArguments) { - if (arg.IsField) { - GetField (typeInfo, arg.MemberName).SetValue (r, arg.TypedValue.Value); - } - else { - GetProperty (typeInfo, arg.MemberName).SetValue (r, arg.TypedValue.Value); - } - } -#endif - return r; - } -#endif + return GenerateCommand ("*").ExecuteDeferredQuery ().GetEnumerator (); + } - public static IEnumerable GetIndices (MemberInfo p) - { -#if ENABLE_IL2CPP || NET8_0_OR_GREATER - return p.GetCustomAttributes (); -#else - var indexedInfo = typeof (IndexedAttribute).GetTypeInfo (); - return - p.CustomAttributes - .Where (x => indexedInfo.IsAssignableFrom (x.AttributeType.GetTypeInfo ())) - .Select (x => (IndexedAttribute)InflateAttribute (x)); -#endif - } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } - public static int? MaxStringLength (MemberInfo p) - { -#if ENABLE_IL2CPP - return p.GetCustomAttribute ()?.Value; -#elif NET8_0_OR_GREATER - return p.GetCustomAttributes ().FirstOrDefault ()?.Value; -#else - var attr = p.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (MaxLengthAttribute)); - if (attr != null) { - var attrv = (MaxLengthAttribute)InflateAttribute (attr); - return attrv.Value; - } - return null; -#endif + /// + /// Queries the database and returns the results as a List. + /// + public List ToList () + { + return GenerateCommand ("*").ExecuteQuery (); } - public static int? MaxStringLength (PropertyInfo p) => MaxStringLength((MemberInfo)p); - - public static bool IsMarkedNotNull (MemberInfo p) - { - return p.CustomAttributes.Any (x => x.AttributeType == typeof (NotNullAttribute)); - } - } - - public partial class SQLiteCommand - { - SQLiteConnection _conn; - private List _bindings; - - public string CommandText { get; set; } - - public SQLiteCommand (SQLiteConnection conn) - { - _conn = conn; - _bindings = new List (); - CommandText = ""; - } - - public int ExecuteNonQuery () - { - if (_conn.Trace) { - _conn.Tracer?.Invoke ("Executing: " + this); - } - - var r = SQLite3.Result.OK; - var stmt = Prepare (); - r = SQLite3.Step (stmt); - Finalize (stmt); - if (r == SQLite3.Result.Done) { - int rowsAffected = SQLite3.Changes (_conn.Handle); - return rowsAffected; - } - else if (r == SQLite3.Result.Error) { - string msg = SQLite3.GetErrmsg (_conn.Handle); - throw SQLiteException.New (r, msg); - } - else if (r == SQLite3.Result.Constraint) { - if (SQLite3.ExtendedErrCode (_conn.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { - throw NotNullConstraintViolationException.New (r, SQLite3.GetErrmsg (_conn.Handle)); - } - } - - throw SQLiteException.New (r, SQLite3.GetErrmsg (_conn.Handle)); - } - - public IEnumerable ExecuteDeferredQuery< -#if NET8_0_OR_GREATER - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] -#endif - T> () - { - return ExecuteDeferredQuery (_conn.GetMapping (typeof (T))); - } + /// + /// Queries the database and returns the results as an array. + /// + public T[] ToArray () + { + return GenerateCommand ("*").ExecuteQuery ().ToArray (); + } - public List ExecuteQuery< -#if NET8_0_OR_GREATER - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] -#endif - T> () - { - return ExecuteDeferredQuery (_conn.GetMapping (typeof (T))).ToList (); - } - - public List ExecuteQuery (TableMapping map) - { - return ExecuteDeferredQuery (map).ToList (); - } - - /// - /// Invoked every time an instance is loaded from the database. - /// - /// - /// The newly created object. - /// - /// - /// This can be overridden in combination with the - /// method to hook into the life-cycle of objects. - /// - protected virtual void OnInstanceCreated (object obj) - { - // Can be overridden. - } - - public IEnumerable ExecuteDeferredQuery (TableMapping map) - { - if (_conn.Trace) { - _conn.Tracer?.Invoke ("Executing Query: " + this); - } - - var stmt = Prepare (); - try { - var cols = new TableMapping.Column[SQLite3.ColumnCount (stmt)]; - var fastColumnSetters = new Action[SQLite3.ColumnCount (stmt)]; - - if (map.Method == TableMapping.MapMethod.ByPosition) - { - Array.Copy(map.Columns, cols, Math.Min(cols.Length, map.Columns.Length)); - } - else if (map.Method == TableMapping.MapMethod.ByName) { - MethodInfo getSetter = null; - if (typeof(T) != map.MappedType) { -#if NET8_0_OR_GREATER - if (!RuntimeFeature.IsDynamicCodeSupported) { - if (map.MappedType.IsValueType) { - throw new NotSupportedException ( - $"Executing a query with a value type mapped type is not supported in AOT environments (The mapped type is '{map.MappedType}')."); - } - } -#endif - getSetter = FastColumnSetter.GetFastSetterMethodInfoUnsafe (map.MappedType); - } - - for (int i = 0; i < cols.Length; i++) { - var name = SQLite3.ColumnName16 (stmt, i); - cols[i] = map.FindColumn (name); - if (cols[i] != null) - if (getSetter != null) { - fastColumnSetters[i] = (Action)getSetter.Invoke(null, new object[]{ _conn, cols[i]}); - } - else { - fastColumnSetters[i] = FastColumnSetter.GetFastSetter(_conn, cols[i]); - } - } - } - - while (SQLite3.Step (stmt) == SQLite3.Result.Row) { - var obj = Activator.CreateInstance (map.MappedType); - for (int i = 0; i < cols.Length; i++) { - if (cols[i] == null) - continue; - - if (fastColumnSetters[i] != null) { - fastColumnSetters[i].Invoke (obj, stmt, i); - } - else { - var colType = SQLite3.ColumnType (stmt, i); - var val = ReadCol (stmt, i, colType, cols[i].ColumnType); - cols[i].SetValue (obj, val); - } - } - OnInstanceCreated (obj); - yield return (T)obj; - } - } - finally { - SQLite3.Finalize (stmt); - } - } - - public T ExecuteScalar () - { - if (_conn.Trace) { - _conn.Tracer?.Invoke ("Executing Query: " + this); - } - - T val = default (T); - - var stmt = Prepare (); - - try { - var r = SQLite3.Step (stmt); - if (r == SQLite3.Result.Row) { - var colType = SQLite3.ColumnType (stmt, 0); - var colval = ReadCol (stmt, 0, colType, typeof (T)); - if (colval != null) { - val = (T)colval; - } - } - else if (r == SQLite3.Result.Done) { - } - else { - throw SQLiteException.New (r, SQLite3.GetErrmsg (_conn.Handle)); - } - } - finally { - Finalize (stmt); - } - - return val; - } - - public IEnumerable ExecuteQueryScalars () - { - if (_conn.Trace) { - _conn.Tracer?.Invoke ("Executing Query: " + this); - } - var stmt = Prepare (); - try { - if (SQLite3.ColumnCount (stmt) < 1) { - throw new InvalidOperationException ("QueryScalars should return at least one column"); - } - while (SQLite3.Step (stmt) == SQLite3.Result.Row) { - var colType = SQLite3.ColumnType (stmt, 0); - var val = ReadCol (stmt, 0, colType, typeof (T)); - if (val == null) { - yield return default (T); - } - else { - yield return (T)val; - } - } - } - finally { - Finalize (stmt); - } - } - - public void Bind (string name, object val) - { - _bindings.Add (new Binding { - Name = name, - Value = val - }); - } - - public void Bind (object val) - { - Bind (null, val); - } - - public override string ToString () - { - var parts = new string[1 + _bindings.Count]; - parts[0] = CommandText; - var i = 1; - foreach (var b in _bindings) { - parts[i] = string.Format (" {0}: {1}", i - 1, b.Value); - i++; - } - return string.Join (Environment.NewLine, parts); - } - - Sqlite3Statement Prepare () - { - var stmt = SQLite3.Prepare2 (_conn.Handle, CommandText); - BindAll (stmt); - return stmt; - } - - void Finalize (Sqlite3Statement stmt) - { - SQLite3.Finalize (stmt); - } - - void BindAll (Sqlite3Statement stmt) - { - int nextIdx = 1; - foreach (var b in _bindings) { - if (b.Name != null) { - b.Index = SQLite3.BindParameterIndex (stmt, b.Name); - } - else { - b.Index = nextIdx++; - } - - BindParameter (stmt, b.Index, b.Value, _conn.StoreDateTimeAsTicks, _conn.DateTimeStringFormat, _conn.StoreTimeSpanAsTicks); - } - } - - static IntPtr NegativePointer = new IntPtr (-1); - - internal static void BindParameter (Sqlite3Statement stmt, int index, object value, bool storeDateTimeAsTicks, string dateTimeStringFormat, bool storeTimeSpanAsTicks) - { - if (value == null) { - SQLite3.BindNull (stmt, index); - } - else { - if (value is Int32) { - SQLite3.BindInt (stmt, index, (int)value); - } - else if (value is String) { - SQLite3.BindText (stmt, index, (string)value, -1, NegativePointer); - } - else if (value is Byte || value is UInt16 || value is SByte || value is Int16) { - SQLite3.BindInt (stmt, index, Convert.ToInt32 (value)); - } - else if (value is Boolean) { - SQLite3.BindInt (stmt, index, (bool)value ? 1 : 0); - } - else if (value is UInt32 || value is Int64 || value is UInt64) { - SQLite3.BindInt64 (stmt, index, Convert.ToInt64 (value)); - } - else if (value is Single || value is Double || value is Decimal) { - SQLite3.BindDouble (stmt, index, Convert.ToDouble (value)); - } - else if (value is TimeSpan) { - if (storeTimeSpanAsTicks) { - SQLite3.BindInt64 (stmt, index, ((TimeSpan)value).Ticks); - } - else { - SQLite3.BindText (stmt, index, ((TimeSpan)value).ToString (), -1, NegativePointer); - } - } - else if (value is DateTime) { - if (storeDateTimeAsTicks) { - SQLite3.BindInt64 (stmt, index, ((DateTime)value).Ticks); - } - else { - SQLite3.BindText (stmt, index, ((DateTime)value).ToString (dateTimeStringFormat, System.Globalization.CultureInfo.InvariantCulture), -1, NegativePointer); - } - } - else if (value is DateTimeOffset) { - SQLite3.BindInt64 (stmt, index, ((DateTimeOffset)value).UtcTicks); - } - else if (value is byte[]) { - SQLite3.BindBlob (stmt, index, (byte[])value, ((byte[])value).Length, NegativePointer); - } - else if (value is Guid) { - SQLite3.BindText (stmt, index, ((Guid)value).ToString (), 72, NegativePointer); - } - else if (value is Uri) { - SQLite3.BindText (stmt, index, ((Uri)value).ToString (), -1, NegativePointer); - } - else if (value is StringBuilder) { - SQLite3.BindText (stmt, index, ((StringBuilder)value).ToString (), -1, NegativePointer); - } - else if (value is UriBuilder) { - SQLite3.BindText (stmt, index, ((UriBuilder)value).ToString (), -1, NegativePointer); - } - else { - // Now we could possibly get an enum, retrieve cached info - var valueType = value.GetType (); - var enumInfo = EnumCache.GetInfo (valueType); - if (enumInfo.IsEnum) { - var enumIntValue = Convert.ToInt32 (value); - if (enumInfo.StoreAsText) - SQLite3.BindText (stmt, index, enumInfo.EnumValues[enumIntValue], -1, NegativePointer); - else - SQLite3.BindInt (stmt, index, enumIntValue); - } - else { - throw new NotSupportedException ("Cannot store type: " + Orm.GetType (value)); - } - } - } - } - - class Binding - { - public string Name { get; set; } - - public object Value { get; set; } - - public int Index { get; set; } - } - - object ReadCol (Sqlite3Statement stmt, int index, SQLite3.ColType type, Type clrType) - { - if (type == SQLite3.ColType.Null) { - return null; - } - else { - var clrTypeInfo = clrType.GetTypeInfo (); - if (clrTypeInfo.IsGenericType && clrTypeInfo.GetGenericTypeDefinition () == typeof (Nullable<>)) { - clrType = clrTypeInfo.GenericTypeArguments[0]; - clrTypeInfo = clrType.GetTypeInfo (); - } - - if (clrType == typeof (String)) { - return SQLite3.ColumnString (stmt, index); - } - else if (clrType == typeof (Int32)) { - return (int)SQLite3.ColumnInt (stmt, index); - } - else if (clrType == typeof (Boolean)) { - return SQLite3.ColumnInt (stmt, index) == 1; - } - else if (clrType == typeof (double)) { - return SQLite3.ColumnDouble (stmt, index); - } - else if (clrType == typeof (float)) { - return (float)SQLite3.ColumnDouble (stmt, index); - } - else if (clrType == typeof (TimeSpan)) { - if (_conn.StoreTimeSpanAsTicks) { - return new TimeSpan (SQLite3.ColumnInt64 (stmt, index)); - } - else { - var text = SQLite3.ColumnString (stmt, index); - TimeSpan resultTime; - if (!TimeSpan.TryParseExact (text, "c", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.TimeSpanStyles.None, out resultTime)) { - resultTime = TimeSpan.Parse (text); - } - return resultTime; - } - } - else if (clrType == typeof (DateTime)) { - if (_conn.StoreDateTimeAsTicks) { - return new DateTime (SQLite3.ColumnInt64 (stmt, index)); - } - else { - var text = SQLite3.ColumnString (stmt, index); - DateTime resultDate; - if (!DateTime.TryParseExact (text, _conn.DateTimeStringFormat, System.Globalization.CultureInfo.InvariantCulture, _conn.DateTimeStyle, out resultDate)) { - resultDate = DateTime.Parse (text); - } - return resultDate; - } - } - else if (clrType == typeof (DateTimeOffset)) { - return new DateTimeOffset (SQLite3.ColumnInt64 (stmt, index), TimeSpan.Zero); - } - else if (clrTypeInfo.IsEnum) { - if (type == SQLite3.ColType.Text) { - var value = SQLite3.ColumnString (stmt, index); - return Enum.Parse (clrType, value.ToString (), true); - } - else - return SQLite3.ColumnInt (stmt, index); - } - else if (clrType == typeof (Int64)) { - return SQLite3.ColumnInt64 (stmt, index); - } - else if (clrType == typeof (UInt64)) { - return (ulong)SQLite3.ColumnInt64 (stmt, index); - } - else if (clrType == typeof (UInt32)) { - return (uint)SQLite3.ColumnInt64 (stmt, index); - } - else if (clrType == typeof (decimal)) { - return (decimal)SQLite3.ColumnDouble (stmt, index); - } - else if (clrType == typeof (Byte)) { - return (byte)SQLite3.ColumnInt (stmt, index); - } - else if (clrType == typeof (UInt16)) { - return (ushort)SQLite3.ColumnInt (stmt, index); - } - else if (clrType == typeof (Int16)) { - return (short)SQLite3.ColumnInt (stmt, index); - } - else if (clrType == typeof (sbyte)) { - return (sbyte)SQLite3.ColumnInt (stmt, index); - } - else if (clrType == typeof (byte[])) { - return SQLite3.ColumnByteArray (stmt, index); - } - else if (clrType == typeof (Guid)) { - var text = SQLite3.ColumnString (stmt, index); - return new Guid (text); - } - else if (clrType == typeof (Uri)) { - var text = SQLite3.ColumnString (stmt, index); - return new Uri (text); - } - else if (clrType == typeof (StringBuilder)) { - var text = SQLite3.ColumnString (stmt, index); - return new StringBuilder (text); - } - else if (clrType == typeof (UriBuilder)) { - var text = SQLite3.ColumnString (stmt, index); - return new UriBuilder (text); - } - else { - throw new NotSupportedException ("Don't know how to read " + clrType); - } - } - } - } - - internal class FastColumnSetter - { /// - /// Gets a for a generic method, suppressing AOT warnings. + /// Returns the first element of this query. /// - /// The type of the destination object that the query will read into. - /// The generic instance. - /// This should only be called when is a reference type. -#if NET8_0_OR_GREATER - [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "This method is only ever called when 'mappedType' is a reference type.")] -#endif - internal static MethodInfo GetFastSetterMethodInfoUnsafe (Type mappedType) + public T First () { - return typeof (FastColumnSetter) - .GetMethod (nameof (GetFastSetter), - BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod (mappedType); + var query = Take (1); + return query.ToList ().First (); } /// - /// Creates a delegate that can be used to quickly set object members from query columns. - /// - /// Note that this frontloads the slow reflection-based type checking for columns to only happen once at the beginning of a query, - /// and then afterwards each row of the query can invoke the delegate returned by this function to get much better performance (up to 10x speed boost, depending on query size and platform). + /// Returns the first element of this query, or null if no element is found. /// - /// The type of the destination object that the query will read into - /// The active connection. Note that this is primarily needed in order to read preferences regarding how certain data types (such as TimeSpan / DateTime) should be encoded in the database. - /// The table mapping used to map the statement column to a member of the destination object type - /// - /// A delegate for fast-setting of object members from statement columns. - /// - /// If no fast setter is available for the requested column (enums in particular cause headache), then this function returns null. - /// - internal static Action GetFastSetter (SQLiteConnection conn, TableMapping.Column column) - { - Action fastSetter = null; - - Type clrType = column.PropertyInfo.PropertyType; - - var clrTypeInfo = clrType.GetTypeInfo (); - if (clrTypeInfo.IsGenericType && clrTypeInfo.GetGenericTypeDefinition () == typeof (Nullable<>)) { - clrType = clrTypeInfo.GenericTypeArguments[0]; - clrTypeInfo = clrType.GetTypeInfo (); - } - - if (clrType == typeof (String)) { - fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { - return SQLite3.ColumnString (stmt, index); - }); - } - else if (clrType == typeof (Int32)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index)=>{ - return SQLite3.ColumnInt (stmt, index); - }); - } - else if (clrType == typeof (Boolean)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return SQLite3.ColumnInt (stmt, index) == 1; - }); - } - else if (clrType == typeof (double)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return SQLite3.ColumnDouble (stmt, index); - }); - } - else if (clrType == typeof (float)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return (float) SQLite3.ColumnDouble (stmt, index); - }); - } - else if (clrType == typeof (TimeSpan)) { - if (conn.StoreTimeSpanAsTicks) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return new TimeSpan (SQLite3.ColumnInt64 (stmt, index)); - }); - } - else { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - var text = SQLite3.ColumnString (stmt, index); - TimeSpan resultTime; - if (!TimeSpan.TryParseExact (text, "c", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.TimeSpanStyles.None, out resultTime)) { - resultTime = TimeSpan.Parse (text); - } - return resultTime; - }); - } - } - else if (clrType == typeof (DateTime)) { - if (conn.StoreDateTimeAsTicks) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return new DateTime (SQLite3.ColumnInt64 (stmt, index)); - }); - } - else { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - var text = SQLite3.ColumnString (stmt, index); - DateTime resultDate; - if (!DateTime.TryParseExact (text, conn.DateTimeStringFormat, System.Globalization.CultureInfo.InvariantCulture, conn.DateTimeStyle, out resultDate)) { - resultDate = DateTime.Parse (text); - } - return resultDate; - }); - } - } - else if (clrType == typeof (DateTimeOffset)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return new DateTimeOffset (SQLite3.ColumnInt64 (stmt, index), TimeSpan.Zero); - }); - } - else if (clrTypeInfo.IsEnum) { - // NOTE: Not sure of a good way (if any?) to do a strongly-typed fast setter like this for enumerated types -- for now, return null and column sets will revert back to the safe (but slow) Reflection-based method of column prop.Set() - } - else if (clrType == typeof (Int64)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return SQLite3.ColumnInt64 (stmt, index); - }); - } - else if (clrType == typeof(UInt64)) - { - fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { - return (ulong)SQLite3.ColumnInt64(stmt, index); - }); - } - else if (clrType == typeof (UInt32)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return (uint)SQLite3.ColumnInt64 (stmt, index); - }); - } - else if (clrType == typeof (decimal)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return (decimal)SQLite3.ColumnDouble (stmt, index); - }); - } - else if (clrType == typeof (Byte)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return (byte)SQLite3.ColumnInt (stmt, index); - }); - } - else if (clrType == typeof (UInt16)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return (ushort)SQLite3.ColumnInt (stmt, index); - }); - } - else if (clrType == typeof (Int16)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return (short)SQLite3.ColumnInt (stmt, index); - }); - } - else if (clrType == typeof (sbyte)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - return (sbyte)SQLite3.ColumnInt (stmt, index); - }); - } - else if (clrType == typeof (byte[])) { - fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { - return SQLite3.ColumnByteArray (stmt, index); - }); - } - else if (clrType == typeof (Guid)) { - fastSetter = CreateNullableTypedSetterDelegate (column, (stmt, index) => { - var text = SQLite3.ColumnString (stmt, index); - return new Guid (text); - }); - } - else if (clrType == typeof (Uri)) { - fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { - var text = SQLite3.ColumnString (stmt, index); - return new Uri (text); - }); - } - else if (clrType == typeof (StringBuilder)) { - fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { - var text = SQLite3.ColumnString (stmt, index); - return new StringBuilder (text); - }); - } - else if (clrType == typeof (UriBuilder)) { - fastSetter = CreateTypedSetterDelegate (column, (stmt, index) => { - var text = SQLite3.ColumnString (stmt, index); - return new UriBuilder (text); - }); - } - else { - // NOTE: Will fall back to the slow setter method in the event that we are unable to create a fast setter delegate for a particular column type - } - return fastSetter; - } - - /// - /// This creates a strongly typed delegate that will permit fast setting of column values given a Sqlite3Statement and a column index. - /// - /// Note that this is identical to CreateTypedSetterDelegate(), but has an extra check to see if it should create a nullable version of the delegate. - /// - /// The type of the object whose member column is being set - /// The CLR type of the member in the object which corresponds to the given SQLite columnn - /// The column mapping that identifies the target member of the destination object - /// A lambda that can be used to retrieve the column value at query-time - /// A strongly-typed delegate - private static Action CreateNullableTypedSetterDelegate (TableMapping.Column column, Func getColumnValue) where ColumnMemberType : struct - { - var clrTypeInfo = column.PropertyInfo.PropertyType.GetTypeInfo(); - bool isNullable = false; - - if (clrTypeInfo.IsGenericType && clrTypeInfo.GetGenericTypeDefinition () == typeof (Nullable<>)) { - isNullable = true; - } - - if (isNullable) { - var setProperty = (Action)Delegate.CreateDelegate ( - typeof (Action), null, - column.PropertyInfo.GetSetMethod ()); - - return (o, stmt, i) => { - var colType = SQLite3.ColumnType (stmt, i); - if (colType != SQLite3.ColType.Null) - setProperty.Invoke ((ObjectType)o, getColumnValue.Invoke (stmt, i)); - }; - } - - return CreateTypedSetterDelegate (column, getColumnValue); - } - - /// - /// This creates a strongly typed delegate that will permit fast setting of column values given a Sqlite3Statement and a column index. - /// - /// The type of the object whose member column is being set - /// The CLR type of the member in the object which corresponds to the given SQLite columnn - /// The column mapping that identifies the target member of the destination object - /// A lambda that can be used to retrieve the column value at query-time - /// A strongly-typed delegate - private static Action CreateTypedSetterDelegate (TableMapping.Column column, Func getColumnValue) - { - var setProperty = (Action)Delegate.CreateDelegate ( - typeof (Action), null, - column.PropertyInfo.GetSetMethod ()); - - return (o, stmt, i) => { - var colType = SQLite3.ColumnType (stmt, i); - if (colType != SQLite3.ColType.Null) - setProperty.Invoke ((ObjectType)o, getColumnValue.Invoke (stmt, i)); - }; - } - } - - /// - /// Since the insert never changed, we only need to prepare once. - /// - class PreparedSqlLiteInsertCommand : IDisposable - { - bool Initialized; - - SQLiteConnection Connection; - - string CommandText; - - Sqlite3Statement Statement; - static readonly Sqlite3Statement NullStatement = default (Sqlite3Statement); - - public PreparedSqlLiteInsertCommand (SQLiteConnection conn, string commandText) - { - Connection = conn; - CommandText = commandText; - } - - public int ExecuteNonQuery (object[] source) - { - if (Initialized && Statement == NullStatement) { - throw new ObjectDisposedException (nameof (PreparedSqlLiteInsertCommand)); - } - - if (Connection.Trace) { - Connection.Tracer?.Invoke ("Executing: " + CommandText); - } - - var r = SQLite3.Result.OK; - - if (!Initialized) { - Statement = SQLite3.Prepare2 (Connection.Handle, CommandText); - Initialized = true; - } - - //bind the values. - if (source != null) { - for (int i = 0; i < source.Length; i++) { - SQLiteCommand.BindParameter (Statement, i + 1, source[i], Connection.StoreDateTimeAsTicks, Connection.DateTimeStringFormat, Connection.StoreTimeSpanAsTicks); - } - } - r = SQLite3.Step (Statement); - - if (r == SQLite3.Result.Done) { - int rowsAffected = SQLite3.Changes (Connection.Handle); - SQLite3.Reset (Statement); - return rowsAffected; - } - else if (r == SQLite3.Result.Error) { - string msg = SQLite3.GetErrmsg (Connection.Handle); - SQLite3.Reset (Statement); - throw SQLiteException.New (r, msg); - } - else if (r == SQLite3.Result.Constraint && SQLite3.ExtendedErrCode (Connection.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { - SQLite3.Reset (Statement); - throw NotNullConstraintViolationException.New (r, SQLite3.GetErrmsg (Connection.Handle)); - } - else { - SQLite3.Reset (Statement); - throw SQLiteException.New (r, SQLite3.GetErrmsg (Connection.Handle)); - } - } - - public void Dispose () - { - Dispose (true); - GC.SuppressFinalize (this); - } - - void Dispose (bool disposing) - { - var s = Statement; - Statement = NullStatement; - Connection = null; - if (s != NullStatement) { - SQLite3.Finalize (s); - } - } - - ~PreparedSqlLiteInsertCommand () - { - Dispose (false); - } - } - - public enum CreateTableResult - { - Created, - Migrated, - } - - public class CreateTablesResult - { - public Dictionary Results { get; private set; } - - public CreateTablesResult () - { - Results = new Dictionary (); - } - } - - public abstract class BaseTableQuery - { - protected class Ordering - { - public string ColumnName { get; set; } - public bool Ascending { get; set; } - } - } - - public class TableQuery< -#if NET8_0_OR_GREATER - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] -#endif - T> : BaseTableQuery, IEnumerable - { - public SQLiteConnection Connection { get; private set; } + public T FirstOrDefault () + { + var query = Take (1); + return query.ToList ().FirstOrDefault (); + } - public TableMapping Table { get; private set; } + /// + /// Returns the first element of this query that matches the predicate. + /// + public T First (Expression> predExpr) + { + return Where (predExpr).First (); + } - Expression _where; - List _orderBys; - int? _limit; - int? _offset; + /// + /// Returns the first element of this query that matches the predicate, or null + /// if no element is found. + /// + public T FirstOrDefault (Expression> predExpr) + { + return Where (predExpr).FirstOrDefault (); + } + } - BaseTableQuery _joinInner; - Expression _joinInnerKeySelector; - BaseTableQuery _joinOuter; - Expression _joinOuterKeySelector; - Expression _joinSelector; + public static class SQLite3 + { + public enum Result : int + { + OK = 0, + Error = 1, + Internal = 2, + Perm = 3, + Abort = 4, + Busy = 5, + Locked = 6, + NoMem = 7, + ReadOnly = 8, + Interrupt = 9, + IOError = 10, + Corrupt = 11, + NotFound = 12, + Full = 13, + CannotOpen = 14, + LockErr = 15, + Empty = 16, + SchemaChngd = 17, + TooBig = 18, + Constraint = 19, + Mismatch = 20, + Misuse = 21, + NotImplementedLFS = 22, + AccessDenied = 23, + Format = 24, + Range = 25, + NonDBFile = 26, + Notice = 27, + Warning = 28, + Row = 100, + Done = 101 + } - Expression _selector; + public enum ExtendedResult : int + { + IOErrorRead = (Result.IOError | (1 << 8)), + IOErrorShortRead = (Result.IOError | (2 << 8)), + IOErrorWrite = (Result.IOError | (3 << 8)), + IOErrorFsync = (Result.IOError | (4 << 8)), + IOErrorDirFSync = (Result.IOError | (5 << 8)), + IOErrorTruncate = (Result.IOError | (6 << 8)), + IOErrorFStat = (Result.IOError | (7 << 8)), + IOErrorUnlock = (Result.IOError | (8 << 8)), + IOErrorRdlock = (Result.IOError | (9 << 8)), + IOErrorDelete = (Result.IOError | (10 << 8)), + IOErrorBlocked = (Result.IOError | (11 << 8)), + IOErrorNoMem = (Result.IOError | (12 << 8)), + IOErrorAccess = (Result.IOError | (13 << 8)), + IOErrorCheckReservedLock = (Result.IOError | (14 << 8)), + IOErrorLock = (Result.IOError | (15 << 8)), + IOErrorClose = (Result.IOError | (16 << 8)), + IOErrorDirClose = (Result.IOError | (17 << 8)), + IOErrorSHMOpen = (Result.IOError | (18 << 8)), + IOErrorSHMSize = (Result.IOError | (19 << 8)), + IOErrorSHMLock = (Result.IOError | (20 << 8)), + IOErrorSHMMap = (Result.IOError | (21 << 8)), + IOErrorSeek = (Result.IOError | (22 << 8)), + IOErrorDeleteNoEnt = (Result.IOError | (23 << 8)), + IOErrorMMap = (Result.IOError | (24 << 8)), + LockedSharedcache = (Result.Locked | (1 << 8)), + BusyRecovery = (Result.Busy | (1 << 8)), + CannottOpenNoTempDir = (Result.CannotOpen | (1 << 8)), + CannotOpenIsDir = (Result.CannotOpen | (2 << 8)), + CannotOpenFullPath = (Result.CannotOpen | (3 << 8)), + CorruptVTab = (Result.Corrupt | (1 << 8)), + ReadonlyRecovery = (Result.ReadOnly | (1 << 8)), + ReadonlyCannotLock = (Result.ReadOnly | (2 << 8)), + ReadonlyRollback = (Result.ReadOnly | (3 << 8)), + AbortRollback = (Result.Abort | (2 << 8)), + ConstraintCheck = (Result.Constraint | (1 << 8)), + ConstraintCommitHook = (Result.Constraint | (2 << 8)), + ConstraintForeignKey = (Result.Constraint | (3 << 8)), + ConstraintFunction = (Result.Constraint | (4 << 8)), + ConstraintNotNull = (Result.Constraint | (5 << 8)), + ConstraintPrimaryKey = (Result.Constraint | (6 << 8)), + ConstraintTrigger = (Result.Constraint | (7 << 8)), + ConstraintUnique = (Result.Constraint | (8 << 8)), + ConstraintVTab = (Result.Constraint | (9 << 8)), + NoticeRecoverWAL = (Result.Notice | (1 << 8)), + NoticeRecoverRollback = (Result.Notice | (2 << 8)) + } - TableQuery (SQLiteConnection conn, TableMapping table) - { - Connection = conn; - Table = table; - } - public TableQuery (SQLiteConnection conn) - { - Connection = conn; - Table = Connection.GetMapping (typeof (T)); - } + public enum ConfigOption : int + { + SingleThread = 1, + MultiThread = 2, + Serialized = 3 + } - public TableQuery Clone< -#if NET8_0_OR_GREATER - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] -#endif - U> () - { - var q = new TableQuery (Connection, Table); - q._where = _where; - q._deferred = _deferred; - if (_orderBys != null) { - q._orderBys = new List (_orderBys); - } - q._limit = _limit; - q._offset = _offset; - q._joinInner = _joinInner; - q._joinInnerKeySelector = _joinInnerKeySelector; - q._joinOuter = _joinOuter; - q._joinOuterKeySelector = _joinOuterKeySelector; - q._joinSelector = _joinSelector; - q._selector = _selector; - return q; - } - - /// - /// Filters the query based on a predicate. - /// - public TableQuery Where (Expression> predExpr) - { - if (predExpr.NodeType == ExpressionType.Lambda) { - var lambda = (LambdaExpression)predExpr; - var pred = lambda.Body; - var q = Clone (); - q.AddWhere (pred); - return q; - } - else { - throw new NotSupportedException ("Must be a predicate"); - } - } - - /// - /// Delete all the rows that match this query. - /// - public int Delete () - { - return Delete (null); - } - - /// - /// Delete all the rows that match this query and the given predicate. - /// - public int Delete (Expression> predExpr) - { - if (_limit.HasValue || _offset.HasValue) - throw new InvalidOperationException ("Cannot delete with limits or offsets"); - - if (_where == null && predExpr == null) - throw new InvalidOperationException ("No condition specified"); - - var pred = _where; - - if (predExpr != null && predExpr.NodeType == ExpressionType.Lambda) { - var lambda = (LambdaExpression)predExpr; - pred = pred != null ? Expression.AndAlso (pred, lambda.Body) : lambda.Body; - } - - var args = new List (); - var cmdText = "delete from \"" + Table.TableName + "\""; - var w = CompileExpr (pred, args); - cmdText += " where " + w.CommandText; - - var command = Connection.CreateCommand (cmdText, args.ToArray ()); - - int result = command.ExecuteNonQuery (); - return result; - } - - /// - /// Yields a given number of elements from the query and then skips the remainder. - /// - public TableQuery Take (int n) - { - var q = Clone (); - q._limit = n; - return q; - } - - /// - /// Skips a given number of elements from the query and then yields the remainder. - /// - public TableQuery Skip (int n) - { - var q = Clone (); - q._offset = n; - return q; - } - - /// - /// Returns the element at a given index - /// - public T ElementAt (int index) - { - return Skip (index).Take (1).First (); - } - - bool _deferred; - public TableQuery Deferred () - { - var q = Clone (); - q._deferred = true; - return q; - } - - /// - /// Order the query results according to a key. - /// - public TableQuery OrderBy (Expression> orderExpr) - { - return AddOrderBy (orderExpr, true); - } - - /// - /// Order the query results according to a key. - /// - public TableQuery OrderByDescending (Expression> orderExpr) - { - return AddOrderBy (orderExpr, false); - } - - /// - /// Order the query results according to a key. - /// - public TableQuery ThenBy (Expression> orderExpr) - { - return AddOrderBy (orderExpr, true); - } - - /// - /// Order the query results according to a key. - /// - public TableQuery ThenByDescending (Expression> orderExpr) - { - return AddOrderBy (orderExpr, false); - } - - TableQuery AddOrderBy (Expression> orderExpr, bool asc) - { - if (orderExpr.NodeType == ExpressionType.Lambda) { - var lambda = (LambdaExpression)orderExpr; - - MemberExpression mem = null; - - var unary = lambda.Body as UnaryExpression; - if (unary != null && unary.NodeType == ExpressionType.Convert) { - mem = unary.Operand as MemberExpression; - } - else { - mem = lambda.Body as MemberExpression; - } - - if (mem != null && (mem.Expression.NodeType == ExpressionType.Parameter)) { - var q = Clone (); - if (q._orderBys == null) { - q._orderBys = new List (); - } - q._orderBys.Add (new Ordering { - ColumnName = Table.FindColumnWithPropertyName (mem.Member.Name).Name, - Ascending = asc - }); - return q; - } - else { - throw new NotSupportedException ("Order By does not support: " + orderExpr); - } - } - else { - throw new NotSupportedException ("Must be a predicate"); - } - } - - private void AddWhere (Expression pred) - { - if (_where == null) { - _where = pred; - } - else { - _where = Expression.AndAlso (_where, pred); - } - } - - ///// - ///// Performs an inner join of two queries based on matching keys extracted from the elements. - ///// - //public TableQuery Join ( - // TableQuery inner, - // Expression> outerKeySelector, - // Expression> innerKeySelector, - // Expression> resultSelector) - //{ - // var q = new TableQuery (Connection, Connection.GetMapping (typeof (TResult))) { - // _joinOuter = this, - // _joinOuterKeySelector = outerKeySelector, - // _joinInner = inner, - // _joinInnerKeySelector = innerKeySelector, - // _joinSelector = resultSelector, - // }; - // return q; - //} - - // Not needed until Joins are supported - // Keeping this commented out forces the default Linq to objects processor to run - //public TableQuery Select (Expression> selector) - //{ - // var q = Clone (); - // q._selector = selector; - // return q; - //} - - private SQLiteCommand GenerateCommand (string selectionList) - { - if (_joinInner != null && _joinOuter != null) { - throw new NotSupportedException ("Joins are not supported."); - } - else { - var cmdText = "select " + selectionList + " from \"" + Table.TableName + "\""; - var args = new List (); - if (_where != null) { - var w = CompileExpr (_where, args); - cmdText += " where " + w.CommandText; - } - if ((_orderBys != null) && (_orderBys.Count > 0)) { - var t = string.Join (", ", _orderBys.Select (o => "\"" + o.ColumnName + "\"" + (o.Ascending ? "" : " desc")).ToArray ()); - cmdText += " order by " + t; - } - if (_limit.HasValue) { - cmdText += " limit " + _limit.Value; - } - if (_offset.HasValue) { - if (!_limit.HasValue) { - cmdText += " limit -1 "; - } - cmdText += " offset " + _offset.Value; - } - return Connection.CreateCommand (cmdText, args.ToArray ()); - } - } - - class CompileResult - { - public string CommandText { get; set; } - - public object Value { get; set; } - } - - private CompileResult CompileExpr (Expression expr, List queryArgs) - { - if (expr == null) { - throw new NotSupportedException ("Expression is NULL"); - } - else if (expr is BinaryExpression) { - var bin = (BinaryExpression)expr; - - // VB turns 'x=="foo"' into 'CompareString(x,"foo",true/false)==0', so we need to unwrap it - // http://blogs.msdn.com/b/vbteam/archive/2007/09/18/vb-expression-trees-string-comparisons.aspx - if (bin.Left.NodeType == ExpressionType.Call) { - var call = (MethodCallExpression)bin.Left; - if (call.Method.DeclaringType.FullName == "Microsoft.VisualBasic.CompilerServices.Operators" - && call.Method.Name == "CompareString") - bin = Expression.MakeBinary (bin.NodeType, call.Arguments[0], call.Arguments[1]); - } - - - var leftr = CompileExpr (bin.Left, queryArgs); - var rightr = CompileExpr (bin.Right, queryArgs); - - //If either side is a parameter and is null, then handle the other side specially (for "is null"/"is not null") - string text; - if (leftr.CommandText == "?" && leftr.Value == null) - text = CompileNullBinaryExpression (bin, rightr); - else if (rightr.CommandText == "?" && rightr.Value == null) - text = CompileNullBinaryExpression (bin, leftr); - else - text = "(" + leftr.CommandText + " " + GetSqlName (bin) + " " + rightr.CommandText + ")"; - return new CompileResult { CommandText = text }; - } - else if (expr.NodeType == ExpressionType.Not) { - var operandExpr = ((UnaryExpression)expr).Operand; - var opr = CompileExpr (operandExpr, queryArgs); - object val = opr.Value; - if (val is bool) - val = !((bool)val); - return new CompileResult { - CommandText = "NOT(" + opr.CommandText + ")", - Value = val - }; - } - else if (expr.NodeType == ExpressionType.Call) { - - var call = (MethodCallExpression)expr; - var args = new CompileResult[call.Arguments.Count]; - var obj = call.Object != null ? CompileExpr (call.Object, queryArgs) : null; - - for (var i = 0; i < args.Length; i++) { - args[i] = CompileExpr (call.Arguments[i], queryArgs); - } - - var sqlCall = ""; - - if (call.Method.Name == "Like" && args.Length == 2) { - sqlCall = "(" + args[0].CommandText + " like " + args[1].CommandText + ")"; - } - else if (call.Method.Name == "Contains" && args.Length == 2) { - sqlCall = "(" + args[1].CommandText + " in " + args[0].CommandText + ")"; - } - else if (call.Method.Name == "Contains" && args.Length == 1) { - if (call.Object != null && call.Object.Type == typeof (string)) { - sqlCall = "( instr(" + obj.CommandText + "," + args[0].CommandText + ") >0 )"; - } - else { - sqlCall = "(" + args[0].CommandText + " in " + obj.CommandText + ")"; - } - } - else if (call.Method.Name == "StartsWith" && args.Length >= 1) { - var startsWithCmpOp = StringComparison.CurrentCulture; - if (args.Length == 2) { - startsWithCmpOp = (StringComparison)args[1].Value; - } - switch (startsWithCmpOp) { - case StringComparison.Ordinal: - case StringComparison.CurrentCulture: - sqlCall = "( substr(" + obj.CommandText + ", 1, " + args[0].Value.ToString ().Length + ") = " + args[0].CommandText + ")"; - break; - case StringComparison.OrdinalIgnoreCase: - case StringComparison.CurrentCultureIgnoreCase: - sqlCall = "(" + obj.CommandText + " like (" + args[0].CommandText + " || '%'))"; - break; - } - - } - else if (call.Method.Name == "EndsWith" && args.Length >= 1) { - var endsWithCmpOp = StringComparison.CurrentCulture; - if (args.Length == 2) { - endsWithCmpOp = (StringComparison)args[1].Value; - } - switch (endsWithCmpOp) { - case StringComparison.Ordinal: - case StringComparison.CurrentCulture: - sqlCall = "( substr(" + obj.CommandText + ", length(" + obj.CommandText + ") - " + args[0].Value.ToString ().Length + "+1, " + args[0].Value.ToString ().Length + ") = " + args[0].CommandText + ")"; - break; - case StringComparison.OrdinalIgnoreCase: - case StringComparison.CurrentCultureIgnoreCase: - sqlCall = "(" + obj.CommandText + " like ('%' || " + args[0].CommandText + "))"; - break; - } - } - else if (call.Method.Name == "Equals" && args.Length == 1) { - sqlCall = "(" + obj.CommandText + " = (" + args[0].CommandText + "))"; - } - else if (call.Method.Name == "ToLower") { - sqlCall = "(lower(" + obj.CommandText + "))"; - } - else if (call.Method.Name == "ToUpper") { - sqlCall = "(upper(" + obj.CommandText + "))"; - } - else if (call.Method.Name == "Replace" && args.Length == 2) { - sqlCall = "(replace(" + obj.CommandText + "," + args[0].CommandText + "," + args[1].CommandText + "))"; - } - else if (call.Method.Name == "IsNullOrEmpty" && args.Length == 1) { - sqlCall = "(" + args[0].CommandText + " is null or" + args[0].CommandText + " ='' )"; - } - else { - sqlCall = call.Method.Name.ToLower () + "(" + string.Join (",", args.Select (a => a.CommandText).ToArray ()) + ")"; - } - return new CompileResult { CommandText = sqlCall }; - - } - else if (expr.NodeType == ExpressionType.Constant) { - var c = (ConstantExpression)expr; - queryArgs.Add (c.Value); - return new CompileResult { - CommandText = "?", - Value = c.Value - }; - } - else if (expr.NodeType == ExpressionType.Convert) { - var u = (UnaryExpression)expr; - var ty = u.Type; - var valr = CompileExpr (u.Operand, queryArgs); - return new CompileResult { - CommandText = valr.CommandText, - Value = valr.Value != null ? ConvertTo (valr.Value, ty) : null - }; - } - else if (expr.NodeType == ExpressionType.MemberAccess) { - var mem = (MemberExpression)expr; - - var paramExpr = mem.Expression as ParameterExpression; - if (paramExpr == null) { - var convert = mem.Expression as UnaryExpression; - if (convert != null && convert.NodeType == ExpressionType.Convert) { - paramExpr = convert.Operand as ParameterExpression; - } - } - - if (paramExpr != null) { - // - // This is a column of our table, output just the column name - // Need to translate it if that column name is mapped - // - var columnName = Table.FindColumnWithPropertyName (mem.Member.Name).Name; - return new CompileResult { CommandText = "\"" + columnName + "\"" }; - } - else { - object obj = null; - if (mem.Expression != null) { - var r = CompileExpr (mem.Expression, queryArgs); - if (r.Value == null) { - throw new NotSupportedException ("Member access failed to compile expression"); - } - if (r.CommandText == "?") { - queryArgs.RemoveAt (queryArgs.Count - 1); - } - obj = r.Value; - } - - // - // Get the member value - // - object val = null; - - if (mem.Member is PropertyInfo) { - var m = (PropertyInfo)mem.Member; - val = m.GetValue (obj, null); - } - else if (mem.Member is FieldInfo) { - var m = (FieldInfo)mem.Member; - val = m.GetValue (obj); - } - else { - throw new NotSupportedException ("MemberExpr: " + mem.Member.GetType ()); - } - - // - // Work special magic for enumerables - // - if (val != null && val is System.Collections.IEnumerable && !(val is string) && !(val is System.Collections.Generic.IEnumerable)) { - var sb = new System.Text.StringBuilder (); - sb.Append ("("); - var head = ""; - foreach (var a in (System.Collections.IEnumerable)val) { - queryArgs.Add (a); - sb.Append (head); - sb.Append ("?"); - head = ","; - } - sb.Append (")"); - return new CompileResult { - CommandText = sb.ToString (), - Value = val - }; - } - else { - queryArgs.Add (val); - return new CompileResult { - CommandText = "?", - Value = val - }; - } - } - } - throw new NotSupportedException ("Cannot compile: " + expr.NodeType.ToString ()); - } - - static object ConvertTo (object obj, Type t) - { - Type nut = Nullable.GetUnderlyingType (t); - - if (nut != null) { - if (obj == null) - return null; - return Convert.ChangeType (obj, nut); - } - else { - return Convert.ChangeType (obj, t); - } - } - - /// - /// Compiles a BinaryExpression where one of the parameters is null. - /// - /// The expression to compile - /// The non-null parameter - private string CompileNullBinaryExpression (BinaryExpression expression, CompileResult parameter) - { - if (expression.NodeType == ExpressionType.Equal) - return "(" + parameter.CommandText + " is ?)"; - else if (expression.NodeType == ExpressionType.NotEqual) - return "(" + parameter.CommandText + " is not ?)"; - else if (expression.NodeType == ExpressionType.GreaterThan - || expression.NodeType == ExpressionType.GreaterThanOrEqual - || expression.NodeType == ExpressionType.LessThan - || expression.NodeType == ExpressionType.LessThanOrEqual) - return "(" + parameter.CommandText + " < ?)"; // always false - else - throw new NotSupportedException ("Cannot compile Null-BinaryExpression with type " + expression.NodeType.ToString ()); - } - - string GetSqlName (Expression expr) - { - var n = expr.NodeType; - if (n == ExpressionType.GreaterThan) - return ">"; - else if (n == ExpressionType.GreaterThanOrEqual) { - return ">="; - } - else if (n == ExpressionType.LessThan) { - return "<"; - } - else if (n == ExpressionType.LessThanOrEqual) { - return "<="; - } - else if (n == ExpressionType.And) { - return "&"; - } - else if (n == ExpressionType.AndAlso) { - return "and"; - } - else if (n == ExpressionType.Or) { - return "|"; - } - else if (n == ExpressionType.OrElse) { - return "or"; - } - else if (n == ExpressionType.Equal) { - return "="; - } - else if (n == ExpressionType.NotEqual) { - return "!="; - } - else { - throw new NotSupportedException ("Cannot get SQL for: " + n); - } - } - - /// - /// Execute SELECT COUNT(*) on the query - /// - public int Count () - { - return GenerateCommand ("count(*)").ExecuteScalar (); - } - - /// - /// Execute SELECT COUNT(*) on the query with an additional WHERE clause. - /// - public int Count (Expression> predExpr) - { - return Where (predExpr).Count (); - } - - public IEnumerator GetEnumerator () - { - if (!_deferred) - return GenerateCommand ("*").ExecuteQuery ().GetEnumerator (); - - return GenerateCommand ("*").ExecuteDeferredQuery ().GetEnumerator (); - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () - { - return GetEnumerator (); - } - - /// - /// Queries the database and returns the results as a List. - /// - public List ToList () - { - return GenerateCommand ("*").ExecuteQuery (); - } - - /// - /// Queries the database and returns the results as an array. - /// - public T[] ToArray () - { - return GenerateCommand ("*").ExecuteQuery ().ToArray (); - } - - /// - /// Returns the first element of this query. - /// - public T First () - { - var query = Take (1); - return query.ToList ().First (); - } - - /// - /// Returns the first element of this query, or null if no element is found. - /// - public T FirstOrDefault () - { - var query = Take (1); - return query.ToList ().FirstOrDefault (); - } - - /// - /// Returns the first element of this query that matches the predicate. - /// - public T First (Expression> predExpr) - { - return Where (predExpr).First (); - } - - /// - /// Returns the first element of this query that matches the predicate, or null - /// if no element is found. - /// - public T FirstOrDefault (Expression> predExpr) - { - return Where (predExpr).FirstOrDefault (); - } - } - - public static class SQLite3 - { - public enum Result : int - { - OK = 0, - Error = 1, - Internal = 2, - Perm = 3, - Abort = 4, - Busy = 5, - Locked = 6, - NoMem = 7, - ReadOnly = 8, - Interrupt = 9, - IOError = 10, - Corrupt = 11, - NotFound = 12, - Full = 13, - CannotOpen = 14, - LockErr = 15, - Empty = 16, - SchemaChngd = 17, - TooBig = 18, - Constraint = 19, - Mismatch = 20, - Misuse = 21, - NotImplementedLFS = 22, - AccessDenied = 23, - Format = 24, - Range = 25, - NonDBFile = 26, - Notice = 27, - Warning = 28, - Row = 100, - Done = 101 - } - - public enum ExtendedResult : int - { - IOErrorRead = (Result.IOError | (1 << 8)), - IOErrorShortRead = (Result.IOError | (2 << 8)), - IOErrorWrite = (Result.IOError | (3 << 8)), - IOErrorFsync = (Result.IOError | (4 << 8)), - IOErrorDirFSync = (Result.IOError | (5 << 8)), - IOErrorTruncate = (Result.IOError | (6 << 8)), - IOErrorFStat = (Result.IOError | (7 << 8)), - IOErrorUnlock = (Result.IOError | (8 << 8)), - IOErrorRdlock = (Result.IOError | (9 << 8)), - IOErrorDelete = (Result.IOError | (10 << 8)), - IOErrorBlocked = (Result.IOError | (11 << 8)), - IOErrorNoMem = (Result.IOError | (12 << 8)), - IOErrorAccess = (Result.IOError | (13 << 8)), - IOErrorCheckReservedLock = (Result.IOError | (14 << 8)), - IOErrorLock = (Result.IOError | (15 << 8)), - IOErrorClose = (Result.IOError | (16 << 8)), - IOErrorDirClose = (Result.IOError | (17 << 8)), - IOErrorSHMOpen = (Result.IOError | (18 << 8)), - IOErrorSHMSize = (Result.IOError | (19 << 8)), - IOErrorSHMLock = (Result.IOError | (20 << 8)), - IOErrorSHMMap = (Result.IOError | (21 << 8)), - IOErrorSeek = (Result.IOError | (22 << 8)), - IOErrorDeleteNoEnt = (Result.IOError | (23 << 8)), - IOErrorMMap = (Result.IOError | (24 << 8)), - LockedSharedcache = (Result.Locked | (1 << 8)), - BusyRecovery = (Result.Busy | (1 << 8)), - CannottOpenNoTempDir = (Result.CannotOpen | (1 << 8)), - CannotOpenIsDir = (Result.CannotOpen | (2 << 8)), - CannotOpenFullPath = (Result.CannotOpen | (3 << 8)), - CorruptVTab = (Result.Corrupt | (1 << 8)), - ReadonlyRecovery = (Result.ReadOnly | (1 << 8)), - ReadonlyCannotLock = (Result.ReadOnly | (2 << 8)), - ReadonlyRollback = (Result.ReadOnly | (3 << 8)), - AbortRollback = (Result.Abort | (2 << 8)), - ConstraintCheck = (Result.Constraint | (1 << 8)), - ConstraintCommitHook = (Result.Constraint | (2 << 8)), - ConstraintForeignKey = (Result.Constraint | (3 << 8)), - ConstraintFunction = (Result.Constraint | (4 << 8)), - ConstraintNotNull = (Result.Constraint | (5 << 8)), - ConstraintPrimaryKey = (Result.Constraint | (6 << 8)), - ConstraintTrigger = (Result.Constraint | (7 << 8)), - ConstraintUnique = (Result.Constraint | (8 << 8)), - ConstraintVTab = (Result.Constraint | (9 << 8)), - NoticeRecoverWAL = (Result.Notice | (1 << 8)), - NoticeRecoverRollback = (Result.Notice | (2 << 8)) - } - - - public enum ConfigOption : int - { - SingleThread = 1, - MultiThread = 2, - Serialized = 3 - } - - const string LibraryPath = "sqlite3"; + const string LibraryPath = "sqlite3"; #if !USE_CSHARP_SQLITE && !USE_WP8_NATIVE_SQLITE && !USE_SQLITEPCL_RAW - [DllImport(LibraryPath, EntryPoint = "sqlite3_threadsafe", CallingConvention=CallingConvention.Cdecl)] - public static extern int Threadsafe (); + [DllImport(LibraryPath, EntryPoint = "sqlite3_threadsafe", CallingConvention=CallingConvention.Cdecl)] + public static extern int Threadsafe (); - [DllImport(LibraryPath, EntryPoint = "sqlite3_open", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Open ([MarshalAs(UnmanagedType.LPStr)] string filename, out IntPtr db); + [DllImport(LibraryPath, EntryPoint = "sqlite3_open", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Open ([MarshalAs(UnmanagedType.LPStr)] string filename, out IntPtr db); - [DllImport(LibraryPath, EntryPoint = "sqlite3_open_v2", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Open ([MarshalAs(UnmanagedType.LPStr)] string filename, out IntPtr db, int flags, [MarshalAs (UnmanagedType.LPStr)] string zvfs); + [DllImport(LibraryPath, EntryPoint = "sqlite3_open_v2", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Open ([MarshalAs(UnmanagedType.LPStr)] string filename, out IntPtr db, int flags, [MarshalAs (UnmanagedType.LPStr)] string zvfs); - [DllImport(LibraryPath, EntryPoint = "sqlite3_open_v2", CallingConvention = CallingConvention.Cdecl)] - public static extern Result Open(byte[] filename, out IntPtr db, int flags, [MarshalAs (UnmanagedType.LPStr)] string zvfs); + [DllImport(LibraryPath, EntryPoint = "sqlite3_open_v2", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Open(byte[] filename, out IntPtr db, int flags, [MarshalAs (UnmanagedType.LPStr)] string zvfs); - [DllImport(LibraryPath, EntryPoint = "sqlite3_open16", CallingConvention = CallingConvention.Cdecl)] - public static extern Result Open16([MarshalAs(UnmanagedType.LPWStr)] string filename, out IntPtr db); + [DllImport(LibraryPath, EntryPoint = "sqlite3_open16", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Open16([MarshalAs(UnmanagedType.LPWStr)] string filename, out IntPtr db); - [DllImport(LibraryPath, EntryPoint = "sqlite3_enable_load_extension", CallingConvention=CallingConvention.Cdecl)] - public static extern Result EnableLoadExtension (IntPtr db, int onoff); + [DllImport(LibraryPath, EntryPoint = "sqlite3_enable_load_extension", CallingConvention=CallingConvention.Cdecl)] + public static extern Result EnableLoadExtension (IntPtr db, int onoff); - [DllImport(LibraryPath, EntryPoint = "sqlite3_close", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Close (IntPtr db); + [DllImport(LibraryPath, EntryPoint = "sqlite3_close", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Close (IntPtr db); - [DllImport(LibraryPath, EntryPoint = "sqlite3_close_v2", CallingConvention = CallingConvention.Cdecl)] - public static extern Result Close2(IntPtr db); + [DllImport(LibraryPath, EntryPoint = "sqlite3_close_v2", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Close2(IntPtr db); - [DllImport(LibraryPath, EntryPoint = "sqlite3_initialize", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Initialize(); + [DllImport(LibraryPath, EntryPoint = "sqlite3_initialize", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Initialize(); - [DllImport(LibraryPath, EntryPoint = "sqlite3_shutdown", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Shutdown(); + [DllImport(LibraryPath, EntryPoint = "sqlite3_shutdown", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Shutdown(); - [DllImport(LibraryPath, EntryPoint = "sqlite3_config", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Config (ConfigOption option); + [DllImport(LibraryPath, EntryPoint = "sqlite3_config", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Config (ConfigOption option); - [DllImport(LibraryPath, EntryPoint = "sqlite3_win32_set_directory", CallingConvention=CallingConvention.Cdecl, CharSet=CharSet.Unicode)] - public static extern int SetDirectory (uint directoryType, string directoryPath); + [DllImport(LibraryPath, EntryPoint = "sqlite3_win32_set_directory", CallingConvention=CallingConvention.Cdecl, CharSet=CharSet.Unicode)] + public static extern int SetDirectory (uint directoryType, string directoryPath); - [DllImport(LibraryPath, EntryPoint = "sqlite3_busy_timeout", CallingConvention=CallingConvention.Cdecl)] - public static extern Result BusyTimeout (IntPtr db, int milliseconds); + [DllImport(LibraryPath, EntryPoint = "sqlite3_busy_timeout", CallingConvention=CallingConvention.Cdecl)] + public static extern Result BusyTimeout (IntPtr db, int milliseconds); - [DllImport(LibraryPath, EntryPoint = "sqlite3_changes", CallingConvention=CallingConvention.Cdecl)] - public static extern int Changes (IntPtr db); + [DllImport(LibraryPath, EntryPoint = "sqlite3_changes", CallingConvention=CallingConvention.Cdecl)] + public static extern int Changes (IntPtr db); - [DllImport(LibraryPath, EntryPoint = "sqlite3_prepare_v2", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Prepare2 (IntPtr db, [MarshalAs(UnmanagedType.LPStr)] string sql, int numBytes, out IntPtr stmt, IntPtr pzTail); + [DllImport(LibraryPath, EntryPoint = "sqlite3_prepare_v2", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Prepare2 (IntPtr db, [MarshalAs(UnmanagedType.LPStr)] string sql, int numBytes, out IntPtr stmt, IntPtr pzTail); #if NETFX_CORE - [DllImport (LibraryPath, EntryPoint = "sqlite3_prepare_v2", CallingConvention = CallingConvention.Cdecl)] - public static extern Result Prepare2 (IntPtr db, byte[] queryBytes, int numBytes, out IntPtr stmt, IntPtr pzTail); + [DllImport (LibraryPath, EntryPoint = "sqlite3_prepare_v2", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Prepare2 (IntPtr db, byte[] queryBytes, int numBytes, out IntPtr stmt, IntPtr pzTail); #endif - public static IntPtr Prepare2 (IntPtr db, string query) - { - IntPtr stmt; + public static IntPtr Prepare2 (IntPtr db, string query) + { + IntPtr stmt; #if NETFX_CORE - byte[] queryBytes = System.Text.UTF8Encoding.UTF8.GetBytes (query); - var r = Prepare2 (db, queryBytes, queryBytes.Length, out stmt, IntPtr.Zero); + byte[] queryBytes = System.Text.UTF8Encoding.UTF8.GetBytes (query); + var r = Prepare2 (db, queryBytes, queryBytes.Length, out stmt, IntPtr.Zero); #else - var r = Prepare2 (db, query, System.Text.UTF8Encoding.UTF8.GetByteCount (query), out stmt, IntPtr.Zero); + var r = Prepare2 (db, query, System.Text.UTF8Encoding.UTF8.GetByteCount (query), out stmt, IntPtr.Zero); #endif - if (r != Result.OK) { - throw SQLiteException.New (r, GetErrmsg (db)); - } - return stmt; - } + if (r != Result.OK) { + throw SQLiteException.New (r, GetErrmsg (db)); + } + return stmt; + } - [DllImport(LibraryPath, EntryPoint = "sqlite3_step", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Step (IntPtr stmt); + [DllImport(LibraryPath, EntryPoint = "sqlite3_step", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Step (IntPtr stmt); - [DllImport(LibraryPath, EntryPoint = "sqlite3_reset", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Reset (IntPtr stmt); + [DllImport(LibraryPath, EntryPoint = "sqlite3_reset", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Reset (IntPtr stmt); - [DllImport(LibraryPath, EntryPoint = "sqlite3_finalize", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Finalize (IntPtr stmt); + [DllImport(LibraryPath, EntryPoint = "sqlite3_finalize", CallingConvention=CallingConvention.Cdecl)] + public static extern Result Finalize (IntPtr stmt); - [DllImport(LibraryPath, EntryPoint = "sqlite3_last_insert_rowid", CallingConvention=CallingConvention.Cdecl)] - public static extern long LastInsertRowid (IntPtr db); + [DllImport(LibraryPath, EntryPoint = "sqlite3_last_insert_rowid", CallingConvention=CallingConvention.Cdecl)] + public static extern long LastInsertRowid (IntPtr db); - [DllImport(LibraryPath, EntryPoint = "sqlite3_errmsg16", CallingConvention=CallingConvention.Cdecl)] - public static extern IntPtr Errmsg (IntPtr db); + [DllImport(LibraryPath, EntryPoint = "sqlite3_errmsg16", CallingConvention=CallingConvention.Cdecl)] + public static extern IntPtr Errmsg (IntPtr db); - public static string GetErrmsg (IntPtr db) - { - return Marshal.PtrToStringUni (Errmsg (db)); - } + public static string GetErrmsg (IntPtr db) + { + return Marshal.PtrToStringUni (Errmsg (db)); + } - [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_parameter_index", CallingConvention=CallingConvention.Cdecl)] - public static extern int BindParameterIndex (IntPtr stmt, [MarshalAs(UnmanagedType.LPStr)] string name); + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_parameter_index", CallingConvention=CallingConvention.Cdecl)] + public static extern int BindParameterIndex (IntPtr stmt, [MarshalAs(UnmanagedType.LPStr)] string name); - [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_null", CallingConvention=CallingConvention.Cdecl)] - public static extern int BindNull (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_null", CallingConvention=CallingConvention.Cdecl)] + public static extern int BindNull (IntPtr stmt, int index); - [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_int", CallingConvention=CallingConvention.Cdecl)] - public static extern int BindInt (IntPtr stmt, int index, int val); + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_int", CallingConvention=CallingConvention.Cdecl)] + public static extern int BindInt (IntPtr stmt, int index, int val); - [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_int64", CallingConvention=CallingConvention.Cdecl)] - public static extern int BindInt64 (IntPtr stmt, int index, long val); + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_int64", CallingConvention=CallingConvention.Cdecl)] + public static extern int BindInt64 (IntPtr stmt, int index, long val); - [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_double", CallingConvention=CallingConvention.Cdecl)] - public static extern int BindDouble (IntPtr stmt, int index, double val); + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_double", CallingConvention=CallingConvention.Cdecl)] + public static extern int BindDouble (IntPtr stmt, int index, double val); - [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_text16", CallingConvention=CallingConvention.Cdecl, CharSet = CharSet.Unicode)] - public static extern int BindText (IntPtr stmt, int index, [MarshalAs(UnmanagedType.LPWStr)] string val, int n, IntPtr free); + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_text16", CallingConvention=CallingConvention.Cdecl, CharSet = CharSet.Unicode)] + public static extern int BindText (IntPtr stmt, int index, [MarshalAs(UnmanagedType.LPWStr)] string val, int n, IntPtr free); - [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_blob", CallingConvention=CallingConvention.Cdecl)] - public static extern int BindBlob (IntPtr stmt, int index, byte[] val, int n, IntPtr free); + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_blob", CallingConvention=CallingConvention.Cdecl)] + public static extern int BindBlob (IntPtr stmt, int index, byte[] val, int n, IntPtr free); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_count", CallingConvention=CallingConvention.Cdecl)] - public static extern int ColumnCount (IntPtr stmt); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_count", CallingConvention=CallingConvention.Cdecl)] + public static extern int ColumnCount (IntPtr stmt); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_name", CallingConvention=CallingConvention.Cdecl)] - public static extern IntPtr ColumnName (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_name", CallingConvention=CallingConvention.Cdecl)] + public static extern IntPtr ColumnName (IntPtr stmt, int index); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_name16", CallingConvention=CallingConvention.Cdecl)] - static extern IntPtr ColumnName16Internal (IntPtr stmt, int index); - public static string ColumnName16(IntPtr stmt, int index) - { - return Marshal.PtrToStringUni(ColumnName16Internal(stmt, index)); - } + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_name16", CallingConvention=CallingConvention.Cdecl)] + static extern IntPtr ColumnName16Internal (IntPtr stmt, int index); + public static string ColumnName16(IntPtr stmt, int index) + { + return Marshal.PtrToStringUni(ColumnName16Internal(stmt, index)); + } - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_type", CallingConvention=CallingConvention.Cdecl)] - public static extern ColType ColumnType (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_type", CallingConvention=CallingConvention.Cdecl)] + public static extern ColType ColumnType (IntPtr stmt, int index); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_int", CallingConvention=CallingConvention.Cdecl)] - public static extern int ColumnInt (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_int", CallingConvention=CallingConvention.Cdecl)] + public static extern int ColumnInt (IntPtr stmt, int index); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_int64", CallingConvention=CallingConvention.Cdecl)] - public static extern long ColumnInt64 (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_int64", CallingConvention=CallingConvention.Cdecl)] + public static extern long ColumnInt64 (IntPtr stmt, int index); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_double", CallingConvention=CallingConvention.Cdecl)] - public static extern double ColumnDouble (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_double", CallingConvention=CallingConvention.Cdecl)] + public static extern double ColumnDouble (IntPtr stmt, int index); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_text", CallingConvention=CallingConvention.Cdecl)] - public static extern IntPtr ColumnText (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_text", CallingConvention=CallingConvention.Cdecl)] + public static extern IntPtr ColumnText (IntPtr stmt, int index); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_text16", CallingConvention=CallingConvention.Cdecl)] - public static extern IntPtr ColumnText16 (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_text16", CallingConvention=CallingConvention.Cdecl)] + public static extern IntPtr ColumnText16 (IntPtr stmt, int index); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_blob", CallingConvention=CallingConvention.Cdecl)] - public static extern IntPtr ColumnBlob (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_blob", CallingConvention=CallingConvention.Cdecl)] + public static extern IntPtr ColumnBlob (IntPtr stmt, int index); - [DllImport(LibraryPath, EntryPoint = "sqlite3_column_bytes", CallingConvention=CallingConvention.Cdecl)] - public static extern int ColumnBytes (IntPtr stmt, int index); + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_bytes", CallingConvention=CallingConvention.Cdecl)] + public static extern int ColumnBytes (IntPtr stmt, int index); - public static string ColumnString (IntPtr stmt, int index) - { - return Marshal.PtrToStringUni (SQLite3.ColumnText16 (stmt, index)); - } + public static string ColumnString (IntPtr stmt, int index) + { + return Marshal.PtrToStringUni (SQLite3.ColumnText16 (stmt, index)); + } - public static byte[] ColumnByteArray (IntPtr stmt, int index) - { - int length = ColumnBytes (stmt, index); - var result = new byte[length]; - if (length > 0) - Marshal.Copy (ColumnBlob (stmt, index), result, 0, length); - return result; - } + public static byte[] ColumnByteArray (IntPtr stmt, int index) + { + int length = ColumnBytes (stmt, index); + var result = new byte[length]; + if (length > 0) + Marshal.Copy (ColumnBlob (stmt, index), result, 0, length); + return result; + } - [DllImport (LibraryPath, EntryPoint = "sqlite3_errcode", CallingConvention = CallingConvention.Cdecl)] - public static extern Result GetResult (Sqlite3DatabaseHandle db); + [DllImport (LibraryPath, EntryPoint = "sqlite3_errcode", CallingConvention = CallingConvention.Cdecl)] + public static extern Result GetResult (Sqlite3DatabaseHandle db); - [DllImport (LibraryPath, EntryPoint = "sqlite3_extended_errcode", CallingConvention = CallingConvention.Cdecl)] - public static extern ExtendedResult ExtendedErrCode (IntPtr db); + [DllImport (LibraryPath, EntryPoint = "sqlite3_extended_errcode", CallingConvention = CallingConvention.Cdecl)] + public static extern ExtendedResult ExtendedErrCode (IntPtr db); - [DllImport (LibraryPath, EntryPoint = "sqlite3_libversion_number", CallingConvention = CallingConvention.Cdecl)] - public static extern int LibVersionNumber (); + [DllImport (LibraryPath, EntryPoint = "sqlite3_libversion_number", CallingConvention = CallingConvention.Cdecl)] + public static extern int LibVersionNumber (); - [DllImport (LibraryPath, EntryPoint = "sqlite3_backup_init", CallingConvention = CallingConvention.Cdecl)] - public static extern Sqlite3BackupHandle BackupInit (Sqlite3DatabaseHandle destDb, [MarshalAs (UnmanagedType.LPStr)] string destName, Sqlite3DatabaseHandle sourceDb, [MarshalAs (UnmanagedType.LPStr)] string sourceName); + [DllImport (LibraryPath, EntryPoint = "sqlite3_backup_init", CallingConvention = CallingConvention.Cdecl)] + public static extern Sqlite3BackupHandle BackupInit (Sqlite3DatabaseHandle destDb, [MarshalAs (UnmanagedType.LPStr)] string destName, Sqlite3DatabaseHandle sourceDb, [MarshalAs (UnmanagedType.LPStr)] string sourceName); - [DllImport (LibraryPath, EntryPoint = "sqlite3_backup_step", CallingConvention = CallingConvention.Cdecl)] - public static extern Result BackupStep (Sqlite3BackupHandle backup, int numPages); + [DllImport (LibraryPath, EntryPoint = "sqlite3_backup_step", CallingConvention = CallingConvention.Cdecl)] + public static extern Result BackupStep (Sqlite3BackupHandle backup, int numPages); - [DllImport (LibraryPath, EntryPoint = "sqlite3_backup_finish", CallingConvention = CallingConvention.Cdecl)] - public static extern Result BackupFinish (Sqlite3BackupHandle backup); + [DllImport (LibraryPath, EntryPoint = "sqlite3_backup_finish", CallingConvention = CallingConvention.Cdecl)] + public static extern Result BackupFinish (Sqlite3BackupHandle backup); #else - public static Result Open (string filename, out Sqlite3DatabaseHandle db) - { - return (Result)Sqlite3.sqlite3_open (filename, out db); - } + public static Result Open (string filename, out Sqlite3DatabaseHandle db) + { + return (Result)Sqlite3.sqlite3_open (filename, out db); + } - public static Result Open (string filename, out Sqlite3DatabaseHandle db, int flags, string vfsName) - { + public static Result Open (string filename, out Sqlite3DatabaseHandle db, int flags, string vfsName) + { #if USE_WP8_NATIVE_SQLITE - return (Result)Sqlite3.sqlite3_open_v2(filename, out db, flags, vfsName ?? ""); + return (Result)Sqlite3.sqlite3_open_v2(filename, out db, flags, vfsName ?? ""); #else - return (Result)Sqlite3.sqlite3_open_v2 (filename, out db, flags, vfsName); + return (Result)Sqlite3.sqlite3_open_v2 (filename, out db, flags, vfsName); #endif - } - - public static Result Close (Sqlite3DatabaseHandle db) - { - return (Result)Sqlite3.sqlite3_close (db); - } - - public static Result Close2 (Sqlite3DatabaseHandle db) - { - return (Result)Sqlite3.sqlite3_close_v2 (db); - } - - public static Result BusyTimeout (Sqlite3DatabaseHandle db, int milliseconds) - { - return (Result)Sqlite3.sqlite3_busy_timeout (db, milliseconds); - } - - public static int Changes (Sqlite3DatabaseHandle db) - { - return Sqlite3.sqlite3_changes (db); - } - - public static Sqlite3Statement Prepare2 (Sqlite3DatabaseHandle db, string query) - { - Sqlite3Statement stmt = default (Sqlite3Statement); + } + + public static Result Close (Sqlite3DatabaseHandle db) + { + return (Result)Sqlite3.sqlite3_close (db); + } + + public static Result Close2 (Sqlite3DatabaseHandle db) + { + return (Result)Sqlite3.sqlite3_close_v2 (db); + } + + public static Result BusyTimeout (Sqlite3DatabaseHandle db, int milliseconds) + { + return (Result)Sqlite3.sqlite3_busy_timeout (db, milliseconds); + } + + public static int Changes (Sqlite3DatabaseHandle db) + { + return Sqlite3.sqlite3_changes (db); + } + + public static Sqlite3Statement Prepare2 (Sqlite3DatabaseHandle db, string query) + { + Sqlite3Statement stmt = default (Sqlite3Statement); #if USE_WP8_NATIVE_SQLITE || USE_SQLITEPCL_RAW - var r = Sqlite3.sqlite3_prepare_v2 (db, query, out stmt); + var r = Sqlite3.sqlite3_prepare_v2 (db, query, out stmt); #else - stmt = new Sqlite3Statement(); - var r = Sqlite3.sqlite3_prepare_v2(db, query, -1, ref stmt, 0); + stmt = new Sqlite3Statement(); + var r = Sqlite3.sqlite3_prepare_v2(db, query, -1, ref stmt, 0); #endif - if (r != 0) { - throw SQLiteException.New ((Result)r, GetErrmsg (db)); - } - return stmt; - } - - public static Result Step (Sqlite3Statement stmt) - { - return (Result)Sqlite3.sqlite3_step (stmt); - } - - public static Result Reset (Sqlite3Statement stmt) - { - return (Result)Sqlite3.sqlite3_reset (stmt); - } - - public static Result Finalize (Sqlite3Statement stmt) - { - return (Result)Sqlite3.sqlite3_finalize (stmt); - } - - public static long LastInsertRowid (Sqlite3DatabaseHandle db) - { - return Sqlite3.sqlite3_last_insert_rowid (db); - } - - public static string GetErrmsg (Sqlite3DatabaseHandle db) - { - return Sqlite3.sqlite3_errmsg (db).utf8_to_string (); - } - - public static int BindParameterIndex (Sqlite3Statement stmt, string name) - { - return Sqlite3.sqlite3_bind_parameter_index (stmt, name); - } - - public static int BindNull (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_bind_null (stmt, index); - } - - public static int BindInt (Sqlite3Statement stmt, int index, int val) - { - return Sqlite3.sqlite3_bind_int (stmt, index, val); - } - - public static int BindInt64 (Sqlite3Statement stmt, int index, long val) - { - return Sqlite3.sqlite3_bind_int64 (stmt, index, val); - } - - public static int BindDouble (Sqlite3Statement stmt, int index, double val) - { - return Sqlite3.sqlite3_bind_double (stmt, index, val); - } - - public static int BindText (Sqlite3Statement stmt, int index, string val, int n, IntPtr free) - { + if (r != 0) { + throw SQLiteException.New ((Result)r, GetErrmsg (db)); + } + return stmt; + } + + public static Result Step (Sqlite3Statement stmt) + { + return (Result)Sqlite3.sqlite3_step (stmt); + } + + public static Result Reset (Sqlite3Statement stmt) + { + return (Result)Sqlite3.sqlite3_reset (stmt); + } + + public static Result Finalize (Sqlite3Statement stmt) + { + return (Result)Sqlite3.sqlite3_finalize (stmt); + } + + public static long LastInsertRowid (Sqlite3DatabaseHandle db) + { + return Sqlite3.sqlite3_last_insert_rowid (db); + } + + public static string GetErrmsg (Sqlite3DatabaseHandle db) + { + return Sqlite3.sqlite3_errmsg (db).utf8_to_string (); + } + + public static int BindParameterIndex (Sqlite3Statement stmt, string name) + { + return Sqlite3.sqlite3_bind_parameter_index (stmt, name); + } + + public static int BindNull (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_bind_null (stmt, index); + } + + public static int BindInt (Sqlite3Statement stmt, int index, int val) + { + return Sqlite3.sqlite3_bind_int (stmt, index, val); + } + + public static int BindInt64 (Sqlite3Statement stmt, int index, long val) + { + return Sqlite3.sqlite3_bind_int64 (stmt, index, val); + } + + public static int BindDouble (Sqlite3Statement stmt, int index, double val) + { + return Sqlite3.sqlite3_bind_double (stmt, index, val); + } + + public static int BindText (Sqlite3Statement stmt, int index, string val, int n, IntPtr free) + { #if USE_WP8_NATIVE_SQLITE - return Sqlite3.sqlite3_bind_text(stmt, index, val, n); + return Sqlite3.sqlite3_bind_text(stmt, index, val, n); #elif USE_SQLITEPCL_RAW - return Sqlite3.sqlite3_bind_text (stmt, index, val); + return Sqlite3.sqlite3_bind_text (stmt, index, val); #else - return Sqlite3.sqlite3_bind_text(stmt, index, val, n, null); + return Sqlite3.sqlite3_bind_text(stmt, index, val, n, null); #endif - } + } - public static int BindBlob (Sqlite3Statement stmt, int index, byte[] val, int n, IntPtr free) - { + public static int BindBlob (Sqlite3Statement stmt, int index, byte[] val, int n, IntPtr free) + { #if USE_WP8_NATIVE_SQLITE - return Sqlite3.sqlite3_bind_blob(stmt, index, val, n); + return Sqlite3.sqlite3_bind_blob(stmt, index, val, n); #elif USE_SQLITEPCL_RAW - return Sqlite3.sqlite3_bind_blob (stmt, index, val); + return Sqlite3.sqlite3_bind_blob (stmt, index, val); #else - return Sqlite3.sqlite3_bind_blob(stmt, index, val, n, null); + return Sqlite3.sqlite3_bind_blob(stmt, index, val, n, null); #endif - } - - public static int ColumnCount (Sqlite3Statement stmt) - { - return Sqlite3.sqlite3_column_count (stmt); - } - - public static string ColumnName (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_name (stmt, index).utf8_to_string (); - } - - public static string ColumnName16 (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_name (stmt, index).utf8_to_string (); - } - - public static ColType ColumnType (Sqlite3Statement stmt, int index) - { - return (ColType)Sqlite3.sqlite3_column_type (stmt, index); - } - - public static int ColumnInt (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_int (stmt, index); - } - - public static long ColumnInt64 (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_int64 (stmt, index); - } - - public static double ColumnDouble (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_double (stmt, index); - } - - public static string ColumnText (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_text (stmt, index).utf8_to_string (); - } - - public static string ColumnText16 (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_text (stmt, index).utf8_to_string (); - } - - public static byte[] ColumnBlob (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_blob (stmt, index).ToArray (); - } - - public static int ColumnBytes (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_bytes (stmt, index); - } - - public static string ColumnString (Sqlite3Statement stmt, int index) - { - return Sqlite3.sqlite3_column_text (stmt, index).utf8_to_string (); - } - - public static byte[] ColumnByteArray (Sqlite3Statement stmt, int index) - { - int length = ColumnBytes (stmt, index); - if (length > 0) { - return ColumnBlob (stmt, index); - } - return new byte[0]; - } - - public static Result EnableLoadExtension (Sqlite3DatabaseHandle db, int onoff) - { - return (Result)Sqlite3.sqlite3_enable_load_extension (db, onoff); - } - - public static int LibVersionNumber () - { - return Sqlite3.sqlite3_libversion_number (); - } - - public static Result GetResult (Sqlite3DatabaseHandle db) - { - return (Result)Sqlite3.sqlite3_errcode (db); - } - - public static ExtendedResult ExtendedErrCode (Sqlite3DatabaseHandle db) - { - return (ExtendedResult)Sqlite3.sqlite3_extended_errcode (db); - } - - public static Sqlite3BackupHandle BackupInit (Sqlite3DatabaseHandle destDb, string destName, Sqlite3DatabaseHandle sourceDb, string sourceName) - { - return Sqlite3.sqlite3_backup_init (destDb, destName, sourceDb, sourceName); - } - - public static Result BackupStep (Sqlite3BackupHandle backup, int numPages) - { - return (Result)Sqlite3.sqlite3_backup_step (backup, numPages); - } - - public static Result BackupFinish (Sqlite3BackupHandle backup) - { - return (Result)Sqlite3.sqlite3_backup_finish (backup); - } + } + + public static int ColumnCount (Sqlite3Statement stmt) + { + return Sqlite3.sqlite3_column_count (stmt); + } + + public static string ColumnName (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_name (stmt, index).utf8_to_string (); + } + + public static string ColumnName16 (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_name (stmt, index).utf8_to_string (); + } + + public static ColType ColumnType (Sqlite3Statement stmt, int index) + { + return (ColType)Sqlite3.sqlite3_column_type (stmt, index); + } + + public static int ColumnInt (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_int (stmt, index); + } + + public static long ColumnInt64 (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_int64 (stmt, index); + } + + public static double ColumnDouble (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_double (stmt, index); + } + + public static string ColumnText (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_text (stmt, index).utf8_to_string (); + } + + public static string ColumnText16 (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_text (stmt, index).utf8_to_string (); + } + + public static byte[] ColumnBlob (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_blob (stmt, index).ToArray (); + } + + public static int ColumnBytes (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_bytes (stmt, index); + } + + public static string ColumnString (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_text (stmt, index).utf8_to_string (); + } + + public static byte[] ColumnByteArray (Sqlite3Statement stmt, int index) + { + int length = ColumnBytes (stmt, index); + if (length > 0) { + return ColumnBlob (stmt, index); + } + return new byte[0]; + } + + public static Result EnableLoadExtension (Sqlite3DatabaseHandle db, int onoff) + { + return (Result)Sqlite3.sqlite3_enable_load_extension (db, onoff); + } + + public static int LibVersionNumber () + { + return Sqlite3.sqlite3_libversion_number (); + } + + public static Result GetResult (Sqlite3DatabaseHandle db) + { + return (Result)Sqlite3.sqlite3_errcode (db); + } + + public static ExtendedResult ExtendedErrCode (Sqlite3DatabaseHandle db) + { + return (ExtendedResult)Sqlite3.sqlite3_extended_errcode (db); + } + + public static Sqlite3BackupHandle BackupInit (Sqlite3DatabaseHandle destDb, string destName, Sqlite3DatabaseHandle sourceDb, string sourceName) + { + return Sqlite3.sqlite3_backup_init (destDb, destName, sourceDb, sourceName); + } + + public static Result BackupStep (Sqlite3BackupHandle backup, int numPages) + { + return (Result)Sqlite3.sqlite3_backup_step (backup, numPages); + } + + public static Result BackupFinish (Sqlite3BackupHandle backup) + { + return (Result)Sqlite3.sqlite3_backup_finish (backup); + } #endif - public enum ColType : int - { - Integer = 1, - Float = 2, - Text = 3, - Blob = 4, - Null = 5 - } - } + public enum ColType : int + { + Integer = 1, + Float = 2, + Text = 3, + Blob = 4, + Null = 5 + } + } } From e57d39a71ec68d492a4db61abefdd29fbc1ffd8e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 14 Apr 2024 15:32:21 -0700 Subject: [PATCH 12/14] Remove exceptions for value type mapped tables --- src/SQLite.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index 33d712d7..a676d705 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -3553,11 +3553,18 @@ public IEnumerable ExecuteDeferredQuery (TableMapping map) MethodInfo getSetter = null; if (typeof(T) != map.MappedType) { #if NET8_0_OR_GREATER + // The runtime feature switch must be on a separate 'if' branch on its own, + // or the analyzer might not be able to correctly follow the program flow. if (!RuntimeFeature.IsDynamicCodeSupported) { if (map.MappedType.IsValueType) { - throw new NotSupportedException ( - $"Executing a query with a value type mapped type is not supported in AOT environments (The mapped type is '{map.MappedType}')."); + getSetter = null; } + else { + getSetter = FastColumnSetter.GetFastSetterMethodInfoUnsafe (map.MappedType); + } + } + else { + getSetter = FastColumnSetter.GetFastSetterMethodInfoUnsafe (map.MappedType); } #endif getSetter = FastColumnSetter.GetFastSetterMethodInfoUnsafe (map.MappedType); From 5985c0228e2c670ebb405c65235fdc85a967c7c7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Apr 2024 09:12:14 -0700 Subject: [PATCH 13/14] Fix incorrect '#ifdef' checks --- src/SQLite.cs | 4 ++-- src/SQLiteAsync.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index a676d705..d020ea7d 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -282,7 +282,7 @@ CreateTablesResult CreateTables< where T3 : new() where T4 : new() where T5 : new(); -#if NET +#if NET8_0_OR_GREATER [RequiresUnreferencedCode ("This method requires 'DynamicallyAccessedMemberTypes.All' on each input 'Type' instance.")] #endif CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None, params Type[] types); @@ -1091,7 +1091,7 @@ public CreateTablesResult CreateTables< /// /// Whether the table was created or migrated for each type. /// -#if NET +#if NET8_0_OR_GREATER [RequiresUnreferencedCode("This method requires 'DynamicallyAccessedMemberTypes.All' on each input 'Type' instance.")] #endif public CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None, params Type[] types) diff --git a/src/SQLiteAsync.cs b/src/SQLiteAsync.cs index 5883bea6..2138754a 100644 --- a/src/SQLiteAsync.cs +++ b/src/SQLiteAsync.cs @@ -141,7 +141,7 @@ Task CreateTablesAsync< where T3 : new() where T4 : new() where T5 : new(); -#if NET +#if NET8_0_OR_GREATER [RequiresUnreferencedCode ("This method requires 'DynamicallyAccessedMemberTypes.All' on each input 'Type' instance.")] #endif Task CreateTablesAsync (CreateFlags createFlags = CreateFlags.None, params Type[] types); @@ -712,7 +712,7 @@ public Task CreateTablesAsync< /// /// Whether the table was created or migrated for each type. /// -#if NET +#if NET8_0_OR_GREATER [RequiresUnreferencedCode ("This method requires 'DynamicallyAccessedMemberTypes.All' on each input 'Type' instance.")] #endif public Task CreateTablesAsync (CreateFlags createFlags = CreateFlags.None, params Type[] types) From 4eacdc21f4d534c219b80f75c8083218c2825037 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Apr 2024 09:13:00 -0700 Subject: [PATCH 14/14] Fix missing '#else' check --- src/SQLite.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index d020ea7d..ae80093c 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -3566,8 +3566,9 @@ public IEnumerable ExecuteDeferredQuery (TableMapping map) else { getSetter = FastColumnSetter.GetFastSetterMethodInfoUnsafe (map.MappedType); } -#endif +#else getSetter = FastColumnSetter.GetFastSetterMethodInfoUnsafe (map.MappedType); +#endif } for (int i = 0; i < cols.Length; i++) {