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 all 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();
}
}
}
249 changes: 249 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/GH0831/FixtureByCode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
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()
{
Assert.Multiple(() =>
{
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");

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

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

Assert.Multiple(() =>
{
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()
{
Assert.Multiple(() =>
{
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()
{
Assert.Multiple(() =>
{
CanFilter(e => decimal.Equals(e.EntityValue, 1.0m));
CanFilter(e => decimal.Equals(1.0m, e.EntityValue));
});
}

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

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

[Test]
public void CanHandleMultiply()
{
Assert.Multiple(() =>
{
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()
{
Assert.Multiple(() =>
{
CanFilter(e => decimal.Negate(e.EntityValue) > -1.0m);
CanSelect(e => decimal.Negate(e.EntityValue));
});
}

[Test]
public void CanHandleRemainder()
{
Assume.That(TestDialect.SupportsModuloOnDecimal, Is.True);

Assert.Multiple(() =>
{
CanFilter(e => decimal.Remainder(e.EntityValue, 2m) == 0);
CanFilter(e => decimal.Remainder(2m, e.EntityValue) < 1);

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

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

Assert.Multiple(() =>
{
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()
{
Assert.Multiple(() =>
{
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));
});
}

[Test]
public void CanHandleTruncate()
{
AssumeFunctionSupported("truncate");

Assert.Multiple(() =>
{
CanFilter(e => decimal.Truncate(e.EntityValue) > 1m);
CanSelect(e => decimal.Truncate(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 = null;
Assert.That(() => inSession = session.Query<Entity>().Select(predicate).ToList(), Throws.Nothing);

Assert.That(inSession, Is.EquivalentTo(expected).Using((decimal a, decimal b) => Math.Abs(a - b) < 0.0001m));
}
}
}
}
5 changes: 5 additions & 0 deletions src/NHibernate.Test/TestDialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,10 @@ public bool SupportsSqlType(SqlType sqlType)
return false;
}
}

/// <summary>
/// Supports the modulo operator on decimal types
/// </summary>
public virtual bool SupportsModuloOnDecimal => true;
}
}
4 changes: 4 additions & 0 deletions src/NHibernate.Test/TestDialects/FirebirdTestDialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@ public FirebirdTestDialect(Dialect.Dialect dialect) : base(dialect)

public override bool SupportsComplexExpressionInGroupBy => false;
public override bool SupportsNonDataBoundCondition => false;
/// <summary>
/// Non-integer arguments are rounded before the division takes place. So, “7.5 mod 2.5” gives 2 (8 mod 3), not 0.
/// </summary>
public override bool SupportsModuloOnDecimal => false;
}
}
5 changes: 5 additions & 0 deletions src/NHibernate.Test/TestDialects/MsSqlCe40TestDialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,10 @@ public MsSqlCe40TestDialect(Dialect.Dialect dialect) : base(dialect)
public override bool SupportsDuplicatedColumnAliases => false;

public override bool SupportsEmptyInserts => false;

/// <summary>
/// Modulo is not supported on real, float, money, and numeric data types. [ Data type = numeric ]
/// </summary>
public override bool SupportsModuloOnDecimal => false;
}
}
2 changes: 2 additions & 0 deletions src/NHibernate.Test/TestDialects/SQLiteTestDialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,7 @@ public override bool SupportsHavingWithoutGroupBy
{
get { return false; }
}

public override bool SupportsModuloOnDecimal => false;
}
}
3 changes: 2 additions & 1 deletion src/NHibernate/Dialect/FirebirdDialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,8 @@ private void RegisterMathematicalFunctions()
RegisterFunction("rand", new NoArgSQLFunction("rand", NHibernateUtil.Double));
RegisterFunction("sign", new StandardSQLFunction("sign", NHibernateUtil.Int32));
RegisterFunction("sqtr", new StandardSQLFunction("sqtr", NHibernateUtil.Double));
RegisterFunction("truncate", new StandardSQLFunction("truncate"));
RegisterFunction("trunc", new StandardSQLFunction("trunc"));
RegisterFunction("truncate", new StandardSQLFunction("trunc"));
RegisterFunction("floor", new StandardSQLFunction("floor"));
RegisterFunction("round", new StandardSQLFunction("round"));
}
Expand Down
1 change: 1 addition & 0 deletions src/NHibernate/Dialect/MsSql2000Dialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ protected virtual void RegisterFunctions()
RegisterFunction("ceil", new StandardSQLFunction("ceiling"));
RegisterFunction("floor", new StandardSQLFunction("floor"));
RegisterFunction("round", new RoundEmulatingSingleParameterFunction());
RegisterFunction("truncate", new SQLFunctionTemplate(null, "round(?1, ?2, 1)"));

RegisterFunction("power", new StandardSQLFunction("power", NHibernateUtil.Double));

Expand Down
1 change: 1 addition & 0 deletions src/NHibernate/Dialect/MsSqlCeDialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ protected virtual void RegisterFunctions()
RegisterFunction("mod", new SQLFunctionTemplate(NHibernateUtil.Int32, "((?1) % (?2))"));

RegisterFunction("round", new RoundEmulatingSingleParameterFunction());
RegisterFunction("truncate", new SQLFunctionTemplate(null, "round(?1, ?2, 1)"));

RegisterFunction("bit_length", new SQLFunctionTemplate(NHibernateUtil.Int32, "datalength(?1) * 8"));
RegisterFunction("extract", new SQLFunctionTemplate(NHibernateUtil.Int32, "datepart(?1, ?3)"));
Expand Down
4 changes: 2 additions & 2 deletions src/NHibernate/Dialect/MySQLDialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,8 @@ protected virtual void RegisterFunctions()
RegisterFunction("ceiling", new StandardSQLFunction("ceiling"));
RegisterFunction("floor", new StandardSQLFunction("floor"));
RegisterFunction("round", new StandardSQLFunction("round"));
RegisterFunction("truncate", new StandardSQLFunction("truncate"));
RegisterFunction("truncate", new StandardSafeSQLFunction("truncate", 2));

RegisterFunction("rand", new NoArgSQLFunction("rand", NHibernateUtil.Double));

RegisterFunction("power", new StandardSQLFunction("power", NHibernateUtil.Double));
Expand Down
1 change: 1 addition & 0 deletions src/NHibernate/Dialect/Oracle8iDialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ protected virtual void RegisterFunctions()

RegisterFunction("round", new StandardSQLFunction("round"));
RegisterFunction("trunc", new StandardSQLFunction("trunc"));
RegisterFunction("truncate", new StandardSQLFunction("trunc"));
RegisterFunction("ceil", new StandardSQLFunction("ceil"));
RegisterFunction("ceiling", new StandardSQLFunction("ceil"));
RegisterFunction("floor", new StandardSQLFunction("floor"));
Expand Down
2 changes: 2 additions & 0 deletions src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@ protected virtual void RegisterMiscellaneousFunctions()
RegisterFunction("transactsql", new StandardSQLFunction("transactsql", NHibernateUtil.String));
RegisterFunction("varexists", new StandardSQLFunction("varexists", NHibernateUtil.Int32));
RegisterFunction("watcomsql", new StandardSQLFunction("watcomsql", NHibernateUtil.String));
RegisterFunction("truncnum", new StandardSafeSQLFunction("truncnum", 2));
Copy link
Member

Choose a reason for hiding this comment

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

I do not think that we should register specific function names for being available in HQL when using their specific dialect, since HQL should be database agnostic. RegisterFunction("truncnum" should not have been added.

Copy link
Member

Choose a reason for hiding this comment

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

I think opposite. Moreover, this is how it always was: dialect specific functions + aliases to the more common name.

Copy link
Member

Choose a reason for hiding this comment

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

"Always" widely depends on dialect. Some do register specific names for some of their functions in addition to the generic one, others do not. (By example SQL-Server registers only generic names for getdate, len, datepart, charindex, same for Postgres and now, ...)
It looks to me as something quite accidental.

This practice of registering specific names does not promote portability of HQL, while an ORM is usually meant to be database agnostic. That is why I think this is a bad thing, causing me to prefer not adding new specific names when we have a generic one. (Removing them after a release would of course be a breaking change for those using them.)

RegisterFunction("truncate", new StandardSafeSQLFunction("truncnum", 2));
}

#region private static readonly string[] DialectKeywords = { ... }
Expand Down
2 changes: 2 additions & 0 deletions src/NHibernate/Linq/Functions/CompareGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ internal class CompareGenerator : BaseHqlGeneratorForMethod, IRuntimeMethodHqlGe

ReflectHelper.GetMethodDefinition<float>(x => x.CompareTo(x)),
ReflectHelper.GetMethodDefinition<double>(x => x.CompareTo(x)),

ReflectHelper.GetMethodDefinition(() => decimal.Compare(default(decimal), default(decimal))),
ReflectHelper.GetMethodDefinition<decimal>(x => x.CompareTo(x)),

ReflectHelper.GetMethodDefinition<DateTime>(x => x.CompareTo(x)),
Expand Down
Loading