Skip to content

Commit 21ab72e

Browse files
committed
Add cross join support for Hql and Linq query provider
1 parent 873feec commit 21ab72e

20 files changed

+168
-15
lines changed

src/NHibernate.Test/Async/Hql/EntityJoinHqlTest.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,27 @@ public async Task EntityJoinWithFetchesAsync()
274274
}
275275
}
276276

277+
[Test]
278+
public async Task CrossJoinAndWhereClauseAsync()
279+
{
280+
using (var sqlLog = new SqlLogSpy())
281+
using (var session = OpenSession())
282+
{
283+
var result = await (session.CreateQuery(
284+
"SELECT s " +
285+
"FROM EntityComplex s cross join EntityComplex q " +
286+
"where s.SameTypeChild.Id = q.SameTypeChild.Id"
287+
).ListAsync());
288+
289+
Assert.That(result, Has.Count.EqualTo(1));
290+
Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected");
291+
if (Dialect.SupportsCrossJoin)
292+
{
293+
Assert.That(sqlLog.GetWholeLog(), Does.Contain("cross join"), "A cross join is expected in the SQL select");
294+
}
295+
}
296+
}
297+
277298
#region Test Setup
278299

279300
protected override HbmMapping GetMappings()

src/NHibernate.Test/Async/Linq/ByMethod/JoinTests.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,31 @@ public async Task MultipleLinqJoinsWithSameProjectionNamesAsync()
3131
Assert.That(orders.Count, Is.EqualTo(828));
3232
Assert.IsTrue(orders.All(x => x.FirstId == x.SecondId - 1 && x.SecondId == x.ThirdId - 1));
3333
}
34+
35+
[Test]
36+
public async Task CrossJoinWithPredicateInOnStatementAsync()
37+
{
38+
var result =
39+
await ((from o in db.Orders
40+
from p in db.Products
41+
join d in db.OrderLines
42+
on new { o.OrderId, p.ProductId } equals new { d.Order.OrderId, d.Product.ProductId }
43+
into details
44+
from d in details
45+
select new { o.OrderId, p.ProductId, d.UnitPrice }).Take(10).ToListAsync());
46+
47+
Assert.That(result.Count, Is.EqualTo(10));
48+
}
49+
50+
[Test]
51+
public async Task CrossJoinWithPredicateInWhereStatementAsync()
52+
{
53+
var result = await ((from o in db.Orders
54+
from o2 in db.Orders.Where(x => x.Freight > 50)
55+
where (o.OrderId == o2.OrderId + 1) || (o.OrderId == o2.OrderId - 1)
56+
select new { o.OrderId, OrderId2 = o2.OrderId }).ToListAsync());
57+
58+
Assert.That(result.Count, Is.EqualTo(720));
59+
}
3460
}
3561
}

src/NHibernate.Test/Hql/EntityJoinHqlTest.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ public void EntityJoinWithFetches()
264264
}
265265

266266
[Test, Ignore("Failing for unrelated reasons")]
267-
public void CrossJoinAndWithClause()
267+
public void ImplicitJoinAndWithClause()
268268
{
269269
//This is about complex theta style join fix that was implemented in hibernate along with entity join functionality
270270
//https://hibernate.atlassian.net/browse/HHH-7321
@@ -279,6 +279,27 @@ public void CrossJoinAndWithClause()
279279
}
280280
}
281281

282+
[Test]
283+
public void CrossJoinAndWhereClause()
284+
{
285+
using (var sqlLog = new SqlLogSpy())
286+
using (var session = OpenSession())
287+
{
288+
var result = session.CreateQuery(
289+
"SELECT s " +
290+
"FROM EntityComplex s cross join EntityComplex q " +
291+
"where s.SameTypeChild.Id = q.SameTypeChild.Id"
292+
).List();
293+
294+
Assert.That(result, Has.Count.EqualTo(1));
295+
Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected");
296+
if (Dialect.SupportsCrossJoin)
297+
{
298+
Assert.That(sqlLog.GetWholeLog(), Does.Contain("cross join"), "A cross join is expected in the SQL select");
299+
}
300+
}
301+
}
302+
282303
#region Test Setup
283304

