From 12d12f64cd6b44d4abf30c94de8842d153a4c8ea Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Tue, 21 May 2019 20:47:13 +0300 Subject: [PATCH 1/4] Hql composite keys In clause support for DBs without row value constructor support --- .../ClassWithCompositeIdFixture.cs | 31 ++++++-- .../ClassWithCompositeIdFixture.cs | 31 ++++++-- .../Ast/ANTLR/Tree/BinaryLogicOperatorNode.cs | 12 +-- .../Hql/Ast/ANTLR/Tree/InLogicOperatorNode.cs | 79 ++++++++++++++++++- 4 files changed, 135 insertions(+), 18 deletions(-) diff --git a/src/NHibernate.Test/Async/CompositeId/ClassWithCompositeIdFixture.cs b/src/NHibernate.Test/Async/CompositeId/ClassWithCompositeIdFixture.cs index 2e0094f1948..e8c042059d9 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,9 +244,7 @@ public async Task HqlAsync() [Test] public async Task HqlInClauseAsync() { - //Need to port changes from InLogicOperatorNode.mutateRowValueConstructorSyntaxInInListSyntax - if (!Dialect.SupportsRowValueConstructorSyntaxInInList) - Assert.Ignore(); + var id3 = new Id(id.KeyString, id.GetKeyShort(), secondId.KeyDateTime); // insert the new objects using (ISession s = OpenSession()) @@ -253,18 +252,38 @@ public async Task HqlInClauseAsync() { 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(id3))); await (t.CommitAsync()); } using (var s = OpenSession()) { - var results = await (s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1, :id2)") + var results1 = await (s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1, :id2)") .SetParameter("id1", id) .SetParameter("id2", secondId) .ListAsync()); - Assert.That(results.Count, Is.EqualTo(2)); + var results2 = await (s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1)") + .SetParameter("id1", id) + .ListAsync()); + var results3 = await (s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1)") + .SetParameter("id1", id) + .ListAsync()); + var results4 = await (s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1, :id2)") + .SetParameter("id1", id) + .SetParameter("id2", secondId) + .ListAsync()); + + Assert.Multiple( + () => + { + Assert.That(results1.Count, Is.EqualTo(2), "in multiple ids"); + Assert.That(results2.Count, Is.EqualTo(1), "in single id"); + Assert.That(results3.Count, Is.EqualTo(2), "not in single id"); + Assert.That(results3.First().Id, Is.EqualTo(id).Or.EqualTo(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..ac6e7ab35f5 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,9 +233,7 @@ public void Hql() [Test] public void HqlInClause() { - //Need to port changes from InLogicOperatorNode.mutateRowValueConstructorSyntaxInInListSyntax - if (!Dialect.SupportsRowValueConstructorSyntaxInInList) - Assert.Ignore(); + var id3 = new Id(id.KeyString, id.GetKeyShort(), secondId.KeyDateTime); // insert the new objects using (ISession s = OpenSession()) @@ -242,18 +241,38 @@ public void HqlInClause() { 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(id3)); t.Commit(); } using (var s = OpenSession()) { - var results = s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1, :id2)") + var results1 = s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1, :id2)") .SetParameter("id1", id) .SetParameter("id2", secondId) .List(); - Assert.That(results.Count, Is.EqualTo(2)); + var results2 = s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1)") + .SetParameter("id1", id) + .List(); + var results3 = s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1)") + .SetParameter("id1", id) + .List(); + var results4 = s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1, :id2)") + .SetParameter("id1", id) + .SetParameter("id2", secondId) + .List(); + + Assert.Multiple( + () => + { + Assert.That(results1.Count, Is.EqualTo(2), "in multiple ids"); + Assert.That(results2.Count, Is.EqualTo(1), "in single id"); + Assert.That(results3.Count, Is.EqualTo(2), "not in single id"); + Assert.That(results3.First().Id, Is.EqualTo(id).Or.EqualTo(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..372cb011e71 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/BinaryLogicOperatorNode.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/BinaryLogicOperatorNode.cs @@ -200,17 +200,19 @@ public IParameterSpecification[] GetEmbeddedParameters() return embeddedParameters.ToArray(); } - private string Translate(int valueElements, string comparisonText, string[] lhsElementTexts, string[] rhsElementTexts) + //TODO 6.0: Change to private protected (C#7.2 feature) + internal 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) + //TODO 6.0: Change to private protected (C#7.2 feature) + internal 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..4e944f8301d 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,81 @@ 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) => + 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; } } } From a57b58b4cd0490162c9af3f1e139cb5c5590b4aa Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Thu, 12 Sep 2019 09:40:51 +0300 Subject: [PATCH 2/4] Fix test --- .../ClassWithCompositeIdFixture.cs | 26 +++++++++++-------- .../ClassWithCompositeIdFixture.cs | 26 +++++++++++-------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/NHibernate.Test/Async/CompositeId/ClassWithCompositeIdFixture.cs b/src/NHibernate.Test/Async/CompositeId/ClassWithCompositeIdFixture.cs index e8c042059d9..ef35b2a5c0a 100644 --- a/src/NHibernate.Test/Async/CompositeId/ClassWithCompositeIdFixture.cs +++ b/src/NHibernate.Test/Async/CompositeId/ClassWithCompositeIdFixture.cs @@ -244,14 +244,16 @@ public async Task HqlAsync() [Test] public async Task HqlInClauseAsync() { - var id3 = new Id(id.KeyString, id.GetKeyShort(), secondId.KeyDateTime); + 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(id1) {OneProperty = 5})); + await (s.SaveAsync(new ClassWithCompositeId(id2) {OneProperty = 10})); await (s.SaveAsync(new ClassWithCompositeId(id3))); await (t.CommitAsync()); @@ -260,27 +262,29 @@ public async Task HqlInClauseAsync() using (var s = OpenSession()) { var results1 = await (s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1, :id2)") - .SetParameter("id1", id) - .SetParameter("id2", secondId) + .SetParameter("id1", id1) + .SetParameter("id2", id2) .ListAsync()); var results2 = await (s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1)") - .SetParameter("id1", id) + .SetParameter("id1", id1) .ListAsync()); var results3 = await (s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1)") - .SetParameter("id1", id) + .SetParameter("id1", id1) .ListAsync()); var results4 = await (s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1, :id2)") - .SetParameter("id1", id) - .SetParameter("id2", secondId) + .SetParameter("id1", id1) + .SetParameter("id2", id2) .ListAsync()); Assert.Multiple( () => { - Assert.That(results1.Count, Is.EqualTo(2), "in multiple ids"); + 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.First().Id, Is.EqualTo(id).Or.EqualTo(id3), "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 ac6e7ab35f5..0236aae348b 100644 --- a/src/NHibernate.Test/CompositeId/ClassWithCompositeIdFixture.cs +++ b/src/NHibernate.Test/CompositeId/ClassWithCompositeIdFixture.cs @@ -233,14 +233,16 @@ public void Hql() [Test] public void HqlInClause() { - var id3 = new Id(id.KeyString, id.GetKeyShort(), secondId.KeyDateTime); + 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(id1) {OneProperty = 5}); + s.Save(new ClassWithCompositeId(id2) {OneProperty = 10}); s.Save(new ClassWithCompositeId(id3)); t.Commit(); @@ -249,27 +251,29 @@ public void HqlInClause() using (var s = OpenSession()) { var results1 = s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1, :id2)") - .SetParameter("id1", id) - .SetParameter("id2", secondId) + .SetParameter("id1", id1) + .SetParameter("id2", id2) .List(); var results2 = s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1)") - .SetParameter("id1", id) + .SetParameter("id1", id1) .List(); var results3 = s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1)") - .SetParameter("id1", id) + .SetParameter("id1", id1) .List(); var results4 = s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1, :id2)") - .SetParameter("id1", id) - .SetParameter("id2", secondId) + .SetParameter("id1", id1) + .SetParameter("id2", id2) .List(); Assert.Multiple( () => { - Assert.That(results1.Count, Is.EqualTo(2), "in multiple ids"); + 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.First().Id, Is.EqualTo(id).Or.EqualTo(id3), "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"); }); From 37edfe2c9df57096aebf9b82c989c219c926087b Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Thu, 12 Sep 2019 09:46:47 +0300 Subject: [PATCH 3/4] Use C#7.2 private protected --- .../Hql/Ast/ANTLR/Tree/BinaryLogicOperatorNode.cs | 6 ++---- src/NHibernate/NHibernate.csproj | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/BinaryLogicOperatorNode.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/BinaryLogicOperatorNode.cs index 372cb011e71..85acd059b0b 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/BinaryLogicOperatorNode.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/BinaryLogicOperatorNode.cs @@ -200,8 +200,7 @@ public IParameterSpecification[] GetEmbeddedParameters() return embeddedParameters.ToArray(); } - //TODO 6.0: Change to private protected (C#7.2 feature) - internal 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 string[valueElements]; for (int i = 0; i < valueElements; i++) @@ -211,8 +210,7 @@ internal string Translate(int valueElements, string comparisonText, string[] lhs return string.Concat("(", string.Join(" and ", multicolumnComparisonClauses), ")"); } - //TODO 6.0: Change to private protected (C#7.2 feature) - internal 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/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 From 33057019729f5e6c9e10a31ba002ed09a9b7f7d9 Mon Sep 17 00:00:00 2001 From: Alexander Zaytsev Date: Fri, 13 Sep 2019 17:24:43 +1200 Subject: [PATCH 4/4] Cleanup code --- .../Hql/Ast/ANTLR/Tree/InLogicOperatorNode.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/InLogicOperatorNode.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/InLogicOperatorNode.cs index 4e944f8301d..0ad4e404bda 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/InLogicOperatorNode.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/InLogicOperatorNode.cs @@ -82,10 +82,13 @@ public override void Initialize() /// /// this is possible for parameter lists and explicit lists. It is completely unreasonable for sub-queries. /// - private static bool IsNodeAcceptable(IASTNode rhsNode) => - rhsNode == null /* empty IN list */ || rhsNode is LiteralNode - || rhsNode is ParameterNode - || rhsNode.Type == HqlSqlWalker.VECTOR_EXPR; + 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