Skip to content

Commit 5b12fbf

Browse files
committed
Correctly resolve referenced query source for the lock
- Add overload to support locking on joins - Rename SetLockMode to WithLock - Restrict tests only to dialects that do support locks - Ignore paging tests if dialect does not support combining locks with paging
1 parent e1ff88d commit 5b12fbf

15 files changed

+248
-25
lines changed

src/NHibernate.Test/Linq/QueryLock.cs

Lines changed: 139 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
using System.Linq;
1+
using System;
2+
using System.Linq;
23
using System.Transactions;
34
using NHibernate.Dialect;
45
using NHibernate.DomainModel.Northwind.Entities;
6+
using NHibernate.Driver;
7+
using NHibernate.Engine;
58
using NHibernate.Exceptions;
69
using NHibernate.Linq;
710
using NUnit.Framework;
@@ -11,47 +14,176 @@ namespace NHibernate.Test.Linq
1114
{
1215
public class QueryLock : LinqTestCase
1316
{
17+
protected override bool AppliesTo(Dialect.Dialect dialect)
18+
{
19+
return TestDialect.SupportsSelectForUpdate;
20+
}
21+
22+
protected override bool AppliesTo(ISessionFactoryImplementor factory)
23+
{
24+
return !(factory.ConnectionProvider.Driver is OdbcDriver);
25+
}
26+
1427
[Test]
15-
public void CanSetLockLinqQueries()
28+
public void CanSetLockLinqQueriesOuter()
1629
{
1730
using (session.BeginTransaction())
1831
{
1932
var result = (from e in db.Customers
20-
select e).SetLockMode(LockMode.Upgrade).ToList();
33+
select e).WithLock(LockMode.Upgrade).ToList();
2134

2235
Assert.That(result, Has.Count.EqualTo(91));
2336
Assert.That(session.GetCurrentLockMode(result[0]), Is.EqualTo(LockMode.Upgrade));
2437
AssertSeparateTransactionIsLockedOut(result[0].CustomerId);
2538
}
2639
}
2740

41+
[Test]
42+
public void CanSetLockLinqQueries()
43+
{
44+
using (session.BeginTransaction())
45+
{
46+
var result = (from e in db.Customers.WithLock(LockMode.Upgrade)
47+
select e).ToList();
48+
49+
Assert.That(result, Has.Count.EqualTo(91));
50+
Assert.That(session.GetCurrentLockMode(result[0]), Is.EqualTo(LockMode.Upgrade));
51+
AssertSeparateTransactionIsLockedOut(result[0].CustomerId);
52+
}
53+
}
54+
55+
[Test]
56+
public void CanSetLockOnJoinHql()
57+
{
58+
using (session.BeginTransaction())
59+
{
60+
session
61+
.CreateQuery("select o from Customer c join c.Orders o")
62+
.SetLockMode("o", LockMode.Upgrade)
63+
.List();
64+
}
65+
}
66+
67+
[Test]
68+
public void CanSetLockOnJoin()
69+
{
70+
using (session.BeginTransaction())
71+
{
72+
var result = (from c in db.Customers
73+
from o in c.Orders.WithLock(LockMode.Upgrade)
74+
select o).ToList();
75+
76+
Assert.That(result, Has.Count.EqualTo(830));
77+
Assert.That(session.GetCurrentLockMode(result[0]), Is.EqualTo(LockMode.Upgrade));
78+
}
79+
}
80+
81+
[Test]
82+
public void CanSetLockOnJoinOuter()
83+
{
84+
using (session.BeginTransaction())
85+
{
86+
var result = (from c in db.Customers
87+
from o in c.Orders
88+
select o).WithLock(LockMode.Upgrade).ToList();
89+
90+
Assert.That(result, Has.Count.EqualTo(830));
91+
Assert.That(session.GetCurrentLockMode(result[0]), Is.EqualTo(LockMode.Upgrade));
92+
}
93+
}
94+
95+
[Test]
96+
public void CanSetLockOnJoinOuterNotSupported()
97+
{
98+
using (session.BeginTransaction())
99+
{
100+
var query = (
101+
from c in db.Customers
102+
from o in c.Orders
103+
select new {o, c}
104+
).WithLock(LockMode.Upgrade);
105+
106+
Assert.Throws<NotSupportedException>(() => query.ToList());
107+
}
108+
}
109+
110+
[Test]
111+
public void CanSetLockOnJoinOuter2Hql()
112+
{
113+
using (session.BeginTransaction())
114+
{
115+
session
116+
.CreateQuery("select o, c from Customer c join c.Orders o")
117+
.SetLockMode("o", LockMode.Upgrade)
118+
.SetLockMode("c", LockMode.Upgrade)
119+
.List();
120+
}
121+
}
122+
123+
[Test]
124+
public void CanSetLockOnBothJoinAndMain()
125+
{
126+
using (session.BeginTransaction())
127+
{
128+
var result = (
129+
from c in db.Customers.WithLock(LockMode.Upgrade)
130+
from o in c.Orders.WithLock(LockMode.Upgrade)
131+
select new {o, c}
132+
).ToList();
133+
134+
Assert.That(result, Has.Count.EqualTo(830));
135+
Assert.That(session.GetCurrentLockMode(result[0].o), Is.EqualTo(LockMode.Upgrade));
136+
Assert.That(session.GetCurrentLockMode(result[0].c), Is.EqualTo(LockMode.Upgrade));
137+
}
138+
}
139+
140+
[Test]
141+
public void CanSetLockOnBothJoinAndMainComplex()
142+
{
143+
using (session.BeginTransaction())
144+
{
145+
var result = (
146+
from c in db.Customers.Where(x => true).WithLock(LockMode.Upgrade)
147+
from o in c.Orders.Where(x => true).WithLock(LockMode.Upgrade)
148+
select new {o, c}
149+
).ToList();
150+
151+
Assert.That(result, Has.Count.EqualTo(830));
152+
Assert.That(session.GetCurrentLockMode(result[0].o), Is.EqualTo(LockMode.Upgrade));
153+
Assert.That(session.GetCurrentLockMode(result[0].c), Is.EqualTo(LockMode.Upgrade));
154+
}
155+
}
28156

29157
[Test]
30158
public void CanSetLockOnLinqPagingQuery()
31159
{
160+
Assume.That(TestDialect.SupportsSelectForUpdateWithPaging, Is.True, "Dialect does not support locking in subqueries");
32161
using (session.BeginTransaction())
33162
{
34163
var result = (from e in db.Customers
35-
select e).Skip(5).Take(5).SetLockMode(LockMode.Upgrade).ToList();
164+
select e).Skip(5).Take(5).WithLock(LockMode.Upgrade).ToList();
36165

37166
Assert.That(result, Has.Count.EqualTo(5));
38167
Assert.That(session.GetCurrentLockMode(result[0]), Is.EqualTo(LockMode.Upgrade));
168+
39169
AssertSeparateTransactionIsLockedOut(result[0].CustomerId);
40170
}
41171
}
42172

