Skip to content

Commit 1e64f0a

Browse files
Support evaluation of DateTime.Now on db side
And of all similar properties: UtcNow, Today, and DateTimeOffset's ones. Part of #959
1 parent 873feec commit 1e64f0a

33 files changed

+729
-26
lines changed

doc/reference/modules/configuration.xml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,57 @@ var session = sessions.OpenSession(conn);
717717
</para>
718718
</entry>
719719
</row>
720+
<row>
721+
<entry>
722+
<literal>linqtohql.legacy_preevaluation</literal>
723+
</entry>
724+
<entry>
725+
Whether to use the legacy pre-evaluation or not in Linq queries. Defaults to <literal>true</literal>.
726+
<para>
727+
<emphasis role="strong">eg.</emphasis>
728+
<literal>true</literal> | <literal>false</literal>
729+
</para>
730+
<para>
731+
Legacy pre-evaluation is causing special properties or functions like <literal>DateTime.Now</literal>
732+
or <literal>Guid.NewGuid()</literal> to be always evaluated with the .Net runtime and replaced in the
733+
query by parameter values.
734+
</para>
735+
<para>
736+
The new pre-evaluation allows them to be converted to HQL function calls which will be run on the db
737+
side. This allows for example to retrieve the server time instead of the client time, or to generate
738+
UUIDs for each row instead of an unique one for all rows.
739+
</para>
740+
<para>
741+
The new pre-evaluation will likely be enabled by default in the next major version (6.0).
742+
</para>
743+
</entry>
744+
</row>
745+
<row>
746+
<entry>
747+
<literal>linqtohql.fallback_on_preevaluation</literal>
748+
</entry>
749+
<entry>
750+
When the new pre-evaluation is enabled, should methods which translation is not supported by the current
751+
dialect fallback to pre-evaluation? Defaults to <literal>false</literal>.
752+
<para>
753+
<emphasis role="strong">eg.</emphasis>
754+
<literal>true</literal> | <literal>false</literal>
755+
</para>
756+
<para>
757+
When this fallback option is enabled while legacy pre-evaluation is disabled, properties or functions
758+
like <literal>DateTime.Now</literal> or <literal>Guid.NewGuid()</literal> used in Linq expressions
759+
will not fail when the dialect does not support them, but will instead be pre-evaluated.
760+
</para>
761+
<para>
762+
When this fallback option is disabled while legacy pre-evaluation is disabled, properties or functions
763+
like <literal>DateTime.Now</literal> or <literal>Guid.NewGuid()</literal> used in Linq expressions
764+
will fail when the dialect does not support them.
765+
</para>
766+
<para>
767+
This option has no effect if the legacy pre-evaluation is enabled.
768+
</para>
769+
</entry>
770+
</row>
720771
<row>
721772
<entry>
722773
<literal>sql_exception_converter</literal>

