diff --git a/src/NHibernate.Test/DialectTest/LockHintAppenderFixture.cs b/src/NHibernate.Test/DialectTest/LockHintAppenderFixture.cs new file mode 100644 index 00000000000..d176f7814cb --- /dev/null +++ b/src/NHibernate.Test/DialectTest/LockHintAppenderFixture.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using NHibernate.Dialect; +using NHibernate.SqlCommand; +using NUnit.Framework; + +namespace NHibernate.Test.DialectTest +{ + [TestFixture] + public class LockHintAppenderFixture + { + private const string MsSql2000LockHint = " with (updlock, rowlock)"; + private MsSql2000Dialect.LockHintAppender _appender; + + [SetUp] + public void SetUp() + { + _appender = new MsSql2000Dialect.LockHintAppender(new MsSql2000Dialect(), new Dictionary { {"person", LockMode.Upgrade} }); + } + + [Test] + public void AppendHintToSingleTableAlias() + { + const string expectedQuery1 = "select * from Person person with (updlock, rowlock)"; + const string expectedQuery2 = "select * from Person as person with (updlock, rowlock)"; + + var result1 = _appender.AppendLockHint(new SqlString(expectedQuery1.Replace(MsSql2000LockHint, string.Empty))); + Assert.That(result1.ToString(), Is.EqualTo(expectedQuery1)); + + var result2 = _appender.AppendLockHint(new SqlString(expectedQuery2.Replace(MsSql2000LockHint, string.Empty))); + Assert.That(result2.ToString(), Is.EqualTo(expectedQuery2)); + } + + [Test] + public void AppendHintToJoinedTableAlias() + { + const string expectedQuery = + "select * from Person person with (updlock, rowlock) inner join Country country on person.Id = country.Id"; + + var result = _appender.AppendLockHint(new SqlString(expectedQuery.Replace(MsSql2000LockHint, string.Empty))); + Assert.That(result.ToString(), Is.EqualTo(expectedQuery)); + } + + [Test] + public void AppendHintToUnionTableAlias() + { + const string expectedQuery = + "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"; + + var result = _appender.AppendLockHint(new SqlString(expectedQuery.Replace(MsSql2000LockHint, string.Empty))); + Assert.That(result.ToString(), Is.EqualTo(expectedQuery)); + } + + [Test] + public void ShouldIgnoreCasing() + { + const string expectedQuery = + "select Id, Name FROM (select Id, Name FROM Employee with (updlock, rowlock) union all select Id, Name from Manager with (updlock, rowlock)) as person"; + + var result = _appender.AppendLockHint(new SqlString(expectedQuery.Replace(MsSql2000LockHint, string.Empty))); + Assert.That(result.ToString(), Is.EqualTo(expectedQuery)); + } + + [Test] + public void ShouldHandleEscapingInSubselect() + { + const string expectedQuery = + "select Id, Name from (select Id, Name from [Employee] with (updlock, rowlock) union all select Id, Name from [Manager] with (updlock, rowlock)) as person"; + + var result = _appender.AppendLockHint(new SqlString(expectedQuery.Replace(MsSql2000LockHint, string.Empty))); + Assert.That(result.ToString(), Is.EqualTo(expectedQuery)); + } + + [Test] + public void ShouldHandleMultilineQuery() + { + const string expectedQuery = @" +select Id, Name from + (select Id, Name from Employee with (updlock, rowlock) union all + select Id, Name from Manager with (updlock, rowlock)) +as person"; + + var result = _appender.AppendLockHint(new SqlString(expectedQuery.Replace(MsSql2000LockHint, string.Empty))); + Assert.That(result.ToString(), Is.EqualTo(expectedQuery)); + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH2408/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/NH2408/Fixture.cs new file mode 100644 index 00000000000..09e97e1caaa --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH2408/Fixture.cs @@ -0,0 +1,21 @@ +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.NH2408 +{ + public class Fixture : BugTestCase + { + [Test] + public void ShouldGenerateCorrectSqlStatement() + { + using (var session = OpenSession()) + { + var query = session.CreateQuery("from Animal a where a.Name = ?"); + query.SetParameter(0, "Prince"); + + query.SetLockMode("a", LockMode.Upgrade); + + Assert.DoesNotThrow(() => query.List()); + } + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH2408/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH2408/Mappings.hbm.xml new file mode 100644 index 00000000000..161df904886 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH2408/Mappings.hbm.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH2408/Model.cs b/src/NHibernate.Test/NHSpecificTest/NH2408/Model.cs new file mode 100644 index 00000000000..85fb08cfb5b --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH2408/Model.cs @@ -0,0 +1,17 @@ +namespace NHibernate.Test.NHSpecificTest.NH2408 +{ + public class Animal + { + public virtual int Id { get; set; } + + public virtual string Name { get; set; } + } + + public class Dog : Animal + { + } + + public class Cat : Animal + { + } +} diff --git a/src/NHibernate.Test/NHibernate.Test.csproj b/src/NHibernate.Test/NHibernate.Test.csproj index 0d8e06b5f1a..35491de4fb3 100644 --- a/src/NHibernate.Test/NHibernate.Test.csproj +++ b/src/NHibernate.Test/NHibernate.Test.csproj @@ -218,6 +218,7 @@ + @@ -672,6 +673,8 @@ + + @@ -2879,6 +2882,7 @@ + diff --git a/src/NHibernate/Dialect/MsSql2000Dialect.cs b/src/NHibernate/Dialect/MsSql2000Dialect.cs index 1be077410d0..54a58bafea9 100644 --- a/src/NHibernate/Dialect/MsSql2000Dialect.cs +++ b/src/NHibernate/Dialect/MsSql2000Dialect.cs @@ -426,25 +426,6 @@ public override string AppendLockHint(LockMode lockMode, string tableName) return tableName; } - private struct LockHintAppender - { - private readonly MsSql2000Dialect dialect; - private readonly IDictionary aliasedLockModes; - - public LockHintAppender(MsSql2000Dialect dialect, IDictionary aliasedLockModes) - { - this.dialect = dialect; - this.aliasedLockModes = aliasedLockModes; - } - - public string ReplaceMatch(Match match) - { - string alias = match.Groups[1].Value; - string lockHint = dialect.AppendLockHint(aliasedLockModes[alias], alias); - return string.Concat(" ", lockHint, match.Groups[2].Value); - } - } - public override SqlString ApplyLocksToSql(SqlString sql, IDictionary aliasedLockModes, IDictionary keyColumnNames) { bool doWork = false; @@ -463,29 +444,7 @@ public override SqlString ApplyLocksToSql(SqlString sql, IDictionary, < alias,>, or < alias$>, the intent is to capture alias names - // in various kinds of "FROM table1 alias1, table2 alias2". - Regex matchRegex = new Regex(" (" + aliasesPattern + ")([, ]|$)"); - - SqlStringBuilder result = new SqlStringBuilder(); - MatchEvaluator evaluator = new LockHintAppender(this, aliasedLockModes).ReplaceMatch; - - foreach (object part in sql.Parts) - { - if (part == Parameter.Placeholder) - { - result.Add((Parameter)part); - continue; - } - - result.Add(matchRegex.Replace((string)part, evaluator)); - } - - return result.ToSqlString(); + return new LockHintAppender(this, aliasedLockModes).AppendLockHint(sql); } public override long TimestampResolutionInTicks @@ -557,5 +516,72 @@ public override bool IsKnownToken(string currentToken, string nextToken) { return currentToken == "n" && nextToken == "'"; // unicode character } + + public struct LockHintAppender + { + private static readonly Regex FromClauseTableNameRegex = new Regex(@"from\s+\[?(\w+)\]?", RegexOptions.IgnoreCase | RegexOptions.Multiline); + + private readonly MsSql2000Dialect _dialect; + private readonly IDictionary _aliasedLockModes; + + private readonly Regex _matchRegex; + private readonly Regex _unionSubclassRegex; + + public LockHintAppender(MsSql2000Dialect dialect, IDictionary aliasedLockModes) + { + _dialect = dialect; + _aliasedLockModes = aliasedLockModes; + + // Regex matching any alias out of those given. Aliases should contain + // no dangerous characters (they are identifiers) so they are not escaped. + var aliasesPattern = StringHelper.Join("|", aliasedLockModes.Keys); + + // Match < alias >, < alias,>, or < alias$>, the intent is to capture alias names + // in various kinds of "FROM table1 alias1, table2 alias2". + _matchRegex = new Regex(" (" + aliasesPattern + ")([, ]|$)"); + _unionSubclassRegex = new Regex(@"from\s+\(((?:.|\r|\n)*)\)(?:\s+as)?\s+(?" + aliasesPattern + ")", RegexOptions.IgnoreCase | RegexOptions.Multiline); + } + + public SqlString AppendLockHint(SqlString sql) + { + var result = new SqlStringBuilder(); + + foreach (object part in sql.Parts) + { + if (part == Parameter.Placeholder) + { + result.Add((Parameter)part); + continue; + } + + result.Add(ProcessUnionSubclassCase((string)part) ?? _matchRegex.Replace((string)part, ReplaceMatch)); + } + + return result.ToSqlString(); + } + + private string ProcessUnionSubclassCase(string part) + { + var unionMatch = _unionSubclassRegex.Match(part); + if (!unionMatch.Success) + { + return null; + } + + var alias = unionMatch.Groups["alias"].Value; + var lockMode = _aliasedLockModes[alias]; + var @this = this; + var replacement = FromClauseTableNameRegex.Replace(unionMatch.Value, m => @this._dialect.AppendLockHint(lockMode, m.Value)); + + return _unionSubclassRegex.Replace(part, replacement); + } + + private string ReplaceMatch(Match match) + { + string alias = match.Groups[1].Value; + string lockHint = _dialect.AppendLockHint(_aliasedLockModes[alias], alias); + return string.Concat(" ", lockHint, match.Groups[2].Value); // TODO: seems like this line is redundant + } + } } }