43173
[Test]
44174
public void CanLockBeforeSkipOnLinqOrderedPageQuery()
45175
{
176+
Assume.That(TestDialect.SupportsSelectForUpdateWithPaging, Is.True, "Dialect does not support locking in subqueries");
46177
using (session.BeginTransaction())
47178
{
48179
var result = (from e in db.Customers
49180
orderby e.CompanyName
50181
select e)
51-
.SetLockMode(LockMode.Upgrade).Skip(5).Take(5).ToList();
182+
.WithLock(LockMode.Upgrade).Skip(5).Take(5).ToList();
52183

53184
Assert.That(result, Has.Count.EqualTo(5));
54185
Assert.That(session.GetCurrentLockMode(result[0]), Is.EqualTo(LockMode.Upgrade));
186+
55187
AssertSeparateTransactionIsLockedOut(result[0].CustomerId);
56188
}
57189
}
@@ -70,7 +202,7 @@ private void AssertSeparateTransactionIsLockedOut(string customerId)
70202
from e in s2.Query<Customer>()
71203
where e.CustomerId == customerId
72204
select e
73-
).SetLockMode(LockMode.UpgradeNoWait)
205+
).WithLock(LockMode.UpgradeNoWait)
74206
.WithOptions(o => o.SetTimeout(5))
75207
.ToList();
76208
Assert.That(result2, Is.Not.Null);
@@ -107,7 +239,7 @@ private static IQueryable<Customer> BuildQueryableAllCustomers(
107239
IQueryable<Customer> dbCustomers,
108240
LockMode lockMode)
109241
{
110-
return (from e in dbCustomers select e).SetLockMode(lockMode).WithOptions(o => o.SetTimeout(5));
242+
return (from e in dbCustomers select e).WithLock(lockMode).WithOptions(o => o.SetTimeout(5));
111243
}
112244
}
113245
}

