Skip to content

Commit 2c53c34

Browse files
bahusoidhazzik
authored andcommitted
Add support for IN clause n hql for composite keys on databases not supporting row value constructor syntax (#2209)
1 parent b845938 commit 2c53c34

File tree

5 files changed

+153
-26
lines changed

5 files changed

+153
-26
lines changed

src/NHibernate.Test/Async/CompositeId/ClassWithCompositeIdFixture.cs

+33-10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
using System;
1212
using System.Collections;
13+
using System.Linq;
1314
using NHibernate.Criterion;
1415
using NUnit.Framework;
1516

@@ -243,28 +244,50 @@ public async Task HqlAsync()
243244
[Test]
244245
public async Task HqlInClauseAsync()
245246
{
246-
//Need to port changes from InLogicOperatorNode.mutateRowValueConstructorSyntaxInInListSyntax
247-
if (!Dialect.SupportsRowValueConstructorSyntaxInInList)
248-
Assert.Ignore();
247+
var id1 = id;
248+
var id2 = secondId;
249+
var id3 = new Id(id.KeyString, id.GetKeyShort(), id2.KeyDateTime);
249250

250251
// insert the new objects
251252
using (ISession s = OpenSession())
252253
using (ITransaction t = s.BeginTransaction())
253254
{
254-
await (s.SaveAsync(new ClassWithCompositeId(id) {OneProperty = 5}));
255-
await (s.SaveAsync(new ClassWithCompositeId(secondId) {OneProperty = 10}));
256-
await (s.SaveAsync(new ClassWithCompositeId(new Id(id.KeyString, id.GetKeyShort(), secondId.KeyDateTime))));
255+
await (s.SaveAsync(new ClassWithCompositeId(id1) {OneProperty = 5}));
256+
await (s.SaveAsync(new ClassWithCompositeId(id2) {OneProperty = 10}));
257+
await (s.SaveAsync(new ClassWithCompositeId(id3)));
257258

258259
await (t.CommitAsync());
259260
}
260261

261262
using (var s = OpenSession())
262263
{
263-
var results = await (s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1, :id2)")
264-
.SetParameter("id1", id)
265-
.SetParameter("id2", secondId)
264+
var results1 = await (s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1, :id2)")
265+
.SetParameter("id1", id1)
266+
.SetParameter("id2", id2)
266267
.ListAsync<ClassWithCompositeId>());
267-
Assert.That(results.Count, Is.EqualTo(2));
268+
var results2 = await (s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1)")
269+
.SetParameter("id1", id1)
270+
.ListAsync<ClassWithCompositeId>());
271+
var results3 = await (s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1)")
272+
.SetParameter("id1", id1)
273+
.ListAsync<ClassWithCompositeId>());
274+
var results4 = await (s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1, :id2)")
275+
.SetParameter("id1", id1)
276+
.SetParameter("id2", id2)
277+
.ListAsync<ClassWithCompositeId>());
278+
279+
Assert.Multiple(
280+
() =>
281+
{
282+
Assert.That(results1.Count, Is.EqualTo(2), "in multiple ids");
283+
Assert.That(results1.Select(x => x.Id), Is.EquivalentTo(new[] {id1, id2}), "in multiple ids");
284+
Assert.That(results2.Count, Is.EqualTo(1), "in single id");
285+
Assert.That(results2.Single().Id, Is.EqualTo(id1), "in single id");
286+
Assert.That(results3.Count, Is.EqualTo(2), "not in single id");
287+
Assert.That(results3.Select(x => x.Id), Is.EquivalentTo(new[] {id2, id3}), "not in single id");
288+
Assert.That(results4.Count, Is.EqualTo(1), "not in multiple ids");
289+
Assert.That(results4.Single().Id, Is.EqualTo(id3), "not in multiple ids");
290+
});
268291
}
269292
}
270293

src/NHibernate.Test/CompositeId/ClassWithCompositeIdFixture.cs

+33-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections;
3+
using System.Linq;
34
using NHibernate.Criterion;
45
using NUnit.Framework;
56

