diff --git a/src/SQLite.cs b/src/SQLite.cs index 72525c56..b6641514 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -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 TableMappings { get; } bool IsInTransaction { get; } @@ -499,6 +502,14 @@ public partial class SQLiteConnection : ISQLiteConnection /// The date time string format. public string DateTimeStringFormat { get; private set; } +#if NET6_0_OR_GREATER + /// + /// The format to use when storing Date properties as strings. + /// + /// The date string format. + public string DateStringFormat { get; private set; } +#endif + /// /// The DateTimeStyles value to use when parsing a DateTime property string. /// @@ -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); @@ -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; } @@ -2738,7 +2757,8 @@ public SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks, o { } - /// +#if NET6_0_OR_GREATER + /// /// Constructs a new SQLiteConnectionString with all the data needed to open an SQLiteConnection. /// /// @@ -2770,13 +2790,60 @@ public SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks, o /// /// Specifies the format to use when storing DateTime properties as strings. /// + /// + /// Specifies the format to use when storing DateOnly properties. + /// /// /// 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) +#else + /// + /// Constructs a new SQLiteConnectionString with all the data needed to open an SQLiteConnection. + /// + /// + /// Specifies the path to the database file. + /// + /// + /// Flags controlling how the connection should be opened. + /// + /// + /// 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. + /// + /// + /// Specifies the encryption key to use on the database. Should be a string or a byte[]. + /// + /// + /// Executes prior to setting key for SQLCipher databases + /// + /// + /// Executes after setting key for SQLCipher databases + /// + /// + /// Specifies the Virtual File System to use on the database. + /// + /// + /// 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. + /// +#endif + public SQLiteConnectionString (string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks, object key = null, Action preKeyAction = null, Action 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)); @@ -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; @@ -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"; } @@ -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); @@ -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); } @@ -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); } @@ -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); diff --git a/tests/SQLite.Tests/DateOnlyTest.cs b/tests/SQLite.Tests/DateOnlyTest.cs new file mode 100644 index 00000000..57f60e21 --- /dev/null +++ b/tests/SQLite.Tests/DateOnlyTest.cs @@ -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 ().Wait (); + + TestObj o, o2; + + o = new TestObj { + ModifiedDate = dateTime, + }; + db.InsertAsync (o).Wait (); + o2 = db.GetAsync (o.Id).Result; + Assert.AreEqual (o.ModifiedDate, o2.ModifiedDate); + + var stored = db.ExecuteScalarAsync ("SELECT ModifiedDate FROM TestObj;").Result; + Assert.AreEqual (expected, stored); + } + + void TestDateOnly (TestDb db, DateOnly date, string expected) + { + db.CreateTable (); + + TestObj o, o2; + + o = new TestObj { + ModifiedDate = date, + }; + db.Insert (o); + o2 = db.Get (o.Id); + Assert.AreEqual (o.ModifiedDate, o2.ModifiedDate); + + var stored = db.ExecuteScalar ("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 ().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 ().Where (x => x.Time == epochTime).ToListAsync (); + Assert.AreEqual (1, res.Count); + + res = await db.Table ().Where (x => x.Time > epochTime).ToListAsync (); + Assert.AreEqual (2, res.Count); + } + } + } +} +#endif