src/NHibernate.Test/TestDialect.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,20 @@ public bool HasIdentityNativeGenerator
4646

4747
public virtual bool SupportsNullCharactersInUtfStrings => true;
4848

49-
public virtual bool SupportsSelectForUpdateOnOuterJoin => true;
49+
/// <summary>
50+
/// Some databases do not support SELECT FOR UPDATE
51+
/// </summary>
52+
public virtual bool SupportsSelectForUpdate => true;
53+
54+
/// <summary>
55+
/// Some databases do not support SELECT FOR UPDATE with paging
56+
/// </summary>
57+
public virtual bool SupportsSelectForUpdateWithPaging => SupportsSelectForUpdate;
58+
59+
/// <summary>
60+
/// Some databases do not support SELECT FOR UPDATE in conjunction with outer joins
61+
/// </summary>
62+
public virtual bool SupportsSelectForUpdateOnOuterJoin => SupportsSelectForUpdate;
5063

5164
public virtual bool SupportsHavingWithoutGroupBy => true;
5265

src/NHibernate.Test/TestDialects/FirebirdTestDialect.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,10 @@ public FirebirdTestDialect(Dialect.Dialect dialect) : base(dialect)
1212
/// Non-integer arguments are rounded before the division takes place. So, “7.5 mod 2.5” gives 2 (8 mod 3), not 0.
1313
/// </summary>
1414
public override bool SupportsModuloOnDecimal => false;
15+
16+
/// <summary>
17+
/// Does not support update locks
18+
/// </summary>
19+
public override bool SupportsSelectForUpdate => false;
1520
}
1621
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace NHibernate.Test.TestDialects
2+
{
3+
public class MsSql2008TestDialect : TestDialect
4+
{
5+
public MsSql2008TestDialect(Dialect.Dialect dialect)
6+
: base(dialect)
7+
{
8+
}
9+
10+
/// <summary>
11+
/// Does not support SELECT FOR UPDATE with paging
12+
/// </summary>
13+
public override bool SupportsSelectForUpdateWithPaging => false;
14+
}
15+
}

src/NHibernate.Test/TestDialects/MsSqlCe40TestDialect.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,10 @@ public MsSqlCe40TestDialect(Dialect.Dialect dialect) : base(dialect)
3030
/// Modulo is not supported on real, float, money, and numeric data types. [ Data type = numeric ]
3131
/// </summary>
3232
public override bool SupportsModuloOnDecimal => false;
33+
34+
/// <summary>
35+
/// Does not support update locks
36+
/// </summary>
37+
public override bool SupportsSelectForUpdate => false;
3338
}
3439
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace NHibernate.Test.TestDialects
2+
{
3+
public class Oracle10gTestDialect : TestDialect
4+
{
5+
public Oracle10gTestDialect(Dialect.Dialect dialect) : base(dialect)
6+
{
7+
}
8+
9+
/// <summary>
10+
/// Does not support SELECT FOR UPDATE with paging
11+
/// </summary>
12+
public override bool SupportsSelectForUpdateWithPaging => false;
13+
}
14+
}

src/NHibernate.Test/TestDialects/SQLiteTestDialect.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,10 @@ public override bool SupportsHavingWithoutGroupBy
4646
}
4747

4848
public override bool SupportsModuloOnDecimal => false;
49+
50+
/// <summary>
51+
/// Does not support update locks
52+
/// </summary>
53+
public override bool SupportsSelectForUpdate => false;
4954
}
5055
}

