From 329b5c28350fbe8a43652ee75f47835b1444e39e Mon Sep 17 00:00:00 2001 From: Eldin Date: Sun, 2 Nov 2025 01:37:12 -0700 Subject: [PATCH 1/4] Allow storage of the DateOnly datatype (.NET 6+) --- src/SQLite.cs | 58 +++++++++++-- tests/SQLite.Tests/DateOnlyTest.cs | 127 +++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 tests/SQLite.Tests/DateOnlyTest.cs diff --git a/src/SQLite.cs b/src/SQLite.cs index 72525c56..8bcf9e46 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -177,6 +177,7 @@ public interface ISQLiteConnection : IDisposable bool StoreDateTimeAsTicks { get; } bool StoreTimeSpanAsTicks { get; } string DateTimeStringFormat { get; } + string DateStringFormat { get; } TimeSpan BusyTimeout { get; set; } IEnumerable TableMappings { get; } bool IsInTransaction { get; } @@ -499,11 +500,18 @@ public partial class SQLiteConnection : ISQLiteConnection /// The date time string format. public string DateTimeStringFormat { get; private set; } + /// + /// The format to use when storing Date properties as strings. + /// + /// The date string format. + public string DateStringFormat { get; private set; } + /// /// The DateTimeStyles value to use when parsing a DateTime property string. /// /// The date time style. internal System.Globalization.DateTimeStyles DateTimeStyle { get; private set; } + //internal System.Globalization.DateTimeStyles DateStyle { get; private set; } #if USE_SQLITEPCL_RAW && !NO_SQLITEPCL_RAW_BATTERIES static SQLiteConnection () @@ -595,6 +603,7 @@ public SQLiteConnection (SQLiteConnectionString connectionString) StoreDateTimeAsTicks = connectionString.StoreDateTimeAsTicks; StoreTimeSpanAsTicks = connectionString.StoreTimeSpanAsTicks; DateTimeStringFormat = connectionString.DateTimeStringFormat; + DateStringFormat = connectionString.DateStringFormat; DateTimeStyle = connectionString.DateTimeStyle; BusyTimeout = TimeSpan.FromSeconds (1.0); @@ -2659,12 +2668,13 @@ public enum NotifyTableChangedAction public class SQLiteConnectionString { const string DateTimeSqliteDefaultFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff"; - + const string DateSqliteDefaultFormat = "yyyy'-'MM'-'dd"; public string UniqueKey { get; } public string DatabasePath { get; } public bool StoreDateTimeAsTicks { get; } public bool StoreTimeSpanAsTicks { get; } public string DateTimeStringFormat { get; } + public string DateStringFormat { get; } public System.Globalization.DateTimeStyles DateTimeStyle { get; } public object Key { get; } public SQLiteOpenFlags OpenFlags { get; } @@ -2770,13 +2780,16 @@ 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) + public SQLiteConnectionString (string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks, object key = null, Action preKeyAction = null, Action postKeyAction = null, string vfsName = null, string dateTimeStringFormat = DateTimeSqliteDefaultFormat, string dateStringFormat = DateSqliteDefaultFormat, 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 +2798,7 @@ public SQLiteConnectionString (string databasePath, SQLiteOpenFlags openFlags, b StoreDateTimeAsTicks = storeDateTimeAsTicks; StoreTimeSpanAsTicks = storeTimeSpanAsTicks; DateTimeStringFormat = dateTimeStringFormat; + DateStringFormat = dateStringFormat; 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 +3338,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 +3734,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 +3791,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 +3902,18 @@ 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); + //return DateOnly.Parse (text); + 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 +4247,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..068d2274 --- /dev/null +++ b/tests/SQLite.Tests/DateOnlyTest.cs @@ -0,0 +1,127 @@ +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 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); + } + } + } +} + From 1c8cc65d2b43fad421d1121421b83b4525d390cf Mon Sep 17 00:00:00 2001 From: Eldin Date: Sun, 2 Nov 2025 01:42:05 -0700 Subject: [PATCH 2/4] Remove commented code that I missed in the prior commit. --- src/SQLite.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index 8bcf9e46..b3bc6e88 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -511,7 +511,6 @@ public partial class SQLiteConnection : ISQLiteConnection /// /// The date time style. internal System.Globalization.DateTimeStyles DateTimeStyle { get; private set; } - //internal System.Globalization.DateTimeStyles DateStyle { get; private set; } #if USE_SQLITEPCL_RAW && !NO_SQLITEPCL_RAW_BATTERIES static SQLiteConnection () From dba7d104aca68d3691bb925c29593f148b91dd1b Mon Sep 17 00:00:00 2001 From: Eldin Date: Sun, 2 Nov 2025 02:20:37 -0700 Subject: [PATCH 3/4] Ensure all DateOnly references are only included for .NET 6+ --- src/SQLite.cs | 61 ++++++++++++++++++++++++++++-- tests/SQLite.Tests/DateOnlyTest.cs | 11 +----- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index b3bc6e88..157ff167 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -177,7 +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; } @@ -602,7 +604,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); @@ -2667,13 +2671,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; } @@ -2747,7 +2755,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. /// /// @@ -2788,7 +2797,51 @@ public SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks, o /// 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, string dateStringFormat = DateSqliteDefaultFormat, 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)); @@ -2797,7 +2850,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; @@ -3903,8 +3958,6 @@ object ReadCol (Sqlite3Statement stmt, int index, SQLite3.ColType type, Type clr } #if NET6_0_OR_GREATER else if (clrType == typeof (DateOnly)) { - //var text = SQLite3.ColumnString (stmt, index); - //return DateOnly.Parse (text); var text = SQLite3.ColumnString (stmt, index); DateOnly resultDate; if (!DateOnly.TryParseExact (text, _conn.DateStringFormat, System.Globalization.CultureInfo.InvariantCulture, _conn.DateTimeStyle, out resultDate)) { diff --git a/tests/SQLite.Tests/DateOnlyTest.cs b/tests/SQLite.Tests/DateOnlyTest.cs index 068d2274..57f60e21 100644 --- a/tests/SQLite.Tests/DateOnlyTest.cs +++ b/tests/SQLite.Tests/DateOnlyTest.cs @@ -1,14 +1,7 @@ +#if NET6_0_OR_GREATER 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 { @@ -124,4 +117,4 @@ public async Task LinqNullable () } } } - +#endif From 0ef2f286a9454d9f195c8923dcee2b30a6800ebe Mon Sep 17 00:00:00 2001 From: Eldin Date: Sun, 2 Nov 2025 02:25:32 -0700 Subject: [PATCH 4/4] Fix missed case of wrapping dateonly related field in #if --- src/SQLite.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/SQLite.cs b/src/SQLite.cs index 157ff167..b6641514 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -502,11 +502,13 @@ 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.