Skip to content

Commit

Permalink
Allow storing TimeSpans as strings (default to ticks)
Browse files Browse the repository at this point in the history
  • Loading branch information
gtbX committed Sep 10, 2019
1 parent 78cd2cd commit cf41ec3
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 12 deletions.
52 changes: 40 additions & 12 deletions src/SQLite.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ public partial class SQLiteConnection : IDisposable
/// </summary>
public bool StoreDateTimeAsTicks { get; private set; }

/// <summary>
/// Whether to store TimeSpan properties as ticks (true) or strings (false).
/// </summary>
public bool StoreTimeSpanAsTicks { get; private set; }

/// <summary>
/// The format to use when storing DateTime properties as strings. Ignored if StoreDateTimeAsTicks is true.
/// </summary>
Expand Down Expand Up @@ -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 += ")";
Expand Down Expand Up @@ -826,7 +831,7 @@ void MigrateTable (TableMapping map, List<ColumnInfo> 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);
}
}
Expand Down Expand Up @@ -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; }
Expand Down Expand Up @@ -2219,13 +2225,20 @@ public SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks, o
/// <param name="dateTimeStringFormat">
/// Specifies the format to use when storing DateTime properties as strings.
/// </param>
public SQLiteConnectionString (string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks, object key = null, Action<SQLiteConnection> preKeyAction = null, Action<SQLiteConnection> postKeyAction = null, string vfsName = null, string dateTimeStringFormat = DateTimeSqliteDefaultFormat)
/// <param name="storeTimeSpanAsTicks">
/// 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.
/// </param>
public SQLiteConnectionString (string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks, object key = null, Action<SQLiteConnection> preKeyAction = null, Action<SQLiteConnection> 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;
Expand Down Expand Up @@ -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 ";
Expand All @@ -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)) {
Expand All @@ -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";
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions src/SQLiteAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ public Task EnableWriteAheadLoggingAsync ()
/// Whether to store DateTime properties as ticks (true) or strings (false).
/// </summary>
public bool StoreDateTimeAsTicks => GetConnection ().StoreDateTimeAsTicks;

/// <summary>
/// Whether to store TimeSpan properties as ticks (true) or strings (false).
/// </summary>
public bool StoreTimeSpanAsTicks => GetConnection ().StoreTimeSpanAsTicks;

/// <summary>
/// Whether to writer queries to <see cref="Tracer"/> during execution.
Expand Down
1 change: 1 addition & 0 deletions tests/SQLite.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
<Compile Include="BackupTest.cs" />
<Compile Include="ReadmeTest.cs" />
<Compile Include="QueryTest.cs" />
<Compile Include="TimeSpanTest.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
Expand Down
85 changes: 85 additions & 0 deletions tests/TimeSpanTest.cs
Original file line number Diff line number Diff line change
@@ -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<TestObj> ().Wait ();

TestObj o, o2;

o = new TestObj {
Duration = new TimeSpan (42, 12, 33, 20, 501),
};
db.InsertAsync (o).Wait ();
o2 = db.GetAsync<TestObj> (o.Id).Result;
Assert.AreEqual (o.Duration, o2.Duration);
}

void TestTimeSpan (TestDb db)
{
db.CreateTable<TestObj> ();

TestObj o, o2;

o = new TestObj {
Duration = new TimeSpan (42, 12, 33, 20, 501),
};
db.Insert (o);
o2 = db.Get<TestObj> (o.Id);
Assert.AreEqual (o.Duration, o2.Duration);
}
}
}

1 comment on commit cf41ec3

@tipa
Copy link
Contributor

@tipa tipa commented on cf41ec3 Sep 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like it defaults to strings, not to ticks. Or am I missing something?
This change broke my code as I was saving timespans as ticks, now the code tries to parse them as strings.
I am creating the database connection like this:
var _db = new SQLiteConnection(databasePath, SQLiteOpenFlags.Create | SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.FullMutex);

Please sign in to comment.