diff --git a/doc/reference/modules/configuration.xml b/doc/reference/modules/configuration.xml
index cb67b15519a..ae80b0a1c10 100644
--- a/doc/reference/modules/configuration.xml
+++ b/doc/reference/modules/configuration.xml
@@ -717,6 +717,57 @@ var session = sessions.OpenSession(conn);
+
+
+ linqtohql.legacy_preevaluation
+
+
+ Whether to use the legacy pre-evaluation or not in Linq queries. Defaults to true .
+
+ eg.
+ true | false
+
+
+ Legacy pre-evaluation is causing special properties or functions like DateTime.Now
+ or Guid.NewGuid() to be always evaluated with the .Net runtime and replaced in the
+ query by parameter values.
+
+
+ The new pre-evaluation allows them to be converted to HQL function calls which will be run on the db
+ side. This allows for example to retrieve the server time instead of the client time, or to generate
+ UUIDs for each row instead of an unique one for all rows.
+
+
+ The new pre-evaluation will likely be enabled by default in the next major version (6.0).
+
+
+
+
+
+ linqtohql.fallback_on_preevaluation
+
+
+ When the new pre-evaluation is enabled, should methods which translation is not supported by the current
+ dialect fallback to pre-evaluation? Defaults to false .
+
+ eg.
+ true | false
+
+
+ When this fallback option is enabled while legacy pre-evaluation is disabled, properties or functions
+ like DateTime.Now or Guid.NewGuid() used in Linq expressions
+ will not fail when the dialect does not support them, but will instead be pre-evaluated.
+
+
+ When this fallback option is disabled while legacy pre-evaluation is disabled, properties or functions
+ like DateTime.Now or Guid.NewGuid() used in Linq expressions
+ will fail when the dialect does not support them.
+
+
+ This option has no effect if the legacy pre-evaluation is enabled.
+
+
+
sql_exception_converter
diff --git a/src/NHibernate.Test/Async/Linq/MiscellaneousTextFixture.cs b/src/NHibernate.Test/Async/Linq/MiscellaneousTextFixture.cs
index 2d660623a63..588d8e112ee 100644
--- a/src/NHibernate.Test/Async/Linq/MiscellaneousTextFixture.cs
+++ b/src/NHibernate.Test/Async/Linq/MiscellaneousTextFixture.cs
@@ -27,7 +27,8 @@ public class MiscellaneousTextFixtureAsync : LinqTestCase
[Test(Description = "This sample uses Count to find the number of Orders placed before yesterday in the database.")]
public async Task CountWithWhereClauseAsync()
{
- var q = from o in db.Orders where o.OrderDate <= DateTime.Today.AddDays(-1) select o;
+ var yesterday = DateTime.Today.AddDays(-1);
+ var q = from o in db.Orders where o.OrderDate <= yesterday select o;
var count = await (q.CountAsync());
diff --git a/src/NHibernate.Test/Async/Linq/PreEvaluationTests.cs b/src/NHibernate.Test/Async/Linq/PreEvaluationTests.cs
new file mode 100644
index 00000000000..1f55f7b6cb8
--- /dev/null
+++ b/src/NHibernate.Test/Async/Linq/PreEvaluationTests.cs
@@ -0,0 +1,152 @@
+//------------------------------------------------------------------------------
+//
+// 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;
+using System.Collections.Generic;
+using System.Linq;
+using NHibernate.Cfg;
+using NHibernate.SqlTypes;
+using NUnit.Framework;
+using Environment = NHibernate.Cfg.Environment;
+using NHibernate.Linq;
+
+namespace NHibernate.Test.Linq
+{
+ using System.Threading.Tasks;
+ [TestFixture(false, false)]
+ [TestFixture(true, false)]
+ [TestFixture(false, true)]
+ public class PreEvaluationTestsAsync : LinqTestCase
+ {
+ private readonly bool LegacyPreEvaluation;
+ private readonly bool FallbackOnPreEvaluation;
+
+ public PreEvaluationTestsAsync(bool legacy, bool fallback)
+ {
+ LegacyPreEvaluation = legacy;
+ FallbackOnPreEvaluation = fallback;
+ }
+
+ protected override void Configure(Configuration configuration)
+ {
+ base.Configure(configuration);
+
+ configuration.SetProperty(Environment.FormatSql, "false");
+ configuration.SetProperty(Environment.LinqToHqlLegacyPreEvaluation, LegacyPreEvaluation.ToString());
+ configuration.SetProperty(Environment.LinqToHqlFallbackOnPreEvaluation, FallbackOnPreEvaluation.ToString());
+ }
+
+ private void RunTest(bool isSupported, Action test)
+ {
+ using (var spy = new SqlLogSpy())
+ {
+ try
+ {
+ test(spy);
+ }
+ catch (QueryException)
+ {
+ if (!isSupported && !FallbackOnPreEvaluation)
+ // Expected failure
+ return;
+ throw;
+ }
+ }
+
+ if (!isSupported && !FallbackOnPreEvaluation)
+ Assert.Fail("The test should have thrown a QueryException, but has not thrown anything");
+ }
+
+ [Test]
+ public async Task CanQueryByRandomIntAsync()
+ {
+ var isSupported = IsFunctionSupported("random") && IsFunctionSupported("floor");
+ var idMin = await (db.Orders.MinAsync(o => o.OrderId));
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var random = new Random();
+ // Dodge a Firebird driver limitation by putting the constants before the order id.
+ // This driver cast parameters to their types in some cases for avoiding Firebird complaining of not
+ // knowing the type of the condition. For some reasons the driver considers the casting should not be
+ // done next to the conditional operator. Having the cast only on one side is enough for avoiding
+ // Firebird complain, so moving the constants on the left side have been put before the order id, in
+ // order for these constants to be casted by the driver.
+ var x = db.Orders.Count(o => -idMin - 1 + o.OrderId < random.Next());
+
+ Assert.That(x, Is.GreaterThan(0));
+ // Next requires support of both floor and rand
+ AssertFunctionInSql(IsFunctionSupported("floor") ? "random" : "floor", spy);
+ });
+ }
+
+ [Test]
+ public async Task CanQueryByRandomIntWithMaxAsync()
+ {
+ var isSupported = IsFunctionSupported("random") && IsFunctionSupported("floor");
+ var idMin = await (db.Orders.MinAsync(o => o.OrderId));
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var random = new Random();
+ // Dodge a Firebird driver limitation by putting the constants before the order id.
+ // This driver cast parameters to their types in some cases for avoiding Firebird complaining of not
+ // knowing the type of the condition. For some reasons the driver considers the casting should not be
+ // done next to the conditional operator. Having the cast only on one side is enough for avoiding
+ // Firebird complain, so moving the constants on the left side have been put before the order id, in
+ // order for these constants to be casted by the driver.
+ var x = db.Orders.Count(o => -idMin + o.OrderId <= random.Next(10));
+
+ Assert.That(x, Is.GreaterThan(0).And.LessThan(11));
+ // Next requires support of both floor and rand
+ AssertFunctionInSql(IsFunctionSupported("floor") ? "random" : "floor", spy);
+ });
+ }
+
+ [Test]
+ public async Task CanQueryByRandomIntWithMinMaxAsync()
+ {
+ var isSupported = IsFunctionSupported("random") && IsFunctionSupported("floor");
+ var idMin = await (db.Orders.MinAsync(o => o.OrderId));
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var random = new Random();
+ // Dodge a Firebird driver limitation by putting the constants before the order id.
+ // This driver cast parameters to their types in some cases for avoiding Firebird complaining of not
+ // knowing the type of the condition. For some reasons the driver considers the casting should not be
+ // done next to the conditional operator. Having the cast only on one side is enough for avoiding
+ // Firebird complain, so moving the constants on the left side have been put before the order id, in
+ // order for these constants to be casted by the driver.
+ var x = db.Orders.Count(o => -idMin + o.OrderId < random.Next(1, 10));
+
+ Assert.That(x, Is.GreaterThan(0).And.LessThan(10));
+ // Next requires support of both floor and rand
+ AssertFunctionInSql(IsFunctionSupported("floor") ? "random" : "floor", spy);
+ });
+ }
+
+ private void AssertFunctionInSql(string functionName, SqlLogSpy spy)
+ {
+ if (!IsFunctionSupported(functionName))
+ Assert.Inconclusive($"{functionName} is not supported by the dialect");
+
+ var function = Dialect.Functions[functionName].Render(new List(), Sfi).ToString();
+
+ if (LegacyPreEvaluation)
+ Assert.That(spy.GetWholeLog(), Does.Not.Contain(function));
+ else
+ Assert.That(spy.GetWholeLog(), Does.Contain(function));
+ }
+ }
+}
diff --git a/src/NHibernate.Test/Linq/MiscellaneousTextFixture.cs b/src/NHibernate.Test/Linq/MiscellaneousTextFixture.cs
index 9eada444639..983919e2914 100644
--- a/src/NHibernate.Test/Linq/MiscellaneousTextFixture.cs
+++ b/src/NHibernate.Test/Linq/MiscellaneousTextFixture.cs
@@ -27,7 +27,8 @@ from s in db.Shippers
[Test(Description = "This sample uses Count to find the number of Orders placed before yesterday in the database.")]
public void CountWithWhereClause()
{
- var q = from o in db.Orders where o.OrderDate <= DateTime.Today.AddDays(-1) select o;
+ var yesterday = DateTime.Today.AddDays(-1);
+ var q = from o in db.Orders where o.OrderDate <= yesterday select o;
var count = q.Count();
diff --git a/src/NHibernate.Test/Linq/PreEvaluationTests.cs b/src/NHibernate.Test/Linq/PreEvaluationTests.cs
new file mode 100644
index 00000000000..4e12c4b9d82
--- /dev/null
+++ b/src/NHibernate.Test/Linq/PreEvaluationTests.cs
@@ -0,0 +1,524 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NHibernate.Cfg;
+using NHibernate.SqlTypes;
+using NUnit.Framework;
+using Environment = NHibernate.Cfg.Environment;
+
+namespace NHibernate.Test.Linq
+{
+ [TestFixture(false, false)]
+ [TestFixture(true, false)]
+ [TestFixture(false, true)]
+ public class PreEvaluationTests : LinqTestCase
+ {
+ private readonly bool LegacyPreEvaluation;
+ private readonly bool FallbackOnPreEvaluation;
+
+ public PreEvaluationTests(bool legacy, bool fallback)
+ {
+ LegacyPreEvaluation = legacy;
+ FallbackOnPreEvaluation = fallback;
+ }
+
+ protected override void Configure(Configuration configuration)
+ {
+ base.Configure(configuration);
+
+ configuration.SetProperty(Environment.FormatSql, "false");
+ configuration.SetProperty(Environment.LinqToHqlLegacyPreEvaluation, LegacyPreEvaluation.ToString());
+ configuration.SetProperty(Environment.LinqToHqlFallbackOnPreEvaluation, FallbackOnPreEvaluation.ToString());
+ }
+
+ [Test]
+ public void CanQueryByDateTimeNowUsingNotEqual()
+ {
+ var isSupported = IsFunctionSupported("current_timestamp");
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var x = db.Orders.Count(o => o.OrderDate.Value != DateTime.Now);
+
+ Assert.That(x, Is.GreaterThan(0));
+ AssertFunctionInSql("current_timestamp", spy);
+ });
+ }
+
+ [Test]
+ public void CanQueryByDateTimeNow()
+ {
+ var isSupported = IsFunctionSupported("current_timestamp");
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var x = db.Orders.Count(o => o.OrderDate.Value < DateTime.Now);
+
+ Assert.That(x, Is.GreaterThan(0));
+ AssertFunctionInSql("current_timestamp", spy);
+ });
+ }
+
+ [Test]
+ public void CanSelectDateTimeNow()
+ {
+ var isSupported = IsFunctionSupported("current_timestamp");
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var x =
+ db
+ .Orders.Select(o => new { id = o.OrderId, d = DateTime.Now })
+ .OrderBy(o => o.id).Take(1).ToList();
+
+ Assert.That(x, Has.Count.GreaterThan(0));
+ Assert.That(x[0].d.Kind, Is.EqualTo(DateTimeKind.Local));
+ AssertFunctionInSql("current_timestamp", spy);
+ });
+ }
+
+ [Test]
+ public void CanQueryByDateTimeUtcNow()
+ {
+ var isSupported = IsFunctionSupported("current_utctimestamp");
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var x = db.Orders.Count(o => o.OrderDate.Value < DateTime.UtcNow);
+
+ Assert.That(x, Is.GreaterThan(0));
+ AssertFunctionInSql("current_utctimestamp", spy);
+ });
+ }
+
+ [Test]
+ public void CanSelectDateTimeUtcNow()
+ {
+ var isSupported = IsFunctionSupported("current_utctimestamp");
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var x =
+ db
+ .Orders.Select(o => new { id = o.OrderId, d = DateTime.UtcNow })
+ .OrderBy(o => o.id).Take(1).ToList();
+
+ Assert.That(x, Has.Count.GreaterThan(0));
+ Assert.That(x[0].d.Kind, Is.EqualTo(DateTimeKind.Utc));
+ AssertFunctionInSql("current_utctimestamp", spy);
+ });
+ }
+
+ [Test]
+ public void CanQueryByDateTimeToday()
+ {
+ var isSupported = IsFunctionSupported("current_date");
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var x = db.Orders.Count(o => o.OrderDate.Value < DateTime.Today);
+
+ Assert.That(x, Is.GreaterThan(0));
+ AssertFunctionInSql("current_date", spy);
+ });
+ }
+
+ [Test]
+ public void CanSelectDateTimeToday()
+ {
+ var isSupported = IsFunctionSupported("current_date");
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var x =
+ db
+ .Orders.Select(o => new { id = o.OrderId, d = DateTime.Today })
+ .OrderBy(o => o.id).Take(1).ToList();
+
+ Assert.That(x, Has.Count.GreaterThan(0));
+ Assert.That(x[0].d.Kind, Is.EqualTo(DateTimeKind.Local));
+ AssertFunctionInSql("current_date", spy);
+ });
+ }
+
+ [Test]
+ public void CanQueryByDateTimeOffsetTimeNow()
+ {
+ if (!TestDialect.SupportsSqlType(SqlTypeFactory.DateTimeOffSet))
+ Assert.Ignore("Dialect does not support DateTimeOffSet");
+
+ var isSupported = IsFunctionSupported("current_timestamp_offset");
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var testDate = DateTimeOffset.Now.AddDays(-1);
+ var x = db.Orders.Count(o => testDate < DateTimeOffset.Now);
+
+ Assert.That(x, Is.GreaterThan(0));
+ AssertFunctionInSql("current_timestamp_offset", spy);
+ });
+ }
+
+ [Test]
+ public void CanSelectDateTimeOffsetNow()
+ {
+ if (!TestDialect.SupportsSqlType(SqlTypeFactory.DateTimeOffSet))
+ Assert.Ignore("Dialect does not support DateTimeOffSet");
+
+ var isSupported = IsFunctionSupported("current_timestamp_offset");
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var x =
+ db
+ .Orders.Select(o => new { id = o.OrderId, d = DateTimeOffset.Now })
+ .OrderBy(o => o.id).Take(1).ToList();
+
+ Assert.That(x, Has.Count.GreaterThan(0));
+ Assert.That(x[0].d.Offset, Is.EqualTo(DateTimeOffset.Now.Offset));
+ AssertFunctionInSql("current_timestamp_offset", spy);
+ });
+ }
+
+ [Test]
+ public void CanQueryByDateTimeOffsetUtcNow()
+ {
+ if (!TestDialect.SupportsSqlType(SqlTypeFactory.DateTimeOffSet))
+ Assert.Ignore("Dialect does not support DateTimeOffSet");
+
+ var isSupported = IsFunctionSupported("current_utctimestamp_offset");
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var testDate = DateTimeOffset.UtcNow.AddDays(-1);
+ var x = db.Orders.Count(o => testDate < DateTimeOffset.UtcNow);
+
+ Assert.That(x, Is.GreaterThan(0));
+ AssertFunctionInSql("current_utctimestamp_offset", spy);
+ });
+ }
+
+ [Test]
+ public void CanSelectDateTimeOffsetUtcNow()
+ {
+ if (!TestDialect.SupportsSqlType(SqlTypeFactory.DateTimeOffSet))
+ Assert.Ignore("Dialect does not support DateTimeOffSet");
+
+ var isSupported = IsFunctionSupported("current_utctimestamp_offset");
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var x =
+ db
+ .Orders.Select(o => new { id = o.OrderId, d = DateTimeOffset.UtcNow })
+ .OrderBy(o => o.id).Take(1).ToList();
+
+ Assert.That(x, Has.Count.GreaterThan(0));
+ Assert.That(x[0].d.Offset, Is.EqualTo(TimeSpan.Zero));
+ AssertFunctionInSql("current_utctimestamp_offset", spy);
+ });
+ }
+
+ private void RunTest(bool isSupported, Action test)
+ {
+ using (var spy = new SqlLogSpy())
+ {
+ try
+ {
+ test(spy);
+ }
+ catch (QueryException)
+ {
+ if (!isSupported && !FallbackOnPreEvaluation)
+ // Expected failure
+ return;
+ throw;
+ }
+ }
+
+ if (!isSupported && !FallbackOnPreEvaluation)
+ Assert.Fail("The test should have thrown a QueryException, but has not thrown anything");
+ }
+
+ [Test]
+ public void CanQueryByNewGuid()
+ {
+ if (!TestDialect.SupportsSqlType(SqlTypeFactory.Guid))
+ Assert.Ignore("Guid are not supported by the target database");
+
+ var isSupported = IsFunctionSupported("new_uuid");
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var guid = Guid.NewGuid();
+ var x = db.Orders.Count(o => guid != Guid.NewGuid());
+
+ Assert.That(x, Is.GreaterThan(0));
+ AssertFunctionInSql("new_uuid", spy);
+ });
+ }
+
+ [Test]
+ public void CanSelectNewGuid()
+ {
+ if (!TestDialect.SupportsSqlType(SqlTypeFactory.Guid))
+ Assert.Ignore("Guid are not supported by the target database");
+
+ var isSupported = IsFunctionSupported("new_uuid");
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var x =
+ db
+ .Orders.Select(o => new { id = o.OrderId, g = Guid.NewGuid() })
+ .OrderBy(o => o.id).Take(1).ToList();
+
+ Assert.That(x, Has.Count.GreaterThan(0));
+ AssertFunctionInSql("new_uuid", spy);
+ });
+ }
+
+ [Test]
+ public void CanQueryByRandomDouble()
+ {
+ var isSupported = IsFunctionSupported("random");
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var random = new Random();
+ var x = db.Orders.Count(o => o.OrderId > random.NextDouble());
+
+ Assert.That(x, Is.GreaterThan(0));
+ AssertFunctionInSql("random", spy);
+ });
+ }
+
+ [Test]
+ public void CanSelectRandomDouble()
+ {
+ var isSupported = IsFunctionSupported("random");
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var random = new Random();
+ var x =
+ db
+ .Orders.Select(o => new { id = o.OrderId, r = random.NextDouble() })
+ .OrderBy(o => o.id).ToList();
+
+ Assert.That(x, Has.Count.GreaterThan(0));
+ var randomValues = x.Select(o => o.r).Distinct().ToArray();
+ Assert.That(randomValues, Has.All.GreaterThanOrEqualTo(0).And.LessThan(1));
+
+ if (!LegacyPreEvaluation && IsFunctionSupported("random"))
+ {
+ // Naïve randomness check
+ Assert.That(
+ randomValues,
+ Has.Length.GreaterThan(x.Count / 2),
+ "Generated values do not seem very random");
+ }
+
+ AssertFunctionInSql("random", spy);
+ });
+ }
+
+ [Test]
+ public void CanQueryByRandomInt()
+ {
+ var isSupported = IsFunctionSupported("random") && IsFunctionSupported("floor");
+ var idMin = db.Orders.Min(o => o.OrderId);
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var random = new Random();
+ // Dodge a Firebird driver limitation by putting the constants before the order id.
+ // This driver cast parameters to their types in some cases for avoiding Firebird complaining of not
+ // knowing the type of the condition. For some reasons the driver considers the casting should not be
+ // done next to the conditional operator. Having the cast only on one side is enough for avoiding
+ // Firebird complain, so moving the constants on the left side have been put before the order id, in
+ // order for these constants to be casted by the driver.
+ var x = db.Orders.Count(o => -idMin - 1 + o.OrderId < random.Next());
+
+ Assert.That(x, Is.GreaterThan(0));
+ // Next requires support of both floor and rand
+ AssertFunctionInSql(IsFunctionSupported("floor") ? "random" : "floor", spy);
+ });
+ }
+
+ [Test]
+ public void CanSelectRandomInt()
+ {
+ var isSupported = IsFunctionSupported("random") && IsFunctionSupported("floor");
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var random = new Random();
+ var x =
+ db
+ .Orders.Select(o => new { id = o.OrderId, r = random.Next() })
+ .OrderBy(o => o.id).ToList();
+
+ Assert.That(x, Has.Count.GreaterThan(0));
+ var randomValues = x.Select(o => o.r).Distinct().ToArray();
+ Assert.That(
+ randomValues,
+ Has.All.GreaterThanOrEqualTo(0).And.LessThan(int.MaxValue).And.TypeOf());
+
+ if (!LegacyPreEvaluation && IsFunctionSupported("random") && IsFunctionSupported("floor"))
+ {
+ // Naïve randomness check
+ Assert.That(
+ randomValues,
+ Has.Length.GreaterThan(x.Count / 2),
+ "Generated values do not seem very random");
+ }
+
+ // Next requires support of both floor and rand
+ AssertFunctionInSql(IsFunctionSupported("floor") ? "random" : "floor", spy);
+ });
+ }
+
+ [Test]
+ public void CanQueryByRandomIntWithMax()
+ {
+ var isSupported = IsFunctionSupported("random") && IsFunctionSupported("floor");
+ var idMin = db.Orders.Min(o => o.OrderId);
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var random = new Random();
+ // Dodge a Firebird driver limitation by putting the constants before the order id.
+ // This driver cast parameters to their types in some cases for avoiding Firebird complaining of not
+ // knowing the type of the condition. For some reasons the driver considers the casting should not be
+ // done next to the conditional operator. Having the cast only on one side is enough for avoiding
+ // Firebird complain, so moving the constants on the left side have been put before the order id, in
+ // order for these constants to be casted by the driver.
+ var x = db.Orders.Count(o => -idMin + o.OrderId <= random.Next(10));
+
+ Assert.That(x, Is.GreaterThan(0).And.LessThan(11));
+ // Next requires support of both floor and rand
+ AssertFunctionInSql(IsFunctionSupported("floor") ? "random" : "floor", spy);
+ });
+ }
+
+ [Test]
+ public void CanSelectRandomIntWithMax()
+ {
+ var isSupported = IsFunctionSupported("random") && IsFunctionSupported("floor");
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var random = new Random();
+ var x =
+ db
+ .Orders.Select(o => new { id = o.OrderId, r = random.Next(10) })
+ .OrderBy(o => o.id).ToList();
+
+ Assert.That(x, Has.Count.GreaterThan(0));
+ var randomValues = x.Select(o => o.r).Distinct().ToArray();
+ Assert.That(randomValues, Has.All.GreaterThanOrEqualTo(0).And.LessThan(10).And.TypeOf());
+
+ if (!LegacyPreEvaluation && IsFunctionSupported("random") && IsFunctionSupported("floor"))
+ {
+ // Naïve randomness check
+ Assert.That(
+ randomValues,
+ Has.Length.GreaterThan(Math.Min(10, x.Count) / 2),
+ "Generated values do not seem very random");
+ }
+
+ // Next requires support of both floor and rand
+ AssertFunctionInSql(IsFunctionSupported("floor") ? "random" : "floor", spy);
+ });
+ }
+
+ [Test]
+ public void CanQueryByRandomIntWithMinMax()
+ {
+ var isSupported = IsFunctionSupported("random") && IsFunctionSupported("floor");
+ var idMin = db.Orders.Min(o => o.OrderId);
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var random = new Random();
+ // Dodge a Firebird driver limitation by putting the constants before the order id.
+ // This driver cast parameters to their types in some cases for avoiding Firebird complaining of not
+ // knowing the type of the condition. For some reasons the driver considers the casting should not be
+ // done next to the conditional operator. Having the cast only on one side is enough for avoiding
+ // Firebird complain, so moving the constants on the left side have been put before the order id, in
+ // order for these constants to be casted by the driver.
+ var x = db.Orders.Count(o => -idMin + o.OrderId < random.Next(1, 10));
+
+ Assert.That(x, Is.GreaterThan(0).And.LessThan(10));
+ // Next requires support of both floor and rand
+ AssertFunctionInSql(IsFunctionSupported("floor") ? "random" : "floor", spy);
+ });
+ }
+
+ [Test]
+ public void CanSelectRandomIntWithMinMax()
+ {
+ var isSupported = IsFunctionSupported("random") && IsFunctionSupported("floor");
+ RunTest(
+ isSupported,
+ spy =>
+ {
+ var random = new Random();
+ var x =
+ db
+ .Orders.Select(o => new { id = o.OrderId, r = random.Next(1, 11) })
+ .OrderBy(o => o.id).ToList();
+
+ Assert.That(x, Has.Count.GreaterThan(0));
+ var randomValues = x.Select(o => o.r).Distinct().ToArray();
+ Assert.That(randomValues, Has.All.GreaterThanOrEqualTo(1).And.LessThan(11).And.TypeOf());
+
+ if (!LegacyPreEvaluation && IsFunctionSupported("random") && IsFunctionSupported("floor"))
+ {
+ // Naïve randomness check
+ Assert.That(
+ randomValues,
+ Has.Length.GreaterThan(Math.Min(10, x.Count) / 2),
+ "Generated values do not seem very random");
+ }
+
+ // Next requires support of both floor and rand
+ AssertFunctionInSql(IsFunctionSupported("floor") ? "random" : "floor", spy);
+ });
+ }
+
+ private void AssertFunctionInSql(string functionName, SqlLogSpy spy)
+ {
+ if (!IsFunctionSupported(functionName))
+ Assert.Inconclusive($"{functionName} is not supported by the dialect");
+
+ var function = Dialect.Functions[functionName].Render(new List(), Sfi).ToString();
+
+ if (LegacyPreEvaluation)
+ Assert.That(spy.GetWholeLog(), Does.Not.Contain(function));
+ else
+ Assert.That(spy.GetWholeLog(), Does.Contain(function));
+ }
+ }
+}
diff --git a/src/NHibernate.Test/Linq/TryGetMappedTests.cs b/src/NHibernate.Test/Linq/TryGetMappedTests.cs
index 11724e1ac9b..b65aa43f701 100644
--- a/src/NHibernate.Test/Linq/TryGetMappedTests.cs
+++ b/src/NHibernate.Test/Linq/TryGetMappedTests.cs
@@ -773,7 +773,7 @@ private void AssertResult(
expectedComponentType = expectedComponentType ?? (o => o == null);
var expression = query.Expression;
- NhRelinqQueryParser.PreTransform(expression);
+ NhRelinqQueryParser.PreTransform(expression, Sfi);
var constantToParameterMap = ExpressionParameterVisitor.Visit(expression, Sfi);
var queryModel = NhRelinqQueryParser.Parse(expression);
var requiredHqlParameters = new List();
diff --git a/src/NHibernate.Test/TestCase.cs b/src/NHibernate.Test/TestCase.cs
index 0d59b989cf4..600265ade3f 100644
--- a/src/NHibernate.Test/TestCase.cs
+++ b/src/NHibernate.Test/TestCase.cs
@@ -460,24 +460,40 @@ protected DateTime RoundForDialect(DateTime value)
}}
};
+ protected bool IsFunctionSupported(string functionName)
+ {
+ // We could test Sfi.SQLFunctionRegistry.HasFunction(functionName) which has the advantage of
+ // accounting for additional functions added in configuration. But Dialect is normally never
+ // null, while Sfi could be not yet initialized, depending from where this function is called.
+ // Furthermore there are currently no additional functions added in configuration for NHibernate
+ // tests.
+ var dialect = Dialect;
+ if (!dialect.Functions.ContainsKey(functionName))
+ return false;
+
+ return !DialectsNotSupportingStandardFunction.TryGetValue(functionName, out var dialects) ||
+ !dialects.Contains(dialect.GetType());
+ }
+
protected void AssumeFunctionSupported(string functionName)
{
// We could test Sfi.SQLFunctionRegistry.HasFunction(functionName) which has the advantage of
- // accounting for additionnal functions added in configuration. But Dialect is normally never
+ // accounting for additional functions added in configuration. But Dialect is normally never
// null, while Sfi could be not yet initialized, depending from where this function is called.
- // Furtermore there are currently no additionnal functions added in configuration for NHibernate
+ // Furthermore there are currently no additional functions added in configuration for NHibernate
// tests.
+ var dialect = Dialect;
Assume.That(
- Dialect.Functions,
+ dialect.Functions,
Does.ContainKey(functionName),
- $"{Dialect} doesn't support {functionName} function.");
+ $"{dialect} doesn't support {functionName} function.");
if (!DialectsNotSupportingStandardFunction.TryGetValue(functionName, out var dialects))
return;
Assume.That(
dialects,
- Does.Not.Contain(Dialect.GetType()),
- $"{Dialect} doesn't support {functionName} standard function.");
+ Does.Not.Contain(dialect.GetType()),
+ $"{dialect} doesn't support {functionName} standard function.");
}
protected void ClearQueryPlanCache()
diff --git a/src/NHibernate/Cfg/Environment.cs b/src/NHibernate/Cfg/Environment.cs
index 6613fe0fd2e..55a21c43637 100644
--- a/src/NHibernate/Cfg/Environment.cs
+++ b/src/NHibernate/Cfg/Environment.cs
@@ -226,6 +226,48 @@ public static string Version
public const string LinqToHqlGeneratorsRegistry = "linqtohql.generatorsregistry";
+ ///
+ /// Whether to use the legacy pre-evaluation or not in Linq queries. true by default.
+ ///
+ ///
+ ///
+ /// Legacy pre-evaluation is causing special properties or functions like DateTime.Now or
+ /// Guid.NewGuid() to be always evaluated with the .Net runtime and replaced in the query by
+ /// parameter values.
+ ///
+ ///
+ /// The new pre-evaluation allows them to be converted to HQL function calls which will be run on the db
+ /// side. This allows for example to retrieve the server time instead of the client time, or to generate
+ /// UUIDs for each row instead of an unique one for all rows. (This does not happen if the dialect does
+ /// not support the required HQL function.)
+ ///
+ ///
+ /// The new pre-evaluation will likely be enabled by default in the next major version (6.0).
+ ///
+ ///
+ public const string LinqToHqlLegacyPreEvaluation = "linqtohql.legacy_preevaluation";
+
+ ///
+ /// When the new pre-evaluation is enabled, should methods which translation is not supported by the current
+ /// dialect fallback to pre-evaluation? false by default.
+ ///
+ ///
+ ///
+ /// When this fallback option is enabled while legacy pre-evaluation is disabled, properties or functions
+ /// like DateTime.Now or Guid.NewGuid() used in Linq expressions will not fail when the dialect does not
+ /// support them, but will instead be pre-evaluated.
+ ///
+ ///
+ /// When this fallback option is disabled while legacy pre-evaluation is disabled, properties or functions
+ /// like DateTime.Now or Guid.NewGuid() used in Linq expressions will fail when the dialect does not
+ /// support them.
+ ///
+ ///
+ /// This option has no effect if the legacy pre-evaluation is enabled.
+ ///
+ ///
+ public const string LinqToHqlFallbackOnPreEvaluation = "linqtohql.fallback_on_preevaluation";
+
/// Enable ordering of insert statements for the purpose of more efficient batching.
public const string OrderInserts = "order_inserts";
diff --git a/src/NHibernate/Cfg/Settings.cs b/src/NHibernate/Cfg/Settings.cs
index 4d4fc1fa96e..be520295c93 100644
--- a/src/NHibernate/Cfg/Settings.cs
+++ b/src/NHibernate/Cfg/Settings.cs
@@ -143,6 +143,44 @@ public Settings()
///
public ILinqToHqlGeneratorsRegistry LinqToHqlGeneratorsRegistry { get; internal set; }
+ ///
+ /// Whether to use the legacy pre-evaluation or not in Linq queries. true by default.
+ ///
+ ///
+ ///
+ /// Legacy pre-evaluation is causing special properties or functions like DateTime.Now or
+ /// Guid.NewGuid() to be always evaluated with the .Net runtime and replaced in the query by
+ /// parameter values.
+ ///
+ ///
+ /// The new pre-evaluation allows them to be converted to HQL function calls which will be run on the db
+ /// side. This allows for example to retrieve the server time instead of the client time, or to generate
+ /// UUIDs for each row instead of an unique one for all rows.
+ ///
+ ///
+ public bool LinqToHqlLegacyPreEvaluation { get; internal set; }
+
+ ///
+ /// When the new pre-evaluation is enabled, should methods which translation is not supported by the current
+ /// dialect fallback to pre-evaluation? false by default.
+ ///
+ ///
+ ///
+ /// When this fallback option is enabled while legacy pre-evaluation is disabled, properties or functions
+ /// like DateTime.Now or Guid.NewGuid() used in Linq expressions will not fail when the dialect does not
+ /// support them, but will instead be pre-evaluated.
+ ///
+ ///
+ /// When this fallback option is disabled while legacy pre-evaluation is disabled, properties or functions
+ /// like DateTime.Now or Guid.NewGuid() used in Linq expressions will fail when the dialect does not
+ /// support them.
+ ///
+ ///
+ /// This option has no effect if the legacy pre-evaluation is enabled.
+ ///
+ ///
+ public bool LinqToHqlFallbackOnPreEvaluation { get; internal set; }
+
public IQueryModelRewriterFactory QueryModelRewriterFactory { get; internal set; }
#endregion
diff --git a/src/NHibernate/Cfg/SettingsFactory.cs b/src/NHibernate/Cfg/SettingsFactory.cs
index 24c5eebce67..46928b002cf 100644
--- a/src/NHibernate/Cfg/SettingsFactory.cs
+++ b/src/NHibernate/Cfg/SettingsFactory.cs
@@ -54,6 +54,15 @@ public Settings BuildSettings(IDictionary properties)
settings.Dialect = dialect;
settings.LinqToHqlGeneratorsRegistry = LinqToHqlGeneratorsRegistryFactory.CreateGeneratorsRegistry(properties);
+ // 6.0 TODO: default to false instead of true, and adjust documentation in xsd, xml comment on Environment
+ // and Setting properties, and doc\reference.
+ settings.LinqToHqlLegacyPreEvaluation = PropertiesHelper.GetBoolean(
+ Environment.LinqToHqlLegacyPreEvaluation,
+ properties,
+ true);
+ settings.LinqToHqlFallbackOnPreEvaluation = PropertiesHelper.GetBoolean(
+ Environment.LinqToHqlFallbackOnPreEvaluation,
+ properties);
#region SQL Exception converter
diff --git a/src/NHibernate/Dialect/DB2Dialect.cs b/src/NHibernate/Dialect/DB2Dialect.cs
index 3eef1635595..bd24dda28d7 100644
--- a/src/NHibernate/Dialect/DB2Dialect.cs
+++ b/src/NHibernate/Dialect/DB2Dialect.cs
@@ -80,6 +80,7 @@ public DB2Dialect()
RegisterFunction("log10", new StandardSQLFunction("log10", NHibernateUtil.Double));
RegisterFunction("radians", new StandardSQLFunction("radians", NHibernateUtil.Double));
RegisterFunction("rand", new NoArgSQLFunction("rand", NHibernateUtil.Double));
+ RegisterFunction("random", new NoArgSQLFunction("rand", NHibernateUtil.Double));
RegisterFunction("sin", new StandardSQLFunction("sin", NHibernateUtil.Double));
RegisterFunction("soundex", new StandardSQLFunction("soundex", NHibernateUtil.String));
RegisterFunction("sqrt", new StandardSQLFunction("sqrt", NHibernateUtil.Double));
@@ -87,6 +88,8 @@ public DB2Dialect()
RegisterFunction("tan", new StandardSQLFunction("tan", NHibernateUtil.Double));
RegisterFunction("variance", new StandardSQLFunction("variance", NHibernateUtil.Double));
+ RegisterFunction("current_timestamp", new NoArgSQLFunction("current_timestamp", NHibernateUtil.LocalDateTime, false));
+ RegisterFunction("current_date", new NoArgSQLFunction("current_date", NHibernateUtil.LocalDate, false));
RegisterFunction("julian_day", new StandardSQLFunction("julian_day", NHibernateUtil.Int32));
RegisterFunction("microsecond", new StandardSQLFunction("microsecond", NHibernateUtil.Int32));
RegisterFunction("midnight_seconds", new StandardSQLFunction("midnight_seconds", NHibernateUtil.Int32));
@@ -138,8 +141,6 @@ public DB2Dialect()
RegisterFunction("bxor", new Function.BitwiseFunctionOperation("bitxor"));
RegisterFunction("bnot", new Function.BitwiseFunctionOperation("bitnot"));
- RegisterFunction("current_timestamp", new NoArgSQLFunction("current_timestamp", NHibernateUtil.DateTime, false));
-
DefaultProperties[Environment.ConnectionDriver] = "NHibernate.Driver.DB2Driver";
}
diff --git a/src/NHibernate/Dialect/Dialect.cs b/src/NHibernate/Dialect/Dialect.cs
index 423c5082361..af1995dca06 100644
--- a/src/NHibernate/Dialect/Dialect.cs
+++ b/src/NHibernate/Dialect/Dialect.cs
@@ -105,7 +105,7 @@ protected Dialect()
// the syntax of current_timestamp is extracted from H3.2 tests
// - test\hql\ASTParserLoadingTest.java
// - test\org\hibernate\test\hql\HQLTest.java
- RegisterFunction("current_timestamp", new NoArgSQLFunction("current_timestamp", NHibernateUtil.DateTime, true));
+ RegisterFunction("current_timestamp", new NoArgSQLFunction("current_timestamp", NHibernateUtil.LocalDateTime, true));
RegisterFunction("sysdate", new NoArgSQLFunction("sysdate", NHibernateUtil.DateTime, false));
//map second/minute/hour/day/month/year to ANSI extract(), override on subclasses
diff --git a/src/NHibernate/Dialect/FirebirdDialect.cs b/src/NHibernate/Dialect/FirebirdDialect.cs
index af32c3e09d5..ba37c00cfaa 100644
--- a/src/NHibernate/Dialect/FirebirdDialect.cs
+++ b/src/NHibernate/Dialect/FirebirdDialect.cs
@@ -154,7 +154,7 @@ public override SqlString Render(IList args, ISessionFactoryImplementor factory)
[Serializable]
private class CurrentTimeStamp : NoArgSQLFunction
{
- public CurrentTimeStamp() : base("current_timestamp", NHibernateUtil.DateTime, true)
+ public CurrentTimeStamp() : base("current_timestamp", NHibernateUtil.LocalDateTime, true)
{
}
@@ -413,6 +413,7 @@ protected virtual void RegisterFunctions()
private void OverrideStandardHQLFunctions()
{
RegisterFunction("current_timestamp", new CurrentTimeStamp());
+ RegisterFunction("current_date", new NoArgSQLFunction("current_date", NHibernateUtil.LocalDate, false));
RegisterFunction("length", new StandardSafeSQLFunction("char_length", NHibernateUtil.Int64, 1));
RegisterFunction("nullif", new StandardSafeSQLFunction("nullif", 2));
RegisterFunction("lower", new StandardSafeSQLFunction("lower", NHibernateUtil.String, 1));
@@ -422,6 +423,7 @@ private void OverrideStandardHQLFunctions()
RegisterFunction("strguid", new StandardSQLFunction("uuid_to_char", NHibernateUtil.String));
RegisterFunction("sysdate", new CastedFunction("today", NHibernateUtil.Date));
RegisterFunction("date", new SQLFunctionTemplate(NHibernateUtil.Date, "cast(?1 as date)"));
+ RegisterFunction("new_uuid", new NoArgSQLFunction("gen_uuid", NHibernateUtil.Guid));
// Bitwise operations
RegisterFunction("band", new Function.BitwiseFunctionOperation("bin_and"));
RegisterFunction("bor", new Function.BitwiseFunctionOperation("bin_or"));
@@ -463,6 +465,7 @@ private void RegisterMathematicalFunctions()
RegisterFunction("log10", new StandardSQLFunction("log10", NHibernateUtil.Double));
RegisterFunction("pi", new NoArgSQLFunction("pi", NHibernateUtil.Double));
RegisterFunction("rand", new NoArgSQLFunction("rand", NHibernateUtil.Double));
+ RegisterFunction("random", new NoArgSQLFunction("rand", NHibernateUtil.Double));
RegisterFunction("sign", new StandardSQLFunction("sign", NHibernateUtil.Int32));
RegisterFunction("sqtr", new StandardSQLFunction("sqtr", NHibernateUtil.Double));
RegisterFunction("trunc", new StandardSQLFunction("trunc"));
diff --git a/src/NHibernate/Dialect/HanaDialectBase.cs b/src/NHibernate/Dialect/HanaDialectBase.cs
index b8de4f2a5d7..4a8d70db2e8 100644
--- a/src/NHibernate/Dialect/HanaDialectBase.cs
+++ b/src/NHibernate/Dialect/HanaDialectBase.cs
@@ -395,6 +395,8 @@ protected virtual void RegisterNHibernateFunctions()
RegisterFunction("iif", new SQLFunctionTemplate(null, "case when ?1 then ?2 else ?3 end"));
RegisterFunction("sysdate", new NoArgSQLFunction("current_timestamp", NHibernateUtil.DateTime, false));
RegisterFunction("truncate", new SQLFunctionTemplateWithRequiredParameters(null, "floor(?1 * power(10, ?2)) / power(10, ?2)", new object[] { null, "0" }));
+ RegisterFunction("new_uuid", new NoArgSQLFunction("sysuuid", NHibernateUtil.Guid, false));
+ RegisterFunction("random", new NoArgSQLFunction("rand", NHibernateUtil.Double));
}
protected virtual void RegisterHANAFunctions()
@@ -439,20 +441,20 @@ protected virtual void RegisterHANAFunctions()
RegisterFunction("cosh", new StandardSQLFunction("cosh", NHibernateUtil.Double));
RegisterFunction("cot", new StandardSQLFunction("cot", NHibernateUtil.Double));
RegisterFunction("current_connection", new NoArgSQLFunction("current_connection", NHibernateUtil.Int32));
- RegisterFunction("current_date", new NoArgSQLFunction("current_date", NHibernateUtil.DateTime, false));
+ RegisterFunction("current_date", new NoArgSQLFunction("current_date", NHibernateUtil.LocalDate, false));
RegisterFunction("current_identity_value", new NoArgSQLFunction("current_identity_value", NHibernateUtil.Int64));
RegisterFunction("current_mvcc_snapshot_timestamp", new NoArgSQLFunction("current_mvcc_snapshot_timestamp", NHibernateUtil.Int32));
RegisterFunction("current_object_schema", new NoArgSQLFunction("current_object_schema", NHibernateUtil.String));
RegisterFunction("current_schema", new NoArgSQLFunction("current_schema", NHibernateUtil.String, false));
RegisterFunction("current_time", new NoArgSQLFunction("current_time", NHibernateUtil.DateTime, false));
- RegisterFunction("current_timestamp", new NoArgSQLFunction("current_timestamp", NHibernateUtil.DateTime, false));
+ RegisterFunction("current_timestamp", new NoArgSQLFunction("current_timestamp", NHibernateUtil.LocalDateTime, false));
RegisterFunction("current_transaction_isolation_level", new NoArgSQLFunction("current_transaction_isolation_level", NHibernateUtil.String, false));
RegisterFunction("current_update_statement_sequence", new NoArgSQLFunction("current_update_statement_sequence", NHibernateUtil.Int64));
RegisterFunction("current_update_transaction", new NoArgSQLFunction("current_update_transaction", NHibernateUtil.Int64));
RegisterFunction("current_user", new NoArgSQLFunction("current_user", NHibernateUtil.String, false));
RegisterFunction("current_utcdate", new NoArgSQLFunction("current_utcdate", NHibernateUtil.DateTime, false));
RegisterFunction("current_utctime", new NoArgSQLFunction("current_utctime", NHibernateUtil.DateTime, false));
- RegisterFunction("current_utctimestamp", new NoArgSQLFunction("current_utctimestamp", NHibernateUtil.DateTime, false));
+ RegisterFunction("current_utctimestamp", new NoArgSQLFunction("current_utctimestamp", NHibernateUtil.UtcDateTime, false));
RegisterFunction("dayname", new StandardSQLFunction("dayname", NHibernateUtil.String));
RegisterFunction("dayofmonth", new StandardSQLFunction("dayofmonth", NHibernateUtil.Int32));
RegisterFunction("dayofyear", new StandardSQLFunction("dayofyear", NHibernateUtil.Int32));
diff --git a/src/NHibernate/Dialect/InformixDialect.cs b/src/NHibernate/Dialect/InformixDialect.cs
index 7739018dc1e..2f942815a57 100644
--- a/src/NHibernate/Dialect/InformixDialect.cs
+++ b/src/NHibernate/Dialect/InformixDialect.cs
@@ -78,7 +78,8 @@ public InformixDialect()
// RegisterFunction("cast", new CastFunction());
// RegisterFunction("concat", new VarArgsSQLFunction(NHibernateUtil.String, "(", "||", ")"));
- RegisterFunction("current_timestamp", new NoArgSQLFunction("current", NHibernateUtil.DateTime, false));
+ RegisterFunction("current_timestamp", new NoArgSQLFunction("current", NHibernateUtil.LocalDateTime, false));
+ RegisterFunction("current_date", new NoArgSQLFunction("today", NHibernateUtil.LocalDate, false));
RegisterFunction("sysdate", new NoArgSQLFunction("today", NHibernateUtil.DateTime, false));
RegisterFunction("current", new NoArgSQLFunction("current", NHibernateUtil.DateTime, false));
RegisterFunction("today", new NoArgSQLFunction("today", NHibernateUtil.DateTime, false));
diff --git a/src/NHibernate/Dialect/MsSql2000Dialect.cs b/src/NHibernate/Dialect/MsSql2000Dialect.cs
index 5cada797985..7acb3cb9b4f 100644
--- a/src/NHibernate/Dialect/MsSql2000Dialect.cs
+++ b/src/NHibernate/Dialect/MsSql2000Dialect.cs
@@ -315,6 +315,8 @@ protected virtual void RegisterFunctions()
RegisterFunction("mod", new SQLFunctionTemplate(NHibernateUtil.Int32, "((?1) % (?2))"));
RegisterFunction("radians", new StandardSQLFunction("radians", NHibernateUtil.Double));
RegisterFunction("rand", new NoArgSQLFunction("rand", NHibernateUtil.Double));
+ // SQL Server rand returns the same value for each row, unless hacking it with a random seed per row
+ RegisterFunction("random", new SQLFunctionTemplate(NHibernateUtil.Double, "rand(checksum(newid()))"));
RegisterFunction("sin", new StandardSQLFunction("sin", NHibernateUtil.Double));
RegisterFunction("soundex", new StandardSQLFunction("soundex", NHibernateUtil.String));
RegisterFunction("sqrt", new StandardSQLFunction("sqrt", NHibernateUtil.Double));
@@ -326,7 +328,9 @@ protected virtual void RegisterFunctions()
RegisterFunction("right", new SQLFunctionTemplate(NHibernateUtil.String, "right(?1, ?2)"));
RegisterFunction("locate", new StandardSQLFunction("charindex", NHibernateUtil.Int32));
- RegisterFunction("current_timestamp", new NoArgSQLFunction("getdate", NHibernateUtil.DateTime, true));
+ RegisterFunction("current_timestamp", new NoArgSQLFunction("getdate", NHibernateUtil.LocalDateTime, true));
+ RegisterFunction("current_date", new SQLFunctionTemplate(NHibernateUtil.LocalDate, "dateadd(dd, 0, datediff(dd, 0, getdate()))"));
+ RegisterFunction("current_utctimestamp", new NoArgSQLFunction("getutcdate", NHibernateUtil.UtcDateTime, true));
RegisterFunction("second", new SQLFunctionTemplate(NHibernateUtil.Int32, "datepart(second, ?1)"));
RegisterFunction("minute", new SQLFunctionTemplate(NHibernateUtil.Int32, "datepart(minute, ?1)"));
RegisterFunction("hour", new SQLFunctionTemplate(NHibernateUtil.Int32, "datepart(hour, ?1)"));
@@ -358,6 +362,8 @@ protected virtual void RegisterFunctions()
RegisterFunction("bit_length", new SQLFunctionTemplate(NHibernateUtil.Int32, "datalength(?1) * 8"));
RegisterFunction("extract", new SQLFunctionTemplate(NHibernateUtil.Int32, "datepart(?1, ?3)"));
+
+ RegisterFunction("new_uuid", new NoArgSQLFunction("newid", NHibernateUtil.Guid));
}
protected virtual void RegisterGuidTypeMapping()
diff --git a/src/NHibernate/Dialect/MsSql2008Dialect.cs b/src/NHibernate/Dialect/MsSql2008Dialect.cs
index 7c40549a700..d0ef5580389 100644
--- a/src/NHibernate/Dialect/MsSql2008Dialect.cs
+++ b/src/NHibernate/Dialect/MsSql2008Dialect.cs
@@ -51,11 +51,20 @@ protected override void RegisterFunctions()
{
RegisterFunction(
"current_timestamp",
- new NoArgSQLFunction("sysdatetime", NHibernateUtil.DateTime, true));
+ new NoArgSQLFunction("sysdatetime", NHibernateUtil.LocalDateTime, true));
+ RegisterFunction(
+ "current_utctimestamp",
+ new NoArgSQLFunction("sysutcdatetime", NHibernateUtil.UtcDateTime, true));
}
+
+ RegisterFunction("current_date", new SQLFunctionTemplate(NHibernateUtil.LocalDate, "cast(getdate() as date)"));
RegisterFunction(
"current_timestamp_offset",
new NoArgSQLFunction("sysdatetimeoffset", NHibernateUtil.DateTimeOffset, true));
+ RegisterFunction(
+ "current_utctimestamp_offset",
+ new SQLFunctionTemplate(NHibernateUtil.DateTimeOffset, "todatetimeoffset(sysutcdatetime(), 0)"));
+ RegisterFunction("date", new SQLFunctionTemplate(NHibernateUtil.Date, "cast(?1 as date)"));
}
protected override void RegisterKeywords()
diff --git a/src/NHibernate/Dialect/MsSqlCeDialect.cs b/src/NHibernate/Dialect/MsSqlCeDialect.cs
index 90da41cf9bb..fd0baed402b 100644
--- a/src/NHibernate/Dialect/MsSqlCeDialect.cs
+++ b/src/NHibernate/Dialect/MsSqlCeDialect.cs
@@ -172,7 +172,8 @@ protected virtual void RegisterFunctions()
RegisterFunction("str", new SQLFunctionTemplate(NHibernateUtil.String, "cast(?1 as nvarchar)"));
RegisterFunction("strguid", new SQLFunctionTemplate(NHibernateUtil.String, "cast(?1 as nvarchar)"));
- RegisterFunction("current_timestamp", new NoArgSQLFunction("getdate", NHibernateUtil.DateTime, true));
+ RegisterFunction("current_timestamp", new NoArgSQLFunction("getdate", NHibernateUtil.LocalDateTime, true));
+ RegisterFunction("current_date", new SQLFunctionTemplate(NHibernateUtil.LocalDate, "dateadd(dd, 0, datediff(dd, 0, getdate()))"));
RegisterFunction("date", new SQLFunctionTemplate(NHibernateUtil.DateTime, "dateadd(dd, 0, datediff(dd, 0, ?1))"));
RegisterFunction("second", new SQLFunctionTemplate(NHibernateUtil.Int32, "datepart(second, ?1)"));
RegisterFunction("minute", new SQLFunctionTemplate(NHibernateUtil.Int32, "datepart(minute, ?1)"));
@@ -200,6 +201,8 @@ protected virtual void RegisterFunctions()
RegisterFunction("bit_length", new SQLFunctionTemplate(NHibernateUtil.Int32, "datalength(?1) * 8"));
RegisterFunction("extract", new SQLFunctionTemplate(NHibernateUtil.Int32, "datepart(?1, ?3)"));
+
+ RegisterFunction("new_uuid", new NoArgSQLFunction("newid", NHibernateUtil.Guid));
}
protected virtual void RegisterDefaultProperties()
diff --git a/src/NHibernate/Dialect/MySQL55Dialect.cs b/src/NHibernate/Dialect/MySQL55Dialect.cs
index c7a8004cb1d..26dd7de2709 100644
--- a/src/NHibernate/Dialect/MySQL55Dialect.cs
+++ b/src/NHibernate/Dialect/MySQL55Dialect.cs
@@ -10,5 +10,12 @@ public MySQL55Dialect()
RegisterColumnType(DbType.Guid, "CHAR(36)");
RegisterFunction("strguid", new SQLFunctionTemplate(NHibernateUtil.String, "?1"));
}
+
+ protected override void RegisterFunctions()
+ {
+ base.RegisterFunctions();
+
+ RegisterFunction("current_utctimestamp", new NoArgSQLFunction("UTC_TIMESTAMP", NHibernateUtil.UtcDateTime, true));
+ }
}
}
diff --git a/src/NHibernate/Dialect/MySQL5Dialect.cs b/src/NHibernate/Dialect/MySQL5Dialect.cs
index 1dfac2f6f46..0797206b02a 100644
--- a/src/NHibernate/Dialect/MySQL5Dialect.cs
+++ b/src/NHibernate/Dialect/MySQL5Dialect.cs
@@ -12,8 +12,14 @@ public MySQL5Dialect()
// My SQL supports precision up to 65, but .Net is limited to 28-29.
RegisterColumnType(DbType.Decimal, 29, "DECIMAL($p, $s)");
RegisterColumnType(DbType.Guid, "BINARY(16)");
+ }
+
+ protected override void RegisterFunctions()
+ {
+ base.RegisterFunctions();
RegisterFunction("strguid", new SQLFunctionTemplate(NHibernateUtil.String, "concat(hex(reverse(substr(?1, 1, 4))), '-', hex(reverse(substring(?1, 5, 2))), '-', hex(reverse(substr(?1, 7, 2))), '-', hex(substr(?1, 9, 2)), '-', hex(substr(?1, 11)))"));
+ RegisterFunction("new_uuid", new NoArgSQLFunction("uuid", NHibernateUtil.Guid));
}
protected override void RegisterCastTypes()
diff --git a/src/NHibernate/Dialect/MySQLDialect.cs b/src/NHibernate/Dialect/MySQLDialect.cs
index 9a83de26eb3..a6caa6d236a 100644
--- a/src/NHibernate/Dialect/MySQLDialect.cs
+++ b/src/NHibernate/Dialect/MySQLDialect.cs
@@ -265,7 +265,8 @@ protected virtual void RegisterFunctions()
RegisterFunction("truncate", new StandardSQLFunctionWithRequiredParameters("truncate", new object[] {null, "0"}));
RegisterFunction("rand", new NoArgSQLFunction("rand", NHibernateUtil.Double));
-
+ RegisterFunction("random", new NoArgSQLFunction("rand", NHibernateUtil.Double));
+
RegisterFunction("power", new StandardSQLFunction("power", NHibernateUtil.Double));
RegisterFunction("stddev", new StandardSQLFunction("stddev", NHibernateUtil.Double));
@@ -294,7 +295,7 @@ protected virtual void RegisterFunctions()
RegisterFunction("hex", new StandardSQLFunction("hex", NHibernateUtil.String));
RegisterFunction("soundex", new StandardSQLFunction("soundex", NHibernateUtil.String));
- RegisterFunction("current_date", new NoArgSQLFunction("current_date", NHibernateUtil.Date, false));
+ RegisterFunction("current_date", new NoArgSQLFunction("current_date", NHibernateUtil.LocalDate, false));
RegisterFunction("current_time", new NoArgSQLFunction("current_time", NHibernateUtil.Time, false));
RegisterFunction("second", new StandardSQLFunction("second", NHibernateUtil.Int32));
diff --git a/src/NHibernate/Dialect/Oracle10gDialect.cs b/src/NHibernate/Dialect/Oracle10gDialect.cs
index caab3e1f492..1ad7f135b44 100644
--- a/src/NHibernate/Dialect/Oracle10gDialect.cs
+++ b/src/NHibernate/Dialect/Oracle10gDialect.cs
@@ -1,3 +1,4 @@
+using NHibernate.Dialect.Function;
using NHibernate.SqlCommand;
namespace NHibernate.Dialect
@@ -16,7 +17,32 @@ public override JoinFragment CreateOuterJoinFragment()
return new ANSIJoinFragment();
}
+ protected override void RegisterFunctions()
+ {
+ base.RegisterFunctions();
+
+ // DBMS_RANDOM package was available in previous versions, but it was requiring initialization and
+ // was not having the value function.
+ // It yields a decimal between 0 included and 1 excluded, with 38 significant digits. It sometimes
+ // causes an overflow when read by the Oracle provider as a .Net Decimal, so better explicitly cast
+ // it to double.
+ RegisterFunction("random", new SQLFunctionTemplate(NHibernateUtil.Double, "cast(DBMS_RANDOM.VALUE() as binary_double)"));
+ }
+
+ /* 6.0 TODO: consider redefining float and double registrations
+ protected override void RegisterNumericTypeMappings()
+ {
+ base.RegisterNumericTypeMappings();
+
+ // Use binary_float (available since 10g) instead of float. With Oracle, float is a decimal but
+ // with a precision expressed in number of bytes instead of digits.
+ RegisterColumnType(DbType.Single, "binary_float");
+ // Using binary_double (available since 10g) instead of double precision. With Oracle, double
+ // precision is a float(126), which is a decimal with a 126 bytes precision.
+ RegisterColumnType(DbType.Double, "binary_double");
+ }*/
+
///
public override bool SupportsCrossJoin => true;
}
-}
\ No newline at end of file
+}
diff --git a/src/NHibernate/Dialect/Oracle8iDialect.cs b/src/NHibernate/Dialect/Oracle8iDialect.cs
index b2103b87965..749c1f0d056 100644
--- a/src/NHibernate/Dialect/Oracle8iDialect.cs
+++ b/src/NHibernate/Dialect/Oracle8iDialect.cs
@@ -252,7 +252,7 @@ protected virtual void RegisterFunctions()
// In Oracle, date includes a time, just with fractional seconds dropped. For actually only having
// the date, it must be truncated. Otherwise comparisons may yield unexpected results.
- RegisterFunction("current_date", new SQLFunctionTemplate(NHibernateUtil.Date, "trunc(current_date)"));
+ RegisterFunction("current_date", new SQLFunctionTemplate(NHibernateUtil.LocalDate, "trunc(current_date)"));
RegisterFunction("current_time", new NoArgSQLFunction("current_timestamp", NHibernateUtil.Time, false));
RegisterFunction("current_timestamp", new CurrentTimeStamp());
@@ -310,6 +310,8 @@ protected virtual void RegisterFunctions()
RegisterFunction("bor", new SQLFunctionTemplate(null, "?1 + ?2 - BITAND(?1, ?2)"));
RegisterFunction("bxor", new SQLFunctionTemplate(null, "?1 + ?2 - BITAND(?1, ?2) * 2"));
RegisterFunction("bnot", new SQLFunctionTemplate(null, "(-1 - ?1)"));
+
+ RegisterFunction("new_uuid", new NoArgSQLFunction("sys_guid", NHibernateUtil.Guid));
}
protected internal virtual void RegisterDefaultProperties()
@@ -571,7 +573,7 @@ public override bool SupportsExistsInSelect
[Serializable]
private class CurrentTimeStamp : NoArgSQLFunction
{
- public CurrentTimeStamp() : base("current_timestamp", NHibernateUtil.DateTime, true) {}
+ public CurrentTimeStamp() : base("current_timestamp", NHibernateUtil.LocalDateTime, true) {}
public override SqlString Render(IList args, ISessionFactoryImplementor factory)
{
diff --git a/src/NHibernate/Dialect/Oracle9iDialect.cs b/src/NHibernate/Dialect/Oracle9iDialect.cs
index 868b8170ccd..b36b1a34b37 100644
--- a/src/NHibernate/Dialect/Oracle9iDialect.cs
+++ b/src/NHibernate/Dialect/Oracle9iDialect.cs
@@ -1,4 +1,5 @@
using System.Data;
+using NHibernate.Dialect.Function;
using NHibernate.SqlCommand;
using NHibernate.SqlTypes;
@@ -41,6 +42,15 @@ protected override void RegisterDateTimeTypeMappings()
RegisterColumnType(DbType.Xml, "XMLTYPE");
}
+ protected override void RegisterFunctions()
+ {
+ base.RegisterFunctions();
+
+ RegisterFunction(
+ "current_utctimestamp",
+ new SQLFunctionTemplate(NHibernateUtil.UtcDateTime, "SYS_EXTRACT_UTC(current_timestamp)"));
+ }
+
public override long TimestampResolutionInTicks => 1;
public override string GetSelectClauseNullString(SqlType sqlType)
diff --git a/src/NHibernate/Dialect/PostgreSQL81Dialect.cs b/src/NHibernate/Dialect/PostgreSQL81Dialect.cs
index 77953561029..b59ca08388c 100644
--- a/src/NHibernate/Dialect/PostgreSQL81Dialect.cs
+++ b/src/NHibernate/Dialect/PostgreSQL81Dialect.cs
@@ -1,4 +1,5 @@
using System.Data;
+using NHibernate.Dialect.Function;
using NHibernate.SqlCommand;
namespace NHibernate.Dialect
@@ -40,6 +41,11 @@ protected override void RegisterDateTimeTypeMappings()
RegisterColumnType(DbType.Time, 6, "time($s)");
// Not overriding default scale: Posgres doc writes it means "no explicit limit", so max of what it can support,
// which suits our needs.
+
+ // timezone seems not available prior to version 8.0
+ RegisterFunction(
+ "current_utctimestamp",
+ new SQLFunctionTemplate(NHibernateUtil.UtcDateTime, "timezone('UTC', current_timestamp)"));
}
public override string ForUpdateNowaitString
diff --git a/src/NHibernate/Dialect/PostgreSQLDialect.cs b/src/NHibernate/Dialect/PostgreSQLDialect.cs
index b81c2df86a8..baa9334d788 100644
--- a/src/NHibernate/Dialect/PostgreSQLDialect.cs
+++ b/src/NHibernate/Dialect/PostgreSQLDialect.cs
@@ -60,7 +60,7 @@ public PostgreSQLDialect()
RegisterColumnType(DbType.String, 1073741823, "text");
// Override standard HQL function
- RegisterFunction("current_timestamp", new NoArgSQLFunction("now", NHibernateUtil.DateTime, true));
+ RegisterFunction("current_timestamp", new NoArgSQLFunction("now", NHibernateUtil.LocalDateTime, true));
RegisterFunction("str", new SQLFunctionTemplate(NHibernateUtil.String, "cast(?1 as varchar)"));
RegisterFunction("locate", new PositionSubstringFunction());
RegisterFunction("iif", new SQLFunctionTemplate(null, "case when ?1 then ?2 else ?3 end"));
@@ -94,9 +94,14 @@ public PostgreSQLDialect()
// Register the date function, since when used in LINQ select clauses, NH must know the data type.
RegisterFunction("date", new SQLFunctionTemplate(NHibernateUtil.Date, "cast(?1 as date)"));
+ RegisterFunction("current_date", new NoArgSQLFunction("current_date", NHibernateUtil.LocalDate, false));
RegisterFunction("strguid", new SQLFunctionTemplate(NHibernateUtil.String, "?1::TEXT"));
+ // The uuid_generate_v4 is not native and must be installed, but SelectGUIDString property already uses it,
+ // and NHibernate.TestDatabaseSetup does install it.
+ RegisterFunction("new_uuid", new NoArgSQLFunction("uuid_generate_v4", NHibernateUtil.Guid));
+
RegisterKeywords();
}
diff --git a/src/NHibernate/Dialect/SQLiteDialect.cs b/src/NHibernate/Dialect/SQLiteDialect.cs
index f1f86743313..864defac9a3 100644
--- a/src/NHibernate/Dialect/SQLiteDialect.cs
+++ b/src/NHibernate/Dialect/SQLiteDialect.cs
@@ -61,6 +61,8 @@ protected virtual void RegisterColumnTypes()
RegisterColumnType(DbType.DateTime, "DATETIME");
RegisterColumnType(DbType.Time, "TIME");
RegisterColumnType(DbType.Boolean, "BOOL");
+ // UNIQUEIDENTIFIER is not a SQLite type, but SQLite does not care much, see
+ // https://www.sqlite.org/datatype3.html
RegisterColumnType(DbType.Guid, "UNIQUEIDENTIFIER");
}
@@ -74,12 +76,17 @@ protected virtual void RegisterFunctions()
RegisterFunction("month", new SQLFunctionTemplate(NHibernateUtil.Int32, "cast(strftime('%m', ?1) as int)"));
RegisterFunction("year", new SQLFunctionTemplate(NHibernateUtil.Int32, "cast(strftime('%Y', ?1) as int)"));
// Uses local time like MSSQL and PostgreSQL.
- RegisterFunction("current_timestamp", new SQLFunctionTemplate(NHibernateUtil.DateTime, "datetime(current_timestamp, 'localtime')"));
+ RegisterFunction("current_timestamp", new SQLFunctionTemplate(NHibernateUtil.LocalDateTime, "datetime(current_timestamp, 'localtime')"));
+ RegisterFunction("current_utctimestamp", new SQLFunctionTemplate(NHibernateUtil.UtcDateTime, "datetime(current_timestamp)"));
// The System.Data.SQLite driver stores both Date and DateTime as 'YYYY-MM-DD HH:MM:SS'
// The SQLite date() function returns YYYY-MM-DD, which unfortunately SQLite does not consider
// as equal to 'YYYY-MM-DD 00:00:00'. Because of this, it is best to return the
// 'YYYY-MM-DD 00:00:00' format for the date function.
RegisterFunction("date", new SQLFunctionTemplate(NHibernateUtil.Date, "datetime(date(?1))"));
+ // SQLite has current_date, but as current_timestamp, it is in UTC. So converting the timestamp to
+ // localtime then to date then, like the above date function, go back to datetime format for comparisons
+ // sake.
+ RegisterFunction("current_date", new SQLFunctionTemplate(NHibernateUtil.LocalDate, "datetime(date(current_timestamp, 'localtime'))"));
RegisterFunction("substring", new StandardSQLFunction("substr", NHibernateUtil.String));
RegisterFunction("left", new SQLFunctionTemplate(NHibernateUtil.String, "substr(?1,1,?2)"));
@@ -106,6 +113,16 @@ protected virtual void RegisterFunctions()
RegisterFunction("strguid", new SQLFunctionTemplate(NHibernateUtil.String, "substr(hex(?1), 7, 2) || substr(hex(?1), 5, 2) || substr(hex(?1), 3, 2) || substr(hex(?1), 1, 2) || '-' || substr(hex(?1), 11, 2) || substr(hex(?1), 9, 2) || '-' || substr(hex(?1), 15, 2) || substr(hex(?1), 13, 2) || '-' || substr(hex(?1), 17, 4) || '-' || substr(hex(?1), 21) "));
else
RegisterFunction("strguid", new SQLFunctionTemplate(NHibernateUtil.String, "cast(?1 as char)"));
+
+ // SQLite random function yields a long, ranging form MinValue to MaxValue. (-9223372036854775808 to
+ // 9223372036854775807). HQL random requires a float from 0 inclusive to 1 exclusive, so we divide by
+ // 9223372036854775808 then 2 for having a value between -0.5 included to 0.5 excluded, and finally
+ // add 0.5. The division is written as "/ 4611686018427387904 / 4" for avoiding overflowing long.
+ RegisterFunction(
+ "random",
+ new SQLFunctionTemplate(
+ NHibernateUtil.Double,
+ "(cast(random() as real) / 4611686018427387904 / 4 + 0.5)"));
}
public override void Configure(IDictionary settings)
diff --git a/src/NHibernate/Dialect/SybaseASA9Dialect.cs b/src/NHibernate/Dialect/SybaseASA9Dialect.cs
index f839871b6a1..dfae7baa471 100644
--- a/src/NHibernate/Dialect/SybaseASA9Dialect.cs
+++ b/src/NHibernate/Dialect/SybaseASA9Dialect.cs
@@ -72,7 +72,7 @@ public SybaseASA9Dialect()
//RegisterColumnType(DbType.Xml, "TEXT");
// Override standard HQL function
- RegisterFunction("current_timestamp", new StandardSQLFunction("current_timestamp"));
+ RegisterFunction("current_timestamp", new StandardSQLFunction("current_timestamp", NHibernateUtil.LocalDateTime));
RegisterFunction("length", new StandardSafeSQLFunction("length", NHibernateUtil.String, 1));
RegisterFunction("nullif", new StandardSafeSQLFunction("nullif", 2));
RegisterFunction("lower", new StandardSafeSQLFunction("lower", NHibernateUtil.String, 1));
diff --git a/src/NHibernate/Dialect/SybaseASE15Dialect.cs b/src/NHibernate/Dialect/SybaseASE15Dialect.cs
index 0a84a822424..a5233395d36 100644
--- a/src/NHibernate/Dialect/SybaseASE15Dialect.cs
+++ b/src/NHibernate/Dialect/SybaseASE15Dialect.cs
@@ -56,6 +56,9 @@ public SybaseASE15Dialect()
RegisterColumnType(DbType.Date, "date");
RegisterColumnType(DbType.Binary, 8000, "varbinary($l)");
RegisterColumnType(DbType.Binary, "varbinary");
+ // newid default is to generate a 32 bytes character uuid (no-dashes), but it has an option for
+ // including dashes, then raising it to 36 bytes.
+ RegisterColumnType(DbType.Guid, "varchar(36)");
RegisterFunction("abs", new StandardSQLFunction("abs"));
RegisterFunction("acos", new StandardSQLFunction("acos", NHibernateUtil.Double));
@@ -68,9 +71,10 @@ public SybaseASE15Dialect()
RegisterFunction("concat", new VarArgsSQLFunction(NHibernateUtil.String, "(","+",")"));
RegisterFunction("cos", new StandardSQLFunction("cos", NHibernateUtil.Double));
RegisterFunction("cot", new StandardSQLFunction("cot", NHibernateUtil.Double));
- RegisterFunction("current_date", new NoArgSQLFunction("current_date", NHibernateUtil.Date));
+ RegisterFunction("current_date", new NoArgSQLFunction("current_date", NHibernateUtil.LocalDate));
RegisterFunction("current_time", new NoArgSQLFunction("current_time", NHibernateUtil.Time));
- RegisterFunction("current_timestamp", new NoArgSQLFunction("getdate", NHibernateUtil.DateTime));
+ RegisterFunction("current_timestamp", new NoArgSQLFunction("getdate", NHibernateUtil.LocalDateTime));
+ RegisterFunction("current_utctimestamp", new NoArgSQLFunction("getutcdate", NHibernateUtil.UtcDateTime));
RegisterFunction("datename", new StandardSQLFunction("datename", NHibernateUtil.String));
RegisterFunction("day", new StandardSQLFunction("day", NHibernateUtil.Int32));
RegisterFunction("degrees", new StandardSQLFunction("degrees", NHibernateUtil.Double));
@@ -94,6 +98,8 @@ public SybaseASE15Dialect()
RegisterFunction("pi", new NoArgSQLFunction("pi", NHibernateUtil.Double));
RegisterFunction("radians", new StandardSQLFunction("radians", NHibernateUtil.Double));
RegisterFunction("rand", new StandardSQLFunction("rand", NHibernateUtil.Double));
+ // rand returns the same value for each row, rand2 returns a new one for each row.
+ RegisterFunction("random", new StandardSQLFunction("rand2", NHibernateUtil.Double));
RegisterFunction("reverse", new StandardSQLFunction("reverse"));
RegisterFunction("round", new StandardSQLFunction("round"));
RegisterFunction("rtrim", new StandardSQLFunction("rtrim"));
@@ -112,6 +118,8 @@ public SybaseASE15Dialect()
RegisterFunction("year", new StandardSQLFunction("year", NHibernateUtil.Int32));
RegisterFunction("substring", new EmulatedLengthSubstringFunction());
+
+ RegisterFunction("new_uuid", new NoArgSQLFunction("newid", NHibernateUtil.Guid));
}
public override string AddColumnString
diff --git a/src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs b/src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs
index 388947b45c4..f5b61250049 100644
--- a/src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs
+++ b/src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs
@@ -142,6 +142,7 @@ protected virtual void RegisterMathFunctions()
RegisterFunction("power", new StandardSQLFunction("power", NHibernateUtil.Double));
RegisterFunction("radians", new StandardSQLFunction("radians", NHibernateUtil.Double));
RegisterFunction("rand", new StandardSQLFunction("rand", NHibernateUtil.Double));
+ RegisterFunction("random", new StandardSQLFunction("rand", NHibernateUtil.Double));
RegisterFunction("remainder", new StandardSQLFunction("remainder"));
RegisterFunction("round", new StandardSQLFunctionWithRequiredParameters("round", new object[] {null, "0"}));
RegisterFunction("sign", new StandardSQLFunction("sign", NHibernateUtil.Int32));
@@ -238,9 +239,9 @@ protected virtual void RegisterDateFunctions()
RegisterFunction("ymd", new StandardSQLFunction("ymd", NHibernateUtil.Date));
// compatibility functions
- RegisterFunction("current_timestamp", new NoArgSQLFunction("getdate", NHibernateUtil.DateTime, true));
+ RegisterFunction("current_timestamp", new NoArgSQLFunction("getdate", NHibernateUtil.LocalDateTime, true));
RegisterFunction("current_time", new NoArgSQLFunction("getdate", NHibernateUtil.Time, true));
- RegisterFunction("current_date", new SQLFunctionTemplate(NHibernateUtil.Date, "date(getdate())"));
+ RegisterFunction("current_date", new SQLFunctionTemplate(NHibernateUtil.LocalDate, "date(getdate())"));
}
protected virtual void RegisterStringFunctions()
@@ -338,6 +339,7 @@ protected virtual void RegisterMiscellaneousFunctions()
RegisterFunction("isnull", new VarArgsSQLFunction("isnull(", ",", ")"));
RegisterFunction("lesser", new StandardSQLFunction("lesser"));
RegisterFunction("newid", new NoArgSQLFunction("newid", NHibernateUtil.String, true));
+ RegisterFunction("new_uuid", new NoArgSQLFunction("newid", NHibernateUtil.Guid));
RegisterFunction("nullif", new StandardSQLFunction("nullif"));
RegisterFunction("number", new NoArgSQLFunction("number", NHibernateUtil.Int32));
RegisterFunction("plan", new VarArgsSQLFunction(NHibernateUtil.String, "plan(", ",", ")"));
diff --git a/src/NHibernate/Dialect/SybaseSQLAnywhere12Dialect.cs b/src/NHibernate/Dialect/SybaseSQLAnywhere12Dialect.cs
index a0b0125e8b7..88a846ba1de 100644
--- a/src/NHibernate/Dialect/SybaseSQLAnywhere12Dialect.cs
+++ b/src/NHibernate/Dialect/SybaseSQLAnywhere12Dialect.cs
@@ -77,9 +77,16 @@ protected override void RegisterDateTimeTypeMappings()
protected override void RegisterDateFunctions()
{
base.RegisterDateFunctions();
+
+ RegisterFunction(
+ "current_utctimestamp",
+ new SQLFunctionTemplate(NHibernateUtil.UtcDateTime, "cast(current UTC timestamp as timestamp)"));
RegisterFunction(
"current_timestamp_offset",
new NoArgSQLFunction("sysdatetimeoffset", NHibernateUtil.DateTimeOffset, true));
+ RegisterFunction(
+ "current_utctimestamp_offset",
+ new SQLFunctionTemplate(NHibernateUtil.DateTimeOffset, "(current UTC timestamp)"));
}
///
diff --git a/src/NHibernate/Linq/Functions/DateTimeNowHqlGenerator.cs b/src/NHibernate/Linq/Functions/DateTimeNowHqlGenerator.cs
new file mode 100644
index 00000000000..9039693fc1c
--- /dev/null
+++ b/src/NHibernate/Linq/Functions/DateTimeNowHqlGenerator.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Reflection;
+using NHibernate.Engine;
+using NHibernate.Hql.Ast;
+using NHibernate.Linq.Visitors;
+using NHibernate.Util;
+using Environment = NHibernate.Cfg.Environment;
+
+namespace NHibernate.Linq.Functions
+{
+ public class DateTimeNowHqlGenerator : BaseHqlGeneratorForProperty, IAllowPreEvaluationHqlGenerator
+ {
+ private static readonly MemberInfo DateTimeNow = ReflectHelper.GetProperty(() => DateTime.Now);
+ private static readonly MemberInfo DateTimeUtcNow = ReflectHelper.GetProperty(() => DateTime.UtcNow);
+ private static readonly MemberInfo DateTimeToday = ReflectHelper.GetProperty(() => DateTime.Today);
+ private static readonly MemberInfo DateTimeOffsetNow = ReflectHelper.GetProperty(() => DateTimeOffset.Now);
+ private static readonly MemberInfo DateTimeOffsetUtcNow = ReflectHelper.GetProperty(() => DateTimeOffset.UtcNow);
+
+ private readonly Dictionary _hqlFunctions = new Dictionary()
+ {
+ { DateTimeNow, "current_timestamp" },
+ { DateTimeUtcNow, "current_utctimestamp" },
+ // There is also sysdate, but it is troublesome: under some databases, "sys" prefixed functions return the
+ // system time (time according to the server time zone) while "current" prefixed functions return the
+ // session time (time according to the connection time zone), thus introducing a discrepancy with
+ // current_timestamp.
+ // Moreover sysdate is registered by default as a datetime, not as a date. (It could make sense for
+ // Oracle, which returns a time part for dates, just dropping fractional seconds. But Oracle dialect
+ // overrides it as a NHibernate date, without truncating it for SQL comparisons...)
+ { DateTimeToday, "current_date" },
+ { DateTimeOffsetNow, "current_timestamp_offset" },
+ { DateTimeOffsetUtcNow, "current_utctimestamp_offset" },
+ };
+
+ public DateTimeNowHqlGenerator()
+ {
+ SupportedProperties = new[]
+ {
+ DateTimeNow,
+ DateTimeUtcNow,
+ DateTimeToday,
+ DateTimeOffsetNow,
+ DateTimeOffsetUtcNow,
+ };
+ }
+
+ public override HqlTreeNode BuildHql(
+ MemberInfo member,
+ Expression expression,
+ HqlTreeBuilder treeBuilder,
+ IHqlExpressionVisitor visitor)
+ {
+ return treeBuilder.MethodCall(_hqlFunctions[member]);
+ }
+
+ public bool AllowPreEvaluation(MemberInfo member, ISessionFactoryImplementor factory)
+ {
+ var functionName = _hqlFunctions[member];
+ if (factory.Dialect.Functions.ContainsKey(functionName))
+ return false;
+
+ if (factory.Settings.LinqToHqlFallbackOnPreEvaluation)
+ return true;
+
+ throw new QueryException(
+ $"Cannot translate {member.DeclaringType.Name}.{member.Name}: {functionName} is " +
+ $"not supported by {factory.Dialect}. Either enable the fallback on pre-evaluation " +
+ $"({Environment.LinqToHqlFallbackOnPreEvaluation}) or evaluate {member.Name} " +
+ "outside of the query.");
+ }
+
+ public bool IgnoreInstance(MemberInfo member)
+ {
+ // They are all static properties
+ return true;
+ }
+ }
+}
diff --git a/src/NHibernate/Linq/Functions/DefaultLinqToHqlGeneratorsRegistry.cs b/src/NHibernate/Linq/Functions/DefaultLinqToHqlGeneratorsRegistry.cs
index ea5ab8a159c..29595877d9f 100644
--- a/src/NHibernate/Linq/Functions/DefaultLinqToHqlGeneratorsRegistry.cs
+++ b/src/NHibernate/Linq/Functions/DefaultLinqToHqlGeneratorsRegistry.cs
@@ -56,6 +56,10 @@ public DefaultLinqToHqlGeneratorsRegistry()
this.Merge(new CollectionContainsGenerator());
this.Merge(new DateTimePropertiesHqlGenerator());
+ this.Merge(new DateTimeNowHqlGenerator());
+
+ this.Merge(new NewGuidHqlGenerator());
+ this.Merge(new RandomHqlGenerator());
this.Merge(new DecimalAddGenerator());
this.Merge(new DecimalDivideGenerator());
diff --git a/src/NHibernate/Linq/Functions/IAllowPreEvaluationHqlGenerator.cs b/src/NHibernate/Linq/Functions/IAllowPreEvaluationHqlGenerator.cs
new file mode 100644
index 00000000000..2cd67b7d2d1
--- /dev/null
+++ b/src/NHibernate/Linq/Functions/IAllowPreEvaluationHqlGenerator.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Reflection;
+using NHibernate.Engine;
+
+namespace NHibernate.Linq.Functions
+{
+ public interface IAllowPreEvaluationHqlGenerator
+ {
+ ///
+ /// Should pre-evaluation be allowed for this property or method?
+ ///
+ /// The property or method.
+ /// The session factory.
+ ///
+ /// if the property or method should be evaluated before running the query whenever possible,
+ /// if it must always be translated to the equivalent HQL call.
+ ///
+ /// Implementors should return by default. Returning
+ /// is mainly useful when the HQL translation is a non-deterministic function call like NEWGUID() or
+ /// a function which value on server side can differ from the equivalent client value, like
+ /// .
+ bool AllowPreEvaluation(MemberInfo member, ISessionFactoryImplementor factory);
+
+ ///
+ /// Should the instance holding the property or method be ignored?
+ ///
+ /// The property or method.
+ ///
+ /// if the property or method translation does not depend on the instance to which it
+ /// belongs, otherwise.
+ ///
+ bool IgnoreInstance(MemberInfo member);
+ }
+}
diff --git a/src/NHibernate/Linq/Functions/IHqlGeneratorForMethod.cs b/src/NHibernate/Linq/Functions/IHqlGeneratorForMethod.cs
index fde4ffd45f0..73ad8b3d9e4 100644
--- a/src/NHibernate/Linq/Functions/IHqlGeneratorForMethod.cs
+++ b/src/NHibernate/Linq/Functions/IHqlGeneratorForMethod.cs
@@ -2,6 +2,7 @@
using System.Collections.ObjectModel;
using System.Linq.Expressions;
using System.Reflection;
+using NHibernate.Engine;
using NHibernate.Hql.Ast;
using NHibernate.Linq.Visitors;
@@ -31,5 +32,45 @@ public static bool AllowsNullableReturnType(this IHqlGeneratorForMethod generato
return true;
}
+
+ // 6.0 TODO: merge into IHqlGeneratorForMethod
+ ///
+ /// Should pre-evaluation be allowed for this method?
+ ///
+ /// The method's HQL generator.
+ /// The method.
+ /// The session factory.
+ ///
+ /// if the method should be evaluated before running the query whenever possible,
+ /// if it must always be translated to the equivalent HQL call.
+ ///
+ public static bool AllowPreEvaluation(
+ this IHqlGeneratorForMethod generator,
+ MemberInfo member,
+ ISessionFactoryImplementor factory)
+ {
+ if (generator is IAllowPreEvaluationHqlGenerator allowPreEvalGenerator)
+ return allowPreEvalGenerator.AllowPreEvaluation(member, factory);
+
+ // By default, everything should be pre-evaluated whenever possible.
+ return true;
+ }
+
+ ///
+ /// Should the instance holding the method be ignored?
+ ///
+ /// The method's HQL generator.
+ /// The method.
+ ///
+ /// if the method translation does not depend on the instance to which it
+ /// belongs, otherwise.
+ ///
+ public static bool IgnoreInstance(this IHqlGeneratorForMethod generator, MemberInfo member)
+ {
+ if (generator is IAllowPreEvaluationHqlGenerator allowPreEvalGenerator)
+ return allowPreEvalGenerator.IgnoreInstance(member);
+
+ return false;
+ }
}
}
diff --git a/src/NHibernate/Linq/Functions/IHqlGeneratorForProperty.cs b/src/NHibernate/Linq/Functions/IHqlGeneratorForProperty.cs
index 83650ae2185..474ea552ced 100644
--- a/src/NHibernate/Linq/Functions/IHqlGeneratorForProperty.cs
+++ b/src/NHibernate/Linq/Functions/IHqlGeneratorForProperty.cs
@@ -1,14 +1,46 @@
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
+using NHibernate.Engine;
using NHibernate.Hql.Ast;
using NHibernate.Linq.Visitors;
namespace NHibernate.Linq.Functions
{
- public interface IHqlGeneratorForProperty
- {
- IEnumerable SupportedProperties { get; }
- HqlTreeNode BuildHql(MemberInfo member, Expression expression, HqlTreeBuilder treeBuilder, IHqlExpressionVisitor visitor);
- }
-}
\ No newline at end of file
+ public interface IHqlGeneratorForProperty
+ {
+ IEnumerable SupportedProperties { get; }
+
+ HqlTreeNode BuildHql(
+ MemberInfo member,
+ Expression expression,
+ HqlTreeBuilder treeBuilder,
+ IHqlExpressionVisitor visitor);
+ }
+
+ // 6.0 TODO: merge into IHqlGeneratorForProperty
+ public static class HqlGeneratorForPropertyExtensions
+ {
+ ///
+ /// Should pre-evaluation be allowed for this property?
+ ///
+ /// The property's HQL generator.
+ /// The property.
+ /// The session factory.
+ ///
+ /// if the property should be evaluated before running the query whenever possible,
+ /// if it must always be translated to the equivalent HQL call.
+ ///
+ public static bool AllowPreEvaluation(
+ this IHqlGeneratorForProperty generator,
+ MemberInfo member,
+ ISessionFactoryImplementor factory)
+ {
+ if (generator is IAllowPreEvaluationHqlGenerator allowPreEvalGenerator)
+ return allowPreEvalGenerator.AllowPreEvaluation(member, factory);
+
+ // By default, everything should be pre-evaluated whenever possible.
+ return true;
+ }
+ }
+}
diff --git a/src/NHibernate/Linq/Functions/NewGuidHqlGenerator.cs b/src/NHibernate/Linq/Functions/NewGuidHqlGenerator.cs
new file mode 100644
index 00000000000..2318d19f51b
--- /dev/null
+++ b/src/NHibernate/Linq/Functions/NewGuidHqlGenerator.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq.Expressions;
+using System.Reflection;
+using NHibernate.Engine;
+using NHibernate.Hql.Ast;
+using NHibernate.Linq.Visitors;
+using NHibernate.Util;
+using Environment = NHibernate.Cfg.Environment;
+
+namespace NHibernate.Linq.Functions
+{
+ public class NewGuidHqlGenerator : BaseHqlGeneratorForMethod, IAllowPreEvaluationHqlGenerator
+ {
+ public NewGuidHqlGenerator()
+ {
+ SupportedMethods = new[]
+ {
+ ReflectHelper.GetMethod(() => Guid.NewGuid())
+ };
+ }
+
+ public override HqlTreeNode BuildHql(
+ MethodInfo method,
+ Expression targetObject,
+ ReadOnlyCollection arguments,
+ HqlTreeBuilder treeBuilder,
+ IHqlExpressionVisitor visitor)
+ {
+ return treeBuilder.MethodCall("new_uuid");
+ }
+
+ public bool AllowPreEvaluation(MemberInfo member, ISessionFactoryImplementor factory)
+ {
+ if (factory.Dialect.Functions.ContainsKey("new_uuid"))
+ return false;
+
+ if (factory.Settings.LinqToHqlFallbackOnPreEvaluation)
+ return true;
+
+ throw new QueryException(
+ "Cannot translate NewGuid: new_uuid is " +
+ $"not supported by {factory.Dialect}. Either enable the fallback on pre-evaluation " +
+ $"({Environment.LinqToHqlFallbackOnPreEvaluation}) or evaluate NewGuid " +
+ "outside of the query.");
+ }
+
+ public bool IgnoreInstance(MemberInfo member)
+ {
+ // There is only a static method
+ return true;
+ }
+ }
+}
diff --git a/src/NHibernate/Linq/Functions/RandomHqlGenerator.cs b/src/NHibernate/Linq/Functions/RandomHqlGenerator.cs
new file mode 100644
index 00000000000..bde4895e1e5
--- /dev/null
+++ b/src/NHibernate/Linq/Functions/RandomHqlGenerator.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq.Expressions;
+using System.Reflection;
+using NHibernate.Engine;
+using NHibernate.Hql.Ast;
+using NHibernate.Linq.Visitors;
+using NHibernate.Util;
+using Environment = NHibernate.Cfg.Environment;
+
+namespace NHibernate.Linq.Functions
+{
+ public class RandomHqlGenerator : BaseHqlGeneratorForMethod, IAllowPreEvaluationHqlGenerator
+ {
+ private readonly MethodInfo _nextDouble = ReflectHelper.GetMethod(r => r.NextDouble());
+ private const string _randomFunctionName = "random";
+ private const string _floorFunctionName = "floor";
+
+ public RandomHqlGenerator()
+ {
+ SupportedMethods = new[]
+ {
+ _nextDouble,
+ ReflectHelper.GetMethod(r => r.Next()),
+ ReflectHelper.GetMethod(r => r.Next(2)),
+ ReflectHelper.GetMethod(r => r.Next(-1, 1))
+ };
+ }
+
+ public override HqlTreeNode BuildHql(
+ MethodInfo method,
+ Expression targetObject,
+ ReadOnlyCollection arguments,
+ HqlTreeBuilder treeBuilder,
+ IHqlExpressionVisitor visitor)
+ {
+ if (method == _nextDouble)
+ return treeBuilder.MethodCall(_randomFunctionName);
+
+ switch (arguments.Count)
+ {
+ case 0:
+ return treeBuilder.Cast(
+ treeBuilder.MethodCall(
+ _floorFunctionName,
+ treeBuilder.Multiply(
+ treeBuilder.MethodCall(_randomFunctionName),
+ treeBuilder.Constant(int.MaxValue))),
+ typeof(int));
+ case 1:
+ return treeBuilder.Cast(
+ treeBuilder.MethodCall(
+ _floorFunctionName,
+ treeBuilder.Multiply(
+ treeBuilder.MethodCall(_randomFunctionName),
+ visitor.Visit(arguments[0]).AsExpression())),
+ typeof(int));
+ case 2:
+ var minValue = visitor.Visit(arguments[0]).AsExpression();
+ var maxValue = visitor.Visit(arguments[1]).AsExpression();
+ return treeBuilder.Cast(
+ treeBuilder.Add(
+ treeBuilder.MethodCall(
+ _floorFunctionName,
+ treeBuilder.Multiply(
+ treeBuilder.MethodCall(_randomFunctionName),
+ treeBuilder.Subtract(maxValue, minValue))),
+ minValue),
+ typeof(int));
+ default:
+ throw new NotSupportedException();
+ }
+ }
+
+ ///
+ public bool AllowPreEvaluation(MemberInfo member, ISessionFactoryImplementor factory)
+ {
+ if (factory.Dialect.Functions.ContainsKey(_randomFunctionName) &&
+ (member == _nextDouble || factory.Dialect.Functions.ContainsKey(_floorFunctionName)))
+ return false;
+
+ if (factory.Settings.LinqToHqlFallbackOnPreEvaluation)
+ return true;
+
+ var functionName = factory.Dialect.Functions.ContainsKey(_randomFunctionName)
+ ? _floorFunctionName
+ : _randomFunctionName;
+ throw new QueryException(
+ $"Cannot translate {member.DeclaringType.Name}.{member.Name}: {functionName} is " +
+ $"not supported by {factory.Dialect}. Either enable the fallback on pre-evaluation " +
+ $"({Environment.LinqToHqlFallbackOnPreEvaluation}) or evaluate {member.Name} " +
+ "outside of the query.");
+ }
+
+ ///
+ public bool IgnoreInstance(MemberInfo member)
+ {
+ // The translation ignores the Random instance, so long if it was specifically seeded: the user should
+ // pass the random value as a local variable in the Linq query in such case.
+ // Returning false here would cause the method, when appearing in a select clause, to be post-evaluated.
+ // Contrary to pre-evaluation, the post-evaluation is done for each row so it at least would avoid having
+ // the same random value for each result.
+ // But that would still be not executed in database which would be unexpected, in my opinion.
+ // It would even cause failures if the random instance used for querying is shared among threads or is
+ // too similarly seeded between queries.
+ return true;
+ }
+ }
+}
diff --git a/src/NHibernate/Linq/NhLinqExpression.cs b/src/NHibernate/Linq/NhLinqExpression.cs
index 2a00840949a..918b56a37f8 100644
--- a/src/NHibernate/Linq/NhLinqExpression.cs
+++ b/src/NHibernate/Linq/NhLinqExpression.cs
@@ -39,7 +39,7 @@ public class NhLinqExpression : IQueryExpression, ICacheableQueryExpression
public NhLinqExpression(Expression expression, ISessionFactoryImplementor sessionFactory)
{
- _expression = NhRelinqQueryParser.PreTransform(expression);
+ _expression = NhRelinqQueryParser.PreTransform(expression, sessionFactory);
// We want logging to be as close as possible to the original expression sent from the
// application. But if we log before partial evaluation done in PreTransform, the log won't
diff --git a/src/NHibernate/Linq/NhRelinqQueryParser.cs b/src/NHibernate/Linq/NhRelinqQueryParser.cs
index bafd050280b..d32908ed0af 100644
--- a/src/NHibernate/Linq/NhRelinqQueryParser.cs
+++ b/src/NHibernate/Linq/NhRelinqQueryParser.cs
@@ -1,8 +1,10 @@
+using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
+using NHibernate.Engine;
using NHibernate.Linq.ExpressionTransformers;
using NHibernate.Linq.Visitors;
using NHibernate.Util;
@@ -44,15 +46,30 @@ static NhRelinqQueryParser()
QueryParser = new QueryParser(expressionTreeParser);
}
+ // Obsolete since v5.3
///
- /// Applies the minimal transformations required before parameterization,
+ /// Applies the minimal transformations required before parametrization,
/// expression key computing and parsing.
///
/// The expression to transform.
/// The transformed expression.
+ [Obsolete("Use overload with an additional sessionFactory parameter")]
public static Expression PreTransform(Expression expression)
{
- var partiallyEvaluatedExpression = NhPartialEvaluatingExpressionVisitor.EvaluateIndependentSubtrees(expression);
+ return PreTransform(expression, null);
+ }
+
+ ///
+ /// Applies the minimal transformations required before parametrization,
+ /// expression key computing and parsing.
+ ///
+ /// The expression to transform.
+ /// The session factory.
+ /// The transformed expression.
+ public static Expression PreTransform(Expression expression, ISessionFactoryImplementor sessionFactory)
+ {
+ var partiallyEvaluatedExpression =
+ NhPartialEvaluatingExpressionVisitor.EvaluateIndependentSubtrees(expression, sessionFactory);
return PreProcessor.Process(partiallyEvaluatedExpression);
}
diff --git a/src/NHibernate/Linq/Visitors/MemberExpressionJoinDetector.cs b/src/NHibernate/Linq/Visitors/MemberExpressionJoinDetector.cs
index 934fba8ec94..f333fc5e61d 100644
--- a/src/NHibernate/Linq/Visitors/MemberExpressionJoinDetector.cs
+++ b/src/NHibernate/Linq/Visitors/MemberExpressionJoinDetector.cs
@@ -32,6 +32,13 @@ public MemberExpressionJoinDetector(IIsEntityDecider isEntityDecider, IJoiner jo
protected override Expression VisitMember(MemberExpression expression)
{
+ // A static member expression such as DateTime.Now has a null Expression.
+ if (expression.Expression == null)
+ {
+ // A static member call is never a join, and it is not an instance member access either.
+ return base.VisitMember(expression);
+ }
+
var isIdentifier = _isEntityDecider.IsIdentifier(expression.Expression.Type, expression.Member.Name);
if (isIdentifier)
_hasIdentifier = true;
diff --git a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs
index 105a098817d..5cfb22fa178 100644
--- a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs
+++ b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs
@@ -2,6 +2,8 @@
using System.Linq;
using System.Linq.Expressions;
using NHibernate.Collection;
+using NHibernate.Engine;
+using NHibernate.Linq.Functions;
using NHibernate.Util;
using Remotion.Linq.Clauses.Expressions;
using Remotion.Linq.Parsing;
@@ -12,20 +14,31 @@ namespace NHibernate.Linq.Visitors
{
internal class NhPartialEvaluatingExpressionVisitor : RelinqExpressionVisitor, IPartialEvaluationExceptionExpressionVisitor
{
+ private readonly ISessionFactoryImplementor _sessionFactory;
+
+ internal NhPartialEvaluatingExpressionVisitor(ISessionFactoryImplementor sessionFactory)
+ {
+ _sessionFactory = sessionFactory;
+ }
+
protected override Expression VisitConstant(ConstantExpression expression)
{
if (expression.Value is Expression value)
{
- return EvaluateIndependentSubtrees(value);
+ return EvaluateIndependentSubtrees(value, _sessionFactory);
}
return base.VisitConstant(expression);
}
- public static Expression EvaluateIndependentSubtrees(Expression expression)
+ public static Expression EvaluateIndependentSubtrees(
+ Expression expression,
+ ISessionFactoryImplementor sessionFactory)
{
- var evaluatedExpression = PartialEvaluatingExpressionVisitor.EvaluateIndependentSubtrees(expression, new NhEvaluatableExpressionFilter());
- return new NhPartialEvaluatingExpressionVisitor().Visit(evaluatedExpression);
+ var evaluatedExpression = PartialEvaluatingExpressionVisitor.EvaluateIndependentSubtrees(
+ expression,
+ new NhEvaluatableExpressionFilter(sessionFactory));
+ return new NhPartialEvaluatingExpressionVisitor(sessionFactory).Visit(evaluatedExpression);
}
public Expression VisitPartialEvaluationException(PartialEvaluationExceptionExpression partialEvaluationExceptionExpression)
@@ -38,6 +51,13 @@ public Expression VisitPartialEvaluationException(PartialEvaluationExceptionExpr
internal class NhEvaluatableExpressionFilter : EvaluatableExpressionFilterBase
{
+ private readonly ISessionFactoryImplementor _sessionFactory;
+
+ internal NhEvaluatableExpressionFilter(ISessionFactoryImplementor sessionFactory)
+ {
+ _sessionFactory = sessionFactory;
+ }
+
public override bool IsEvaluatableConstant(ConstantExpression node)
{
if (node.Value is IPersistentCollection && node.Value is IQueryable)
@@ -48,6 +68,18 @@ public override bool IsEvaluatableConstant(ConstantExpression node)
return base.IsEvaluatableConstant(node);
}
+ public override bool IsEvaluatableMember(MemberExpression node)
+ {
+ if (node == null)
+ throw new ArgumentNullException(nameof(node));
+
+ if (_sessionFactory == null || _sessionFactory.Settings.LinqToHqlLegacyPreEvaluation ||
+ !_sessionFactory.Settings.LinqToHqlGeneratorsRegistry.TryGetGenerator(node.Member, out var generator))
+ return true;
+
+ return generator.AllowPreEvaluation(node.Member, _sessionFactory);
+ }
+
public override bool IsEvaluatableMethodCall(MethodCallExpression node)
{
if (node == null)
@@ -56,8 +88,14 @@ public override bool IsEvaluatableMethodCall(MethodCallExpression node)
var attributes = node.Method
.GetCustomAttributes(typeof(LinqExtensionMethodAttributeBase), false)
.ToArray(x => (LinqExtensionMethodAttributeBase) x);
- return attributes.Length == 0 ||
- attributes.Any(a => a.PreEvaluation == LinqExtensionPreEvaluation.AllowPreEvaluation);
+ if (attributes.Length > 0)
+ return attributes.Any(a => a.PreEvaluation == LinqExtensionPreEvaluation.AllowPreEvaluation);
+
+ if (_sessionFactory == null || _sessionFactory.Settings.LinqToHqlLegacyPreEvaluation ||
+ !_sessionFactory.Settings.LinqToHqlGeneratorsRegistry.TryGetGenerator(node.Method, out var generator))
+ return true;
+
+ return generator.AllowPreEvaluation(node.Method, _sessionFactory);
}
}
}
diff --git a/src/NHibernate/Linq/Visitors/NullableExpressionDetector.cs b/src/NHibernate/Linq/Visitors/NullableExpressionDetector.cs
index d9e2d6c06f5..0fa6e4da40d 100644
--- a/src/NHibernate/Linq/Visitors/NullableExpressionDetector.cs
+++ b/src/NHibernate/Linq/Visitors/NullableExpressionDetector.cs
@@ -128,6 +128,13 @@ private bool IsNullable(MemberExpression memberExpression, BinaryExpression equa
{
if (_functionRegistry.TryGetGenerator(memberExpression.Member, out _))
{
+ // The expression can be null when the member is static (e.g. DateTime.Now).
+ // In such cases we suppose that the value cannot be null.
+ if (memberExpression.Expression == null)
+ {
+ return false;
+ }
+
// We have to skip the property as it will be converted to a function that can return null
// if the argument is null
return IsNullable(memberExpression.Expression, equalityExpression);
diff --git a/src/NHibernate/Linq/Visitors/SelectClauseNominator.cs b/src/NHibernate/Linq/Visitors/SelectClauseNominator.cs
index 085d6681926..9205b4f1b05 100644
--- a/src/NHibernate/Linq/Visitors/SelectClauseNominator.cs
+++ b/src/NHibernate/Linq/Visitors/SelectClauseNominator.cs
@@ -114,11 +114,14 @@ private bool IsRegisteredFunction(Expression expression)
if (expression.NodeType == ExpressionType.Call)
{
var methodCallExpression = (MethodCallExpression) expression;
- IHqlGeneratorForMethod methodGenerator;
- if (_functionRegistry.TryGetGenerator(methodCallExpression.Method, out methodGenerator))
+ if (_functionRegistry.TryGetGenerator(methodCallExpression.Method, out var methodGenerator))
{
- return methodCallExpression.Object == null || // is static or extension method
- methodCallExpression.Object.NodeType != ExpressionType.Constant; // does not belong to parameter
+ // is static or extension method
+ return methodCallExpression.Object == null ||
+ // does not belong to parameter
+ methodCallExpression.Object.NodeType != ExpressionType.Constant ||
+ // does not ignore the parameter it belongs to
+ methodGenerator.IgnoreInstance(methodCallExpression.Method);
}
}
else if (expression is NhSumExpression ||
diff --git a/src/NHibernate/Linq/Visitors/WhereJoinDetector.cs b/src/NHibernate/Linq/Visitors/WhereJoinDetector.cs
index 68f88c6cc55..4be0b8d2af2 100644
--- a/src/NHibernate/Linq/Visitors/WhereJoinDetector.cs
+++ b/src/NHibernate/Linq/Visitors/WhereJoinDetector.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
-using NHibernate.Linq.Clauses;
using NHibernate.Linq.ReWriters;
using Remotion.Linq.Clauses;
using Remotion.Linq.Clauses.Expressions;
@@ -289,7 +288,7 @@ protected override Expression VisitSubQuery(SubQueryExpression expression)
return expression;
}
- // We would usually get NULL if one of our inner member expresions was null.
+ // We would usually get NULL if one of our inner member expressions was null.
// However, it's possible a method call will convert the null value from the failed join into a non-null value.
// This could be optimized by actually checking what the method does. For example StartsWith("s") would leave null as null and would still allow us to inner join.
//protected override Expression VisitMethodCall(MethodCallExpression expression)
@@ -307,7 +306,17 @@ protected override Expression VisitMember(MemberExpression expression)
// I'm not sure what processing re-linq does to strange member expressions.
// TODO: I suspect this code doesn't add the right joins for the last case.
- var isIdentifier = _isEntityDecider.IsIdentifier(expression.Expression.Type, expression.Member.Name);
+ // A static member expression such as DateTime.Now has a null Expression.
+ if (expression.Expression == null)
+ {
+ // A static member call is never a join, and it is not an instance member access either: leave
+ // the current value on stack, untouched.
+ return base.VisitMember(expression);
+ }
+
+ var isIdentifier = _isEntityDecider.IsIdentifier(
+ expression.Expression.Type,
+ expression.Member.Name);
if (!isIdentifier)
_memberExpressionDepth++;
@@ -332,7 +341,7 @@ protected override Expression VisitMember(MemberExpression expression)
values.MemberExpressionValuesIfEmptyOuterJoined[key] = PossibleValueSet.CreateNull(expression.Type);
}
SetResultValues(values);
-
+
return result;
}
diff --git a/src/NHibernate/NHibernateUtil.cs b/src/NHibernate/NHibernateUtil.cs
index e781db52bef..226d26bf67c 100644
--- a/src/NHibernate/NHibernateUtil.cs
+++ b/src/NHibernate/NHibernateUtil.cs
@@ -137,6 +137,11 @@ public static IType GuessType(System.Type type)
///
public static readonly DateType Date = new DateType();
+ ///
+ /// NHibernate local date type
+ ///
+ public static readonly DateType LocalDate = new LocalDateType();
+
///
/// NHibernate decimal type
///
diff --git a/src/NHibernate/Type/DateType.cs b/src/NHibernate/Type/DateType.cs
index 08e4097a7a7..76d3fbb99e9 100644
--- a/src/NHibernate/Type/DateType.cs
+++ b/src/NHibernate/Type/DateType.cs
@@ -35,7 +35,7 @@ public DateType() : base(SqlTypeFactory.Date)
///
protected override DateTime AdjustDateTime(DateTime dateValue) =>
- dateValue.Date;
+ Kind == DateTimeKind.Unspecified ? dateValue.Date : DateTime.SpecifyKind(dateValue.Date, Kind);
///
public override bool IsEqual(object x, object y)
diff --git a/src/NHibernate/Type/LocalDateType.cs b/src/NHibernate/Type/LocalDateType.cs
new file mode 100644
index 00000000000..d6fadaec31b
--- /dev/null
+++ b/src/NHibernate/Type/LocalDateType.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Data;
+
+namespace NHibernate.Type
+{
+ ///
+ /// Maps the Year, Month, and Day of a Property to a
+ /// column. Specify when reading
+ /// dates from .
+ ///
+ [Serializable]
+ public class LocalDateType : DateType
+ {
+ ///
+ protected override DateTimeKind Kind => DateTimeKind.Local;
+ }
+}
diff --git a/src/NHibernate/Util/ReflectHelper.cs b/src/NHibernate/Util/ReflectHelper.cs
index 7de13090ff7..fc9856203ba 100644
--- a/src/NHibernate/Util/ReflectHelper.cs
+++ b/src/NHibernate/Util/ReflectHelper.cs
@@ -149,6 +149,21 @@ public static MemberInfo GetProperty(Expression
+ /// Gets the static field or property to be accessed.
+ ///
+ /// The type of the property.
+ /// The expression representing the property getter.
+ /// The of the property.
+ public static MemberInfo GetProperty(Expression> property)
+ {
+ if (property == null)
+ {
+ throw new ArgumentNullException(nameof(property));
+ }
+ return ((MemberExpression)property.Body).Member;
+ }
+
internal static bool ParameterTypesMatch(ParameterInfo[] parameters, System.Type[] types)
{
if (parameters.Length != types.Length)
diff --git a/src/NHibernate/nhibernate-configuration.xsd b/src/NHibernate/nhibernate-configuration.xsd
index 85fc25825e1..c8280b03de8 100644
--- a/src/NHibernate/nhibernate-configuration.xsd
+++ b/src/NHibernate/nhibernate-configuration.xsd
@@ -152,6 +152,40 @@
+
+
+
+ Whether to use the legacy pre-evaluation or not in Linq queries. true by default.
+
+ Legacy pre-evaluation is causing special properties or functions like DateTime.Now or Guid.NewGuid()
+ to be always evaluated with the .Net runtime and replaced in the query by parameter values.
+
+ The new pre-evaluation allows them to be converted to HQL function calls which will be run on the db
+ side. This allows for example to retrieve the server time instead of the client time, or to generate
+ UUIDs for each row instead of an unique one for all rows.
+
+ The new pre-evaluation will likely be enabled by default in the next major version (6.0).
+
+
+
+
+
+
+ When the new pre-evaluation is enabled, should methods which translation is not supported by the current
+ dialect fallback to pre-evaluation? false by default.
+
+ When this fallback option is enabled while legacy pre-evaluation is disabled, properties or functions
+ like DateTime.Now or Guid.NewGuid() used in Linq expressions will not fail when the dialect does not
+ support them, but will instead be pre-evaluated.
+
+ When this fallback option is disabled while legacy pre-evaluation is disabled, properties or functions
+ like DateTime.Now or Guid.NewGuid() used in Linq expressions will fail when the dialect does not
+ support them.
+
+ This option has no effect if the legacy pre-evaluation is enabled.
+
+
+