Skip to content

Commit 58272eb

Browse files
Support evaluation of Guid.NewGuid() on db side
Part of #959 Co-authored-by: maca88 <[email protected]>
1 parent d84b43e commit 58272eb

16 files changed

+151
-5
lines changed

src/NHibernate.Test/Linq/PreEvaluationTests.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,46 @@ private void RunTest(bool isSupported, Action<SqlLogSpy> test)
251251
Assert.Fail("The test should have thrown a QueryException, but has not thrown anything");
252252
}
253253

254+
[Test]
255+
public void CanQueryByNewGuid()
256+
{
257+
if (!TestDialect.SupportsSqlType(SqlTypeFactory.Guid))
258+
Assert.Ignore("Guid are not supported by the target database");
259+
260+
var isSupported = IsFunctionSupported("new_uuid");
261+
RunTest(
262+
isSupported,
263+
spy =>
264+
{
265+
var guid = Guid.NewGuid();
266+
var x = db.Orders.Count(o => guid != Guid.NewGuid());
267+
268+
Assert.That(x, Is.GreaterThan(0));
269+
AssertFunctionInSql("new_uuid", spy);
270+
});
271+
}
272+
273+
[Test]
274+
public void CanSelectNewGuid()
275+
{
276+
if (!TestDialect.SupportsSqlType(SqlTypeFactory.Guid))
277+
Assert.Ignore("Guid are not supported by the target database");
278+
279+
var isSupported = IsFunctionSupported("new_uuid");
280+
RunTest(
281+
isSupported,
282+
spy =>
283+
{
284+
var x =
285+
db
286+
.Orders.Select(o => new { id = o.OrderId, g = Guid.NewGuid() })
287+
.OrderBy(o => o.id).Take(1).ToList();
288+
289+
Assert.That(x, Has.Count.GreaterThan(0));
290+
AssertFunctionInSql("new_uuid", spy);
291+
});
292+
}
293+
254294
private void AssertFunctionInSql(string functionName, SqlLogSpy spy)
255295
{
256296
if (!IsFunctionSupported(functionName))

src/NHibernate/Dialect/FirebirdDialect.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ private void OverrideStandardHQLFunctions()
423423
RegisterFunction("strguid", new StandardSQLFunction("uuid_to_char", NHibernateUtil.String));
424424
RegisterFunction("sysdate", new CastedFunction("today", NHibernateUtil.Date));
425425
RegisterFunction("date", new SQLFunctionTemplate(NHibernateUtil.Date, "cast(?1 as date)"));
426+
RegisterFunction("new_uuid", new NoArgSQLFunction("gen_uuid", NHibernateUtil.Guid));
426427
// Bitwise operations
427428
RegisterFunction("band", new Function.BitwiseFunctionOperation("bin_and"));
428429
RegisterFunction("bor", new Function.BitwiseFunctionOperation("bin_or"));

src/NHibernate/Dialect/HanaDialectBase.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ protected virtual void RegisterNHibernateFunctions()
395395
RegisterFunction("iif", new SQLFunctionTemplate(null, "case when ?1 then ?2 else ?3 end"));
396396
RegisterFunction("sysdate", new NoArgSQLFunction("current_timestamp", NHibernateUtil.DateTime, false));
397397
RegisterFunction("truncate", new SQLFunctionTemplateWithRequiredParameters(null, "floor(?1 * power(10, ?2)) / power(10, ?2)", new object[] { null, "0" }));
398+
RegisterFunction("new_uuid", new NoArgSQLFunction("sysuuid", NHibernateUtil.Guid, false));
398399
}
399400

400401
protected virtual void RegisterHANAFunctions()

src/NHibernate/Dialect/MsSql2000Dialect.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,8 @@ protected virtual void RegisterFunctions()
360360

361361
RegisterFunction("bit_length", new SQLFunctionTemplate(NHibernateUtil.Int32, "datalength(?1) * 8"));
362362
RegisterFunction("extract", new SQLFunctionTemplate(NHibernateUtil.Int32, "datepart(?1, ?3)"));
363+
364+
RegisterFunction("new_uuid", new NoArgSQLFunction("newid", NHibernateUtil.Guid));
363365
}
364366

365367
protected virtual void RegisterGuidTypeMapping()

src/NHibernate/Dialect/MsSqlCeDialect.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,8 @@ protected virtual void RegisterFunctions()
201201

202202
RegisterFunction("bit_length", new SQLFunctionTemplate(NHibernateUtil.Int32, "datalength(?1) * 8"));
203203
RegisterFunction("extract", new SQLFunctionTemplate(NHibernateUtil.Int32, "datepart(?1, ?3)"));
204+
205+
RegisterFunction("new_uuid", new NoArgSQLFunction("newid", NHibernateUtil.Guid));
204206
}
205207

206208
protected virtual void RegisterDefaultProperties()

