diff --git a/releasenotes.txt b/releasenotes.txt
index 024a2a306d5..33e7829f17a 100644
--- a/releasenotes.txt
+++ b/releasenotes.txt
@@ -1,4 +1,17 @@
-Build 5.5.2
+Build 5.5.3
+=============================
+
+Release notes - NHibernate - Version 5.5.3
+
+2 issues were resolved in this release.
+
+** Task
+
+ * #3692 Release 5.5.3
+ * #3691 Merge 5.4.10 into 5.5.x
+
+
+Build 5.5.2
=============================
Release notes - NHibernate - Version 5.5.2
@@ -122,6 +135,23 @@ Release notes - NHibernate - Version 5.5.0
* #3412 Revive hql ParsingFixture
+Build 5.4.10
+=============================
+
+Release notes - NHibernate - Version 5.4.10
+
+3 issues were resolved in this release.
+
+** Bug
+
+ * #3609 Fitering with a subquery on a many-to-one with property-ref generates invalid SQL
+ * #3607 Invalid ByCode serialization to XML for OneToOne mappings
+
+** Task
+
+ * #3688 Release 5.4.10
+
+
Build 5.4.9
=============================
diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH3609/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH3609/Fixture.cs
new file mode 100644
index 00000000000..55554bf75f6
--- /dev/null
+++ b/src/NHibernate.Test/Async/NHSpecificTest/GH3609/Fixture.cs
@@ -0,0 +1,114 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by AsyncGenerator.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+
+using System;
+using System.Linq;
+using NUnit.Framework;
+using NHibernate.Linq;
+
+namespace NHibernate.Test.NHSpecificTest.GH3609
+{
+ using System.Threading.Tasks;
+ [TestFixture]
+ public class FixtureAsync : BugTestCase
+ {
+ protected override void OnSetUp()
+ {
+ using var session = OpenSession();
+ using var transaction = session.BeginTransaction();
+
+ var order = new Order
+ {
+ UniqueId = "0ab92479-8a17-4dbc-9bef-ce4344940cec",
+ CreatedDate = new DateTime(2024, 09, 24)
+ };
+ session.Save(order);
+ session.Save(new LineItem { Order = order, ItemName = "Bananas", Amount = 5 });
+ session.Save(new CleanLineItem { Order = order, ItemName = "Bananas", Amount = 5 });
+
+ order = new Order
+ {
+ UniqueId = "4ca17d84-97aa-489f-8701-302a3879a388",
+ CreatedDate = new DateTime(2021, 09, 19)
+ };
+ session.Save(order);
+ session.Save(new LineItem { Order = order, ItemName = "Apples", Amount = 10 });
+ session.Save(new CleanLineItem { Order = order, ItemName = "Apples", Amount = 10 });
+
+ transaction.Commit();
+ }
+
+ protected override void OnTearDown()
+ {
+ using var session = OpenSession();
+ using var transaction = session.BeginTransaction();
+
+ session.CreateQuery("delete from CleanLineItem").ExecuteUpdate();
+ session.CreateQuery("delete from System.Object").ExecuteUpdate();
+
+ transaction.Commit();
+ }
+
+ [Test]
+ public async Task QueryWithAnyAsync()
+ {
+ using var session = OpenSession();
+ using var transaction = session.BeginTransaction();
+
+ // This form of query is how we first discovered the issue. This is a simplified reproduction of the
+ // sort of Linq that we were using in our app. It seems to occur when we force an EXISTS( ... ) subquery.
+ var validOrders = session.Query().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
+ var orderCount = await (session.Query().CountAsync(x => validOrders.Any(y => y == x.Order)));
+
+ Assert.That(orderCount, Is.EqualTo(1));
+ await (transaction.CommitAsync());
+ }
+
+ [Test]
+ public async Task QueryWithAnyOnCleanLinesAsync()
+ {
+ using var session = OpenSession();
+ using var transaction = session.BeginTransaction();
+
+ // This form of query is how we first discovered the issue. This is a simplified reproduction of the
+ // sort of Linq that we were using in our app. It seems to occur when we force an EXISTS( ... ) subquery.
+ var validOrders = session.Query().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
+ var orderCount = await (session.Query().CountAsync(x => validOrders.Any(y => y == x.Order)));
+
+ Assert.That(orderCount, Is.EqualTo(1));
+ await (transaction.CommitAsync());
+ }
+
+ [Test]
+ public async Task QueryWithContainsAsync()
+ {
+ using var session = OpenSession();
+ using var transaction = session.BeginTransaction();
+
+ var validOrders = session.Query().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
+ var orderCount = await (session.Query().CountAsync(x => validOrders.Contains(x.Order)));
+
+ Assert.That(orderCount, Is.EqualTo(1));
+ await (transaction.CommitAsync());
+ }
+
+ [Test]
+ public async Task SimpleQueryForDataWhichWasInsertedViaAdoShouldProvideExpectedResultsAsync()
+ {
+ using var session = OpenSession();
+ using var transaction = session.BeginTransaction();
+
+ // This style of equivalent query does not exhibit the problem. This test passes no matter which NH version.
+ var lineItem = await (session.Query().FirstOrDefaultAsync(x => x.Order.CreatedDate > new DateTime(2024, 9, 10)));
+ Assert.That(lineItem, Is.Not.Null);
+ await (transaction.CommitAsync());
+ }
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/GH3607/FixtureByCode.cs b/src/NHibernate.Test/NHSpecificTest/GH3607/FixtureByCode.cs
new file mode 100644
index 00000000000..0ce9b1a592c
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/GH3607/FixtureByCode.cs
@@ -0,0 +1,31 @@
+using NHibernate.Cfg;
+using NHibernate.Cfg.MappingSchema;
+using NHibernate.Mapping.ByCode;
+using NUnit.Framework;
+
+namespace NHibernate.Test.NHSpecificTest.GH3607
+{
+ ///
+ /// By code mapping serialization failure since v5.4.1. Adapted from .
+ ///
+ [TestFixture]
+ public class FixtureByCode : TestCaseMappingByCode
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.AddMappings(new[] { typeof(OrderMapping), typeof(LineItemMapping), typeof(LineItemDataMapping) });
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+
+ [Test]
+ public void SerializeMappingToXml()
+ {
+ var mapping = GetMappings();
+ string serialized = "";
+ Assert.That(() => serialized = mapping.AsString(), Throws.Nothing, "Mapping serialization failure");
+ var config = new Configuration();
+ Assert.That(() => config.AddXml(serialized), Throws.Nothing, "Configuration with serialized mapping has failed");
+ }
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/GH3607/LineItem.cs b/src/NHibernate.Test/NHSpecificTest/GH3607/LineItem.cs
new file mode 100644
index 00000000000..146a7407b26
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/GH3607/LineItem.cs
@@ -0,0 +1,15 @@
+namespace NHibernate.Test.NHSpecificTest.GH3607
+{
+ public class LineItem
+ {
+ public virtual int Id { get; set; }
+
+ public virtual Order ParentOrder { get; set; }
+
+ public virtual string ItemName { get; set; }
+
+ public virtual decimal Amount { get; set; }
+
+ public virtual LineItemData Data { get; set; }
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/GH3607/LineItemData.cs b/src/NHibernate.Test/NHSpecificTest/GH3607/LineItemData.cs
new file mode 100644
index 00000000000..5bcfa3ad64c
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/GH3607/LineItemData.cs
@@ -0,0 +1,9 @@
+namespace NHibernate.Test.NHSpecificTest.GH3607
+{
+ public class LineItemData
+ {
+ public virtual LineItem LineItem { get; set; }
+
+ public virtual string Data { get; set; }
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/GH3607/LineItemDataMapping.cs b/src/NHibernate.Test/NHSpecificTest/GH3607/LineItemDataMapping.cs
new file mode 100644
index 00000000000..66634421a3b
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/GH3607/LineItemDataMapping.cs
@@ -0,0 +1,14 @@
+using NHibernate.Mapping.ByCode.Conformist;
+
+namespace NHibernate.Test.NHSpecificTest.GH3607
+{
+ public class LineItemDataMapping : ClassMapping
+ {
+ public LineItemDataMapping()
+ {
+ OneToOne(x => x.LineItem, m => m.Constrained(true));
+
+ Property(x => x.Data);
+ }
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/GH3607/LineItemMapping.cs b/src/NHibernate.Test/NHSpecificTest/GH3607/LineItemMapping.cs
new file mode 100644
index 00000000000..3918b0a84f3
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/GH3607/LineItemMapping.cs
@@ -0,0 +1,21 @@
+using NHibernate.Mapping.ByCode;
+using NHibernate.Mapping.ByCode.Conformist;
+
+namespace NHibernate.Test.NHSpecificTest.GH3607
+{
+ public class LineItemMapping : ClassMapping
+ {
+ public LineItemMapping()
+ {
+ Id(x => x.Id, m => m.Generator(new IdentityGeneratorDef()));
+
+ Property(x => x.ItemName);
+
+ Property(x => x.Amount);
+
+ ManyToOne(x => x.ParentOrder);
+
+ ManyToOne(x => x.Data);
+ }
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/GH3607/Order.cs b/src/NHibernate.Test/NHSpecificTest/GH3607/Order.cs
new file mode 100644
index 00000000000..be3b9d05ffc
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/GH3607/Order.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+
+namespace NHibernate.Test.NHSpecificTest.GH3607
+{
+ public class Order
+ {
+ public virtual int Id { get; set; }
+
+ public virtual DateTime CreatedDate { get; set; }
+
+ public virtual ISet Items { get; protected set; } = new HashSet();
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/GH3607/OrderMapping.cs b/src/NHibernate.Test/NHSpecificTest/GH3607/OrderMapping.cs
new file mode 100644
index 00000000000..036c65e47a5
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/GH3607/OrderMapping.cs
@@ -0,0 +1,22 @@
+using NHibernate.Mapping.ByCode;
+using NHibernate.Mapping.ByCode.Conformist;
+
+namespace NHibernate.Test.NHSpecificTest.GH3607
+{
+ public class OrderMapping : ClassMapping
+ {
+ public OrderMapping()
+ {
+ Table("`Order`");
+ Id(x => x.Id, m => m.Generator(new IdentityGeneratorDef()));
+
+ Property(x => x.CreatedDate);
+
+ Set(x => x.Items, m =>
+ {
+ m.Inverse(true);
+ m.OptimisticLock(true);
+ }, a => a.OneToMany());
+ }
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/GH3609/Entities.cs b/src/NHibernate.Test/NHSpecificTest/GH3609/Entities.cs
new file mode 100644
index 00000000000..79489468534
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/GH3609/Entities.cs
@@ -0,0 +1,35 @@
+using System;
+
+namespace NHibernate.Test.NHSpecificTest.GH3609
+{
+ public class Order
+ {
+ public virtual long Id { get; set; }
+
+ public virtual string UniqueId { get; set; } = Guid.NewGuid().ToString();
+
+ public virtual DateTime CreatedDate { get; set; }
+ }
+
+ public class LineItem
+ {
+ public virtual long Id { get; set; }
+
+ public virtual Order Order { get; set; }
+
+ public virtual string ItemName { get; set; }
+
+ public virtual decimal Amount { get; set; }
+ }
+
+ public class CleanLineItem
+ {
+ public virtual long Id { get; set; }
+
+ public virtual Order Order { get; set; }
+
+ public virtual string ItemName { get; set; }
+
+ public virtual decimal Amount { get; set; }
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/GH3609/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/GH3609/Fixture.cs
new file mode 100644
index 00000000000..57a0a8053dd
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/GH3609/Fixture.cs
@@ -0,0 +1,102 @@
+using System;
+using System.Linq;
+using NUnit.Framework;
+
+namespace NHibernate.Test.NHSpecificTest.GH3609
+{
+ [TestFixture]
+ public class Fixture : BugTestCase
+ {
+ protected override void OnSetUp()
+ {
+ using var session = OpenSession();
+ using var transaction = session.BeginTransaction();
+
+ var order = new Order
+ {
+ UniqueId = "0ab92479-8a17-4dbc-9bef-ce4344940cec",
+ CreatedDate = new DateTime(2024, 09, 24)
+ };
+ session.Save(order);
+ session.Save(new LineItem { Order = order, ItemName = "Bananas", Amount = 5 });
+ session.Save(new CleanLineItem { Order = order, ItemName = "Bananas", Amount = 5 });
+
+ order = new Order
+ {
+ UniqueId = "4ca17d84-97aa-489f-8701-302a3879a388",
+ CreatedDate = new DateTime(2021, 09, 19)
+ };
+ session.Save(order);
+ session.Save(new LineItem { Order = order, ItemName = "Apples", Amount = 10 });
+ session.Save(new CleanLineItem { Order = order, ItemName = "Apples", Amount = 10 });
+
+ transaction.Commit();
+ }
+
+ protected override void OnTearDown()
+ {
+ using var session = OpenSession();
+ using var transaction = session.BeginTransaction();
+
+ session.CreateQuery("delete from CleanLineItem").ExecuteUpdate();
+ session.CreateQuery("delete from System.Object").ExecuteUpdate();
+
+ transaction.Commit();
+ }
+
+ [Test]
+ public void QueryWithAny()
+ {
+ using var session = OpenSession();
+ using var transaction = session.BeginTransaction();
+
+ // This form of query is how we first discovered the issue. This is a simplified reproduction of the
+ // sort of Linq that we were using in our app. It seems to occur when we force an EXISTS( ... ) subquery.
+ var validOrders = session.Query().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
+ var orderCount = session.Query().Count(x => validOrders.Any(y => y == x.Order));
+
+ Assert.That(orderCount, Is.EqualTo(1));
+ transaction.Commit();
+ }
+
+ [Test]
+ public void QueryWithAnyOnCleanLines()
+ {
+ using var session = OpenSession();
+ using var transaction = session.BeginTransaction();
+
+ // This form of query is how we first discovered the issue. This is a simplified reproduction of the
+ // sort of Linq that we were using in our app. It seems to occur when we force an EXISTS( ... ) subquery.
+ var validOrders = session.Query().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
+ var orderCount = session.Query().Count(x => validOrders.Any(y => y == x.Order));
+
+ Assert.That(orderCount, Is.EqualTo(1));
+ transaction.Commit();
+ }
+
+ [Test]
+ public void QueryWithContains()
+ {
+ using var session = OpenSession();
+ using var transaction = session.BeginTransaction();
+
+ var validOrders = session.Query().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
+ var orderCount = session.Query().Count(x => validOrders.Contains(x.Order));
+
+ Assert.That(orderCount, Is.EqualTo(1));
+ transaction.Commit();
+ }
+
+ [Test]
+ public void SimpleQueryForDataWhichWasInsertedViaAdoShouldProvideExpectedResults()
+ {
+ using var session = OpenSession();
+ using var transaction = session.BeginTransaction();
+
+ // This style of equivalent query does not exhibit the problem. This test passes no matter which NH version.
+ var lineItem = session.Query().FirstOrDefault(x => x.Order.CreatedDate > new DateTime(2024, 9, 10));
+ Assert.That(lineItem, Is.Not.Null);
+ transaction.Commit();
+ }
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/GH3609/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/GH3609/Mappings.hbm.xml
new file mode 100644
index 00000000000..43191f32951
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/GH3609/Mappings.hbm.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/NHibernate/Cfg/MappingSchema/HbmOneToOne.cs b/src/NHibernate/Cfg/MappingSchema/HbmOneToOne.cs
index e0740e17937..aec82a44ee4 100644
--- a/src/NHibernate/Cfg/MappingSchema/HbmOneToOne.cs
+++ b/src/NHibernate/Cfg/MappingSchema/HbmOneToOne.cs
@@ -18,9 +18,13 @@ public string Access
get { return access; }
}
+ // 6.0 Todo : remove XmlIgnore after removing the setter. See #3607 fix.
+ [XmlIgnore]
public bool OptimisticLock
{
get => optimisticlock;
+ // Since v5.4.10
+ [Obsolete("Providing a setter for OptimisticLock was unintended and will be removed.")]
set => optimisticlock = value;
}
diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs
index 686ab1d05ff..54f812b780d 100644
--- a/src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs
+++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs
@@ -163,7 +163,7 @@ public override void ResolveFirstChild()
string propName = property.Text;
_propertyName = propName;
- // If the uresolved property path isn't set yet, just use the property name.
+ // If the unresolved property path isn't set yet, just use the property name.
if (_propertyPath == null)
{
_propertyPath = propName;
@@ -402,10 +402,14 @@ private void DereferenceEntity(EntityType entityType, bool implicitJoin, string
string property = _propertyName;
bool joinIsNeeded;
- //For nullable entity comparisons we always need to add join (like not constrained one-to-one or not-found ignore associations)
- bool comparisonWithNullableEntity = entityType.IsNullable && Walker.IsComparativeExpressionClause && !IsCorrelatedSubselect;
+ // For nullable entity comparisons we always need to add join (like not constrained one-to-one or not-found ignore associations).
+ var comparisonWithNullableEntity = Walker.IsComparativeExpressionClause && entityType.IsNullable;
+ // For property-ref association comparison, we also need to join unless finding a way in the node for the other hand of the comparison
+ // to detect it should yield the property-ref columns instead of the primary key columns. And if the other hand is an association too,
+ // it may be a reference to the primary key, so we would need to join anyway.
+ var comparisonThroughPropertyRef = Walker.IsComparativeExpressionClause && !entityType.IsReferenceToPrimaryKey;
- if ( IsDotNode( parent ) )
+ if (IsDotNode(parent))
{
// our parent is another dot node, meaning we are being further dereferenced.
// thus we need to generate a join unless the parent refers to the associated
@@ -426,15 +430,18 @@ private void DereferenceEntity(EntityType entityType, bool implicitJoin, string
else
{
joinIsNeeded = generateJoin || (Walker.IsInSelect && !Walker.IsInCase) || (Walker.IsInFrom && !Walker.IsComparativeExpressionClause)
- || comparisonWithNullableEntity;
+ || comparisonWithNullableEntity || comparisonThroughPropertyRef;
}
if ( joinIsNeeded )
{
- DereferenceEntityJoin(classAlias, entityType, implicitJoin, parent, comparisonWithNullableEntity);
- if (comparisonWithNullableEntity)
+ // Subselect queries use theta style joins, which cannot be forced to left outer joins.
+ var forceLeftJoin = comparisonWithNullableEntity && !IsCorrelatedSubselect;
+ DereferenceEntityJoin(classAlias, entityType, implicitJoin, parent, forceLeftJoin);
+ if (comparisonWithNullableEntity || comparisonThroughPropertyRef)
{
_columns = FromElement.GetIdentityColumns();
+ DataType = FromElement.EntityPersister.EntityMetamodel.EntityType;
}
}
else