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-3787 - Decimal truncation in Linq ternary expression #707

Merged
merged 5 commits into from
Dec 7, 2017
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
140 changes: 140 additions & 0 deletions src/NHibernate.Test/Async/NHSpecificTest/NH3787/TestFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by AsyncGenerator.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------


using System.Linq;
using NHibernate.Criterion;
using NHibernate.Linq;
using NHibernate.Transform;
using NHibernate.Type;
using NUnit.Framework;

namespace NHibernate.Test.NHSpecificTest.NH3787
{
using System.Threading.Tasks;
[TestFixture]
public class TestFixtureAsync : BugTestCase
{
private const decimal _testRate = 12345.1234567890123M;

protected override bool AppliesTo(Dialect.Dialect dialect)
{
return !TestDialect.HasBrokenDecimalType;
}

protected override void OnSetUp()
{
base.OnSetUp();

using (var s = OpenSession())
using (var t = s.BeginTransaction())
{
var testEntity = new TestEntity
{
UsePreviousRate = true,
PreviousRate = _testRate,
Rate = 54321.1234567890123M
};
s.Save(testEntity);
t.Commit();
}
}

protected override void OnTearDown()
{
using (var s = OpenSession())
using (var t = s.BeginTransaction())
{
s.CreateQuery("delete from TestEntity").ExecuteUpdate();
t.Commit();
}
}

[Test]
public async Task TestLinqQueryAsync()
{
using (var s = OpenSession())
using (var t = s.BeginTransaction())
{
var queryResult = await (s
.Query<TestEntity>()
.Where(e => e.PreviousRate == _testRate)
.ToListAsync());

Assert.That(queryResult.Count, Is.EqualTo(1));
Assert.That(queryResult[0].PreviousRate, Is.EqualTo(_testRate));
await (t.CommitAsync());
}
}

[Test]
public async Task TestLinqProjectionAsync()
{
using (var s = OpenSession())
using (var t = s.BeginTransaction())
{
var queryResult = await ((from test in s.Query<TestEntity>()
select new RateDto { Rate = test.UsePreviousRate ? test.PreviousRate : test.Rate }).ToListAsync());

// Check it has not been truncated to the default scale (10) of NHibernate.
Assert.That(queryResult[0].Rate, Is.EqualTo(_testRate));
await (t.CommitAsync());
}
}

[Test]
public async Task TestLinqQueryOnExpressionAsync()
{
using (var s = OpenSession())
using (var t = s.BeginTransaction())
{
var queryResult = await (s
.Query<TestEntity>()
.Where(
// Without MappedAs, the test fails for SQL Server because it would restrict its parameter to the dialect's default scale.
e => (e.UsePreviousRate ? e.PreviousRate : e.Rate) == _testRate.MappedAs(TypeFactory.Basic("decimal(18,13)")))
.ToListAsync());

Assert.That(queryResult.Count, Is.EqualTo(1));
Assert.That(queryResult[0].PreviousRate, Is.EqualTo(_testRate));
await (t.CommitAsync());
}
}

[Test]
public async Task TestQueryOverProjectionAsync()
{
using (var s = OpenSession())
using (var t = s.BeginTransaction())
{
TestEntity testEntity = null;

var rateDto = new RateDto();
//Generated sql
//exec sp_executesql N'SELECT (case when this_.UsePreviousRate = @p0 then this_.PreviousRate else this_.Rate end) as y0_ FROM [TestEntity] this_',N'@p0 bit',@p0=1
var query = s
.QueryOver(() => testEntity)
.Select(
Projections
.Alias(
Projections.Conditional(
Restrictions.Eq(Projections.Property(() => testEntity.UsePreviousRate), true),
Projections.Property(() => testEntity.PreviousRate),
Projections.Property(() => testEntity.Rate)),
"Rate")
.WithAlias(() => rateDto.Rate));

var queryResult = await (query.TransformUsing(Transformers.AliasToBean<RateDto>()).ListAsync<RateDto>());

Assert.That(queryResult[0].Rate, Is.EqualTo(_testRate));
await (t.CommitAsync());
}
}
}
}
12 changes: 12 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/NH3787/Mappings.hbm.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="NHibernate.Test"
namespace="NHibernate.Test.NHSpecificTest.NH3787">
<class name="TestEntity" table="TestEntity">
<id name="Id">
<generator class="native"/>
</id>
<property name="UsePreviousRate" type="boolean" not-null="true"/>
<property name="PreviousRate" type="decimal(18,13)" not-null="true"/>
<property name="Rate" type="decimal(18,13)" not-null="true"/>
</class>
</hibernate-mapping>
7 changes: 7 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/NH3787/RateDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace NHibernate.Test.NHSpecificTest.NH3787
{
public class RateDto
{
public decimal Rate { get; set; }
}
}
10 changes: 10 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/NH3787/TestEntity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace NHibernate.Test.NHSpecificTest.NH3787
{
public class TestEntity
{
public virtual int Id { get; set; }
public virtual bool UsePreviousRate { get; set; }
public virtual decimal Rate { get; set; }
public virtual decimal PreviousRate { get; set; }
}
}
129 changes: 129 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/NH3787/TestFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using System.Linq;
using NHibernate.Criterion;
using NHibernate.Linq;
using NHibernate.Transform;
using NHibernate.Type;
using NUnit.Framework;

