Skip to content
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

NH-4088 - Dialect.GetCastTypeName is buggy #709

Merged
merged 3 commits into from
Oct 9, 2017
Merged
Show file tree
Hide file tree
Changes from 2 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
55 changes: 55 additions & 0 deletions src/NHibernate.Test/Async/Criteria/ProjectionsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
//------------------------------------------------------------------------------


using System;
using System.Collections;
using System.Collections.Generic;
using NHibernate.Criterion;
using NHibernate.Dialect;
using NHibernate.Type;
using NUnit.Framework;

namespace NHibernate.Test.Criteria
Expand Down Expand Up @@ -100,6 +102,59 @@ public async Task UsingSqlFunctions_Concat_WithCastAsync()
}
}

[Test]
public async Task CastWithLengthAsync()
{
using (var s = OpenSession())
{
try
{
var shortName = await (s
.CreateCriteria<Student>()
.SetProjection(
Projections.Cast(
TypeFactory.GetStringType(3),
Projections.Property("Name")))
.UniqueResultAsync<string>());
Assert.That(shortName, Is.EqualTo("aye"));
}
catch (Exception e)
{
if (!e.Message.Contains("truncation") &&
(e.InnerException == null || !e.InnerException.Message.Contains("truncation")))
throw;
}
}
}

[Test]
public async Task CastWithPrecisionScaleAsync()
{
if (TestDialect.HasBrokenDecimalType)
Assert.Ignore("Dialect does not correctly handle decimal.");

using (var s = OpenSession())
{
var value = await (s
.CreateCriteria<Student>()
.SetProjection(
Projections.Cast(
TypeFactory.Basic("decimal(18,9)"),
Projections.Constant(123456789.123456789m, TypeFactory.Basic("decimal(18,9)"))))
.UniqueResultAsync<decimal>());
Assert.That(value, Is.EqualTo(123456789.123456789m), "Same type cast");

value = await (s
.CreateCriteria<Student>()
.SetProjection(
Projections.Cast(
TypeFactory.Basic("decimal(18,7)"),
Projections.Constant(123456789.987654321m, TypeFactory.Basic("decimal(18,9)"))))
.UniqueResultAsync<decimal>());
Assert.That(value, Is.EqualTo(123456789.9876543m), "Reduced scale cast");
}
}

[Test]
public async Task CanUseParametersWithProjectionsAsync()
{
Expand Down
3 changes: 1 addition & 2 deletions src/NHibernate.Test/Async/DialectTest/DialectFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using NHibernate.Dialect;
using NHibernate.Driver;
using NHibernate.Engine;
Expand Down Expand Up @@ -79,4 +78,4 @@ public async Task CurrentTimestampSelectionAsync()
}
}
}
}
}
55 changes: 55 additions & 0 deletions src/NHibernate.Test/Criteria/ProjectionsTest.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
using NHibernate.Criterion;
using NHibernate.Dialect;
using NHibernate.Type;
using NUnit.Framework;

namespace NHibernate.Test.Criteria
Expand Down Expand Up @@ -89,6 +91,59 @@ public void UsingSqlFunctions_Concat_WithCast()
}
}

[Test]
public void CastWithLength()
{
using (var s = OpenSession())
{
try
{
var shortName = s
.CreateCriteria<Student>()
.SetProjection(
Projections.Cast(
TypeFactory.GetStringType(3),
Projections.Property("Name")))
.UniqueResult<string>();
Assert.That(shortName, Is.EqualTo("aye"));
}
catch (Exception e)
{
if (!e.Message.Contains("truncation") &&
(e.InnerException == null || !e.InnerException.Message.Contains("truncation")))
throw;
}
}
}

[Test]
public void CastWithPrecisionScale()
{
if (TestDialect.HasBrokenDecimalType)
Assert.Ignore("Dialect does not correctly handle decimal.");

using (var s = OpenSession())
{
var value = s
.CreateCriteria<Student>()
.SetProjection(
Projections.Cast(
TypeFactory.Basic("decimal(18,9)"),
Projections.Constant(123456789.123456789m, TypeFactory.Basic("decimal(18,9)"))))
.UniqueResult<decimal>();
Assert.That(value, Is.EqualTo(123456789.123456789m), "Same type cast");

value = s
.CreateCriteria<Student>()
.SetProjection(
Projections.Cast(
TypeFactory.Basic("decimal(18,7)"),
Projections.Constant(123456789.987654321m, TypeFactory.Basic("decimal(18,9)"))))
.UniqueResult<decimal>();
Assert.That(value, Is.EqualTo(123456789.9876543m), "Reduced scale cast");
}
}

[Test]
public void CanUseParametersWithProjections()
{
Expand Down
26 changes: 24 additions & 2 deletions src/NHibernate.Test/DialectTest/DialectFixture.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using NHibernate.Dialect;
using NHibernate.Driver;
using NHibernate.Engine;
Expand Down Expand Up @@ -171,5 +170,28 @@ public void CurrentTimestampSelection()
}
}
}

