Skip to content

NH-3918 - Nominate equality expressions in SELECT clauses #522

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 1 commit into from
Nov 26, 2016
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
29 changes: 29 additions & 0 deletions src/NHibernate.Test/Linq/SelectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,35 @@ public void CanSelectConditionalEntity()
Assert.That(fatherInsteadOfChild, Has.Exactly(2).With.Property("SerialNumber").EqualTo("5678"));
}

[Test]
public void CanSelectConditionalEntityWithCast()
{
var fatherInsteadOfChild = db.Mammals.Select(a => a.Father.SerialNumber == "5678" ? (object)a.Father : (object)a).ToList();
Assert.That(fatherInsteadOfChild, Has.Exactly(2).With.Property("SerialNumber").EqualTo("5678"));
}

[Test]
public void CanSelectConditionalEntityValue()
{
var fatherInsteadOfChild = db.Animals.Select(a => a.Father.SerialNumber == "5678" ? a.Father.SerialNumber : a.SerialNumber).ToList();
Assert.That(fatherInsteadOfChild, Has.Exactly(2).EqualTo("5678"));
}

[Test]
public void CanSelectConditionalEntityValueWithEntityComparison()
{
var father = db.Animals.Single(a => a.SerialNumber == "5678");
var fatherInsteadOfChild = db.Animals.Select(a => a.Father == father ? a.Father.SerialNumber : a.SerialNumber).ToList();
Assert.That(fatherInsteadOfChild, Has.Exactly(2).EqualTo("5678"));
}

[Test]
public void CanSelectConditionalEntityValueWithEntityComparisonRepeat()
{
// Check again in the same ISessionFactory to ensure caching doesn't cause failures
CanSelectConditionalEntityValueWithEntityComparison();
}