namespace NHibernate.Test.NHSpecificTest.NH3787
{
[TestFixture]
public class TestFixture : BugTestCase
{
private const decimal _testRate = 12345.1234567890123M;

protected override bool AppliesTo(Dialect.Dialect dialect)
{
return !TestDialect.HasBrokenDecimalType;
}

protected override void OnSetUp()
{
base.OnSetUp();

using (var s = OpenSession())
using (var t = s.BeginTransaction())
{
var testEntity = new TestEntity
{
UsePreviousRate = true,
PreviousRate = _testRate,
Rate = 54321.1234567890123M
};
s.Save(testEntity);
t.Commit();
}
}

protected override void OnTearDown()
{
using (var s = OpenSession())
using (var t = s.BeginTransaction())
{
s.CreateQuery("delete from TestEntity").ExecuteUpdate();
t.Commit();
}
}

[Test]
public void TestLinqQuery()
{
using (var s = OpenSession())
using (var t = s.BeginTransaction())
{
var queryResult = s
.Query<TestEntity>()
.Where(e => e.PreviousRate == _testRate)
.ToList();

Assert.That(queryResult.Count, Is.EqualTo(1));
Assert.That(queryResult[0].PreviousRate, Is.EqualTo(_testRate));
t.Commit();
}
}

[Test]
public void TestLinqProjection()
{
using (var s = OpenSession())
using (var t = s.BeginTransaction())
{
var queryResult = (from test in s.Query<TestEntity>()
select new RateDto { Rate = test.UsePreviousRate ? test.PreviousRate : test.Rate }).ToList();

// Check it has not been truncated to the default scale (10) of NHibernate.
Assert.That(queryResult[0].Rate, Is.EqualTo(_testRate));
t.Commit();
}
}

[Test]
public void TestLinqQueryOnExpression()
{
using (var s = OpenSession())
using (var t = s.BeginTransaction())
{
var queryResult = s
.Query<TestEntity>()
.Where(
// Without MappedAs, the test fails for SQL Server because it would restrict its parameter to the dialect's default scale.
e => (e.UsePreviousRate ? e.PreviousRate : e.Rate) == _testRate.MappedAs(TypeFactory.Basic("decimal(18,13)")))
.ToList();

Assert.That(queryResult.Count, Is.EqualTo(1));
Assert.That(queryResult[0].PreviousRate, Is.EqualTo(_testRate));
t.Commit();
}
}

[Test]
public void TestQueryOverProjection()
{
using (var s = OpenSession())
using (var t = s.BeginTransaction())
{
TestEntity testEntity = null;

var rateDto = new RateDto();
//Generated sql
//exec sp_executesql N'SELECT (case when this_.UsePreviousRate = @p0 then this_.PreviousRate else this_.Rate end) as y0_ FROM [TestEntity] this_',N'@p0 bit',@p0=1
var query = s
.QueryOver(() => testEntity)
.Select(
Projections
.Alias(
Projections.Conditional(
Restrictions.Eq(Projections.Property(() => testEntity.UsePreviousRate), true),
Projections.Property(() => testEntity.PreviousRate),
Projections.Property(() => testEntity.Rate)),
"Rate")
.WithAlias(() => rateDto.Rate));

var queryResult = query.TransformUsing(Transformers.AliasToBean<RateDto>()).List<RateDto>();

Assert.That(queryResult[0].Rate, Is.EqualTo(_testRate));
t.Commit();
}
}
}
}
1 change: 1 addition & 0 deletions src/NHibernate/Dialect/Dialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ protected Dialect()
RegisterFunction("upper", new StandardSQLFunction("upper"));
RegisterFunction("lower", new StandardSQLFunction("lower"));
RegisterFunction("cast", new CastFunction());
RegisterFunction("transparentcast", new TransparentCastFunction());
RegisterFunction("extract", new AnsiExtractFunction());
RegisterFunction("concat", new VarArgsSQLFunction(NHibernateUtil.String, "(", "||", ")"));