284305
protected override HbmMapping GetMappings()

src/NHibernate.Test/Linq/ByMethod/JoinTests.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,31 @@ public void MultipleLinqJoinsWithSameProjectionNames()
1919
Assert.That(orders.Count, Is.EqualTo(828));
2020
Assert.IsTrue(orders.All(x => x.FirstId == x.SecondId - 1 && x.SecondId == x.ThirdId - 1));
2121
}
22+
23+
[Test]
24+
public void CrossJoinWithPredicateInOnStatement()
25+
{
26+
var result =
27+
(from o in db.Orders
28+
from p in db.Products
29+
join d in db.OrderLines
30+
on new { o.OrderId, p.ProductId } equals new { d.Order.OrderId, d.Product.ProductId }
31+
into details
32+
from d in details
33+
select new { o.OrderId, p.ProductId, d.UnitPrice }).Take(10).ToList();
34+
35+
Assert.That(result.Count, Is.EqualTo(10));
36+
}
37+
38+
[Test]
39+
public void CrossJoinWithPredicateInWhereStatement()
40+
{
41+
var result = (from o in db.Orders
42+
from o2 in db.Orders.Where(x => x.Freight > 50)
43+
where (o.OrderId == o2.OrderId + 1) || (o.OrderId == o2.OrderId - 1)
44+
select new { o.OrderId, OrderId2 = o2.OrderId }).ToList();
45+
46+
Assert.That(result.Count, Is.EqualTo(720));
47+
}
2248
}
2349
}

src/NHibernate/AdoNet/Util/BasicFormatter.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ static BasicFormatter()
2020
{
2121
beginClauses.Add("left");
2222
beginClauses.Add("right");
23+
beginClauses.Add("cross");
2324
beginClauses.Add("inner");
2425
beginClauses.Add("outer");
2526
beginClauses.Add("group");

src/NHibernate/Dialect/DB2Dialect.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,9 @@ public override string ForUpdateString
300300

301301
public override bool SupportsResultSetPositionQueryMethodsOnForwardOnlyCursor => false;
302302

303+
/// <inheritdoc />
304+
public override bool SupportsCrossJoin => false; // DB2 v9.1 doesn't support 'cross join' syntax
305+
303306
public override bool SupportsLobValueChangePropogation => false;
304307

305308
public override bool SupportsExistsInSelect => false;

src/NHibernate/Dialect/Dialect.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1337,6 +1337,11 @@ public virtual JoinFragment CreateOuterJoinFragment()
13371337
return new ANSIJoinFragment();
13381338
}
13391339

1340+
/// <summary>
1341+
/// Does this dialect support CROSS JOIN?
1342+
/// </summary>
1343+
public virtual bool SupportsCrossJoin => true;
1344+
13401345
/// <summary>
13411346
/// Create a <see cref="CaseFragment"/> strategy responsible
13421347
/// for handling this dialect's variations in how CASE statements are

src/NHibernate/Dialect/InformixDialect0940.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,10 @@ public override JoinFragment CreateOuterJoinFragment()
126126
return new ANSIJoinFragment();
127127
}
128128

129-
/// <summary>
129+
/// <inheritdoc />
130+
public override bool SupportsCrossJoin => false;
131+
132+
/// <summary>
130133
/// Does this Dialect have some kind of <c>LIMIT</c> syntax?
131134
/// </summary>
132135
/// <value>False, unless overridden.</value>

src/NHibernate/Dialect/Oracle10gDialect.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,8 @@ public override JoinFragment CreateOuterJoinFragment()
1515
{
1616
return new ANSIJoinFragment();
1717
}
18+
19+
/// <inheritdoc />
20+
public override bool SupportsCrossJoin => true;
1821
}
1922
}

src/NHibernate/Dialect/Oracle8iDialect.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,9 @@ public override JoinFragment CreateOuterJoinFragment()
328328
return new OracleJoinFragment();
329329
}
330330

