From 4b1eaf861284cc82c902c16f26a86b604afd1257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericdelaporte@users.noreply.github.com> Date: Mon, 13 Apr 2020 20:27:35 +0200 Subject: [PATCH 1/4] Fix SQLite typing Invalid types were declared in SQLite dialect, causing cast errors, and having led to an undue hack disabling casting for some types. --- src/NHibernate/Dialect/SQLiteDialect.cs | 60 +++++++++++------------ src/NHibernate/Type/TimeAsTimeSpanType.cs | 7 +-- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/NHibernate/Dialect/SQLiteDialect.cs b/src/NHibernate/Dialect/SQLiteDialect.cs index 864defac9a3..70646b72a18 100644 --- a/src/NHibernate/Dialect/SQLiteDialect.cs +++ b/src/NHibernate/Dialect/SQLiteDialect.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Data; using System.Data.Common; @@ -38,32 +37,46 @@ public SQLiteDialect() protected virtual void RegisterColumnTypes() { + // SQLite really has only five types, and a very lax typing system, see https://www.sqlite.org/datatype3.html + // Please do not map (again) fancy types that do not actually exist in SQLite, as this is kind of supported by + // SQLite but creates bugs in convert operations. RegisterColumnType(DbType.Binary, "BLOB"); - RegisterColumnType(DbType.Byte, "TINYINT"); - RegisterColumnType(DbType.Int16, "SMALLINT"); - RegisterColumnType(DbType.Int32, "INT"); - RegisterColumnType(DbType.Int64, "BIGINT"); + RegisterColumnType(DbType.Byte, "INTEGER"); + RegisterColumnType(DbType.Int16, "INTEGER"); + RegisterColumnType(DbType.Int32, "INTEGER"); + RegisterColumnType(DbType.Int64, "INTEGER"); RegisterColumnType(DbType.SByte, "INTEGER"); RegisterColumnType(DbType.UInt16, "INTEGER"); RegisterColumnType(DbType.UInt32, "INTEGER"); RegisterColumnType(DbType.UInt64, "INTEGER"); + + // NUMERIC and REAL are almost the same, they are binary floating point numbers. There is only a slight difference + // for values without a floating part. They will be represented as integers with numeric, but still as floating + // values with real. The side-effect of this is numeric being able of storing exactly bigger integers than real. RegisterColumnType(DbType.Currency, "NUMERIC"); RegisterColumnType(DbType.Decimal, "NUMERIC"); - RegisterColumnType(DbType.Double, "DOUBLE"); - RegisterColumnType(DbType.Single, "DOUBLE"); + RegisterColumnType(DbType.Double, "REAL"); + RegisterColumnType(DbType.Single, "REAL"); RegisterColumnType(DbType.VarNumeric, "NUMERIC"); + RegisterColumnType(DbType.AnsiString, "TEXT"); RegisterColumnType(DbType.String, "TEXT"); RegisterColumnType(DbType.AnsiStringFixedLength, "TEXT"); RegisterColumnType(DbType.StringFixedLength, "TEXT"); - RegisterColumnType(DbType.Date, "DATE"); - RegisterColumnType(DbType.DateTime, "DATETIME"); - RegisterColumnType(DbType.Time, "TIME"); - RegisterColumnType(DbType.Boolean, "BOOL"); - // UNIQUEIDENTIFIER is not a SQLite type, but SQLite does not care much, see - // https://www.sqlite.org/datatype3.html - RegisterColumnType(DbType.Guid, "UNIQUEIDENTIFIER"); + // https://www.sqlite.org/datatype3.html#boolean_datatype + RegisterColumnType(DbType.Boolean, "INTEGER"); + + // See https://www.sqlite.org/datatype3.html#date_and_time_datatype, we have three choices for date and time + // The one causing the less issues in case of an explicit cast is text. Beware, System.Data.SQLite has an + // internal use only "DATETIME" type. Using it causes it to directly convert the text stored into SQLite to + // a .Net DateTime, but also causes columns in SQLite to have numeric affinity and convert to destroy the + // value. As said in their chm documentation, this "DATETIME" type is for System.Data.SQLite internal use only. + RegisterColumnType(DbType.Date, "TEXT"); + RegisterColumnType(DbType.DateTime, "TEXT"); + RegisterColumnType(DbType.Time, "TEXT"); + + RegisterColumnType(DbType.Guid, _binaryGuid ? "BLOB" : "TEXT"); } protected virtual void RegisterFunctions() @@ -98,8 +111,6 @@ protected virtual void RegisterFunctions() RegisterFunction("iif", new SQLFunctionTemplate(null, "case when ?1 then ?2 else ?3 end")); - RegisterFunction("cast", new SQLiteCastFunction()); - RegisterFunction("round", new StandardSQLFunction("round")); // SQLite has no built-in support of bitwise xor, but can emulate it. @@ -112,7 +123,7 @@ protected virtual void RegisterFunctions() if (_binaryGuid) RegisterFunction("strguid", new SQLFunctionTemplate(NHibernateUtil.String, "substr(hex(?1), 7, 2) || substr(hex(?1), 5, 2) || substr(hex(?1), 3, 2) || substr(hex(?1), 1, 2) || '-' || substr(hex(?1), 11, 2) || substr(hex(?1), 9, 2) || '-' || substr(hex(?1), 15, 2) || substr(hex(?1), 13, 2) || '-' || substr(hex(?1), 17, 4) || '-' || substr(hex(?1), 21) ")); else - RegisterFunction("strguid", new SQLFunctionTemplate(NHibernateUtil.String, "cast(?1 as char)")); + RegisterFunction("strguid", new SQLFunctionTemplate(NHibernateUtil.String, "cast(?1 as text)")); // SQLite random function yields a long, ranging form MinValue to MaxValue. (-9223372036854775808 to // 9223372036854775807). HQL random requires a float from 0 inclusive to 1 exclusive, so we divide by @@ -131,7 +142,8 @@ public override void Configure(IDictionary settings) ConfigureBinaryGuid(settings); - // Re-register functions depending on settings. + // Re-register functions and types depending on settings. + RegisterColumnTypes(); RegisterFunctions(); } @@ -484,17 +496,5 @@ public override bool SupportsForeignKeyConstraintInAlterTable // Said to be unlimited. http://sqlite.1065341.n5.nabble.com/Max-limits-on-the-following-td37859.html /// public override int MaxAliasLength => 128; - - [Serializable] - protected class SQLiteCastFunction : CastFunction - { - protected override bool CastingIsRequired(string sqlType) - { - // SQLite doesn't support casting to datetime types. It assumes you want an integer and destroys the date string. - if (StringHelper.ContainsCaseInsensitive(sqlType, "date") || StringHelper.ContainsCaseInsensitive(sqlType, "time")) - return false; - return true; - } - } } } diff --git a/src/NHibernate/Type/TimeAsTimeSpanType.cs b/src/NHibernate/Type/TimeAsTimeSpanType.cs index 51fa6745b57..3f1dcf54937 100644 --- a/src/NHibernate/Type/TimeAsTimeSpanType.cs +++ b/src/NHibernate/Type/TimeAsTimeSpanType.cs @@ -43,10 +43,11 @@ public override object Get(DbDataReader rs, int index, ISessionImplementor sessi try { var value = rs[index]; - if(value is TimeSpan time) //For those dialects where DbType.Time means TimeSpan. + if (value is TimeSpan time) //For those dialects where DbType.Time means TimeSpan. return time; - - return ((DateTime)value).TimeOfDay; + + var dbValue = Convert.ToDateTime(value); + return dbValue.TimeOfDay; } catch (Exception ex) { From cd3fc0494c9c8568a15c51c4f1a1088793f9a60d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericdelaporte@users.noreply.github.com> Date: Mon, 13 Apr 2020 21:19:35 +0200 Subject: [PATCH 2/4] Switch decimal to REAL storage To be squashed --- src/NHibernate/Dialect/SQLiteDialect.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/NHibernate/Dialect/SQLiteDialect.cs b/src/NHibernate/Dialect/SQLiteDialect.cs index 70646b72a18..b2079626c17 100644 --- a/src/NHibernate/Dialect/SQLiteDialect.cs +++ b/src/NHibernate/Dialect/SQLiteDialect.cs @@ -53,11 +53,13 @@ protected virtual void RegisterColumnTypes() // NUMERIC and REAL are almost the same, they are binary floating point numbers. There is only a slight difference // for values without a floating part. They will be represented as integers with numeric, but still as floating // values with real. The side-effect of this is numeric being able of storing exactly bigger integers than real. - RegisterColumnType(DbType.Currency, "NUMERIC"); - RegisterColumnType(DbType.Decimal, "NUMERIC"); + // But it also creates bugs in division, when dividing two numeric happening to be integers, the result is then + // never fractional. So we use "REAL" for all. + RegisterColumnType(DbType.Currency, "REAL"); + RegisterColumnType(DbType.Decimal, "REAL"); RegisterColumnType(DbType.Double, "REAL"); RegisterColumnType(DbType.Single, "REAL"); - RegisterColumnType(DbType.VarNumeric, "NUMERIC"); + RegisterColumnType(DbType.VarNumeric, "REAL"); RegisterColumnType(DbType.AnsiString, "TEXT"); RegisterColumnType(DbType.String, "TEXT"); From bfa41be5ffddde74c6478223f7c2aec65f657121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericdelaporte@users.noreply.github.com> Date: Mon, 13 Apr 2020 21:29:51 +0200 Subject: [PATCH 3/4] Add a todo about convert To be squashed --- src/NHibernate/Type/TimeAsTimeSpanType.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/NHibernate/Type/TimeAsTimeSpanType.cs b/src/NHibernate/Type/TimeAsTimeSpanType.cs index 3f1dcf54937..e525ecfa555 100644 --- a/src/NHibernate/Type/TimeAsTimeSpanType.cs +++ b/src/NHibernate/Type/TimeAsTimeSpanType.cs @@ -46,6 +46,8 @@ public override object Get(DbDataReader rs, int index, ISessionImplementor sessi if (value is TimeSpan time) //For those dialects where DbType.Time means TimeSpan. return time; + // Todo: investigate if this convert should be made culture invariant, here and in other NHibernate types, + // such as AbstractDateTimeType and TimeType, or even in all other places doing such converts in NHibernate. var dbValue = Convert.ToDateTime(value); return dbValue.TimeOfDay; } From ed6ac5b69d6bcf1974526d4596b06f0217aab5c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericdelaporte@users.noreply.github.com> Date: Tue, 14 Apr 2020 00:00:55 +0200 Subject: [PATCH 4/4] Bring back a class gone unused for avoiding a binary breaking change --- src/NHibernate/Dialect/SQLiteDialect.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/NHibernate/Dialect/SQLiteDialect.cs b/src/NHibernate/Dialect/SQLiteDialect.cs index b2079626c17..5eb1f51dd87 100644 --- a/src/NHibernate/Dialect/SQLiteDialect.cs +++ b/src/NHibernate/Dialect/SQLiteDialect.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Data; using System.Data.Common; @@ -498,5 +499,19 @@ public override bool SupportsForeignKeyConstraintInAlterTable // Said to be unlimited. http://sqlite.1065341.n5.nabble.com/Max-limits-on-the-following-td37859.html /// public override int MaxAliasLength => 128; + + // Since v5.3 + [Obsolete("This class has no usage in NHibernate anymore and will be removed in a future version. Use or extend CastFunction instead.")] + [Serializable] + protected class SQLiteCastFunction : CastFunction + { + protected override bool CastingIsRequired(string sqlType) + { + if (StringHelper.ContainsCaseInsensitive(sqlType, "date") || + StringHelper.ContainsCaseInsensitive(sqlType, "time")) + return false; + return true; + } + } } }