Skip to content

Commit

Permalink
Merge pull request #869 from perfectco/custom-datetime
Browse files Browse the repository at this point in the history
Custom datetime format, string-based timespan
  • Loading branch information
praeclarum authored Sep 20, 2019
2 parents 1454c82 + 8649a68 commit 9f7c08c
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 17 deletions.
81 changes: 65 additions & 16 deletions src/SQLite.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,23 @@ 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>
/// <value>The date time string format.</value>
public string DateTimeStringFormat { get; private set; }

/// <summary>
/// The DateTimeStyles value to use when parsing a DateTime property string.
/// </summary>
/// <value>The date time style.</value>
internal System.Globalization.DateTimeStyles DateTimeStyle { get; private set; }

#if USE_SQLITEPCL_RAW && !NO_SQLITEPCL_RAW_BATTERIES
static SQLiteConnection ()
{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 += ")";
Expand Down Expand Up @@ -812,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 @@ -2090,9 +2109,14 @@ public enum NotifyTableChangedAction
/// </summary>
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<SQLiteConnection> PreKeyAction { get; }
Expand Down Expand Up @@ -2194,13 +2218,25 @@ public SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks, o
/// <param name="vfsName">
/// Specifies the Virtual File System to use on the database.
/// </param>
public SQLiteConnectionString (string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks, object key = null, Action<SQLiteConnection> preKeyAction = null, Action<SQLiteConnection> postKeyAction = null, string vfsName = null)
/// <param name="dateTimeStringFormat">
/// Specifies the format to use when storing DateTime properties as strings.
/// </param>
/// <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;
PreKeyAction = preKeyAction;
PostKeyAction = postKeyAction;
Expand Down Expand Up @@ -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 ";
Expand All @@ -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)) {
Expand All @@ -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";
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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);
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
20 changes: 19 additions & 1 deletion tests/DateTimeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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<TestObj> ().Wait ();
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
7 changes: 7 additions & 0 deletions tests/TestDb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
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);
}
}
}

0 comments on commit 9f7c08c

Please sign in to comment.