Skip to content

Commit 66fa1d4

Browse files
Fix SQLite typing (#2346)
Co-authored-by: maca88 <bostjan.markezic@siol.net>
1 parent 25bc518 commit 66fa1d4

File tree

2 files changed

+45
-25
lines changed

2 files changed

+45
-25
lines changed

src/NHibernate/Dialect/SQLiteDialect.cs

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -38,32 +38,48 @@ public SQLiteDialect()
3838

3939
protected virtual void RegisterColumnTypes()
4040
{
41+
// SQLite really has only five types, and a very lax typing system, see https://www.sqlite.org/datatype3.html
42+
// Please do not map (again) fancy types that do not actually exist in SQLite, as this is kind of supported by
43+
// SQLite but creates bugs in convert operations.
4144
RegisterColumnType(DbType.Binary, "BLOB");
42-
RegisterColumnType(DbType.Byte, "TINYINT");
43-
RegisterColumnType(DbType.Int16, "SMALLINT");
44-
RegisterColumnType(DbType.Int32, "INT");
45-
RegisterColumnType(DbType.Int64, "BIGINT");
45+
RegisterColumnType(DbType.Byte, "INTEGER");
46+
RegisterColumnType(DbType.Int16, "INTEGER");
47+
RegisterColumnType(DbType.Int32, "INTEGER");
48+
RegisterColumnType(DbType.Int64, "INTEGER");
4649
RegisterColumnType(DbType.SByte, "INTEGER");
4750
RegisterColumnType(DbType.UInt16, "INTEGER");
4851
RegisterColumnType(DbType.UInt32, "INTEGER");
4952
RegisterColumnType(DbType.UInt64, "INTEGER");
50-
RegisterColumnType(DbType.Currency, "NUMERIC");
51-
RegisterColumnType(DbType.Decimal, "NUMERIC");
52-
RegisterColumnType(DbType.Double, "DOUBLE");
53-
RegisterColumnType(DbType.Single, "DOUBLE");
54-
RegisterColumnType(DbType.VarNumeric, "NUMERIC");
53+
54+
// NUMERIC and REAL are almost the same, they are binary floating point numbers. There is only a slight difference
55+
// for values without a floating part. They will be represented as integers with numeric, but still as floating
56+
// values with real. The side-effect of this is numeric being able of storing exactly bigger integers than real.
57+
// But it also creates bugs in division, when dividing two numeric happening to be integers, the result is then
58+
// never fractional. So we use "REAL" for all.
59+
RegisterColumnType(DbType.Currency, "REAL");
60+
RegisterColumnType(DbType.Decimal, "REAL");
61+
RegisterColumnType(DbType.Double, "REAL");
62+
RegisterColumnType(DbType.Single, "REAL");
63+
RegisterColumnType(DbType.VarNumeric, "REAL");
64+
5565
RegisterColumnType(DbType.AnsiString, "TEXT");
5666
RegisterColumnType(DbType.String, "TEXT");
5767
RegisterColumnType(DbType.AnsiStringFixedLength, "TEXT");
5868
RegisterColumnType(DbType.StringFixedLength, "TEXT");
5969

60-
RegisterColumnType(DbType.Date, "DATE");
61-
RegisterColumnType(DbType.DateTime, "DATETIME");
62-
RegisterColumnType(DbType.Time, "TIME");
63-
RegisterColumnType(DbType.Boolean, "BOOL");
64-
// UNIQUEIDENTIFIER is not a SQLite type, but SQLite does not care much, see
65-
// https://www.sqlite.org/datatype3.html
66-
RegisterColumnType(DbType.Guid, "UNIQUEIDENTIFIER");
70+
// https://www.sqlite.org/datatype3.html#boolean_datatype
71+
RegisterColumnType(DbType.Boolean, "INTEGER");
72+
73+
// See https://www.sqlite.org/datatype3.html#date_and_time_datatype, we have three choices for date and time
74+
// The one causing the less issues in case of an explicit cast is text. Beware, System.Data.SQLite has an
75+
// internal use only "DATETIME" type. Using it causes it to directly convert the text stored into SQLite to
76+
// a .Net DateTime, but also causes columns in SQLite to have numeric affinity and convert to destroy the
77+
// value. As said in their chm documentation, this "DATETIME" type is for System.Data.SQLite internal use only.
78+
RegisterColumnType(DbType.Date, "TEXT");
79+
RegisterColumnType(DbType.DateTime, "TEXT");
80+
RegisterColumnType(DbType.Time, "TEXT");
81+
82+
RegisterColumnType(DbType.Guid, _binaryGuid ? "BLOB" : "TEXT");
6783
}
6884

6985
protected virtual void RegisterFunctions()
@@ -98,8 +114,6 @@ protected virtual void RegisterFunctions()
98114

99115
RegisterFunction("iif", new SQLFunctionTemplate(null, "case when ?1 then ?2 else ?3 end"));
100116

101-
RegisterFunction("cast", new SQLiteCastFunction());
102-
103117
RegisterFunction("round", new StandardSQLFunction("round"));
104118

105119
// SQLite has no built-in support of bitwise xor, but can emulate it.
@@ -112,7 +126,7 @@ protected virtual void RegisterFunctions()
112126
if (_binaryGuid)
113127
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) "));
114128
else
115-
RegisterFunction("strguid", new SQLFunctionTemplate(NHibernateUtil.String, "cast(?1 as char)"));
129+
RegisterFunction("strguid", new SQLFunctionTemplate(NHibernateUtil.String, "cast(?1 as text)"));
116130

