Skip to content

Commit 58c815e

Browse files
Dmitry Naumovhazzik
authored andcommitted
NH-2408 - Fix pessimistic locking of union subclasses in Microsoft SQL Server dialects
1 parent 3f1aa07 commit 58c815e

File tree

6 files changed

+216
-42
lines changed

6 files changed

+216
-42
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using System.Collections.Generic;
2+
using NHibernate.Dialect;
3+
using NHibernate.SqlCommand;
4+
using NUnit.Framework;
5+
6+
namespace NHibernate.Test.DialectTest
7+
{
8+
[TestFixture]
9+
public class LockHintAppenderFixture
10+
{
11+
private const string MsSql2000LockHint = " with (updlock, rowlock)";
12+
private MsSql2000Dialect.LockHintAppender _appender;
13+
14+
[SetUp]
15+
public void SetUp()
16+
{
17+
_appender = new MsSql2000Dialect.LockHintAppender(new MsSql2000Dialect(), new Dictionary<string, LockMode> { {"person", LockMode.Upgrade} });
18+
}
19+
20+
[Test]
21+
public void AppendHintToSingleTableAlias()
22+
{
23+
const string expectedQuery1 = "select * from Person person with (updlock, rowlock)";
24+
const string expectedQuery2 = "select * from Person as person with (updlock, rowlock)";
25+
26+
var result1 = _appender.AppendLockHint(new SqlString(expectedQuery1.Replace(MsSql2000LockHint, string.Empty)));
27+
Assert.That(result1.ToString(), Is.EqualTo(expectedQuery1));
28+
29+
var result2 = _appender.AppendLockHint(new SqlString(expectedQuery2.Replace(MsSql2000LockHint, string.Empty)));
30+
Assert.That(result2.ToString(), Is.EqualTo(expectedQuery2));
31+
}
32+
33+
[Test]
34+
public void AppendHintToJoinedTableAlias()
35+
{
36+
const string expectedQuery =
37+
"select * from Person person with (updlock, rowlock) inner join Country country on person.Id = country.Id";
38+
39+
var result = _appender.AppendLockHint(new SqlString(expectedQuery.Replace(MsSql2000LockHint, string.Empty)));
40+
Assert.That(result.ToString(), Is.EqualTo(expectedQuery));
41+
}
42+
43+
[Test]
44+
public void AppendHintToUnionTableAlias()
45+
{
46+
const string expectedQuery =
47+
"select Id, Name from (select Id, CONCAT(FirstName, LastName) from Employee with (updlock, rowlock) union all select Id, CONCAT(FirstName, LastName) from Manager with (updlock, rowlock)) as person";
48+
49+
var result = _appender.AppendLockHint(new SqlString(expectedQuery.Replace(MsSql2000LockHint, string.Empty)));
50+
Assert.That(result.ToString(), Is.EqualTo(expectedQuery));
51+
}
52+
53+
[Test]
54+
public void ShouldIgnoreCasing()
55+
{
56+
const string expectedQuery =
57+
"select Id, Name FROM (select Id, Name FROM Employee with (updlock, rowlock) union all select Id, Name from Manager with (updlock, rowlock)) as person";
58+
59+
var result = _appender.AppendLockHint(new SqlString(expectedQuery.Replace(MsSql2000LockHint, string.Empty)));
60+
Assert.That(result.ToString(), Is.EqualTo(expectedQuery));
61+
}
62+
63+
[Test]
64+
public void ShouldHandleEscapingInSubselect()
65+
{
66+
const string expectedQuery =
67+
"select Id, Name from (select Id, Name from [Employee] with (updlock, rowlock) union all select Id, Name from [Manager] with (updlock, rowlock)) as person";
68+
69+
var result = _appender.AppendLockHint(new SqlString(expectedQuery.Replace(MsSql2000LockHint, string.Empty)));
70+
Assert.That(result.ToString(), Is.EqualTo(expectedQuery));
71+
}
72+
73+
[Test]
74+
public void ShouldHandleMultilineQuery()
75+
{
76+
const string expectedQuery = @"
77+
select Id, Name from
78+
(select Id, Name from Employee with (updlock, rowlock) union all
79+
select Id, Name from Manager with (updlock, rowlock))
80+
as person";
81+
82+
var result = _appender.AppendLockHint(new SqlString(expectedQuery.Replace(MsSql2000LockHint, string.Empty)));
83+
Assert.That(result.ToString(), Is.EqualTo(expectedQuery));
84+
}
85+
}
86+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using NUnit.Framework;
2+
3+
namespace NHibernate.Test.NHSpecificTest.NH2408
4+
{
5+
public class Fixture : BugTestCase
6+
{
7+
[Test]
8+
public void ShouldGenerateCorrectSqlStatement()
9+
{
10+
using (var session = OpenSession())
11+
{
12+
var query = session.CreateQuery("from Animal a where a.Name = ?");
13+
query.SetParameter(0, "Prince");
14+
15+
query.SetLockMode("a", LockMode.Upgrade);
16+
17+
Assert.DoesNotThrow(() => query.List());
18+
}
19+
}
20+
}
21+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
3+
namespace="NHibernate.Test.NHSpecificTest.NH2408"
4+
assembly="NHibernate.Test"
5+
>
6+
<class name="Animal">
7+
<id name="Id">
8+
<generator class="increment"/>
9+
</id>
10+
<property name="Name"/>
11+
12+
<union-subclass name="Dog" table="`Dog`">
13+
</union-subclass>
14+
15+
<union-subclass name="Cat">
16+
</union-subclass>
17+
18+
</class>
19+
20+
</hibernate-mapping>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace NHibernate.Test.NHSpecificTest.NH2408
2+
{
3+
public class Animal
4+
{
5+
public virtual int Id { get; set; }
6+
7+
public virtual string Name { get; set; }
8+
}
9+
10+
public class Dog : Animal
11+
{
12+
}
13+
14+
public class Cat : Animal
15+
{
16+
}
17+
}

src/NHibernate.Test/NHibernate.Test.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@
218218
<Compile Include="Criteria\Reptile.cs" />
219219
<Compile Include="DialectTest\FunctionTests\SubstringSupportFixture.cs" />
220220
<Compile Include="DialectTest\FunctionTests\SequenceSupportFixture.cs" />
221+
<Compile Include="DialectTest\LockHintAppenderFixture.cs" />
221222
<Compile Include="DialectTest\MsSqlCe40DialectFixture.cs" />
222223
<Compile Include="DialectTest\SchemaTests\ColumnMetaDataFixture.cs" />
223224
<Compile Include="DriverTest\DbProviderFactoryDriveConnectionCommandProviderTest.cs" />
@@ -672,6 +673,8 @@
672673
<Compile Include="NHSpecificTest\NH2297\Entity.cs" />
673674
<Compile Include="NHSpecificTest\NH2297\Fixture.cs" />
674675
<Compile Include="NHSpecificTest\NH2297\InvalidCustomCompositeUserTypeBase.cs" />
676+
<Compile Include="NHSpecificTest\NH2408\Fixture.cs" />
677+
<Compile Include="NHSpecificTest\NH2408\Model.cs" />
675678
<Compile Include="NHSpecificTest\NH3324\ChildEntity.cs" />
676679
<Compile Include="NHSpecificTest\NH3324\Entity.cs" />
677680
<Compile Include="NHSpecificTest\NH3324\FixtureByCode.cs" />
@@ -2879,6 +2882,7 @@
28792882
</ItemGroup>
28802883
<ItemGroup>
28812884
<EmbeddedResource Include="NHSpecificTest\NH3408\Mappings.hbm.xml" />
2885+
<EmbeddedResource Include="NHSpecificTest\NH2408\Mappings.hbm.xml" />
28822886
<Content Include="NHSpecificTest\NH3324\Mappings.hbm.xml" />
28832887
<EmbeddedResource Include="NHSpecificTest\NH2297\MappingsNames.hbm.xml" />
28842888
<EmbeddedResource Include="NHSpecificTest\NH2297\MappingsTypes.hbm.xml" />

src/NHibernate/Dialect/MsSql2000Dialect.cs

Lines changed: 68 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -426,25 +426,6 @@ public override string AppendLockHint(LockMode lockMode, string tableName)
426426
return tableName;
427427
}
428428

429-
private struct LockHintAppender
430-
{
431-
private readonly MsSql2000Dialect dialect;
432-
private readonly IDictionary<string, LockMode> aliasedLockModes;
433-
434-
public LockHintAppender(MsSql2000Dialect dialect, IDictionary<string, LockMode> aliasedLockModes)
435-
{
436-
this.dialect = dialect;
437-
this.aliasedLockModes = aliasedLockModes;
438-
}
439-
440-
public string ReplaceMatch(Match match)
441-
{
442-
string alias = match.Groups[1].Value;
443-
string lockHint = dialect.AppendLockHint(aliasedLockModes[alias], alias);
444-
return string.Concat(" ", lockHint, match.Groups[2].Value);
445-
}
446-
}
447-
448429
public override SqlString ApplyLocksToSql(SqlString sql, IDictionary<string, LockMode> aliasedLockModes, IDictionary<string, string[]> keyColumnNames)
449430
{
450431
bool doWork = false;
@@ -463,29 +444,7 @@ public override SqlString ApplyLocksToSql(SqlString sql, IDictionary<string, Loc
463444
return sql;
464445
}
465446

466-
// Regex matching any alias out of those given. Aliases should contain
467-
// no dangerous characters (they are identifiers) so they are not escaped.
468-
string aliasesPattern = StringHelper.Join("|", aliasedLockModes.Keys);
469-
470-
// Match < alias >, < alias,>, or < alias$>, the intent is to capture alias names
471-
// in various kinds of "FROM table1 alias1, table2 alias2".
472-
Regex matchRegex = new Regex(" (" + aliasesPattern + ")([, ]|$)");
473-
474-
SqlStringBuilder result = new SqlStringBuilder();
475-
MatchEvaluator evaluator = new LockHintAppender(this, aliasedLockModes).ReplaceMatch;
476-
477-
foreach (object part in sql.Parts)
478-
{
479-
if (part == Parameter.Placeholder)
480-
{
481-
result.Add((Parameter)part);
482-
continue;
483-
}
484-
485-
result.Add(matchRegex.Replace((string)part, evaluator));
486-
}
487-
488-
return result.ToSqlString();
447+
return new LockHintAppender(this, aliasedLockModes).AppendLockHint(sql);
489448
}
490449

491450
public override long TimestampResolutionInTicks
@@ -557,5 +516,72 @@ public override bool IsKnownToken(string currentToken, string nextToken)
557516
{
558517
return currentToken == "n" && nextToken == "'"; // unicode character
559518
}
519+
520+
public struct LockHintAppender
521+
{
522+
private static readonly Regex FromClauseTableNameRegex = new Regex(@"from\s+\[?(\w+)\]?", RegexOptions.IgnoreCase | RegexOptions.Multiline);
523+
524+
private readonly MsSql2000Dialect _dialect;
525+
private readonly IDictionary<string, LockMode> _aliasedLockModes;
526+
527+
private readonly Regex _matchRegex;
528+
private readonly Regex _unionSubclassRegex;
529+
530+
public LockHintAppender(MsSql2000Dialect dialect, IDictionary<string, LockMode> aliasedLockModes)
531+
{
532+
_dialect = dialect;
533+
_aliasedLockModes = aliasedLockModes;
534+
535+
// Regex matching any alias out of those given. Aliases should contain
536+
// no dangerous characters (they are identifiers) so they are not escaped.
537+
var aliasesPattern = StringHelper.Join("|", aliasedLockModes.Keys);
538+
539+
// Match < alias >, < alias,>, or < alias$>, the intent is to capture alias names
540+
// in various kinds of "FROM table1 alias1, table2 alias2".
541+
_matchRegex = new Regex(" (" + aliasesPattern + ")([, ]|$)");
542+
_unionSubclassRegex = new Regex(@"from\s+\(((?:.|\r|\n)*)\)(?:\s+as)?\s+(?<alias>" + aliasesPattern + ")", RegexOptions.IgnoreCase | RegexOptions.Multiline);
543+
}
544+
545+
public SqlString AppendLockHint(SqlString sql)
546+
{
547+
var result = new SqlStringBuilder();
548+
549+
foreach (object part in sql.Parts)
550+
{
551+
if (part == Parameter.Placeholder)
552+
{
553+
result.Add((Parameter)part);
554+
continue;
555+
}
556+
557+
result.Add(ProcessUnionSubclassCase((string)part) ?? _matchRegex.Replace((string)part, ReplaceMatch));
558+
}
559+
560+
return result.ToSqlString();
561+
}
562+
563+
private string ProcessUnionSubclassCase(string part)
564+
{
565+
var unionMatch = _unionSubclassRegex.Match(part);
566+
if (!unionMatch.Success)
567+
{
568+
return null;
569+
}
570+
571+
var alias = unionMatch.Groups["alias"].Value;
572+
var lockMode = _aliasedLockModes[alias];
573+
var @this = this;
574+
var replacement = FromClauseTableNameRegex.Replace(unionMatch.Value, m => @this._dialect.AppendLockHint(lockMode, m.Value));
575+
576+
return _unionSubclassRegex.Replace(part, replacement);
577+
}
578+
579+
private string ReplaceMatch(Match match)
580+
{
581+
string alias = match.Groups[1].Value;
582+
string lockHint = _dialect.AppendLockHint(_aliasedLockModes[alias], alias);
583+
return string.Concat(" ", lockHint, match.Groups[2].Value); // TODO: seems like this line is redundant
584+
}
585+
}
560586
}
561587
}

0 commit comments

Comments
 (0)