diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH3787/TestFixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH3787/TestFixture.cs
new file mode 100644
index 00000000000..0cc7243bf40
--- /dev/null
+++ b/src/NHibernate.Test/Async/NHSpecificTest/NH3787/TestFixture.cs
@@ -0,0 +1,140 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by AsyncGenerator.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+
+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()
+ .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()
+ 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()
+ .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()).ListAsync());
+
+ Assert.That(queryResult[0].Rate, Is.EqualTo(_testRate));
+ await (t.CommitAsync());
+ }
+ }
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/NH3787/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH3787/Mappings.hbm.xml
new file mode 100644
index 00000000000..56a52e4eb78
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/NH3787/Mappings.hbm.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/NHibernate.Test/NHSpecificTest/NH3787/RateDto.cs b/src/NHibernate.Test/NHSpecificTest/NH3787/RateDto.cs
new file mode 100644
index 00000000000..a36b9439733
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/NH3787/RateDto.cs
@@ -0,0 +1,7 @@
+namespace NHibernate.Test.NHSpecificTest.NH3787
+{
+ public class RateDto
+ {
+ public decimal Rate { get; set; }
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/NH3787/TestEntity.cs b/src/NHibernate.Test/NHSpecificTest/NH3787/TestEntity.cs
new file mode 100644
index 00000000000..6bad2e9c918
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/NH3787/TestEntity.cs
@@ -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; }
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/NH3787/TestFixture.cs b/src/NHibernate.Test/NHSpecificTest/NH3787/TestFixture.cs
new file mode 100644
index 00000000000..91f6bbd514f
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/NH3787/TestFixture.cs
@@ -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()
+ .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()
+ 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()
+ .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()).List();
+
+ Assert.That(queryResult[0].Rate, Is.EqualTo(_testRate));
+ t.Commit();
+ }
+ }
+ }
+}
diff --git a/src/NHibernate/Dialect/Dialect.cs b/src/NHibernate/Dialect/Dialect.cs
index 212cc1327e5..753462bbc27 100644
--- a/src/NHibernate/Dialect/Dialect.cs
+++ b/src/NHibernate/Dialect/Dialect.cs
@@ -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, "(", "||", ")"));
diff --git a/src/NHibernate/Dialect/Function/TransparentCastFunction.cs b/src/NHibernate/Dialect/Function/TransparentCastFunction.cs
new file mode 100644
index 00000000000..3572fc422ce
--- /dev/null
+++ b/src/NHibernate/Dialect/Function/TransparentCastFunction.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace NHibernate.Dialect.Function
+{
+ ///
+ /// A HQL only cast for helping HQL knowing the type. Does not generates any actual cast in SQL code.
+ ///
+ [Serializable]
+ public class TransparentCastFunction : CastFunction
+ {
+ protected override bool CastingIsRequired(string sqlType)
+ {
+ return false;
+ }
+ }
+}
diff --git a/src/NHibernate/Dialect/SQLiteDialect.cs b/src/NHibernate/Dialect/SQLiteDialect.cs
index 03748e0b022..737b8ce31e2 100644
--- a/src/NHibernate/Dialect/SQLiteDialect.cs
+++ b/src/NHibernate/Dialect/SQLiteDialect.cs
@@ -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 = { ... }
diff --git a/src/NHibernate/Hql/Ast/ANTLR/SessionFactoryHelperExtensions.cs b/src/NHibernate/Hql/Ast/ANTLR/SessionFactoryHelperExtensions.cs
index d2dc73c2934..142c8a0c4ab 100644
--- a/src/NHibernate/Hql/Ast/ANTLR/SessionFactoryHelperExtensions.cs
+++ b/src/NHibernate/Hql/Ast/ANTLR/SessionFactoryHelperExtensions.cs
@@ -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);
}
diff --git a/src/NHibernate/Hql/Ast/HqlTreeBuilder.cs b/src/NHibernate/Hql/Ast/HqlTreeBuilder.cs
index bbe3517c776..e49cac5a5b3 100755
--- a/src/NHibernate/Hql/Ast/HqlTreeBuilder.cs
+++ b/src/NHibernate/Hql/Ast/HqlTreeBuilder.cs
@@ -301,6 +301,17 @@ public HqlCast Cast(HqlExpression expression, System.Type type)
return new HqlCast(_factory, expression, type);
}
+ ///
+ /// Generate a cast node intended solely to hint HQL at the resulting type, without issuing an actual SQL cast.
+ ///
+ /// The expression to cast.
+ /// The resulting type.
+ /// A node.
+ public HqlTransparentCast TransparentCast(HqlExpression expression, System.Type type)
+ {
+ return new HqlTransparentCast(_factory, expression, type);
+ }
+
public HqlBitwiseNot BitwiseNot()
{
return new HqlBitwiseNot(_factory);
diff --git a/src/NHibernate/Hql/Ast/HqlTreeNode.cs b/src/NHibernate/Hql/Ast/HqlTreeNode.cs
index b44efbe4c84..fba59d196dd 100755
--- a/src/NHibernate/Hql/Ast/HqlTreeNode.cs
+++ b/src/NHibernate/Hql/Ast/HqlTreeNode.cs
@@ -701,6 +701,19 @@ public HqlCast(IASTFactory factory, HqlExpression expression, System.Type type)
}
}
+ ///
+ /// Cast node intended solely to hint HQL at the resulting type, without issuing an actual SQL cast.
+ ///
+ 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)
diff --git a/src/NHibernate/Linq/Visitors/HqlGeneratorExpressionVisitor.cs b/src/NHibernate/Linq/Visitors/HqlGeneratorExpressionVisitor.cs
index 0b707fb52db..598c2828bde 100644
--- a/src/NHibernate/Linq/Visitors/HqlGeneratorExpressionVisitor.cs
+++ b/src/NHibernate/Linq/Visitors/HqlGeneratorExpressionVisitor.cs
@@ -537,9 +537,12 @@ protected HqlTreeNode VisitConditionalExpression(ConditionalExpression expressio
HqlExpression @case = _hqlTreeBuilder.Case(new[] {_hqlTreeBuilder.When(test, ifTrue)}, ifFalse);
- return (expression.Type == typeof (bool) || expression.Type == (typeof (bool?)))
- ? @case
- : _hqlTreeBuilder.Cast(@case, expression.Type);
+ // If both operands are parameters, HQL will not be able to determine the resulting type before
+ // parameters binding. But it has to compute result set columns type before parameters are bound,
+ // so an artificial cast is introduced to hint HQL at the resulting type.
+ return expression.Type == typeof(bool) || expression.Type == typeof(bool?)
+ ? @case
+ : _hqlTreeBuilder.TransparentCast(@case, expression.Type);
}
protected HqlTreeNode VisitSubQueryExpression(SubQueryExpression expression)