Skip to content

Add query support for the static methods of System.Decimal #1533

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
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
0e6d7a9
Add query support for the static methods of System.Decimal #831
weelink Jan 15, 2018
ef0490e
Fix type of predicate to be expression
hazzik Jan 15, 2018
10d0567
Add missing namespace import
hazzik Jan 15, 2018
ec6ae5d
Merge branch 'master' into feature/GH0831_AddQuerySupportForDecimal
weelink Jan 16, 2018
6b86b78
Apply mapping between Decimal.* and SQL functions
weelink Jan 16, 2018
d8190c5
Ignore tests if the dialect does not support the function
weelink Jan 18, 2018
bda62c9
Merge branch 'master' into feature/GH0831_AddQuerySupportForDecimal
weelink Jan 20, 2018
1926597
Replace IgnoreIfNotSupported with TestCase.AssumeFunctionSupported
weelink Jan 20, 2018
8c98694
Add mod-function that expects a decimal
weelink Jan 20, 2018
11bfbf2
Merge branch 'master' into feature/GH0831_AddQuerySupportForDecimal
weelink Jan 21, 2018
f16a8ef
Replace 'mod_decimal'-hack with casting the result to decimal
weelink Jan 21, 2018
4724f23
Merge branch 'feature/GH0831_AddQuerySupportForDecimal' of https://gi…
weelink Jan 21, 2018
56a9aae
Merge branch 'master' into feature/GH0831_AddQuerySupportForDecimal
hazzik Jan 22, 2018
6359afb
Replace Cast with TransparentCast to avoid casting in the database
weelink Jan 22, 2018
aba2f26
Introduce a property to indicate if mod on decimal is supported
weelink Jan 23, 2018
f28a34e
Merge branch 'master' into feature/GH0831_AddQuerySupportForDecimal
weelink Jan 23, 2018
edc2996
Revert show_sql=true in the App.Config
weelink Jan 23, 2018
cc18f88
Fix 'round' for PostgreSQL
weelink Jan 27, 2018
cee8732
Merge branch 'master' into feature/GH0831_AddQuerySupportForDecimal
weelink Jan 27, 2018
7247f80
Merge branch 'master' into feature/GH0831_AddQuerySupportForDecimal
weelink Jan 28, 2018
e217489
Merge branch 'master' into feature/GH0831_AddQuerySupportForDecimal
weelink Feb 1, 2018
31887cc
Merge branch 'master' into feature/GH0831_AddQuerySupportForDecimal
weelink Feb 4, 2018
4828214
Add support for Firebird
weelink Feb 9, 2018
2f9d0e7
Transparent cast the results to the correct type
weelink Feb 12, 2018
7c61ab0
Skip testing modulo for SQLite
weelink Feb 12, 2018
d009181
Merge branch 'master' into feature/GH0831_AddQuerySupportForDecimal
weelink Feb 18, 2018
a6d2e25
Split DecimalGenerator into specific generators
weelink Feb 21, 2018
bd577f1
Explicit cast the result of 'Divide' to fix Oracle
weelink Feb 28, 2018
c6806cf
Merge branch 'master' into feature/GH0831_AddQuerySupportForDecimal
weelink Feb 28, 2018
e7cbfa0
Merge branch 'master' into feature/GH0831_AddQuerySupportForDecimal
weelink Feb 28, 2018
e1b1e5d
Make Assert.Multiple more effective
weelink Mar 1, 2018
f9acc6a
Revert accidental changes to NHibernate.sln.DotSettings
weelink Mar 1, 2018
33c985b
Compare decimal values in any order, within a small tolerance
weelink Mar 1, 2018
4b51cfb
Merge branch 'master' into feature/GH0831_AddQuerySupportForDecimal
fredericDelaporte Mar 3, 2018
e0f50c8
Merge DecimalEqualsGenerator into EqualsGenerator
hazzik Mar 3, 2018
16719fd
Merge DecimalCompareGenerator into CompareGenerator
hazzik Mar 3, 2018
28dde43
Merge DecimalFloorGenerator & DecimalCeilingGenerator into MathGenerator
hazzik Mar 3, 2018
8906825
Adjust RoundGenerator to consider Math.Round methods
hazzik Mar 3, 2018
63a5b2a
Implement Decimal.Truncate
hazzik Mar 3, 2018
50f27c4
Make generators internal
hazzik Mar 3, 2018
a533078
Revert "Implement Decimal.Truncate"
hazzik Mar 3, 2018
7c1594a
Add support for Decimal.Truncate
weelink Mar 4, 2018
3abadf6
Redefine truncate function for MySQL and SQL Server
weelink Mar 5, 2018
f6c5038
Fix truncate registration
hazzik Mar 5, 2018
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
27 changes: 27 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/GH0831/Entity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;

