-
Notifications
You must be signed in to change notification settings - Fork 934
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
Add query support for the static methods of System.Decimal #1533
Conversation
Since this is my first contribution and the change was too easy, could someone inspect this PR more than usual? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just a quick first review. FireBird fails because Value
is a keyword and must be escaped with back-tilt in the mappings, by specifying the Column
property.
ReflectHelper.GetMethodDefinition(() => decimal.Round(default(decimal))), | ||
ReflectHelper.GetMethodDefinition(() => decimal.Round(default(decimal), default(int))), | ||
ReflectHelper.GetMethodDefinition(() => decimal.Round(default(decimal), default(MidpointRounding))), | ||
ReflectHelper.GetMethodDefinition(() => decimal.Round(default(decimal), default(int), default(MidpointRounding))), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is MidpointRounding
argument actually supported? It looks likely to me it is in fact ignored. In such case, better do as the Math
generator do: do not support overloads with this argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably not. I'll remove this option. It worked in the tests because I was passing a Func<> and not an Expression<>.
CanHandle(e => decimal.Subtract(2m, e.Value) > 1m); | ||
} | ||
|
||
private void CanHandle(Func<Entity, bool> predicate) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be Expression<Func<Entity, bool>>
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated the code so that it handles Expression<Func<Expression, bool>>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some database needs to be ignored for some tests, see comments.
SQLite completly fails. Maybe due to its broken support of decimal (it does not actually support them, it stores them as float instead ; TestDialect.HasBrokenDecimalType
is true
for SQLite due to this), but I would have still expected other test results for it. It needs some investigation I think.
[Test] | ||
public void CanHandleCeiling() | ||
{ | ||
CanHandle(e => decimal.Ceiling(e.EntityValue) > 1.0m); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oracle8iDialect
bug showcased here. Its ceiling function is registered under the name ceil
instead of being registered as ceiling
.
RegisterFunction("ceil", new StandardSQLFunction("ceil"));
Changing it would be a breaking change, so instead an additional registration would be needed, by adding the line:
RegisterFunction("ceiling", new StandardSQLFunction("ceil"));
Since this bug is a bit unrelated to #831, I am adding a PR for it (#1535).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed this as well and wanted to create a new bug for it, but you beat me to it
[Test] | ||
public void CanHandleRemainder() | ||
{ | ||
CanHandle(e => decimal.Remainder(e.EntityValue, 2) == 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some databases (SQLServerCE) do not support remainder on decimal. So a new property on TestDialect
is needed, like SupportsModuloOnDecimal
, true
by default, and overridden to false
in MsSqlCe40TestDialect
.
Then remainder test should Assume.That(TestDialect.SupportsModuloOnDecimal, Is.True);
Firebird has a special semantic for modulo on non integer arguments: it rounds them beforehand. Thus the test failure. I think we should just consider it does not support modulo on decimal, so override SupportsModuloOnDecimal
to false
in FirebirdTestDialect
too.
[Test] | ||
public void CanHandleDivide() | ||
{ | ||
CanHandle(e => decimal.Divide(e.EntityValue, 1.25m) < 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some databases (Firebird) do not support divide/multiply on decimal. So a new property on TestDialect
is needed, like SupportsDecimalMultiplication
, true
by default, and overridden to false
in FirebirdTestDialect
.
Then divide and multiply tests should Assume.That(TestDialect.SupportsDecimalMultiplication, Is.True);
[Test] | ||
public void CanHandleAdd() | ||
{ | ||
CanHandle(e => decimal.Add(e.EntityValue, 2) > 3.0m); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its failure on Firebird seems to me due to some Firebird limitation. It looks like the operation on the column is enough to cause Firebird to lose track of the data type.
Maybe a workaround could be to add a cast, but I think we should just ignore that Firebird limitation with something like:
Assume.That(TestDialect.SupportsNonDataBoundCondition, Is.True);
Granted, here the condition is data bound, but the addition (or subtraction) on the data causes the same trouble. And I lack an idea for a new TestDialect
property name for such a case.
I need to investigate the problems with SQLite a bit more. The results of executing the queries in the test are not consistent with what I see if I run the queries myself in a SQLite client. E.g. 'ceiling' is not supported if I run the query in the client, but I get no exception in the test. And the There might be something strange going on. |
If some functions are not supported and have to be ignored, something like Assume.That(Dialect.Functions, Does.ContainKey("ceiling"), Dialect + " doesn't support ceiling function."); should take care of it. Of course this would also ignore the trouble for Oracle which only registers Update: now better use |
It seems that NHibernate and/or SQLite are in a disagreement about how to interpret the return type of the methods. The following does not work:
Adding an explicit
This will be translated into @fredericDelaporte Do you have a possible direction for a solution? I'm not familiar with the codebase enough to figure it out on my own. |
Executing the query manually is probably done without using Another cause may be a kind of parameter type mismatch with the resulting type from addition. So this may be dodged by using By the way, have you tried the query with an explicitly decimal literal? |
CanHandle(e => decimal.Subtract(2m, e.EntityValue) > 1m); | ||
} | ||
|
||
private void CanHandle(Expression<Func<Entity, bool>> predicate) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A more complete test would be to not only test filtering (what does this CanHandle
, which could be renamed CanFilter
) but also test selecting.
Can you add a CanSelect
test ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. Will add it shortly
} | ||
} | ||
|
||
private void IgnoreIfNotSupported(string function) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the merge of #1535 in master, this helper should be removed for using AssumeFunctionSupported
(inherited from TestCase
) instead.
I've added a It turns out that the return types are different. The return type of the
Adding an explicit cast to From what I understand from the documentation of SQLite, it doesn't have a type system like other databases do. It determines the type of the value by looking at the value, not the column definition. It ignores the declared type. Changing the parameter to a decimal explicitly (i.e. |
…thub.com/weelink/nhibernate-core into feature/GH0831_AddQuerySupportForDecimal
So here we have a behavior mismatch to investigate. In .Net runtime, executing Should we accept such a mismatch? Well, if no database implements a decimal remainder yielding non integer results, it would be moot to not accept it. SQL Server seems to yield only integer results by example. |
{ | ||
IEnumerable<decimal> inSession = session.Query<Entity>().Select(predicate).ToList(); | ||
|
||
Assert.That(inSession.OrderBy(x => x), Is.EqualTo(expected.OrderBy(x => x)).Within(0.001M)); |
There was a problem hiding this comment.
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.
return treeBuilder.Multiply(expressions[0], expressions[1]); | ||
case "remainder": | ||
HqlMethodCall mod = treeBuilder.MethodCall("mod", expressions[0], expressions[1]); | ||
return treeBuilder.Cast(mod, typeof(decimal)); |
There was a problem hiding this comment.
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.)
SQL Server CE doesn't support this Firebird rounds the values before calculating
Actually, there is possibly incorrect generation for compare. Need to add tests which consider nullable columns
Thanks for all the help |
@fredericDelaporte if my changes are OK. I'll merge the PR in a couple of hours. |
This reverts commit 63a5b2a.
I've reverted |
I completely forgot about Decimal.Truncate. I'll try to make it work. |
In Firebird the function is called 'trunc' MySQL requires the number of decimals to be specified
The SQL Server definition can be solved by using the |
|
@@ -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)); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.)
Fixes #831