@@ -232,28 +233,50 @@ public void Hql()
232233
[Test]
233234
public void HqlInClause()
234235
{
235-
//Need to port changes from InLogicOperatorNode.mutateRowValueConstructorSyntaxInInListSyntax
236-
if (!Dialect.SupportsRowValueConstructorSyntaxInInList)
237-
Assert.Ignore();
236+
var id1 = id;
237+
var id2 = secondId;
238+
var id3 = new Id(id.KeyString, id.GetKeyShort(), id2.KeyDateTime);
238239

239240
// insert the new objects
240241
using (ISession s = OpenSession())
241242
using (ITransaction t = s.BeginTransaction())
242243
{
243-
s.Save(new ClassWithCompositeId(id) {OneProperty = 5});
244-
s.Save(new ClassWithCompositeId(secondId) {OneProperty = 10});
245-
s.Save(new ClassWithCompositeId(new Id(id.KeyString, id.GetKeyShort(), secondId.KeyDateTime)));
244+
s.Save(new ClassWithCompositeId(id1) {OneProperty = 5});
245+
s.Save(new ClassWithCompositeId(id2) {OneProperty = 10});
246+
s.Save(new ClassWithCompositeId(id3));
246247

247248
t.Commit();
248249
}
249250

250251
using (var s = OpenSession())
251252
{
252-
var results = s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1, :id2)")
253-
.SetParameter("id1", id)
254-
.SetParameter("id2", secondId)
253+
var results1 = s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1, :id2)")
254+
.SetParameter("id1", id1)
255+
.SetParameter("id2", id2)
255256
.List<ClassWithCompositeId>();
256-
Assert.That(results.Count, Is.EqualTo(2));
257+
var results2 = s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1)")
258+
.SetParameter("id1", id1)
259+
.List<ClassWithCompositeId>();
260+
var results3 = s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1)")
261+
.SetParameter("id1", id1)
262+
.List<ClassWithCompositeId>();
263+
var results4 = s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1, :id2)")
264+
.SetParameter("id1", id1)
265+
.SetParameter("id2", id2)
266+
.List<ClassWithCompositeId>();
267+
268+
Assert.Multiple(
269+
() =>
270+
{
271+
Assert.That(results1.Count, Is.EqualTo(2), "in multiple ids");
272+
Assert.That(results1.Select(x => x.Id), Is.EquivalentTo(new[] {id1, id2}), "in multiple ids");
273+
Assert.That(results2.Count, Is.EqualTo(1), "in single id");
274+
Assert.That(results2.Single().Id, Is.EqualTo(id1), "in single id");
275+
Assert.That(results3.Count, Is.EqualTo(2), "not in single id");
276+
Assert.That(results3.Select(x => x.Id), Is.EquivalentTo(new[] {id2, id3}), "not in single id");
277+
Assert.That(results4.Count, Is.EqualTo(1), "not in multiple ids");
278+
Assert.That(results4.Single().Id, Is.EqualTo(id3), "not in multiple ids");
279+
});
257280
}
258281
}
259282

src/NHibernate/Hql/Ast/ANTLR/Tree/BinaryLogicOperatorNode.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -200,17 +200,17 @@ public IParameterSpecification[] GetEmbeddedParameters()
200200
return embeddedParameters.ToArray();
201201
}
202202

203-
private string Translate(int valueElements, string comparisonText, string[] lhsElementTexts, string[] rhsElementTexts)
203+
private protected string Translate(int valueElements, string comparisonText, string[] lhsElementTexts, string[] rhsElementTexts)
204204
{
205-
var multicolumnComparisonClauses = new List<string>();
205+
var multicolumnComparisonClauses = new string[valueElements];
206206
for (int i = 0; i < valueElements; i++)
207207
{
208-
multicolumnComparisonClauses.Add(string.Format("{0} {1} {2}", lhsElementTexts[i], comparisonText, rhsElementTexts[i]));
208+
multicolumnComparisonClauses[i] = string.Join(" ", lhsElementTexts[i], comparisonText, rhsElementTexts[i]);
209209
}
210-
return "(" + string.Join(" and ", multicolumnComparisonClauses.ToArray()) + ")";
210+
return string.Concat("(", string.Join(" and ", multicolumnComparisonClauses), ")");
211211
}
212212