331+
/// <inheritdoc />
332+
public override bool SupportsCrossJoin => false;
333+
331334
/// <summary>
332335
/// Map case support to the Oracle DECODE function. Oracle did not
333336
/// add support for CASE until 9i.

src/NHibernate/Dialect/SybaseASA9Dialect.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ public override bool OffsetStartsAtOne
9797
get { return true; }
9898
}
9999

100+
/// <inheritdoc />
101+
public override bool SupportsCrossJoin => false;
102+
100103
public override SqlString GetLimitString(SqlString queryString, SqlString offset, SqlString limit)
101104
{
102105
int intSelectInsertPoint = GetAfterSelectInsertPoint(queryString);

src/NHibernate/Dialect/SybaseASE15Dialect.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,10 @@ public override bool SupportsExpectedLobUsagePattern
247247
{
248248
get { return false; }
249249
}
250-
250+
251+
/// <inheritdoc />
252+
public override bool SupportsCrossJoin => false;
253+
251254
public override char OpenQuote
252255
{
253256
get { return '['; }

src/NHibernate/Hql/Ast/ANTLR/Hql.g

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ tokens
1919
BETWEEN='between';
2020
CLASS='class';
2121
COUNT='count';
22+
CROSS='cross';
2223
DELETE='delete';
2324
DESCENDING='desc';
2425
DOT;
@@ -255,6 +256,7 @@ fromClause
255256
fromJoin
256257
: ( ( ( LEFT | RIGHT ) (OUTER)? ) | FULL | INNER )? JOIN^ (FETCH)? path (asAlias)? (propertyFetch)? (withClause)?
257258
| ( ( ( LEFT | RIGHT ) (OUTER)? ) | FULL | INNER )? JOIN^ (FETCH)? ELEMENTS! OPEN! path CLOSE! (asAlias)? (propertyFetch)? (withClause)?
259+
| CROSS JOIN^ { WeakKeywords(); } path (asAlias)?
258260
;
259261
260262
withClause

src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,9 @@ joinType returns [int j]
306306
| INNER {
307307
$j = INNER;
308308
}
309+
| CROSS {
310+
$j = CROSS;
311+
}
309312
;
310313
311314
// Matches a path and returns the normalized string for the path (usually

src/NHibernate/Hql/Ast/ANTLR/Util/JoinProcessor.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public static JoinType ToHibernateJoinType(int astJoinType)
4848
return JoinType.RightOuterJoin;
4949
case HqlSqlWalker.FULL:
5050
return JoinType.FullJoin;
51+
case HqlSqlWalker.CROSS:
52+
return JoinType.CrossJoin;
5153
default:
5254
throw new AssertionFailure("undefined join type " + astJoinType);
5355
}

src/NHibernate/Hql/Ast/HqlTreeBuilder.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,11 @@ public HqlLeftJoin LeftJoin(HqlExpression expression, HqlAlias @alias)
483483
return new HqlLeftJoin(_factory, expression, @alias);
484484
}
485485

486+
public HqlCrossJoin CrossJoin(HqlExpression expression, HqlAlias @alias)
487+
{
488+
return new HqlCrossJoin(_factory, expression, @alias);
489+
}
490+
486491
public HqlFetchJoin FetchJoin(HqlExpression expression, HqlAlias @alias)
487492
{
488493
return new HqlFetchJoin(_factory, expression, @alias);

src/NHibernate/Hql/Ast/HqlTreeNode.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,13 @@ public class HqlLeftJoin : HqlTreeNode
851851
}
852852
}
853853

854+
public class HqlCrossJoin : HqlTreeNode
855+
{
856+
public HqlCrossJoin(IASTFactory factory, HqlExpression expression, HqlAlias @alias) : base(HqlSqlWalker.JOIN, "join", factory, new HqlCross(factory), expression, @alias)
857+
{
858+
}
859+
}
860+
854861
public class HqlFetchJoin : HqlTreeNode
855862
{
856863
public HqlFetchJoin(IASTFactory factory, HqlExpression expression, HqlAlias @alias)
@@ -906,6 +913,14 @@ public HqlLeft(IASTFactory factory)
906913
}
907914
}
908915