117131
// SQLite random function yields a long, ranging form MinValue to MaxValue. (-9223372036854775808 to
118132
// 9223372036854775807). HQL random requires a float from 0 inclusive to 1 exclusive, so we divide by
@@ -131,7 +145,8 @@ public override void Configure(IDictionary<string, string> settings)
131145

132146
ConfigureBinaryGuid(settings);
133147

134-
// Re-register functions depending on settings.
148+
// Re-register functions and types depending on settings.
149+
RegisterColumnTypes();
135150
RegisterFunctions();
136151
}
137152

@@ -485,13 +500,15 @@ public override bool SupportsForeignKeyConstraintInAlterTable
485500
/// <inheritdoc />
486501
public override int MaxAliasLength => 128;
487502

503+
// Since v5.3
504+
[Obsolete("This class has no usage in NHibernate anymore and will be removed in a future version. Use or extend CastFunction instead.")]
488505
[Serializable]
489506
protected class SQLiteCastFunction : CastFunction
490507
{
491508
protected override bool CastingIsRequired(string sqlType)
492509
{
493-
// SQLite doesn't support casting to datetime types. It assumes you want an integer and destroys the date string.
494-
if (StringHelper.ContainsCaseInsensitive(sqlType, "date") || StringHelper.ContainsCaseInsensitive(sqlType, "time"))
510+
if (StringHelper.ContainsCaseInsensitive(sqlType, "date") ||
511+
StringHelper.ContainsCaseInsensitive(sqlType, "time"))
495512
return false;
496513
return true;
497514
}

src/NHibernate/Type/TimeAsTimeSpanType.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,13 @@ public override object Get(DbDataReader rs, int index, ISessionImplementor sessi
4343
try
4444
{
4545
var value = rs[index];
46-
if(value is TimeSpan time) //For those dialects where DbType.Time means TimeSpan.
46+
if (value is TimeSpan time) //For those dialects where DbType.Time means TimeSpan.
4747
return time;
48-
49-
return ((DateTime)value).TimeOfDay;
48+
49+
// Todo: investigate if this convert should be made culture invariant, here and in other NHibernate types,
50+
// such as AbstractDateTimeType and TimeType, or even in all other places doing such converts in NHibernate.
51+
var dbValue = Convert.ToDateTime(value);
52+
return dbValue.TimeOfDay;
5053
}
5154
catch (Exception ex)
5255
{

0 commit comments

Comments
 (0)