Skip to content

Commit e38f41b

Browse files
Support evaluation of Guid.NewGuid() on db side
Part of #959
1 parent 6ca3ed2 commit e38f41b

17 files changed

+189
-11
lines changed

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,40 @@ public async Task CanSelectDateTimeOffsetUtcNowAsync()
324324
Assert.Fail("The test should have thrown a QueryException, but has not thrown anything");
325325
}
326326

327+
[Test]
328+
public async Task CanQueryByNewGuidAsync()
329+
{
330+
if (!TestDialect.SupportsSqlType(SqlTypeFactory.Guid))
331+
Assert.Ignore("Guid are not supported by the target database");
332+
333+
using (var spy = new SqlLogSpy())
334+
{
335+
var guid = Guid.NewGuid();
336+
var x = await (db.Orders.CountAsync(o => guid != Guid.NewGuid()));
337+
338+
Assert.That(x, Is.GreaterThan(0));
339+
AssertFunctionInSql("new_uuid", spy);
340+
}
341+
}
342+
343+
[Test]
344+
public async Task CanSelectNewGuidAsync()
345+
{
346+
if (!TestDialect.SupportsSqlType(SqlTypeFactory.Guid))
347+
Assert.Ignore("Guid are not supported by the target database");
348+
349+
using (var spy = new SqlLogSpy())
350+
{
351+
var x =
352+
await (db
353+
.Orders.Select(o => new { id = o.OrderId, g = Guid.NewGuid() })
354+
.OrderBy(o => o.id).Take(1).ToListAsync());
355+
356+
Assert.That(x, Has.Count.GreaterThan(0));
357+
AssertFunctionInSql("new_uuid", spy);
358+
}
359+
}
360+
327361
private void AssertFunctionInSql(string functionName, SqlLogSpy spy)
328362
{
329363
if (!IsFunctionSupported(functionName))

src/NHibernate.Test/Linq/PreEvaluationTests.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,40 @@ public void CanSelectDateTimeOffsetUtcNow()
312312
Assert.Fail("The test should have thrown a QueryException, but has not thrown anything");
313313
}
314314

315+
[Test]
316+
public void CanQueryByNewGuid()
317+
{
318+
if (!TestDialect.SupportsSqlType(SqlTypeFactory.Guid))
319+
Assert.Ignore("Guid are not supported by the target database");
320+
321+
using (var spy = new SqlLogSpy())
322+
{
323+
var guid = Guid.NewGuid();
324+
var x = db.Orders.Count(o => guid != Guid.NewGuid());
325+
326+
Assert.That(x, Is.GreaterThan(0));
327+
AssertFunctionInSql("new_uuid", spy);
328+
}
329+
}
330+
331+
[Test]
332+
public void CanSelectNewGuid()
333+
{
334+
if (!TestDialect.SupportsSqlType(SqlTypeFactory.Guid))
335+
Assert.Ignore("Guid are not supported by the target database");
336+
337+
using (var spy = new SqlLogSpy())
338+
{
339+
var x =
340+
db
341+
.Orders.Select(o => new { id = o.OrderId, g = Guid.NewGuid() })
342+
.OrderBy(o => o.id).Take(1).ToList();
343+
344+
Assert.That(x, Has.Count.GreaterThan(0));
345+
AssertFunctionInSql("new_uuid", spy);
346+
}
347+
}
348+
315349
private void AssertFunctionInSql(string functionName, SqlLogSpy spy)
316350
{
317351
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
@@ -359,6 +359,8 @@ protected virtual void RegisterFunctions()
359359

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

364366
protected virtual void RegisterGuidTypeMapping()

src/NHibernate/Dialect/MsSqlCeDialect.cs

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

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

205207
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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ protected virtual void RegisterColumnTypes()
5454
RegisterColumnType(DbType.DateTime, "DATETIME");
5555
RegisterColumnType(DbType.Time, "TIME");
5656
RegisterColumnType(DbType.Boolean, "BOOL");
57+
// UNIQUEIDENTIFIER is not a SQLite type, but SQLite does not care much, see
58+
// https://www.sqlite.org/datatype3.html
5759
RegisterColumnType(DbType.Guid, "UNIQUEIDENTIFIER");
5860
}
5961

