diff --git a/src/AsyncGenerator.yml b/src/AsyncGenerator.yml index 68ae0a1c735..99c765a110c 100644 --- a/src/AsyncGenerator.yml +++ b/src/AsyncGenerator.yml @@ -110,6 +110,10 @@ - conversion: Ignore name: GetEnumerator containingTypeName: IFutureEnumerable +# TODO 6.0: Consider if ComputeFlattenedParameters should remain ignored or not + - conversion: Ignore + name: ComputeFlattenedParameters + containingTypeName: SqlQueryImpl - conversion: ToAsync name: ExecuteReader containingTypeName: IBatcher diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH3079/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH3079/Fixture.cs new file mode 100644 index 00000000000..e9980e3a398 --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/NH3079/Fixture.cs @@ -0,0 +1,246 @@ +//------------------------------------------------------------------------------ +// +// 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.Collections.Generic; +using System.Linq; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.NH3079 +{ + using System.Threading.Tasks; + [TestFixture] + public class FixtureAsync : BugTestCase + { + // Disable second level cache + protected override string CacheConcurrencyStrategy => null; + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.CreateQuery("delete from Employment").ExecuteUpdate(); + + s.CreateQuery("delete from System.Object").ExecuteUpdate(); + + t.Commit(); + } + } + + protected override void OnSetUp() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var personList = new List(); + var employerList = new List(); + + // Global id to avoid false positive assertion with positional parameter + var gId = 1; + + for (var i = 0; i < 3; i++) + { + var personCpId = new PersonCpId { IdA = gId++, IdB = gId++ }; + var personObj = new Person + { CpId = personCpId, Name = "PERSON_" + personCpId.IdA + "_" + personCpId.IdB }; + s.Save(personObj); + personList.Add(personObj); + } + + for (var i = 0; i < 3; i++) + { + var employerCpId = new EmployerCpId { IdA = gId++, IdB = gId++ }; + var employerObj = new Employer + { CpId = employerCpId, Name = "EMPLOYER_" + employerCpId.IdA + "_" + employerCpId.IdB }; + s.Save(employerObj); + employerList.Add(employerObj); + } + + var employmentIds = new[] + { + gId++, + gId++, + gId++, + gId++, + }; + var employmentNames = new[] + { + //P1 + E1 + "EMPLOYMENT_" + employmentIds[0] + "_" + + personList[0].CpId.IdA + "_" + personList[0].CpId.IdA + "_" + + employerList[0].CpId.IdA + "_" + employerList[0].CpId.IdB, + //P1 + E2 + "EMPLOYMENT_" + employmentIds[1] + "_" + + personList[0].CpId.IdA + "_" + personList[0].CpId.IdA + "_" + + employerList[1].CpId.IdA + "_" + employerList[1].CpId.IdB, + //P2 + E2 + "EMPLOYMENT_" + employmentIds[2] + "_" + + personList[1].CpId.IdA + "_" + personList[1].CpId.IdA + "_" + + employerList[1].CpId.IdA + "_" + employerList[1].CpId.IdB, + //P2 + E3 + "EMPLOYMENT_" + employmentIds[2] + "_" + + personList[1].CpId.IdA + "_" + personList[1].CpId.IdA + "_" + + employerList[2].CpId.IdA + "_" + employerList[2].CpId.IdB + }; + var employmentPersons = new[] + { + personList[0], + personList[0], + personList[1], + personList[1] + }; + var employmentEmployers = new[] + { + employerList[0], + employerList[1], + employerList[1], + employerList[2] + }; + + for (var k = 0; k < employmentIds.Length; k++) + { + var employmentCpId = new EmploymentCpId + { + Id = employmentIds[k], + PersonObj = employmentPersons[k], + EmployerObj = employmentEmployers[k] + }; + var employmentObj = new Employment { CpId = employmentCpId, Name = employmentNames[k] }; + s.Save(employmentObj); + } + + for (var i = 0; i < 3; i++) + { + var personNoComponentObj = new PersonNoComponent { IdA = gId++, IdB = gId++ }; + personNoComponentObj.Name = "PERSON_NO_COMPONENT_" + personNoComponentObj.IdA + "_" + + personNoComponentObj.IdB; + s.Save(personNoComponentObj); + } + + t.Commit(); + } + } + + // Test reproducing the problem. + [Test] + public async Task GetPersonTestAsync() + { + using (var session = OpenSession()) + { + var person1_2 = await (session.GetAsync(new PersonCpId { IdA = 1, IdB = 2 })); + Assert.That(person1_2.Name, Is.EqualTo("PERSON_1_2")); + Assert.That( + person1_2.EmploymentList.Select(e => e.Name), + Is.EquivalentTo(new[] { "EMPLOYMENT_13_1_1_7_8", "EMPLOYMENT_14_1_1_9_10" })); + } + } + + // Test reproducing the problem. + [Test] + public async Task GetEmployerTestAsync() + { + using (var session = OpenSession()) + { + var employer7_8 = await (session.GetAsync(new EmployerCpId { IdA = 7, IdB = 8 })); + Assert.That(employer7_8.Name, Is.EqualTo("EMPLOYER_7_8")); + Assert.That( + employer7_8.EmploymentList.Select(e => e.Name), + Is.EquivalentTo(new[] { "EMPLOYMENT_13_1_1_7_8" })); + } + } + + [Test] + public async Task GetEmploymentTestAsync() + { + using (var session = OpenSession()) + { + var employment_13_1_2_7_8 = + await (session.GetAsync( + new EmploymentCpId + { + Id = 13, + PersonObj = + new Person + { + CpId = new PersonCpId { IdA = 1, IdB = 2 } + }, + EmployerObj = + new Employer + { + CpId = new EmployerCpId { IdA = 7, IdB = 8 } + } + })); + Assert.That(employment_13_1_2_7_8.Name, Is.EqualTo("EMPLOYMENT_13_1_1_7_8")); + } + } + + [Test] + public async Task HqlPersonPositionalAsync() + { + using (var session = OpenSession()) + { + var personList = + await (session + .GetNamedQuery("personPositional") + .SetParameter(0, new PersonCpId { IdA = 1, IdB = 2 }) + .SetParameter(1, new PersonCpId { IdA = 3, IdB = 4 }) + .ListAsync()); + Assert.That( + personList.Select(e => e.Name), + Is.EquivalentTo(new[] { "PERSON_1_2", "PERSON_3_4" })); + } + } + + [Test] + public async Task HqlPersonNamedAsync() + { + using (var session = OpenSession()) + { + var personList = + await (session + .GetNamedQuery("personNamed") + .SetParameter("id1", new PersonCpId { IdA = 1, IdB = 2 }) + .SetParameter("id2", new PersonCpId { IdA = 3, IdB = 4 }) + .ListAsync()); + Assert.That( + personList.Select(e => e.Name), + Is.EquivalentTo(new[] { "PERSON_1_2", "PERSON_3_4" })); + } + } + + [Test] + public async Task GetPersonNoComponentAsync() + { + using (var session = OpenSession()) + { + var person17_18 = + await (session.GetAsync(new PersonNoComponent { IdA = 17, IdB = 18 })); + Assert.That(person17_18.Name, Is.EqualTo("PERSON_NO_COMPONENT_17_18")); + } + } + + [Test] + public async Task SqlPersonNoComponentAsync() + { + using (var session = OpenSession()) + { + var personList = + await (session + .GetNamedQuery("personNoComponentSql") + .SetParameter(0, new PersonNoComponent { IdA = 17, IdB = 18 }) + .SetParameter(1, new PersonNoComponent { IdA = 19, IdB = 20 }) + .ListAsync()); + Assert.That( + personList.Select(e => e.Name), + Is.EquivalentTo(new[] { "PERSON_NO_COMPONENT_17_18", "PERSON_NO_COMPONENT_19_20" })); + } + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH3079/Employer.cs b/src/NHibernate.Test/NHSpecificTest/NH3079/Employer.cs new file mode 100644 index 00000000000..c5fe0ffd9ec --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3079/Employer.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.NH3079 +{ + public class Employer + { + public virtual EmployerCpId CpId { get; set; } + + public virtual string Name { get; set; } + + public virtual ICollection EmploymentList { get; set; } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH3079/EmployerCpId.cs b/src/NHibernate.Test/NHSpecificTest/NH3079/EmployerCpId.cs new file mode 100644 index 00000000000..dd8bd8cb69a --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3079/EmployerCpId.cs @@ -0,0 +1,22 @@ +namespace NHibernate.Test.NHSpecificTest.NH3079 +{ + public class EmployerCpId + { + public virtual int IdA { get; set; } + + public virtual int IdB { get; set; } + + public override bool Equals(object obj) + { + if (!(obj is EmployerCpId objCpId)) + return false; + + return IdA == objCpId.IdA && IdB == objCpId.IdB; + } + + public override int GetHashCode() + { + return IdA.GetHashCode() ^ IdB.GetHashCode(); + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH3079/Employment.cs b/src/NHibernate.Test/NHSpecificTest/NH3079/Employment.cs new file mode 100644 index 00000000000..a84fd07cc6c --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3079/Employment.cs @@ -0,0 +1,9 @@ +namespace NHibernate.Test.NHSpecificTest.NH3079 +{ + public class Employment + { + public virtual EmploymentCpId CpId { get; set; } + + public virtual string Name { get; set; } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH3079/EmploymentCpId.cs b/src/NHibernate.Test/NHSpecificTest/NH3079/EmploymentCpId.cs new file mode 100644 index 00000000000..2207044966a --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3079/EmploymentCpId.cs @@ -0,0 +1,25 @@ +namespace NHibernate.Test.NHSpecificTest.NH3079 +{ + public class EmploymentCpId + { + public virtual int Id { get; set; } + + public virtual Person PersonObj { get; set; } + + public virtual Employer EmployerObj { get; set; } + + public override bool Equals(object obj) + { + if (!(obj is EmploymentCpId objCpId)) + return false; + + return Id == objCpId.Id && PersonObj.CpId == objCpId.PersonObj.CpId && + EmployerObj.CpId == objCpId.EmployerObj.CpId; + } + + public override int GetHashCode() + { + return Id.GetHashCode() ^ PersonObj.CpId.GetHashCode() ^ EmployerObj.CpId.GetHashCode(); + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH3079/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/NH3079/Fixture.cs new file mode 100644 index 00000000000..f5a298c6afd --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3079/Fixture.cs @@ -0,0 +1,235 @@ +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.NH3079 +{ + [TestFixture] + public class Fixture : BugTestCase + { + // Disable second level cache + protected override string CacheConcurrencyStrategy => null; + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.CreateQuery("delete from Employment").ExecuteUpdate(); + + s.CreateQuery("delete from System.Object").ExecuteUpdate(); + + t.Commit(); + } + } + + protected override void OnSetUp() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var personList = new List(); + var employerList = new List(); + + // Global id to avoid false positive assertion with positional parameter + var gId = 1; + + for (var i = 0; i < 3; i++) + { + var personCpId = new PersonCpId { IdA = gId++, IdB = gId++ }; + var personObj = new Person + { CpId = personCpId, Name = "PERSON_" + personCpId.IdA + "_" + personCpId.IdB }; + s.Save(personObj); + personList.Add(personObj); + } + + for (var i = 0; i < 3; i++) + { + var employerCpId = new EmployerCpId { IdA = gId++, IdB = gId++ }; + var employerObj = new Employer + { CpId = employerCpId, Name = "EMPLOYER_" + employerCpId.IdA + "_" + employerCpId.IdB }; + s.Save(employerObj); + employerList.Add(employerObj); + } + + var employmentIds = new[] + { + gId++, + gId++, + gId++, + gId++, + }; + var employmentNames = new[] + { + //P1 + E1 + "EMPLOYMENT_" + employmentIds[0] + "_" + + personList[0].CpId.IdA + "_" + personList[0].CpId.IdA + "_" + + employerList[0].CpId.IdA + "_" + employerList[0].CpId.IdB, + //P1 + E2 + "EMPLOYMENT_" + employmentIds[1] + "_" + + personList[0].CpId.IdA + "_" + personList[0].CpId.IdA + "_" + + employerList[1].CpId.IdA + "_" + employerList[1].CpId.IdB, + //P2 + E2 + "EMPLOYMENT_" + employmentIds[2] + "_" + + personList[1].CpId.IdA + "_" + personList[1].CpId.IdA + "_" + + employerList[1].CpId.IdA + "_" + employerList[1].CpId.IdB, + //P2 + E3 + "EMPLOYMENT_" + employmentIds[2] + "_" + + personList[1].CpId.IdA + "_" + personList[1].CpId.IdA + "_" + + employerList[2].CpId.IdA + "_" + employerList[2].CpId.IdB + }; + var employmentPersons = new[] + { + personList[0], + personList[0], + personList[1], + personList[1] + }; + var employmentEmployers = new[] + { + employerList[0], + employerList[1], + employerList[1], + employerList[2] + }; + + for (var k = 0; k < employmentIds.Length; k++) + { + var employmentCpId = new EmploymentCpId + { + Id = employmentIds[k], + PersonObj = employmentPersons[k], + EmployerObj = employmentEmployers[k] + }; + var employmentObj = new Employment { CpId = employmentCpId, Name = employmentNames[k] }; + s.Save(employmentObj); + } + + for (var i = 0; i < 3; i++) + { + var personNoComponentObj = new PersonNoComponent { IdA = gId++, IdB = gId++ }; + personNoComponentObj.Name = "PERSON_NO_COMPONENT_" + personNoComponentObj.IdA + "_" + + personNoComponentObj.IdB; + s.Save(personNoComponentObj); + } + + t.Commit(); + } + } + + // Test reproducing the problem. + [Test] + public void GetPersonTest() + { + using (var session = OpenSession()) + { + var person1_2 = session.Get(new PersonCpId { IdA = 1, IdB = 2 }); + Assert.That(person1_2.Name, Is.EqualTo("PERSON_1_2")); + Assert.That( + person1_2.EmploymentList.Select(e => e.Name), + Is.EquivalentTo(new[] { "EMPLOYMENT_13_1_1_7_8", "EMPLOYMENT_14_1_1_9_10" })); + } + } + + // Test reproducing the problem. + [Test] + public void GetEmployerTest() + { + using (var session = OpenSession()) + { + var employer7_8 = session.Get(new EmployerCpId { IdA = 7, IdB = 8 }); + Assert.That(employer7_8.Name, Is.EqualTo("EMPLOYER_7_8")); + Assert.That( + employer7_8.EmploymentList.Select(e => e.Name), + Is.EquivalentTo(new[] { "EMPLOYMENT_13_1_1_7_8" })); + } + } + + [Test] + public void GetEmploymentTest() + { + using (var session = OpenSession()) + { + var employment_13_1_2_7_8 = + session.Get( + new EmploymentCpId + { + Id = 13, + PersonObj = + new Person + { + CpId = new PersonCpId { IdA = 1, IdB = 2 } + }, + EmployerObj = + new Employer + { + CpId = new EmployerCpId { IdA = 7, IdB = 8 } + } + }); + Assert.That(employment_13_1_2_7_8.Name, Is.EqualTo("EMPLOYMENT_13_1_1_7_8")); + } + } + + [Test] + public void HqlPersonPositional() + { + using (var session = OpenSession()) + { + var personList = + session + .GetNamedQuery("personPositional") + .SetParameter(0, new PersonCpId { IdA = 1, IdB = 2 }) + .SetParameter(1, new PersonCpId { IdA = 3, IdB = 4 }) + .List(); + Assert.That( + personList.Select(e => e.Name), + Is.EquivalentTo(new[] { "PERSON_1_2", "PERSON_3_4" })); + } + } + + [Test] + public void HqlPersonNamed() + { + using (var session = OpenSession()) + { + var personList = + session + .GetNamedQuery("personNamed") + .SetParameter("id1", new PersonCpId { IdA = 1, IdB = 2 }) + .SetParameter("id2", new PersonCpId { IdA = 3, IdB = 4 }) + .List(); + Assert.That( + personList.Select(e => e.Name), + Is.EquivalentTo(new[] { "PERSON_1_2", "PERSON_3_4" })); + } + } + + [Test] + public void GetPersonNoComponent() + { + using (var session = OpenSession()) + { + var person17_18 = + session.Get(new PersonNoComponent { IdA = 17, IdB = 18 }); + Assert.That(person17_18.Name, Is.EqualTo("PERSON_NO_COMPONENT_17_18")); + } + } + + [Test] + public void SqlPersonNoComponent() + { + using (var session = OpenSession()) + { + var personList = + session + .GetNamedQuery("personNoComponentSql") + .SetParameter(0, new PersonNoComponent { IdA = 17, IdB = 18 }) + .SetParameter(1, new PersonNoComponent { IdA = 19, IdB = 20 }) + .List(); + Assert.That( + personList.Select(e => e.Name), + Is.EquivalentTo(new[] { "PERSON_NO_COMPONENT_17_18", "PERSON_NO_COMPONENT_19_20" })); + } + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH3079/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH3079/Mappings.hbm.xml new file mode 100644 index 00000000000..a9b8a606aa3 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3079/Mappings.hbm.xml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/NHSpecificTest/NH3079/Person.cs b/src/NHibernate.Test/NHSpecificTest/NH3079/Person.cs new file mode 100644 index 00000000000..b058470b237 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3079/Person.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.NH3079 +{ + public class Person + { + public virtual PersonCpId CpId { get; set; } + + public virtual string Name { get; set; } + + public virtual ICollection EmploymentList { get; set; } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH3079/PersonCpId.cs b/src/NHibernate.Test/NHSpecificTest/NH3079/PersonCpId.cs new file mode 100644 index 00000000000..51788014334 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3079/PersonCpId.cs @@ -0,0 +1,22 @@ +namespace NHibernate.Test.NHSpecificTest.NH3079 +{ + public class PersonCpId + { + public int IdA { get; set; } + + public int IdB { get; set; } + + public override bool Equals(object obj) + { + if (!(obj is PersonCpId objCpId)) + return false; + + return IdA == objCpId.IdA && IdB == objCpId.IdB; + } + + public override int GetHashCode() + { + return IdA.GetHashCode() ^ IdB.GetHashCode(); + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH3079/PersonNoComponent.cs b/src/NHibernate.Test/NHSpecificTest/NH3079/PersonNoComponent.cs new file mode 100644 index 00000000000..7948701375d --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3079/PersonNoComponent.cs @@ -0,0 +1,24 @@ +namespace NHibernate.Test.NHSpecificTest.NH3079 +{ + public class PersonNoComponent + { + public virtual int IdA { get; set; } + + public virtual int IdB { get; set; } + + public virtual string Name { get; set; } + + public override bool Equals(object obj) + { + if (!(obj is PersonNoComponent objNoComponent)) + return false; + + return IdA == objNoComponent.IdA && IdB == objNoComponent.IdB; + } + + public override int GetHashCode() + { + return IdA.GetHashCode() ^ IdB.GetHashCode(); + } + } +} diff --git a/src/NHibernate/Async/Impl/SqlQueryImpl.cs b/src/NHibernate/Async/Impl/SqlQueryImpl.cs index ec46826430a..f4a0342ce37 100644 --- a/src/NHibernate/Async/Impl/SqlQueryImpl.cs +++ b/src/NHibernate/Async/Impl/SqlQueryImpl.cs @@ -11,6 +11,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using NHibernate.Engine; using NHibernate.Engine.Query; using NHibernate.Engine.Query.Sql; @@ -98,6 +99,7 @@ public partial class SqlQueryImpl : AbstractQueryImpl, ISQLQuery, ISynchronizabl Before(); try { + ComputeFlattenedParameters(); return await (Session.ExecuteNativeUpdateAsync(GenerateQuerySpecification(namedParams), GetQueryParameters(namedParams), cancellationToken)).ConfigureAwait(false); } finally diff --git a/src/NHibernate/Impl/AbstractQueryImpl.cs b/src/NHibernate/Impl/AbstractQueryImpl.cs index ba1e1402854..7b79057165c 100644 --- a/src/NHibernate/Impl/AbstractQueryImpl.cs +++ b/src/NHibernate/Impl/AbstractQueryImpl.cs @@ -79,11 +79,11 @@ protected internal virtual void VerifyParameters() } /// - /// Perform parameter validation. Used prior to executing the encapsulated query. + /// Perform parameters validation. Flatten them if needed. Used prior to executing the encapsulated query. /// /// - /// if true, the first ? will not be verified since - /// its needed for e.g. callable statements returning a out parameter + /// If true, the first positional parameter will not be verified since + /// its needed for e.g. callable statements returning an out parameter. /// protected internal virtual void VerifyParameters(bool reserveFirstParameter) { @@ -95,11 +95,15 @@ protected internal virtual void VerifyParameters(bool reserveFirstParameter) throw new QueryException("Not all named parameters have been set: " + CollectionPrinter.ToString(missingParams), QueryString); } - int positionalValueSpan = 0; - for (int i = 0; i < values.Count; i++) + var positionalValueSpan = 0; + // Values and Types may be overriden to yield refined parameters, check them + // instead of the fields. + var values = Values; + var types = Types; + for (var i = 0; i < values.Count; i++) { - object obj = types[i]; - if (values[i] == UNSET_PARAMETER || obj == UNSET_TYPE) + var type = types[i]; + if (values[i] == UNSET_PARAMETER || type == UNSET_TYPE) { if (reserveFirstParameter && i == 0) { @@ -763,12 +767,13 @@ protected IDictionary NamedParameterLists get { return namedParameterLists; } } - protected IList Values + // TODO 6.0: Change type to IList + protected virtual IList Values { get { return values; } } - protected IList Types + protected virtual IList Types { get { return types; } } diff --git a/src/NHibernate/Impl/SqlQueryImpl.cs b/src/NHibernate/Impl/SqlQueryImpl.cs index 079c390cde1..5629e2f714a 100644 --- a/src/NHibernate/Impl/SqlQueryImpl.cs +++ b/src/NHibernate/Impl/SqlQueryImpl.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using NHibernate.Engine; using NHibernate.Engine.Query; using NHibernate.Engine.Query.Sql; @@ -29,6 +30,8 @@ public partial class SqlQueryImpl : AbstractQueryImpl, ISQLQuery, ISynchronizabl private readonly bool callable; private bool autoDiscoverTypes; private readonly HashSet addedQuerySpaces = new HashSet(); + private List _flattenedTypes; + private List _flattenedValues; /// Constructs a SQLQueryImpl given a sql query defined in the mappings. /// The representation of the defined sql-query. @@ -298,6 +301,66 @@ protected internal override void VerifyParameters() } } + /// + protected internal override void VerifyParameters(bool reserveFirstParameter) + { + ComputeFlattenedParameters(); + base.VerifyParameters(reserveFirstParameter); + } + + // Flattening parameters is required for custom SQL loaders when entities have composite ids. + // See NH-3079 (#1117) + private void ComputeFlattenedParameters() + { + _flattenedTypes = new List(base.Types.Count * 2); + _flattenedValues = new List(base.Types.Count * 2); + FlattenTypesAndValues(base.Types, base.Values); + + void FlattenTypesAndValues(IList types, IList values) + { + for (var i = 0; i < types.Count; i++) + { + var type = types[i]; + var value = values[i]; + if (type is EntityType entityType) + { + type = entityType.GetIdentifierType(session); + value = entityType.GetIdentifier(value, session); + } + + if (type is IAbstractComponentType componentType) + { + FlattenTypesAndValues( + componentType.Subtypes, + componentType.GetPropertyValues(value, session)); + } + else + { + _flattenedTypes.Add(type); + _flattenedValues.Add(value); + } + } + } + } + + protected override IList Values => _flattenedValues ?? + throw new InvalidOperationException("Flattened parameters have not been computed"); + + protected override IList Types => _flattenedTypes ?? + throw new InvalidOperationException("Flattened parameters have not been computed"); + + public override object[] ValueArray() + { + // TODO 6.0: Change to Values.ToArray() + return _flattenedValues?.ToArray() ?? + throw new InvalidOperationException("Flattened parameters have not been computed"); + } + + public override IType[] TypeArray() + { + return Types.ToArray(); + } + public override IQuery SetLockMode(string alias, LockMode lockMode) { throw new NotSupportedException("cannot set the lock mode for a native SQL query"); @@ -309,6 +372,7 @@ public override int ExecuteUpdate() Before(); try { + ComputeFlattenedParameters(); return Session.ExecuteNativeUpdate(GenerateQuerySpecification(namedParams), GetQueryParameters(namedParams)); } finally