[Test]
public void CanSelectConditionalObject()
{
Expand Down
127 changes: 127 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/NH3918/FixtureByCode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using NHibernate.Cfg.MappingSchema;
using NHibernate.Linq;
using NHibernate.Mapping.ByCode;
using NUnit.Framework;

namespace NHibernate.Test.NHSpecificTest.NH3918
{
public class ByCodeFixture : TestCaseMappingByCode
{
protected override HbmMapping GetMappings()
{
var mapper = new ModelMapper();
mapper.Class<Owner>(rc =>
{
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
rc.Property(x => x.Name);
});
mapper.Class<Entity>(rc =>
{
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
rc.Property(x => x.Name);
rc.ManyToOne(m => m.Owner, m =>
{
m.Column("OwnerId");
});
});

return mapper.CompileMappingForAllExplicitlyAddedEntities();
}

protected override void OnSetUp()
{
using (ISession session = OpenSession())
using (ITransaction transaction = session.BeginTransaction())
{
var bob = CreateOwner(session, "Bob");
var carl = CreateOwner(session, "Carl");
var doug = CreateOwner(session, "Doug");

CreateEntity(session, "Test 1", bob);
CreateEntity(session, "Test 2", carl);
CreateEntity(session, "Test 3", doug);
CreateEntity(session, "Test 4", bob);
CreateEntity(session, "Test 5", carl);
CreateEntity(session, "Test 6", doug);

session.Flush();
transaction.Commit();
}
}

protected Owner CreateOwner(ISession session, string name)
{
var t = new Owner { Name = name };
session.Save(t);
return t;
}

protected void CreateEntity(ISession session, string name, Owner owner)
{
var t = new Entity
{
Name = name,
Owner = owner,
};
session.Save(t);
}

protected override void OnTearDown()
{
using (ISession session = OpenSession())
using (ITransaction transaction = session.BeginTransaction())
{
session.Delete("from Entity");
session.Delete("from Owner");

session.Flush();
transaction.Commit();
}
}

[Test]
public void EntityComparisonTest()
{
using (ISession session = OpenSession())
using (session.BeginTransaction())
{
var bob = session.Query<Owner>().Single(o => o.Name == "Bob");

var queryWithWhere = session.Query<Entity>()
.Where(WhereExpression(bob))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for future - this indenting looks wrong depending on tab-size, because tab has been used also for alignment within a statement. Tabs are for indenting, space for alignment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted. NH differs from my usual development that uses spaces for everything, so I'm used to just pressing tab to align things.

.Select(e => e.Name);
var queryWithSelect = session.Query<Entity>()
.Select(SelectExpression(bob));

var resultsFromWhere = queryWithWhere.ToList();
var resultsFromSelect = queryWithSelect.ToList();

Assert.That(resultsFromSelect.Where(x => (bool)x[1]).Select(x => (string)x[0]), Is.EquivalentTo(resultsFromWhere));
}
}

[Test]
public void EntityComparisonTestAgain()
{
// When the entire fixture is run this will execute the test again within the same ISessionFactory which will test caching
EntityComparisonTest();
}

protected Expression<Func<Entity, bool>> WhereExpression(Owner owner)
{
return e => e.Owner == owner;
}

protected Expression<Func<Entity, object[]>> SelectExpression(Owner owner)
{
return e => new object[]
{
e.Name,
e.Owner == owner
};
}
}
}
25 changes: 25 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/NH3918/Model.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;

namespace NHibernate.Test.NHSpecificTest.NH3918
{
public interface IModelObject
{
Guid Id { get; set; }
string Name { get; set; }
}

public class Entity : IModelObject
{
public virtual Guid Id { get; set; }
public virtual string Name { get; set; }

public virtual Owner Owner { get; set; }
}

public class Owner : IModelObject
{
public virtual Guid Id { get; set; }
public virtual string Name { get; set; }
}
}
2 changes: 2 additions & 0 deletions src/NHibernate.Test/NHibernate.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,8 @@
<Compile Include="NHSpecificTest\NH2204\Fixture.cs" />
<Compile Include="NHSpecificTest\NH3912\BatcherLovingEntity.cs" />
<Compile Include="NHSpecificTest\NH3912\ReusableBatcherFixture.cs" />
<Compile Include="NHSpecificTest\NH3918\Model.cs" />
<Compile Include="NHSpecificTest\NH3918\FixtureByCode.cs" />
<Compile Include="NHSpecificTest\NH3414\Entity.cs" />
<Compile Include="NHSpecificTest\NH3414\FixtureByCode.cs" />
<Compile Include="NHSpecificTest\NH2218\Fixture.cs" />
Expand Down
15 changes: 12 additions & 3 deletions src/NHibernate/Linq/Visitors/SelectClauseNominator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public override Expression VisitExpression(Expression expression)
return innerExpression;
}

var projectConstantsInHql = _stateStack.Peek() || IsRegisteredFunction(expression);
var projectConstantsInHql = _stateStack.Peek() || expression.NodeType == ExpressionType.Equal || IsRegisteredFunction(expression);

// Set some flags, unless we already have proper values for them:
// projectConstantsInHql if they are inside a method call executed server side.
Expand Down Expand Up @@ -153,8 +153,17 @@ private bool CanBeEvaluatedInHqlSelectStatement(Expression expression, bool proj
if (expression.NodeType == ExpressionType.Call)
{
// Depends if it's in the function registry
if (!IsRegisteredFunction(expression))
return false;
return IsRegisteredFunction(expression);
}

if (expression.NodeType == ExpressionType.Conditional)
{
// Theoretically, any conditional that returns a CAST-able primitive should be constructable in HQL.
// The type needs to be CAST-able because HQL wraps the CASE clause in a CAST and only supports
// certain types (as defined by the HqlIdent constructor that takes a System.Type as the second argument).
// However, this may still not cover all cases, so to limit the nomination of conditional expressions,
// we will only consider those which are already getting constants projected into them.
return projectConstantsInHql;
}

// Assume all is good
Expand Down