Skip to content

Commit b03cf6a

Browse files
ggeurtsoskarb
authored andcommitted
NH-2977
- Added limit SQL generation for SQL Server queries with comments, common table expressions, and stored procedure invocations. - Modified loader to fall back to client-side limit/offset behavior if dialect does not support generation of server side limit/offset SQL for a given (custom) SQL statement.
1 parent a668a39 commit b03cf6a

File tree

12 files changed

+743
-229
lines changed

12 files changed

+743
-229
lines changed

src/NHibernate.Test/DialectTest/MsSql2005DialectFixture.cs

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public void OnlyOffsetLimit()
7575
var d = new MsSql2005Dialect();
7676

7777
SqlString str = d.GetLimitString(new SqlString("select distinct c.Contact_Id as Contact1_19_0_, c._Rating as Rating2_19_0_ from dbo.Contact c where COALESCE(c.Rating, 0) > 0 order by c.Rating desc , c.Last_Name , c.First_Name"), null, new SqlString("10"));
78-
Assert.That(str.ToString(), Is.EqualTo("select distinct TOP (10) c.Contact_Id as Contact1_19_0_, c._Rating as Rating2_19_0_ from dbo.Contact c where COALESCE(c.Rating, 0) > 0 order by c.Rating desc , c.Last_Name , c.First_Name"));
78+
Assert.That(str.ToString(), Is.EqualTo("select distinct TOP (10) c.Contact_Id as Contact1_19_0_, c._Rating as Rating2_19_0_ from dbo.Contact c where COALESCE(c.Rating, 0) > 0 order by c.Rating desc , c.Last_Name , c.First_Name"));
7979
}
8080