src/NHibernate.Test/TestDialects/SapSQLAnywhere17TestDialect.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,10 @@ public SapSQLAnywhere17TestDialect(Dialect.Dialect dialect)
3838
/// <c>numeric</c>. See https://stackoverflow.com/q/52558715/1178314.
3939
/// </remarks>
4040
public override bool HasBrokenTypeInferenceOnSelectedParameters => true;
41+
42+
/// <summary>
43+
/// Does not support SELECT FOR UPDATE
44+
/// </summary>
45+
public override bool SupportsSelectForUpdate => false;
4146
}
4247
}

src/NHibernate/Linq/LinqExtensionMethods.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2514,15 +2514,21 @@ public static IQueryable<T> CacheRegion<T>(this IQueryable<T> query, string regi
25142514
public static IQueryable<T> Timeout<T>(this IQueryable<T> query, int timeout)
25152515
=> query.WithOptions(o => o.SetTimeout(timeout));
25162516

2517-
public static IQueryable<T> SetLockMode<T>(this IQueryable<T> query, LockMode lockMode)
2517+
public static IQueryable<T> WithLock<T>(this IQueryable<T> query, LockMode lockMode)
25182518
{
2519-
var method = ReflectHelper.GetMethod(() => SetLockMode(query, lockMode));
2519+
var method = ReflectHelper.GetMethod(() => WithLock(query, lockMode));
25202520

25212521
var callExpression = Expression.Call(method, query.Expression, Expression.Constant(lockMode));
25222522

25232523
return new NhQueryable<T>(query.Provider, callExpression);
25242524
}
25252525

2526+
public static IEnumerable<T> WithLock<T>(this IEnumerable<T> query, LockMode lockMode)
2527+
{
2528+
throw new InvalidOperationException(
2529+
"The NHibernate.Linq.LinqExtensionMethods.WithLock(IEnumerable<T>, LockMode) method can only be used in a Linq expression.");
2530+
}
2531+
25262532
/// <summary>
25272533
/// Allows to specify the parameter NHibernate type to use for a literal in a queryable expression.
25282534
/// </summary>
Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1+
using System;
12
using System.Linq.Expressions;
23
using Remotion.Linq.Clauses;
4+
using Remotion.Linq.Clauses.Expressions;
35
using Remotion.Linq.Parsing.Structure.IntermediateModel;
46

57
namespace NHibernate.Linq
68
{
79
internal class LockExpressionNode : ResultOperatorExpressionNodeBase
810
{
9-
private readonly MethodCallExpressionParseInfo _parseInfo;
11+
private static readonly ParameterExpression Parameter = Expression.Parameter(typeof(object));
12+
1013
private readonly ConstantExpression _lockMode;
14+
private readonly ResolvedExpressionCache<Expression> _cache;
1115

1216
public LockExpressionNode(MethodCallExpressionParseInfo parseInfo, ConstantExpression lockMode)
1317
: base(parseInfo, null, null)
1418
{
15-
_parseInfo = parseInfo;
1619
_lockMode = lockMode;
20+
_cache = new ResolvedExpressionCache<Expression>(this);
1721
}
1822

1923
public override Expression Resolve(ParameterExpression inputParameter, Expression expressionToBeResolved, ClauseGenerationContext clauseGenerationContext)
@@ -23,7 +27,15 @@ public override Expression Resolve(ParameterExpression inputParameter, Expressio
2327

2428
protected override ResultOperatorBase CreateResultOperator(ClauseGenerationContext clauseGenerationContext)
2529
{
26-
return new LockResultOperator(_parseInfo, _lockMode);
30+
//Resolve identity expression (_=>_). Normally this would be resolved into QuerySourceReferenceExpression.
31+
32+
var expression = _cache.GetOrCreate(
33+
r => r.GetResolvedExpression(Parameter, Parameter, clauseGenerationContext));
34+
35+
if (!(expression is QuerySourceReferenceExpression qsrExpression))
36+
throw new NotSupportedException($"WithLock is not supported on {expression}");
37+
38+
return new LockResultOperator(qsrExpression, _lockMode);
2739
}
2840
}
2941
}

0 commit comments

Comments
 (0)