Skip to content

IN clause support in hql for composite keys on databases without row value constructor support #2209

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

using System;
using System.Collections;
using System.Linq;
using NHibernate.Criterion;
using NUnit.Framework;

Expand Down Expand Up @@ -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<ClassWithCompositeId>());
Assert.That(results.Count, Is.EqualTo(2));
var results2 = await (s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1)")
.SetParameter("id1", id1)
.ListAsync<ClassWithCompositeId>());
var results3 = await (s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1)")
.SetParameter("id1", id1)
.ListAsync<ClassWithCompositeId>());
var results4 = await (s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1, :id2)")
.SetParameter("id1", id1)
.SetParameter("id2", id2)
.ListAsync<ClassWithCompositeId>());

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");
});
}
}

Expand Down
43 changes: 33 additions & 10 deletions src/NHibernate.Test/CompositeId/ClassWithCompositeIdFixture.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections;
using System.Linq;
using NHibernate.Criterion;
using NUnit.Framework;

Expand Down Expand Up @@ -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<ClassWithCompositeId>();
Assert.That(results.Count, Is.EqualTo(2));
var results2 = s.CreateQuery("from ClassWithCompositeId x where x.Id in (:id1)")
.SetParameter("id1", id1)
.List<ClassWithCompositeId>();
var results3 = s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1)")
.SetParameter("id1", id1)
.List<ClassWithCompositeId>();
var results4 = s.CreateQuery("from ClassWithCompositeId x where x.Id not in (:id1, :id2)")
.SetParameter("id1", id1)
.SetParameter("id2", id2)
.List<ClassWithCompositeId>();

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");
});
}
}

Expand Down
10 changes: 5 additions & 5 deletions src/NHibernate/Hql/Ast/ANTLR/Tree/BinaryLogicOperatorNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>();
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 )
{
Expand Down
82 changes: 81 additions & 1 deletion src/NHibernate/Hql/Ast/ANTLR/Tree/InLogicOperatorNode.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Antlr.Runtime;
using NHibernate.Type;

Expand Down Expand Up @@ -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)
{
Expand All @@ -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);
}
}

/// <summary>
/// this is possible for parameter lists and explicit lists. It is completely unreasonable for sub-queries.
/// </summary>
private static bool IsNodeAcceptable(IASTNode rhsNode)
{
return rhsNode == null /* empty IN list */
|| rhsNode is LiteralNode
|| rhsNode is ParameterNode
|| rhsNode.Type == HqlSqlWalker.VECTOR_EXPR;
}

/// <summary>
/// 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') ..."
/// </summary>
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<string>();

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;
}
}
}
1 change: 1 addition & 0 deletions src/NHibernate/NHibernate.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

<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>
<PackageTags>ORM; O/RM; DataBase; DAL; ObjectRelationalMapping; NHibernate; ADO.Net; Core</PackageTags>
<LangVersion>7.2</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down