src/NHibernate.Test/Async/Linq/MiscellaneousTextFixture.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ public class MiscellaneousTextFixtureAsync : LinqTestCase
2727
[Test(Description = "This sample uses Count to find the number of Orders placed before yesterday in the database.")]
2828
public async Task CountWithWhereClauseAsync()
2929
{
30-
var q = from o in db.Orders where o.OrderDate <= DateTime.Today.AddDays(-1) select o;
30+
var yesterday = DateTime.Today.AddDays(-1);
31+
var q = from o in db.Orders where o.OrderDate <= yesterday select o;
3132

3233
var count = await (q.CountAsync());
3334

src/NHibernate.Test/Linq/MiscellaneousTextFixture.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ from s in db.Shippers
2727
[Test(Description = "This sample uses Count to find the number of Orders placed before yesterday in the database.")]
2828
public void CountWithWhereClause()
2929
{
30-
var q = from o in db.Orders where o.OrderDate <= DateTime.Today.AddDays(-1) select o;
30+
var yesterday = DateTime.Today.AddDays(-1);
31+
var q = from o in db.Orders where o.OrderDate <= yesterday select o;
3132

3233
var count = q.Count();
3334

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using NHibernate.Cfg;
5+
using NHibernate.SqlTypes;
6+
using NUnit.Framework;
7+
using Environment = NHibernate.Cfg.Environment;
8+
9+
namespace NHibernate.Test.Linq
10+
{
11+
[TestFixture(false, false)]
12+
[TestFixture(true, false)]
13+
[TestFixture(false, true)]
14+
public class PreEvaluationTests : LinqTestCase
15+
{
16+
private readonly bool LegacyPreEvaluation;
17+
private readonly bool FallbackOnPreEvaluation;
18+
19+
public PreEvaluationTests(bool legacy, bool fallback)
20+
{
21+
LegacyPreEvaluation = legacy;
22+
FallbackOnPreEvaluation = fallback;
23+
}
24+
25+
protected override void Configure(Configuration configuration)
26+
{
27+
base.Configure(configuration);
28+
29+
configuration.SetProperty(Environment.FormatSql, "false");
30+
configuration.SetProperty(Environment.LinqToHqlLegacyPreEvaluation, LegacyPreEvaluation.ToString());
31+
configuration.SetProperty(Environment.LinqToHqlFallbackOnPreEvaluation, FallbackOnPreEvaluation.ToString());
32+
}
33+
34+
[Test]
35+
public void CanQueryByDateTimeNow()
36+
{
37+
var isSupported = IsFunctionSupported("current_timestamp");
38+
RunTest(
39+
isSupported,
40+
spy =>
41+
{
42+
var x = db.Orders.Count(o => o.OrderDate.Value < DateTime.Now);
43+
44+
Assert.That(x, Is.GreaterThan(0));
45+
AssertFunctionInSql("current_timestamp", spy);
46+
});
47+
}
48+
49+
[Test]
50+
public void CanSelectDateTimeNow()
51+
{
52+
var isSupported = IsFunctionSupported("current_timestamp");
53+
RunTest(
54+
isSupported,
55+
spy =>
56+
{
57+
var x =
58+
db
59+
.Orders.Select(o => new { id = o.OrderId, d = DateTime.Now })
60+
.OrderBy(o => o.id).Take(1).ToList();
61+
62+
Assert.That(x, Has.Count.GreaterThan(0));
63+
AssertFunctionInSql("current_timestamp", spy);
64+
});
65+
}
66+
67+
[Test]
68+
public void CanQueryByDateTimeUtcNow()
69+
{
70+
var isSupported = IsFunctionSupported("current_utctimestamp");
71+
RunTest(
72+
isSupported,
73+
spy =>
74+
{
75+
var x = db.Orders.Count(o => o.OrderDate.Value < DateTime.UtcNow);
76+
77+
Assert.That(x, Is.GreaterThan(0));
78+
AssertFunctionInSql("current_utctimestamp", spy);
79+
});
80+
}
81+
82+
[Test]
83+
public void CanSelectDateTimeUtcNow()
84+
{
85+
var isSupported = IsFunctionSupported("current_utctimestamp");
86+
RunTest(
87+
isSupported,
88+
spy =>
89+
{
90+
var x =
91+
db
92+
.Orders.Select(o => new { id = o.OrderId, d = DateTime.UtcNow })
93+
.OrderBy(o => o.id).Take(1).ToList();
94+
95+
Assert.That(x, Has.Count.GreaterThan(0));
96+
AssertFunctionInSql("current_utctimestamp", spy);
97+
});
98+
}
99+
100+
[Test]
101+
public void CanQueryByDateTimeToday()
102+
{
103+
var isSupported = IsFunctionSupported("current_date");
104+
RunTest(
105+
isSupported,
106+
spy =>
107+
{
108+
var x = db.Orders.Count(o => o.OrderDate.Value < DateTime.Today);
109+
110+
Assert.That(x, Is.GreaterThan(0));
111+
AssertFunctionInSql("current_date", spy);
112+
});
113+
}
114+
115+
[Test]
116+
public void CanSelectDateTimeToday()
117+
{
118+
var isSupported = IsFunctionSupported("current_date");
119+
RunTest(
120+
isSupported,
121+
spy =>
122+
{
123+
var x =
124+
db
125+
.Orders.Select(o => new { id = o.OrderId, d = DateTime.Today })
126+
.OrderBy(o => o.id).Take(1).ToList();
127+
128+
Assert.That(x, Has.Count.GreaterThan(0));
129+
AssertFunctionInSql("current_date", spy);
130+
});
131+
}
132+
133+
[Test]
134+
public void CanQueryByDateTimeOffsetTimeNow()
135+
{
136+
if (!TestDialect.SupportsSqlType(SqlTypeFactory.DateTimeOffSet))
137+
Assert.Ignore("Dialect does not support DateTimeOffSet");
138+
139+
var isSupported = IsFunctionSupported("current_timestamp_offset");
140+
RunTest(
141+
isSupported,
142+
spy =>
143+
{
144+
var testDate = DateTimeOffset.Now.AddDays(-1);
145+
var x = db.Orders.Count(o => testDate < DateTimeOffset.Now);
146+
147+
Assert.That(x, Is.GreaterThan(0));
148+
AssertFunctionInSql("current_timestamp_offset", spy);
149+
});
150+
}
151+
152+
[Test]
153+
public void CanSelectDateTimeOffsetNow()
154+
{
155+
if (!TestDialect.SupportsSqlType(SqlTypeFactory.DateTimeOffSet))
156+
Assert.Ignore("Dialect does not support DateTimeOffSet");
157+
158+
var isSupported = IsFunctionSupported("current_timestamp_offset");
159+
RunTest(
160+
isSupported,
161+
spy =>
162+
{
163+
var x =
164+
db
165+
.Orders.Select(o => new { id = o.OrderId, d = DateTimeOffset.Now })
166+
.OrderBy(o => o.id).Take(1).ToList();
167+
168+
Assert.That(x, Has.Count.GreaterThan(0));
169+
AssertFunctionInSql("current_timestamp_offset", spy);
170+
});
171+
}
172+
173+
[Test]
174+
public void CanQueryByDateTimeOffsetUtcNow()
175+
{
176+
if (!TestDialect.SupportsSqlType(SqlTypeFactory.DateTimeOffSet))
177+
Assert.Ignore("Dialect does not support DateTimeOffSet");
178+
179+
var isSupported = IsFunctionSupported("current_utctimestamp_offset");
180+
RunTest(
181+
isSupported,
182+
spy =>
183+
{
184+
var testDate = DateTimeOffset.UtcNow.AddDays(-1);
185+
var x = db.Orders.Count(o => testDate < DateTimeOffset.UtcNow);
186+
187+
Assert.That(x, Is.GreaterThan(0));
188+
AssertFunctionInSql("current_utctimestamp_offset", spy);
189+
});
190+
}
191+
192+
[Test]
193+
public void CanSelectDateTimeOffsetUtcNow()
194+
{
195+
if (!TestDialect.SupportsSqlType(SqlTypeFactory.DateTimeOffSet))
196+
Assert.Ignore("Dialect does not support DateTimeOffSet");
197+
198+
var isSupported = IsFunctionSupported("current_utctimestamp_offset");
199+
RunTest(
200+
isSupported,
201+
spy =>
202+
{
203+
var x =
204+
db
205+
.Orders.Select(o => new { id = o.OrderId, d = DateTimeOffset.UtcNow })
206+
.OrderBy(o => o.id).Take(1).ToList();
207+
208+
Assert.That(x, Has.Count.GreaterThan(0));
209+
AssertFunctionInSql("current_utctimestamp_offset", spy);
210+
});
211+
}
212+
213+
private void RunTest(bool isSupported, Action<SqlLogSpy> test)
214+
{
215+
using (var spy = new SqlLogSpy())
216+
{
217+
try
218+
{
219+
test(spy);
220+
}
221+
catch (QueryException)
222+
{
223+
if (!isSupported && !FallbackOnPreEvaluation)
224+
// Expected failure
225+
return;
226+
throw;
227+
}
228+
}
229+
230+
if (!isSupported && !FallbackOnPreEvaluation)
231+
Assert.Fail("The test should have thrown a QueryException, but has not thrown anything");
232+
}
233+
234+
private void AssertFunctionInSql(string functionName, SqlLogSpy spy)
235+
{
236+
if (!IsFunctionSupported(functionName))
237+
Assert.Inconclusive($"{functionName} is not supported by the dialect");
238+
239+
var function = Dialect.Functions[functionName].Render(new List<object>(), Sfi).ToString();
240+
241+
if (LegacyPreEvaluation)
242+
Assert.That(spy.GetWholeLog(), Does.Not.Contain(function));
243+
else
244+
Assert.That(spy.GetWholeLog(), Does.Contain(function));
245+
}
246+
}
247+
}

src/NHibernate.Test/Linq/TryGetMappedTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -773,7 +773,7 @@ private void AssertResult(
773773
expectedComponentType = expectedComponentType ?? (o => o == null);
774774

775775
var expression = query.Expression;
776-
NhRelinqQueryParser.PreTransform(expression);
776+
NhRelinqQueryParser.PreTransform(expression, Sfi);
777777
var constantToParameterMap = ExpressionParameterVisitor.Visit(expression, Sfi);
778778
var queryModel = NhRelinqQueryParser.Parse(expression);
779779
var requiredHqlParameters = new List<NamedParameterDescriptor>();

src/NHibernate.Test/TestCase.cs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -457,24 +457,40 @@ protected DateTime RoundForDialect(DateTime value)
457457
}}
458458
};
459459