namespace NHibernate.Test.NHSpecificTest.GH0831
{
class Entity
{
public virtual Guid Id { get; set; }
public virtual decimal EntityValue { get; set; }

public override int GetHashCode()
{
return Id.GetHashCode();
}

public override bool Equals(object obj)
{
var that = obj as Entity;

return (that != null) && Id.Equals(that.Id);
}

public override string ToString()
{
return EntityValue.ToString();
}
}
}
201 changes: 201 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/GH0831/FixtureByCode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

using NHibernate.Cfg.MappingSchema;
using NHibernate.Mapping.ByCode;

using NUnit.Framework;

namespace NHibernate.Test.NHSpecificTest.GH0831
{
public class ByCodeFixture : TestCaseMappingByCode
{
private readonly IList<Entity> entities = new List<Entity>
{
new Entity { EntityValue = 0.5m },
new Entity { EntityValue = 1.0m },
new Entity { EntityValue = 1.5m },
new Entity { EntityValue = 2.0m },
new Entity { EntityValue = 2.5m },
new Entity { EntityValue = 3.0m }
};

protected override HbmMapping GetMappings()
{
var mapper = new ModelMapper();
mapper.Class<Entity>(rc =>
{
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
rc.Property(x => x.EntityValue);
});

return mapper.CompileMappingForAllExplicitlyAddedEntities();
}

protected override void OnSetUp()
{
using (ISession session = OpenSession())
using (ITransaction transaction = session.BeginTransaction())
{
foreach (Entity entity in entities)
{
session.Save(entity);
}

session.Flush();
transaction.Commit();
}
}

protected override void OnTearDown()
{
using (ISession session = OpenSession())
using (ITransaction transaction = session.BeginTransaction())
{
session.Delete("from System.Object");

session.Flush();
transaction.Commit();
}
}

[Test]
public void CanHandleAdd()
{
CanFilter(e => decimal.Add(e.EntityValue, 2) > 3.0m);
CanFilter(e => decimal.Add(2, e.EntityValue) > 3.0m);

CanSelect(e => decimal.Add(e.EntityValue, 2));
CanSelect(e => decimal.Add(2, e.EntityValue));
}

[Test]
public void CanHandleCeiling()
{
AssumeFunctionSupported("ceiling");

CanFilter(e => decimal.Ceiling(e.EntityValue) > 1.0m);
CanSelect(e => decimal.Ceiling(e.EntityValue));
}

[Test]
public void CanHandleCompare()
{
AssumeFunctionSupported("sign");

CanFilter(e => decimal.Compare(e.EntityValue, 1.5m) < 1);
CanFilter(e => decimal.Compare(1.0m, e.EntityValue) < 1);

CanSelect(e => decimal.Compare(e.EntityValue, 1.5m));
CanSelect(e => decimal.Compare(1.0m, e.EntityValue));
}

[Test]
public void CanHandleDivide()
{
CanFilter(e => decimal.Divide(e.EntityValue, 1.25m) < 1);
CanFilter(e => decimal.Divide(1.25m, e.EntityValue) < 1);

CanSelect(e => decimal.Divide(e.EntityValue, 1.25m));
CanSelect(e => decimal.Divide(1.25m, e.EntityValue));
}

[Test]
public void CanHandleEquals()
{
CanFilter(e => decimal.Equals(e.EntityValue, 1.0m));
CanFilter(e => decimal.Equals(1.0m, e.EntityValue));
}

[Test]
public void CanHandleFloor()
{
AssumeFunctionSupported("floor");

CanFilter(e => decimal.Floor(e.EntityValue) > 1.0m);
CanSelect(e => decimal.Floor(e.EntityValue));
}

[Test]
public void CanHandleMultiply()
{
CanFilter(e => decimal.Multiply(e.EntityValue, 10m) > 10m);
CanFilter(e => decimal.Multiply(10m, e.EntityValue) > 10m);

CanSelect(e => decimal.Multiply(e.EntityValue, 10m));
CanSelect(e => decimal.Multiply(10m, e.EntityValue));
}

[Test]
public void CanHandleNegate()
{
CanFilter(e => decimal.Negate(e.EntityValue) > -1.0m);
CanSelect(e => decimal.Negate(e.EntityValue));
}

[Test]
public void CanHandleRemainder()
{
CanFilter(e => decimal.Remainder(e.EntityValue, 2) == 0);
CanFilter(e => decimal.Remainder(2, e.EntityValue) < 1);

CanSelect(e => decimal.Remainder(e.EntityValue, 2));
CanSelect(e => decimal.Remainder(2, e.EntityValue));
}

[Test]
public void CanHandleRound()
{
AssumeFunctionSupported("round");

CanFilter(e => decimal.Round(e.EntityValue) >= 2.0m);
CanFilter(e => decimal.Round(e.EntityValue, 1) >= 1.5m);

// SQL round() always rounds up.
CanSelect(e => decimal.Round(e.EntityValue), entities.Select(e => decimal.Round(e.EntityValue, MidpointRounding.AwayFromZero)));
CanSelect(e => decimal.Round(e.EntityValue, 1), entities.Select(e => decimal.Round(e.EntityValue, 1, MidpointRounding.AwayFromZero)));
}

[Test]
public void CanHandleSubtract()
{
CanFilter(e => decimal.Subtract(e.EntityValue, 1m) > 1m);
CanFilter(e => decimal.Subtract(2m, e.EntityValue) > 1m);

CanSelect(e => decimal.Subtract(e.EntityValue, 1m));
CanSelect(e => decimal.Subtract(2m, e.EntityValue));
}

private void CanFilter(Expression<Func<Entity, bool>> predicate)
{
using (ISession session = OpenSession())
using (session.BeginTransaction())
{
IEnumerable<Entity> inMemory = entities.Where(predicate.Compile()).ToList();
IEnumerable<Entity> inSession = session.Query<Entity>().Where(predicate).ToList();

CollectionAssert.AreEquivalent(inMemory, inSession);
}
}

private void CanSelect(Expression<Func<Entity, decimal>> predicate)
{
IEnumerable<decimal> inMemory = entities.Select(predicate.Compile()).ToList();
Copy link
Member

Choose a reason for hiding this comment

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

If you wish to keep on explicitly ordering the in-memory case, add the ordering here:
entities.OrderBy(e => e.EntityValue).Select(predicate.Compile()).ToList().

Copy link
Contributor Author

@weelink weelink Mar 1, 2018

Choose a reason for hiding this comment

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

No. I would like to get rid of it entirely. It is noise. It should be replaced with an assertion that uses a tolerance when comparing values and where the order does not matter. As far as I can find, NUnit doesn't have this. It can either assert with a tolerance, or where the order doesn't matter, not both.

I noticed that NHibernate.Test already has a ObjectAssertion, NHAssert and SubclassAssert. I can add one for collections.

I've changed the assertion to ignore order.


CanSelect(predicate, inMemory);
}

private void CanSelect(Expression<Func<Entity, decimal>> predicate, IEnumerable<decimal> expected)
{
using (ISession session = OpenSession())
using (session.BeginTransaction())
{
IEnumerable<decimal> inSession = session.Query<Entity>().Select(predicate).ToList();
Copy link
Member

Choose a reason for hiding this comment

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

For the Assert.Multiple to be more effective, this line would need to be change to something like:

IEnumerable<decimal> inSession = null;
Assert.That(() => inSession = session.Query<Entity>().Select(predicate).ToList(), Throws.Nothing);

(And use inSession?.OrderBy(x => x) below, or as I was suggesting previously, change the test to:

IEnumerable<decimal> inSession = null;
Assert.That(() => inSession = session.Query<Entity>().OrderBy(e => e.EntityValue).Select(predicate).ToList(), Throws.Nothing);

Assert.That(inSession, Is.EqualTo(expected).Within(0.001M));

)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll change this soon. It took a month, but I think I'm nearly there.


Assert.That(inSession.OrderBy(x => x), Is.EqualTo(expected.OrderBy(x => x)).Within(0.001M));
Copy link
Member

Choose a reason for hiding this comment

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

I would rather avoid the order by on result values, which may hide a trouble (granted, unlikely). Instead, the inSession query could be completed by an .OrderBy(e => e.EntityValue) before the Select, which should ensure the db results have been computed on a list having the same ordering than the in-memory one.

}
}
}
}
71 changes: 71 additions & 0 deletions src/NHibernate/Linq/Functions/DecimalGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