213-
private static string[] ExtractMutationTexts(IASTNode operand, int count)
213+
private protected static string[] ExtractMutationTexts(IASTNode operand, int count)
214214
{
215215
if ( operand is ParameterNode )
216216
{

src/NHibernate/Hql/Ast/ANTLR/Tree/InLogicOperatorNode.cs

+81-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using Antlr.Runtime;
34
using NHibernate.Type;
45

@@ -39,9 +40,10 @@ public override void Initialize()
3940
// some form of property ref and that the children of the in-list represent
4041
// one-or-more params.
4142
var lhsNode = lhs as SqlNode;
43+
IType lhsType = null;
4244
if (lhsNode != null)
4345
{
44-
IType lhsType = lhsNode.DataType;
46+
lhsType = lhsNode.DataType;
4547
IASTNode inListChild = inList.GetChild(0);
4648
while (inListChild != null)
4749
{
@@ -53,6 +55,84 @@ public override void Initialize()
5355
inListChild = inListChild.NextSibling;
5456
}
5557
}
58+
59+
var sessionFactory = SessionFactoryHelper.Factory;
60+
if (sessionFactory.Dialect.SupportsRowValueConstructorSyntaxInInList)
61+
return;
62+
63+
lhsType = lhsType ?? ExtractDataType(lhs);
64+
if (lhsType == null)
65+
return;
66+
67+
var rhsNode = inList.GetFirstChild();
68+
if (rhsNode == null || !IsNodeAcceptable(rhsNode))
69+
return;
70+
71+
var lhsColumnSpan = lhsType.GetColumnSpan(sessionFactory);
72+
var rhsColumnSpan = rhsNode.Type == HqlSqlWalker.VECTOR_EXPR
73+
? rhsNode.ChildCount
74+
: ExtractDataType(rhsNode)?.GetColumnSpan(sessionFactory) ?? 0;
75+
76+
if (lhsColumnSpan > 1 && rhsColumnSpan > 1)
77+
{
78+
MutateRowValueConstructorSyntaxInInListSyntax(lhs, lhsColumnSpan, rhsNode, rhsColumnSpan);
79+
}
80+
}
81+
82+
/// <summary>
83+
/// this is possible for parameter lists and explicit lists. It is completely unreasonable for sub-queries.
84+
/// </summary>
85+
private static bool IsNodeAcceptable(IASTNode rhsNode)
86+
{
87+
return rhsNode == null /* empty IN list */
88+
|| rhsNode is LiteralNode
89+
|| rhsNode is ParameterNode
90+
|| rhsNode.Type == HqlSqlWalker.VECTOR_EXPR;
91+
}
92+
93+
/// <summary>
94+
/// Mutate the subtree relating to a row-value-constructor in "in" list to instead use
95+
/// a series of ORen and ANDed predicates. This allows multi-column type comparisons
96+
/// and explicit row-value-constructor in "in" list syntax even on databases which do
97+
/// not support row-value-constructor in "in" list.
98+
///
99+
/// For example, here we'd mutate "... where (col1, col2) in ( ('val1', 'val2'), ('val3', 'val4') ) ..." to
100+
/// "... where (col1 = 'val1' and col2 = 'val2') or (col1 = 'val3' and val2 = 'val4') ..."
101+
/// </summary>
102+
private void MutateRowValueConstructorSyntaxInInListSyntax(IASTNode lhsNode, int lhsColumnSpan, IASTNode rhsNode, int rhsColumnSpan)
103+
{
104+
//NHibenate specific: In hibernate they recreate new tree in HQL. In NHibernate we just replace node with generated SQL
105+
// (same as it's done in BinaryLogicOperatorNode)
106+
107+
string[] lhsElementTexts = ExtractMutationTexts(lhsNode, lhsColumnSpan);
108+
109+
if (lhsNode is ParameterNode lhsParam && lhsParam.HqlParameterSpecification != null)
110+
{
111+
AddEmbeddedParameter(lhsParam.HqlParameterSpecification);
112+
}
113+
114+
var negated = Type == HqlSqlWalker.NOT_IN;
115+
116+
var andElementsNodeList = new List<string>();
117+
118+
while (rhsNode != null)
119+
{
120+
string[] rhsElementTexts = ExtractMutationTexts(rhsNode, rhsColumnSpan);
121+
if (rhsNode is ParameterNode rhsParam && rhsParam.HqlParameterSpecification != null)
122+
{
123+
AddEmbeddedParameter(rhsParam.HqlParameterSpecification);
124+
}
125+
126+
var text = Translate(lhsColumnSpan, "=", lhsElementTexts, rhsElementTexts);
127+
128+
andElementsNodeList.Add(negated ? string.Concat("( not ", text, ")") : text);
129+
rhsNode = rhsNode.NextSibling;
130+
}
131+
132+
ClearChildren();
133+
Type = HqlSqlWalker.SQL_TOKEN;
134+
var sqlToken = string.Join(negated ? " and " : " or ", andElementsNodeList);
135+
Text = andElementsNodeList.Count > 1 ? string.Concat("(", sqlToken, ")") : sqlToken;
56136
}
57137
}
58138
}

src/NHibernate/NHibernate.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
<PackageDescription>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.</PackageDescription>
1414
<PackageTags>ORM; O/RM; DataBase; DAL; ObjectRelationalMapping; NHibernate; ADO.Net; Core</PackageTags>
15+
<LangVersion>7.2</LangVersion>
1516
</PropertyGroup>
1617

1718
<ItemGroup>

0 commit comments

Comments
 (0)