diff --git a/src/SQLite.cs b/src/SQLite.cs index c1eac952..7504a8d3 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -210,6 +210,23 @@ 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. + /// + /// 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 () { @@ -298,6 +315,8 @@ public SQLiteConnection (SQLiteConnectionString connectionString) _open = true; StoreDateTimeAsTicks = connectionString.StoreDateTimeAsTicks; + DateTimeStringFormat = connectionString.DateTimeStringFormat; + DateTimeStyle = connectionString.DateTimeStyle; BusyTimeout = TimeSpan.FromSeconds (0.1); Tracer = line => Debug.WriteLine (line); @@ -546,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 += ")"; @@ -812,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); } } @@ -2090,9 +2109,14 @@ public enum NotifyTableChangedAction /// 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; } @@ -2194,13 +2218,25 @@ public SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks, o /// /// Specifies the Virtual File System to use on the database. /// - public SQLiteConnectionString (string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks, object key = null, Action preKeyAction = null, Action postKeyAction = null, string vfsName = null) + /// + /// 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; @@ -2596,9 +2632,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 "; @@ -2616,7 +2652,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)) { @@ -2634,7 +2670,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"; @@ -2920,15 +2956,13 @@ void BindAll (Sqlite3Statement stmt) b.Index = nextIdx++; } - BindParameter (stmt, b.Index, b.Value, _conn.StoreDateTimeAsTicks); + BindParameter (stmt, b.Index, b.Value, _conn.StoreDateTimeAsTicks, _conn.DateTimeStringFormat, _conn.StoreTimeSpanAsTicks); } } static IntPtr NegativePointer = new IntPtr (-1); - const string DateTimeExactStoreFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff"; - - internal static void BindParameter (Sqlite3Statement stmt, int index, object value, bool storeDateTimeAsTicks) + internal static void BindParameter (Sqlite3Statement stmt, int index, object value, bool storeDateTimeAsTicks, string dateTimeStringFormat, bool storeTimeSpanAsTicks) { if (value == null) { SQLite3.BindNull (stmt, index); @@ -2953,14 +2987,19 @@ 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) { SQLite3.BindInt64 (stmt, index, ((DateTime)value).Ticks); } else { - SQLite3.BindText (stmt, index, ((DateTime)value).ToString (DateTimeExactStoreFormat, System.Globalization.CultureInfo.InvariantCulture), -1, NegativePointer); + SQLite3.BindText (stmt, index, ((DateTime)value).ToString (dateTimeStringFormat, System.Globalization.CultureInfo.InvariantCulture), -1, NegativePointer); } } else if (value is DateTimeOffset) { @@ -3036,7 +3075,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) { @@ -3045,7 +3094,7 @@ object ReadCol (Sqlite3Statement stmt, int index, SQLite3.ColType type, Type clr else { var text = SQLite3.ColumnString (stmt, index); DateTime resultDate; - if (!DateTime.TryParseExact (text, DateTimeExactStoreFormat, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out resultDate)) { + if (!DateTime.TryParseExact (text, _conn.DateTimeStringFormat, System.Globalization.CultureInfo.InvariantCulture, _conn.DateTimeStyle, out resultDate)) { resultDate = DateTime.Parse (text); } return resultDate; @@ -3149,7 +3198,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); + 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/DateTimeTest.cs b/tests/DateTimeTest.cs index 37e42fbe..949c8051 100644 --- a/tests/DateTimeTest.cs +++ b/tests/DateTimeTest.cs @@ -35,7 +35,15 @@ public void AsTicks () [Test] public void AsStrings () { - var db = new TestDb (storeDateTimeAsTicks: false); + var db = new TestDb (storeDateTimeAsTicks: false); + TestDateTime (db); + } + + [TestCase ("o")] + [TestCase ("MMM'-'dd'-'yyyy' 'HH':'mm':'ss'.'fffffff")] + public void AsCustomStrings (string format) + { + var db = new TestDb (CustomDateTimeString (format)); TestDateTime (db); } @@ -53,6 +61,16 @@ public void AsyncAsString () TestAsyncDateTime (db); } + [TestCase ("o")] + [TestCase ("MMM'-'dd'-'yyyy' 'HH':'mm':'ss'.'fffffff")] + public void AsyncAsCustomStrings (string format) + { + var db = new SQLiteAsyncConnection (CustomDateTimeString (format)); + TestAsyncDateTime (db); + } + + SQLiteConnectionString CustomDateTimeString (string dateTimeFormat) => new SQLiteConnectionString (TestPath.GetTempFileName (), false, dateTimeFormat); + void TestAsyncDateTime (SQLiteAsyncConnection db) { db.CreateTableAsync ().Wait (); 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/TestDb.cs b/tests/TestDb.cs index 9b4ed8d9..56cd3a99 100644 --- a/tests/TestDb.cs +++ b/tests/TestDb.cs @@ -60,6 +60,13 @@ public TestDb (bool storeDateTimeAsTicks = true, object key = null, bool wal = t EnableWriteAheadLogging (); } + public TestDb (SQLiteConnectionString connectionString, bool wal = true) : base (connectionString) + { + Trace = true; + if (wal) + EnableWriteAheadLogging (); + } + public TestDb (string path, bool storeDateTimeAsTicks = true, object key = null, bool wal = true) : base (new SQLiteConnectionString (path, storeDateTimeAsTicks, key: key)) { Trace = true; 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); + } + } +}