916+
public class HqlCross : HqlTreeNode
917+
{
918+
public HqlCross(IASTFactory factory)
919+
: base(HqlSqlWalker.CROSS, "cross", factory)
920+
{
921+
}
922+
}
923+
909924
public class HqlAny : HqlBooleanExpression
910925
{
911926
public HqlAny(IASTFactory factory) : base(HqlSqlWalker.ANY, "any", factory)

src/NHibernate/Linq/Visitors/QueryModelVisitor.cs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -315,22 +315,20 @@ public override void VisitMainFromClause(MainFromClause fromClause, QueryModel q
315315
public override void VisitAdditionalFromClause(AdditionalFromClause fromClause, QueryModel queryModel, int index)
316316
{
317317
var querySourceName = VisitorParameters.QuerySourceNamer.GetName(fromClause);
318-
318+
var fromExpressionTree = HqlGeneratorExpressionVisitor.Visit(fromClause.FromExpression, VisitorParameters);
319+
var alias = _hqlTree.TreeBuilder.Alias(querySourceName);
319320
if (fromClause.FromExpression is MemberExpression)
320321
{
321322
// It's a join
322-
_hqlTree.AddFromClause(
323-
_hqlTree.TreeBuilder.Join(
324-
HqlGeneratorExpressionVisitor.Visit(fromClause.FromExpression, VisitorParameters).AsExpression(),
325-
_hqlTree.TreeBuilder.Alias(querySourceName)));
323+
_hqlTree.AddFromClause(_hqlTree.TreeBuilder.Join(fromExpressionTree.AsExpression(), alias));
326324
}
327325
else
328326
{
329-
// TODO - exact same code as in MainFromClause; refactor this out
330-
_hqlTree.AddFromClause(
331-
_hqlTree.TreeBuilder.Range(
332-
HqlGeneratorExpressionVisitor.Visit(fromClause.FromExpression, VisitorParameters),
333-
_hqlTree.TreeBuilder.Alias(querySourceName)));
327+
var join = VisitorParameters.SessionFactory.Dialect.SupportsCrossJoin
328+
? _hqlTree.TreeBuilder.CrossJoin(fromExpressionTree.AsExpression(), alias)
329+
: (HqlTreeNode) _hqlTree.TreeBuilder.Range(fromExpressionTree, alias);
330+
331+
_hqlTree.AddFromClause(join);
334332
}
335333

336334
base.VisitAdditionalFromClause(fromClause, queryModel, index);

src/NHibernate/SqlCommand/ANSIJoinFragment.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,21 @@ public override void AddJoin(string tableName, string alias, string[] fkColumns,
3333
case JoinType.FullJoin:
3434
joinString = " full outer join ";
3535
break;
36+
case JoinType.CrossJoin:
37+
joinString = " cross join ";
38+
break;
3639
default:
3740
throw new AssertionFailure("undefined join type");
3841
}
3942

40-
_fromFragment.Add(joinString + tableName + ' ' + alias + " on ");
43+
_fromFragment.Add(joinString).Add(tableName).Add(" ").Add(alias).Add(" ");
44+
if (joinType == JoinType.CrossJoin)
45+
{
46+
// Cross join does not have an 'on' statement
47+
return;
48+
}
4149

50+
_fromFragment.Add("on ");
4251
if (fkColumns.Length == 0)
4352
{
4453
AddBareCondition(_fromFragment, on);

src/NHibernate/SqlCommand/JoinFragment.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ public enum JoinType
1010
InnerJoin = 0,
1111
FullJoin = 4,
1212
LeftOuterJoin = 1,
13-
RightOuterJoin = 2
13+
RightOuterJoin = 2,
14+
CrossJoin = 8
1415
}
1516

1617
/// <summary>

0 commit comments

Comments
 (0)