Skip to content

Fix SQLite SQL types registrations #2346

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 14, 2020
Merged
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
61 changes: 39 additions & 22 deletions src/NHibernate/Dialect/SQLiteDialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,32 +38,48 @@ 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");
RegisterColumnType(DbType.Currency, "NUMERIC");
RegisterColumnType(DbType.Decimal, "NUMERIC");
RegisterColumnType(DbType.Double, "DOUBLE");
RegisterColumnType(DbType.Single, "DOUBLE");
RegisterColumnType(DbType.VarNumeric, "NUMERIC");

// 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.
// 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, "REAL");

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()
Expand Down Expand Up @@ -98,8 +114,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.
Expand All @@ -112,7 +126,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
Expand All @@ -131,7 +145,8 @@ public override void Configure(IDictionary<string, string> settings)

ConfigureBinaryGuid(settings);

// Re-register functions depending on settings.
// Re-register functions and types depending on settings.
RegisterColumnTypes();
RegisterFunctions();
}

Expand Down Expand Up @@ -485,13 +500,15 @@ public override bool SupportsForeignKeyConstraintInAlterTable
/// <inheritdoc />
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)
{
// 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"))
if (StringHelper.ContainsCaseInsensitive(sqlType, "date") ||
StringHelper.ContainsCaseInsensitive(sqlType, "time"))
return false;
return true;
}
Expand Down
9 changes: 6 additions & 3 deletions src/NHibernate/Type/TimeAsTimeSpanType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,13 @@ 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;

// 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);
Copy link
Member Author

@fredericDelaporte fredericDelaporte Apr 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The direct cast was not failing thanks to having previously used the "internal use only" DATETIME type, causing the data provider to do the convert itself. But this was also wrecking casting, and as documented in their chm, this type is not meant for being used outside of the provider itself.

So we need now to do the convert, as are already doing other NHibernate types dealing with DateTime, see TimeType by example.

return dbValue.TimeOfDay;
}
catch (Exception ex)
{
Expand Down