diff --git a/src/NHibernate.Test/Async/Hql/Ast/WithClauseFixture.cs b/src/NHibernate.Test/Async/Hql/Ast/WithClauseFixture.cs index fadd86df8c0..f14008a2ec0 100644 --- a/src/NHibernate.Test/Async/Hql/Ast/WithClauseFixture.cs +++ b/src/NHibernate.Test/Async/Hql/Ast/WithClauseFixture.cs @@ -8,9 +8,7 @@ //------------------------------------------------------------------------------ -using System; using System.Collections; -using NHibernate.Exceptions; using NHibernate.Hql.Ast.ANTLR; using NUnit.Framework; @@ -54,40 +52,21 @@ public async Task WithClauseFailsWithFetchAsync() } [Test] - public async Task ValidWithSemanticsAsync() + public async Task WithClauseOnSubclassesAsync() { using (var s = OpenSession()) { await (s.CreateQuery( "from Animal a inner join a.offspring o inner join o.mother as m inner join m.father as f with o.bodyWeight > 1").ListAsync()); - } - } - [Test] - public async Task InvalidWithSemanticsAsync() - { - using (ISession s = OpenSession()) - { - // PROBLEM : f.bodyWeight is a reference to a column on the Animal table; however, the 'f' - // alias relates to the Human.friends collection which the aonther Human entity. The issue - // here is the way JoinSequence and Joinable (the persister) interact to generate the - // joins relating to the sublcass/superclass tables - Assert.ThrowsAsync( - () => - s.CreateQuery("from Human h inner join h.friends as f with f.bodyWeight < :someLimit").SetDouble("someLimit", 1).ListAsync()); - - //The query below is no longer throw InvalidWithClauseException but generates "invalid" SQL to better support complex with join clauses. - //Invalid SQL means that additional joins for "o.mother.father" are currently added after "offspring" join. Some DBs can process such query and some can't. - try - { - await (s.CreateQuery("from Human h inner join h.offspring o with o.mother.father = :cousin") - .SetInt32("cousin", 123) - .ListAsync()); - } - catch (GenericADOException) - { - //Apparently SQLite can process queries with wrong join orders - } + // f.bodyWeight is a reference to a column on the Animal table; however, the 'f' + // alias relates to the Human.friends collection which the aonther Human entity. + // Group join allows us to use such constructs + await (s.CreateQuery("from Human h inner join h.friends as f with f.bodyWeight < :someLimit").SetDouble("someLimit", 1).ListAsync()); + + await (s.CreateQuery("from Human h inner join h.offspring o with o.mother.father = :cousin") + .SetInt32("cousin", 123) + .ListAsync()); } } diff --git a/src/NHibernate.Test/Async/Hql/EntityJoinHqlTest.cs b/src/NHibernate.Test/Async/Hql/EntityJoinHqlTest.cs index 0632a01771a..ca151b49690 100644 --- a/src/NHibernate.Test/Async/Hql/EntityJoinHqlTest.cs +++ b/src/NHibernate.Test/Async/Hql/EntityJoinHqlTest.cs @@ -392,6 +392,30 @@ public async Task EntityJoinWithFetchesAsync() } } + [Test] + public async Task WithImpliedJoinOnAssociationAsync() + { + using (var session = OpenSession()) + { + var result = await (session.CreateQuery( + "SELECT s " + + "FROM EntityComplex s left join s.SameTypeChild q on q.SameTypeChild.SameTypeChild.Name = s.Name" + ).ListAsync()); + } + } + + [Test] + public async Task WithImpliedEntityJoinAsync() + { + using (var session = OpenSession()) + { + var result = await (session.CreateQuery( + "SELECT s " + + "FROM EntityComplex s left join EntityComplex q on q.SameTypeChild.SameTypeChild.Name = s.Name" + ).ListAsync()); + } + } + [Test] public async Task CrossJoinAndWhereClauseAsync() { diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH2049/Fixture2049.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH2049/Fixture2049.cs index ce5c05a14b0..7dba9726ebd 100644 --- a/src/NHibernate.Test/Async/NHSpecificTest/NH2049/Fixture2049.cs +++ b/src/NHibernate.Test/Async/NHSpecificTest/NH2049/Fixture2049.cs @@ -65,14 +65,13 @@ public async Task CanCriteriaQueryWithFilterOnJoinClassBaseClassPropertyAsync() } [Test] - [KnownBug("Known bug NH-2049.", "NHibernate.Exceptions.GenericADOException")] public async Task CanHqlQueryWithFilterOnJoinClassBaseClassPropertyAsync() { using (ISession session = OpenSession()) { session.EnableFilter("DeletedCustomer").SetParameter("deleted", false); - var persons = await (session.CreateQuery("from Person as person left join person.IndividualCustomer as indCustomer") - .ListAsync()); + var persons = await (session.CreateQuery("from Person as person inner join fetch person.IndividualCustomer as indCustomer") + .ListAsync()); Assert.That(persons, Has.Count.EqualTo(1)); Assert.That(persons[0].Id, Is.EqualTo(1)); diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH2208/Filter.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH2208/Filter.cs new file mode 100644 index 00000000000..60f0af7aa3b --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/NH2208/Filter.cs @@ -0,0 +1,29 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.NH2208 +{ + using System.Threading.Tasks; + [TestFixture] + public class FilterAsync : BugTestCase + { + [Test] + public async Task TestAsync() + { + using (ISession session = OpenSession()) + { + session.EnableFilter("myfilter"); + await (session.CreateQuery("from E1 e join fetch e.BO").ListAsync()); + } + } + } +} diff --git a/src/NHibernate.Test/Hql/Ast/WithClauseFixture.cs b/src/NHibernate.Test/Hql/Ast/WithClauseFixture.cs index b3e4a44f571..540fa631a84 100644 --- a/src/NHibernate.Test/Hql/Ast/WithClauseFixture.cs +++ b/src/NHibernate.Test/Hql/Ast/WithClauseFixture.cs @@ -1,6 +1,4 @@ -using System; using System.Collections; -using NHibernate.Exceptions; using NHibernate.Hql.Ast.ANTLR; using NUnit.Framework; @@ -42,40 +40,21 @@ public void WithClauseFailsWithFetch() } [Test] - public void ValidWithSemantics() + public void WithClauseOnSubclasses() { using (var s = OpenSession()) { s.CreateQuery( "from Animal a inner join a.offspring o inner join o.mother as m inner join m.father as f with o.bodyWeight > 1").List(); - } - } - [Test] - public void InvalidWithSemantics() - { - using (ISession s = OpenSession()) - { - // PROBLEM : f.bodyWeight is a reference to a column on the Animal table; however, the 'f' - // alias relates to the Human.friends collection which the aonther Human entity. The issue - // here is the way JoinSequence and Joinable (the persister) interact to generate the - // joins relating to the sublcass/superclass tables - Assert.Throws( - () => - s.CreateQuery("from Human h inner join h.friends as f with f.bodyWeight < :someLimit").SetDouble("someLimit", 1).List()); - - //The query below is no longer throw InvalidWithClauseException but generates "invalid" SQL to better support complex with join clauses. - //Invalid SQL means that additional joins for "o.mother.father" are currently added after "offspring" join. Some DBs can process such query and some can't. - try - { - s.CreateQuery("from Human h inner join h.offspring o with o.mother.father = :cousin") - .SetInt32("cousin", 123) - .List(); - } - catch (GenericADOException) - { - //Apparently SQLite can process queries with wrong join orders - } + // f.bodyWeight is a reference to a column on the Animal table; however, the 'f' + // alias relates to the Human.friends collection which the aonther Human entity. + // Group join allows us to use such constructs + s.CreateQuery("from Human h inner join h.friends as f with f.bodyWeight < :someLimit").SetDouble("someLimit", 1).List(); + + s.CreateQuery("from Human h inner join h.offspring o with o.mother.father = :cousin") + .SetInt32("cousin", 123) + .List(); } } diff --git a/src/NHibernate.Test/Hql/EntityJoinHqlTest.cs b/src/NHibernate.Test/Hql/EntityJoinHqlTest.cs index f01b6303d30..1fc649d6302 100644 --- a/src/NHibernate.Test/Hql/EntityJoinHqlTest.cs +++ b/src/NHibernate.Test/Hql/EntityJoinHqlTest.cs @@ -382,7 +382,7 @@ public void EntityJoinWithFetches() } [Test, Ignore("Failing for unrelated reasons")] - public void ImplicitJoinAndWithClause() + public void CrossJoinAndWithClause() { //This is about complex theta style join fix that was implemented in hibernate along with entity join functionality //https://hibernate.atlassian.net/browse/HHH-7321 @@ -391,12 +391,36 @@ public void ImplicitJoinAndWithClause() { session.CreateQuery( "SELECT s " + - "FROM EntityComplex s, EntityComplex q " + + "FROM EntityComplex s CROSS JOIN EntityComplex q " + "LEFT JOIN s.SameTypeChild AS sa WITH sa.SameTypeChild.Id = q.SameTypeChild.Id" ).List(); } } + [Test] + public void WithImpliedJoinOnAssociation() + { + using (var session = OpenSession()) + { + var result = session.CreateQuery( + "SELECT s " + + "FROM EntityComplex s left join s.SameTypeChild q on q.SameTypeChild.SameTypeChild.Name = s.Name" + ).List(); + } + } + + [Test] + public void WithImpliedEntityJoin() + { + using (var session = OpenSession()) + { + var result = session.CreateQuery( + "SELECT s " + + "FROM EntityComplex s left join EntityComplex q on q.SameTypeChild.SameTypeChild.Name = s.Name" + ).List(); + } + } + [Test] public void CrossJoinAndWhereClause() { diff --git a/src/NHibernate.Test/NHSpecificTest/NH2049/Fixture2049.cs b/src/NHibernate.Test/NHSpecificTest/NH2049/Fixture2049.cs index 9d0e4ed7a20..8d5356b488a 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH2049/Fixture2049.cs +++ b/src/NHibernate.Test/NHSpecificTest/NH2049/Fixture2049.cs @@ -54,14 +54,13 @@ public void CanCriteriaQueryWithFilterOnJoinClassBaseClassProperty() } [Test] - [KnownBug("Known bug NH-2049.", "NHibernate.Exceptions.GenericADOException")] public void CanHqlQueryWithFilterOnJoinClassBaseClassProperty() { using (ISession session = OpenSession()) { session.EnableFilter("DeletedCustomer").SetParameter("deleted", false); - var persons = session.CreateQuery("from Person as person left join person.IndividualCustomer as indCustomer") - .List(); + var persons = session.CreateQuery("from Person as person inner join fetch person.IndividualCustomer as indCustomer") + .List(); Assert.That(persons, Has.Count.EqualTo(1)); Assert.That(persons[0].Id, Is.EqualTo(1)); diff --git a/src/NHibernate.Test/NHSpecificTest/NH2208/Filter.cs b/src/NHibernate.Test/NHSpecificTest/NH2208/Filter.cs index 86ef23441e8..2301c20e3ba 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH2208/Filter.cs +++ b/src/NHibernate.Test/NHSpecificTest/NH2208/Filter.cs @@ -5,7 +5,7 @@ namespace NHibernate.Test.NHSpecificTest.NH2208 [TestFixture] public class Filter : BugTestCase { - [Test, Ignore("Not fixed yet")] + [Test] public void Test() { using (ISession session = OpenSession()) diff --git a/src/NHibernate/Engine/JoinSequence.cs b/src/NHibernate/Engine/JoinSequence.cs index ddd68a3dd17..3b3bae924cc 100644 --- a/src/NHibernate/Engine/JoinSequence.cs +++ b/src/NHibernate/Engine/JoinSequence.cs @@ -1,6 +1,8 @@ -using System.Collections; +using System; using System.Collections.Generic; +using System.Linq; using System.Text; +using NHibernate.Hql.Ast.ANTLR.Tree; using NHibernate.Persister.Collection; using NHibernate.Persister.Entity; using NHibernate.SqlCommand; @@ -133,80 +135,61 @@ public JoinFragment ToJoinFragment() public JoinFragment ToJoinFragment(IDictionary enabledFilters, bool includeExtraJoins) { - return ToJoinFragment(enabledFilters, includeExtraJoins, null, null); + return ToJoinFragment(enabledFilters, includeExtraJoins, true, null); } + public JoinFragment ToJoinFragment(IDictionary enabledFilters, bool includeAllSubclassJoins, SqlString withClause) + { + return ToJoinFragment(enabledFilters, includeAllSubclassJoins, true, withClause); + } + + // Since 5.4 + [Obsolete("This method has no more usages and will be removed in a future version.")] public JoinFragment ToJoinFragment( IDictionary enabledFilters, bool includeExtraJoins, SqlString withClauseFragment, string withClauseJoinAlias) { - return ToJoinFragment(enabledFilters, includeExtraJoins, withClauseFragment, withClauseJoinAlias, rootAlias); + return ToJoinFragment(enabledFilters, includeExtraJoins, true, withClauseFragment); } - internal virtual JoinFragment ToJoinFragment( + internal JoinFragment ToJoinFragment( IDictionary enabledFilters, - bool includeExtraJoins, - SqlString withClauseFragment, - string withClauseJoinAlias, - string withRootAlias) + bool includeAllSubclassJoins, + bool renderSubclassJoins, + SqlString withClauseFragment) { QueryJoinFragment joinFragment = new QueryJoinFragment(factory.Dialect, useThetaStyle); if (rootJoinable != null) { - joinFragment.AddCrossJoin(rootJoinable.TableName, withRootAlias); - string filterCondition = rootJoinable.FilterFragment(withRootAlias, enabledFilters); + joinFragment.AddCrossJoin(rootJoinable.TableName, rootAlias); + string filterCondition = rootJoinable.FilterFragment(rootAlias, enabledFilters); // JoinProcessor needs to know if the where clause fragment came from a dynamic filter or not so it // can put the where clause fragment in the right place in the SQL AST. 'hasFilterCondition' keeps track // of that fact. joinFragment.HasFilterCondition = joinFragment.AddCondition(filterCondition); - if (includeExtraJoins) - { - //TODO: not quite sure about the full implications of this! - AddExtraJoins(joinFragment, withRootAlias, rootJoinable, true); - } + AddSubclassJoins(joinFragment, rootAlias, rootJoinable, true, includeAllSubclassJoins); } + var withClauses = new SqlString[joins.Count]; IJoinable last = rootJoinable; - for (int i = 0; i < joins.Count; i++) { Join join = joins[i]; - string on = join.AssociationType.GetOnCondition(join.Alias, factory, enabledFilters); - SqlString condition = new SqlString(); - if (last != null && - IsManyToManyRoot(last) && - ((IQueryableCollection)last).ElementType == join.AssociationType) - { - // the current join represents the join between a many-to-many association table - // and its "target" table. Here we need to apply any additional filters - // defined specifically on the many-to-many - string manyToManyFilter = ((IQueryableCollection)last) - .GetManyToManyFilterFragment(join.Alias, enabledFilters); - condition = new SqlString("".Equals(manyToManyFilter) - ? on - : "".Equals(on) - ? manyToManyFilter - : on + " and " + manyToManyFilter); - } - else - { - // NH Different behavior : NH1179 and NH1293 - // Apply filters in Many-To-One association - var enabledForManyToOne = FilterHelper.GetEnabledForManyToOne(enabledFilters); - condition = new SqlString(string.IsNullOrEmpty(on) && enabledForManyToOne.Count > 0 - ? join.Joinable.FilterFragment(join.Alias, enabledForManyToOne) - : on); - } - if (withClauseFragment != null) - { - if (join.Alias.Equals(withClauseJoinAlias)) - { - condition = condition.Append(" and ", withClauseFragment); - } - } + withClauses[i] = GetWithClause(enabledFilters, ref withClauseFragment, join, last); + last = join.Joinable; + } + + if (rootJoinable == null && ProcessAsTableGroupJoin(includeAllSubclassJoins, withClauses, joinFragment)) + { + return joinFragment; + } + + for (int i = 0; i < joins.Count; i++) + { + Join join = joins[i]; // NH: the variable "condition" have to be a SqlString because it may contains Parameter instances with BackTrack joinFragment.AddJoin( @@ -215,18 +198,15 @@ internal virtual JoinFragment ToJoinFragment( join.LHSColumns, JoinHelper.GetRHSColumnNames(join.AssociationType, factory), join.JoinType, - condition - ); - if (includeExtraJoins) - { - //TODO: not quite sure about the full implications of this! - AddExtraJoins(joinFragment, join.Alias, join.Joinable, join.JoinType == JoinType.InnerJoin); - } - last = join.Joinable; + withClauses[i] + ); + + AddSubclassJoins(joinFragment, join.Alias, join.Joinable, join.JoinType == JoinType.InnerJoin, renderSubclassJoins); } + if (next != null) { - joinFragment.AddFragment(next.ToJoinFragment(enabledFilters, includeExtraJoins)); + joinFragment.AddFragment(next.ToJoinFragment(enabledFilters, includeAllSubclassJoins)); } joinFragment.AddCondition(conditions.ToSqlString()); if (isFromPart) @@ -234,6 +214,156 @@ internal virtual JoinFragment ToJoinFragment( return joinFragment; } + private SqlString GetWithClause(IDictionary enabledFilters, ref SqlString withClauseFragment, Join join, IJoinable last) + { + string on = join.AssociationType.GetOnCondition(join.Alias, factory, enabledFilters); + var withConditions = new List(); + + if (!string.IsNullOrEmpty(on)) + withConditions.Add(on); + + if (last != null && + IsManyToManyRoot(last) && + ((IQueryableCollection) last).ElementType == join.AssociationType) + { + // the current join represents the join between a many-to-many association table + // and its "target" table. Here we need to apply any additional filters + // defined specifically on the many-to-many + string manyToManyFilter = ((IQueryableCollection) last) + .GetManyToManyFilterFragment(join.Alias, enabledFilters); + + if (!string.IsNullOrEmpty(manyToManyFilter)) + withConditions.Add(manyToManyFilter); + } + else if (string.IsNullOrEmpty(on)) + { + // NH Different behavior : NH1179 and NH1293 + // Apply filters in Many-To-One association + var enabledForManyToOne = FilterHelper.GetEnabledForManyToOne(enabledFilters); + if (enabledForManyToOne.Count > 0) + withConditions.Add(join.Joinable.FilterFragment(join.Alias, enabledForManyToOne)); + } + + if (withClauseFragment != null && !IsManyToManyRoot(join.Joinable)) + { + withConditions.Add(withClauseFragment); + withClauseFragment = null; + } + + return SqlStringHelper.JoinParts(" and ", withConditions); + } + + private bool ProcessAsTableGroupJoin(bool includeAllSubclassJoins, SqlString[] withClauseFragments, JoinFragment joinFragment) + { + if (!NeedsTableGroupJoin(joins, withClauseFragments, includeAllSubclassJoins)) + return false; + + var first = joins[0]; + string joinString = ANSIJoinFragment.GetJoinString(first.JoinType); + joinFragment.AddFromFragmentString( + new SqlString( + joinString, + " (", + first.Joinable.TableName, + " ", + first.Alias + )); + + foreach (var join in joins) + { + if (join != first) + joinFragment.AddJoin( + join.Joinable.TableName, + join.Alias, + join.LHSColumns, + JoinHelper.GetRHSColumnNames(join.AssociationType, factory), + join.JoinType, + SqlString.Empty); + + AddSubclassJoins( + joinFragment, + join.Alias, + join.Joinable, + // TODO (from hibernate): Think about if this could be made always true + // NH Specific: made always true (original check: join.JoinType == JoinType.InnerJoin) + true, + includeAllSubclassJoins + ); + } + + var tableGroupWithClause = GetTableGroupJoinWithClause(withClauseFragments, first); + joinFragment.AddFromFragmentString(tableGroupWithClause); + return true; + } + + private SqlString GetTableGroupJoinWithClause(SqlString[] withClauseFragments, Join first) + { + SqlStringBuilder fromFragment = new SqlStringBuilder(); + fromFragment.Add(")").Add(" on "); + + String[] lhsColumns = first.LHSColumns; + var isAssociationJoin = lhsColumns.Length > 0; + if (isAssociationJoin) + { + String rhsAlias = first.Alias; + String[] rhsColumns = JoinHelper.GetRHSColumnNames(first.AssociationType, factory); + for (int j = 0; j < lhsColumns.Length; j++) + { + fromFragment.Add(lhsColumns[j]); + fromFragment.Add("="); + fromFragment.Add(rhsAlias); + fromFragment.Add("."); + fromFragment.Add(rhsColumns[j]); + if (j < lhsColumns.Length - 1) + { + fromFragment.Add(" and "); + } + } + } + + for (var i= 0; i < withClauseFragments.Length; i++) + { + var withClause = withClauseFragments[i]; + if (SqlStringHelper.IsEmpty(withClause)) + continue; + + if (withClause.StartsWithCaseInsensitive(" and ")) + { + if (!isAssociationJoin) + { + withClause = withClause.Substring(4); + } + } + else if (isAssociationJoin) + { + fromFragment.Add(" and "); + } + + fromFragment.Add(withClause); + } + + return fromFragment.ToSqlString(); + } + + private bool NeedsTableGroupJoin(List joins, SqlString[] withClauseFragments, bool includeSubclasses) + { + // If the rewrite is disabled or we don't have a with clause, we don't need a table group join + if ( /*!collectionJoinSubquery ||*/ withClauseFragments.All(x => SqlStringHelper.IsEmpty(x))) + { + return false; + } + // If we only have one join, a table group join is only necessary if subclass columns are used in the with clause + if (joins.Count == 1) + { + return joins[0].Joinable is AbstractEntityPersister persister && persister.HasSubclassJoins(includeSubclasses); + //NH Specific: No alias processing + //return isSubclassAliasDereferenced( joins[ 0], withClauseFragment ); + } + + //NH Specific: No alias processing (see hibernate JoinSequence.NeedsTableGroupJoin) + return true; + } + private bool IsManyToManyRoot(IJoinable joinable) { if (joinable != null && joinable.IsCollection) @@ -249,9 +379,9 @@ private bool IsIncluded(string alias) return selector != null && selector.IncludeSubclasses(alias); } - private protected void AddExtraJoins(JoinFragment joinFragment, string alias, IJoinable joinable, bool innerJoin) + private void AddSubclassJoins(JoinFragment joinFragment, String alias, IJoinable joinable, bool innerJoin, bool includeSubclassJoins) { - bool include = IsIncluded(alias); + bool include = includeSubclassJoins && IsIncluded(alias); joinFragment.AddJoins(joinable.FromJoinFragment(alias, innerJoin, include), joinable.WhereJoinFragment(alias, innerJoin, include)); } @@ -327,5 +457,11 @@ public interface ISelector internal string RootAlias => rootAlias; public ISessionFactoryImplementor Factory => factory; + + public JoinSequence AddJoin(FromElement fromElement) + { + joins.AddRange(fromElement.JoinSequence.joins); + return this; + } } } diff --git a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs index e75da1bf7c7..31f8254b230 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs @@ -1229,8 +1229,7 @@ private void HandleWithFragment(FromElement fromElement, IASTNode hqlWithNode) sql.whereExpr(); - var withClauseFragment = new SqlString("(", sql.GetSQL(), ")"); - fromElement.SetWithClauseFragment(visitor.GetJoinAlias(), withClauseFragment); + fromElement.WithClauseFragment = new SqlString("(", sql.GetSQL(), ")"); } catch (SemanticException) { @@ -1250,44 +1249,15 @@ private void HandleWithFragment(FromElement fromElement, IASTNode hqlWithNode) class WithClauseVisitor : IVisitationStrategy { private readonly FromElement _joinFragment; - private readonly bool _multiTable; public WithClauseVisitor(FromElement fromElement) { _joinFragment = fromElement; - _multiTable = (fromElement.EntityPersister as IQueryable)?.IsMultiTable == true; } public void Visit(IASTNode node) { - // todo : currently expects that the individual with expressions apply to the same sql table join. - // This may not be the case for joined-subclass where the property values - // might be coming from different tables in the joined hierarchy. At some - // point we should expand this to support that capability. However, that has - // some difficulties: - // 1) the biggest is how to handle ORs when the individual comparisons are - // linked to different sql joins. - // 2) here we would need to track each comparison individually, along with - // the join alias to which it applies and then pass that information - // back to the FromElement so it can pass it along to the JoinSequence - if (_multiTable && node is DotNode dotNode) - { - FromElement fromElement = dotNode.FromElement; - if (_joinFragment == fromElement) - { - var joinAlias = ExtractAppliedAlias(dotNode); - //See WithClauseFixture.InvalidWithSemantics to understand the logic behind this check - // todo : temporary - // needed because currently persister is the one that - // creates and renders the join fragments for inheritence - // hierarchies... - if (joinAlias != _joinFragment.TableAlias) - { - throw new InvalidWithClauseException("with clause can only reference columns in the driving table"); - } - } - } - else if (node is ParameterNode paramNode) + if (node is ParameterNode paramNode) { ApplyParameterSpecification(paramNode.HqlParameterSpecification); } @@ -1314,11 +1284,8 @@ private void ApplyParameterSpecification(IParameterSpecification paramSpec) _joinFragment.AddEmbeddedParameter(paramSpec); } - private static String ExtractAppliedAlias(IASTNode dotNode) - { - return dotNode.Text.Substring( 0, dotNode.Text.IndexOf( '.' ) ); - } - + // Since 5.4 + [Obsolete("This method has no more usages and will be removed in a future version.")] public String GetJoinAlias() { return _joinFragment.TableAlias; diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinFromElement.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinFromElement.cs index e2f4ce33365..acb6b2b81db 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinFromElement.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinFromElement.cs @@ -18,7 +18,6 @@ public EntityJoinFromElement(FromClause fromClause, IQueryable entityPersister, JoinSequence = new EntityJoinJoinSequenceImpl( SessionFactoryHelper.Factory, entityType, - entityPersister.TableName, tableAlias, joinType); diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinJoinSequenceImpl.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinJoinSequenceImpl.cs index 430515223d0..1c41d857cd0 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinJoinSequenceImpl.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinJoinSequenceImpl.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using NHibernate.Engine; using NHibernate.SqlCommand; using NHibernate.Type; @@ -8,44 +7,11 @@ namespace NHibernate.Hql.Ast.ANTLR.Tree { class EntityJoinJoinSequenceImpl : JoinSequence { - private readonly EntityType _entityType; - private readonly string _tableName; - private readonly string _tableAlias; - private readonly JoinType _joinType; - - public EntityJoinJoinSequenceImpl(ISessionFactoryImplementor factory, EntityType entityType, string tableName, string tableAlias, JoinType joinType):base(factory) + public EntityJoinJoinSequenceImpl(ISessionFactoryImplementor factory, EntityType entityType, string tableAlias, JoinType joinType):base(factory) { - _entityType = entityType; - _tableName = tableName; - _tableAlias = tableAlias; - _joinType = joinType; - } - - internal override JoinFragment ToJoinFragment( - IDictionary enabledFilters, - bool includeExtraJoins, - SqlString withClauseFragment, - string withClauseJoinAlias, - string withRootAlias) - { - var joinFragment = new ANSIJoinFragment(); - - var on = withClauseFragment ?? SqlString.Empty; - //Note: filters logic commented due to following issues - //1) Original code is non functional as SqlString is immutable and so all Append results are lost. Correct code would look like: on = on.Append(filters); - //2) Also it seems GetOnCondition always returns empty string for entity join (as IsReferenceToPrimaryKey is always true). - // So if filters for entity join really make sense we need to inline GetOnCondition part that retrieves filters -// var filters = _entityType.GetOnCondition(_tableAlias, Factory, enabledFilters); -// if (!string.IsNullOrEmpty(filters)) -// { -// on.Append(" and ").Append(filters); -// } - joinFragment.AddJoin(_tableName, _tableAlias, Array.Empty(), Array.Empty(), _joinType, on); - if (includeExtraJoins) - { - AddExtraJoins(joinFragment, _tableAlias, _entityType.GetAssociatedJoinable(Factory), _joinType == JoinType.InnerJoin); - } - return joinFragment; + AddJoin(entityType, tableAlias, joinType, Array.Empty()); + //Note: filters don't work with entity joins + //as EntytyType.GetOnCondition always returns empty string for entity join (as IsReferenceToPrimaryKey is always true). } } } diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElement.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElement.cs index fa4796baebc..ffd15584604 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElement.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElement.cs @@ -76,6 +76,8 @@ public void SetAllPropertyFetch(bool fetch) _isAllPropertyFetch = fetch; } + // Since 5.4 + [Obsolete("This method has no more usages and will be removed in a future version.")] public void SetWithClauseFragment(String withClauseJoinAlias, SqlString withClauseFragment) { _withClauseJoinAlias = withClauseJoinAlias; @@ -179,6 +181,8 @@ public virtual bool IsImplied get { return false; } // This is an explicit FROM element. } + internal bool? IsPartOfJoinSequence { get; set; } + public bool IsDereferencedBySuperclassOrSubclassProperty { get @@ -311,8 +315,11 @@ public FromElement RealOrigin public SqlString WithClauseFragment { get { return _withClauseFragment; } + set { _withClauseFragment = value; } } + // Since 5.4 + [Obsolete("This method has no more usages and will be removed in a future version.")] public string WithClauseJoinAlias { get { return _withClauseJoinAlias; } @@ -518,18 +525,16 @@ internal string[] GetIdentityColumns() { propertyName = NHibernate.Persister.Entity.EntityPersister.EntityID; } - if (Walker.StatementType == HqlSqlWalker.SELECT || Walker.IsSubQuery) - { - cols = GetPropertyMapping(propertyName).ToColumns(table, propertyName); - } - else - { - cols = GetPropertyMapping(propertyName).ToColumns(propertyName); - } + + cols = UseTableAliases + ? GetPropertyMapping(propertyName).ToColumns(table, propertyName) + : GetPropertyMapping(propertyName).ToColumns(propertyName); return cols; } + internal bool UseTableAliases => Walker.StatementType == HqlSqlWalker.SELECT || Walker.IsSubQuery; + public void HandlePropertyBeingDereferenced(IType propertySource, string propertyName) { if (QueryableCollection != null && CollectionProperties.IsCollectionProperty(propertyName)) diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElementType.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElementType.cs index 78b677b8e44..5856bfad1dc 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElementType.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElementType.cs @@ -119,7 +119,12 @@ public JoinSequence JoinSequence var joinable = _persister as IJoinable; if (joinable != null) { - return _fromElement.SessionFactoryHelper.CreateJoinSequence().SetRoot(joinable, TableAlias); + // the delete and update statements created here will never be executed when IsMultiTable is true, + // only the where clause will be used by MultiTableUpdateExecutor/MultiTableDeleteExecutor. In that case + // we have to use the alias from the persister. + var useAlias = _fromElement.UseTableAliases || _fromElement.Queryable.IsMultiTable; + + return _fromElement.SessionFactoryHelper.CreateJoinSequence().SetRoot(joinable, useAlias ? TableAlias : string.Empty); } return null; // TODO: Should this really return null? If not, figure out something better to do here. diff --git a/src/NHibernate/Hql/Ast/ANTLR/Util/JoinProcessor.cs b/src/NHibernate/Hql/Ast/ANTLR/Util/JoinProcessor.cs index 8fbcfba7fd7..9140e3780f0 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Util/JoinProcessor.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Util/JoinProcessor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using NHibernate.Engine; using NHibernate.Hql.Ast.ANTLR.Tree; using NHibernate.SqlCommand; @@ -66,8 +67,6 @@ public void ProcessJoins(QueryNode query) public void ProcessJoins(IRestrictableStatement query) { FromClause fromClause = query.FromClause; - var supportRootAlias = !(query is DeleteStatement || query is UpdateStatement); - IList fromElements; if ( DotNode.UseThetaStyleImplicitJoins ) { @@ -85,14 +84,50 @@ public void ProcessJoins(IRestrictableStatement query) fromElements.Add(t[i]); } } - else + else { fromElements = fromClause.GetFromElementsTyped(); + + for (var index = fromElements.Count - 1; index >= 0; index--) + { + var fromElement = fromElements[index]; + // We found an implied from element that is used in the WITH clause of another from element, so it need to become part of it's join sequence + if (fromElement.IsImplied && fromElement.IsPartOfJoinSequence == null) + { + var origin = fromElement.Origin; + while(origin.IsImplied) + { + origin = origin.Origin; + origin.IsPartOfJoinSequence = false; + } + + if (origin.WithClauseFragment?.Contains(fromElement.TableAlias + ".") == true) + { + List elements = new List(); + while(fromElement.IsImplied) + { + elements.Add(fromElement); + // This from element will be rendered as part of the origins join sequence + fromElement.Text = string.Empty; + fromElement.IsPartOfJoinSequence = true; + fromElement = fromElement.Origin; + } + + for (var i = elements.Count - 1; i >= 0; i--) + { + origin.JoinSequence.AddJoin(elements[i]); + } + } + } + } } // Iterate through the alias,JoinSequence pairs and generate SQL token nodes. foreach (FromElement fromElement in fromElements) { + if(fromElement.IsPartOfJoinSequence == true) + continue; + JoinSequence join = fromElement.JoinSequence; join.SetSelector(new JoinSequenceSelector(_walker, fromClause, fromElement)); @@ -100,18 +135,16 @@ public void ProcessJoins(IRestrictableStatement query) // the delete and update statements created here will never be executed when IsMultiTable is true, // only the where clause will be used by MultiTableUpdateExecutor/MultiTableDeleteExecutor. In that case // we have to use the alias from the persister. - AddJoinNodes( query, join, fromElement, supportRootAlias || fromElement.Queryable.IsMultiTable); + AddJoinNodes( query, join, fromElement); } } - private void AddJoinNodes(IRestrictableStatement query, JoinSequence join, FromElement fromElement, bool supportRootAlias) + private void AddJoinNodes(IRestrictableStatement query, JoinSequence join, FromElement fromElement) { JoinFragment joinFragment = join.ToJoinFragment( _walker.EnabledFilters, fromElement.UseFromFragment || fromElement.IsDereferencedBySuperclassOrSubclassProperty, - fromElement.WithClauseFragment, - fromElement.WithClauseJoinAlias, - supportRootAlias ? join.RootAlias : string.Empty + fromElement.WithClauseFragment ); SqlString frag = joinFragment.ToFromFragmentString; diff --git a/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs b/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs index 78afb6e2b09..2c71d037c0b 100644 --- a/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs +++ b/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs @@ -3779,12 +3779,10 @@ private JoinFragment CreateJoin(string name, bool innerjoin, bool includeSubclas int tableSpan = SubclassTableSpan; for (int j = 1; j < tableSpan; j++) //notice that we skip the first table; it is the driving table! { - string[] idCols = StringHelper.Qualify(name, GetJoinIdKeyColumns(j)); //some joins may be to non primary keys - - bool joinIsIncluded = IsClassOrSuperclassTable(j) || - (includeSubclasses && !IsSubclassTableSequentialSelect(j) && !IsSubclassTableLazy(j)); + var joinIsIncluded = IsJoinIncluded(includeSubclasses, j); if (joinIsIncluded) { + string[] idCols = StringHelper.Qualify(name, GetJoinIdKeyColumns(j)); //some joins may be to non primary keys join.AddJoin(GetSubclassTableName(j), GenerateTableAlias(name, j), idCols, @@ -3798,6 +3796,26 @@ private JoinFragment CreateJoin(string name, bool innerjoin, bool includeSubclas return join; } + internal bool HasSubclassJoins(bool includeSubclasses) + { + if (SubclassTableSpan == 1) + return false; + + for (int i = 1; i < SubclassTableSpan; ++i) + { + if (IsJoinIncluded(includeSubclasses, i)) + return true; + } + + return false; + } + + private bool IsJoinIncluded(bool includeSubclasses, int j) + { + return IsClassOrSuperclassTable(j) || + (includeSubclasses && !IsSubclassTableSequentialSelect(j) && !IsSubclassTableLazy(j)); + } + private JoinFragment CreateJoin(int[] tableNumbers, string drivingAlias) { string[] keyCols = StringHelper.Qualify(drivingAlias, GetSubclassTableKeyColumns(tableNumbers[0])); diff --git a/src/NHibernate/SqlCommand/ANSIJoinFragment.cs b/src/NHibernate/SqlCommand/ANSIJoinFragment.cs index 063c361eb56..a0c17158cb8 100644 --- a/src/NHibernate/SqlCommand/ANSIJoinFragment.cs +++ b/src/NHibernate/SqlCommand/ANSIJoinFragment.cs @@ -18,27 +18,7 @@ public override void AddJoin(string tableName, string alias, string[] fkColumns, public override void AddJoin(string tableName, string alias, string[] fkColumns, string[] pkColumns, JoinType joinType, SqlString on) { - string joinString; - switch (joinType) - { - case JoinType.InnerJoin: - joinString = " inner join "; - break; - case JoinType.LeftOuterJoin: - joinString = " left outer join "; - break; - case JoinType.RightOuterJoin: - joinString = " right outer join "; - break; - case JoinType.FullJoin: - joinString = " full outer join "; - break; - case JoinType.CrossJoin: - joinString = " cross join "; - break; - default: - throw new AssertionFailure("undefined join type"); - } + var joinString = GetJoinString(joinType); _fromFragment.Add(joinString).Add(tableName).Add(" ").Add(alias).Add(" "); if (joinType == JoinType.CrossJoin) @@ -66,6 +46,25 @@ public override void AddJoin(string tableName, string alias, string[] fkColumns, AddCondition(_fromFragment, on); } + internal static string GetJoinString(JoinType joinType) + { + switch (joinType) + { + case JoinType.InnerJoin: + return " inner join "; + case JoinType.LeftOuterJoin: + return " left outer join "; + case JoinType.RightOuterJoin: + return " right outer join "; + case JoinType.FullJoin: + return " full outer join "; + case JoinType.CrossJoin: + return " cross join "; + default: + throw new AssertionFailure("undefined join type"); + } + } + public override SqlString ToFromFragmentString { get { return _fromFragment.ToSqlString(); } diff --git a/src/NHibernate/SqlCommand/SqlString.cs b/src/NHibernate/SqlCommand/SqlString.cs index 33caf5e0096..793453a5d6a 100644 --- a/src/NHibernate/SqlCommand/SqlString.cs +++ b/src/NHibernate/SqlCommand/SqlString.cs @@ -425,6 +425,11 @@ internal int IndexOfOrdinal(string text) return IndexOf(text, 0, _length, StringComparison.Ordinal); } + internal bool Contains(string text) + { + return IndexOfOrdinal(text) >= 0; + } + /// /// Returns the index of the first occurrence of , case-insensitive. /// diff --git a/src/NHibernate/SqlCommand/SqlStringHelper.cs b/src/NHibernate/SqlCommand/SqlStringHelper.cs index d1aa2af5c3b..4530881e259 100644 --- a/src/NHibernate/SqlCommand/SqlStringHelper.cs +++ b/src/NHibernate/SqlCommand/SqlStringHelper.cs @@ -30,6 +30,27 @@ public static SqlString Join(SqlString separator, IEnumerable objects) return buf.ToSqlString(); } + internal static SqlString JoinParts(object separator, IList parts) + { + if (parts.Count == 0) + return SqlString.Empty; + + if (parts.Count == 1) + return parts[0] is SqlString sqlstring + ? sqlstring + : new SqlString(parts); + + var buf = new SqlStringBuilder(); + + buf.AddObject(parts[0]); + for (var index = 1; index < parts.Count; index++) + { + buf.AddObject(separator).AddObject(parts[index]); + } + + return buf.ToSqlString(); + } + internal static SqlString Join(string separator, IList strings) { if (strings.Count == 0)