From 90f5234ad4f2fde8255a094fef743eb6c271dbf1 Mon Sep 17 00:00:00 2001 From: Duncan M Date: Thu, 21 Jul 2016 15:27:51 -0600 Subject: [PATCH] NH-3889 - Fixed the HqlSqlWalker grammar to handle subqueries without creating implicit joins --- .../NHSpecificTest/NH3889/Entity.cs | 40 +++ .../NHSpecificTest/NH3889/FixtureByCode.cs | 265 ++++++++++++++++++ src/NHibernate.Test/NHibernate.Test.csproj | 2 + src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g | 8 +- 4 files changed, 313 insertions(+), 2 deletions(-) create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3889/Entity.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3889/FixtureByCode.cs diff --git a/src/NHibernate.Test/NHSpecificTest/NH3889/Entity.cs b/src/NHibernate.Test/NHSpecificTest/NH3889/Entity.cs new file mode 100644 index 00000000000..fedc8c81c9d --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3889/Entity.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.NH3889 +{ + public class TimeRecord + { + public virtual Guid Id { get; set; } + public virtual Project Project { get; set; } + public virtual Job ActualJob { get; set; } + public virtual decimal Hours { get; set; } + + public virtual TimeSetting Setting { get; set; } + } + + public class TimeSetting + { + public virtual Guid Id { get; set; } + public virtual TimeInclude Include { get; set; } + } + + public class TimeInclude + { + public virtual Guid Id { get; set; } + public virtual bool Flag { get; set; } + } + + public class Project + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + public virtual Job Job { get; set; } + } + + public class Job + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH3889/FixtureByCode.cs b/src/NHibernate.Test/NHSpecificTest/NH3889/FixtureByCode.cs new file mode 100644 index 00000000000..269bf039784 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3889/FixtureByCode.cs @@ -0,0 +1,265 @@ +using System; +using System.Linq; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Dialect; +using NHibernate.Linq; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.NH3889 +{ + public class ByCodeFixture : TestCaseMappingByCode + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.ManyToOne(x => x.Job, map => + { + map.Column("JobId"); + map.NotNullable(true); + }); + }); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Hours); + rc.ManyToOne(x => x.Project, map => + { + map.Column("ProjectId"); + map.NotNullable(true); + }); + rc.ManyToOne(x => x.ActualJob, map => + { + map.Column("ActualJobId"); + }); + rc.ManyToOne(x => x.Setting, map => + { + map.Column("SettingId"); + }); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.ManyToOne(x => x.Include, map => + { + map.Column("IncludeId"); + }); + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Flag); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var job_a = new Job { Name = "Big Job" }; + session.Save(job_a); + var job_b = new Job { Name = "Small Job" }; + session.Save(job_b); + + var project_a = new Project { Job = job_a, Name = "Big Job - Part A" }; + session.Save(project_a); + var project_b = new Project { Job = job_a, Name = "Big Job - Part B" }; + session.Save(project_b); + var project_c = new Project { Job = job_b, Name = "Small Job - Rework" }; + session.Save(project_c); + + var include = new TimeInclude { Flag = true }; + session.Save(include); + var setting = new TimeSetting { Include = include }; + session.Save(setting); + + session.Save(new TimeRecord {Project = project_a, Hours = 2, Setting = setting }/*.AddTime(2)*/); + session.Save(new TimeRecord {Project = project_a, Hours = 3, Setting = setting }/*.AddTime(3)*/); + session.Save(new TimeRecord {Project = project_b, Hours = 5, Setting = setting }/*.AddTime(2).AddTime(3)*/); + session.Save(new TimeRecord {Project = project_b, Hours = 2, Setting = setting }/*.AddTime(1).AddTime(1)*/); + session.Save(new TimeRecord {Project = project_c, Hours = 7, Setting = setting }/*.AddTime(2).AddTime(3).AddTime(2)*/); + session.Save(new TimeRecord {Project = project_c, ActualJob = job_a, Hours = 3, Setting = setting }/*.AddTime(1).AddTime(1).AddTime(1)*/); + + session.Flush(); + transaction.Commit(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + session.Delete("from TimeRecord"); + session.Delete("from TimeInclude"); + session.Delete("from TimeSetting"); + session.Delete("from Project"); + session.Delete("from Job"); + + session.Flush(); + transaction.Commit(); + } + } + + [Test] + public void CoalesceOnEntitySum() + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var job_a = session.Query().Single(j => j.Name == "Big Job"); + var job_a_hours = session.Query() + .Where(t => (t.ActualJob ?? t.Project.Job) == job_a) + .Sum(t => t.Hours); + Assert.That(job_a_hours, Is.EqualTo(15)); + + var job_b = session.Query().Single(j => j.Name == "Small Job"); + var job_b_hours = session.Query() + .Where(t => (t.ActualJob ?? t.Project.Job) == job_b) + .Sum(t => t.Hours); + Assert.That(job_b_hours, Is.EqualTo(7)); + } + } + + [Test] + public void CoalesceOnEntitySumWithExtraJoin() + { + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var include = session.Query().Single(); + + var job_a = session.Query().Single(j => j.Name == "Big Job"); + var job_a_hours = session.Query() + .Where(t => (t.ActualJob ?? t.Project.Job) == job_a) + .Where(t => t.Setting.Include == include) + .Sum(t => t.Hours); + Assert.That(job_a_hours, Is.EqualTo(15)); + + var job_b = session.Query().Single(j => j.Name == "Small Job"); + var job_b_hours = session.Query() + .Where(t => (t.ActualJob ?? t.Project.Job) == job_b) + .Where(t => t.Setting.Include == include) + .Sum(t => t.Hours); + Assert.That(job_b_hours, Is.EqualTo(7)); + } + } + + [Test] + public void CoalesceOnEntitySubselectSum() + { + AssertDialect(); + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var query = session.Query() + .Select(j => new + { + Job = j, + Hours = session.Query() + .Where(t => (t.ActualJob ?? t.Project.Job) == j) + .Sum(t => (decimal?)t.Hours) ?? 0 + }); + var results = query.ToList(); + + Assert.That(results.Count, Is.EqualTo(2)); + Assert.That(results.Single(x => x.Job.Name == "Big Job").Hours, Is.EqualTo(15)); + Assert.That(results.Single(x => x.Job.Name == "Small Job").Hours, Is.EqualTo(7)); + } + } + + [Test] + public void CoalesceOnEntitySubselectSumWithExtraJoin() + { + AssertDialect(); + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var include = session.Query().Single(); + + var query = session.Query() + .Select(j => new + { + Job = j, + Hours = session.Query() + .Where(t => (t.ActualJob ?? t.Project.Job) == j) + .Where(t => t.Setting.Include == include) + .Sum(t => (decimal?)t.Hours) ?? 0 + }); + var results = query.ToList(); + + Assert.That(results.Count, Is.EqualTo(2)); + Assert.That(results.Single(x => x.Job.Name == "Big Job").Hours, Is.EqualTo(15)); + Assert.That(results.Single(x => x.Job.Name == "Small Job").Hours, Is.EqualTo(7)); + } + } + + [Test] + public void CoalesceOnIdSubselectSum() + { + AssertDialect(); + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var query = session.Query() + .Select(j => new + { + Job = j, + Hours = session.Query() + .Where(t => ((Guid?)t.ActualJob.Id ?? t.Project.Job.Id) == j.Id) + .Sum(t => (decimal?)t.Hours) ?? 0 + }); + var results = query.ToList(); + + Assert.That(results.Count, Is.EqualTo(2)); + Assert.That(results.Single(x => x.Job.Name == "Big Job").Hours, Is.EqualTo(15)); + Assert.That(results.Single(x => x.Job.Name == "Small Job").Hours, Is.EqualTo(7)); + } + } + + [Test] + public void CoalesceOnIdSubselectSumWithExtraJoin() + { + AssertDialect(); + using (var session = OpenSession()) + using (session.BeginTransaction()) + { + var include = session.Query().Single(); + + var query = session.Query() + .Select(j => new + { + Job = j, + Hours = session.Query() + .Where(t => ((Guid?)t.ActualJob.Id ?? t.Project.Job.Id) == j.Id) + .Where(t => t.Setting.Include == include) + .Sum(t => (decimal?)t.Hours) ?? 0 + }); + var results = query.ToList(); + + Assert.That(results.Count, Is.EqualTo(2)); + Assert.That(results.Single(x => x.Job.Name == "Big Job").Hours, Is.EqualTo(15)); + Assert.That(results.Single(x => x.Job.Name == "Small Job").Hours, Is.EqualTo(7)); + } + } + + void AssertDialect() + { + if (Dialect is MsSqlCeDialect) Assert.Ignore(Dialect.GetType() + " does not support this type of query"); + } + } +} diff --git a/src/NHibernate.Test/NHibernate.Test.csproj b/src/NHibernate.Test/NHibernate.Test.csproj index d31e8d99cec..f21597364cf 100644 --- a/src/NHibernate.Test/NHibernate.Test.csproj +++ b/src/NHibernate.Test/NHibernate.Test.csproj @@ -742,6 +742,8 @@ + + diff --git a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g index 03aa65c82b3..fc3eef075bc 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g +++ b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g @@ -123,11 +123,16 @@ query // The query / subquery rule. Pops the current 'from node' context // (list of aliases). unionedQuery! + @init{ + bool oldInSelect = _inSelect; + _inSelect = false; + } @after { // Antlr note: #x_in refers to the input AST, #x refers to the output AST BeforeStatementCompletion( "select" ); ProcessQuery( $s.tree, $unionedQuery.tree ); AfterStatementCompletion( "select" ); + _inSelect = oldInSelect; } : ^( QUERY { BeforeStatement( "select", SELECT ); } // The first phase places the FROM first to make processing the SELECT simpler. @@ -189,11 +194,10 @@ selectClause! ; selectExprList @init{ - bool oldInSelect = _inSelect; _inSelect = true; } : ( selectExpr | aliasedSelectExpr )+ { - _inSelect = oldInSelect; + _inSelect = false; } ;