src/NHibernate/Dialect/MySQL5Dialect.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@ public MySQL5Dialect()
1212
// My SQL supports precision up to 65, but .Net is limited to 28-29.
1313
RegisterColumnType(DbType.Decimal, 29, "DECIMAL($p, $s)");
1414
RegisterColumnType(DbType.Guid, "BINARY(16)");
15+
}
16+
17+
protected override void RegisterFunctions()
18+
{
19+
base.RegisterFunctions();
1520

1621
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)))"));
22+
RegisterFunction("new_uuid", new NoArgSQLFunction("uuid", NHibernateUtil.Guid));
1723
}
1824

1925
protected override void RegisterCastTypes()

src/NHibernate/Dialect/Oracle8iDialect.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,8 @@ protected virtual void RegisterFunctions()
310310
RegisterFunction("bor", new SQLFunctionTemplate(null, "?1 + ?2 - BITAND(?1, ?2)"));
311311
RegisterFunction("bxor", new SQLFunctionTemplate(null, "?1 + ?2 - BITAND(?1, ?2) * 2"));
312312
RegisterFunction("bnot", new SQLFunctionTemplate(null, "(-1 - ?1)"));
313+
314+
RegisterFunction("new_uuid", new NoArgSQLFunction("sys_guid", NHibernateUtil.Guid));
313315
}
314316

315317
protected internal virtual void RegisterDefaultProperties()

src/NHibernate/Dialect/PostgreSQLDialect.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ public PostgreSQLDialect()
9898

9999
RegisterFunction("strguid", new SQLFunctionTemplate(NHibernateUtil.String, "?1::TEXT"));
100100

101+
// The uuid_generate_v4 is not native and must be installed, but SelectGUIDString property already uses it,
102+
// and NHibernate.TestDatabaseSetup does install it.
103+
RegisterFunction("new_uuid", new NoArgSQLFunction("uuid_generate_v4", NHibernateUtil.Guid));
104+
101105
RegisterKeywords();
102106
}
103107

src/NHibernate/Dialect/SQLiteDialect.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ protected virtual void RegisterColumnTypes()
6161
RegisterColumnType(DbType.DateTime, "DATETIME");
6262
RegisterColumnType(DbType.Time, "TIME");
6363
RegisterColumnType(DbType.Boolean, "BOOL");
64+
// UNIQUEIDENTIFIER is not a SQLite type, but SQLite does not care much, see
65+
// https://www.sqlite.org/datatype3.html
6466
RegisterColumnType(DbType.Guid, "UNIQUEIDENTIFIER");
6567
}
6668

src/NHibernate/Dialect/SybaseASE15Dialect.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ public SybaseASE15Dialect()
5656
RegisterColumnType(DbType.Date, "date");
5757
RegisterColumnType(DbType.Binary, 8000, "varbinary($l)");
5858
RegisterColumnType(DbType.Binary, "varbinary");
59+
// newid default is to generate a 32 bytes character uuid (no-dashes), but it has an option for
60+
// including dashes, then raising it to 36 bytes.
61+
RegisterColumnType(DbType.Guid, "varchar(36)");
5962

6063
RegisterFunction("abs", new StandardSQLFunction("abs"));
6164
RegisterFunction("acos", new StandardSQLFunction("acos", NHibernateUtil.Double));
@@ -113,6 +116,8 @@ public SybaseASE15Dialect()
113116
RegisterFunction("year", new StandardSQLFunction("year", NHibernateUtil.Int32));
114117

115118
RegisterFunction("substring", new EmulatedLengthSubstringFunction());
119+
120+
RegisterFunction("new_uuid", new NoArgSQLFunction("newid", NHibernateUtil.Guid));
116121
}
117122

118123
public override string AddColumnString

src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ protected virtual void RegisterMiscellaneousFunctions()
338338
RegisterFunction("isnull", new VarArgsSQLFunction("isnull(", ",", ")"));
339339
RegisterFunction("lesser", new StandardSQLFunction("lesser"));
340340
RegisterFunction("newid", new NoArgSQLFunction("newid", NHibernateUtil.String, true));
341+
RegisterFunction("new_uuid", new NoArgSQLFunction("newid", NHibernateUtil.Guid));
341342
RegisterFunction("nullif", new StandardSQLFunction("nullif"));
342343
RegisterFunction("number", new NoArgSQLFunction("number", NHibernateUtil.Int32));
343344
RegisterFunction("plan", new VarArgsSQLFunction(NHibernateUtil.String, "plan(", ",", ")"));

src/NHibernate/Linq/Functions/DefaultLinqToHqlGeneratorsRegistry.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ public DefaultLinqToHqlGeneratorsRegistry()
5858
this.Merge(new DateTimePropertiesHqlGenerator());
5959
this.Merge(new DateTimeNowHqlGenerator());
6060

61+
this.Merge(new NewGuidHqlGenerator());
62+
6163
this.Merge(new DecimalAddGenerator());
6264
this.Merge(new DecimalDivideGenerator());
6365
this.Merge(new DecimalMultiplyGenerator());

