From d84b43e58e1afa5bc35f32990932a1927139eeba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericDelaporte@users.noreply.github.com> Date: Wed, 3 Oct 2018 20:26:14 +0200 Subject: [PATCH 1/3] Support evaluation of DateTime.Now on db side And of all similar properties: UtcNow, Today, and DateTimeOffset's ones. Part of #959 Co-authored-by: maca88 --- doc/reference/modules/configuration.xml | 51 ++++ .../Async/Linq/MiscellaneousTextFixture.cs | 3 +- .../Linq/MiscellaneousTextFixture.cs | 3 +- .../Linq/PreEvaluationTests.cs | 267 ++++++++++++++++++ src/NHibernate.Test/Linq/TryGetMappedTests.cs | 2 +- src/NHibernate.Test/TestCase.cs | 28 +- src/NHibernate/Cfg/Environment.cs | 42 +++ src/NHibernate/Cfg/Settings.cs | 38 +++ src/NHibernate/Cfg/SettingsFactory.cs | 9 + src/NHibernate/Dialect/DB2Dialect.cs | 4 +- src/NHibernate/Dialect/Dialect.cs | 2 +- src/NHibernate/Dialect/FirebirdDialect.cs | 3 +- src/NHibernate/Dialect/HanaDialectBase.cs | 6 +- src/NHibernate/Dialect/InformixDialect.cs | 3 +- src/NHibernate/Dialect/MsSql2000Dialect.cs | 4 +- src/NHibernate/Dialect/MsSql2008Dialect.cs | 11 +- src/NHibernate/Dialect/MsSqlCeDialect.cs | 3 +- src/NHibernate/Dialect/MySQL55Dialect.cs | 7 + src/NHibernate/Dialect/MySQLDialect.cs | 2 +- src/NHibernate/Dialect/Oracle8iDialect.cs | 4 +- src/NHibernate/Dialect/Oracle9iDialect.cs | 10 + src/NHibernate/Dialect/PostgreSQL81Dialect.cs | 6 + src/NHibernate/Dialect/PostgreSQLDialect.cs | 3 +- src/NHibernate/Dialect/SQLiteDialect.cs | 7 +- src/NHibernate/Dialect/SybaseASA9Dialect.cs | 2 +- src/NHibernate/Dialect/SybaseASE15Dialect.cs | 5 +- .../Dialect/SybaseSQLAnywhere10Dialect.cs | 4 +- .../Dialect/SybaseSQLAnywhere12Dialect.cs | 7 + .../Linq/Functions/DateTimeNowHqlGenerator.cs | 74 +++++ .../DefaultLinqToHqlGeneratorsRegistry.cs | 1 + .../IAllowPreEvaluationHqlGenerator.cs | 24 ++ .../Functions/IHqlGeneratorForProperty.cs | 44 ++- src/NHibernate/Linq/NhLinqExpression.cs | 2 +- src/NHibernate/Linq/NhRelinqQueryParser.cs | 21 +- .../Visitors/MemberExpressionJoinDetector.cs | 7 + .../NhPartialEvaluatingExpressionVisitor.cs | 40 ++- .../Visitors/NullableExpressionDetector.cs | 7 + .../Linq/Visitors/WhereJoinDetector.cs | 17 +- src/NHibernate/NHibernateUtil.cs | 5 + src/NHibernate/Type/DateType.cs | 2 +- src/NHibernate/Type/LocalDateType.cs | 17 ++ src/NHibernate/Util/ReflectHelper.cs | 15 + src/NHibernate/nhibernate-configuration.xsd | 34 +++ 43 files changed, 798 insertions(+), 48 deletions(-) create mode 100644 src/NHibernate.Test/Linq/PreEvaluationTests.cs create mode 100644 src/NHibernate/Linq/Functions/DateTimeNowHqlGenerator.cs create mode 100644 src/NHibernate/Linq/Functions/IAllowPreEvaluationHqlGenerator.cs create mode 100644 src/NHibernate/Type/LocalDateType.cs 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/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..e5921352f16 --- /dev/null +++ b/src/NHibernate.Test/Linq/PreEvaluationTests.cs @@ -0,0 +1,267 @@ +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"); + } + + 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..81c7aae473d 100644 --- a/src/NHibernate/Dialect/DB2Dialect.cs +++ b/src/NHibernate/Dialect/DB2Dialect.cs @@ -87,6 +87,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 +140,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..e4b8b5dcb8a 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)); diff --git a/src/NHibernate/Dialect/HanaDialectBase.cs b/src/NHibernate/Dialect/HanaDialectBase.cs index b8de4f2a5d7..150ba9aab2d 100644 --- a/src/NHibernate/Dialect/HanaDialectBase.cs +++ b/src/NHibernate/Dialect/HanaDialectBase.cs @@ -439,20 +439,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..e4dd5a9e0af 100644 --- a/src/NHibernate/Dialect/MsSql2000Dialect.cs +++ b/src/NHibernate/Dialect/MsSql2000Dialect.cs @@ -326,7 +326,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)")); 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..46c15f40cf5 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)")); 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/MySQLDialect.cs b/src/NHibernate/Dialect/MySQLDialect.cs index 9a83de26eb3..a1816e95209 100644 --- a/src/NHibernate/Dialect/MySQLDialect.cs +++ b/src/NHibernate/Dialect/MySQLDialect.cs @@ -294,7 +294,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/Oracle8iDialect.cs b/src/NHibernate/Dialect/Oracle8iDialect.cs index b2103b87965..5d6bad35705 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()); @@ -571,7 +571,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..da522e533f3 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,6 +94,7 @@ 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")); diff --git a/src/NHibernate/Dialect/SQLiteDialect.cs b/src/NHibernate/Dialect/SQLiteDialect.cs index f1f86743313..22506edf333 100644 --- a/src/NHibernate/Dialect/SQLiteDialect.cs +++ b/src/NHibernate/Dialect/SQLiteDialect.cs @@ -74,12 +74,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)")); 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..de9514431ca 100644 --- a/src/NHibernate/Dialect/SybaseASE15Dialect.cs +++ b/src/NHibernate/Dialect/SybaseASE15Dialect.cs @@ -68,9 +68,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)); diff --git a/src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs b/src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs index 388947b45c4..10256111696 100644 --- a/src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs +++ b/src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs @@ -238,9 +238,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() 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..ef2c154b81d --- /dev/null +++ b/src/NHibernate/Linq/Functions/DateTimeNowHqlGenerator.cs @@ -0,0 +1,74 @@ +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."); + } + } +} diff --git a/src/NHibernate/Linq/Functions/DefaultLinqToHqlGeneratorsRegistry.cs b/src/NHibernate/Linq/Functions/DefaultLinqToHqlGeneratorsRegistry.cs index ea5ab8a159c..27c28e1bf72 100644 --- a/src/NHibernate/Linq/Functions/DefaultLinqToHqlGeneratorsRegistry.cs +++ b/src/NHibernate/Linq/Functions/DefaultLinqToHqlGeneratorsRegistry.cs @@ -56,6 +56,7 @@ public DefaultLinqToHqlGeneratorsRegistry() this.Merge(new CollectionContainsGenerator()); this.Merge(new DateTimePropertiesHqlGenerator()); + this.Merge(new DateTimeNowHqlGenerator()); 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..ab2266b0f9a --- /dev/null +++ b/src/NHibernate/Linq/Functions/IAllowPreEvaluationHqlGenerator.cs @@ -0,0 +1,24 @@ +using System; +using System.Reflection; +using NHibernate.Engine; + +namespace NHibernate.Linq.Functions +{ + public interface IAllowPreEvaluationHqlGenerator + { + /// + /// Should pre-evaluation be allowed for this property? + /// + /// 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. + /// + /// 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); + } +} 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/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..dccc28df9ab 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) 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/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. + + + From 58272ebe9561775470bc8242aea13f7cf1a53c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericDelaporte@users.noreply.github.com> Date: Thu, 4 Oct 2018 19:37:55 +0200 Subject: [PATCH 2/3] Support evaluation of Guid.NewGuid() on db side Part of #959 Co-authored-by: maca88 --- .../Linq/PreEvaluationTests.cs | 40 ++++++++++++++++ src/NHibernate/Dialect/FirebirdDialect.cs | 1 + src/NHibernate/Dialect/HanaDialectBase.cs | 1 + src/NHibernate/Dialect/MsSql2000Dialect.cs | 2 + src/NHibernate/Dialect/MsSqlCeDialect.cs | 2 + src/NHibernate/Dialect/MySQL5Dialect.cs | 6 +++ src/NHibernate/Dialect/Oracle8iDialect.cs | 2 + src/NHibernate/Dialect/PostgreSQLDialect.cs | 4 ++ src/NHibernate/Dialect/SQLiteDialect.cs | 2 + src/NHibernate/Dialect/SybaseASE15Dialect.cs | 5 ++ .../Dialect/SybaseSQLAnywhere10Dialect.cs | 1 + .../DefaultLinqToHqlGeneratorsRegistry.cs | 2 + .../IAllowPreEvaluationHqlGenerator.cs | 6 +-- .../Linq/Functions/IHqlGeneratorForMethod.cs | 24 ++++++++++ .../Linq/Functions/NewGuidHqlGenerator.cs | 48 +++++++++++++++++++ .../NhPartialEvaluatingExpressionVisitor.cs | 10 +++- 16 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 src/NHibernate/Linq/Functions/NewGuidHqlGenerator.cs diff --git a/src/NHibernate.Test/Linq/PreEvaluationTests.cs b/src/NHibernate.Test/Linq/PreEvaluationTests.cs index e5921352f16..aec3b583298 100644 --- a/src/NHibernate.Test/Linq/PreEvaluationTests.cs +++ b/src/NHibernate.Test/Linq/PreEvaluationTests.cs @@ -251,6 +251,46 @@ private void RunTest(bool isSupported, Action test) 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); + }); + } + private void AssertFunctionInSql(string functionName, SqlLogSpy spy) { if (!IsFunctionSupported(functionName)) diff --git a/src/NHibernate/Dialect/FirebirdDialect.cs b/src/NHibernate/Dialect/FirebirdDialect.cs index e4b8b5dcb8a..09c7fbf1f89 100644 --- a/src/NHibernate/Dialect/FirebirdDialect.cs +++ b/src/NHibernate/Dialect/FirebirdDialect.cs @@ -423,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")); diff --git a/src/NHibernate/Dialect/HanaDialectBase.cs b/src/NHibernate/Dialect/HanaDialectBase.cs index 150ba9aab2d..d94da6f10bc 100644 --- a/src/NHibernate/Dialect/HanaDialectBase.cs +++ b/src/NHibernate/Dialect/HanaDialectBase.cs @@ -395,6 +395,7 @@ 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)); } protected virtual void RegisterHANAFunctions() diff --git a/src/NHibernate/Dialect/MsSql2000Dialect.cs b/src/NHibernate/Dialect/MsSql2000Dialect.cs index e4dd5a9e0af..aab663e0880 100644 --- a/src/NHibernate/Dialect/MsSql2000Dialect.cs +++ b/src/NHibernate/Dialect/MsSql2000Dialect.cs @@ -360,6 +360,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/MsSqlCeDialect.cs b/src/NHibernate/Dialect/MsSqlCeDialect.cs index 46c15f40cf5..fd0baed402b 100644 --- a/src/NHibernate/Dialect/MsSqlCeDialect.cs +++ b/src/NHibernate/Dialect/MsSqlCeDialect.cs @@ -201,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/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/Oracle8iDialect.cs b/src/NHibernate/Dialect/Oracle8iDialect.cs index 5d6bad35705..749c1f0d056 100644 --- a/src/NHibernate/Dialect/Oracle8iDialect.cs +++ b/src/NHibernate/Dialect/Oracle8iDialect.cs @@ -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() diff --git a/src/NHibernate/Dialect/PostgreSQLDialect.cs b/src/NHibernate/Dialect/PostgreSQLDialect.cs index da522e533f3..baa9334d788 100644 --- a/src/NHibernate/Dialect/PostgreSQLDialect.cs +++ b/src/NHibernate/Dialect/PostgreSQLDialect.cs @@ -98,6 +98,10 @@ public PostgreSQLDialect() 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 22506edf333..f8124d486c4 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"); } diff --git a/src/NHibernate/Dialect/SybaseASE15Dialect.cs b/src/NHibernate/Dialect/SybaseASE15Dialect.cs index de9514431ca..0a2800c5bb6 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)); @@ -113,6 +116,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 10256111696..99da6ce9fbd 100644 --- a/src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs +++ b/src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs @@ -338,6 +338,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/Linq/Functions/DefaultLinqToHqlGeneratorsRegistry.cs b/src/NHibernate/Linq/Functions/DefaultLinqToHqlGeneratorsRegistry.cs index 27c28e1bf72..0e921dff7eb 100644 --- a/src/NHibernate/Linq/Functions/DefaultLinqToHqlGeneratorsRegistry.cs +++ b/src/NHibernate/Linq/Functions/DefaultLinqToHqlGeneratorsRegistry.cs @@ -58,6 +58,8 @@ public DefaultLinqToHqlGeneratorsRegistry() this.Merge(new DateTimePropertiesHqlGenerator()); this.Merge(new DateTimeNowHqlGenerator()); + this.Merge(new NewGuidHqlGenerator()); + this.Merge(new DecimalAddGenerator()); this.Merge(new DecimalDivideGenerator()); this.Merge(new DecimalMultiplyGenerator()); diff --git a/src/NHibernate/Linq/Functions/IAllowPreEvaluationHqlGenerator.cs b/src/NHibernate/Linq/Functions/IAllowPreEvaluationHqlGenerator.cs index ab2266b0f9a..f15afa7c7bc 100644 --- a/src/NHibernate/Linq/Functions/IAllowPreEvaluationHqlGenerator.cs +++ b/src/NHibernate/Linq/Functions/IAllowPreEvaluationHqlGenerator.cs @@ -7,12 +7,12 @@ namespace NHibernate.Linq.Functions public interface IAllowPreEvaluationHqlGenerator { /// - /// Should pre-evaluation be allowed for this property? + /// Should pre-evaluation be allowed for this property or method? /// - /// The property. + /// The property or method. /// The session factory. /// - /// if the property should be evaluated before running the query whenever possible, + /// 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 diff --git a/src/NHibernate/Linq/Functions/IHqlGeneratorForMethod.cs b/src/NHibernate/Linq/Functions/IHqlGeneratorForMethod.cs index fde4ffd45f0..c4b871e6380 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,28 @@ 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; + } } } diff --git a/src/NHibernate/Linq/Functions/NewGuidHqlGenerator.cs b/src/NHibernate/Linq/Functions/NewGuidHqlGenerator.cs new file mode 100644 index 00000000000..a33720aed55 --- /dev/null +++ b/src/NHibernate/Linq/Functions/NewGuidHqlGenerator.cs @@ -0,0 +1,48 @@ +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."); + } + } +} diff --git a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs index dccc28df9ab..5cfb22fa178 100644 --- a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs +++ b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs @@ -88,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); } } } From 3a8a1ba0c21c255db06243ac4e394da68c9f22e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericDelaporte@users.noreply.github.com> Date: Sat, 6 Oct 2018 14:28:28 +0200 Subject: [PATCH 3/3] Support evaluation of Random.Next and NextDouble on db side Fixes #959, along with two previous commits --- .../Async/Linq/PreEvaluationTests.cs | 152 ++++++++++++ .../Linq/PreEvaluationTests.cs | 217 ++++++++++++++++++ src/NHibernate/Dialect/DB2Dialect.cs | 1 + src/NHibernate/Dialect/FirebirdDialect.cs | 1 + src/NHibernate/Dialect/HanaDialectBase.cs | 1 + src/NHibernate/Dialect/MsSql2000Dialect.cs | 2 + src/NHibernate/Dialect/MySQLDialect.cs | 3 +- src/NHibernate/Dialect/Oracle10gDialect.cs | 28 ++- src/NHibernate/Dialect/SQLiteDialect.cs | 10 + src/NHibernate/Dialect/SybaseASE15Dialect.cs | 2 + .../Dialect/SybaseSQLAnywhere10Dialect.cs | 1 + .../Linq/Functions/DateTimeNowHqlGenerator.cs | 6 + .../DefaultLinqToHqlGeneratorsRegistry.cs | 1 + .../IAllowPreEvaluationHqlGenerator.cs | 10 + .../Linq/Functions/IHqlGeneratorForMethod.cs | 17 ++ .../Linq/Functions/NewGuidHqlGenerator.cs | 6 + .../Linq/Functions/RandomHqlGenerator.cs | 109 +++++++++ .../Linq/Visitors/SelectClauseNominator.cs | 11 +- 18 files changed, 572 insertions(+), 6 deletions(-) create mode 100644 src/NHibernate.Test/Async/Linq/PreEvaluationTests.cs create mode 100644 src/NHibernate/Linq/Functions/RandomHqlGenerator.cs 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/PreEvaluationTests.cs b/src/NHibernate.Test/Linq/PreEvaluationTests.cs index aec3b583298..4e12c4b9d82 100644 --- a/src/NHibernate.Test/Linq/PreEvaluationTests.cs +++ b/src/NHibernate.Test/Linq/PreEvaluationTests.cs @@ -291,6 +291,223 @@ public void CanSelectNewGuid() }); } + [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)) diff --git a/src/NHibernate/Dialect/DB2Dialect.cs b/src/NHibernate/Dialect/DB2Dialect.cs index 81c7aae473d..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)); diff --git a/src/NHibernate/Dialect/FirebirdDialect.cs b/src/NHibernate/Dialect/FirebirdDialect.cs index 09c7fbf1f89..ba37c00cfaa 100644 --- a/src/NHibernate/Dialect/FirebirdDialect.cs +++ b/src/NHibernate/Dialect/FirebirdDialect.cs @@ -465,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 d94da6f10bc..4a8d70db2e8 100644 --- a/src/NHibernate/Dialect/HanaDialectBase.cs +++ b/src/NHibernate/Dialect/HanaDialectBase.cs @@ -396,6 +396,7 @@ protected virtual void RegisterNHibernateFunctions() 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() diff --git a/src/NHibernate/Dialect/MsSql2000Dialect.cs b/src/NHibernate/Dialect/MsSql2000Dialect.cs index aab663e0880..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)); diff --git a/src/NHibernate/Dialect/MySQLDialect.cs b/src/NHibernate/Dialect/MySQLDialect.cs index a1816e95209..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)); 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/SQLiteDialect.cs b/src/NHibernate/Dialect/SQLiteDialect.cs index f8124d486c4..864defac9a3 100644 --- a/src/NHibernate/Dialect/SQLiteDialect.cs +++ b/src/NHibernate/Dialect/SQLiteDialect.cs @@ -113,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/SybaseASE15Dialect.cs b/src/NHibernate/Dialect/SybaseASE15Dialect.cs index 0a2800c5bb6..a5233395d36 100644 --- a/src/NHibernate/Dialect/SybaseASE15Dialect.cs +++ b/src/NHibernate/Dialect/SybaseASE15Dialect.cs @@ -98,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")); diff --git a/src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs b/src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs index 99da6ce9fbd..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)); diff --git a/src/NHibernate/Linq/Functions/DateTimeNowHqlGenerator.cs b/src/NHibernate/Linq/Functions/DateTimeNowHqlGenerator.cs index ef2c154b81d..9039693fc1c 100644 --- a/src/NHibernate/Linq/Functions/DateTimeNowHqlGenerator.cs +++ b/src/NHibernate/Linq/Functions/DateTimeNowHqlGenerator.cs @@ -70,5 +70,11 @@ public bool AllowPreEvaluation(MemberInfo member, ISessionFactoryImplementor fac $"({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 0e921dff7eb..29595877d9f 100644 --- a/src/NHibernate/Linq/Functions/DefaultLinqToHqlGeneratorsRegistry.cs +++ b/src/NHibernate/Linq/Functions/DefaultLinqToHqlGeneratorsRegistry.cs @@ -59,6 +59,7 @@ public DefaultLinqToHqlGeneratorsRegistry() 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 index f15afa7c7bc..2cd67b7d2d1 100644 --- a/src/NHibernate/Linq/Functions/IAllowPreEvaluationHqlGenerator.cs +++ b/src/NHibernate/Linq/Functions/IAllowPreEvaluationHqlGenerator.cs @@ -20,5 +20,15 @@ public interface IAllowPreEvaluationHqlGenerator /// 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 c4b871e6380..73ad8b3d9e4 100644 --- a/src/NHibernate/Linq/Functions/IHqlGeneratorForMethod.cs +++ b/src/NHibernate/Linq/Functions/IHqlGeneratorForMethod.cs @@ -55,5 +55,22 @@ public static bool AllowPreEvaluation( // 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/NewGuidHqlGenerator.cs b/src/NHibernate/Linq/Functions/NewGuidHqlGenerator.cs index a33720aed55..2318d19f51b 100644 --- a/src/NHibernate/Linq/Functions/NewGuidHqlGenerator.cs +++ b/src/NHibernate/Linq/Functions/NewGuidHqlGenerator.cs @@ -44,5 +44,11 @@ public bool AllowPreEvaluation(MemberInfo member, ISessionFactoryImplementor fac $"({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/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 ||