diff --git a/src/SQLite.cs b/src/SQLite.cs index 335d5923..60cff5b8 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -210,6 +210,11 @@ public partial class SQLiteConnection : IDisposable /// 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. /// @@ -560,7 +565,7 @@ public CreateTableResult CreateTable (Type ty, CreateFlags createFlags = CreateF // Build query. var query = "create " + @virtual + "table if not exists \"" + map.TableName + "\" " + @using + "(\n"; - var decls = map.Columns.Select (p => Orm.SqlDecl (p, StoreDateTimeAsTicks)); + var decls = map.Columns.Select (p => Orm.SqlDecl (p, StoreDateTimeAsTicks, StoreTimeSpanAsTicks)); var decl = string.Join (",\n", decls.ToArray ()); query += decl; query += ")"; @@ -826,7 +831,7 @@ void MigrateTable (TableMapping map, List existingCols) } foreach (var p in toBeAdded) { - var addCol = "alter table \"" + map.TableName + "\" add column " + Orm.SqlDecl (p, StoreDateTimeAsTicks); + var addCol = "alter table \"" + map.TableName + "\" add column " + Orm.SqlDecl (p, StoreDateTimeAsTicks, StoreTimeSpanAsTicks); Execute (addCol); } } @@ -2110,6 +2115,7 @@ public class SQLiteConnectionString 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; } @@ -2219,13 +2225,20 @@ public SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks, o /// /// Specifies the format to use when storing DateTime properties as strings. /// - public SQLiteConnectionString (string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks, object key = null, Action preKeyAction = null, Action postKeyAction = null, string vfsName = null, string dateTimeStringFormat = DateTimeSqliteDefaultFormat) + /// + /// 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; @@ -2623,9 +2636,9 @@ public static Type GetType (object obj) return obj.GetType (); } - public static string SqlDecl (TableMapping.Column p, bool storeDateTimeAsTicks) + public static string SqlDecl (TableMapping.Column p, bool storeDateTimeAsTicks, bool storeTimeSpanAsTicks) { - string decl = "\"" + p.Name + "\" " + SqlType (p, storeDateTimeAsTicks) + " "; + string decl = "\"" + p.Name + "\" " + SqlType (p, storeDateTimeAsTicks, storeTimeSpanAsTicks) + " "; if (p.IsPK) { decl += "primary key "; @@ -2643,7 +2656,7 @@ public static string SqlDecl (TableMapping.Column p, bool storeDateTimeAsTicks) return decl; } - public static string SqlType (TableMapping.Column p, bool storeDateTimeAsTicks) + 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)) { @@ -2661,7 +2674,7 @@ public static string SqlType (TableMapping.Column p, bool storeDateTimeAsTicks) return "varchar"; } else if (clrType == typeof (TimeSpan)) { - return "bigint"; + return storeTimeSpanAsTicks ? "bigint" : "time"; } else if (clrType == typeof (DateTime)) { return storeDateTimeAsTicks ? "bigint" : "datetime"; @@ -2947,13 +2960,13 @@ void BindAll (Sqlite3Statement stmt) b.Index = nextIdx++; } - BindParameter (stmt, b.Index, b.Value, _conn.StoreDateTimeAsTicks, _conn.DateTimeStringFormat); + 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) + internal static void BindParameter (Sqlite3Statement stmt, int index, object value, bool storeDateTimeAsTicks, string dateTimeStringFormat, bool storeTimeSpanAsTicks) { if (value == null) { SQLite3.BindNull (stmt, index); @@ -2978,7 +2991,12 @@ internal static void BindParameter (Sqlite3Statement stmt, int index, object val SQLite3.BindDouble (stmt, index, Convert.ToDouble (value)); } else if (value is TimeSpan) { - SQLite3.BindInt64 (stmt, index, ((TimeSpan)value).Ticks); + 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) { @@ -3061,7 +3079,17 @@ object ReadCol (Sqlite3Statement stmt, int index, SQLite3.ColType type, Type clr return (float)SQLite3.ColumnDouble (stmt, index); } else if (clrType == typeof (TimeSpan)) { - return new TimeSpan (SQLite3.ColumnInt64 (stmt, index)); + 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) { @@ -3174,7 +3202,7 @@ public int ExecuteNonQuery (object[] source) //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); + SQLiteCommand.BindParameter (Statement, i + 1, source[i], Connection.StoreDateTimeAsTicks, Connection.DateTimeStringFormat, Connection.StoreTimeSpanAsTicks); } } r = SQLite3.Step (Statement); diff --git a/src/SQLiteAsync.cs b/src/SQLiteAsync.cs index 77255098..ac82d828 100644 --- a/src/SQLiteAsync.cs +++ b/src/SQLiteAsync.cs @@ -137,6 +137,11 @@ public Task EnableWriteAheadLoggingAsync () /// Whether to store DateTime properties as ticks (true) or strings (false). /// public bool StoreDateTimeAsTicks => GetConnection ().StoreDateTimeAsTicks; + + /// + /// Whether to store TimeSpan properties as ticks (true) or strings (false). + /// + public bool StoreTimeSpanAsTicks => GetConnection ().StoreTimeSpanAsTicks; /// /// Whether to writer queries to during execution. diff --git a/tests/SQLite.Tests.csproj b/tests/SQLite.Tests.csproj index 77d8dc7a..06e4de58 100644 --- a/tests/SQLite.Tests.csproj +++ b/tests/SQLite.Tests.csproj @@ -84,6 +84,7 @@ + diff --git a/tests/TimeSpanTest.cs b/tests/TimeSpanTest.cs new file mode 100644 index 00000000..af169a17 --- /dev/null +++ b/tests/TimeSpanTest.cs @@ -0,0 +1,85 @@ +using System; +using System.Threading.Tasks; + +#if NETFX_CORE +using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using SetUp = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestInitializeAttribute; +using TestFixture = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestClassAttribute; +using Test = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestMethodAttribute; +#else +using NUnit.Framework; +#endif + +namespace SQLite.Tests +{ + [TestFixture] + public class TimeSpanTest + { + class TestObj + { + [PrimaryKey, AutoIncrement] + public int Id { get; set; } + + public string Name { get; set; } + public TimeSpan Duration { get; set; } + } + + [Test] + public void AsTicks () + { + var db = new TestDb (TimeSpanAsTicks (true)); + TestTimeSpan (db); + } + + [Test] + public void AsStrings () + { + var db = new TestDb (TimeSpanAsTicks (false)); + TestTimeSpan (db); + } + + [Test] + public void AsyncAsTicks () + { + var db = new SQLiteAsyncConnection (TimeSpanAsTicks (true)); + TestAsyncTimeSpan (db); + } + + [Test] + public void AsyncAsStrings () + { + var db = new SQLiteAsyncConnection (TimeSpanAsTicks (false)); + TestAsyncTimeSpan (db); + } + + SQLiteConnectionString TimeSpanAsTicks (bool asTicks = true) => new SQLiteConnectionString (TestPath.GetTempFileName (), SQLiteOpenFlags.Create | SQLiteOpenFlags.ReadWrite, true, storeTimeSpanAsTicks: asTicks); + + void TestAsyncTimeSpan (SQLiteAsyncConnection db) + { + db.CreateTableAsync ().Wait (); + + TestObj o, o2; + + o = new TestObj { + Duration = new TimeSpan (42, 12, 33, 20, 501), + }; + db.InsertAsync (o).Wait (); + o2 = db.GetAsync (o.Id).Result; + Assert.AreEqual (o.Duration, o2.Duration); + } + + void TestTimeSpan (TestDb db) + { + db.CreateTable (); + + TestObj o, o2; + + o = new TestObj { + Duration = new TimeSpan (42, 12, 33, 20, 501), + }; + db.Insert (o); + o2 = db.Get (o.Id); + Assert.AreEqual (o.Duration, o2.Duration); + } + } +}