diff --git a/src/NHibernate.Test/Async/CompositeId/ClassWithCompositeIdFixture.cs b/src/NHibernate.Test/Async/CompositeId/ClassWithCompositeIdFixture.cs index 2e0094f1948..ef35b2a5c0a 100644 --- a/src/NHibernate.Test/Async/CompositeId/ClassWithCompositeIdFixture.cs +++ b/src/NHibernate.Test/Async/CompositeId/ClassWithCompositeIdFixture.cs @@ -10,6 +10,7 @@ using System; using System.Collections; +using System.Linq; using NHibernate.Criterion; using NUnit.Framework; @@ -243,28 +244,50 @@ public async Task HqlAsync() [Test] public async Task HqlInClauseAsync() { - //Need to port changes from InLogicOperatorNode.mutateRowValueConstructorSyntaxInInListSyntax - if (!Dialect.SupportsRowValueConstructorSyntaxInInList) - Assert.Ignore(); + var id1 = id; + var id2 = secondId; + var id3 = new Id(id.KeyString, id.GetKeyShort(), id2.KeyDateTime); // insert the new objects using (ISession s = OpenSession()) using (ITransaction t = s.BeginTransaction()) { - await (s.SaveAsync(new ClassWithCompositeId(id) {OneProperty = 5})); - await (s.SaveAsync(new ClassWithCompositeId(secondId) {OneProperty = 10})); - await (s.SaveAsync(new ClassWithCompositeId(new Id(id.KeyString, id.GetKeyShort(), secondId.KeyDateTime)))); + await (s.SaveAsync(new ClassWithCompositeId(id1) {OneProperty = 5})); + await (s.SaveAsync(new ClassWithCompositeId(id2) {OneProperty = 10})); + await (s.SaveAsync(new ClassWithCompositeId(id3))); await (t.CommitAsync()); } using (var s = OpenSession()) { - var results = await (s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1, :id2)") - .SetParameter("id1", id) - .SetParameter("id2", secondId) + var results1 = await (s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1, :id2)") + .SetParameter("id1", id1) + .SetParameter("id2", id2) .ListAsync()); - Assert.That(results.Count, Is.EqualTo(2)); + var results2 = await (s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1)") + .SetParameter("id1", id1) + .ListAsync()); + var results3 = await (s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1)") + .SetParameter("id1", id1) + .ListAsync()); + var results4 = await (s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1, :id2)") + .SetParameter("id1", id1) + .SetParameter("id2", id2) + .ListAsync()); + + Assert.Multiple( + () => + { + Assert.That(results1.Count, Is.EqualTo(2), "in multiple ids"); + Assert.That(results1.Select(x => x.Id), Is.EquivalentTo(new[] {id1, id2}), "in multiple ids"); + Assert.That(results2.Count, Is.EqualTo(1), "in single id"); + Assert.That(results2.Single().Id, Is.EqualTo(id1), "in single id"); + Assert.That(results3.Count, Is.EqualTo(2), "not in single id"); + Assert.That(results3.Select(x => x.Id), Is.EquivalentTo(new[] {id2, id3}), "not in single id"); + Assert.That(results4.Count, Is.EqualTo(1), "not in multiple ids"); + Assert.That(results4.Single().Id, Is.EqualTo(id3), "not in multiple ids"); + }); } } diff --git a/src/NHibernate.Test/CompositeId/ClassWithCompositeIdFixture.cs b/src/NHibernate.Test/CompositeId/ClassWithCompositeIdFixture.cs index 58a2cdbecb8..0236aae348b 100644 --- a/src/NHibernate.Test/CompositeId/ClassWithCompositeIdFixture.cs +++ b/src/NHibernate.Test/CompositeId/ClassWithCompositeIdFixture.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Linq; using NHibernate.Criterion; using NUnit.Framework; @@ -232,28 +233,50 @@ public void Hql() [Test] public void HqlInClause() { - //Need to port changes from InLogicOperatorNode.mutateRowValueConstructorSyntaxInInListSyntax - if (!Dialect.SupportsRowValueConstructorSyntaxInInList) - Assert.Ignore(); + var id1 = id; + var id2 = secondId; + var id3 = new Id(id.KeyString, id.GetKeyShort(), id2.KeyDateTime); // insert the new objects using (ISession s = OpenSession()) using (ITransaction t = s.BeginTransaction()) { - s.Save(new ClassWithCompositeId(id) {OneProperty = 5}); - s.Save(new ClassWithCompositeId(secondId) {OneProperty = 10}); - s.Save(new ClassWithCompositeId(new Id(id.KeyString, id.GetKeyShort(), secondId.KeyDateTime))); + s.Save(new ClassWithCompositeId(id1) {OneProperty = 5}); + s.Save(new ClassWithCompositeId(id2) {OneProperty = 10}); + s.Save(new ClassWithCompositeId(id3)); t.Commit(); } using (var s = OpenSession()) { - var results = s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1, :id2)") - .SetParameter("id1", id) - .SetParameter("id2", secondId) + var results1 = s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1, :id2)") + .SetParameter("id1", id1) + .SetParameter("id2", id2) .List(); - Assert.That(results.Count, Is.EqualTo(2)); + var results2 = s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1)") + .SetParameter("id1", id1) + .List(); + var results3 = s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1)") + .SetParameter("id1", id1) + .List(); + var results4 = s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1, :id2)") + .SetParameter("id1", id1) + .SetParameter("id2", id2) + .List(); + + Assert.Multiple( + () => + { + Assert.That(results1.Count, Is.EqualTo(2), "in multiple ids"); + Assert.That(results1.Select(x => x.Id), Is.EquivalentTo(new[] {id1, id2}), "in multiple ids"); + Assert.That(results2.Count, Is.EqualTo(1), "in single id"); + Assert.That(results2.Single().Id, Is.EqualTo(id1), "in single id"); + Assert.That(results3.Count, Is.EqualTo(2), "not in single id"); + Assert.That(results3.Select(x => x.Id), Is.EquivalentTo(new[] {id2, id3}), "not in single id"); + Assert.That(results4.Count, Is.EqualTo(1), "not in multiple ids"); + Assert.That(results4.Single().Id, Is.EqualTo(id3), "not in multiple ids"); + }); } } diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/BinaryLogicOperatorNode.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/BinaryLogicOperatorNode.cs index c3d0c4b3ecf..85acd059b0b 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/BinaryLogicOperatorNode.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/BinaryLogicOperatorNode.cs @@ -200,17 +200,17 @@ public IParameterSpecification[] GetEmbeddedParameters() return embeddedParameters.ToArray(); } - private string Translate(int valueElements, string comparisonText, string[] lhsElementTexts, string[] rhsElementTexts) + private protected string Translate(int valueElements, string comparisonText, string[] lhsElementTexts, string[] rhsElementTexts) { - var multicolumnComparisonClauses = new List(); + var multicolumnComparisonClauses = new string[valueElements]; for (int i = 0; i < valueElements; i++) { - multicolumnComparisonClauses.Add(string.Format("{0} {1} {2}", lhsElementTexts[i], comparisonText, rhsElementTexts[i])); + multicolumnComparisonClauses[i] = string.Join(" ", lhsElementTexts[i], comparisonText, rhsElementTexts[i]); } - return "(" + string.Join(" and ", multicolumnComparisonClauses.ToArray()) + ")"; + return string.Concat("(", string.Join(" and ", multicolumnComparisonClauses), ")"); } - private static string[] ExtractMutationTexts(IASTNode operand, int count) + private protected static string[] ExtractMutationTexts(IASTNode operand, int count) { if ( operand is ParameterNode ) { diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/InLogicOperatorNode.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/InLogicOperatorNode.cs index 5557bb1d4c6..0ad4e404bda 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/InLogicOperatorNode.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/InLogicOperatorNode.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Antlr.Runtime; using NHibernate.Type; @@ -39,9 +40,10 @@ public override void Initialize() // some form of property ref and that the children of the in-list represent // one-or-more params. var lhsNode = lhs as SqlNode; + IType lhsType = null; if (lhsNode != null) { - IType lhsType = lhsNode.DataType; + lhsType = lhsNode.DataType; IASTNode inListChild = inList.GetChild(0); while (inListChild != null) { @@ -53,6 +55,84 @@ public override void Initialize() inListChild = inListChild.NextSibling; } } + + var sessionFactory = SessionFactoryHelper.Factory; + if (sessionFactory.Dialect.SupportsRowValueConstructorSyntaxInInList) + return; + + lhsType = lhsType ?? ExtractDataType(lhs); + if (lhsType == null) + return; + + var rhsNode = inList.GetFirstChild(); + if (rhsNode == null || !IsNodeAcceptable(rhsNode)) + return; + + var lhsColumnSpan = lhsType.GetColumnSpan(sessionFactory); + var rhsColumnSpan = rhsNode.Type == HqlSqlWalker.VECTOR_EXPR + ? rhsNode.ChildCount + : ExtractDataType(rhsNode)?.GetColumnSpan(sessionFactory) ?? 0; + + if (lhsColumnSpan > 1 && rhsColumnSpan > 1) + { + MutateRowValueConstructorSyntaxInInListSyntax(lhs, lhsColumnSpan, rhsNode, rhsColumnSpan); + } + } + + /// + /// this is possible for parameter lists and explicit lists. It is completely unreasonable for sub-queries. + /// + private static bool IsNodeAcceptable(IASTNode rhsNode) + { + return rhsNode == null /* empty IN list */ + || rhsNode is LiteralNode + || rhsNode is ParameterNode + || rhsNode.Type == HqlSqlWalker.VECTOR_EXPR; + } + + /// + /// Mutate the subtree relating to a row-value-constructor in "in" list to instead use + /// a series of ORen and ANDed predicates. This allows multi-column type comparisons + /// and explicit row-value-constructor in "in" list syntax even on databases which do + /// not support row-value-constructor in "in" list. + /// + /// For example, here we'd mutate "... where (col1, col2) in ( ('val1', 'val2'), ('val3', 'val4') ) ..." to + /// "... where (col1 = 'val1' and col2 = 'val2') or (col1 = 'val3' and val2 = 'val4') ..." + /// + private void MutateRowValueConstructorSyntaxInInListSyntax(IASTNode lhsNode, int lhsColumnSpan, IASTNode rhsNode, int rhsColumnSpan) + { + //NHibenate specific: In hibernate they recreate new tree in HQL. In NHibernate we just replace node with generated SQL + // (same as it's done in BinaryLogicOperatorNode) + + string[] lhsElementTexts = ExtractMutationTexts(lhsNode, lhsColumnSpan); + + if (lhsNode is ParameterNode lhsParam && lhsParam.HqlParameterSpecification != null) + { + AddEmbeddedParameter(lhsParam.HqlParameterSpecification); + } + + var negated = Type == HqlSqlWalker.NOT_IN; + + var andElementsNodeList = new List(); + + while (rhsNode != null) + { + string[] rhsElementTexts = ExtractMutationTexts(rhsNode, rhsColumnSpan); + if (rhsNode is ParameterNode rhsParam && rhsParam.HqlParameterSpecification != null) + { + AddEmbeddedParameter(rhsParam.HqlParameterSpecification); + } + + var text = Translate(lhsColumnSpan, "=", lhsElementTexts, rhsElementTexts); + + andElementsNodeList.Add(negated ? string.Concat("( not ", text, ")") : text); + rhsNode = rhsNode.NextSibling; + } + + ClearChildren(); + Type = HqlSqlWalker.SQL_TOKEN; + var sqlToken = string.Join(negated ? " and " : " or ", andElementsNodeList); + Text = andElementsNodeList.Count > 1 ? string.Concat("(", sqlToken, ")") : sqlToken; } } } diff --git a/src/NHibernate/NHibernate.csproj b/src/NHibernate/NHibernate.csproj index 2661cc1bde7..2ef674c8484 100644 --- a/src/NHibernate/NHibernate.csproj +++ b/src/NHibernate/NHibernate.csproj @@ -12,6 +12,7 @@ NHibernate is a mature, open source object-relational mapper for the .NET framework. It is actively developed, fully featured and used in thousands of successful projects. ORM; O/RM; DataBase; DAL; ObjectRelationalMapping; NHibernate; ADO.Net; Core + 7.2