Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 108 additions & 6 deletions src/SQLite.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ public interface ISQLiteConnection : IDisposable
bool StoreDateTimeAsTicks { get; }
bool StoreTimeSpanAsTicks { get; }
string DateTimeStringFormat { get; }
#if NET6_0_OR_GREATER
string DateStringFormat { get; }
#endif
TimeSpan BusyTimeout { get; set; }
IEnumerable<TableMapping> TableMappings { get; }
bool IsInTransaction { get; }
Expand Down Expand Up @@ -499,6 +502,14 @@ public partial class SQLiteConnection : ISQLiteConnection
/// <value>The date time string format.</value>
public string DateTimeStringFormat { get; private set; }

#if NET6_0_OR_GREATER
/// <summary>
/// The format to use when storing Date properties as strings.
/// </summary>
/// <value>The date string format.</value>
public string DateStringFormat { get; private set; }
#endif

/// <summary>
/// The DateTimeStyles value to use when parsing a DateTime property string.
/// </summary>
Expand Down Expand Up @@ -595,6 +606,9 @@ public SQLiteConnection (SQLiteConnectionString connectionString)
StoreDateTimeAsTicks = connectionString.StoreDateTimeAsTicks;
StoreTimeSpanAsTicks = connectionString.StoreTimeSpanAsTicks;
DateTimeStringFormat = connectionString.DateTimeStringFormat;
#if NET6_0_OR_GREATER
DateStringFormat = connectionString.DateStringFormat;
#endif
DateTimeStyle = connectionString.DateTimeStyle;

BusyTimeout = TimeSpan.FromSeconds (1.0);
Expand Down Expand Up @@ -2659,12 +2673,17 @@ public enum NotifyTableChangedAction
public class SQLiteConnectionString
{
const string DateTimeSqliteDefaultFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff";

#if NET6_0_OR_GREATER
const string DateSqliteDefaultFormat = "yyyy'-'MM'-'dd";
#endif
public string UniqueKey { get; }
public string DatabasePath { get; }
public bool StoreDateTimeAsTicks { get; }
public bool StoreTimeSpanAsTicks { get; }
public string DateTimeStringFormat { get; }
#if NET6_0_OR_GREATER
public string DateStringFormat { get; }
#endif
public System.Globalization.DateTimeStyles DateTimeStyle { get; }
public object Key { get; }
public SQLiteOpenFlags OpenFlags { get; }
Expand Down Expand Up @@ -2738,7 +2757,8 @@ public SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks, o
{
}

/// <summary>
#if NET6_0_OR_GREATER
/// <summary>
/// Constructs a new SQLiteConnectionString with all the data needed to open an SQLiteConnection.
/// </summary>
/// <param name="databasePath">
Expand Down Expand Up @@ -2770,13 +2790,60 @@ public SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks, o
/// <param name="dateTimeStringFormat">
/// Specifies the format to use when storing DateTime properties as strings.
/// </param>
/// <param name="dateStringFormat">
/// Specifies the format to use when storing DateOnly properties.
/// </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)
#else
/// <summary>
/// Constructs a new SQLiteConnectionString with all the data needed to open an SQLiteConnection.
/// </summary>
/// <param name="databasePath">
/// Specifies the path to the database file.
/// </param>
/// <param name="openFlags">
/// Flags controlling how the connection should be opened.
/// </param>
/// <param name="storeDateTimeAsTicks">
/// Specifies whether to store DateTime 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 storeDateTimeAsTicks = true.
/// If you use DateTimeOffset properties, it will be always stored as ticks regardingless
/// the storeDateTimeAsTicks parameter.
/// </param>
/// <param name="key">
/// Specifies the encryption key to use on the database. Should be a string or a byte[].
/// </param>
/// <param name="preKeyAction">
/// Executes prior to setting key for SQLCipher databases
/// </param>
/// <param name="postKeyAction">
/// Executes after setting key for SQLCipher databases
/// </param>
/// <param name="vfsName">
/// Specifies the Virtual File System to use on the database.
/// </param>
/// <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>
#endif
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,
#if NET6_0_OR_GREATER
string dateStringFormat = DateSqliteDefaultFormat,
#endif
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));
Expand All @@ -2785,6 +2852,9 @@ public SQLiteConnectionString (string databasePath, SQLiteOpenFlags openFlags, b
StoreDateTimeAsTicks = storeDateTimeAsTicks;
StoreTimeSpanAsTicks = storeTimeSpanAsTicks;
DateTimeStringFormat = dateTimeStringFormat;
#if NET6_0_OR_GREATER
DateStringFormat = dateStringFormat;
#endif
DateTimeStyle = "o".Equals (DateTimeStringFormat, StringComparison.OrdinalIgnoreCase) || "r".Equals (DateTimeStringFormat, StringComparison.OrdinalIgnoreCase) ? System.Globalization.DateTimeStyles.RoundtripKind : System.Globalization.DateTimeStyles.None;
Key = key;
PreKeyAction = preKeyAction;
Expand Down Expand Up @@ -3324,6 +3394,11 @@ public static string SqlType (TableMapping.Column p, bool storeDateTimeAsTicks,
else if (clrType == typeof (DateTime)) {
return storeDateTimeAsTicks ? "bigint" : "datetime";
}
#if NET6_0_OR_GREATER
else if (clrType == typeof (DateOnly)) {
return "text";
}
#endif
else if (clrType == typeof (DateTimeOffset)) {
return "bigint";
}
Expand Down Expand Up @@ -3715,13 +3790,21 @@ void BindAll (Sqlite3Statement stmt)
b.Index = nextIdx++;
}

BindParameter (stmt, b.Index, b.Value, _conn.StoreDateTimeAsTicks, _conn.DateTimeStringFormat, _conn.StoreTimeSpanAsTicks);
BindParameter (stmt, b.Index, b.Value, _conn.StoreDateTimeAsTicks, _conn.DateTimeStringFormat,
#if NET6_0_OR_GREATER
_conn.DateStringFormat,
#endif
_conn.StoreTimeSpanAsTicks);
}
}