Expand Down
16 changes: 16 additions & 0 deletions src/NHibernate/Dialect/Function/TransparentCastFunction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;

namespace NHibernate.Dialect.Function
{
/// <summary>
/// A HQL only cast for helping HQL knowing the type. Does not generates any actual cast in SQL code.
/// </summary>
[Serializable]
public class TransparentCastFunction : CastFunction
{
protected override bool CastingIsRequired(string sqlType)
{
return false;
}
}
}
3 changes: 3 additions & 0 deletions src/NHibernate/Dialect/SQLiteDialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ protected virtual void RegisterFunctions()
RegisterFunction("cast", new SQLiteCastFunction());

RegisterFunction("round", new StandardSQLFunction("round"));

// NH-3787: SQLite requires the cast in SQL too for not defaulting to string.
RegisterFunction("transparentcast", new CastFunction());
}

#region private static readonly string[] DialectKeywords = { ... }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public IType FindFunctionReturnType(String functionName, IASTNode first)

if (first != null)
{
if (functionName == "cast")
if (sqlFunction is CastFunction)
{
argumentType = TypeFactory.HeuristicType(first.NextSibling.Text);
}
Expand Down
11 changes: 11 additions & 0 deletions src/NHibernate/Hql/Ast/HqlTreeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,17 @@ public HqlCast Cast(HqlExpression expression, System.Type type)
return new HqlCast(_factory, expression, type);
}

/// <summary>
/// Generate a cast node intended solely to hint HQL at the resulting type, without issuing an actual SQL cast.
/// </summary>
/// <param name="expression">The expression to cast.</param>
/// <param name="type">The resulting type.</param>
/// <returns>A <see cref="HqlTransparentCast"/> node.</returns>
public HqlTransparentCast TransparentCast(HqlExpression expression, System.Type type)
{
return new HqlTransparentCast(_factory, expression, type);
}

public HqlBitwiseNot BitwiseNot()
{
return new HqlBitwiseNot(_factory);
Expand Down
13 changes: 13 additions & 0 deletions src/NHibernate/Hql/Ast/HqlTreeNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,19 @@ public HqlCast(IASTFactory factory, HqlExpression expression, System.Type type)
}
}

/// <summary>
/// Cast node intended solely to hint HQL at the resulting type, without issuing an actual SQL cast.
/// </summary>
public class HqlTransparentCast : HqlExpression
{
public HqlTransparentCast(IASTFactory factory, HqlExpression expression, System.Type type)
: base(HqlSqlWalker.METHOD_CALL, "method", factory)
{
AddChild(new HqlIdent(factory, "transparentcast"));
AddChild(new HqlExpressionList(factory, expression, new HqlIdent(factory, type)));
}
}

public class HqlCoalesce : HqlExpression
{
public HqlCoalesce(IASTFactory factory, HqlExpression lhs, HqlExpression rhs)
Expand Down
Loading