Skip to content

Commit

Permalink
Initial commit with repro case for nhibernate/nhibernate-core#1277
Browse files Browse the repository at this point in the history
  • Loading branch information
craigfowler committed Aug 16, 2020
0 parents commit 1fd5e72
Show file tree
Hide file tree
Showing 11 changed files with 332 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.vs/
bin/
obj/
17 changes: 17 additions & 0 deletions NHibernate-MbC-join-mapping.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Tests", "NHibernate.Tests\NHibernate.Tests.csproj", "{2244649E-15A9-4A35-8B26-8A174F7CC50A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2244649E-15A9-4A35-8B26-8A174F7CC50A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2244649E-15A9-4A35-8B26-8A174F7CC50A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2244649E-15A9-4A35-8B26-8A174F7CC50A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2244649E-15A9-4A35-8B26-8A174F7CC50A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
33 changes: 33 additions & 0 deletions NHibernate.Tests/CreateDb.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
BEGIN TRANSACTION;

-- Schema
CREATE TABLE Person (
PersonId INT PRIMARY KEY,
Name VARCHAR
);
CREATE TABLE Address (
AddressId INT PRIMARY KEY,
AddressPersonId INT NOT NULL,
City VARCHAR,
CONSTRAINT FK_ADDRESS_HAS_PERSON FOREIGN KEY (AddressPersonId) REFERENCES Person
);
CREATE TABLE PayrollInfo (
PayrollInfoId INT PRIMARY KEY,
PayrollInfoPersonId INT NOT NULL,
PayrollNumber INT,
CONSTRAINT FK_PAYROLLINFO_HAS_PERSON FOREIGN KEY (PayrollInfoId) REFERENCES Person
);

COMMIT;

BEGIN TRANSACTION;

-- Data
INSERT INTO Person(PersonId, Name)
VALUES(1, 'Jane Doe');
INSERT INTO Address(AddressPersonId, City)
VALUES(1, 'Nowhere');
INSERT INTO PayrollInfo(PayrollInfoPersonId, PayrollNumber)
VALUES(1, 1234);

COMMIT;
44 changes: 44 additions & 0 deletions NHibernate.Tests/DbInitializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.Data;
using System.Data.Common;
using System.IO;
using System.Reflection;

namespace NHibernate.Tests
{
/// <summary>
/// Initializer class executes the script <c>CreateDb.sql</c> on the specified connection.
/// </summary>
public class DbInitializer
{
public void InitializeDatabase(DbConnection connection)
{
if (connection == null)
throw new ArgumentNullException(nameof(connection));

if (connection.State != ConnectionState.Open)
connection.Open();

var initScript = GetInitScript();
ExecuteScript(connection, initScript);
}

string GetInitScript()
{
using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("NHibernate.Tests.CreateDb.sql"))
using(var reader = new StreamReader(stream))
{
return reader.ReadToEnd();
}
}

void ExecuteScript(DbConnection connection, string script)
{
using (var command = connection.CreateCommand())
{
command.CommandText = script;
command.ExecuteNonQuery();
}
}
}
}
40 changes: 40 additions & 0 deletions NHibernate.Tests/MappingWithTwoJoinsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using NUnit.Framework;
using System.Linq;

namespace NHibernate.Tests
{
[TestFixture]
public class MappingWithTwoJoinsTests
{
[Test]
public void NHibernate_should_not_throw_when_querying_a_Person_mapped_using_MbC()
{
var sessionFactoryCreator = new SessionFactoryCreator();

using (var sessionFactory = sessionFactoryCreator.GetSessionFactoryWithMbcMappings())
using(var session = sessionFactory.OpenSession())
{
var initializer = new DbInitializer();
initializer.InitializeDatabase(session.Connection);

Assert.That(() => session.Query<Person>().FirstOrDefault(x => x.Name == "Jane Doe"), Throws.Nothing);
}
}

[Test]
public void NHibernate_should_not_throw_when_querying_a_Person_mapped_using_XML()
{
var sessionFactoryCreator = new SessionFactoryCreator();

using (var sessionFactory = sessionFactoryCreator.GetSessionFactoryWithXmlMappings())
using (var session = sessionFactory.OpenSession())
{
var initializer = new DbInitializer();
initializer.InitializeDatabase(session.Connection);

Assert.That(() => session.Query<Person>().FirstOrDefault(x => x.Name == "Jane Doe"), Throws.Nothing);
}
}
}
}
23 changes: 23 additions & 0 deletions NHibernate.Tests/NHibernate.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="NHibernate" Version="5.3.2" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.113.1" />
</ItemGroup>
<ItemGroup>
<None Remove="Mappings\PersonMap.hbm.xml" />
<None Remove="CreateDb.sql" />
<None Remove="PersonMap.hbm.xml" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="CreateDb.sql" />
<EmbeddedResource Include="PersonMap.hbm.xml" />
</ItemGroup>
</Project>
17 changes: 17 additions & 0 deletions NHibernate.Tests/Person.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
namespace NHibernate.Tests
{
public class Person
{
public virtual int PersonId { get; set; }

// Comes from the Person table
public virtual string Name { get; set; }

// Comes from the Address table
public virtual string City { get; set; }

// Comes from the PayrollInfo table
public virtual int PayrollNumber { get; set; }
}
}
29 changes: 29 additions & 0 deletions NHibernate.Tests/PersonMap.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using NHibernate.Mapping.ByCode.Conformist;