static IntPtr NegativePointer = new IntPtr (-1);

internal static void BindParameter (Sqlite3Statement stmt, int index, object value, bool storeDateTimeAsTicks, string dateTimeStringFormat, bool storeTimeSpanAsTicks)
internal static void BindParameter (Sqlite3Statement stmt, int index, object value, bool storeDateTimeAsTicks, string dateTimeStringFormat,
#if NET6_0_OR_GREATER
string dateStringFormat,
#endif
bool storeTimeSpanAsTicks)
{
if (value == null) {
SQLite3.BindNull (stmt, index);
Expand Down Expand Up @@ -3764,6 +3847,11 @@ internal static void BindParameter (Sqlite3Statement stmt, int index, object val
else if (value is DateTimeOffset) {
SQLite3.BindInt64 (stmt, index, ((DateTimeOffset)value).UtcTicks);
}
#if NET6_0_OR_GREATER
else if (value is DateOnly) {
SQLite3.BindText (stmt, index, ((DateOnly)value).ToString (dateStringFormat, System.Globalization.CultureInfo.InvariantCulture), -1, NegativePointer);
}
#endif
else if (value is byte[]) {
SQLite3.BindBlob (stmt, index, (byte[])value, ((byte[])value).Length, NegativePointer);
}
Expand Down Expand Up @@ -3870,6 +3958,16 @@ object ReadCol (Sqlite3Statement stmt, int index, SQLite3.ColType type, Type clr
else
return SQLite3.ColumnInt (stmt, index);
}
#if NET6_0_OR_GREATER
else if (clrType == typeof (DateOnly)) {
var text = SQLite3.ColumnString (stmt, index);
DateOnly resultDate;
if (!DateOnly.TryParseExact (text, _conn.DateStringFormat, System.Globalization.CultureInfo.InvariantCulture, _conn.DateTimeStyle, out resultDate)) {
resultDate = DateOnly.Parse (text);
}
return resultDate;
}
#endif
else if (clrType == typeof (Int64)) {
return SQLite3.ColumnInt64 (stmt, index);
}
Expand Down Expand Up @@ -4203,7 +4301,11 @@ 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, Connection.StoreTimeSpanAsTicks);
SQLiteCommand.BindParameter (Statement, i + 1, source[i], Connection.StoreDateTimeAsTicks, Connection.DateTimeStringFormat,
#if NET6_0_OR_GREATER
Connection.DateStringFormat,
#endif
Connection.StoreTimeSpanAsTicks);
}
}
r = SQLite3.Step (Statement);
Expand Down
120 changes: 120 additions & 0 deletions tests/SQLite.Tests/DateOnlyTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#if NET6_0_OR_GREATER
using System;
using System.Threading.Tasks;
using NUnit.Framework;