8181
[Test]
@@ -222,7 +222,85 @@ public void GetIfExistsDropConstraintTest_For_Schema_other_than_dbo()
222222
public void GetLimitStringWithSqlComments()
223223
{
224224
var d = new MsSql2005Dialect();
225-
Assert.Throws<NotSupportedException>(() => d.GetLimitString(new SqlString(" /* criteria query */ SELECT p from lcdtm"), null, new SqlString("2")));
225+
var limitSqlQuery = d.GetLimitString(new SqlString(" /* criteria query */ SELECT p from lcdtm"), null, new SqlString("2"));
226+
Assert.That(limitSqlQuery, Is.Not.Null);
227+
Assert.That(limitSqlQuery.ToString(), Is.EqualTo(" /* criteria query */ SELECT TOP (2) p from lcdtm"));
228+
}
229+
230+
[Test]
231+
public void GetLimitStringWithSqlCommonTableExpression()
232+
{
233+
const string SQL = @"
234+
WITH DirectReports (ManagerID, EmployeeID, Title, DeptID, Level)
235+
( -- Anchor member definition
236+
SELECT ManagerID, EmployeeID, Title, Deptid, 0 AS Level
237+
FROM MyEmployees
238+
WHERE ManagerID IS NULL
239+
240+
UNION ALL
241+
242+
-- Recursive member definition
243+
SELECT e.ManagerID, e.EmployeeID, e.Title, e.Deptid, Level + 1
244+
FROM MyEmployees AS e
245+
INNER JOIN DirectReports AS ON e.ManagerID = d.EmployeeID
246+
)
247+
-- Statement that executes the CTE
248+
SELECT ManagerID, EmployeeID, Title, Level
249+
FROM DirectReports";
250+
251+
const string EXPECTED_SQL = @"
252+
WITH DirectReports (ManagerID, EmployeeID, Title, DeptID, Level)
253+
( -- Anchor member definition
254+
SELECT ManagerID, EmployeeID, Title, Deptid, 0 AS Level
255+
FROM MyEmployees
256+
WHERE ManagerID IS NULL
257+
258+
UNION ALL
259+
260+
-- Recursive member definition
261+
SELECT e.ManagerID, e.EmployeeID, e.Title, e.Deptid, Level + 1
262+
FROM MyEmployees AS e
263+
INNER JOIN DirectReports AS ON e.ManagerID = d.EmployeeID
264+
)
265+
-- Statement that executes the CTE
266+
SELECT TOP (2) ManagerID, EmployeeID, Title, Level
267+
FROM DirectReports";
268+
269+
var d = new MsSql2005Dialect();
270+
var limitSqlQuery = d.GetLimitString(new SqlString(SQL), null, new SqlString("2"));
271+
Assert.That(limitSqlQuery, Is.Not.Null);
272+
Assert.That(limitSqlQuery.ToString(), Is.EqualTo(EXPECTED_SQL));
273+
}
274+
275+
[Test]
276+
public void DontReturnLimitStringForStoredProcedureCall()
277+
{
278+
VerifyLimitStringForStoredProcedureCalls("EXEC sp_stored_procedures");
279+
VerifyLimitStringForStoredProcedureCalls(@"
280+
DECLARE @id int
281+
SELECT @id = id FROM persons WHERE name LIKE ?
282+
EXEC get_person_summary @id");
283+
VerifyLimitStringForStoredProcedureCalls(@"
284+
DECLARE @id int
285+
SELECT DISTINCT TOP 1 @id = id FROM persons WHERE name LIKE ?
286+
EXEC get_person_summary @id");
287+
VerifyLimitStringForStoredProcedureCalls(@"
288+
DECLARE @id int
289+
SELECT DISTINCT TOP (?) PERCENT WITH TIES @id = id FROM persons WHERE name LIKE ?
290+
EXEC get_person_summary @id");
291+
}
292+
293+
private static void VerifyLimitStringForStoredProcedureCalls(string sql)
294+
{
295+
var d = new MsSql2005Dialect();
296+
var limitSql = d.GetLimitString(new SqlString(sql), null, new SqlString("2"));
297+
Assert.That(limitSql, Is.Null, "Limit only: {0}", sql);
298+
299+
limitSql = d.GetLimitString(new SqlString(sql), new SqlString("10"), null);
300+
Assert.That(limitSql, Is.Null, "Offset only: {0}", sql);
301+
302+
limitSql = d.GetLimitString(new SqlString(sql), new SqlString("10"), new SqlString("2"));
303+
Assert.That(limitSql, Is.Null, "Limit and Offset: {0}", sql);
226304
}
227305
}
228306
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using NHibernate.Cfg.MappingSchema;
2+
using NUnit.Framework;
3+
4+
namespace NHibernate.Test.NHSpecificTest.NH2977
5+
{
6+
/// <summary>
7+
/// Fixture using 'by code' mappings
8+
/// </summary>
9+
/// <remarks>
10+
/// This fixture is identical to <see cref="Fixture" /> except the <see cref="Entity" /> mapping is performed
11+
/// by code in the GetMappings method, and does not require the <c>Mappings.hbm.xml</c> file. Use this approach
12+
/// if you prefer.
13+
/// </remarks>
14+
public class ByCodeFixture : TestCaseMappingByCode
15+
{
16+
protected override HbmMapping GetMappings()
17+
{
18+
return new HbmMapping();
19+
}
20+
21+
protected override bool AppliesTo(Dialect.Dialect dialect)
22+
{
23+
return dialect is Dialect.MsSql2000Dialect;
24+
}
25+
26+
[Test]
27+
public void CanGetUniqueStoredProcedureResult()
28+
{
29+
using (ISession session = OpenSession())
30+
using (session.BeginTransaction())
31+
{
32+
var result = session.CreateSQLQuery("EXEC sp_stored_procedures ?")
33+
.SetString(0, "sp_help")
34+
.UniqueResult();
35+
Assert.That(result, Is.Not.Null);
36+
}
37+
}
38+
39+
[Test]
40+
public void CanLimitStoredProcedureResults()
41+
{
42+
using (ISession session = OpenSession())
43+
using (session.BeginTransaction())
44+
{
45+
var result = session.CreateSQLQuery("EXEC sp_stored_procedures")
46+
.SetMaxResults(5)
47+
.List();
48+
Assert.That(result, Has.Count.EqualTo(5));
49+
}
50+
}
51+
}
52+
}

src/NHibernate.Test/NHibernate.Test.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,7 @@
10361036
<Compile Include="NHSpecificTest\NH2960\Fixture.cs" />
10371037
<Compile Include="NHSpecificTest\NH2959\Entity.cs" />
10381038
<Compile Include="NHSpecificTest\NH2959\Fixture.cs" />
1039+
<Compile Include="NHSpecificTest\NH2977\FixtureByCode.cs" />
10391040
<Compile Include="NHSpecificTest\NH3010\FixtureWithBatcher.cs" />
10401041
<Compile Include="NHSpecificTest\NH3010\FixtureWithNoBatcher.cs" />
10411042
<Compile Include="NHSpecificTest\NH3010\Model.cs" />

src/NHibernate/Dialect/Dialect.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,20 +1452,22 @@ public virtual bool OffsetStartsAtOne
14521452
}
14531453

14541454
/// <summary>
1455-
/// Add a <c>LIMIT</c> clause to the given SQL <c>SELECT</c>.
1455+
/// Attempts to add a <c>LIMIT</c> clause to the given SQL <c>SELECT</c>.
14561456
/// Expects any database-specific offset and limit adjustments to have already been performed (ex. UseMaxForLimit, OffsetStartsAtOne).
14571457
/// </summary>
14581458
/// <param name="queryString">The <see cref="SqlString"/> to base the limit query off.</param>
14591459
/// <param name="offset">Offset of the first row to be returned by the query. This may be represented as a parameter, a string literal, or a null value if no limit is requested. This should have already been adjusted to account for OffsetStartsAtOne.</param>
14601460
/// <param name="limit">Maximum number of rows to be returned by the query. This may be represented as a parameter, a string literal, or a null value if no offset is requested. This should have already been adjusted to account for UseMaxForLimit.</param>
1461-
/// <returns>A new <see cref="SqlString"/> that contains the <c>LIMIT</c> clause.</returns>
1461+
/// <returns>A new <see cref="SqlString"/> that contains the <c>LIMIT</c> clause. Returns <c>null</c>
1462+
/// if <paramref name="queryString"/> represents a SQL statement to which a limit clause cannot be added,
1463+
/// for example when the query string is custom SQL invoking a stored procedure.</returns>
14621464
public virtual SqlString GetLimitString(SqlString queryString, SqlString offset, SqlString limit)
14631465
{
14641466
throw new NotSupportedException("Dialect does not have support for limit strings.");
14651467
}
14661468

14671469
/// <summary>
1468-
/// Generates a string to limit the result set to a number of maximum results with a specified offset into the results.
1470+
/// Attempts to generate a string to limit the result set to a number of maximum results with a specified offset into the results.
14691471
/// Expects any database-specific offset and limit adjustments to have already been performed (ex. UseMaxForLimit, OffsetStartsAtOne).
14701472
/// Performs error checking based on the various dialect limit support options. If both parameters and fixed valeus are
14711473
/// specified, this will use the parameter option if possible. Otherwise, it will fall back to a fixed string.

src/NHibernate/Dialect/MsSql2000Dialect.cs

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using NHibernate.Engine;
1010
using NHibernate.Mapping;
1111
using NHibernate.SqlCommand;
12+
using NHibernate.SqlCommand.Parser;
1213
using NHibernate.Type;
1314
using NHibernate.Util;
1415
using Environment = NHibernate.Cfg.Environment;
@@ -340,15 +341,26 @@ public override bool SupportsVariableLimit
340341

341342
public override SqlString GetLimitString(SqlString querySqlString, SqlString offset, SqlString limit)
342343
{
343-
/*
344-
* "SELECT TOP limit rest-of-sql-statement"
345-
*/
344+
int insertPoint;
345+
return TryFindLimitInsertPoint(querySqlString, out insertPoint)
346+
? querySqlString.Insert(insertPoint, new SqlString("top ", limit, " "))
347+
: null;
348+
}
346349

347-
SqlStringBuilder topFragment = new SqlStringBuilder();
348-
topFragment.Add(" top ");
349-
topFragment.Add(limit);
350+
protected static bool TryFindLimitInsertPoint(SqlString sql, out int result)
351+
{
352+
var tokenEnum = new SqlTokenizer(sql).GetEnumerator();
353+
354+
SqlToken selectToken;
355+
bool isDistinct;
356+
if (tokenEnum.TryParseUntilFirstMsSqlSelectColumn(out selectToken, out isDistinct))
357+
{
358+
result = tokenEnum.Current.SqlIndex;
359+
return true;
360+
}
350361

351-
return querySqlString.Insert(GetAfterSelectInsertPoint(querySqlString), topFragment.ToSqlString());
362+
result = -1;
363+
return false;
352364
}
353365

354366
/// <summary>
@@ -398,19 +410,6 @@ public override string UnQuote(string quoted)
398410
return quoted.Replace(new string(CloseQuote, 2), CloseQuote.ToString());
399411
}
400412

401-
private static int GetAfterSelectInsertPoint(SqlString sql)
402-
{
403-
if (sql.StartsWithCaseInsensitive("select distinct"))
404-
{
405-
return 15;
406-
}
407-
else if (sql.StartsWithCaseInsensitive("select"))
408-
{
409-
return 6;
410-
}
411-
throw new NotSupportedException("The query should start with 'SELECT' or 'SELECT DISTINCT'");
412-
}
413-
414413
protected bool NeedsLockHint(LockMode lockMode)
415414
{
416415
return lockMode.GreaterThan(LockMode.Read);

src/NHibernate/Dialect/MsSql2005Dialect.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,4 @@ public override string AppendLockHint(LockMode lockMode, string tableName)
103103
return tableName;
104104
}
105105
}
106-
}
106+
}

0 commit comments

Comments
 (0)