diff --git a/doc/reference/modules/query_linq.xml b/doc/reference/modules/query_linq.xml index 32e0e041aa1..fceba0e0f39 100644 --- a/doc/reference/modules/query_linq.xml +++ b/doc/reference/modules/query_linq.xml @@ -8,13 +8,13 @@ The Linq provider works as an extension of the ISession. It is defined in the NHibernate.Linq namespace, so this namespace has to be imported for using the - Linq provider. Of course, the LINQ namespace is still needed too. + Linq provider. Of course, the Linq namespace is still needed too. Note: NHibernate has another querying API which uses lambda, QueryOver. - It should not be confused with a LINQ provider. + It should not be confused with a Linq provider. @@ -545,9 +545,10 @@ IList cats = } }]]> - It is required that at least one of the parameters of the method call has its value originating - from an entity. Otherwise, the Linq provider will try to evaluate the method call with .Net - runtime. + The method call will always be translated to SQL if at least one of the parameters of the + method call has its value originating from an entity. Otherwise, the Linq provider will try to + evaluate the method call with .Net runtime instead. Since NHibernate 5.0, if this runtime + evaluation fails (throws an exception), then the method call will be translated to SQL too. @@ -565,7 +566,7 @@ IList cats =   As an example, here is how to add support for an AsNullable method which - would allow to call aggregates which lay yield null without to explicitly + would allow to call aggregates which may yield null without to explicitly cast to the nullable type of the aggregate. cats = } }]]> - Adding support in Linq to NHibernate for this custom method requires a generator. For this + Adding support in Linq to NHibernate for a custom method requires a generator. For this AsNullable method, we need a method generator, declaring statically its supported method. @@ -612,8 +613,8 @@ IList cats = For adding AsNullableGenerator in Linq to NHibernate provider, a new - generators registry should be used. Derive from the default one and merge it. (Static - declaration of method support case.) + generators registry should be used. Derive from the default one and merge it. (Here we + have a static declaration of method support case.) () + .OrderBy(e => SqlServerFunction.NewID()); + + Assert.DoesNotThrow(() => { result.ToList(); }); + Assert.That(sqlInterceptor.Sql.ToString(), Does.Contain(nameof(SqlServerFunction.NewID)).IgnoreCase); + } + } + } + + public static class SqlServerFunction + { + [LinqExtensionMethod] + public static Guid NewID() + { + throw new InvalidOperationException("To be translated to SQL only"); + } + } + + public class SqlInterceptor: EmptyInterceptor + { + public SqlString Sql { get; private set; } + + public override SqlString OnPrepareStatement(SqlString sql) + { + Sql = sql; + return base.OnPrepareStatement(sql); + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3386/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH3386/Mappings.hbm.xml new file mode 100644 index 00000000000..0a13a0df6ff --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3386/Mappings.hbm.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3961/DateParametersComparedTo.cs b/src/NHibernate.Test/NHSpecificTest/NH3961/DateParametersComparedTo.cs new file mode 100644 index 00000000000..678e90c8502 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3961/DateParametersComparedTo.cs @@ -0,0 +1,282 @@ +using System; +using System.Globalization; +using System.Linq; +using NHibernate.Linq; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.NH3961 +{ + [TestFixture] + public class DateParametersComparedTo : BugTestCase + { + private DateTime _testDate; + private CultureInfo _backupCulture; + private CultureInfo _backupUICulture; + private readonly CultureInfo _testCulture = CultureInfo.GetCultureInfo("fr-FR"); + + protected override void OnSetUp() + { + using (ISession session = OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + // day > 12 for ensuring a mdy/dmy mix-up would cause a failure. + _testDate = new DateTime(2017, 03, 15); + var e1 = new Entity { Name = "Bob", NullableDateTime = _testDate, NonNullableDateTime = _testDate }; + session.Save(e1); + + var e2 = new Entity { Name = "Sally", NullableDateTime = _testDate.AddDays(1), NonNullableDateTime = _testDate.AddDays(1) }; + session.Save(e2); + + session.Flush(); + transaction.Commit(); + } + + _backupCulture = CultureInfo.CurrentCulture; + _backupUICulture = CultureInfo.CurrentUICulture; + // "CultureInfo.CurrentCulture =": Fx 4.6 only; affect trough Thread.CurrentThread instead if in need of supporting a previous Fx. + // This test needs a culture using a dmy date format. If the test system does not support fr-FR, try find another one... + // This test assumes the SQL user language is set as English, otherwise it may not showcase the failure. + CultureInfo.CurrentCulture = _testCulture; + CultureInfo.CurrentUICulture = _testCulture; + } + + protected override void OnTearDown() + { + using (ISession session = OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + session.Delete("from System.Object"); + + session.Flush(); + transaction.Commit(); + } + CultureInfo.CurrentCulture = _backupCulture; + CultureInfo.CurrentUICulture = _backupUICulture; + } + + // Non-reg test case + [Test] + public void NonNullableMappedAsDateShouldBeCultureAgnostic() + { + using (ISession session = OpenSession()) + using (session.BeginTransaction()) + { + var result = session.Query() + .Where(e => e.NonNullableDateTime == _testDate.MappedAs(NHibernateUtil.Date)) + .ToList(); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("Bob", result[0].Name); + } + } + + // Non-reg test case + [Test] + public void NonNullableMappedAsDateShouldIgnoreTime() + { + using (ISession session = OpenSession()) + { + var result = session.Query() + .Where(e => e.NonNullableDateTime == _testDate.AddMinutes(10).MappedAs(NHibernateUtil.Date)) + .ToList(); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("Bob", result[0].Name); + } + } + + // Non-reg test case + [Test] + public void NonNullableMappedAsDateTimeShouldBeCultureAgnostic() + { + using (ISession session = OpenSession()) + using (session.BeginTransaction()) + { + var result = session.Query() + .Where(e => e.NonNullableDateTime == _testDate.MappedAs(NHibernateUtil.DateTime)) + .ToList(); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("Bob", result[0].Name); + } + } + + // Non-reg test case + [Test] + public void NonNullableMappedAsTimestampShouldBeCultureAgnostic() + { + using (ISession session = OpenSession()) + using (session.BeginTransaction()) + { + var result = session.Query() + .Where(e => e.NonNullableDateTime == _testDate.MappedAs(NHibernateUtil.Timestamp)) + .ToList(); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("Bob", result[0].Name); + } + } + + // Non-reg test case + [Test] + public void NonNullableParameterValueShouldNotBeCachedWithMappedAsAnd() + { + // Dodges the query parameter formatting bug for showcasing the parameter value bug + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-US"); + CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture; + try + { + using (ISession session = OpenSession()) + { + var result = session.Query() + .Where(e => e.NonNullableDateTime == _testDate.MappedAs(NHibernateUtil.DateTime)) + .ToList(); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("Bob", result[0].Name); + + var testDate = _testDate.AddMinutes(10); + result = session.Query() + .Where(e => e.NonNullableDateTime == testDate.MappedAs(NHibernateUtil.DateTime)) + .ToList(); + + CollectionAssert.IsEmpty(result); + } + } + finally + { + CultureInfo.CurrentCulture = _testCulture; + CultureInfo.CurrentUICulture = _testCulture; + } + } + + // Non-reg test case + [Test] + public void NonNullableShouldBeCultureAgnostic() + { + using (ISession session = OpenSession()) + using (session.BeginTransaction()) + { + var result = session.Query() + .Where(e => e.NonNullableDateTime == _testDate) + .ToList(); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("Bob", result[0].Name); + } + } + + // Failing test case till NH-3961 is fixed + [Test] + public void NullableMappedAsDateShouldBeCultureAgnostic() + { + using (ISession session = OpenSession()) + using (session.BeginTransaction()) + { + var result = session.Query() + .Where(e => e.NullableDateTime == _testDate.MappedAs(NHibernateUtil.Date)) + .ToList(); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("Bob", result[0].Name); + } + } + + // Failing test case till NH-3961 is fixed + [Test] + public void NullableMappedAsDateShouldIgnoreTime() + { + var testDate = _testDate.AddMinutes(10); + using (ISession session = OpenSession()) + { + var result = session.Query() + .Where(e => e.NullableDateTime == testDate.MappedAs(NHibernateUtil.Date)) + .ToList(); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("Bob", result[0].Name); + } + } + + // Failing test case till NH-3961 is fixed + [Test] + public void NullableMappedAsDateTimeShouldBeCultureAgnostic() + { + using (ISession session = OpenSession()) + using (session.BeginTransaction()) + { + var result = session.Query() + .Where(e => e.NullableDateTime == _testDate.MappedAs(NHibernateUtil.DateTime)) + .ToList(); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("Bob", result[0].Name); + } + } + + // Failing test case till NH-3961 is fixed + [Test] + public void NullableMappedAsTimestampShouldBeCultureAgnostic() + { + using (ISession session = OpenSession()) + using (session.BeginTransaction()) + { + var result = session.Query() + .Where(e => e.NullableDateTime == _testDate.MappedAs(NHibernateUtil.Timestamp)) + .ToList(); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("Bob", result[0].Name); + } + } + + // Failing test case till NH-3961 is fixed + [Test] + public void NullableParameterValueShouldNotBeCachedWithMappedAs() + { + // Dodges the query parameter formatting bug for showcasing the parameter value bug + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-US"); + CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture; + try + { + using (ISession session = OpenSession()) + { + var result = session.Query() + .Where(e => e.NullableDateTime == _testDate.MappedAs(NHibernateUtil.DateTime)) + .ToList(); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("Bob", result[0].Name); + + var testDate = _testDate.AddMinutes(10); + result = session.Query() + .Where(e => e.NullableDateTime == testDate.MappedAs(NHibernateUtil.DateTime)) + .ToList(); + + CollectionAssert.IsEmpty(result); + } + } + finally + { + CultureInfo.CurrentCulture = _testCulture; + CultureInfo.CurrentUICulture = _testCulture; + } + } + + // Non-reg test case + [Test] + public void NullableShouldBeCultureAgnostic() + { + using (ISession session = OpenSession()) + using (session.BeginTransaction()) + { + var result = session.Query() + .Where(e => e.NullableDateTime == _testDate) + .ToList(); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("Bob", result[0].Name); + } + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3961/Entity.cs b/src/NHibernate.Test/NHSpecificTest/NH3961/Entity.cs new file mode 100644 index 00000000000..c5fbfca1b15 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3961/Entity.cs @@ -0,0 +1,12 @@ +using System; + +namespace NHibernate.Test.NHSpecificTest.NH3961 +{ + public class Entity + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + public virtual DateTime NonNullableDateTime { get; set; } + public virtual DateTime? NullableDateTime { get; set; } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3961/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH3961/Mappings.hbm.xml new file mode 100644 index 00000000000..3795a14547c --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3961/Mappings.hbm.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/NHibernate.Test/NHibernate.Test.csproj b/src/NHibernate.Test/NHibernate.Test.csproj index 8fae8736b23..e6b4cd82bf4 100644 --- a/src/NHibernate.Test/NHibernate.Test.csproj +++ b/src/NHibernate.Test/NHibernate.Test.csproj @@ -737,6 +737,10 @@ + + + + @@ -3208,6 +3212,8 @@ + + diff --git a/src/NHibernate/Linq/NhRelinqQueryParser.cs b/src/NHibernate/Linq/NhRelinqQueryParser.cs index 86c1410099e..4e3b04f04e1 100644 --- a/src/NHibernate/Linq/NhRelinqQueryParser.cs +++ b/src/NHibernate/Linq/NhRelinqQueryParser.cs @@ -12,6 +12,7 @@ using Remotion.Linq.Parsing.Structure; using Remotion.Linq.Parsing.Structure.IntermediateModel; using Remotion.Linq.Parsing.Structure.NodeTypeProviders; +using Remotion.Linq.Parsing.Structure.ExpressionTreeProcessors; namespace NHibernate.Linq { @@ -26,9 +27,11 @@ static NhRelinqQueryParser() transformerRegistry.Register(new RemoveRedundantCast()); transformerRegistry.Register(new SimplifyCompareTransformer()); - var processor = ExpressionTreeParser.CreateDefaultProcessor(transformerRegistry); - // Add custom processors here: - // processor.InnerProcessors.Add (new MyExpressionTreeProcessor()); + // If needing a compound processor for adding other processing, do not use + // ExpressionTreeParser.CreateDefaultProcessor(transformerRegistry), it would + // cause NH-3961 again by including a PartialEvaluatingExpressionTreeProcessor. + // Directly instanciate a CompoundExpressionTreeProcessor instead. + var processor = new TransformingExpressionTreeProcessor(transformerRegistry); var nodeTypeProvider = new NHibernateNodeTypeProvider();