[Test]
public void GetDecimalTypeName()
{
var cfg = TestConfigurationHelper.GetDefaultConfiguration();
var dialect = Dialect.Dialect.GetDialect(cfg.Properties);

Assert.That(dialect.GetTypeName(SqlTypeFactory.GetSqlType(DbType.Decimal, 40, 40)), Does.Not.Contain("40"), "oversized decimal");
Assert.That(dialect.GetTypeName(SqlTypeFactory.GetSqlType(DbType.Decimal, 3, 2)), Does.Match(@"^[^(]*(\(\s*3\s*,\s*2\s*\))?\s*$"), "small decimal");
}

[Test]
public void GetTypeCastName()
{
var cfg = TestConfigurationHelper.GetDefaultConfiguration();
cfg.SetProperty(Environment.QueryDefaultCastLength, "20");
cfg.SetProperty(Environment.QueryDefaultCastPrecision, "10");
cfg.SetProperty(Environment.QueryDefaultCastScale, "3");
var dialect = Dialect.Dialect.GetDialect(cfg.Properties);

Assert.That(dialect.GetCastTypeName(SqlTypeFactory.Decimal), Does.Match(@"^[^(]*(\(\s*10\s*,\s*3\s*\))?\s*$"), "decimal");
Assert.That(dialect.GetCastTypeName(new SqlType(DbType.String)), Does.Match(@"^[^(]*(\(\s*20\s*\))?\s*$"), "string");
}
}
}
}
6 changes: 3 additions & 3 deletions src/NHibernate.Test/DriverTest/FirebirdClientDriverFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public void ConnectionPooling_OpenThenCloseTwoAtTheSameTime_TowConnectionsArePoo
public void AdjustCommand_StringParametersWithinConditionalSelect_ThenParameterIsWrappedByAVarcharCastStatement()
{
MakeDriver();
var cmd = BuildSelectCaseCommand(SqlTypeFactory.GetString(0));
var cmd = BuildSelectCaseCommand(SqlTypeFactory.GetString(255));

_driver.AdjustCommand(cmd);

Expand All @@ -100,7 +100,7 @@ public void AdjustCommand_IntParametersWithinConditionalSelect_ThenParameterIsWr
public void AdjustCommand_ParameterWithinSelectConcat_ParameterIsCasted()
{
MakeDriver();
var cmd = BuildSelectConcatCommand(SqlTypeFactory.GetString(0));
var cmd = BuildSelectConcatCommand(SqlTypeFactory.GetString(255));

_driver.AdjustCommand(cmd);

Expand All @@ -112,7 +112,7 @@ public void AdjustCommand_ParameterWithinSelectConcat_ParameterIsCasted()
public void AdjustCommand_ParameterWithinSelectAddFunction_ParameterIsCasted()
{
MakeDriver();
var cmd = BuildSelectAddCommand(SqlTypeFactory.GetString(0));
var cmd = BuildSelectAddCommand(SqlTypeFactory.GetString(255));

_driver.AdjustCommand(cmd);

Expand Down
18 changes: 18 additions & 0 deletions src/NHibernate/Cfg/Environment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,24 @@ public static string Version

public const string QueryModelRewriterFactory = "query.query_model_rewriter_factory";

/// <summary>
/// Set the default length used in casting when the target type is length bound and
/// does not specify it. <c>4000</c> by default, automatically trim down according to dialect type registration.
/// </summary>
public const string QueryDefaultCastLength = "query.default_cast_length";

/// <summary>
/// Set the default precision used in casting when the target type is decimal and
/// does not specify it. <c>28</c> by default, automatically trim down according to dialect type registration.
/// </summary>
public const string QueryDefaultCastPrecision = "query.default_cast_precision";

/// <summary>
/// Set the default scale used in casting when the target type is decimal and
/// does not specify it. <c>10</c> by default, automatically trim down according to dialect type registration.
/// </summary>
public const string QueryDefaultCastScale = "query.default_cast_scale";

/// <summary>
/// This may need to be set to 3 if you are using the OdbcDriver with MS SQL Server 2008+.
/// </summary>
Expand Down
3 changes: 2 additions & 1 deletion src/NHibernate/Dialect/DB2Dialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public DB2Dialect()
RegisterColumnType(DbType.Date, "DATE");
RegisterColumnType(DbType.DateTime, "TIMESTAMP");
RegisterColumnType(DbType.Decimal, "DECIMAL(19,5)");
RegisterColumnType(DbType.Decimal, 19, "DECIMAL(19, $l)");
// DB2 max precision is 31, but .Net is 28-29 anyway.
RegisterColumnType(DbType.Decimal, 28, "DECIMAL($p, $s)");
RegisterColumnType(DbType.Double, "DOUBLE");
RegisterColumnType(DbType.Int16, "SMALLINT");
RegisterColumnType(DbType.Int32, "INTEGER");
Expand Down
58 changes: 41 additions & 17 deletions src/NHibernate/Dialect/Dialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ private static Dialect InstantiateDialect(string dialectName, IDictionary<string
/// <param name="settings">The configuration settings.</param>
public virtual void Configure(IDictionary<string, string> settings)
{
DefaultCastLength = PropertiesHelper.GetInt32(Environment.QueryDefaultCastLength, settings, 4000);
DefaultCastPrecision = PropertiesHelper.GetByte(Environment.QueryDefaultCastPrecision, settings, null) ?? 28;
DefaultCastScale = PropertiesHelper.GetByte(Environment.QueryDefaultCastScale, settings, null) ?? 10;
Copy link
Member Author

Choose a reason for hiding this comment

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

There are some inconsistencies in those PropertiesHelper methods.

I think the right signature is the GetInt32 one, but completed with an overload yielding a nullable and not taking any default.

}

#endregion
Expand All @@ -215,17 +218,10 @@ public virtual string GetTypeName(SqlType sqlType)
{
if (sqlType.LengthDefined || sqlType.PrecisionDefined || sqlType.ScaleDefined)
{
string resultWithLength = _typeNames.Get(sqlType.DbType, sqlType.Length, sqlType.Precision, sqlType.Scale);
if (resultWithLength != null) return resultWithLength;
return _typeNames.Get(sqlType.DbType, sqlType.Length, sqlType.Precision, sqlType.Scale);
Copy link
Member Author

Choose a reason for hiding this comment

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

Those TypeNames methods are never yielding null. (Unless a Put was done with null, but now Put throws null argument exception if it is attempted.)

}

string result = _typeNames.Get(sqlType.DbType);
if (result == null)
{
throw new HibernateException(string.Format("No default type mapping for SqlType {0}", sqlType));
}

return result;
return _typeNames.Get(sqlType.DbType);
}

/// <summary>
Expand All @@ -239,12 +235,7 @@ public virtual string GetTypeName(SqlType sqlType)
/// <returns>The database type name used by ddl.</returns>
public virtual string GetTypeName(SqlType sqlType, int length, int precision, int scale)
{
string result = _typeNames.Get(sqlType.DbType, length, precision, scale);
if (result == null)
{
throw new HibernateException(string.Format("No type mapping for SqlType {0} of length {1}", sqlType, length));
}
return result;
return _typeNames.Get(sqlType.DbType, length, precision, scale);
}

/// <summary>
Expand All @@ -257,15 +248,48 @@ public virtual string GetLongestTypeName(DbType dbType)
return _typeNames.GetLongest(dbType);
}

protected int DefaultCastLength { get; set; }
protected byte DefaultCastPrecision { get; set; }
protected byte DefaultCastScale { get; set; }

/// <summary>
/// Get the name of the database type appropriate for casting operations
/// (via the CAST() SQL function) for the given <see cref="SqlType"/> typecode.
/// </summary>
/// <param name="sqlType">The <see cref="SqlType"/> typecode </param>
/// <returns> The database type name </returns>
public virtual string GetCastTypeName(SqlType sqlType)
public virtual string GetCastTypeName(SqlType sqlType) =>
GetCastTypeName(sqlType, _typeNames);

/// <summary>
/// Get the name of the database type appropriate for casting operations
/// (via the CAST() SQL function) for the given <see cref="SqlType"/> typecode.
/// </summary>
/// <param name="sqlType">The <see cref="SqlType"/> typecode.</param>
/// <param name="castTypeNames">The source for type names.</param>
/// <returns>The database type name.</returns>
protected virtual string GetCastTypeName(SqlType sqlType, TypeNames castTypeNames)
{
return GetTypeName(sqlType, Column.DefaultLength, Column.DefaultPrecision, Column.DefaultScale);
if (sqlType.LengthDefined || sqlType.PrecisionDefined || sqlType.ScaleDefined)
return castTypeNames.Get(sqlType.DbType, sqlType.Length, sqlType.Precision, sqlType.Scale);
switch (sqlType.DbType)
{
case DbType.Decimal:
// We cannot know if the user needs its digit after or before the dot, so use a configurable
// default.
return castTypeNames.Get(DbType.Decimal, 0, DefaultCastPrecision, DefaultCastScale);
case DbType.DateTime:
case DbType.DateTime2:
case DbType.DateTimeOffset:
case DbType.Time:
case DbType.Currency:
// Use default for these, dialects are supposed to map them to max capacity
return castTypeNames.Get(sqlType.DbType);
default:
// Other types are either length bound or not length/precision/scale bound. Otherwise they need to be
// handled previously.
return castTypeNames.Get(sqlType.DbType, DefaultCastLength, 0, 0);
}
}

/// <summary>
Expand Down
Loading