460+
protected bool IsFunctionSupported(string functionName)
461+
{
462+
// We could test Sfi.SQLFunctionRegistry.HasFunction(functionName) which has the advantage of
463+
// accounting for additional functions added in configuration. But Dialect is normally never
464+
// null, while Sfi could be not yet initialized, depending from where this function is called.
465+
// Furthermore there are currently no additional functions added in configuration for NHibernate
466+
// tests.
467+
var dialect = Dialect;
468+
if (!dialect.Functions.ContainsKey(functionName))
469+
return false;
470+
471+
return !DialectsNotSupportingStandardFunction.TryGetValue(functionName, out var dialects) ||
472+
!dialects.Contains(dialect.GetType());
473+
}
474+
460475
protected void AssumeFunctionSupported(string functionName)
461476
{
462477
// We could test Sfi.SQLFunctionRegistry.HasFunction(functionName) which has the advantage of
463-
// accounting for additionnal functions added in configuration. But Dialect is normally never
478+
// accounting for additional functions added in configuration. But Dialect is normally never
464479
// null, while Sfi could be not yet initialized, depending from where this function is called.
465-
// Furtermore there are currently no additionnal functions added in configuration for NHibernate
480+
// Furthermore there are currently no additional functions added in configuration for NHibernate
466481
// tests.
482+
var dialect = Dialect;
467483
Assume.That(
468-
Dialect.Functions,
484+
dialect.Functions,
469485
Does.ContainKey(functionName),
470-
$"{Dialect} doesn't support {functionName} function.");
486+
$"{dialect} doesn't support {functionName} function.");
471487

472488
if (!DialectsNotSupportingStandardFunction.TryGetValue(functionName, out var dialects))
473489
return;
474490
Assume.That(
475491
dialects,
476-
Does.Not.Contain(Dialect.GetType()),
477-
$"{Dialect} doesn't support {functionName} standard function.");
492+
Does.Not.Contain(dialect.GetType()),
493+
$"{dialect} doesn't support {functionName} standard function.");
478494
}
479495

480496
#endregion

0 commit comments

Comments
 (0)