From 344821ed418e24a19da452116d9b0dc7c96482ab Mon Sep 17 00:00:00 2001 From: maca88 Date: Sat, 6 Apr 2019 19:16:46 +0200 Subject: [PATCH 1/5] Port Hibernate's support subqueries in HQL as CASE statement alternatives --- src/NHibernate.Test/Async/Hql/SubQueryTest.cs | 71 +++++++++++++++++++ src/NHibernate.Test/Hql/SubQueryTest.cs | 59 +++++++++++++++ .../Hql/SubQueryTestEntities.cs | 29 ++++++++ src/NHibernate/Hql/Ast/ANTLR/Hql.g | 29 +++++--- src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g | 24 ++++++- 5 files changed, 200 insertions(+), 12 deletions(-) create mode 100644 src/NHibernate.Test/Async/Hql/SubQueryTest.cs create mode 100644 src/NHibernate.Test/Hql/SubQueryTest.cs create mode 100644 src/NHibernate.Test/Hql/SubQueryTestEntities.cs diff --git a/src/NHibernate.Test/Async/Hql/SubQueryTest.cs b/src/NHibernate.Test/Async/Hql/SubQueryTest.cs new file mode 100644 index 00000000000..7a519fe352e --- /dev/null +++ b/src/NHibernate.Test/Async/Hql/SubQueryTest.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; + +namespace NHibernate.Test.Hql +{ + using System.Threading.Tasks; + using System.Threading; + [TestFixture] + public class SubQueryTestAsync : TestCaseMappingByCode + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.Native)); + rc.Property(x => x.RootName); + rc.ManyToOne(x => x.Branch); + }); + + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.Native)); + rc.Property(x => x.BranchName); + rc.Bag(x => x.Leafs, cm => cm.Cascade(Mapping.ByCode.Cascade.All), x => x.OneToMany()); + }); + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.Native)); + rc.Property(x => x.LeafName); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + [TestCase("SELECT CASE WHEN l.id IS NOT NULL THEN (SELECT COUNT(r.id) FROM Root r) ELSE 0 END FROM Leaf l")] + [TestCase("SELECT CASE WHEN (SELECT COUNT(r.id) FROM Root r) > 1 THEN 1 ELSE 0 END FROM Leaf l")] + [TestCase("SELECT CASE WHEN l.id > 1 THEN 1 ELSE (SELECT COUNT(r.id) FROM Root r) END FROM Leaf l")] + [TestCase("SELECT CASE (SELECT COUNT(r.id) FROM Root r) WHEN 1 THEN 1 ELSE 0 END FROM Leaf l")] + [TestCase("SELECT CASE l.id WHEN (SELECT COUNT(r.id) FROM Root r) THEN 1 ELSE 0 END FROM Leaf l")] + public async Task TestSubQueryAsync(string query, CancellationToken cancellationToken = default(CancellationToken)) + { + if (!Dialect.SupportsScalarSubSelects) + { + Assert.Ignore(Dialect.GetType().Name + " does not support scalar sub-queries"); + } + + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + // Simple syntax check + await (session.CreateQuery(query).ListAsync(cancellationToken)); + await (transaction.CommitAsync(cancellationToken)); + } + } + } +} diff --git a/src/NHibernate.Test/Hql/SubQueryTest.cs b/src/NHibernate.Test/Hql/SubQueryTest.cs new file mode 100644 index 00000000000..52c9133ec74 --- /dev/null +++ b/src/NHibernate.Test/Hql/SubQueryTest.cs @@ -0,0 +1,59 @@ +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; + +namespace NHibernate.Test.Hql +{ + [TestFixture] + public class SubQueryTest : TestCaseMappingByCode + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.Native)); + rc.Property(x => x.RootName); + rc.ManyToOne(x => x.Branch); + }); + + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.Native)); + rc.Property(x => x.BranchName); + rc.Bag(x => x.Leafs, cm => cm.Cascade(Mapping.ByCode.Cascade.All), x => x.OneToMany()); + }); + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.Native)); + rc.Property(x => x.LeafName); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + [TestCase("SELECT CASE WHEN l.id IS NOT NULL THEN (SELECT COUNT(r.id) FROM Root r) ELSE 0 END FROM Leaf l")] + [TestCase("SELECT CASE WHEN (SELECT COUNT(r.id) FROM Root r) > 1 THEN 1 ELSE 0 END FROM Leaf l")] + [TestCase("SELECT CASE WHEN l.id > 1 THEN 1 ELSE (SELECT COUNT(r.id) FROM Root r) END FROM Leaf l")] + [TestCase("SELECT CASE (SELECT COUNT(r.id) FROM Root r) WHEN 1 THEN 1 ELSE 0 END FROM Leaf l")] + [TestCase("SELECT CASE l.id WHEN (SELECT COUNT(r.id) FROM Root r) THEN 1 ELSE 0 END FROM Leaf l")] + public void TestSubQuery(string query) + { + if (!Dialect.SupportsScalarSubSelects) + { + Assert.Ignore(Dialect.GetType().Name + " does not support scalar sub-queries"); + } + + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + // Simple syntax check + session.CreateQuery(query).List(); + transaction.Commit(); + } + } + } +} diff --git a/src/NHibernate.Test/Hql/SubQueryTestEntities.cs b/src/NHibernate.Test/Hql/SubQueryTestEntities.cs new file mode 100644 index 00000000000..2b431eeacf7 --- /dev/null +++ b/src/NHibernate.Test/Hql/SubQueryTestEntities.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace NHibernate.Test.Hql +{ + public class Root + { + public virtual int Id { get; set; } + + public virtual string RootName { get; set; } + + public virtual Branch Branch { get; set; } + } + + public class Branch + { + public virtual int Id { get; set; } + + public virtual string BranchName { get; set; } + + public virtual IList Leafs { get; set; } + } + + public class Leaf + { + public virtual int Id { get; set; } + + public virtual string LeafName { get; set; } + } +} diff --git a/src/NHibernate/Hql/Ast/ANTLR/Hql.g b/src/NHibernate/Hql/Ast/ANTLR/Hql.g index 2783936a19a..bac72902581 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Hql.g +++ b/src/NHibernate/Hql/Ast/ANTLR/Hql.g @@ -527,23 +527,32 @@ unaryExpression ; caseExpression - : CASE (whenClause)+ (elseClause)? END - -> ^(CASE whenClause+ elseClause?) - | CASE unaryExpression (altWhenClause)+ (elseClause)? END - -> ^(CASE2 unaryExpression altWhenClause+ elseClause?) + // NOTE : the unaryExpression rule contains the subQuery rule + : simpleCaseStatement + | searchedCaseStatement ; - -whenClause - : (WHEN^ logicalExpression THEN! expression) + +simpleCaseStatement + : CASE unaryExpression (simpleCaseWhenClause)+ (elseClause)? END + -> ^(CASE2 unaryExpression simpleCaseWhenClause+ elseClause?) ; - -altWhenClause - : (WHEN^ unaryExpression THEN! expression) + +simpleCaseWhenClause + : (WHEN^ unaryExpression THEN! unaryExpression) ; elseClause : (ELSE^ expression) ; + +searchedCaseStatement + : CASE (searchedCaseWhenClause)+ (elseClause)? END + -> ^(CASE searchedCaseWhenClause+ elseClause?) + ; + +searchedCaseWhenClause + : (WHEN^ logicalExpression THEN! unaryExpression) + ; quantifiedExpression : ( SOME^ | EXISTS^ | ALL^ | ANY^ ) diff --git a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g index 78825998019..befd1f3a2e5 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g +++ b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g @@ -432,8 +432,28 @@ arithmeticExpr ; caseExpr - : ^(CASE { _inCase = true; } (^(WHEN logicalExpr expr))+ (^(ELSE expr))?) { _inCase = false; } - | ^(CASE2 { _inCase = true; } expr (^(WHEN expr expr))+ (^(ELSE expr))?) { _inCase = false; } + : simpleCaseExpression + | searchedCaseExpression + ; + +simpleCaseExpression + : ^(CASE2 {_inCase=true;} exprOrSubquery (simpleCaseWhenClause)+ (elseClause)?) {_inCase=false;} + ; + +simpleCaseWhenClause + : ^(WHEN exprOrSubquery exprOrSubquery) + ; + +elseClause + : ^(ELSE exprOrSubquery) + ; + +searchedCaseExpression + : ^(CASE {_inCase = true;} (searchedCaseWhenClause)+ (elseClause)?) {_inCase = false;} + ; + +searchedCaseWhenClause + : ^(WHEN logicalExpr exprOrSubquery) ; //TODO: I don't think we need this anymore .. how is it different to From 2644ef723e8bac125c3a2e655690f68eef76107b Mon Sep 17 00:00:00 2001 From: maca88 Date: Sat, 6 Apr 2019 21:37:40 +0200 Subject: [PATCH 2/5] Fix broken test --- src/NHibernate.Test/Hql/Ast/SqlTranslationFixture.cs | 10 ++++++++++ src/NHibernate/Hql/Ast/ANTLR/Hql.g | 8 ++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/NHibernate.Test/Hql/Ast/SqlTranslationFixture.cs b/src/NHibernate.Test/Hql/Ast/SqlTranslationFixture.cs index 2e2cd4cddff..ab734057d4f 100644 --- a/src/NHibernate.Test/Hql/Ast/SqlTranslationFixture.cs +++ b/src/NHibernate.Test/Hql/Ast/SqlTranslationFixture.cs @@ -23,6 +23,16 @@ public void CaseClauseWithMath() Assert.DoesNotThrow(() => GetSql(queryWithoutParen)); } + [Test] + public void SimpleCaseClauseWithMath() + { + const string query = "from SimpleClass s where (case (cast(s.IntValue as long) * :pAValue) when (cast(s.IntValue as long) * :pAValue) then (cast(s.IntValue as long) * :pAValue) else 1 end) > 0"; + Assert.DoesNotThrow(() => GetSql(query)); + + const string queryWithoutParen = "from SimpleClass s where (case cast(s.IntValue as long) * :pAValue when cast(s.IntValue as long) * :pAValue then cast(s.IntValue as long) * :pAValue else 1 end) > 0"; + Assert.DoesNotThrow(() => GetSql(queryWithoutParen)); + } + [Test] public void Union() { diff --git a/src/NHibernate/Hql/Ast/ANTLR/Hql.g b/src/NHibernate/Hql/Ast/ANTLR/Hql.g index bac72902581..0b29493b410 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Hql.g +++ b/src/NHibernate/Hql/Ast/ANTLR/Hql.g @@ -533,12 +533,12 @@ caseExpression ; simpleCaseStatement - : CASE unaryExpression (simpleCaseWhenClause)+ (elseClause)? END - -> ^(CASE2 unaryExpression simpleCaseWhenClause+ elseClause?) + : CASE expression (simpleCaseWhenClause)+ (elseClause)? END + -> ^(CASE2 expression simpleCaseWhenClause+ elseClause?) ; simpleCaseWhenClause - : (WHEN^ unaryExpression THEN! unaryExpression) + : (WHEN^ expression THEN! expression) ; elseClause @@ -551,7 +551,7 @@ searchedCaseStatement ; searchedCaseWhenClause - : (WHEN^ logicalExpression THEN! unaryExpression) + : (WHEN^ logicalExpression THEN! expression) ; quantifiedExpression From e4446079f67c4afa511825ac6b1d1a4e796303d2 Mon Sep 17 00:00:00 2001 From: maca88 Date: Sun, 7 Apr 2019 10:42:16 +0200 Subject: [PATCH 3/5] Fix previously not supported test --- .../Async/NHSpecificTest/GH1879/FixtureByCode.cs | 4 ++-- .../NHSpecificTest/GH1879/FixtureByCode.cs | 3 ++- .../Linq/ReWriters/ConditionalQueryReferenceExpander.cs | 8 ++++++++ .../Linq/ReWriters/SubQueryConditionalExpander.cs | 8 ++++++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH1879/FixtureByCode.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH1879/FixtureByCode.cs index d57b65898b5..0dac0e740f5 100644 --- a/src/NHibernate.Test/Async/NHSpecificTest/GH1879/FixtureByCode.cs +++ b/src/NHibernate.Test/Async/NHSpecificTest/GH1879/FixtureByCode.cs @@ -12,6 +12,7 @@ using System.Collections.Generic; using System.Linq; using NHibernate.Cfg.MappingSchema; +using NHibernate.Exceptions; using NHibernate.Mapping.ByCode; using NHibernate.Type; using NUnit.Framework; @@ -124,8 +125,7 @@ protected async Task AreEqualAsync( { expectedResult = await (expectedQuery(session.Query()).ToListAsync(cancellationToken)); } - catch (OperationCanceledException) { throw; } - catch (Exception e) + catch (GenericADOException e) { Assert.Ignore($"Not currently supported query: {e}"); } diff --git a/src/NHibernate.Test/NHSpecificTest/GH1879/FixtureByCode.cs b/src/NHibernate.Test/NHSpecificTest/GH1879/FixtureByCode.cs index 3fe9211a449..6b491f59d00 100644 --- a/src/NHibernate.Test/NHSpecificTest/GH1879/FixtureByCode.cs +++ b/src/NHibernate.Test/NHSpecificTest/GH1879/FixtureByCode.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using NHibernate.Cfg.MappingSchema; +using NHibernate.Exceptions; using NHibernate.Mapping.ByCode; using NHibernate.Type; using NUnit.Framework; @@ -111,7 +112,7 @@ protected void AreEqual( { expectedResult = expectedQuery(session.Query()).ToList(); } - catch (Exception e) + catch (GenericADOException e) { Assert.Ignore($"Not currently supported query: {e}"); } diff --git a/src/NHibernate/Linq/ReWriters/ConditionalQueryReferenceExpander.cs b/src/NHibernate/Linq/ReWriters/ConditionalQueryReferenceExpander.cs index b83be7077b4..c67039b1863 100644 --- a/src/NHibernate/Linq/ReWriters/ConditionalQueryReferenceExpander.cs +++ b/src/NHibernate/Linq/ReWriters/ConditionalQueryReferenceExpander.cs @@ -31,6 +31,14 @@ public override void VisitSelectClause(SelectClause selectClause, QueryModel que _expander.Transform(selectClause); } + public override void VisitMainFromClause(MainFromClause fromClause, QueryModel queryModel) + { + if (fromClause.FromExpression is SubQueryExpression subqueryExpression) + { + VisitQueryModel(subqueryExpression.QueryModel); + } + } + public override void VisitOrdering(Ordering ordering, QueryModel queryModel, OrderByClause orderByClause, int index) { _expander.Transform(ordering); diff --git a/src/NHibernate/Linq/ReWriters/SubQueryConditionalExpander.cs b/src/NHibernate/Linq/ReWriters/SubQueryConditionalExpander.cs index 3c6d3a4231f..5319018b021 100644 --- a/src/NHibernate/Linq/ReWriters/SubQueryConditionalExpander.cs +++ b/src/NHibernate/Linq/ReWriters/SubQueryConditionalExpander.cs @@ -24,6 +24,14 @@ public override void VisitSelectClause(SelectClause selectClause, QueryModel que _expander.Transform(selectClause); } + public override void VisitMainFromClause(MainFromClause fromClause, QueryModel queryModel) + { + if (fromClause.FromExpression is SubQueryExpression subqueryExpression) + { + VisitQueryModel(subqueryExpression.QueryModel); + } + } + public override void VisitOrdering(Ordering ordering, QueryModel queryModel, OrderByClause orderByClause, int index) { _expander.Transform(ordering); From 006809b193c9562980269c863c52e077a32d0332 Mon Sep 17 00:00:00 2001 From: maca88 Date: Sat, 13 Apr 2019 21:51:24 +0200 Subject: [PATCH 4/5] Code review changes --- src/NHibernate.Test/Async/Hql/SubQueryTest.cs | 10 +++++----- src/NHibernate.Test/Hql/SubQueryTest.cs | 10 +++++----- src/NHibernate/Hql/Ast/ANTLR/Hql.g | 1 - 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/NHibernate.Test/Async/Hql/SubQueryTest.cs b/src/NHibernate.Test/Async/Hql/SubQueryTest.cs index 7a519fe352e..42f0eb517a3 100644 --- a/src/NHibernate.Test/Async/Hql/SubQueryTest.cs +++ b/src/NHibernate.Test/Async/Hql/SubQueryTest.cs @@ -47,6 +47,11 @@ protected override HbmMapping GetMappings() return mapper.CompileMappingForAllExplicitlyAddedEntities(); } + protected override bool AppliesTo(Dialect.Dialect dialect) + { + return dialect.SupportsScalarSubSelects; + } + [TestCase("SELECT CASE WHEN l.id IS NOT NULL THEN (SELECT COUNT(r.id) FROM Root r) ELSE 0 END FROM Leaf l")] [TestCase("SELECT CASE WHEN (SELECT COUNT(r.id) FROM Root r) > 1 THEN 1 ELSE 0 END FROM Leaf l")] [TestCase("SELECT CASE WHEN l.id > 1 THEN 1 ELSE (SELECT COUNT(r.id) FROM Root r) END FROM Leaf l")] @@ -54,11 +59,6 @@ protected override HbmMapping GetMappings() [TestCase("SELECT CASE l.id WHEN (SELECT COUNT(r.id) FROM Root r) THEN 1 ELSE 0 END FROM Leaf l")] public async Task TestSubQueryAsync(string query, CancellationToken cancellationToken = default(CancellationToken)) { - if (!Dialect.SupportsScalarSubSelects) - { - Assert.Ignore(Dialect.GetType().Name + " does not support scalar sub-queries"); - } - using (var session = OpenSession()) using (var transaction = session.BeginTransaction()) { diff --git a/src/NHibernate.Test/Hql/SubQueryTest.cs b/src/NHibernate.Test/Hql/SubQueryTest.cs index 52c9133ec74..caf665de998 100644 --- a/src/NHibernate.Test/Hql/SubQueryTest.cs +++ b/src/NHibernate.Test/Hql/SubQueryTest.cs @@ -35,6 +35,11 @@ protected override HbmMapping GetMappings() return mapper.CompileMappingForAllExplicitlyAddedEntities(); } + protected override bool AppliesTo(Dialect.Dialect dialect) + { + return dialect.SupportsScalarSubSelects; + } + [TestCase("SELECT CASE WHEN l.id IS NOT NULL THEN (SELECT COUNT(r.id) FROM Root r) ELSE 0 END FROM Leaf l")] [TestCase("SELECT CASE WHEN (SELECT COUNT(r.id) FROM Root r) > 1 THEN 1 ELSE 0 END FROM Leaf l")] [TestCase("SELECT CASE WHEN l.id > 1 THEN 1 ELSE (SELECT COUNT(r.id) FROM Root r) END FROM Leaf l")] @@ -42,11 +47,6 @@ protected override HbmMapping GetMappings() [TestCase("SELECT CASE l.id WHEN (SELECT COUNT(r.id) FROM Root r) THEN 1 ELSE 0 END FROM Leaf l")] public void TestSubQuery(string query) { - if (!Dialect.SupportsScalarSubSelects) - { - Assert.Ignore(Dialect.GetType().Name + " does not support scalar sub-queries"); - } - using (var session = OpenSession()) using (var transaction = session.BeginTransaction()) { diff --git a/src/NHibernate/Hql/Ast/ANTLR/Hql.g b/src/NHibernate/Hql/Ast/ANTLR/Hql.g index 0b29493b410..de0b52d8a7c 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Hql.g +++ b/src/NHibernate/Hql/Ast/ANTLR/Hql.g @@ -527,7 +527,6 @@ unaryExpression ; caseExpression - // NOTE : the unaryExpression rule contains the subQuery rule : simpleCaseStatement | searchedCaseStatement ; From d79fd247ddac78397c1c61f6c280c32a5519bd47 Mon Sep 17 00:00:00 2001 From: Alexander Zaytsev Date: Wed, 19 Jun 2019 13:32:34 +1200 Subject: [PATCH 5/5] Regenerate Async --- src/NHibernate.Test/Async/Hql/SubQueryTest.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/NHibernate.Test/Async/Hql/SubQueryTest.cs b/src/NHibernate.Test/Async/Hql/SubQueryTest.cs index 42f0eb517a3..f88c5ed3629 100644 --- a/src/NHibernate.Test/Async/Hql/SubQueryTest.cs +++ b/src/NHibernate.Test/Async/Hql/SubQueryTest.cs @@ -15,7 +15,6 @@ namespace NHibernate.Test.Hql { using System.Threading.Tasks; - using System.Threading; [TestFixture] public class SubQueryTestAsync : TestCaseMappingByCode { @@ -57,14 +56,14 @@ protected override bool AppliesTo(Dialect.Dialect dialect) [TestCase("SELECT CASE WHEN l.id > 1 THEN 1 ELSE (SELECT COUNT(r.id) FROM Root r) END FROM Leaf l")] [TestCase("SELECT CASE (SELECT COUNT(r.id) FROM Root r) WHEN 1 THEN 1 ELSE 0 END FROM Leaf l")] [TestCase("SELECT CASE l.id WHEN (SELECT COUNT(r.id) FROM Root r) THEN 1 ELSE 0 END FROM Leaf l")] - public async Task TestSubQueryAsync(string query, CancellationToken cancellationToken = default(CancellationToken)) + public async Task TestSubQueryAsync(string query) { using (var session = OpenSession()) using (var transaction = session.BeginTransaction()) { // Simple syntax check - await (session.CreateQuery(query).ListAsync(cancellationToken)); - await (transaction.CommitAsync(cancellationToken)); + await (session.CreateQuery(query).ListAsync()); + await (transaction.CommitAsync()); } } }