using NHibernate.Hql.Ast;
using NHibernate.Linq.Visitors;
using NHibernate.Util;

namespace NHibernate.Linq.Functions
{
public class DecimalGenerator : BaseHqlGeneratorForMethod
{
public DecimalGenerator()
{
SupportedMethods = new[]
{
ReflectHelper.GetMethodDefinition(() => decimal.Add(default(decimal), default(decimal))),
ReflectHelper.GetMethodDefinition(() => decimal.Ceiling(default(decimal))),
ReflectHelper.GetMethodDefinition(() => decimal.Compare(default(decimal), default(decimal))),
ReflectHelper.GetMethodDefinition(() => decimal.Divide(default(decimal), default(decimal))),
ReflectHelper.GetMethodDefinition(() => decimal.Equals(default(decimal), default(decimal))),
ReflectHelper.GetMethodDefinition(() => decimal.Floor(default(decimal))),
ReflectHelper.GetMethodDefinition(() => decimal.Multiply(default(decimal), default(decimal))),
ReflectHelper.GetMethodDefinition(() => decimal.Negate(default(decimal))),
ReflectHelper.GetMethodDefinition(() => decimal.Remainder(default(decimal), default(decimal))),
ReflectHelper.GetMethodDefinition(() => decimal.Round(default(decimal))),
ReflectHelper.GetMethodDefinition(() => decimal.Round(default(decimal), default(int))),
ReflectHelper.GetMethodDefinition(() => decimal.Subtract(default(decimal), default(decimal)))
};
}

public override HqlTreeNode BuildHql(MethodInfo method, Expression targetObject, ReadOnlyCollection<Expression> arguments, HqlTreeBuilder treeBuilder, IHqlExpressionVisitor visitor)
{
string function = method.Name.ToLowerInvariant();

HqlExpression[] expressions = arguments.Select(x => visitor.Visit(x).AsExpression()).ToArray();

switch (function)
Copy link
Member

@hazzik hazzik Jan 30, 2018

Choose a reason for hiding this comment

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

It seems that it would be better to split the class into different generators.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct. After everything works I need to clean it all up.

{
case "add":
return treeBuilder.Add(expressions[0], expressions[1]);
case "subtract":
return treeBuilder.Subtract(expressions[0], expressions[1]);
case "divide":
return treeBuilder.Divide(expressions[0], expressions[1]);
case "equals":
return treeBuilder.Equality(expressions[0], expressions[1]);
case "negate":
return treeBuilder.Negate(expressions[0]);
case "compare":
return treeBuilder.MethodCall("sign", treeBuilder.Subtract(expressions[0], expressions[1]));
case "multiply":
return treeBuilder.Multiply(expressions[0], expressions[1]);
case "remainder":
HqlMethodCall mod = treeBuilder.MethodCall("mod", expressions[0], expressions[1]);
return treeBuilder.Cast(mod, typeof(decimal));
Copy link
Member

Choose a reason for hiding this comment

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

Clearly a better solution. Now is it needed to have the cast in the resulting SQL query? If no, you could use a TransparentCast instead, which is just a hint for the HQL engine, and does not go into the generated SQL query. (Well, for SQLite, it does still perform the cast in SQL too, because that db has special needs.)

case "round":
HqlExpression numberOfDecimals = (arguments.Count == 2) ? expressions[1] : treeBuilder.Constant(0);
return treeBuilder.MethodCall("round", expressions[0], numberOfDecimals);
}

if (arguments.Count == 2)
{
return treeBuilder.MethodCall(function, expressions[0], expressions[1]);
Copy link
Member

Choose a reason for hiding this comment

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

There is no functions which accepts 2 arguments anymore. Only functions left after the switch above is "ceiling" and "floor", both accept single argument.

}

return treeBuilder.MethodCall(function, expressions[0]);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public DefaultLinqToHqlGeneratorsRegistry()
this.Merge(new CollectionContainsGenerator());

this.Merge(new DateTimePropertiesHqlGenerator());

this.Merge(new DecimalGenerator());
}

protected bool GetRuntimeMethodGenerator(MethodInfo method, out IHqlGeneratorForMethod methodGenerator)
Expand Down Expand Up @@ -100,4 +102,4 @@ public void RegisterGenerator(IRuntimeMethodHqlGenerator generator)
runtimeMethodHqlGenerators.Add(generator);
}
}
}
}