namespace SQLite.Tests
{
[TestFixture]
public class DateOnlyTest
{
const string DefaultSQLiteDateString = "yyyy'-'MM'-'dd";

class TestObj
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }

public string Name { get; set; }
public DateOnly ModifiedDate { get; set; }
}

[Test]
public void AsStrings ()
{
var date = new DateOnly (2012, 1, 14);
var db = new TestDb (storeDateTimeAsTicks: false);
TestDateOnly (db, date, date.ToString (DefaultSQLiteDateString));
}

[TestCase ("o")]
[TestCase ("MMM'-'dd'-'yyyy")]
public void AsCustomStrings (string format)
{
var dateTime = new DateOnly(2012, 1, 14);
var db = new TestDb (CustomDateString (format));
TestDateOnly (db, dateTime, dateTime.ToString (format, System.Globalization.CultureInfo.InvariantCulture));
}

[Test]
public void AsyncAsString ()
{
var date = new DateOnly(2012, 1, 14);
var db = new SQLiteAsyncConnection (TestPath.GetTempFileName (), false);
TestAsyncDateTime (db, date, date.ToString (DefaultSQLiteDateString));
}

[TestCase ("o")]
[TestCase ("MMM'-'dd'-'yyyy")]
public void AsyncAsCustomStrings (string format)
{
var dateTime = new DateOnly (2012, 1, 14);
var db = new SQLiteAsyncConnection (CustomDateString (format));
TestAsyncDateTime (db, dateTime, dateTime.ToString (format,System.Globalization.CultureInfo.InvariantCulture));
}

SQLiteConnectionString CustomDateString (string dateTimeFormat) => new SQLiteConnectionString (TestPath.GetTempFileName (), SQLiteOpenFlags.Create | SQLiteOpenFlags.ReadWrite, false, dateStringFormat: dateTimeFormat);

void TestAsyncDateTime (SQLiteAsyncConnection db, DateOnly dateTime, string expected)
{
db.CreateTableAsync<TestObj> ().Wait ();

TestObj o, o2;

o = new TestObj {
ModifiedDate = dateTime,
};
db.InsertAsync (o).Wait ();
o2 = db.GetAsync<TestObj> (o.Id).Result;
Assert.AreEqual (o.ModifiedDate, o2.ModifiedDate);

var stored = db.ExecuteScalarAsync<string> ("SELECT ModifiedDate FROM TestObj;").Result;
Assert.AreEqual (expected, stored);
}

void TestDateOnly (TestDb db, DateOnly date, string expected)
{
db.CreateTable<TestObj> ();

TestObj o, o2;

o = new TestObj {
ModifiedDate = date,
};
db.Insert (o);
o2 = db.Get<TestObj> (o.Id);
Assert.AreEqual (o.ModifiedDate, o2.ModifiedDate);

var stored = db.ExecuteScalar<string> ("SELECT ModifiedDate FROM TestObj;");
Assert.AreEqual (expected, stored);
}

class NullableDateObj
{
public DateTime? Time { get; set; }
}

[Test]
public async Task LinqNullable ()
{
foreach (var option in new[] { true, false }) {
var db = new SQLiteAsyncConnection (TestPath.GetTempFileName (), option);
await db.CreateTableAsync<NullableDateObj> ().ConfigureAwait (false);

var epochTime = new DateTime (1970, 1, 1);

await db.InsertAsync (new NullableDateObj { Time = epochTime });
await db.InsertAsync (new NullableDateObj { Time = new DateTime (1980, 7, 23) });
await db.InsertAsync (new NullableDateObj { Time = null });
await db.InsertAsync (new NullableDateObj { Time = new DateTime (2019, 1, 23) });

var res = await db.Table<NullableDateObj> ().Where (x => x.Time == epochTime).ToListAsync ();
Assert.AreEqual (1, res.Count);

res = await db.Table<NullableDateObj> ().Where (x => x.Time > epochTime).ToListAsync ();
Assert.AreEqual (2, res.Count);
}
}
}
}
#endif