@@ -101,6 +103,11 @@ protected virtual void RegisterFunctions()
101103
RegisterFunction("transparentcast", new CastFunction());
102104

103105
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) "));
106+
// This does not generate a valid uuid, but SelectGUIDString property already uses this.
107+
// See https://www.ietf.org/rfc/rfc4122.txt
108+
// System.Guid does not seem to care about the uuid validity, but this may still cause
109+
// other issues.
110+
RegisterFunction("new_uuid", new SQLFunctionTemplate(NHibernateUtil.Guid, "randomblob(16)"));
104111
}
105112

106113
#region private static readonly string[] DialectKeywords = { ... }

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: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,47 @@
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

89
namespace NHibernate.Linq.Functions
910
{
10-
public interface IHqlGeneratorForMethod
11-
{
12-
IEnumerable<MethodInfo> SupportedMethods { get; }
13-
HqlTreeNode BuildHql(MethodInfo method, Expression targetObject, ReadOnlyCollection<Expression> arguments, HqlTreeBuilder treeBuilder, IHqlExpressionVisitor visitor);
14-
}
15-
}
11+
public interface IHqlGeneratorForMethod
12+
{
13+
IEnumerable<MethodInfo> SupportedMethods { get; }
14+
15+
HqlTreeNode BuildHql(
16+
MethodInfo method,
17+
Expression targetObject,
18+
ReadOnlyCollection<Expression> arguments,
19+
HqlTreeBuilder treeBuilder,
20+
IHqlExpressionVisitor visitor);
21+
}
22+
23+
// 6.0 TODO: merge into IHqlGeneratorForMethod
24+
public static class HqlGeneratorForMethodExtensions
25+
{
26+
/// <summary>
27+
/// Should pre-evaluation be allowed for this method?
28+
/// </summary>
29+
/// <param name="generator">The method's HQL generator.</param>
30+
/// <param name="member">The method.</param>
31+
/// <param name="factory">The session factory.</param>
32+
/// <returns>
33+
/// <see langword="true" /> if the method should be evaluated before running the query whenever possible,
34+
/// <see langword="false" /> if it must always be translated to the equivalent HQL call.
35+
/// </returns>
36+
public static bool AllowPreEvaluation(
37+
this IHqlGeneratorForMethod generator,
38+
MemberInfo member,
39+
ISessionFactoryImplementor factory)
40+
{
41+
if (generator is IAllowPreEvaluationHqlGenerator allowPreEvalGenerator)
42+
return allowPreEvalGenerator.AllowPreEvaluation(member, factory);
43+
44+
// By default, everything should be pre-evaluated whenever possible.
45+
return true;
46+
}
47+
}
48+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
10+
namespace NHibernate.Linq.Functions
11+
{
12+
public class NewGuidHqlGenerator : BaseHqlGeneratorForMethod, IAllowPreEvaluationHqlGenerator
13+
{
14+
public NewGuidHqlGenerator()
15+
{
16+
SupportedMethods = new[]
17+
{
18+
ReflectHelper.GetMethod(() => Guid.NewGuid())
19+
};
20+
}
21+
22+
public override HqlTreeNode BuildHql(
23+
MethodInfo method,
24+
Expression targetObject,
25+
ReadOnlyCollection<Expression> arguments,
26+
HqlTreeBuilder treeBuilder,
27+
IHqlExpressionVisitor visitor)
28+
{
29+
return treeBuilder.MethodCall("new_uuid");
30+
}
31+
32+
public bool AllowPreEvaluation(MemberInfo member, ISessionFactoryImplementor factory)
33+
{
34+
// When not supported, allow pre-evaluation on client side as a fallback.
35+
return !factory.Dialect.Functions.ContainsKey("new_uuid");
36+
}
37+
}
38+
}

src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs

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

0 commit comments

Comments
 (0)