DateTime Values in SQLite When Using MvvmCross

July 14th 2014 SQLite MvvmCross

In my spare time I'm developing an application in MvvmCross, using SQLite for local data storage. I'm taking advantage of MvvmCross SQLite-Net plugin. Recently I stumbled across a very strange behavior. The issue involved a fairly simple table with a DATETIME column:

CREATE TABLE [Session] (
  [Id] INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
  [StartedAt] DATETIME NOT NULL,
  [ClosedAt] DATETIME,
  [Remark] NVARCHAR(100));

I had a corresponding model class in my code:

public class Session
{
  [PrimaryKey, AutoIncrement]
  public int Id { get; set; }
  public DateTime StartedAt { get; set; }
  public DateTime? ClosedAt { get; set; }
  public string Remark { get; set; }
}

I used the following code for inserting new records:

var session = new Session
{
  StartedAt = DateTime.Now
};
connection.Insert(session);

Re-reading the value from the database also behaved as expected:

var result = connection.Table<Session>.ToList()
                       .Single(s => s.Id == session.Id);
// no error here
Assert.AreEqual(session.StartedAt, result.StartedAt);

Everything seemed okay, until I tried to take a look at the table using SQLite Expert. This is how the above inserted record looked like:

Inserted record, as seen in SQLite Expert

Of course, this called for further investigation.

It turns out, SQLite's handling of DateTime values is quite strange. There is no separate storage class for DateTime values. Instead, they can be stored as TEXT, REAL or INTEGER values. Built-in date and time functions transparently support either one of those.

To make matters even more complicated, SQLite supports DATETIME data type inside CREATE TABLE statements. Such a column is assigned a NUMERIC type affinity which isn't a storage class, either. It can contain values of any supported storage class, but tries to convert the inserted text to INTEGER or REAL if it can be done in a lossless manner.

Now is the time to include SQLite-Net into the equation. This library supports storing of DateTime values either as TEXT or as INTEGER values. The mode is selected by the storeDateTimeAsTicks flag when creating a new SQLiteConnection. When creating a table with the library, the column data type is either DATETIME (for TEXT values) or BIGINT (for INTEGER values, i.e. ticks).

By default the library doesn't store DateTime values as ticks, which should still make everything work fine in my case. But here's, where the third player enters the stage: MvvmCross SQLite-Net plugin. It is actually a fork of SQLite-Net library with only minor changes to make it work easily with MvvmCross. But the one thing it does change, is the default storage mode for DateTime values: when using the plugin, by default they are stored as ticks, i.e. INTEGER values. This finally explains the strange behavior I observed: obviously SQLite Expert incorrectly interpreted the stored value since it didn't match its expectations based on the column data type.

I decided to avoid the issue by switching the DateTime storage mode in SQLite-Net back to TEXT. MvvmCross abstracts platform specifics by providing a different SQliteConnectionFactory for each one of them. These factories implement two interfaces: ISQLiteConnectionFactory and ISQLiteConnectionFactoryEx. Platform-specific application setup registers the correct factory which implements both factory interfaces.

Usually you'll obtain the correct factory in your view model by adding a constructor parameter of the required type. As long as you're satisfied with the default connection settings, you can use the ISQLiteConnectionFactory interface:

public abstract class ViewModelBase
{
  private readonly ISQLiteConnectionFactory _sqliteConnectionFactory;

  private string _filename = "db.sqlite";

  protected ViewModelBase(ISQLiteConnectionFactory sqliteConnectionFactory)
  {
    _sqliteConnectionFactory = sqliteConnectionFactory;
  }

  protected ISQLiteConnection CreateConnection()
  {
    return _sqliteConnectionFactory.Create(_filename);
  }
}

To change the default setting, ISQLiteConnectionFactoryEx interface needs to be used:

public abstract class ViewModelBase
{
  private readonly ISQLiteConnectionFactoryEx _sqliteConnectionFactory;

  private string _filename = "db.sqlite";

  protected ViewModelBase(ISQLiteConnectionFactoryEx sqliteConnectionFactory)
  {
    _sqliteConnectionFactory = sqliteConnectionFactory;
  }

  protected ISQLiteConnection CreateConnection()
  {
    return _sqliteConnectionFactory.CreateEx(_filename,
      new SQLiteConnectionOptions { StoreDateTimeAsTicks = false });
  }
}

No other code needs to be changed.

I chose TEXT storage class over INTEGER because of convenience. Having the column data type of DATETIME instead of BIGINT makes it much easier to work with data outside my application. This choice does have a disadvantage, though: less precision in stored values. In particular, the following test will now most likely fail:

var session = new Session
{
  StartedAt = DateTime.Now
};
connection.Insert(session);

var result = connection.Table<Session>.ToList()
                       .Single(s => s.Id == session.Id);
// will fail most of the time:
// milliseconds are stripped when stored to database
Assert.AreEqual(session.StartedAt, result.StartedAt);

In my case this wasn't an issue. Seconds are enough precise for me. You'll have to decide for yourself, which mode of storage is more suitable in your situation, though.

Copyright
Creative Commons License