namespace NHibernate.Tests
{
public class PersonMap : ClassMapping<Person>
{
// Note that the XML mapping in the same directory as this is equivalent.
// Only one of the two mappings should be used at a time.

public PersonMap()
{
Id(x => x.PersonId);

Property(x => x.Name);

Join("PayrollInfo", j =>
{
j.Key(k => k.Column("PayrollInfoPersonId"));
j.Property(x => x.PayrollNumber);
});

Join("Address", j =>
{
j.Key(k => k.Column("AddressPersonId"));
j.Property(x => x.City);
});
}
}
}
25 changes: 25 additions & 0 deletions NHibernate.Tests/PersonMap.hbm.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="NHibernate.Tests"
assembly="NHibernate.Tests">

<!--
Note that the Mapping-by-Code mapping in the same directory as this is equivalent.
Only one of the two mappings should be used at a time.
-->

<class name="Person" table="Person">
<id name="PersonId" />
<property name="Name" />

<join table="PayrollInfo">
<key column="PayrollInfoPersonId" />
<property name="PayrollNumber" />
</join>

<join table="Address">
<key column="AddressPersonId" />
<property name="City" />
</join>
</class>

</hibernate-mapping>
55 changes: 55 additions & 0 deletions NHibernate.Tests/SessionFactoryCreator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Reflection;
using NHibernate.Cfg;
using NHibernate.Mapping.ByCode;

namespace NHibernate.Tests
{
public class SessionFactoryCreator
{
public ISessionFactory GetSessionFactoryWithMbcMappings()
{
var config = GetConfigWithoutMappings();
AddMbcMappings(config);
return config.BuildSessionFactory();
}

public ISessionFactory GetSessionFactoryWithXmlMappings()
{
var config = GetConfigWithoutMappings();
AddXmlMappings(config);
return config.BuildSessionFactory();
}

Configuration GetConfigWithoutMappings()
{
var config = new Configuration();

// For simplicity of reproduction I'm using SQLite in-memory DB, but
// I have seen the same problem reproduced using MS-SQL 2012 as well, so
// I don't think that db/dialect is relevant.
config.DataBaseIntegration(db =>
{
db.Driver<Driver.SQLite20Driver>();
db.Dialect<Dialect.SQLiteDialect>();
db.ConnectionString = "Data Source=:memory:;Version=3;New=True;";
});

return config;
}

void AddMbcMappings(Configuration config)
{
var mapper = new ConventionModelMapper();
mapper.AddMapping<PersonMap>();
var mapping = mapper.CompileMappingForAllExplicitlyAddedEntities();

config.AddDeserializedMapping(mapping, "MbC-Mappings");
}

void AddXmlMappings(Configuration config)
{
config.AddAssembly(Assembly.GetExecutingAssembly());
}
}
}
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Mapping-By-Code `Join` repro case
This repository is a reproduction case for [a bug in NHibernate] whereby Mapping By Code (MbC) produces incorrect SQL if there is more than one `Join` in a single class mapping. It would seem that parts of the join mapping from the first of the joins are accidentally overrwriting/overriding the same parts of the join mapping from subsequent joins in the same class mapping file. More information may be found at the linked issue.

The reproduction case framed as a pair of NUnit test cases, so to see the results just clone the repo and run **dotnet test**.

* The test case `NHibernate_should_not_throw_when_querying_a_Person_mapped_using_XML` passes, and shows that NHibernate behaves correctly when not using MbC
* The test case `NHibernate_should_not_throw_when_querying_a_Person_mapped_using_MbC` fails, demonstrating the bug

Note that in this reproduction case, to make it as easy as possible to run, I have used an in-memory SQLite database. I have also seen this same bug reproduced against MS-SQL server 2012, so I do not think that the database, driver or dialect are related to the problem.

### What the tests do
The test project sets up two instances of `ISessionFactory`, which *should theoretically be equivalent*. One session factory uses MbC (see the class `PersonMap`) and the other session factory uses XML HBM mappings (see the file `PersonMap.hbm.xml`).

A class named `DbInitializer` is used to create a sample database schema matching those mappings, with some sample data. The tests then query this data using a Linq query.

* In the case of the XML-based mappings, everything works OK and no exception is raised.

* In the case of the MbC-based mappings, the query throws an exception, due to incorrect SQL being sent to the database driver.

### The exception & SQL
The exception raised from the MbC test case is an `NHibernate.Exceptions.GenericADOException`, wrapping a native ADO exception from the database driver, complaining about invalid SQL logic.

The SQL which is sent to the database (in the failing test) is as follows. *I have made trivial whitespace changes for readability, as well as adding a comment to point out the error*.

```sql
select
person0_.PersonId as personid1_0_,
person0_.Name as name2_0_,
person0_1_.PayrollNumber as payrollnumber2_1_,
person0_2_.City as city2_2_

from
Person person0_
inner join PayrollInfo person0_1_
on person0_.PersonId=person0_1_.AddressPersonId
-- ^^^ This is the error, it is the wrong column name,
-- it should be PayrollInfoPersonId
inner join Address person0_2_
on person0_.PersonId=person0_2_.AddressPersonId

where person0_.Name=@p0

limit 1
```

[a bug in NHibernate]: https://github.com/nhibernate/nhibernate-core/issues/1277

0 comments on commit 1fd5e72

Please sign in to comment.