src/NHibernate/Linq/Functions/IAllowPreEvaluationHqlGenerator.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ namespace NHibernate.Linq.Functions
77
public interface IAllowPreEvaluationHqlGenerator
88
{
99
/// <summary>
10-
/// Should pre-evaluation be allowed for this property?
10+
/// Should pre-evaluation be allowed for this property or method?
1111
/// </summary>
12-
/// <param name="member">The property.</param>
12+
/// <param name="member">The property or method.</param>
1313
/// <param name="factory">The session factory.</param>
1414
/// <returns>
15-
/// <see langword="true" /> if the property should be evaluated before running the query whenever possible,
15+
/// <see langword="true" /> if the property or method should be evaluated before running the query whenever possible,
1616
/// <see langword="false" /> if it must always be translated to the equivalent HQL call.
1717
/// </returns>
1818
/// <remarks>Implementors should return <see langword="true" /> by default. Returning <see langword="false" />

src/NHibernate/Linq/Functions/IHqlGeneratorForMethod.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.ObjectModel;
33
using System.Linq.Expressions;
44
using System.Reflection;
5+
using NHibernate.Engine;
56
using NHibernate.Hql.Ast;
67
using NHibernate.Linq.Visitors;
78

@@ -31,5 +32,28 @@ public static bool AllowsNullableReturnType(this IHqlGeneratorForMethod generato
3132

3233
return true;
3334
}
35+
36+
// 6.0 TODO: merge into IHqlGeneratorForMethod
37+
/// <summary>
38+
/// Should pre-evaluation be allowed for this method?
39+
/// </summary>
40+
/// <param name="generator">The method's HQL generator.</param>
41+
/// <param name="member">The method.</param>
42+
/// <param name="factory">The session factory.</param>
43+
/// <returns>
44+
/// <see langword="true" /> if the method should be evaluated before running the query whenever possible,
45+
/// <see langword="false" /> if it must always be translated to the equivalent HQL call.
46+
/// </returns>
47+
public static bool AllowPreEvaluation(
48+
this IHqlGeneratorForMethod generator,
49+
MemberInfo member,
50+
ISessionFactoryImplementor factory)
51+
{
52+
if (generator is IAllowPreEvaluationHqlGenerator allowPreEvalGenerator)
53+
return allowPreEvalGenerator.AllowPreEvaluation(member, factory);
54+
55+
// By default, everything should be pre-evaluated whenever possible.
56+
return true;
57+
}
3458
}
3559
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.Collections.ObjectModel;
3+
using System.Linq.Expressions;
4+
using System.Reflection;
5+
using NHibernate.Engine;
6+
using NHibernate.Hql.Ast;
7+
using NHibernate.Linq.Visitors;
8+
using NHibernate.Util;
9+
using Environment = NHibernate.Cfg.Environment;
10+
11+
namespace NHibernate.Linq.Functions
12+
{
13+
public class NewGuidHqlGenerator : BaseHqlGeneratorForMethod, IAllowPreEvaluationHqlGenerator
14+
{
15+
public NewGuidHqlGenerator()
16+
{
17+
SupportedMethods = new[]
18+
{
19+
ReflectHelper.GetMethod(() => Guid.NewGuid())
20+
};
21+
}
22+
23+
public override HqlTreeNode BuildHql(
24+
MethodInfo method,
25+
Expression targetObject,
26+
ReadOnlyCollection<Expression> arguments,
27+
HqlTreeBuilder treeBuilder,
28+
IHqlExpressionVisitor visitor)
29+
{
30+
return treeBuilder.MethodCall("new_uuid");
31+
}
32+
33+
public bool AllowPreEvaluation(MemberInfo member, ISessionFactoryImplementor factory)
34+
{
35+
if (factory.Dialect.Functions.ContainsKey("new_uuid"))
36+
return false;
37+
38+
if (factory.Settings.LinqToHqlFallbackOnPreEvaluation)
39+
return true;
40+
41+
throw new QueryException(
42+
"Cannot translate NewGuid: new_uuid is " +
43+
$"not supported by {factory.Dialect}. Either enable the fallback on pre-evaluation " +
44+
$"({Environment.LinqToHqlFallbackOnPreEvaluation}) or evaluate NewGuid " +
45+
"outside of the query.");
46+
}
47+
}
48+
}

src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,14 @@ public override bool IsEvaluatableMethodCall(MethodCallExpression node)
8888
var attributes = node.Method
8989
.GetCustomAttributes(typeof(LinqExtensionMethodAttributeBase), false)
9090
.ToArray(x => (LinqExtensionMethodAttributeBase) x);
91-
return attributes.Length == 0 ||
92-
attributes.Any(a => a.PreEvaluation == LinqExtensionPreEvaluation.AllowPreEvaluation);
91+
if (attributes.Length > 0)
92+
return attributes.Any(a => a.PreEvaluation == LinqExtensionPreEvaluation.AllowPreEvaluation);
93+
94+
if (_sessionFactory == null || _sessionFactory.Settings.LinqToHqlLegacyPreEvaluation ||
95+
!_sessionFactory.Settings.LinqToHqlGeneratorsRegistry.TryGetGenerator(node.Method, out var generator))
96+
return true;
97+
98+
return generator.AllowPreEvaluation(node.Method, _sessionFactory);
9399
}
94100
}
95101
}

0 commit comments

Comments
 (0)