From 153f5a0635e6dc9847791b38f80776ea50b19732 Mon Sep 17 00:00:00 2001 From: Gunnar Liljas Date: Thu, 1 Jan 2015 16:54:18 +0100 Subject: [PATCH 1/7] NH-3488 - Strongly Typed Updates and Deletes --- .../Hql/Ast/BulkManipulation.cs | 48 + .../NHSpecificTest/NH3488/BaseFixture.cs | 46 + .../NHSpecificTest/NH3488/Domain/Address.cs | 41 + .../NHSpecificTest/NH3488/Domain/Animal.cs | 74 + .../NH3488/Domain/Animal.hbm.xml | 154 ++ .../NH3488/Domain/Classification.cs | 8 + .../NH3488/Domain/CrazyCompositeKey.cs | 51 + .../NH3488/Domain/DomesticAnimal.cs | 16 + .../Domain/EntityWithCrazyCompositeKey.cs | 42 + .../EntityWithCrazyCompositeKey.hbm.xml | 25 + .../NHSpecificTest/NH3488/Domain/Human.cs | 95 ++ .../NH3488/Domain/IntegerVersioned.cs | 29 + .../NHSpecificTest/NH3488/Domain/Joiner.cs | 27 + .../NHSpecificTest/NH3488/Domain/Mammal.cs | 22 + .../NH3488/Domain/Multi.hbm.xml | 104 ++ .../NHSpecificTest/NH3488/Domain/Name.cs | 27 + .../NHSpecificTest/NH3488/Domain/Reptile.cs | 29 + .../NH3488/Domain/SimpleAssociatedEntity.cs | 77 + .../NH3488/Domain/SimpleClass.cs | 9 + .../NH3488/Domain/SimpleClass.hbm.xml | 15 + .../NH3488/Domain/SimpleClassWithComponent.cs | 10 + .../Domain/SimpleClassWithComponent.hbm.xml | 19 + .../Domain/SimpleEntityWithAssociation.cs | 61 + .../SimpleEntityWithAssociation.hbm.xml | 31 + .../NH3488/Domain/StateProvince.cs | 27 + .../NH3488/Domain/TimestampVersioned.cs | 31 + .../NHSpecificTest/NH3488/Domain/User.cs | 36 + .../NH3488/Domain/Vehicle.hbm.xml | 29 + .../NHSpecificTest/NH3488/Domain/Vehicles.cs | 43 + .../NH3488/Domain/Versions.hbm.xml | 25 + .../NHSpecificTest/NH3488/Domain/Zoo.cs | 53 + .../NH3488/LinqBulkManipulationFixture.cs | 1282 +++++++++++++++++ src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs | 17 +- src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g | 8 + src/NHibernate/Hql/Ast/HqlTreeBuilder.cs | 45 +- src/NHibernate/Hql/Ast/HqlTreeNode.cs | 53 + src/NHibernate/Linq/Assignment.cs | 19 + src/NHibernate/Linq/Assignments.cs | 153 ++ src/NHibernate/Linq/DefaultQueryProvider.cs | 42 +- src/NHibernate/Linq/InsertSyntax.cs | 49 + src/NHibernate/Linq/IntermediateHqlTree.cs | 52 +- src/NHibernate/Linq/LinqExtensionMethods.cs | 76 +- src/NHibernate/Linq/NhLinqDeleteExpression.cs | 22 + src/NHibernate/Linq/NhLinqExpression.cs | 11 +- src/NHibernate/Linq/NhLinqInsertExpression.cs | 39 + src/NHibernate/Linq/NhLinqUpdateExpression.cs | 43 + src/NHibernate/Linq/QueryMode.cs | 16 + src/NHibernate/Linq/UpdateSyntax.cs | 51 + .../Visitors/HqlGeneratorExpressionVisitor.cs | 2 +- .../Linq/Visitors/QueryModelVisitor.cs | 79 +- .../Linq/Visitors/VisitorParameters.cs | 2 + src/NHibernate/Linq/Visitors/VisitorUtil.cs | 30 + 52 files changed, 3352 insertions(+), 43 deletions(-) create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/BaseFixture.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Address.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Animal.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Animal.hbm.xml create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Classification.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/CrazyCompositeKey.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/DomesticAnimal.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/EntityWithCrazyCompositeKey.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/EntityWithCrazyCompositeKey.hbm.xml create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Human.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/IntegerVersioned.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Joiner.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Mammal.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Multi.hbm.xml create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Name.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Reptile.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleAssociatedEntity.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClass.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClass.hbm.xml create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClassWithComponent.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClassWithComponent.hbm.xml create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleEntityWithAssociation.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleEntityWithAssociation.hbm.xml create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/StateProvince.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/TimestampVersioned.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/User.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Vehicle.hbm.xml create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Vehicles.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Versions.hbm.xml create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Zoo.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/LinqBulkManipulationFixture.cs create mode 100644 src/NHibernate/Linq/Assignment.cs create mode 100644 src/NHibernate/Linq/Assignments.cs create mode 100644 src/NHibernate/Linq/InsertSyntax.cs create mode 100644 src/NHibernate/Linq/NhLinqDeleteExpression.cs create mode 100644 src/NHibernate/Linq/NhLinqInsertExpression.cs create mode 100644 src/NHibernate/Linq/NhLinqUpdateExpression.cs create mode 100644 src/NHibernate/Linq/QueryMode.cs create mode 100644 src/NHibernate/Linq/UpdateSyntax.cs diff --git a/src/NHibernate.Test/Hql/Ast/BulkManipulation.cs b/src/NHibernate.Test/Hql/Ast/BulkManipulation.cs index 8885e9e8813..8d8bfe625e2 100644 --- a/src/NHibernate.Test/Hql/Ast/BulkManipulation.cs +++ b/src/NHibernate.Test/Hql/Ast/BulkManipulation.cs @@ -70,6 +70,29 @@ public void SimpleInsert() data.Cleanup(); } + [Test] + public void SimpleInsertFromAggregate() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + s.CreateQuery("insert into Pickup (id, Vin, Owner) select id, max(Vin), max(Owner) from Car group by id").ExecuteUpdate(); + + t.Commit(); + t = s.BeginTransaction(); + + s.CreateQuery("delete Vehicle").ExecuteUpdate(); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] public void InsertWithManyToOne() { @@ -92,6 +115,31 @@ public void InsertWithManyToOne() data.Cleanup(); } + [Test] + public void InsertWithManyToOneAsParameter() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + var mother = data.Butterfly; + + s.CreateQuery( + "insert into Animal (description, bodyWeight, mother) select description, bodyWeight, :mother from Human") + .SetEntity("mother",mother) + .ExecuteUpdate(); + + t.Commit(); + t = s.BeginTransaction(); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + [Test] public void InsertWithMismatchedTypes() { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/BaseFixture.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/BaseFixture.cs new file mode 100644 index 00000000000..010d1f9d129 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/BaseFixture.cs @@ -0,0 +1,46 @@ +using System.Collections; +using System.Collections.Generic; +using NHibernate.Hql.Ast.ANTLR; +using NHibernate.Util; + +namespace NHibernate.Test.NHSpecificTest.NH3488 +{ + public class BaseFixture: TestCase + { + private readonly IDictionary emptyfilters = new CollectionHelper.EmptyMapClass(); + + #region Overrides of TestCase + + protected override IList Mappings + { + get { return new string[0]; } + } + + #endregion + + protected override void Configure(Cfg.Configuration configuration) + { + var assembly = GetType().Assembly; + string mappingNamespace = GetType().Namespace; + foreach (var resource in assembly.GetManifestResourceNames()) + { + if (resource.StartsWith(mappingNamespace) && resource.EndsWith(".hbm.xml")) + { + configuration.AddResource(resource, assembly); + } + } + } + + public string GetSql(string query) + { + return GetSql(query, null); + } + + public string GetSql(string query, IDictionary replacements) + { + var qt = new QueryTranslatorImpl(null, new HqlParseEngine(query, false, Sfi).Parse(), emptyfilters, Sfi); + qt.Compile(replacements, false); + return qt.SQLString; + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Address.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Address.cs new file mode 100644 index 00000000000..46d222abdd8 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Address.cs @@ -0,0 +1,41 @@ +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class Address + { + private string street; + private string city; + private string postalCode; + private string country; + private StateProvince stateProvince; + + public string Street + { + get { return street; } + set { street = value; } + } + + public string City + { + get { return city; } + set { city = value; } + } + + public string PostalCode + { + get { return postalCode; } + set { postalCode = value; } + } + + public string Country + { + get { return country; } + set { country = value; } + } + + public StateProvince StateProvince + { + get { return stateProvince; } + set { stateProvince = value; } + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Animal.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Animal.cs new file mode 100644 index 00000000000..ab4016e660b --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Animal.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class Animal + { + private long id; + private float bodyWeight; + private ISet offspring; + private Animal mother; + private Animal father; + private string description; + private Zoo zoo; + private string serialNumber; + + public virtual long Id + { + get { return id; } + set { id = value; } + } + + public virtual float BodyWeight + { + get { return bodyWeight; } + set { bodyWeight = value; } + } + + public virtual ISet Offspring + { + get { return offspring; } + set { offspring = value; } + } + + public virtual Animal Mother + { + get { return mother; } + set { mother = value; } + } + + public virtual Animal Father + { + get { return father; } + set { father = value; } + } + + public virtual string Description + { + get { return description; } + set { description = value; } + } + + public virtual Zoo Zoo + { + get { return zoo; } + set { zoo = value; } + } + + public virtual string SerialNumber + { + get { return serialNumber; } + set { serialNumber = value; } + } + + public virtual void AddOffspring(Animal offSpring) + { + if (offspring == null) + { + offspring = new HashSet(); + } + + offspring.Add(offSpring); + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Animal.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Animal.hbm.xml new file mode 100644 index 00000000000..ab163ae27ba --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Animal.hbm.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + human + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Classification.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Classification.cs new file mode 100644 index 00000000000..5fbfe740ce1 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Classification.cs @@ -0,0 +1,8 @@ +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public enum Classification + { + Cool = 0, + Lame = 1 + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/CrazyCompositeKey.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/CrazyCompositeKey.cs new file mode 100644 index 00000000000..177dcbe843d --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/CrazyCompositeKey.cs @@ -0,0 +1,51 @@ +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class CrazyCompositeKey + { + private long id; + private long otherId; + private int? requestedHash; + + public virtual long Id + { + get { return id; } + set { id = value; } + } + + public virtual long OtherId + { + get { return otherId; } + set { otherId = value; } + } + + public override bool Equals(object obj) + { + return Equals(obj as CrazyCompositeKey); + } + + public virtual bool Equals(CrazyCompositeKey other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + if (ReferenceEquals(this, other)) + { + return true; + } + return other.id == id && other.otherId == otherId; + } + + public override int GetHashCode() + { + if (!requestedHash.HasValue) + { + unchecked + { + requestedHash = (id.GetHashCode() * 397) ^ otherId.GetHashCode(); + } + } + return requestedHash.Value; + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/DomesticAnimal.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/DomesticAnimal.cs new file mode 100644 index 00000000000..51c2a10f8d7 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/DomesticAnimal.cs @@ -0,0 +1,16 @@ +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class DomesticAnimal: Mammal + { + private Human owner; + + public virtual Human Owner + { + get { return owner; } + set { owner = value; } + } + } + + public class Cat : DomesticAnimal { } + public class Dog : DomesticAnimal { } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/EntityWithCrazyCompositeKey.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/EntityWithCrazyCompositeKey.cs new file mode 100644 index 00000000000..da3367daa3e --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/EntityWithCrazyCompositeKey.cs @@ -0,0 +1,42 @@ +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class EntityWithCrazyCompositeKey + { + private CrazyCompositeKey id; + private string name; + + public virtual CrazyCompositeKey Id + { + get { return id; } + set { id = value; } + } + + public virtual string Name + { + get { return name; } + set { name = value; } + } + + public virtual EntityWithCrazyCompositeKey Parent { get; set; } + } + + public class EntityReferencingEntityWithCrazyCompositeKey + { + private long id; + private string name; + + public virtual long Id + { + get { return id; } + set { id = value; } + } + + public virtual string Name + { + get { return name; } + set { name = value; } + } + + public virtual EntityWithCrazyCompositeKey Parent { get; set; } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/EntityWithCrazyCompositeKey.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/EntityWithCrazyCompositeKey.hbm.xml new file mode 100644 index 00000000000..0fc29196018 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/EntityWithCrazyCompositeKey.hbm.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Human.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Human.cs new file mode 100644 index 00000000000..7a8cb9abea9 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Human.cs @@ -0,0 +1,95 @@ +using System.Collections; +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class Human: Mammal + { + private Name name; + private string nickName; + private ICollection friends; + private ICollection pets; + private IDictionary family; + private double height; + + private long bigIntegerValue; + private decimal bigDecimalValue; + private int intValue; + private float floatValue; + + private ISet nickNames; + private IDictionary addresses; + + public virtual Name Name + { + get { return name; } + set { name = value; } + } + + public virtual string NickName + { + get { return nickName; } + set { nickName = value; } + } + + public virtual ICollection Friends + { + get { return friends; } + set { friends = value; } + } + + public virtual ICollection Pets + { + get { return pets; } + set { pets = value; } + } + + public virtual IDictionary Family + { + get { return family; } + set { family = value; } + } + + public virtual double Height + { + get { return height; } + set { height = value; } + } + + public virtual long BigIntegerValue + { + get { return bigIntegerValue; } + set { bigIntegerValue = value; } + } + + public virtual decimal BigDecimalValue + { + get { return bigDecimalValue; } + set { bigDecimalValue = value; } + } + + public virtual int IntValue + { + get { return intValue; } + set { intValue = value; } + } + + public virtual float FloatValue + { + get { return floatValue; } + set { floatValue = value; } + } + + public virtual ISet NickNames + { + get { return nickNames; } + set { nickNames = value; } + } + + public virtual IDictionary Addresses + { + get { return addresses; } + set { addresses = value; } + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/IntegerVersioned.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/IntegerVersioned.cs new file mode 100644 index 00000000000..268ba1ca302 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/IntegerVersioned.cs @@ -0,0 +1,29 @@ +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class IntegerVersioned + { + private long id; + private int version; + private string name; + + public virtual long Id + { + get { return id; } + set { id = value; } + } + + public virtual int Version + { + get { return version; } + set { version = value; } + } + + public virtual string Name + { + get { return name; } + set { name = value; } + } + + public virtual string Data { get; set; } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Joiner.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Joiner.cs new file mode 100644 index 00000000000..b8c5f04582c --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Joiner.cs @@ -0,0 +1,27 @@ +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class Joiner + { + private long id; + private string name; + private string joinedName; + + public virtual long Id + { + get { return id; } + set { id = value; } + } + + public virtual string Name + { + get { return name; } + set { name = value; } + } + + public virtual string JoinedName + { + get { return joinedName; } + set { joinedName = value; } + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Mammal.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Mammal.cs new file mode 100644 index 00000000000..982b106b31f --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Mammal.cs @@ -0,0 +1,22 @@ +using System; + +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class Mammal: Animal + { + private bool pregnant; + private DateTime birthdate; + + public virtual bool Pregnant + { + get { return pregnant; } + set { pregnant = value; } + } + + public virtual DateTime Birthdate + { + get { return birthdate; } + set { birthdate = value; } + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Multi.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Multi.hbm.xml new file mode 100644 index 00000000000..694cf276f23 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Multi.hbm.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Name.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Name.cs new file mode 100644 index 00000000000..073c3094ec9 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Name.cs @@ -0,0 +1,27 @@ +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class Name + { + private string first; + private char initial; + private string last; + + public string First + { + get { return first; } + set { first = value; } + } + + public char Initial + { + get { return initial; } + set { initial = value; } + } + + public string Last + { + get { return last; } + set { last = value; } + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Reptile.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Reptile.cs new file mode 100644 index 00000000000..17f8ed4c772 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Reptile.cs @@ -0,0 +1,29 @@ +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class Reptile: Animal + { + private float bodyTemperature; + public virtual float BodyTemperature + { + get { return bodyTemperature; } + set { bodyTemperature = value; } + } + } + + public class Dragon : Animal + { + private float fireTemperature; + public virtual float FireTemperature + { + get { return fireTemperature; } + protected set { fireTemperature = value; } + } + + public virtual void SetFireTemperature(float temperature) + { + fireTemperature = temperature; + } + } + + public class Lizard : Reptile { } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleAssociatedEntity.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleAssociatedEntity.cs new file mode 100644 index 00000000000..33daccd19e5 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleAssociatedEntity.cs @@ -0,0 +1,77 @@ +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class SimpleAssociatedEntity + { + private long id; + private string name; + private int? requestedHash; + private SimpleEntityWithAssociation owner; + + public virtual long Id + { + get { return id; } + set { id = value; } + } + + public virtual string Name + { + get { return name; } + set { name = value; } + } + + public virtual SimpleEntityWithAssociation Owner + { + get { return owner; } + set { owner = value; } + } + + public virtual void BindToOwner(SimpleEntityWithAssociation owner) + { + if (owner != this.owner) + { + UnbindFromCurrentOwner(); + } + this.owner = owner; + if (owner != null) + { + owner.AssociatedEntities.Add(this); + } + } + + public virtual void UnbindFromCurrentOwner() + { + if (owner != null) + { + owner.AssociatedEntities.Remove(this); + owner = null; + } + } + + public override bool Equals(object obj) + { + return Equals(obj as SimpleAssociatedEntity); + } + + public virtual bool Equals(SimpleAssociatedEntity other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + if (ReferenceEquals(this, other)) + { + return true; + } + return Equals(other.Id, Id); + } + + public override int GetHashCode() + { + if (!requestedHash.HasValue) + { + requestedHash = Id.GetHashCode(); + } + return requestedHash.Value; + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClass.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClass.cs new file mode 100644 index 00000000000..42d6cbc4945 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClass.cs @@ -0,0 +1,9 @@ +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class SimpleClass + { + public virtual string Description { get; set; } + public virtual long LongValue { get; set; } + public virtual int IntValue { get; set; } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClass.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClass.hbm.xml new file mode 100644 index 00000000000..a799eb53791 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClass.hbm.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClassWithComponent.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClassWithComponent.cs new file mode 100644 index 00000000000..4d49daef802 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClassWithComponent.cs @@ -0,0 +1,10 @@ +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class SimpleClassWithComponent + { + public virtual string Description { get; set; } + public virtual long LongValue { get; set; } + public virtual int IntValue { get; set; } + public virtual Name Name { get; set; } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClassWithComponent.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClassWithComponent.hbm.xml new file mode 100644 index 00000000000..f53c5f1906f --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClassWithComponent.hbm.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleEntityWithAssociation.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleEntityWithAssociation.cs new file mode 100644 index 00000000000..cb04723a90d --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleEntityWithAssociation.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class SimpleEntityWithAssociation + { + private long id; + private string name; + private ISet associatedEntities = new HashSet(); + private ISet manyToManyAssociatedEntities = new HashSet(); + + public virtual long Id + { + get { return id; } + set { id = value; } + } + + public virtual string Name + { + get { return name; } + set { name = value; } + } + + public virtual ISet AssociatedEntities + { + get { return associatedEntities; } + set { associatedEntities = value; } + } + + public virtual ISet ManyToManyAssociatedEntities + { + get { return manyToManyAssociatedEntities; } + set { manyToManyAssociatedEntities = value; } + } + + public virtual SimpleAssociatedEntity AddAssociation(string aName) + { + var result = new SimpleAssociatedEntity {Name = aName, Owner = this}; + AddAssociation(result); + return result; + } + + public virtual void AddAssociation(SimpleAssociatedEntity association) + { + association.BindToOwner(this); + } + + public virtual void RemoveAssociation(SimpleAssociatedEntity association) + { + if (AssociatedEntities.Contains(association)) + { + association.UnbindFromCurrentOwner(); + } + else + { + throw new ArgumentException("SimpleAssociatedEntity [" + association + "] not currently bound to this [" + this + "]"); + } + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleEntityWithAssociation.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleEntityWithAssociation.hbm.xml new file mode 100644 index 00000000000..f57611a092a --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleEntityWithAssociation.hbm.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/StateProvince.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/StateProvince.cs new file mode 100644 index 00000000000..502c2aebd95 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/StateProvince.cs @@ -0,0 +1,27 @@ +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class StateProvince + { + private long id; + private string name; + private string isoCode; + + public virtual long Id + { + get { return id; } + set { id = value; } + } + + public virtual string Name + { + get { return name; } + set { name = value; } + } + + public virtual string IsoCode + { + get { return isoCode; } + set { isoCode = value; } + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/TimestampVersioned.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/TimestampVersioned.cs new file mode 100644 index 00000000000..2ef0a6ae315 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/TimestampVersioned.cs @@ -0,0 +1,31 @@ +using System; + +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class TimestampVersioned + { + private long id; + private DateTime version; + private string name; + + public virtual long Id + { + get { return id; } + set { id = value; } + } + + public virtual DateTime Version + { + get { return version; } + set { version = value; } + } + + public virtual string Name + { + get { return name; } + set { name = value; } + } + + public virtual string Data { get; set; } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/User.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/User.cs new file mode 100644 index 00000000000..135d4c4273c --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/User.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class User + { + private long id; + private string userName; + private Human human; + private IList permissions; + + public virtual long Id + { + get { return id; } + set { id = value; } + } + + public virtual string UserName + { + get { return userName; } + set { userName = value; } + } + + public virtual Human Human + { + get { return human; } + set { human = value; } + } + + public virtual IList Permissions + { + get { return permissions; } + set { permissions = value; } + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Vehicle.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Vehicle.hbm.xml new file mode 100644 index 00000000000..ac86e3e22be --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Vehicle.hbm.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + delete from CAR where owner = ? + + \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Vehicles.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Vehicles.cs new file mode 100644 index 00000000000..8d37daa8499 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Vehicles.cs @@ -0,0 +1,43 @@ +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class Vehicle + { + private long id; + private string vin; + private string owner; + + public virtual long Id + { + get { return id; } + set { id = value; } + } + + public virtual string Vin + { + get { return vin; } + set { vin = value; } + } + + public virtual string Owner + { + get { return owner; } + set { owner = value; } + } + } + + public class Car : Vehicle + { + } + + public class Truck : Vehicle + { + } + + public class Pickup : Truck + { + } + + public class SUV : Truck + { + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Versions.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Versions.hbm.xml new file mode 100644 index 00000000000..45c30ba6bde --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Versions.hbm.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Zoo.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Zoo.cs new file mode 100644 index 00000000000..ed87ac6be1f --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Zoo.cs @@ -0,0 +1,53 @@ +using System.Collections; +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +{ + public class Zoo + { + private long id; + private string name; + private Classification classification; + private IDictionary animals; + private IDictionary mammals; + private Address address; + + public virtual long Id + { + get { return id; } + set { id = value; } + } + + public virtual string Name + { + get { return name; } + set { name = value; } + } + + public virtual Classification Classification + { + get { return classification; } + set { classification = value; } + } + + public virtual IDictionary Animals + { + get { return animals; } + set { animals = value; } + } + + public virtual IDictionary Mammals + { + get { return mammals; } + set { mammals = value; } + } + + public virtual Address Address + { + get { return address; } + set { address = value; } + } + } + + public class PettingZoo : Zoo { } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/LinqBulkManipulationFixture.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/LinqBulkManipulationFixture.cs new file mode 100644 index 00000000000..f9485480b42 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3488/LinqBulkManipulationFixture.cs @@ -0,0 +1,1282 @@ +using NHibernate.Dialect; +using NHibernate.DomainModel; +using NHibernate.Hql.Ast.ANTLR; +using NHibernate.Id; +using NHibernate.Linq; +using NHibernate.Persister.Entity; +using NHibernate.Test.NHSpecificTest.NH3488.Domain; +using NUnit.Framework; +using System; +using System.Collections; +using System.Linq; +using System.Threading; + +namespace NHibernate.Test.NHSpecificTest.NH3488 +{ + [TestFixture] + public class LinqBulkManipulationFixture : BaseFixture + { + public ISession OpenNewSession() + { + return OpenSession(); + } + + + #region INSERTS + + [Test] + public void SimpleInsert() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + s.Query().Insert().As(x=>new Pickup{Id=x.Id,Vin=x.Vin,Owner=x.Owner}); + + t.Commit(); + t = s.BeginTransaction(); + + s.CreateQuery("delete Vehicle").ExecuteUpdate(); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void SimpleInsertFromAggregate() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + s.Query() + .GroupBy(x => x.Id) + .Select(x => new { Id = x.Key, Vin = x.Max(y => y.Vin), Owner = x.Max(y => y.Owner) }) + .Insert().As(x => new Pickup { Id = x.Id, Vin = x.Vin, Owner = x.Owner }); + + t.Commit(); + t = s.BeginTransaction(); + + s.CreateQuery("delete Vehicle").ExecuteUpdate(); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void SimpleInsertFromLimited() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + s.Query() + .Skip(1) + .Take(1) + .Insert().As(x => new Pickup { Id = x.Id, Vin = x.Vin, Owner = x.Owner }); + + t.Commit(); + t = s.BeginTransaction(); + + s.CreateQuery("delete Vehicle").ExecuteUpdate(); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void SimpleInsertWithConstants() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + s.Query() + .Insert().Into(x => x.Set(y=>y.Id,y=>y.Id).Set(y=>y.Vin,y=>y.Vin).Set(y=>y.Owner,"The owner")); + + t.Commit(); + t = s.BeginTransaction(); + + s.CreateQuery("delete Vehicle").ExecuteUpdate(); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void SimpleInsertFromProjection() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + s.Query() + .Select(x=>new {x.Id,x.Owner,UpperOwner=x.Owner.ToUpper()}) + .Insert().Into(x => x.Set(y => y.Id, y => y.Id).Set(y => y.Vin, y => y.UpperOwner)); + + t.Commit(); + t = s.BeginTransaction(); + + s.CreateQuery("delete Vehicle").ExecuteUpdate(); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void InsertWithClientSideRequirementsThrowsException() + { + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + Assert.Throws(() => + s.Query() + .Insert().As(x => new Pickup {Id = x.Id, Vin = x.Vin, Owner = x.Owner.PadRight(200)})); + + t.Commit(); + t = s.BeginTransaction(); + + s.CreateQuery("delete Vehicle").ExecuteUpdate(); + + t.Commit(); + s.Close(); + } + + + [Test] + public void InsertWithManyToOne() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + s.Query() + .Insert().As(x => new Animal {Description = x.Description, BodyWeight = x.BodyWeight, Mother = x.Mother}); + + t.Commit(); + t = s.BeginTransaction(); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + + [Test] + public void InsertWithManyToOneAsParameter() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + s.Query() + .Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = data.Butterfly }); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void InsertWithManyToOneWithCompositeKey() + { + + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + var parent = new EntityWithCrazyCompositeKey {Id = new CrazyCompositeKey {Id=1, OtherId=1}, Name = "Parent"}; + + s.Save(parent); + + t.Commit(); + t = s.BeginTransaction(); + + s.Query() + .Insert().As(x => new EntityReferencingEntityWithCrazyCompositeKey { Name = "Child", Parent = x }); + + t.Commit(); + t = s.BeginTransaction(); + + s.CreateQuery("delete EntityReferencingEntityWithCrazyCompositeKey").ExecuteUpdate(); + s.CreateQuery("delete EntityWithCrazyCompositeKey").ExecuteUpdate(); + + t.Commit(); + s.Close(); + + + } + + + + [Test] + public void InsertIntoSuperclassPropertiesFails() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + Assert.Throws( + () => s.Query().Insert().As(x=>new Human{Id=x.Id,BodyWeight = x.BodyWeight}), + "superclass prop insertion did not error"); + + t.Commit(); + t = s.BeginTransaction(); + + s.CreateQuery("delete Animal where Mother is not null").ExecuteUpdate(); + s.CreateQuery("delete Animal where Father is not null").ExecuteUpdate(); + s.CreateQuery("delete Animal").ExecuteUpdate(); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void InsertAcrossMappedJoinFails() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + Assert.Throws( + () => s.Query().Insert().As(x=>new Joiner{Name = x.Vin,JoinedName = x.Owner}), + "mapped-join insertion did not error"); + + t.Commit(); + t = s.BeginTransaction(); + + s.CreateQuery("delete Joiner").ExecuteUpdate(); + s.CreateQuery("delete Vehicle").ExecuteUpdate(); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + public void InsertWithGeneratedId() + { + // Make sure the env supports bulk inserts with generated ids... + IEntityPersister persister = Sfi.GetEntityPersister(typeof(PettingZoo).FullName); + IIdentifierGenerator generator = persister.IdentifierGenerator; + if (!HqlSqlWalker.SupportsIdGenWithBulkInsertion(generator)) + { + return; + } + + // create a Zoo + var zoo = new Zoo { Name = "zoo" }; + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + s.Save(zoo); + t.Commit(); + s.Close(); + + s = OpenSession(); + t = s.BeginTransaction(); + int count = s.Query().Insert().As(x=>new PettingZoo{Name=x.Name}); + t.Commit(); + s.Close(); + Assert.That(count, Is.EqualTo(1), "unexpected insertion count"); + + s = OpenSession(); + t = s.BeginTransaction(); + var pz = (PettingZoo)s.CreateQuery("from PettingZoo").UniqueResult(); + t.Commit(); + s.Close(); + + Assert.That(zoo.Name, Is.EqualTo(pz.Name)); + Assert.That(zoo.Id != pz.Id); + + s = OpenSession(); + t = s.BeginTransaction(); + s.CreateQuery("delete Zoo").ExecuteUpdate(); + t.Commit(); + s.Close(); + } + + [Test] + public void InsertWithGeneratedVersionAndId() + { + // Make sure the env supports bulk inserts with generated ids... + IEntityPersister persister = Sfi.GetEntityPersister(typeof(IntegerVersioned).FullName); + IIdentifierGenerator generator = persister.IdentifierGenerator; + if (!HqlSqlWalker.SupportsIdGenWithBulkInsertion(generator)) + { + return; + } + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + var entity = new IntegerVersioned { Name = "int-vers" }; + s.Save(entity); + s.CreateQuery("select Id, Name, Version from IntegerVersioned").List(); + t.Commit(); + s.Close(); + + long initialId = entity.Id; + int initialVersion = entity.Version; + + s = OpenSession(); + t = s.BeginTransaction(); + int count = + s.Query() + .Where(x => x.Id == entity.Id) + .Insert().As(x => new IntegerVersioned {Name = x.Name, Data = x.Data}); + t.Commit(); + s.Close(); + + Assert.That(count, Is.EqualTo(1), "unexpected insertion count"); + + s = OpenSession(); + t = s.BeginTransaction(); + var created = + (IntegerVersioned) + s.CreateQuery("from IntegerVersioned where Id <> :initialId").SetInt64("initialId", initialId).UniqueResult(); + t.Commit(); + s.Close(); + + Assert.That(created.Version, Is.EqualTo(initialVersion), "version was not seeded"); + + s = OpenSession(); + t = s.BeginTransaction(); + s.CreateQuery("delete IntegerVersioned").ExecuteUpdate(); + t.Commit(); + s.Close(); + } + + [Test] + public void InsertWithGeneratedTimestampVersion() + { + // Make sure the env supports bulk inserts with generated ids... + IEntityPersister persister = Sfi.GetEntityPersister(typeof(TimestampVersioned).FullName); + IIdentifierGenerator generator = persister.IdentifierGenerator; + if (!HqlSqlWalker.SupportsIdGenWithBulkInsertion(generator)) + { + return; + } + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + var entity = new TimestampVersioned { Name = "int-vers" }; + s.Save(entity); + s.CreateQuery("select Id, Name, Version from TimestampVersioned").List(); + t.Commit(); + s.Close(); + + long initialId = entity.Id; + //Date initialVersion = entity.getVersion(); + + s = OpenSession(); + t = s.BeginTransaction(); + int count = + s.Query() + .Where(x => x.Id == entity.Id) + .Insert().As(x => new TimestampVersioned {Name = x.Name, Data = x.Data}); + + t.Commit(); + s.Close(); + + Assert.That(count, Is.EqualTo(1), "unexpected insertion count"); + + s = OpenSession(); + t = s.BeginTransaction(); + var created = + (TimestampVersioned) + s.CreateQuery("from TimestampVersioned where Id <> :initialId").SetInt64("initialId", initialId).UniqueResult(); + t.Commit(); + s.Close(); + + Assert.That(created.Version, Is.GreaterThan(DateTime.Today)); + + s = OpenSession(); + t = s.BeginTransaction(); + s.CreateQuery("delete TimestampVersioned").ExecuteUpdate(); + t.Commit(); + s.Close(); + } + + [Test] + public void InsertWithSelectListUsingJoins() + { + // this is just checking parsing and syntax... + ISession s = OpenSession(); + s.BeginTransaction(); + + s.Query().Where(x=>x.Mother.Mother!=null) + .Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight }); + + s.CreateQuery("delete from Animal").ExecuteUpdate(); + s.Transaction.Commit(); + s.Close(); + } + + [Test] + public void InsertToComponent() + { + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + var human = new SimpleClassWithComponent { Name = new Name { First = "Stevee", Initial = 'X', Last = "Ebersole" } }; + + s.Save(human); + t.Commit(); + + string correctName = "Steve"; + + t = s.BeginTransaction(); + int count = + s.Query().Insert().Into(x => x.Set(y => y.Name.First, y => correctName)); + Assert.That(count, Is.EqualTo(1), "incorrect insert count"); + + count = + s.Query() + .Where(x=>x.Name.First==correctName) + .Insert().As(x => new SimpleClassWithComponent {Name = new Name {First = x.Name.First,Last=x.Name.Last,Initial = 'Z'}}); + Assert.That(count, Is.EqualTo(1), "incorrect insert count"); + t.Commit(); + + t = s.BeginTransaction(); + + s.CreateQuery("delete SimpleClassWithComponent").ExecuteUpdate(); + t.Commit(); + + s.Close(); + } + + #endregion + + + #region UPDATES + + + [Test] + public void UpdateWithWhereExistsSubquery() + { + // multi-table ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + var joe = new Human { Name = new Name { First = "Joe", Initial = 'Q', Last = "Public" } }; + s.Save(joe); + var doll = new Human { Name = new Name { First = "Kyu", Initial = 'P', Last = "Doll" }, Friends = new[] { joe } }; + s.Save(doll); + t.Commit(); + s.Close(); + + s = OpenSession(); + t = s.BeginTransaction(); + + int count = s.Query() + .Where(x => x.Friends.OfType().Any(f => f.Name.Last == "Public")) + .Update().Assign(x => x.Set(y => y.Description, "updated")); + Assert.That(count, Is.EqualTo(1)); + s.Delete(doll); + s.Delete(joe); + t.Commit(); + s.Close(); + + // single-table (one-to-many & many-to-many) ~~~~~~~~~~~~~~~~~~~~~~~~~~ + s = OpenSession(); + t = s.BeginTransaction(); + var entity = new SimpleEntityWithAssociation(); + var other = new SimpleEntityWithAssociation(); + entity.Name = "main"; + other.Name = "many-to-many-association"; + entity.ManyToManyAssociatedEntities.Add(other); + entity.AddAssociation("one-to-many-association"); + s.Save(entity); + t.Commit(); + s.Close(); + + s = OpenSession(); + t = s.BeginTransaction(); + // one-to-many test + + count = s.Query() + .Where(x => x.AssociatedEntities.Any(a => a.Name == "one-to-many-association")) + .Update().Assign(x => x.Set(y => y.Name, "updated")); + Assert.That(count, Is.EqualTo(1)); + // many-to-many test + if (Dialect.SupportsSubqueryOnMutatingTable) + { + count = s.Query() + .Where(x => x.ManyToManyAssociatedEntities.Any(a => a.Name == "many-to-many-association")) + .Update().Assign(x => x.Set(y => y.Name, "updated")); + + Assert.That(count, Is.EqualTo(1)); + } + IEnumerator mtm = entity.ManyToManyAssociatedEntities.GetEnumerator(); + mtm.MoveNext(); + s.Delete(mtm.Current); + s.Delete(entity); + t.Commit(); + s.Close(); + } + + [Test] + public void IncrementCounterVersion() + { + IntegerVersioned entity; + + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) + { + entity = new IntegerVersioned { Name = "int-vers", Data = "foo" }; + s.Save(entity); + t.Commit(); + } + + int initialVersion = entity.Version; + + using (ISession s = OpenSession()) + { + using (ITransaction t = s.BeginTransaction()) + { + // Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26 + int count = + s.Query() + .Update().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd"), true); + Assert.That(count, Is.EqualTo(1), "incorrect exec count"); + t.Commit(); + } + + using (ITransaction t = s.BeginTransaction()) + { + entity = s.Get(entity.Id); + s.Delete(entity); + t.Commit(); + } + } + + Assert.That(entity.Version, Is.EqualTo(initialVersion + 1), "version not incremented"); + Assert.That(entity.Name, Is.EqualTo("int-versupd")); + Assert.That(entity.Data, Is.EqualTo("fooupd")); + } + + [Test] + public void IncrementTimestampVersion() + { + TimestampVersioned entity; + + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) + { + entity = new TimestampVersioned { Name = "ts-vers", Data = "foo" }; + s.Save(entity); + t.Commit(); + } + + DateTime initialVersion = entity.Version; + + Thread.Sleep(1300); + + using (ISession s = OpenSession()) + { + using (ITransaction t = s.BeginTransaction()) + { + // Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26 + int count = s.Query(). + Update().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd"), true); + Assert.That(count, Is.EqualTo(1), "incorrect exec count"); + t.Commit(); + } + + using (ITransaction t = s.BeginTransaction()) + { + entity = s.Load(entity.Id); + s.Delete(entity); + t.Commit(); + } + } + + Assert.That(entity.Version, Is.GreaterThan(initialVersion), "version not incremented"); + Assert.That(entity.Name, Is.EqualTo("ts-versupd")); + Assert.That(entity.Data, Is.EqualTo("fooupd")); + } + + [Test] + public void UpdateOnComponent() + { + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + var human = new Human { Name = new Name { First = "Stevee", Initial = 'X', Last = "Ebersole" } }; + + s.Save(human); + t.Commit(); + + string correctName = "Steve"; + + t = s.BeginTransaction(); + int count = + s.Query().Where(x => x.Id == human.Id).Update().As(x => new Human{Name={First = correctName}}); + + Assert.That(count, Is.EqualTo(1), "incorrect update count"); + t.Commit(); + + t = s.BeginTransaction(); + s.Refresh(human); + + Assert.That(human.Name.First, Is.EqualTo(correctName), "Update did not execute properly"); + + s.CreateQuery("delete Human").ExecuteUpdate(); + t.Commit(); + + s.Close(); + } + + [Test] + public void UpdateWithClientSideRequirementsThrowsException() + { + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + var human = new Human { Name = new Name { First = "Stevee", Initial = 'X', Last = "Ebersole" } }; + + s.Save(human); + t.Commit(); + + t = s.BeginTransaction(); + + Assert.Throws(()=> + s.Query().Where(x => x.Id == human.Id).Update().As(x => new Human { Name = { First = x.Name.First.PadLeft(200) } }) + ); + + t.Commit(); + + t = s.BeginTransaction(); + + s.CreateQuery("delete Human").ExecuteUpdate(); + t.Commit(); + + s.Close(); + } + + [Test] + public void UpdateOnManyToOne() + { + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + s.Query().Where(x => x.Id == 2).Update().Assign(x => x.Set(y => y.Mother, y => null)); + + if (!(Dialect is MySQLDialect)) + { + // MySQL does not support (even un-correlated) subqueries against the update-mutating table + s.Query().Where(x => x.Id == 2).Update().Assign(x => x.Set(y => y.Mother, y => s.Query().First(z => z.Id == 1))); + } + + t.Commit(); + s.Close(); + } + + + + [Test] + public void UpdateOnDiscriminatorSubclass() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + int count = s.Query().Update().Assign(x => x.Set(y => y.Name, y => y.Name)); + Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass update count"); + + t.Rollback(); + t = s.BeginTransaction(); + + count = s.Query().Where(x => x.Id == data.PettingZoo.Id).Update().Assign(x => x.Set(y => y.Name, y => y.Name)); + Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass update count"); + + t.Rollback(); + t = s.BeginTransaction(); + + count = s.Query().Update().Assign(x => x.Set(y => y.Name, y => y.Name)); + Assert.That(count, Is.EqualTo(2), "Incorrect discrim subclass update count"); + + t.Rollback(); + t = s.BeginTransaction(); + + // TODO : not so sure this should be allowed. Seems to me that if they specify an alias, + // property-refs should be required to be qualified. + count = s.Query().Where(x => x.Id == data.Zoo.Id).Update().Assign(x => x.Set(y => y.Name, y => y.Name)); + Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass update count"); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void UpdateOnAnimal() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + //int count = s.Query().Where(x => x.Description == data.Frog.Description).Update(x => x.Set(y => y.Description, y => y.Description)); + //Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); + + int count = + s.Query().Where(x => x.Description == data.Polliwog.Description).Update().Assign(x => x.Set(y => y.Description, y => "Tadpole")); + Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); + + var tadpole = s.Load(data.Polliwog.Id); + + Assert.That(tadpole.Description, Is.EqualTo("Tadpole"), "Update did not take effect"); + + count = + s.Query().Update().Assign(x => x.Set(y => y.FireTemperature, 300)); + Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); + + + count = + s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, y => y.BodyWeight + 1 + 1)); + Assert.That(count, Is.EqualTo(7), "incorrect count on 'complex' update assignment"); + + if (!(Dialect is MySQLDialect)) + { + // MySQL does not support (even un-correlated) subqueries against the update-mutating table + s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, y => s.Query().Max(z => z.BodyWeight))); + } + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void UpdateOnDragonWithProtectedProperty() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + int count = + s.Query().Update().Assign(x => x.Set(y => y.FireTemperature, 300)); + Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void UpdateMultiplePropertyOnAnimal() + { + var data = new TestData(this); + data.Prepare(); + + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) + { + int count = + + s.Query() + .Where(x => x.Description == data.Polliwog.Description) + .Update().Assign(x => x.Set(y => y.Description, y => "Tadpole").Set(y => y.BodyWeight, 3)); + + + + Assert.That(count, Is.EqualTo(1)); + t.Commit(); + } + + using (ISession s = OpenSession()) + using (s.BeginTransaction()) + { + var tadpole = s.Get(data.Polliwog.Id); + Assert.That(tadpole.Description, Is.EqualTo("Tadpole")); + Assert.That(tadpole.BodyWeight, Is.EqualTo(3)); + } + + data.Cleanup(); + } + + [Test] + public void UpdateOnMammal() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + int count = s.Query().Update().Assign(x=>x.Set(y=>y.Description,y=>y.Description)); + + + Assert.That(count, Is.EqualTo(2), "incorrect update count against 'middle' of joined-subclass hierarchy"); + + count = s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, 25)); + Assert.That(count, Is.EqualTo(2), "incorrect update count against 'middle' of joined-subclass hierarchy"); + + if (!(Dialect is MySQLDialect)) + { + // MySQL does not support (even un-correlated) subqueries against the update-mutating table + count = s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, y => s.Query().Max(z => z.BodyWeight))); + Assert.That(count, Is.EqualTo(2), "incorrect update count against 'middle' of joined-subclass hierarchy"); + } + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void UpdateSetNullUnionSubclass() + { + var data = new TestData(this); + data.Prepare(); + + // These should reach out into *all* subclass tables... + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + int count = s.Query().Update().Assign(x => x.Set(y => y.Owner, "Steve")); + Assert.That(count, Is.EqualTo(4), "incorrect restricted update count"); + count = s.Query().Where(x => x.Owner == "Steve").Update().Assign(x => x.Set(y => y.Owner, (string)null)); + Assert.That(count, Is.EqualTo(4), "incorrect restricted update count"); + + count = s.CreateQuery("delete Vehicle where Owner is null").ExecuteUpdate(); + Assert.That(count, Is.EqualTo(4), "incorrect restricted update count"); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void UpdateSetNullOnDiscriminatorSubclass() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + int count = s.Query().Update().Assign(x => x.Set(y => y.Address.City, (string)null)); + + Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); + count = s.CreateQuery("delete Zoo where Address.City is null").ExecuteUpdate(); + Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); + + count = s.Query().Update().Assign(x => x.Set(y => y.Address.City, (string)null)); + Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); + count = s.CreateQuery("delete Zoo where Address.City is null").ExecuteUpdate(); + Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void UpdateSetNullOnJoinedSubclass() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + int count = s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, -1)); + Assert.That(count, Is.EqualTo(2), "Incorrect deletion count on joined subclass"); + + count = s.CreateQuery("delete Animal where BodyWeight = -1").ExecuteUpdate(); + Assert.That(count, Is.EqualTo(2), "Incorrect deletion count on joined subclass"); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + #endregion + + + #region DELETES + + [Test] + public void DeleteWithSubquery() + { + // setup the test data... + ISession s = OpenSession(); + s.BeginTransaction(); + var owner = new SimpleEntityWithAssociation {Name = "myEntity-1"}; + owner.AddAssociation("assoc-1"); + owner.AddAssociation("assoc-2"); + owner.AddAssociation("assoc-3"); + s.Save(owner); + var owner2 = new SimpleEntityWithAssociation {Name = "myEntity-2"}; + owner2.AddAssociation("assoc-1"); + owner2.AddAssociation("assoc-2"); + owner2.AddAssociation("assoc-3"); + owner2.AddAssociation("assoc-4"); + s.Save(owner2); + var owner3 = new SimpleEntityWithAssociation {Name = "myEntity-3"}; + s.Save(owner3); + s.Transaction.Commit(); + s.Close(); + + // now try the bulk delete + s = OpenSession(); + s.BeginTransaction(); + int count = s.Query().Where(x=>x.AssociatedEntities.Count==0 && x.Name.Contains("")).Delete(); + Assert.That(count, Is.EqualTo(1), "Incorrect delete count"); + s.Transaction.Commit(); + s.Close(); + + // finally, clean up + s = OpenSession(); + s.BeginTransaction(); + s.CreateQuery("delete SimpleAssociatedEntity").ExecuteUpdate(); + s.CreateQuery("delete SimpleEntityWithAssociation").ExecuteUpdate(); + s.Transaction.Commit(); + s.Close(); + } + + + + [Test] + public void SimpleDeleteOnAnimal() + { + if (Dialect.HasSelfReferentialForeignKeyBug) + { + Assert.Ignore("self referential FK bug", "HQL delete testing"); + return; + } + + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + int count = s.Query().Where(x=>x.Id==data.Polliwog.Id).Delete(); + Assert.That(count, Is.EqualTo(1), "Incorrect delete count"); + + count = s.Query().Where(x => x.Id == data.Catepillar.Id).Delete(); + Assert.That(count, Is.EqualTo(1), "Incorrect delete count"); + + // HHH-873... + if (Dialect.SupportsSubqueryOnMutatingTable) + { + count = s.Query().Where(x=>s.Query().Contains(x)).Delete(); + Assert.That(count, Is.EqualTo(0)); + } + + count = s.Query().Delete(); + Assert.That(count, Is.EqualTo(5), "Incorrect delete count"); + + IList list = s.Query().ToList(); + Assert.That(list, Is.Empty, "table not empty"); + + t.Commit(); + s.Close(); + data.Cleanup(); + } + + [Test] + public void DeleteOnDiscriminatorSubclass() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + int count = s.Query().Delete(); + Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); + + count = s.Query().Delete(); + Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void DeleteOnJoinedSubclass() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + int count = s.Query().Where(x=>x.BodyWeight>150).Delete(); + Assert.That(count, Is.EqualTo(1), "Incorrect deletion count on joined subclass"); + + count = s.Query().Delete(); + Assert.That(count, Is.EqualTo(1), "Incorrect deletion count on joined subclass"); + + count = s.Query().Delete(); + Assert.That(count, Is.EqualTo(0), "Incorrect deletion count on joined subclass"); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void DeleteOnMappedJoin() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + int count = s.Query().Where(x=>x.JoinedName == "joined-name").Delete(); + Assert.That(count, Is.EqualTo(1), "Incorrect deletion count on joined class"); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void DeleteUnionSubclassAbstractRoot() + { + var data = new TestData(this); + data.Prepare(); + + // These should reach out into *all* subclass tables... + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + int count = s.Query().Where(x => x.Owner == "Steve").Delete(); + Assert.That(count, Is.EqualTo(1), "incorrect restricted update count"); + + count = s.Query().Delete(); + Assert.That(count, Is.EqualTo(3), "incorrect update count"); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void DeleteUnionSubclassConcreteSubclass() + { + var data = new TestData(this); + data.Prepare(); + + // These should only affect the given table + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + int count = s.Query().Where(x=>x.Owner =="Steve").Delete(); + Assert.That(count, Is.EqualTo(1), "incorrect restricted update count"); + + count = s.Query().Delete(); + Assert.That(count, Is.EqualTo(2), "incorrect update count"); + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void DeleteUnionSubclassLeafSubclass() + { + var data = new TestData(this); + data.Prepare(); + + // These should only affect the given table + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + int count = s.Query().Where(x=>x.Owner == "Kirsten").Delete(); + Assert.That(count, Is.EqualTo(1), "incorrect restricted update count"); + + count = s.Query().Delete(); + Assert.That(count, Is.EqualTo(0), "incorrect update count"); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void DeleteRestrictedOnManyToOne() + { + var data = new TestData(this); + data.Prepare(); + + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + int count = s.Query().Where(x=>x.Mother == data.Butterfly).Delete(); + Assert.That(count, Is.EqualTo(1)); + + t.Commit(); + s.Close(); + + data.Cleanup(); + } + + [Test] + public void DeleteSyntaxWithCompositeId() + { + ISession s = OpenSession(); + ITransaction t = s.BeginTransaction(); + + s.Query().Where(x=>x.Id.Id == 1 && x.Id.OtherId == 2).Delete(); + + t.Commit(); + s.Close(); + } + + #endregion + + private class TestData + { + private readonly LinqBulkManipulationFixture tc; + public Animal Polliwog; + public Animal Catepillar; + public Animal Frog; + public Animal Butterfly; + + public Zoo Zoo; + public Zoo PettingZoo; + + public TestData(LinqBulkManipulationFixture tc) + { + this.tc = tc; + } + + public void Prepare() + { + ISession s = tc.OpenNewSession(); + ITransaction txn = s.BeginTransaction(); + + Polliwog = new Animal {BodyWeight = 12, Description = "Polliwog"}; + + Catepillar = new Animal {BodyWeight = 10, Description = "Catepillar"}; + + Frog = new Animal {BodyWeight = 34, Description = "Frog"}; + + Polliwog.Father = Frog; + Frog.AddOffspring(Polliwog); + + Butterfly = new Animal {BodyWeight = 9, Description = "Butterfly"}; + + Catepillar.Mother = Butterfly; + Butterfly.AddOffspring(Catepillar); + + s.Save(Frog); + s.Save(Polliwog); + s.Save(Butterfly); + s.Save(Catepillar); + + var dog = new Dog {BodyWeight = 200, Description = "dog"}; + s.Save(dog); + + var cat = new Cat {BodyWeight = 100, Description = "cat"}; + s.Save(cat); + + var dragon = new Dragon(); + dragon.SetFireTemperature(200); + s.Save(dragon); + + Zoo = new Zoo {Name = "Zoo"}; + var add = new Address {City = "MEL", Country = "AU", Street = "Main st", PostalCode = "3000"}; + Zoo.Address = add; + + PettingZoo = new PettingZoo {Name = "Petting Zoo"}; + var addr = new Address {City = "Sydney", Country = "AU", Street = "High st", PostalCode = "2000"}; + PettingZoo.Address = addr; + + s.Save(Zoo); + s.Save(PettingZoo); + + var joiner = new Joiner {JoinedName = "joined-name", Name = "name"}; + s.Save(joiner); + + var car = new Car {Vin = "123c", Owner = "Kirsten"}; + s.Save(car); + + var truck = new Truck {Vin = "123t", Owner = "Steve"}; + s.Save(truck); + + var suv = new SUV {Vin = "123s", Owner = "Joe"}; + s.Save(suv); + + var pickup = new Pickup {Vin = "123p", Owner = "Cecelia"}; + s.Save(pickup); + + txn.Commit(); + s.Close(); + } + + public void Cleanup() + { + ISession s = tc.OpenNewSession(); + ITransaction txn = s.BeginTransaction(); + + // workaround awesome HSQLDB "feature" + s.CreateQuery("delete from Animal where Mother is not null or Father is not null").ExecuteUpdate(); + s.CreateQuery("delete from Animal").ExecuteUpdate(); + s.CreateQuery("delete from Zoo").ExecuteUpdate(); + s.CreateQuery("delete from Joiner").ExecuteUpdate(); + s.CreateQuery("delete from Vehicle").ExecuteUpdate(); + + txn.Commit(); + s.Close(); + } + } + } +} \ No newline at end of file diff --git a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs index 568c6b3dae9..1cec6041a05 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs @@ -62,6 +62,7 @@ public partial class HqlSqlWalker private IASTFactory _nodeFactory; private readonly List assignmentSpecifications = new List(); private int numberOfParametersInSetClause; + private Stack clauseStack=new Stack(); public HqlSqlWalker(QueryTranslatorImpl qti, ISessionFactoryImplementor sfi, @@ -85,7 +86,7 @@ public override void ReportError(RecognitionException e) /* protected override void Mismatch(IIntStream input, int ttype, BitSet follow) { - throw new MismatchedTokenException(ttype, input); + throw new MismatchedTokenException(ttype, input); } public override object RecoverFromMismatchedSet(IIntStream input, RecognitionException e, BitSet follow) @@ -407,9 +408,19 @@ void AfterStatementCompletion(string statementName) void HandleClauseStart(int clauseType) { + clauseStack.Push(_currentClauseType); _currentClauseType = clauseType; } + void HandleClauseEnd(int clauseType) + { + if (clauseType != _currentClauseType) + { + throw new SemanticException("Mismatched clause parsing"); + } + _currentClauseType=clauseStack.Pop(); + } + IASTNode CreateIntoClause(string path, IASTNode propertySpec) { var persister = (IQueryable) SessionFactoryHelper.RequireClassPersister(path); @@ -956,8 +967,8 @@ public bool IsComparativeExpressionClause // Note: once we add support for "JOIN ... ON ...", // the ON clause needs to get included here return CurrentClauseType == WHERE || - CurrentClauseType == WITH || - IsInCase; + CurrentClauseType == WITH || + IsInCase; } } diff --git a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g index fc3eef075bc..404001981a5 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g +++ b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g @@ -92,6 +92,7 @@ intoClause! } : ^( INTO { HandleClauseStart( INTO ); } (p=path) ps=insertablePropertySpec ) ; + finally {HandleClauseEnd( INTO );} insertablePropertySpec : ^( RANGE (IDENT)+ ) @@ -100,6 +101,7 @@ insertablePropertySpec setClause : ^( SET { HandleClauseStart( SET ); } (assignment)* ) ; + finally {HandleClauseEnd( SET );} assignment @after { @@ -153,6 +155,7 @@ unionedQuery! orderClause : ^(ORDER { HandleClauseStart( ORDER ); } (orderExprs)) ; + finally {HandleClauseEnd( ORDER );} orderExprs : orderExpr ( ASCENDING | DESCENDING )? (orderExprs)? @@ -183,6 +186,7 @@ takeClause groupClause : ^(GROUP { HandleClauseStart( GROUP ); } (expr)+ ) ; + finally {HandleClauseEnd( GROUP );} havingClause : ^(HAVING logicalExpr) @@ -192,6 +196,7 @@ selectClause! : ^(SELECT { HandleClauseStart( SELECT ); BeforeSelectClause(); } (d=DISTINCT)? x=selectExprList ) -> ^(SELECT_CLAUSE["{select clause}"] $d? $x) ; + finally {HandleClauseEnd( SELECT );} selectExprList @init{ _inSelect = true; @@ -240,6 +245,7 @@ aggregateExpr fromClause : ^(f=FROM { PushFromClause($f.tree); HandleClauseStart( FROM ); } fromElementList ) ; + finally {HandleClauseEnd( FROM );} fromElementList @init{ bool oldInFrom = _inFrom; @@ -323,11 +329,13 @@ withClause : ^(w=WITH { HandleClauseStart( WITH ); } b=logicalExpr ) -> ^($w $b) ; + finally {HandleClauseEnd( WITH );} whereClause : ^(w=WHERE { HandleClauseStart( WHERE ); } b=logicalExpr ) -> ^($w $b) ; + finally {HandleClauseEnd( WHERE );} logicalExpr : ^(AND logicalExpr logicalExpr) diff --git a/src/NHibernate/Hql/Ast/HqlTreeBuilder.cs b/src/NHibernate/Hql/Ast/HqlTreeBuilder.cs index d384c6f75ed..ce6e72bbde1 100755 --- a/src/NHibernate/Hql/Ast/HqlTreeBuilder.cs +++ b/src/NHibernate/Hql/Ast/HqlTreeBuilder.cs @@ -34,6 +34,27 @@ public HqlTreeNode Query(HqlSelectFrom selectFrom, HqlWhere where, HqlOrderBy or return new HqlQuery(_factory, selectFrom, where, orderBy); } + public HqlDelete Delete(HqlFrom @from) + { + return new HqlDelete(_factory, @from); + } + + public HqlUpdate Update(HqlFrom @from,HqlSet set) + { + return new HqlUpdate(_factory, @from,set); + } + + public HqlUpdate Update(HqlVersioned versioned,HqlFrom @from, HqlSet set) + { + return new HqlUpdate(_factory, versioned, @from, set); + } + + public HqlInsert Insert(HqlInto into, HqlQuery query) + { + return new HqlInsert(_factory, into, query); + } + + public HqlSelectFrom SelectFrom() { return new HqlSelectFrom(_factory); @@ -64,9 +85,9 @@ public HqlFrom From() return new HqlFrom(_factory); } - public HqlRange Range(HqlIdent ident) + public HqlRange Range(params HqlIdent[] idents) { - return new HqlRange(_factory, ident); + return new HqlRange(_factory, idents); } public HqlRange Range(HqlTreeNode ident, HqlAlias alias) @@ -475,5 +496,25 @@ public HqlTreeNode Indices(HqlExpression dictionary) { return new HqlIndices(_factory, dictionary); } + + public HqlSet Set() + { + return new HqlSet(_factory); + } + + public HqlSet Set(HqlExpression expression) + { + return new HqlSet(_factory, expression); + } + + public HqlVersioned Versioned() + { + return new HqlVersioned(_factory); + } + + public HqlInto Into() + { + return new HqlInto(_factory); + } } } diff --git a/src/NHibernate/Hql/Ast/HqlTreeNode.cs b/src/NHibernate/Hql/Ast/HqlTreeNode.cs index a6db5c46330..0f9fe081ec8 100755 --- a/src/NHibernate/Hql/Ast/HqlTreeNode.cs +++ b/src/NHibernate/Hql/Ast/HqlTreeNode.cs @@ -282,6 +282,59 @@ internal HqlSelectFrom(IASTFactory factory, params HqlTreeNode[] children) } } + public class HqlDelete : HqlStatement + { + internal HqlDelete(IASTFactory factory, params HqlTreeNode[] children) + : base(HqlSqlWalker.DELETE, "delete", factory, children) + { + } + } + + public class HqlUpdate : HqlStatement + { + internal HqlUpdate(IASTFactory factory, params HqlTreeNode[] children) + : base(HqlSqlWalker.UPDATE, "update", factory, children) + { + } + } + + public class HqlVersioned : HqlExpression + { + public HqlVersioned(IASTFactory factory) + : base(HqlSqlWalker.VERSIONED, "versioned", factory) + { + } + } + + public class HqlInsert : HqlStatement + { + internal HqlInsert(IASTFactory factory, params HqlTreeNode[] children) + : base(HqlSqlWalker.INSERT, "insert", factory, children) + { + } + } + + public class HqlInto : HqlStatement + { + public HqlInto(IASTFactory factory, params HqlTreeNode[] children) + : base(HqlSqlWalker.INTO, "into", factory,children) + { + } + } + + public class HqlSet : HqlStatement + { + public HqlSet(IASTFactory factory) + : base(HqlSqlWalker.SET, "set", factory) + { + } + + public HqlSet(IASTFactory factory, HqlExpression expression) + : base(HqlSqlWalker.SET, "set", factory, expression) + { + } + } + public class HqlAlias : HqlExpression { public HqlAlias(IASTFactory factory, string @alias) diff --git a/src/NHibernate/Linq/Assignment.cs b/src/NHibernate/Linq/Assignment.cs new file mode 100644 index 00000000000..a905cebadc9 --- /dev/null +++ b/src/NHibernate/Linq/Assignment.cs @@ -0,0 +1,19 @@ +using System.Linq.Expressions; + +namespace NHibernate.Linq +{ + /// + /// Specifies one assignment in an update or insert query + /// + public class Assignment + { + public string PropertyPath { get; set; } + public Expression Expression { get; set; } + + public Assignment(string propertyPath,Expression expression) + { + PropertyPath = propertyPath; + Expression = expression; + } + } +} \ No newline at end of file diff --git a/src/NHibernate/Linq/Assignments.cs b/src/NHibernate/Linq/Assignments.cs new file mode 100644 index 00000000000..37769970753 --- /dev/null +++ b/src/NHibernate/Linq/Assignments.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using NHibernate.Linq.Visitors; +using Remotion.Linq.Utilities; + +namespace NHibernate.Linq +{ + public abstract class Assignments + { + protected static readonly ConstructorInfo DictionaryConstructorInfo = typeof(Dictionary).GetConstructor(new[] { typeof(int) }); + protected static readonly MethodInfo DictionaryAddMethodInfo = typeof(Dictionary).GetMethod("Add"); + } + + /// + /// Class to hold assigments used in updates and inserts + /// + /// The type of the input. + /// The type of the output. + public class Assignments : Assignments + { + private readonly List _sets = new List(); + + /// + /// Sets the specified property. + /// + /// The type of the property. + /// The property. + /// The expression that should be assigned to the property. + /// + public Assignments Set(Expression> property, Expression> expression) + { + if (expression == null) + throw new ArgumentNullException(nameof(expression)); + var member = GetMemberExpression(property); + _sets.Add(new Assignment(member.GetMemberPath(), expression)); + return this; + } + + /// + /// Sets the specified property. + /// + /// The type of the property. + /// The property. + /// The value. + /// + public Assignments Set(Expression> property, TProp value) + { + var member = GetMemberExpression(property); + _sets.Add(new Assignment(member.GetMemberPath(), Expression.Constant(value, typeof(TProp)))); + return this; + } + + private static MemberExpression GetMemberExpression(Expression> property) + { + if (property == null) + throw new ArgumentNullException(nameof(property)); + var param = property.Parameters.Single(); + var member = property.Body as MemberExpression ?? + throw new ArgumentException($"The property expression must refer to a property of {param.Name}({param.Type.Name})", nameof(property)); + return member; + } + + /// + /// Converts the assignments into a to lambda expression, which creates a Dictionary<string,object%gt;. + /// + /// + public LambdaExpression ConvertToDictionaryExpression() + { + var param = Expression.Parameter(typeof(TInput)); + var inits = new List(); + foreach (var set in _sets) + { + var setter = set.Expression; + var setterLambda = setter as LambdaExpression; + if (setterLambda != null) + { + setter = setterLambda.Body.Replace(setterLambda.Parameters.First(), param); + } + inits.Add(Expression.ElementInit(DictionaryAddMethodInfo, Expression.Constant(set.PropertyPath), + Expression.Convert( + setter, + typeof(object)))); + + } + + + //The ListInit is intentionally "infected" with the lambda parameter (param), in the form of an IIF. + //The only relevance is to make sure that the ListInit is not evaluated by the PartialEvaluatingExpressionTreeVisitor, + //which could turn it into a Constant + var listInit = Expression.ListInit( + Expression.New( + DictionaryConstructorInfo, + Expression.Condition( + Expression.Equal(param, Expression.Constant(null, typeof(TInput))), + Expression.Constant(_sets.Count), + Expression.Constant(_sets.Count))), + inits); + + + + return Expression.Lambda(listInit, param); + } + + public static Assignments FromExpression(Expression> expression) + { + if (expression == null) + throw new ArgumentNullException(nameof(expression)); + var instance = new Assignments(); + var memberInitExpression = expression.Body as MemberInitExpression; + + if (memberInitExpression == null) + { + throw new ArgumentException("The expression must be member initialization, e.g. x => new Dog{Name = x.Name,Age = x.Age + 5}"); + } + + AddSetsFromBindings(memberInitExpression.Bindings, instance, "", expression.Parameters); + + return instance; + } + + private static void AddSetsFromBindings(IEnumerable bindings, Assignments instance, string path, ReadOnlyCollection parameters) + { + foreach (var binding in bindings) + { + if (binding.BindingType == MemberBindingType.Assignment) // {Property="Value"} + { + AddSetsFromAssignment((MemberAssignment)binding, instance, path + "." + binding.Member.Name, parameters); + } + else if (binding.BindingType == MemberBindingType.MemberBinding) // {Property={SubProperty="Value}} + { + AddSetsFromBindings(((MemberMemberBinding) binding).Bindings, instance, path + "." + binding.Member.Name, parameters); + } + } + } + + private static void AddSetsFromAssignment(MemberAssignment assignment, Assignments instance, string path, ReadOnlyCollection parameters) + { + var memberInit = assignment.Expression as MemberInitExpression; // {Property=new Instance{SubProperty="Value"}} + if (memberInit!=null) + { + AddSetsFromBindings(memberInit.Bindings, instance, path, parameters); + } + else + { + instance._sets.Add(new Assignment(path.Substring(1), Expression.Lambda(assignment.Expression, parameters))); + } + } + } +} \ No newline at end of file diff --git a/src/NHibernate/Linq/DefaultQueryProvider.cs b/src/NHibernate/Linq/DefaultQueryProvider.cs index 26c09ffe96f..3e84e6b611d 100644 --- a/src/NHibernate/Linq/DefaultQueryProvider.cs +++ b/src/NHibernate/Linq/DefaultQueryProvider.cs @@ -16,6 +16,9 @@ public interface INhQueryProvider : IQueryProvider IEnumerable ExecuteFuture(Expression expression); IFutureValue ExecuteFutureValue(Expression expression); void SetResultTransformerAndAdditionalCriteria(IQuery query, NhLinqExpression nhExpression, IDictionary> parameters); + int ExecuteDelete(Expression predicate); + int ExecuteUpdate(Expression expression, Assignments assignments, bool versioned); + int ExecuteInsert(Expression expression, Assignments assignments); } public class DefaultQueryProvider : INhQueryProvider @@ -45,14 +48,14 @@ public virtual object Execute(Expression expression) public TResult Execute(Expression expression) { - return (TResult) Execute(expression); + return (TResult)Execute(expression); } public virtual IQueryable CreateQuery(Expression expression) { MethodInfo m = CreateQueryMethodDefinition.MakeGenericMethod(expression.Type.GetGenericArguments()[0]); - return (IQueryable) m.Invoke(this, new object[] {expression}); + return (IQueryable)m.Invoke(this, new object[] { expression }); } public virtual IQueryable CreateQuery(Expression expression) @@ -94,7 +97,7 @@ protected virtual NhLinqExpression PrepareQuery(Expression expression, out IQuer query = Session.CreateQuery(nhLinqExpression); - nhQuery = (NhLinqExpression) ((ExpressionQueryImpl) query).QueryExpression; + nhQuery = (NhLinqExpression)((ExpressionQueryImpl)query).QueryExpression; SetParameters(query, nhLinqExpression.ParameterValuesByName); SetResultTransformerAndAdditionalCriteria(query, nhQuery, nhLinqExpression.ParameterValuesByName); @@ -170,5 +173,38 @@ public virtual void SetResultTransformerAndAdditionalCriteria(IQuery query, NhLi criteria(query, parameters); } } + + public int ExecuteDelete(Expression predicate) + { + var nhLinqExpression = new NhLinqDeleteExpression(predicate, Session.Factory); + + var query = Session.CreateQuery(nhLinqExpression); + + SetParameters(query, nhLinqExpression.ParameterValuesByName); + + return query.ExecuteUpdate(); + } + + public int ExecuteUpdate(Expression expression, Assignments assignments, bool versioned) + { + var nhLinqExpression = new NhLinqUpdateExpression(expression, assignments, Session.Factory, versioned); + + var query = Session.CreateQuery(nhLinqExpression); + + SetParameters(query, nhLinqExpression.ParameterValuesByName); + + return query.ExecuteUpdate(); + } + + public int ExecuteInsert(Expression expression, Assignments assignments) + { + var nhLinqExpression = new NhLinqInsertExpression(expression, assignments, Session.Factory); + + var query = Session.CreateQuery(nhLinqExpression); + + SetParameters(query, nhLinqExpression.ParameterValuesByName); + + return query.ExecuteUpdate(); + } } } \ No newline at end of file diff --git a/src/NHibernate/Linq/InsertSyntax.cs b/src/NHibernate/Linq/InsertSyntax.cs new file mode 100644 index 00000000000..81bd034a5c4 --- /dev/null +++ b/src/NHibernate/Linq/InsertSyntax.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq.Expressions; + +namespace NHibernate.Linq +{ + public class InsertSyntax + { + private readonly Expression _sourceExpression; + private readonly INhQueryProvider _provider; + + internal InsertSyntax(Expression sourceExpression, INhQueryProvider provider) + { + _sourceExpression = sourceExpression; + _provider = provider; + } + + /// + /// Executes the insert, using the specified assignments. + /// + /// The type of the output. + /// The assignments. + /// + public int Into(Action> assignmentActions) + { + if (assignmentActions == null) + throw new ArgumentNullException(nameof(assignmentActions)); + var assignments = new Assignments(); + assignmentActions.Invoke(assignments); + return InsertInto(assignments); + } + + /// + /// Executes the insert, inserting new entities as specified by the expression + /// + /// The type of the output. + /// The expression. + /// + public int As(Expression> expression) + { + var assignments = Assignments.FromExpression(expression); + return InsertInto(assignments); + } + + private int InsertInto(Assignments assignments) + { + return _provider.ExecuteInsert(_sourceExpression, assignments); + } + } +} \ No newline at end of file diff --git a/src/NHibernate/Linq/IntermediateHqlTree.cs b/src/NHibernate/Linq/IntermediateHqlTree.cs index 9b72eefeeb8..5f0baaec0d0 100644 --- a/src/NHibernate/Linq/IntermediateHqlTree.cs +++ b/src/NHibernate/Linq/IntermediateHqlTree.cs @@ -29,6 +29,7 @@ public class IntermediateHqlTree private HqlHaving _hqlHaving; private HqlTreeNode _root; private HqlOrderBy _orderBy; + private HqlInsert _insertRoot; public bool IsRoot { @@ -42,6 +43,7 @@ public HqlTreeNode Root { get { + //Strange side effects in a property getter... ExecuteAddHavingClause(_hqlHaving); ExecuteAddOrderBy(_orderBy); ExecuteAddSkipClause(_skipCount); @@ -58,16 +60,37 @@ public HqlTreeNode Root public HqlTreeBuilder TreeBuilder { get; } - public IntermediateHqlTree(bool root) + public IntermediateHqlTree(bool root,QueryMode mode) { _isRoot = root; TreeBuilder = new HqlTreeBuilder(); - _root = TreeBuilder.Query(TreeBuilder.SelectFrom(TreeBuilder.From())); + if (mode==QueryMode.Delete) + { + _root = TreeBuilder.Delete(TreeBuilder.From()); + } + else if (mode==QueryMode.Update) + { + _root = TreeBuilder.Update(TreeBuilder.From(), TreeBuilder.Set()); + } + else if (mode == QueryMode.UpdateVersioned) + { + _root = TreeBuilder.Update(TreeBuilder.Versioned(),TreeBuilder.From(), TreeBuilder.Set()); + } + else if (mode == QueryMode.Insert) + { + _root = TreeBuilder.Query(TreeBuilder.SelectFrom(TreeBuilder.From())); + _insertRoot = TreeBuilder.Insert(TreeBuilder.Into(), _root as HqlQuery); + } + else + { + _root = TreeBuilder.Query(TreeBuilder.SelectFrom(TreeBuilder.From())); + } } public ExpressionToHqlTranslationResults GetTranslation() { - return new ExpressionToHqlTranslationResults(Root, + var translationRoot = _insertRoot ?? Root; + return new ExpressionToHqlTranslationResults(translationRoot, _itemTransformers, _listTransformers, _postExecuteTransformers, @@ -102,9 +125,16 @@ public void AddSelectClause(HqlTreeNode select) _root.NodesPreOrder.OfType().First().AddChild(select); } + public void AddInsertClause(HqlIdent target,HqlRange columnSpec) + { + var into = _insertRoot.NodesPreOrder.OfType().Single(); + into.AddChild(target); + into.AddChild(columnSpec); + } + public void AddGroupByClause(HqlGroupBy groupBy) { - this._root.AddChild(groupBy); + _root.AddChild(groupBy); } public void AddOrderByClause(HqlExpression orderBy, HqlDirectionStatement direction) @@ -211,6 +241,20 @@ public void AddHavingClause(HqlBooleanExpression where) } } + public void AddSet(HqlEquality equality) + { + var currentSet = _root.NodesPreOrder.OfType().FirstOrDefault(); + if (currentSet == null) + { + currentSet = TreeBuilder.Set(equality); + _root.AddChild(currentSet); + } + else + { + currentSet.AddChild(equality); + } + } + public void AddAdditionalCriteria(Action>> criteria) { _additionalCriteria.Add(criteria); diff --git a/src/NHibernate/Linq/LinqExtensionMethods.cs b/src/NHibernate/Linq/LinqExtensionMethods.cs index 9410db58311..ec3d112fcc2 100644 --- a/src/NHibernate/Linq/LinqExtensionMethods.cs +++ b/src/NHibernate/Linq/LinqExtensionMethods.cs @@ -47,14 +47,7 @@ public static IQueryable Query(this IStatelessSession session, string enti /// is not a . public static IEnumerable ToFuture(this IQueryable source) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - if (!(source.Provider is INhQueryProvider provider)) - { - throw new NotSupportedException($"Source {nameof(source.Provider)} must be a {nameof(INhQueryProvider)}"); - } + var provider = GetNhProvider(source); return provider.ExecuteFuture(source.Expression); } @@ -69,14 +62,7 @@ public static IEnumerable ToFuture(this IQueryable so /// is not a . public static IFutureValue ToFutureValue(this IQueryable source) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - if (!(source.Provider is INhQueryProvider provider)) - { - throw new NotSupportedException($"Source {nameof(source.Provider)} must be a {nameof(INhQueryProvider)}"); - } + var provider = GetNhProvider(source); var future = provider.ExecuteFuture(source.Expression); return new FutureValue(() => future); } @@ -94,14 +80,7 @@ public static IFutureValue ToFutureValue(this IQueryable is not a . public static IFutureValue ToFutureValue(this IQueryable source, Expression, TResult>> selector) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - if (!(source.Provider is INhQueryProvider provider)) - { - throw new NotSupportedException($"Source {nameof(source.Provider)} must be a {nameof(INhQueryProvider)}"); - } + var provider = GetNhProvider(source); var expression = ReplacingExpressionVisitor .Replace(selector.Parameters.Single(), source.Expression, selector.Body); @@ -143,9 +122,58 @@ public static IQueryable CacheRegion(this IQueryable query, string regi public static IQueryable Timeout(this IQueryable query, int timeout) => query.SetOptions(o => o.SetTimeout(timeout)); + /// + /// Deletes all entities in the specified query. The delete operation is performed in the database. + /// + /// The type of the elements of . + /// The query matching the entities to delete. + /// The number of deleted entities. + public static int Delete(this IQueryable source) + { + var provider = GetNhProvider(source); + return provider.ExecuteDelete(source.Expression); + } + + /// + /// Updates the entities in the query, using the specified assignments. The update operation is performed in the database. + /// + /// The type of the elements of . + /// The query matching the entities to update. + /// An update builder, allowing to specify assignments to the entities properties. + public static UpdateSyntax Update(this IQueryable source) + { + var provider = GetNhProvider(source); + return new UpdateSyntax(source.Expression, provider); + } + + /// + /// Inserts new entities into the database, using other stored entities as a source. Uses INSERT INTO [...] SELECT FROM [...] in the database. + /// + /// The type of the input. + /// The query matching entities source of the data to insert. + /// An insert builder, allowing to specify target entity class and assignments to its properties. + public static InsertSyntax Insert(this IQueryable source) + { + var provider = GetNhProvider(source); + return new InsertSyntax(source.Expression, provider); + } + public static T MappedAs(this T parameter, IType type) { throw new InvalidOperationException("The method should be used inside Linq to indicate a type of a parameter"); } + + private static INhQueryProvider GetNhProvider(IQueryable source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + if (!(source.Provider is INhQueryProvider provider)) + { + throw new NotSupportedException($"Source {nameof(source.Provider)} must be a {nameof(INhQueryProvider)}"); + } + return provider; + } } } diff --git a/src/NHibernate/Linq/NhLinqDeleteExpression.cs b/src/NHibernate/Linq/NhLinqDeleteExpression.cs new file mode 100644 index 00000000000..bec060696ba --- /dev/null +++ b/src/NHibernate/Linq/NhLinqDeleteExpression.cs @@ -0,0 +1,22 @@ +using System.Linq.Expressions; +using NHibernate.Engine; +using NHibernate.Linq.Visitors; +using Remotion.Linq; + +namespace NHibernate.Linq +{ + public class NhLinqDeleteExpression : NhLinqExpression + { + public NhLinqDeleteExpression(Expression expression, ISessionFactoryImplementor sessionFactory) + : base(expression, sessionFactory) + { + Key = Key + "DELETE"; + } + + protected override ExpressionToHqlTranslationResults GenerateHqlQuery(QueryModel queryModel, VisitorParameters visitorParameters) + { + visitorParameters.EntityType = Type; + return QueryModelVisitor.GenerateHqlQuery(queryModel, visitorParameters, true, null, QueryMode.Delete); + } + } +} \ No newline at end of file diff --git a/src/NHibernate/Linq/NhLinqExpression.cs b/src/NHibernate/Linq/NhLinqExpression.cs index c4c06f8197a..a661f4ba577 100644 --- a/src/NHibernate/Linq/NhLinqExpression.cs +++ b/src/NHibernate/Linq/NhLinqExpression.cs @@ -8,12 +8,13 @@ using NHibernate.Linq.Visitors; using NHibernate.Param; using NHibernate.Type; +using Remotion.Linq; namespace NHibernate.Linq { public class NhLinqExpression : IQueryExpression { - public string Key { get; } + public string Key { get; protected set; } public System.Type Type { get; private set; } @@ -64,7 +65,7 @@ public IASTNode Translate(ISessionFactoryImplementor sessionFactory, bool filter var queryModel = NhRelinqQueryParser.Parse(_expression); var visitorParameters = new VisitorParameters(sessionFactory, _constantToParameterMap, requiredHqlParameters, querySourceNamer); - ExpressionToHqlTranslationResults = QueryModelVisitor.GenerateHqlQuery(queryModel, visitorParameters, true, ReturnType); + ExpressionToHqlTranslationResults = GenerateHqlQuery(queryModel, visitorParameters); if (ExpressionToHqlTranslationResults.ExecuteResultTypeOverride != null) Type = ExpressionToHqlTranslationResults.ExecuteResultTypeOverride; @@ -74,6 +75,12 @@ public IASTNode Translate(ISessionFactoryImplementor sessionFactory, bool filter return ExpressionToHqlTranslationResults.Statement.AstNode; } + protected virtual ExpressionToHqlTranslationResults GenerateHqlQuery(QueryModel queryModel, VisitorParameters visitorParameters) + { + visitorParameters.EntityType = Type; + return QueryModelVisitor.GenerateHqlQuery(queryModel, visitorParameters, true, ReturnType, QueryMode.Select); + } + internal void CopyExpressionTranslation(NhLinqExpression other) { ExpressionToHqlTranslationResults = other.ExpressionToHqlTranslationResults; diff --git a/src/NHibernate/Linq/NhLinqInsertExpression.cs b/src/NHibernate/Linq/NhLinqInsertExpression.cs new file mode 100644 index 00000000000..555ff6ce20e --- /dev/null +++ b/src/NHibernate/Linq/NhLinqInsertExpression.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using NHibernate.Engine; +using NHibernate.Linq.Expressions; +using NHibernate.Linq.Visitors; +using Remotion.Linq; +using Remotion.Linq.Clauses.Expressions; +using System.Reflection; + +namespace NHibernate.Linq +{ + public class NhLinqInsertExpression : NhLinqExpression + { + public NhLinqInsertExpression(Expression expression, Assignments assignments, ISessionFactoryImplementor sessionFactory) + : base(RewriteForInsert(expression, assignments), sessionFactory) + { + Key = Key + "INSERT"; + } + + internal static Expression RewriteForInsert(Expression expression, Assignments assignments) + { + var lambda = assignments.ConvertToDictionaryExpression(); + + return + Expression.Call( + typeof(Queryable), "Select", + new System.Type[] { typeof(TInput), lambda.Body.Type }, + expression, Expression.Quote(lambda)); + } + + protected override ExpressionToHqlTranslationResults GenerateHqlQuery(QueryModel queryModel, VisitorParameters visitorParameters) + { + visitorParameters.EntityType = typeof (TOutput); + return QueryModelVisitor.GenerateHqlQuery(queryModel, visitorParameters, true, null, QueryMode.Insert); + } + } +} \ No newline at end of file diff --git a/src/NHibernate/Linq/NhLinqUpdateExpression.cs b/src/NHibernate/Linq/NhLinqUpdateExpression.cs new file mode 100644 index 00000000000..f19623b46e0 --- /dev/null +++ b/src/NHibernate/Linq/NhLinqUpdateExpression.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using NHibernate.Engine; +using NHibernate.Linq.Expressions; +using NHibernate.Linq.Visitors; +using Remotion.Linq; +using Remotion.Linq.Clauses.Expressions; + +namespace NHibernate.Linq +{ + public class NhLinqUpdateExpression : NhLinqExpression + { + private readonly bool _versioned; + + public NhLinqUpdateExpression(Expression expression, Assignments assignments, ISessionFactoryImplementor sessionFactory, bool versioned) + : base(RewriteForUpdate(expression, assignments), sessionFactory) + { + _versioned = versioned; + Key = Key + "UPDATE" + versioned; + } + + protected override ExpressionToHqlTranslationResults GenerateHqlQuery(QueryModel queryModel, VisitorParameters visitorParameters) + { + visitorParameters.EntityType = typeof(T); + return QueryModelVisitor.GenerateHqlQuery(queryModel, visitorParameters, true, null, _versioned ? QueryMode.UpdateVersioned : QueryMode.Update); + } + + internal static Expression RewriteForUpdate(Expression expression, Assignments assignments) + { + var lambda = assignments.ConvertToDictionaryExpression(); + + return + Expression.Call( + typeof(Queryable), "Select", + new System.Type[] { typeof(T), lambda.Body.Type }, + expression, Expression.Quote(lambda)); + } + } + + +} \ No newline at end of file diff --git a/src/NHibernate/Linq/QueryMode.cs b/src/NHibernate/Linq/QueryMode.cs new file mode 100644 index 00000000000..15ed5842f81 --- /dev/null +++ b/src/NHibernate/Linq/QueryMode.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NHibernate.Linq +{ + public enum QueryMode + { + Select, + Delete, + Update, + UpdateVersioned, + Insert + } +} diff --git a/src/NHibernate/Linq/UpdateSyntax.cs b/src/NHibernate/Linq/UpdateSyntax.cs new file mode 100644 index 00000000000..53b2841d85c --- /dev/null +++ b/src/NHibernate/Linq/UpdateSyntax.cs @@ -0,0 +1,51 @@ +using System; +using System.Linq.Expressions; + +namespace NHibernate.Linq +{ + public class UpdateSyntax + { + private readonly Expression _sourceExpression; + private readonly INhQueryProvider _provider; + + internal UpdateSyntax(Expression sourceExpression, INhQueryProvider provider) + { + _sourceExpression = sourceExpression; + _provider = provider; + } + + /// + /// Specify the assignments and execute the update. + /// + /// The assignments. + /// if set to true [versioned]. + /// + public int Assign(Action> assignments, bool versioned = false) + { + var u = new Assignments(); + assignments.Invoke(u); + + return ExecuteUpdate(versioned, u); + } + + /// + /// Specify the assignments and execute the update. + /// + /// + /// The query. + /// The assignments expressed as a member initialization, e.g. x => new Dog{Name = x.Name,Age = x.Age + 5}. + /// if set to true [versioned]. + /// + public int As(Expression> expression, bool versioned = false) + { + + var assignments = Assignments.FromExpression(expression); + return ExecuteUpdate(versioned, assignments); + } + + private int ExecuteUpdate(bool versioned, Assignments assignments) + { + return _provider.ExecuteUpdate(_sourceExpression, assignments, versioned); + } + } +} \ No newline at end of file diff --git a/src/NHibernate/Linq/Visitors/HqlGeneratorExpressionVisitor.cs b/src/NHibernate/Linq/Visitors/HqlGeneratorExpressionVisitor.cs index cb4ec1cca66..2894e744315 100644 --- a/src/NHibernate/Linq/Visitors/HqlGeneratorExpressionVisitor.cs +++ b/src/NHibernate/Linq/Visitors/HqlGeneratorExpressionVisitor.cs @@ -545,7 +545,7 @@ protected HqlTreeNode VisitConditionalExpression(ConditionalExpression expressio protected HqlTreeNode VisitSubQueryExpression(SubQueryExpression expression) { - ExpressionToHqlTranslationResults query = QueryModelVisitor.GenerateHqlQuery(expression.QueryModel, _parameters, false, null); + ExpressionToHqlTranslationResults query = QueryModelVisitor.GenerateHqlQuery(expression.QueryModel, _parameters, false, null, QueryMode.Select); return query.Statement; } diff --git a/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs b/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs index 4527a9ee95d..c785ff1470f 100644 --- a/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs +++ b/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using System.Reflection; using NHibernate.Hql.Ast; @@ -22,8 +23,10 @@ namespace NHibernate.Linq.Visitors { public class QueryModelVisitor : NhQueryModelVisitorBase, INhQueryModelVisitor { + private readonly QueryMode _queryMode; + public static ExpressionToHqlTranslationResults GenerateHqlQuery(QueryModel queryModel, VisitorParameters parameters, bool root, - NhLinqExpressionReturnType? rootReturnType) + NhLinqExpressionReturnType? rootReturnType, QueryMode queryMode) { NestedSelectRewriter.ReWrite(queryModel, parameters.SessionFactory); @@ -82,7 +85,7 @@ public static ExpressionToHqlTranslationResults GenerateHqlQuery(QueryModel quer // Identify and name query sources QuerySourceIdentifier.Visit(parameters.QuerySourceNamer, queryModel); - var visitor = new QueryModelVisitor(parameters, root, queryModel, rootReturnType) + var visitor = new QueryModelVisitor(parameters, root, queryModel, rootReturnType, queryMode) { RewrittenOperatorResult = result, }; @@ -132,12 +135,13 @@ static QueryModelVisitor() } private QueryModelVisitor(VisitorParameters visitorParameters, bool root, QueryModel queryModel, - NhLinqExpressionReturnType? rootReturnType) + NhLinqExpressionReturnType? rootReturnType, QueryMode queryMode) { + _queryMode = queryMode; VisitorParameters = visitorParameters; Model = queryModel; _rootReturnType = root ? rootReturnType : null; - _hqlTree = new IntermediateHqlTree(root); + _hqlTree = new IntermediateHqlTree(root, queryMode); } private void Visit() @@ -372,6 +376,25 @@ public override void VisitSelectClause(SelectClause selectClause, QueryModel que { CurrentEvaluationType = selectClause.GetOutputDataInfo(); + switch (_queryMode) + { + case QueryMode.Delete: + return; + case QueryMode.Update: + case QueryMode.UpdateVersioned: + { + VisitUpdateClause(selectClause.Selector); + return; + } + case QueryMode.Insert: + { + VisitInsertClause(selectClause.Selector); + return; + } + } + + //This is a standard select query + var visitor = new SelectClauseVisitor(typeof(object[]), VisitorParameters); visitor.VisitSelector(selectClause.Selector); @@ -386,6 +409,54 @@ public override void VisitSelectClause(SelectClause selectClause, QueryModel que base.VisitSelectClause(selectClause, queryModel); } + private void VisitInsertClause(Expression expression) + { + var listInit = expression as ListInitExpression; + var insertedType = VisitorParameters.EntityType; + var idents = new List(); + var selectColumns = new List(); + + if (listInit == null) + { + throw new QueryException("Malformed insert expression"); + } + + //Extract the insert clause from the projected ListInit + foreach (var assignment in listInit.Initializers) + { + var member = assignment.Arguments[0] as ConstantExpression; + var value = assignment.Arguments[1]; + + //The target property + idents.Add(_hqlTree.TreeBuilder.Ident((string)member.Value)); + + var valueHql = HqlGeneratorExpressionVisitor.Visit(value, VisitorParameters).AsExpression(); + selectColumns.Add(valueHql); + }; + + //Add the insert clause ([INSERT INTO] insertedType (list of properties)) + _hqlTree.AddInsertClause(_hqlTree.TreeBuilder.Ident(insertedType.FullName), + _hqlTree.TreeBuilder.Range(idents.ToArray())); + + + //... and then the select clause + _hqlTree.AddSelectClause(_hqlTree.TreeBuilder.Select(selectColumns)); + } + + private void VisitUpdateClause(Expression expression) + { + var listInit = expression as ListInitExpression; + foreach (var initializer in listInit.Initializers) + { + var member = initializer.Arguments[0] as ConstantExpression; + var setter = initializer.Arguments[1]; + var setterHql = HqlGeneratorExpressionVisitor.Visit(setter, VisitorParameters).AsExpression(); + + _hqlTree.AddSet(_hqlTree.TreeBuilder.Equality(_hqlTree.TreeBuilder.Ident((string)member.Value), + setterHql)); + } + } + public override void VisitWhereClause(WhereClause whereClause, QueryModel queryModel, int index) { var visitor = new SimplifyConditionalVisitor(); diff --git a/src/NHibernate/Linq/Visitors/VisitorParameters.cs b/src/NHibernate/Linq/Visitors/VisitorParameters.cs index 27ef5de7029..30578db83db 100644 --- a/src/NHibernate/Linq/Visitors/VisitorParameters.cs +++ b/src/NHibernate/Linq/Visitors/VisitorParameters.cs @@ -16,6 +16,8 @@ public class VisitorParameters public QuerySourceNamer QuerySourceNamer { get; set; } + public System.Type EntityType { get; set; } + public VisitorParameters( ISessionFactoryImplementor sessionFactory, IDictionary constantToParameterMap, diff --git a/src/NHibernate/Linq/Visitors/VisitorUtil.cs b/src/NHibernate/Linq/Visitors/VisitorUtil.cs index b9d531f6d95..6175dcf374b 100644 --- a/src/NHibernate/Linq/Visitors/VisitorUtil.cs +++ b/src/NHibernate/Linq/Visitors/VisitorUtil.cs @@ -5,6 +5,7 @@ using System.Reflection; using NHibernate.Util; using Remotion.Linq.Clauses.Expressions; +using Remotion.Linq.Parsing.ExpressionVisitors; namespace NHibernate.Linq.Visitors { @@ -103,5 +104,34 @@ public static bool IsBooleanConstant(Expression expression, out bool value) value = false; // Dummy value. return false; } + + /// + /// Replaces a specific expression in an expression tree with a replacement expression. + /// + /// The expression to search. + /// The expression to search for. + /// The expression to replace with. + /// + public static Expression Replace(this Expression expression, Expression oldExpression, Expression newExpression) + { + return ReplacingExpressionVisitor.Replace(oldExpression, newExpression, expression); + } + + /// + /// Gets the member path. + /// + /// The member expression. + /// + public static string GetMemberPath(this MemberExpression memberExpression) + { + var path = memberExpression.Member.Name; + var parentProp = memberExpression.Expression as MemberExpression; + while (parentProp != null) + { + path = parentProp.Member.Name + "." + path; + parentProp = parentProp.Expression as MemberExpression; + } + return path; + } } } From 39f5e6f645d2accc36cea35c6c98ab4174eb0296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= Date: Fri, 31 Mar 2017 15:54:48 +0200 Subject: [PATCH 2/7] NH-3488 - Documentation, modernizing tests, minor refactoring * C#7 * White-spaces * Use of new ReflectionCache * ... --- doc/reference/modules/batch.xml | 3 +- doc/reference/modules/query_linq.xml | 95 ++ .../Domain/Address.cs | 2 +- .../Domain/Animal.cs | 2 +- .../Domain/Animal.hbm.xml | 2 +- .../Domain/Classification.cs | 2 +- .../Domain/CrazyCompositeKey.cs | 2 +- .../Domain/DomesticAnimal.cs | 2 +- .../Domain/EntityWithCrazyCompositeKey.cs | 2 +- .../EntityWithCrazyCompositeKey.hbm.xml | 26 + .../Domain/Human.cs | 2 +- .../Domain/IntegerVersioned.cs | 2 +- .../Domain/Joiner.cs | 2 +- .../Domain/Mammal.cs | 2 +- .../Domain/Multi.hbm.xml | 0 .../Domain/Name.cs | 2 +- .../Domain/Reptile.cs | 2 +- .../Domain/SimpleAssociatedEntity.cs | 2 +- .../Domain/SimpleClass.cs | 2 +- .../Domain/SimpleClass.hbm.xml | 2 +- .../Domain/SimpleClassWithComponent.cs | 2 +- .../Domain/SimpleClassWithComponent.hbm.xml | 2 +- .../Domain/SimpleEntityWithAssociation.cs | 2 +- .../SimpleEntityWithAssociation.hbm.xml | 2 +- .../Domain/StateProvince.cs | 2 +- .../Domain/TimestampVersioned.cs | 2 +- .../Domain/User.cs | 2 +- .../Domain/Vehicle.hbm.xml | 2 +- .../Domain/Vehicles.cs | 2 +- .../Domain/Versions.hbm.xml | 2 +- .../Domain/Zoo.cs | 2 +- .../LinqBulkManipulation/Fixture.cs | 1107 ++++++++++++++ .../NHSpecificTest/NH3488/BaseFixture.cs | 46 - .../EntityWithCrazyCompositeKey.hbm.xml | 25 - .../NH3488/LinqBulkManipulationFixture.cs | 1282 ----------------- src/NHibernate/Hql/Ast/HqlTreeBuilder.cs | 7 +- src/NHibernate/Linq/Assignment.cs | 8 +- src/NHibernate/Linq/Assignments.cs | 74 +- src/NHibernate/Linq/DefaultQueryProvider.cs | 4 +- src/NHibernate/Linq/InsertSyntax.cs | 28 +- src/NHibernate/Linq/IntermediateHqlTree.cs | 28 +- src/NHibernate/Linq/LinqExtensionMethods.cs | 12 +- src/NHibernate/Linq/NhLinqDeleteExpression.cs | 12 +- src/NHibernate/Linq/NhLinqExpression.cs | 24 +- src/NHibernate/Linq/NhLinqInsertExpression.cs | 38 +- src/NHibernate/Linq/NhLinqUpdateExpression.cs | 37 +- src/NHibernate/Linq/QueryMode.cs | 7 +- src/NHibernate/Linq/UpdateSyntax.cs | 19 +- .../Visitors/HqlGeneratorExpressionVisitor.cs | 2 +- .../Linq/Visitors/QueryModelVisitor.cs | 50 +- .../Linq/Visitors/VisitorParameters.cs | 13 +- src/NHibernate/Util/ReflectionCache.cs | 7 + 52 files changed, 1436 insertions(+), 1572 deletions(-) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/Address.cs (92%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/Animal.cs (95%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/Animal.hbm.xml (98%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/Classification.cs (52%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/CrazyCompositeKey.cs (93%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/DomesticAnimal.cs (81%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/EntityWithCrazyCompositeKey.cs (92%) create mode 100644 src/NHibernate.Test/LinqBulkManipulation/Domain/EntityWithCrazyCompositeKey.hbm.xml rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/Human.cs (96%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/IntegerVersioned.cs (88%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/Joiner.cs (87%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/Mammal.cs (84%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/Multi.hbm.xml (100%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/Name.cs (86%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/Reptile.cs (90%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/SimpleAssociatedEntity.cs (95%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/SimpleClass.cs (76%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/SimpleClass.hbm.xml (85%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/SimpleClassWithComponent.cs (80%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/SimpleClassWithComponent.hbm.xml (90%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/SimpleEntityWithAssociation.cs (96%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/SimpleEntityWithAssociation.hbm.xml (95%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/StateProvince.cs (86%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/TimestampVersioned.cs (88%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/User.cs (90%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/Vehicle.hbm.xml (94%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/Vehicles.cs (89%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/Versions.hbm.xml (91%) rename src/NHibernate.Test/{NHSpecificTest/NH3488 => LinqBulkManipulation}/Domain/Zoo.cs (94%) create mode 100644 src/NHibernate.Test/LinqBulkManipulation/Fixture.cs delete mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/BaseFixture.cs delete mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/Domain/EntityWithCrazyCompositeKey.hbm.xml delete mode 100644 src/NHibernate.Test/NHSpecificTest/NH3488/LinqBulkManipulationFixture.cs diff --git a/doc/reference/modules/batch.xml b/doc/reference/modules/batch.xml index 4fe189babc3..d12935744a3 100644 --- a/doc/reference/modules/batch.xml +++ b/doc/reference/modules/batch.xml @@ -133,7 +133,8 @@ session.Close();]]> (DML) statements: INSERT, UPDATE, DELETE) data directly in the database will not affect in-memory state. However, NHibernate provides methods for bulk SQL-style DML statement execution which are performed through the - Hibernate Query Language (HQL). + Hibernate Query Language (HQL). A + Linq implementation is available too. diff --git a/doc/reference/modules/query_linq.xml b/doc/reference/modules/query_linq.xml index 1947540fff8..a8d4016facc 100644 --- a/doc/reference/modules/query_linq.xml +++ b/doc/reference/modules/query_linq.xml @@ -467,6 +467,101 @@ IList oldCats = .ToList();]]> + + Modifying entities inside the database + + + Beginning with NHibernate 5.0, Linq queries can be used for inserting, updating or deleting entities. + The query defines the data to delete, update or insert, and then Delete, + Update and Insert queryable extension methods allow to delete it, + or instruct in which way it should updated or inserted. Those queries happen entirely inside the + database, without extracting corresponding entities out of the database. + + + These operations are a Linq implementation of , with the same abilities + and limitations. + + + + Inserting new entities + + Insert method extension expects a NHibernate queryable defining the data source of + the insert. This data can be entities or a projection. Then it allows specifying the target entity type + to insert, and how to convert source data to those target entities. Two forms of target specification + exist. + + + Using projection to target entity: + + () + .Where(c => c.BodyWeight > 20) + .Insert() + .As(c => new Dog { Name = c.Name + "dog", BodyWeight = c.BodyWeight });]]> + + Or using assignments: + + () + .Where(c => c.BodyWeight > 20) + .Insert() + .Into(a => a + .Set(d => d.Name, c => c.Name + "dog") + .Set(d => d.BodyWeight, c => c.BodyWeight));]]> + + In both cases, unspecified properties are not included in the resulting SQL insert. + version and + timestamp properties are + exceptions. If not specified, they are inserted with their seed value. + + + For more information on Insert limitations, please refer to + . + + + + + Updating entities + + Update method extension expects a queryable defining the entities to update. + Then it allows specifying which properties should be updated with which values. As for + Insert, two forms of target specification exist. + + + Using projection to updated entity: + + () + .Where(c => c.BodyWeight > 20) + .Update() + .As(c => new Cat { BodyWeight = c.BodyWeight / 2 });]]> + + Or using assignments: + + () + .Where(c => c.BodyWeight > 20) + .Update() + .Assign(a => a + .Set(c => c.BodyWeight, c => c.BodyWeight / 2));]]> + + In both cases, unspecified properties are not included in the resulting SQL update. This could + be changed for version and + timestamp properties: + As and Assign methods take an optional boolean parameter, + versioned, which allows incrementing the version. Custom version types + (NHibernate.Usertype.IUserVersionType) are not supported. + + + + + Deleting entities + + Delete method extension expects a queryable defining the entities to delete. + It immediately deletes them. + + () + .Where(c => c.BodyWeight > 20) + .Delete();]]> + + + Query cache diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Address.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Address.cs similarity index 92% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Address.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/Address.cs index 46d222abdd8..dfbb87f6b16 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Address.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Address.cs @@ -1,4 +1,4 @@ -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class Address { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Animal.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Animal.cs similarity index 95% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Animal.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/Animal.cs index ab4016e660b..b19e460916b 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Animal.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Animal.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class Animal { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Animal.hbm.xml b/src/NHibernate.Test/LinqBulkManipulation/Domain/Animal.hbm.xml similarity index 98% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Animal.hbm.xml rename to src/NHibernate.Test/LinqBulkManipulation/Domain/Animal.hbm.xml index ab163ae27ba..b0b88de798c 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Animal.hbm.xml +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Animal.hbm.xml @@ -1,7 +1,7 @@ diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Classification.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Classification.cs similarity index 52% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Classification.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/Classification.cs index 5fbfe740ce1..9751b45dd05 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Classification.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Classification.cs @@ -1,4 +1,4 @@ -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public enum Classification { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/CrazyCompositeKey.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/CrazyCompositeKey.cs similarity index 93% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/CrazyCompositeKey.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/CrazyCompositeKey.cs index 177dcbe843d..6558078cdc4 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/CrazyCompositeKey.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/CrazyCompositeKey.cs @@ -1,4 +1,4 @@ -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class CrazyCompositeKey { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/DomesticAnimal.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/DomesticAnimal.cs similarity index 81% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/DomesticAnimal.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/DomesticAnimal.cs index 51c2a10f8d7..48d9b122b02 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/DomesticAnimal.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/DomesticAnimal.cs @@ -1,4 +1,4 @@ -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class DomesticAnimal: Mammal { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/EntityWithCrazyCompositeKey.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/EntityWithCrazyCompositeKey.cs similarity index 92% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/EntityWithCrazyCompositeKey.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/EntityWithCrazyCompositeKey.cs index da3367daa3e..5761a8b6c06 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/EntityWithCrazyCompositeKey.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/EntityWithCrazyCompositeKey.cs @@ -1,4 +1,4 @@ -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class EntityWithCrazyCompositeKey { diff --git a/src/NHibernate.Test/LinqBulkManipulation/Domain/EntityWithCrazyCompositeKey.hbm.xml b/src/NHibernate.Test/LinqBulkManipulation/Domain/EntityWithCrazyCompositeKey.hbm.xml new file mode 100644 index 00000000000..716ad292402 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/EntityWithCrazyCompositeKey.hbm.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Human.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Human.cs similarity index 96% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Human.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/Human.cs index 7a8cb9abea9..92501e7b581 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Human.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Human.cs @@ -1,7 +1,7 @@ using System.Collections; using System.Collections.Generic; -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class Human: Mammal { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/IntegerVersioned.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/IntegerVersioned.cs similarity index 88% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/IntegerVersioned.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/IntegerVersioned.cs index 268ba1ca302..7e09ddfd978 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/IntegerVersioned.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/IntegerVersioned.cs @@ -1,4 +1,4 @@ -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class IntegerVersioned { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Joiner.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Joiner.cs similarity index 87% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Joiner.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/Joiner.cs index b8c5f04582c..a13d3f2b051 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Joiner.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Joiner.cs @@ -1,4 +1,4 @@ -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class Joiner { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Mammal.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Mammal.cs similarity index 84% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Mammal.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/Mammal.cs index 982b106b31f..3daf5057ac3 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Mammal.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Mammal.cs @@ -1,6 +1,6 @@ using System; -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class Mammal: Animal { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Multi.hbm.xml b/src/NHibernate.Test/LinqBulkManipulation/Domain/Multi.hbm.xml similarity index 100% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Multi.hbm.xml rename to src/NHibernate.Test/LinqBulkManipulation/Domain/Multi.hbm.xml diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Name.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Name.cs similarity index 86% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Name.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/Name.cs index 073c3094ec9..52406bec0cb 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Name.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Name.cs @@ -1,4 +1,4 @@ -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class Name { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Reptile.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Reptile.cs similarity index 90% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Reptile.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/Reptile.cs index 17f8ed4c772..abe34f0980e 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Reptile.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Reptile.cs @@ -1,4 +1,4 @@ -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class Reptile: Animal { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleAssociatedEntity.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleAssociatedEntity.cs similarity index 95% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleAssociatedEntity.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleAssociatedEntity.cs index 33daccd19e5..210dedd10ef 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleAssociatedEntity.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleAssociatedEntity.cs @@ -1,4 +1,4 @@ -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class SimpleAssociatedEntity { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClass.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClass.cs similarity index 76% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClass.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClass.cs index 42d6cbc4945..b2b88291171 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClass.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClass.cs @@ -1,4 +1,4 @@ -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class SimpleClass { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClass.hbm.xml b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClass.hbm.xml similarity index 85% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClass.hbm.xml rename to src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClass.hbm.xml index a799eb53791..0335a1ce786 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClass.hbm.xml +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClass.hbm.xml @@ -1,7 +1,7 @@  + namespace="NHibernate.Test.LinqBulkManipulation.Domain"> diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClassWithComponent.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClassWithComponent.cs similarity index 80% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClassWithComponent.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClassWithComponent.cs index 4d49daef802..8f4b12bbc6d 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClassWithComponent.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClassWithComponent.cs @@ -1,4 +1,4 @@ -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class SimpleClassWithComponent { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClassWithComponent.hbm.xml b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClassWithComponent.hbm.xml similarity index 90% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClassWithComponent.hbm.xml rename to src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClassWithComponent.hbm.xml index f53c5f1906f..6d1aea93a16 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleClassWithComponent.hbm.xml +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClassWithComponent.hbm.xml @@ -1,7 +1,7 @@  + namespace="NHibernate.Test.LinqBulkManipulation.Domain"> diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleEntityWithAssociation.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleEntityWithAssociation.cs similarity index 96% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleEntityWithAssociation.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleEntityWithAssociation.cs index cb04723a90d..e7898f81cba 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleEntityWithAssociation.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleEntityWithAssociation.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class SimpleEntityWithAssociation { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleEntityWithAssociation.hbm.xml b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleEntityWithAssociation.hbm.xml similarity index 95% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleEntityWithAssociation.hbm.xml rename to src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleEntityWithAssociation.hbm.xml index f57611a092a..ff40c101ce4 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/SimpleEntityWithAssociation.hbm.xml +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleEntityWithAssociation.hbm.xml @@ -1,7 +1,7 @@ + namespace="NHibernate.Test.LinqBulkManipulation.Domain"> diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/StateProvince.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/StateProvince.cs similarity index 86% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/StateProvince.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/StateProvince.cs index 502c2aebd95..f4953f9ef5d 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/StateProvince.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/StateProvince.cs @@ -1,4 +1,4 @@ -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class StateProvince { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/TimestampVersioned.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/TimestampVersioned.cs similarity index 88% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/TimestampVersioned.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/TimestampVersioned.cs index 2ef0a6ae315..b599637fe4d 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/TimestampVersioned.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/TimestampVersioned.cs @@ -1,6 +1,6 @@ using System; -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class TimestampVersioned { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/User.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/User.cs similarity index 90% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/User.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/User.cs index 135d4c4273c..7e4f0c99704 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/User.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/User.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class User { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Vehicle.hbm.xml b/src/NHibernate.Test/LinqBulkManipulation/Domain/Vehicle.hbm.xml similarity index 94% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Vehicle.hbm.xml rename to src/NHibernate.Test/LinqBulkManipulation/Domain/Vehicle.hbm.xml index ac86e3e22be..4978345625a 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Vehicle.hbm.xml +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Vehicle.hbm.xml @@ -1,7 +1,7 @@ + namespace="NHibernate.Test.LinqBulkManipulation.Domain"> diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Vehicles.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Vehicles.cs similarity index 89% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Vehicles.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/Vehicles.cs index 8d37daa8499..6c43d24bb63 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Vehicles.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Vehicles.cs @@ -1,4 +1,4 @@ -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class Vehicle { diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Versions.hbm.xml b/src/NHibernate.Test/LinqBulkManipulation/Domain/Versions.hbm.xml similarity index 91% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Versions.hbm.xml rename to src/NHibernate.Test/LinqBulkManipulation/Domain/Versions.hbm.xml index 45c30ba6bde..59f3295c339 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Versions.hbm.xml +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Versions.hbm.xml @@ -1,7 +1,7 @@ diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Zoo.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Zoo.cs similarity index 94% rename from src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Zoo.cs rename to src/NHibernate.Test/LinqBulkManipulation/Domain/Zoo.cs index ed87ac6be1f..be9c239c281 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/Zoo.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Zoo.cs @@ -1,7 +1,7 @@ using System.Collections; using System.Collections.Generic; -namespace NHibernate.Test.NHSpecificTest.NH3488.Domain +namespace NHibernate.Test.LinqBulkManipulation.Domain { public class Zoo { diff --git a/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs b/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs new file mode 100644 index 00000000000..63ca88c16d8 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs @@ -0,0 +1,1107 @@ +using System; +using System.Collections; +using System.Linq; +using System.Threading; +using NHibernate.Dialect; +using NHibernate.DomainModel; +using NHibernate.Hql.Ast.ANTLR; +using NHibernate.Linq; +using NHibernate.Test.LinqBulkManipulation.Domain; +using NUnit.Framework; + +namespace NHibernate.Test.LinqBulkManipulation +{ + [TestFixture] + public class Fixture : TestCase + { + protected override IList Mappings => new string[0]; + + protected override void Configure(Cfg.Configuration configuration) + { + var type = typeof(Fixture); + var assembly = type.Assembly; + var mappingNamespace = type.Namespace; + foreach (var resource in assembly.GetManifestResourceNames()) + { + if (resource.StartsWith(mappingNamespace) && resource.EndsWith(".hbm.xml")) + { + configuration.AddResource(resource, assembly); + } + } + } + + private Animal _polliwog; + private Animal _catepillar; + private Animal _frog; + private Animal _butterfly; + private Zoo _zoo; + private Zoo _pettingZoo; + private Human _joe; + private Human _doll; + private Human _stevee; + private IntegerVersioned _intVersioned; + private TimestampVersioned _timeVersioned; + + protected override void OnSetUp() + { + using (var s = OpenSession()) + using (var txn = s.BeginTransaction()) + { + _polliwog = new Animal { BodyWeight = 12, Description = "Polliwog" }; + _catepillar = new Animal { BodyWeight = 10, Description = "Catepillar" }; + _frog = new Animal { BodyWeight = 34, Description = "Frog" }; + _butterfly = new Animal { BodyWeight = 9, Description = "Butterfly" }; + + _polliwog.Father = _frog; + _frog.AddOffspring(_polliwog); + _catepillar.Mother = _butterfly; + _butterfly.AddOffspring(_catepillar); + + s.Save(_frog); + s.Save(_polliwog); + s.Save(_butterfly); + s.Save(_catepillar); + + var dog = new Dog { BodyWeight = 200, Description = "dog" }; + s.Save(dog); + var cat = new Cat { BodyWeight = 100, Description = "cat" }; + s.Save(cat); + + var dragon = new Dragon(); + dragon.SetFireTemperature(200); + s.Save(dragon); + + _zoo = new Zoo { Name = "Zoo" }; + var add = new Address { City = "MEL", Country = "AU", Street = "Main st", PostalCode = "3000" }; + _zoo.Address = add; + + _pettingZoo = new PettingZoo { Name = "Petting Zoo" }; + var addr = new Address { City = "Sydney", Country = "AU", Street = "High st", PostalCode = "2000" }; + _pettingZoo.Address = addr; + + s.Save(_zoo); + s.Save(_pettingZoo); + + var joiner = new Joiner { JoinedName = "joined-name", Name = "name" }; + s.Save(joiner); + + var car = new Car { Vin = "123c", Owner = "Kirsten" }; + s.Save(car); + var truck = new Truck { Vin = "123t", Owner = "Steve" }; + s.Save(truck); + var suv = new SUV { Vin = "123s", Owner = "Joe" }; + s.Save(suv); + var pickup = new Pickup { Vin = "123p", Owner = "Cecelia" }; + s.Save(pickup); + + var entCompKey = new EntityWithCrazyCompositeKey { Id = new CrazyCompositeKey { Id = 1, OtherId = 1 }, Name = "Parent" }; + s.Save(entCompKey); + + _joe = new Human { Name = new Name { First = "Joe", Initial = 'Q', Last = "Public" } }; + _doll = new Human { Name = new Name { First = "Kyu", Initial = 'P', Last = "Doll" }, Friends = new[] { _joe } }; + _stevee = new Human { Name = new Name { First = "Stevee", Initial = 'X', Last = "Ebersole" } }; + s.Save(_joe); + s.Save(_doll); + s.Save(_stevee); + + _intVersioned = new IntegerVersioned { Name = "int-vers", Data = "foo" }; + s.Save(_intVersioned); + + _timeVersioned = new TimestampVersioned { Name = "ts-vers", Data = "foo" }; + s.Save(_timeVersioned); + + var scwc = new SimpleClassWithComponent { Name = new Name { First = "Stevee", Initial = 'X', Last = "Ebersole" } }; + s.Save(scwc); + + var mainEntWithAssoc = new SimpleEntityWithAssociation() { Name = "main" }; + var otherEntWithAssoc = new SimpleEntityWithAssociation() { Name = "many-to-many-association" }; + mainEntWithAssoc.ManyToManyAssociatedEntities.Add(otherEntWithAssoc); + mainEntWithAssoc.AddAssociation("one-to-many-association"); + s.Save(mainEntWithAssoc); + + var owner = new SimpleEntityWithAssociation { Name = "myEntity-1" }; + owner.AddAssociation("assoc-1"); + owner.AddAssociation("assoc-2"); + owner.AddAssociation("assoc-3"); + s.Save(owner); + var owner2 = new SimpleEntityWithAssociation { Name = "myEntity-2" }; + owner2.AddAssociation("assoc-1"); + owner2.AddAssociation("assoc-2"); + owner2.AddAssociation("assoc-3"); + owner2.AddAssociation("assoc-4"); + s.Save(owner2); + var owner3 = new SimpleEntityWithAssociation { Name = "myEntity-3" }; + s.Save(owner3); + + txn.Commit(); + } + } + + protected override void OnTearDown() + { + if (!Dialect.SupportsTemporaryTables) + { + // Give-up usual cleanup due to TPC: cannot perform multi-table deletes using dialect not supporting temp tables + DropSchema(); + CreateSchema(); + return; + } + + using (var s = OpenSession()) + using (var txn = s.BeginTransaction()) + { + // workaround FK + var doll = s.Query().SingleOrDefault(h => h.Id == _doll.Id); + if (doll != null) + s.Delete(doll); + var entity = s.Query().SingleOrDefault(e => e.ManyToManyAssociatedEntities.Any()); + if (entity != null) + { + s.Delete(entity.ManyToManyAssociatedEntities.First()); + s.Delete(entity); + } + s.Flush(); + s.CreateQuery("delete from Animal where Mother is not null or Father is not null").ExecuteUpdate(); + + s.CreateQuery("delete from Animal").ExecuteUpdate(); + s.CreateQuery("delete from Zoo").ExecuteUpdate(); + s.CreateQuery("delete from Joiner").ExecuteUpdate(); + s.CreateQuery("delete from Vehicle").ExecuteUpdate(); + s.CreateQuery("delete EntityReferencingEntityWithCrazyCompositeKey").ExecuteUpdate(); + s.CreateQuery("delete EntityWithCrazyCompositeKey").ExecuteUpdate(); + s.CreateQuery("delete IntegerVersioned").ExecuteUpdate(); + s.CreateQuery("delete TimestampVersioned").ExecuteUpdate(); + s.CreateQuery("delete SimpleClassWithComponent").ExecuteUpdate(); + s.CreateQuery("delete SimpleAssociatedEntity").ExecuteUpdate(); + s.CreateQuery("delete SimpleEntityWithAssociation").ExecuteUpdate(); + + txn.Commit(); + } + } + + #region INSERTS + + [Test] + public void SimpleInsert() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query().Insert().As(x => new Pickup { Id = x.Id, Vin = x.Vin, Owner = x.Owner }); + Assert.AreEqual(1, count); + + t.Commit(); + } + } + + [Test] + public void SimpleInsertFromAggregate() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query() + .GroupBy(x => x.Id) + .Select(x => new { Id = x.Key, Vin = x.Max(y => y.Vin), Owner = x.Max(y => y.Owner) }) + .Insert().As(x => new Pickup { Id = x.Id, Vin = x.Vin, Owner = x.Owner }); + Assert.AreEqual(1, count); + + t.Commit(); + } + } + + [Test] + public void SimpleInsertFromLimited() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query() + .Skip(1) + .Take(1) + .Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner }); + Assert.AreEqual(1, count); + + t.Commit(); + } + } + + [Test] + public void SimpleInsertWithConstants() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query() + .Insert().Into(x => x.Set(y => y.Id, y => y.Id).Set(y => y.Vin, y => y.Vin).Set(y => y.Owner, "The owner")); + Assert.AreEqual(1, count); + + t.Commit(); + } + } + + [Test] + public void SimpleInsertFromProjection() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query() + .Select(x => new { x.Id, x.Owner, UpperOwner = x.Owner.ToUpper() }) + .Insert().Into(x => x.Set(y => y.Id, y => y.Id).Set(y => y.Vin, y => y.UpperOwner)); + Assert.AreEqual(1, count); + + t.Commit(); + } + } + + [Test] + public void InsertWithClientSideRequirementsThrowsException() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + Assert.Throws(() => + s.Query() + .Insert().As(x => new Pickup { Id = x.Id, Vin = x.Vin, Owner = x.Owner.PadRight(200) })); + + t.Commit(); + } + } + + [Test] + public void InsertWithManyToOne() + { + CheckSupportOfBulkInsertionWithGeneratedId(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query() + .Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = x.Mother }); + Assert.AreEqual(3, count); + + t.Commit(); + } + } + + [Test] + public void InsertWithManyToOneAsParameter() + { + CheckSupportOfBulkInsertionWithGeneratedId(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query() + .Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = _butterfly }); + Assert.AreEqual(3, count); + + t.Commit(); + } + } + + [Test] + public void InsertWithManyToOneWithCompositeKey() + { + CheckSupportOfBulkInsertionWithGeneratedId(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query() + .Insert().As(x => new EntityReferencingEntityWithCrazyCompositeKey { Name = "Child", Parent = x }); + Assert.AreEqual(1, count); + + t.Commit(); + } + } + + [Test] + public void InsertIntoSuperclassPropertiesFails() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + Assert.Throws( + () => s.Query().Insert().As(x => new Human { Id = x.Id, BodyWeight = x.BodyWeight }), + "superclass prop insertion did not error"); + + t.Commit(); + } + } + + [Test] + public void InsertAcrossMappedJoinFails() + { + CheckSupportOfBulkInsertionWithGeneratedId(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + Assert.Throws( + () => s.Query().Insert().As(x => new Joiner { Name = x.Vin, JoinedName = x.Owner }), + "mapped-join insertion did not error"); + + t.Commit(); + } + } + + [Test] + public void InsertWithGeneratedId() + { + CheckSupportOfBulkInsertionWithGeneratedId(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query().Where(z => z.Id == _zoo.Id).Insert().As(x => new PettingZoo { Name = x.Name }); + Assert.That(count, Is.EqualTo(1), "unexpected insertion count"); + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var pz = s.Query().Single(z => z.Name == _zoo.Name); + t.Commit(); + + Assert.That(_zoo.Id != pz.Id); + } + } + + [Test] + public void InsertWithGeneratedVersionAndId() + { + CheckSupportOfBulkInsertionWithGeneratedId(); + + var initialId = _intVersioned.Id; + var initialVersion = _intVersioned.Version; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = + s.Query() + .Where(x => x.Id == initialId) + .Insert().As(x => new IntegerVersioned { Name = x.Name, Data = x.Data }); + Assert.That(count, Is.EqualTo(1), "unexpected insertion count"); + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var created = s.Query().Single(iv => iv.Id != initialId); + Assert.That(created.Version, Is.EqualTo(initialVersion), "version was not seeded"); + t.Commit(); + } + } + + [Test] + public void InsertWithGeneratedTimestampVersion() + { + CheckSupportOfBulkInsertionWithGeneratedId(); + + var initialId = _timeVersioned.Id; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = + s.Query() + .Where(x => x.Id == initialId) + .Insert().As(x => new TimestampVersioned { Name = x.Name, Data = x.Data }); + Assert.That(count, Is.EqualTo(1), "unexpected insertion count"); + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var created = s.Query().Single(tv => tv.Id != initialId); + Assert.That(created.Version, Is.GreaterThan(DateTime.Today)); + t.Commit(); + } + } + + [Test] + public void InsertWithSelectListUsingJoins() + { + CheckSupportOfBulkInsertionWithGeneratedId(); + + // this is just checking parsing and syntax... + using (var s = OpenSession()) + { + s.BeginTransaction(); + + Assert.DoesNotThrow(() => + { + s.Query().Where(x => x.Mother.Mother != null) + .Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight }); + }); + + s.Transaction.Commit(); + } + } + + [Test] + public void InsertToComponent() + { + CheckSupportOfBulkInsertionWithGeneratedId(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + const string correctName = "Steve"; + + var count = s + .Query() + // Avoid Firebird unstable cursor bug by filtering. + // https://firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-dml-insert.html#fblangref25-dml-insert-select-unstable + .Where(sc => sc.Name.First != correctName) + .Insert().Into(x => x.Set(y => y.Name.First, y => correctName)); + Assert.That(count, Is.EqualTo(1), "incorrect insert count"); + + count = + s.Query() + .Where(x => x.Name.First == correctName && x.Name.Initial != 'Z') + .Insert().As(x => new SimpleClassWithComponent { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'Z' } }); + Assert.That(count, Is.EqualTo(1), "incorrect insert from corrected count"); + t.Commit(); + } + } + + private void CheckSupportOfBulkInsertionWithGeneratedId() + { + // Make sure the env supports bulk inserts with generated ids... + var persister = Sfi.GetEntityPersister(typeof(T).FullName); + var generator = persister.IdentifierGenerator; + if (!HqlSqlWalker.SupportsIdGenWithBulkInsertion(generator)) + { + Assert.Ignore($"Identifier generator {generator.GetType().Name} for entity {typeof(T).FullName} does not support bulk insertions."); + } + } + + #endregion + + #region UPDATES + + [Test] + public void UpdateWithWhereExistsSubquery() + { + if (!Dialect.SupportsTemporaryTables) + { + Assert.Ignore("Cannot perform multi-table updates using dialect not supporting temp tables."); + } + + // multi-table ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query() + .Where(x => x.Friends.OfType().Any(f => f.Name.Last == "Public")) + .Update().Assign(x => x.Set(y => y.Description, "updated")); + Assert.That(count, Is.EqualTo(1)); + t.Commit(); + } + + // single-table (one-to-many & many-to-many) ~~~~~~~~~~~~~~~~~~~~~~~~~~ + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + // one-to-many test + var count = s.Query() + .Where(x => x.AssociatedEntities.Any(a => a.Name == "one-to-many-association")) + .Update().Assign(x => x.Set(y => y.Name, "updated")); + Assert.That(count, Is.EqualTo(1)); + // many-to-many test + if (Dialect.SupportsSubqueryOnMutatingTable) + { + count = s.Query() + .Where(x => x.ManyToManyAssociatedEntities.Any(a => a.Name == "many-to-many-association")) + .Update().Assign(x => x.Set(y => y.Name, "updated")); + + Assert.That(count, Is.EqualTo(1)); + } + t.Commit(); + } + } + + [Test] + public void IncrementCounterVersion() + { + var initialVersion = _intVersioned.Version; + + using (var s = OpenSession()) + { + using (var t = s.BeginTransaction()) + { + // Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26 + var count = + s.Query() + .Update().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd"), true); + Assert.That(count, Is.EqualTo(1), "incorrect exec count"); + t.Commit(); + } + + using (var t = s.BeginTransaction()) + { + var entity = s.Get(_intVersioned.Id); + Assert.That(entity.Version, Is.EqualTo(initialVersion + 1), "version not incremented"); + Assert.That(entity.Name, Is.EqualTo("int-versupd")); + Assert.That(entity.Data, Is.EqualTo("fooupd")); + t.Commit(); + } + } + } + + [Test] + public void IncrementTimestampVersion() + { + var initialVersion = _timeVersioned.Version; + + Thread.Sleep(1300); + + using (var s = OpenSession()) + { + using (var t = s.BeginTransaction()) + { + // Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26 + var count = s.Query() + .Update().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd"), true); + Assert.That(count, Is.EqualTo(1), "incorrect exec count"); + t.Commit(); + } + + using (var t = s.BeginTransaction()) + { + var entity = s.Load(_timeVersioned.Id); + Assert.That(entity.Version, Is.GreaterThan(initialVersion), "version not incremented"); + Assert.That(entity.Name, Is.EqualTo("ts-versupd")); + Assert.That(entity.Data, Is.EqualTo("fooupd")); + t.Commit(); + } + } + } + + [Test] + public void UpdateOnComponent() + { + if (!Dialect.SupportsTemporaryTables) + { + Assert.Ignore("Cannot perform multi-table updates using dialect not supporting temp tables."); + } + + using (var s = OpenSession()) + { + const string correctName = "Steve"; + + using (var t = s.BeginTransaction()) + { + var count = + s.Query().Where(x => x.Id == _stevee.Id).Update().As(x => new Human { Name = { First = correctName } }); + + Assert.That(count, Is.EqualTo(1), "incorrect update count"); + t.Commit(); + } + + using (var t = s.BeginTransaction()) + { + s.Refresh(_stevee); + + Assert.That(_stevee.Name.First, Is.EqualTo(correctName), "Update did not execute properly"); + + t.Commit(); + } + } + } + + [Test] + public void UpdateWithClientSideRequirementsThrowsException() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + Assert.Throws(() => + s.Query().Where(x => x.Id == _stevee.Id).Update().As(x => new Human { Name = { First = x.Name.First.PadLeft(200) } }) + ); + + t.Commit(); + } + } + + [Test] + public void UpdateOnManyToOne() + { + if (!Dialect.SupportsTemporaryTables) + { + Assert.Ignore("Cannot perform multi-table updates using dialect not supporting temp tables."); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + Assert.DoesNotThrow(() => { s.Query().Where(x => x.Id == 2).Update().Assign(x => x.Set(y => y.Mother, y => null)); }); + + if (Dialect.SupportsSubqueryOnMutatingTable) + { + Assert.DoesNotThrow( + () => { s.Query().Where(x => x.Id == 2).Update().Assign(x => x.Set(y => y.Mother, y => s.Query().First(z => z.Id == 1))); }); + } + + t.Commit(); + } + } + + [Test] + public void UpdateOnDiscriminatorSubclass() + { + using (var s = OpenSession()) + { + using (var t = s.BeginTransaction()) + { + var count = s.Query().Update().Assign(x => x.Set(y => y.Name, y => y.Name)); + Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass update count"); + + t.Rollback(); + } + using (var t = s.BeginTransaction()) + { + var count = s.Query().Where(x => x.Id == _pettingZoo.Id).Update().Assign(x => x.Set(y => y.Name, y => y.Name)); + Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass update count"); + + t.Rollback(); + } + using (var t = s.BeginTransaction()) + { + + var count = s.Query().Update().Assign(x => x.Set(y => y.Name, y => y.Name)); + Assert.That(count, Is.EqualTo(2), "Incorrect discrim subclass update count"); + + t.Rollback(); + } + using (var t = s.BeginTransaction()) + { + // TODO : not so sure this should be allowed. Seems to me that if they specify an alias, + // property-refs should be required to be qualified. + var count = s.Query().Where(x => x.Id == _zoo.Id).Update().Assign(x => x.Set(y => y.Name, y => y.Name)); + Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass update count"); + + t.Commit(); + } + } + } + + [Test] + public void UpdateOnAnimal() + { + if (!Dialect.SupportsTemporaryTables) + { + Assert.Ignore("Cannot perform multi-table updates using dialect not supporting temp tables."); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + //var count = s.Query().Where(x => x.Description == data.Frog.Description).Update(x => x.Set(y => y.Description, y => y.Description)); + //Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); + + var count = + s.Query().Where(x => x.Description == _polliwog.Description).Update().Assign(x => x.Set(y => y.Description, y => "Tadpole")); + Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); + + var tadpole = s.Load(_polliwog.Id); + + Assert.That(tadpole.Description, Is.EqualTo("Tadpole"), "Update did not take effect"); + + count = + s.Query().Update().Assign(x => x.Set(y => y.FireTemperature, 300)); + Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); + + + count = + s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, y => y.BodyWeight + 1 + 1)); + Assert.That(count, Is.EqualTo(10), "incorrect count on 'complex' update assignment"); + + if (Dialect.SupportsSubqueryOnMutatingTable) + { + Assert.DoesNotThrow(() => { s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, y => s.Query().Max(z => z.BodyWeight))); }); + } + + t.Commit(); + } + } + + [Test] + public void UpdateOnDragonWithProtectedProperty() + { + if (!Dialect.SupportsTemporaryTables) + { + Assert.Ignore("Cannot perform multi-table updates using dialect not supporting temp tables."); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = + s.Query().Update().Assign(x => x.Set(y => y.FireTemperature, 300)); + Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); + + t.Commit(); + } + } + + [Test] + public void UpdateMultiplePropertyOnAnimal() + { + if (!Dialect.SupportsTemporaryTables) + { + Assert.Ignore("Cannot perform multi-table updates using dialect not supporting temp tables."); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = + s.Query() + .Where(x => x.Description == _polliwog.Description) + .Update().Assign(x => x.Set(y => y.Description, y => "Tadpole").Set(y => y.BodyWeight, 3)); + + Assert.That(count, Is.EqualTo(1)); + t.Commit(); + } + + using (var s = OpenSession()) + using (s.BeginTransaction()) + { + var tadpole = s.Get(_polliwog.Id); + Assert.That(tadpole.Description, Is.EqualTo("Tadpole")); + Assert.That(tadpole.BodyWeight, Is.EqualTo(3)); + } + } + + [Test] + public void UpdateOnMammal() + { + if (!Dialect.SupportsTemporaryTables) + { + Assert.Ignore("Cannot perform multi-table updates using dialect not supporting temp tables."); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query().Update().Assign(x => x.Set(y => y.Description, y => y.Description)); + + Assert.That(count, Is.EqualTo(5), "incorrect update count against 'middle' of joined-subclass hierarchy"); + + count = s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, 25)); + Assert.That(count, Is.EqualTo(5), "incorrect update count against 'middle' of joined-subclass hierarchy"); + + if (Dialect.SupportsSubqueryOnMutatingTable) + { + count = s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, y => s.Query().Max(z => z.BodyWeight))); + Assert.That(count, Is.EqualTo(5), "incorrect update count against 'middle' of joined-subclass hierarchy"); + } + + t.Commit(); + } + } + + [Test] + public void UpdateSetNullUnionSubclass() + { + if (!Dialect.SupportsTemporaryTables) + { + Assert.Ignore("Cannot perform multi-table updates using dialect not supporting temp tables."); + } + + // These should reach out into *all* subclass tables... + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query().Update().Assign(x => x.Set(y => y.Owner, "Steve")); + Assert.That(count, Is.EqualTo(4), "incorrect restricted update count"); + count = s.Query().Where(x => x.Owner == "Steve").Update().Assign(x => x.Set(y => y.Owner, default(string))); + Assert.That(count, Is.EqualTo(4), "incorrect restricted update count"); + + count = s.CreateQuery("delete Vehicle where Owner is null").ExecuteUpdate(); + Assert.That(count, Is.EqualTo(4), "incorrect restricted update count"); + + t.Commit(); + } + } + + [Test] + public void UpdateSetNullOnDiscriminatorSubclass() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query().Update().Assign(x => x.Set(y => y.Address.City, default(string))); + + Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); + count = s.CreateQuery("delete Zoo where Address.City is null").ExecuteUpdate(); + Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); + + count = s.Query().Update().Assign(x => x.Set(y => y.Address.City, default(string))); + Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); + count = s.CreateQuery("delete Zoo where Address.City is null").ExecuteUpdate(); + Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); + + t.Commit(); + } + } + + [Test] + public void UpdateSetNullOnJoinedSubclass() + { + if (!Dialect.SupportsTemporaryTables) + { + Assert.Ignore("Cannot perform multi-table updates using dialect not supporting temp tables."); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, -1)); + Assert.That(count, Is.EqualTo(5), "Incorrect update count on joined subclass"); + + count = s.Query().Count(m => m.BodyWeight > -1.0001 && m.BodyWeight < -0.9999); + Assert.That(count, Is.EqualTo(5), "Incorrect body weight count"); + + t.Commit(); + } + } + + #endregion + + #region DELETES + + [Test] + public void DeleteWithSubquery() + { + if (Dialect is MsSqlCeDialect) + { + Assert.Ignore("Test failing on Ms SQL CE."); + } + + using (var s = OpenSession()) + { + s.BeginTransaction(); + var count = s.Query().Where(x => x.AssociatedEntities.Count == 0 && x.Name.Contains("myEntity")).Delete(); + Assert.That(count, Is.EqualTo(1), "Incorrect delete count"); + s.Transaction.Commit(); + } + } + + [Test] + public void SimpleDeleteOnAnimal() + { + if (Dialect.HasSelfReferentialForeignKeyBug) + { + Assert.Ignore("Self referential FK bug"); + } + if (!Dialect.SupportsTemporaryTables) + { + Assert.Ignore("Cannot perform multi-table deletes using dialect not supporting temp tables."); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + // Get rid of FK which may fail the test + _doll.Friends = new Human[0]; + s.Update(_doll); + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + + var count = s.Query().Where(x => x.Id == _polliwog.Id).Delete(); + Assert.That(count, Is.EqualTo(1), "Incorrect delete count"); + + count = s.Query().Where(x => x.Id == _catepillar.Id).Delete(); + Assert.That(count, Is.EqualTo(1), "Incorrect delete count"); + + if (Dialect.SupportsSubqueryOnMutatingTable) + { + count = s.Query().Where(x => s.Query().Contains(x)).Delete(); + Assert.That(count, Is.EqualTo(0)); + } + + count = s.Query().Delete(); + Assert.That(count, Is.EqualTo(8), "Incorrect delete count"); + + IList list = s.Query().ToList(); + Assert.That(list, Is.Empty, "table not empty"); + + t.Commit(); + } + } + + [Test] + public void DeleteOnDiscriminatorSubclass() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query().Delete(); + Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); + + count = s.Query().Delete(); + Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); + + t.Commit(); + } + } + + [Test] + public void DeleteOnJoinedSubclass() + { + if (!Dialect.SupportsTemporaryTables) + { + Assert.Ignore("Cannot perform multi-table deletes using dialect not supporting temp tables."); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + // Get rid of FK which may fail the test + _doll.Friends = new Human[0]; + s.Update(_doll); + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query().Where(x => x.BodyWeight > 150).Delete(); + Assert.That(count, Is.EqualTo(1), "Incorrect deletion count on joined subclass"); + + count = s.Query().Delete(); + Assert.That(count, Is.EqualTo(4), "Incorrect deletion count on joined subclass"); + + count = s.Query().Delete(); + Assert.That(count, Is.EqualTo(0), "Incorrect deletion count on joined subclass"); + + t.Commit(); + } + } + + [Test] + public void DeleteOnMappedJoin() + { + if (!Dialect.SupportsTemporaryTables) + { + Assert.Ignore("Cannot perform multi-table deletes using dialect not supporting temp tables."); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query().Where(x => x.JoinedName == "joined-name").Delete(); + Assert.That(count, Is.EqualTo(1), "Incorrect deletion count on joined class"); + + t.Commit(); + } + } + + [Test] + public void DeleteUnionSubclassAbstractRoot() + { + if (!Dialect.SupportsTemporaryTables) + { + Assert.Ignore("Cannot perform multi-table deletes using dialect not supporting temp tables."); + } + + // These should reach out into *all* subclass tables... + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query().Where(x => x.Owner == "Steve").Delete(); + Assert.That(count, Is.EqualTo(1), "incorrect restricted update count"); + + count = s.Query().Delete(); + Assert.That(count, Is.EqualTo(3), "incorrect update count"); + + t.Commit(); + } + } + + [Test] + public void DeleteUnionSubclassConcreteSubclass() + { + if (!Dialect.SupportsTemporaryTables) + { + Assert.Ignore("Cannot perform multi-table deletes using dialect not supporting temp tables."); + } + + // These should only affect the given table + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query().Where(x => x.Owner == "Steve").Delete(); + Assert.That(count, Is.EqualTo(1), "incorrect restricted update count"); + + count = s.Query().Delete(); + Assert.That(count, Is.EqualTo(2), "incorrect update count"); + t.Commit(); + } + } + + [Test] + public void DeleteUnionSubclassLeafSubclass() + { + // These should only affect the given table + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query().Where(x => x.Owner == "Kirsten").Delete(); + Assert.That(count, Is.EqualTo(1), "incorrect restricted update count"); + + count = s.Query().Delete(); + Assert.That(count, Is.EqualTo(0), "incorrect update count"); + + t.Commit(); + } + } + + [Test] + public void DeleteRestrictedOnManyToOne() + { + if (!Dialect.SupportsTemporaryTables) + { + Assert.Ignore("Cannot perform multi-table deletes using dialect not supporting temp tables."); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query().Where(x => x.Mother == _butterfly).Delete(); + Assert.That(count, Is.EqualTo(1)); + + t.Commit(); + } + } + + [Test] + public void DeleteSyntaxWithCompositeId() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.Query().Where(x => x.Id.Id == 1 && x.Id.OtherId == 2).Delete(); + + t.Commit(); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/BaseFixture.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/BaseFixture.cs deleted file mode 100644 index 010d1f9d129..00000000000 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/BaseFixture.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using NHibernate.Hql.Ast.ANTLR; -using NHibernate.Util; - -namespace NHibernate.Test.NHSpecificTest.NH3488 -{ - public class BaseFixture: TestCase - { - private readonly IDictionary emptyfilters = new CollectionHelper.EmptyMapClass(); - - #region Overrides of TestCase - - protected override IList Mappings - { - get { return new string[0]; } - } - - #endregion - - protected override void Configure(Cfg.Configuration configuration) - { - var assembly = GetType().Assembly; - string mappingNamespace = GetType().Namespace; - foreach (var resource in assembly.GetManifestResourceNames()) - { - if (resource.StartsWith(mappingNamespace) && resource.EndsWith(".hbm.xml")) - { - configuration.AddResource(resource, assembly); - } - } - } - - public string GetSql(string query) - { - return GetSql(query, null); - } - - public string GetSql(string query, IDictionary replacements) - { - var qt = new QueryTranslatorImpl(null, new HqlParseEngine(query, false, Sfi).Parse(), emptyfilters, Sfi); - qt.Compile(replacements, false); - return qt.SQLString; - } - } -} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/EntityWithCrazyCompositeKey.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/EntityWithCrazyCompositeKey.hbm.xml deleted file mode 100644 index 0fc29196018..00000000000 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/Domain/EntityWithCrazyCompositeKey.hbm.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3488/LinqBulkManipulationFixture.cs b/src/NHibernate.Test/NHSpecificTest/NH3488/LinqBulkManipulationFixture.cs deleted file mode 100644 index f9485480b42..00000000000 --- a/src/NHibernate.Test/NHSpecificTest/NH3488/LinqBulkManipulationFixture.cs +++ /dev/null @@ -1,1282 +0,0 @@ -using NHibernate.Dialect; -using NHibernate.DomainModel; -using NHibernate.Hql.Ast.ANTLR; -using NHibernate.Id; -using NHibernate.Linq; -using NHibernate.Persister.Entity; -using NHibernate.Test.NHSpecificTest.NH3488.Domain; -using NUnit.Framework; -using System; -using System.Collections; -using System.Linq; -using System.Threading; - -namespace NHibernate.Test.NHSpecificTest.NH3488 -{ - [TestFixture] - public class LinqBulkManipulationFixture : BaseFixture - { - public ISession OpenNewSession() - { - return OpenSession(); - } - - - #region INSERTS - - [Test] - public void SimpleInsert() - { - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - s.Query().Insert().As(x=>new Pickup{Id=x.Id,Vin=x.Vin,Owner=x.Owner}); - - t.Commit(); - t = s.BeginTransaction(); - - s.CreateQuery("delete Vehicle").ExecuteUpdate(); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void SimpleInsertFromAggregate() - { - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - s.Query() - .GroupBy(x => x.Id) - .Select(x => new { Id = x.Key, Vin = x.Max(y => y.Vin), Owner = x.Max(y => y.Owner) }) - .Insert().As(x => new Pickup { Id = x.Id, Vin = x.Vin, Owner = x.Owner }); - - t.Commit(); - t = s.BeginTransaction(); - - s.CreateQuery("delete Vehicle").ExecuteUpdate(); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void SimpleInsertFromLimited() - { - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - s.Query() - .Skip(1) - .Take(1) - .Insert().As(x => new Pickup { Id = x.Id, Vin = x.Vin, Owner = x.Owner }); - - t.Commit(); - t = s.BeginTransaction(); - - s.CreateQuery("delete Vehicle").ExecuteUpdate(); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void SimpleInsertWithConstants() - { - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - s.Query() - .Insert().Into(x => x.Set(y=>y.Id,y=>y.Id).Set(y=>y.Vin,y=>y.Vin).Set(y=>y.Owner,"The owner")); - - t.Commit(); - t = s.BeginTransaction(); - - s.CreateQuery("delete Vehicle").ExecuteUpdate(); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void SimpleInsertFromProjection() - { - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - s.Query() - .Select(x=>new {x.Id,x.Owner,UpperOwner=x.Owner.ToUpper()}) - .Insert().Into(x => x.Set(y => y.Id, y => y.Id).Set(y => y.Vin, y => y.UpperOwner)); - - t.Commit(); - t = s.BeginTransaction(); - - s.CreateQuery("delete Vehicle").ExecuteUpdate(); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void InsertWithClientSideRequirementsThrowsException() - { - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - Assert.Throws(() => - s.Query() - .Insert().As(x => new Pickup {Id = x.Id, Vin = x.Vin, Owner = x.Owner.PadRight(200)})); - - t.Commit(); - t = s.BeginTransaction(); - - s.CreateQuery("delete Vehicle").ExecuteUpdate(); - - t.Commit(); - s.Close(); - } - - - [Test] - public void InsertWithManyToOne() - { - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - s.Query() - .Insert().As(x => new Animal {Description = x.Description, BodyWeight = x.BodyWeight, Mother = x.Mother}); - - t.Commit(); - t = s.BeginTransaction(); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - - [Test] - public void InsertWithManyToOneAsParameter() - { - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - s.Query() - .Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = data.Butterfly }); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void InsertWithManyToOneWithCompositeKey() - { - - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - var parent = new EntityWithCrazyCompositeKey {Id = new CrazyCompositeKey {Id=1, OtherId=1}, Name = "Parent"}; - - s.Save(parent); - - t.Commit(); - t = s.BeginTransaction(); - - s.Query() - .Insert().As(x => new EntityReferencingEntityWithCrazyCompositeKey { Name = "Child", Parent = x }); - - t.Commit(); - t = s.BeginTransaction(); - - s.CreateQuery("delete EntityReferencingEntityWithCrazyCompositeKey").ExecuteUpdate(); - s.CreateQuery("delete EntityWithCrazyCompositeKey").ExecuteUpdate(); - - t.Commit(); - s.Close(); - - - } - - - - [Test] - public void InsertIntoSuperclassPropertiesFails() - { - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - Assert.Throws( - () => s.Query().Insert().As(x=>new Human{Id=x.Id,BodyWeight = x.BodyWeight}), - "superclass prop insertion did not error"); - - t.Commit(); - t = s.BeginTransaction(); - - s.CreateQuery("delete Animal where Mother is not null").ExecuteUpdate(); - s.CreateQuery("delete Animal where Father is not null").ExecuteUpdate(); - s.CreateQuery("delete Animal").ExecuteUpdate(); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void InsertAcrossMappedJoinFails() - { - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - Assert.Throws( - () => s.Query().Insert().As(x=>new Joiner{Name = x.Vin,JoinedName = x.Owner}), - "mapped-join insertion did not error"); - - t.Commit(); - t = s.BeginTransaction(); - - s.CreateQuery("delete Joiner").ExecuteUpdate(); - s.CreateQuery("delete Vehicle").ExecuteUpdate(); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - public void InsertWithGeneratedId() - { - // Make sure the env supports bulk inserts with generated ids... - IEntityPersister persister = Sfi.GetEntityPersister(typeof(PettingZoo).FullName); - IIdentifierGenerator generator = persister.IdentifierGenerator; - if (!HqlSqlWalker.SupportsIdGenWithBulkInsertion(generator)) - { - return; - } - - // create a Zoo - var zoo = new Zoo { Name = "zoo" }; - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - s.Save(zoo); - t.Commit(); - s.Close(); - - s = OpenSession(); - t = s.BeginTransaction(); - int count = s.Query().Insert().As(x=>new PettingZoo{Name=x.Name}); - t.Commit(); - s.Close(); - Assert.That(count, Is.EqualTo(1), "unexpected insertion count"); - - s = OpenSession(); - t = s.BeginTransaction(); - var pz = (PettingZoo)s.CreateQuery("from PettingZoo").UniqueResult(); - t.Commit(); - s.Close(); - - Assert.That(zoo.Name, Is.EqualTo(pz.Name)); - Assert.That(zoo.Id != pz.Id); - - s = OpenSession(); - t = s.BeginTransaction(); - s.CreateQuery("delete Zoo").ExecuteUpdate(); - t.Commit(); - s.Close(); - } - - [Test] - public void InsertWithGeneratedVersionAndId() - { - // Make sure the env supports bulk inserts with generated ids... - IEntityPersister persister = Sfi.GetEntityPersister(typeof(IntegerVersioned).FullName); - IIdentifierGenerator generator = persister.IdentifierGenerator; - if (!HqlSqlWalker.SupportsIdGenWithBulkInsertion(generator)) - { - return; - } - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - var entity = new IntegerVersioned { Name = "int-vers" }; - s.Save(entity); - s.CreateQuery("select Id, Name, Version from IntegerVersioned").List(); - t.Commit(); - s.Close(); - - long initialId = entity.Id; - int initialVersion = entity.Version; - - s = OpenSession(); - t = s.BeginTransaction(); - int count = - s.Query() - .Where(x => x.Id == entity.Id) - .Insert().As(x => new IntegerVersioned {Name = x.Name, Data = x.Data}); - t.Commit(); - s.Close(); - - Assert.That(count, Is.EqualTo(1), "unexpected insertion count"); - - s = OpenSession(); - t = s.BeginTransaction(); - var created = - (IntegerVersioned) - s.CreateQuery("from IntegerVersioned where Id <> :initialId").SetInt64("initialId", initialId).UniqueResult(); - t.Commit(); - s.Close(); - - Assert.That(created.Version, Is.EqualTo(initialVersion), "version was not seeded"); - - s = OpenSession(); - t = s.BeginTransaction(); - s.CreateQuery("delete IntegerVersioned").ExecuteUpdate(); - t.Commit(); - s.Close(); - } - - [Test] - public void InsertWithGeneratedTimestampVersion() - { - // Make sure the env supports bulk inserts with generated ids... - IEntityPersister persister = Sfi.GetEntityPersister(typeof(TimestampVersioned).FullName); - IIdentifierGenerator generator = persister.IdentifierGenerator; - if (!HqlSqlWalker.SupportsIdGenWithBulkInsertion(generator)) - { - return; - } - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - var entity = new TimestampVersioned { Name = "int-vers" }; - s.Save(entity); - s.CreateQuery("select Id, Name, Version from TimestampVersioned").List(); - t.Commit(); - s.Close(); - - long initialId = entity.Id; - //Date initialVersion = entity.getVersion(); - - s = OpenSession(); - t = s.BeginTransaction(); - int count = - s.Query() - .Where(x => x.Id == entity.Id) - .Insert().As(x => new TimestampVersioned {Name = x.Name, Data = x.Data}); - - t.Commit(); - s.Close(); - - Assert.That(count, Is.EqualTo(1), "unexpected insertion count"); - - s = OpenSession(); - t = s.BeginTransaction(); - var created = - (TimestampVersioned) - s.CreateQuery("from TimestampVersioned where Id <> :initialId").SetInt64("initialId", initialId).UniqueResult(); - t.Commit(); - s.Close(); - - Assert.That(created.Version, Is.GreaterThan(DateTime.Today)); - - s = OpenSession(); - t = s.BeginTransaction(); - s.CreateQuery("delete TimestampVersioned").ExecuteUpdate(); - t.Commit(); - s.Close(); - } - - [Test] - public void InsertWithSelectListUsingJoins() - { - // this is just checking parsing and syntax... - ISession s = OpenSession(); - s.BeginTransaction(); - - s.Query().Where(x=>x.Mother.Mother!=null) - .Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight }); - - s.CreateQuery("delete from Animal").ExecuteUpdate(); - s.Transaction.Commit(); - s.Close(); - } - - [Test] - public void InsertToComponent() - { - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - var human = new SimpleClassWithComponent { Name = new Name { First = "Stevee", Initial = 'X', Last = "Ebersole" } }; - - s.Save(human); - t.Commit(); - - string correctName = "Steve"; - - t = s.BeginTransaction(); - int count = - s.Query().Insert().Into(x => x.Set(y => y.Name.First, y => correctName)); - Assert.That(count, Is.EqualTo(1), "incorrect insert count"); - - count = - s.Query() - .Where(x=>x.Name.First==correctName) - .Insert().As(x => new SimpleClassWithComponent {Name = new Name {First = x.Name.First,Last=x.Name.Last,Initial = 'Z'}}); - Assert.That(count, Is.EqualTo(1), "incorrect insert count"); - t.Commit(); - - t = s.BeginTransaction(); - - s.CreateQuery("delete SimpleClassWithComponent").ExecuteUpdate(); - t.Commit(); - - s.Close(); - } - - #endregion - - - #region UPDATES - - - [Test] - public void UpdateWithWhereExistsSubquery() - { - // multi-table ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - var joe = new Human { Name = new Name { First = "Joe", Initial = 'Q', Last = "Public" } }; - s.Save(joe); - var doll = new Human { Name = new Name { First = "Kyu", Initial = 'P', Last = "Doll" }, Friends = new[] { joe } }; - s.Save(doll); - t.Commit(); - s.Close(); - - s = OpenSession(); - t = s.BeginTransaction(); - - int count = s.Query() - .Where(x => x.Friends.OfType().Any(f => f.Name.Last == "Public")) - .Update().Assign(x => x.Set(y => y.Description, "updated")); - Assert.That(count, Is.EqualTo(1)); - s.Delete(doll); - s.Delete(joe); - t.Commit(); - s.Close(); - - // single-table (one-to-many & many-to-many) ~~~~~~~~~~~~~~~~~~~~~~~~~~ - s = OpenSession(); - t = s.BeginTransaction(); - var entity = new SimpleEntityWithAssociation(); - var other = new SimpleEntityWithAssociation(); - entity.Name = "main"; - other.Name = "many-to-many-association"; - entity.ManyToManyAssociatedEntities.Add(other); - entity.AddAssociation("one-to-many-association"); - s.Save(entity); - t.Commit(); - s.Close(); - - s = OpenSession(); - t = s.BeginTransaction(); - // one-to-many test - - count = s.Query() - .Where(x => x.AssociatedEntities.Any(a => a.Name == "one-to-many-association")) - .Update().Assign(x => x.Set(y => y.Name, "updated")); - Assert.That(count, Is.EqualTo(1)); - // many-to-many test - if (Dialect.SupportsSubqueryOnMutatingTable) - { - count = s.Query() - .Where(x => x.ManyToManyAssociatedEntities.Any(a => a.Name == "many-to-many-association")) - .Update().Assign(x => x.Set(y => y.Name, "updated")); - - Assert.That(count, Is.EqualTo(1)); - } - IEnumerator mtm = entity.ManyToManyAssociatedEntities.GetEnumerator(); - mtm.MoveNext(); - s.Delete(mtm.Current); - s.Delete(entity); - t.Commit(); - s.Close(); - } - - [Test] - public void IncrementCounterVersion() - { - IntegerVersioned entity; - - using (ISession s = OpenSession()) - using (ITransaction t = s.BeginTransaction()) - { - entity = new IntegerVersioned { Name = "int-vers", Data = "foo" }; - s.Save(entity); - t.Commit(); - } - - int initialVersion = entity.Version; - - using (ISession s = OpenSession()) - { - using (ITransaction t = s.BeginTransaction()) - { - // Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26 - int count = - s.Query() - .Update().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd"), true); - Assert.That(count, Is.EqualTo(1), "incorrect exec count"); - t.Commit(); - } - - using (ITransaction t = s.BeginTransaction()) - { - entity = s.Get(entity.Id); - s.Delete(entity); - t.Commit(); - } - } - - Assert.That(entity.Version, Is.EqualTo(initialVersion + 1), "version not incremented"); - Assert.That(entity.Name, Is.EqualTo("int-versupd")); - Assert.That(entity.Data, Is.EqualTo("fooupd")); - } - - [Test] - public void IncrementTimestampVersion() - { - TimestampVersioned entity; - - using (ISession s = OpenSession()) - using (ITransaction t = s.BeginTransaction()) - { - entity = new TimestampVersioned { Name = "ts-vers", Data = "foo" }; - s.Save(entity); - t.Commit(); - } - - DateTime initialVersion = entity.Version; - - Thread.Sleep(1300); - - using (ISession s = OpenSession()) - { - using (ITransaction t = s.BeginTransaction()) - { - // Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26 - int count = s.Query(). - Update().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd"), true); - Assert.That(count, Is.EqualTo(1), "incorrect exec count"); - t.Commit(); - } - - using (ITransaction t = s.BeginTransaction()) - { - entity = s.Load(entity.Id); - s.Delete(entity); - t.Commit(); - } - } - - Assert.That(entity.Version, Is.GreaterThan(initialVersion), "version not incremented"); - Assert.That(entity.Name, Is.EqualTo("ts-versupd")); - Assert.That(entity.Data, Is.EqualTo("fooupd")); - } - - [Test] - public void UpdateOnComponent() - { - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - var human = new Human { Name = new Name { First = "Stevee", Initial = 'X', Last = "Ebersole" } }; - - s.Save(human); - t.Commit(); - - string correctName = "Steve"; - - t = s.BeginTransaction(); - int count = - s.Query().Where(x => x.Id == human.Id).Update().As(x => new Human{Name={First = correctName}}); - - Assert.That(count, Is.EqualTo(1), "incorrect update count"); - t.Commit(); - - t = s.BeginTransaction(); - s.Refresh(human); - - Assert.That(human.Name.First, Is.EqualTo(correctName), "Update did not execute properly"); - - s.CreateQuery("delete Human").ExecuteUpdate(); - t.Commit(); - - s.Close(); - } - - [Test] - public void UpdateWithClientSideRequirementsThrowsException() - { - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - var human = new Human { Name = new Name { First = "Stevee", Initial = 'X', Last = "Ebersole" } }; - - s.Save(human); - t.Commit(); - - t = s.BeginTransaction(); - - Assert.Throws(()=> - s.Query().Where(x => x.Id == human.Id).Update().As(x => new Human { Name = { First = x.Name.First.PadLeft(200) } }) - ); - - t.Commit(); - - t = s.BeginTransaction(); - - s.CreateQuery("delete Human").ExecuteUpdate(); - t.Commit(); - - s.Close(); - } - - [Test] - public void UpdateOnManyToOne() - { - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - s.Query().Where(x => x.Id == 2).Update().Assign(x => x.Set(y => y.Mother, y => null)); - - if (!(Dialect is MySQLDialect)) - { - // MySQL does not support (even un-correlated) subqueries against the update-mutating table - s.Query().Where(x => x.Id == 2).Update().Assign(x => x.Set(y => y.Mother, y => s.Query().First(z => z.Id == 1))); - } - - t.Commit(); - s.Close(); - } - - - - [Test] - public void UpdateOnDiscriminatorSubclass() - { - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - int count = s.Query().Update().Assign(x => x.Set(y => y.Name, y => y.Name)); - Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass update count"); - - t.Rollback(); - t = s.BeginTransaction(); - - count = s.Query().Where(x => x.Id == data.PettingZoo.Id).Update().Assign(x => x.Set(y => y.Name, y => y.Name)); - Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass update count"); - - t.Rollback(); - t = s.BeginTransaction(); - - count = s.Query().Update().Assign(x => x.Set(y => y.Name, y => y.Name)); - Assert.That(count, Is.EqualTo(2), "Incorrect discrim subclass update count"); - - t.Rollback(); - t = s.BeginTransaction(); - - // TODO : not so sure this should be allowed. Seems to me that if they specify an alias, - // property-refs should be required to be qualified. - count = s.Query().Where(x => x.Id == data.Zoo.Id).Update().Assign(x => x.Set(y => y.Name, y => y.Name)); - Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass update count"); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void UpdateOnAnimal() - { - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - //int count = s.Query().Where(x => x.Description == data.Frog.Description).Update(x => x.Set(y => y.Description, y => y.Description)); - //Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); - - int count = - s.Query().Where(x => x.Description == data.Polliwog.Description).Update().Assign(x => x.Set(y => y.Description, y => "Tadpole")); - Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); - - var tadpole = s.Load(data.Polliwog.Id); - - Assert.That(tadpole.Description, Is.EqualTo("Tadpole"), "Update did not take effect"); - - count = - s.Query().Update().Assign(x => x.Set(y => y.FireTemperature, 300)); - Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); - - - count = - s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, y => y.BodyWeight + 1 + 1)); - Assert.That(count, Is.EqualTo(7), "incorrect count on 'complex' update assignment"); - - if (!(Dialect is MySQLDialect)) - { - // MySQL does not support (even un-correlated) subqueries against the update-mutating table - s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, y => s.Query().Max(z => z.BodyWeight))); - } - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void UpdateOnDragonWithProtectedProperty() - { - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - int count = - s.Query().Update().Assign(x => x.Set(y => y.FireTemperature, 300)); - Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void UpdateMultiplePropertyOnAnimal() - { - var data = new TestData(this); - data.Prepare(); - - using (ISession s = OpenSession()) - using (ITransaction t = s.BeginTransaction()) - { - int count = - - s.Query() - .Where(x => x.Description == data.Polliwog.Description) - .Update().Assign(x => x.Set(y => y.Description, y => "Tadpole").Set(y => y.BodyWeight, 3)); - - - - Assert.That(count, Is.EqualTo(1)); - t.Commit(); - } - - using (ISession s = OpenSession()) - using (s.BeginTransaction()) - { - var tadpole = s.Get(data.Polliwog.Id); - Assert.That(tadpole.Description, Is.EqualTo("Tadpole")); - Assert.That(tadpole.BodyWeight, Is.EqualTo(3)); - } - - data.Cleanup(); - } - - [Test] - public void UpdateOnMammal() - { - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - int count = s.Query().Update().Assign(x=>x.Set(y=>y.Description,y=>y.Description)); - - - Assert.That(count, Is.EqualTo(2), "incorrect update count against 'middle' of joined-subclass hierarchy"); - - count = s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, 25)); - Assert.That(count, Is.EqualTo(2), "incorrect update count against 'middle' of joined-subclass hierarchy"); - - if (!(Dialect is MySQLDialect)) - { - // MySQL does not support (even un-correlated) subqueries against the update-mutating table - count = s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, y => s.Query().Max(z => z.BodyWeight))); - Assert.That(count, Is.EqualTo(2), "incorrect update count against 'middle' of joined-subclass hierarchy"); - } - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void UpdateSetNullUnionSubclass() - { - var data = new TestData(this); - data.Prepare(); - - // These should reach out into *all* subclass tables... - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - int count = s.Query().Update().Assign(x => x.Set(y => y.Owner, "Steve")); - Assert.That(count, Is.EqualTo(4), "incorrect restricted update count"); - count = s.Query().Where(x => x.Owner == "Steve").Update().Assign(x => x.Set(y => y.Owner, (string)null)); - Assert.That(count, Is.EqualTo(4), "incorrect restricted update count"); - - count = s.CreateQuery("delete Vehicle where Owner is null").ExecuteUpdate(); - Assert.That(count, Is.EqualTo(4), "incorrect restricted update count"); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void UpdateSetNullOnDiscriminatorSubclass() - { - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - int count = s.Query().Update().Assign(x => x.Set(y => y.Address.City, (string)null)); - - Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); - count = s.CreateQuery("delete Zoo where Address.City is null").ExecuteUpdate(); - Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); - - count = s.Query().Update().Assign(x => x.Set(y => y.Address.City, (string)null)); - Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); - count = s.CreateQuery("delete Zoo where Address.City is null").ExecuteUpdate(); - Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void UpdateSetNullOnJoinedSubclass() - { - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - int count = s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, -1)); - Assert.That(count, Is.EqualTo(2), "Incorrect deletion count on joined subclass"); - - count = s.CreateQuery("delete Animal where BodyWeight = -1").ExecuteUpdate(); - Assert.That(count, Is.EqualTo(2), "Incorrect deletion count on joined subclass"); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - #endregion - - - #region DELETES - - [Test] - public void DeleteWithSubquery() - { - // setup the test data... - ISession s = OpenSession(); - s.BeginTransaction(); - var owner = new SimpleEntityWithAssociation {Name = "myEntity-1"}; - owner.AddAssociation("assoc-1"); - owner.AddAssociation("assoc-2"); - owner.AddAssociation("assoc-3"); - s.Save(owner); - var owner2 = new SimpleEntityWithAssociation {Name = "myEntity-2"}; - owner2.AddAssociation("assoc-1"); - owner2.AddAssociation("assoc-2"); - owner2.AddAssociation("assoc-3"); - owner2.AddAssociation("assoc-4"); - s.Save(owner2); - var owner3 = new SimpleEntityWithAssociation {Name = "myEntity-3"}; - s.Save(owner3); - s.Transaction.Commit(); - s.Close(); - - // now try the bulk delete - s = OpenSession(); - s.BeginTransaction(); - int count = s.Query().Where(x=>x.AssociatedEntities.Count==0 && x.Name.Contains("")).Delete(); - Assert.That(count, Is.EqualTo(1), "Incorrect delete count"); - s.Transaction.Commit(); - s.Close(); - - // finally, clean up - s = OpenSession(); - s.BeginTransaction(); - s.CreateQuery("delete SimpleAssociatedEntity").ExecuteUpdate(); - s.CreateQuery("delete SimpleEntityWithAssociation").ExecuteUpdate(); - s.Transaction.Commit(); - s.Close(); - } - - - - [Test] - public void SimpleDeleteOnAnimal() - { - if (Dialect.HasSelfReferentialForeignKeyBug) - { - Assert.Ignore("self referential FK bug", "HQL delete testing"); - return; - } - - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - int count = s.Query().Where(x=>x.Id==data.Polliwog.Id).Delete(); - Assert.That(count, Is.EqualTo(1), "Incorrect delete count"); - - count = s.Query().Where(x => x.Id == data.Catepillar.Id).Delete(); - Assert.That(count, Is.EqualTo(1), "Incorrect delete count"); - - // HHH-873... - if (Dialect.SupportsSubqueryOnMutatingTable) - { - count = s.Query().Where(x=>s.Query().Contains(x)).Delete(); - Assert.That(count, Is.EqualTo(0)); - } - - count = s.Query().Delete(); - Assert.That(count, Is.EqualTo(5), "Incorrect delete count"); - - IList list = s.Query().ToList(); - Assert.That(list, Is.Empty, "table not empty"); - - t.Commit(); - s.Close(); - data.Cleanup(); - } - - [Test] - public void DeleteOnDiscriminatorSubclass() - { - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - int count = s.Query().Delete(); - Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); - - count = s.Query().Delete(); - Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void DeleteOnJoinedSubclass() - { - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - int count = s.Query().Where(x=>x.BodyWeight>150).Delete(); - Assert.That(count, Is.EqualTo(1), "Incorrect deletion count on joined subclass"); - - count = s.Query().Delete(); - Assert.That(count, Is.EqualTo(1), "Incorrect deletion count on joined subclass"); - - count = s.Query().Delete(); - Assert.That(count, Is.EqualTo(0), "Incorrect deletion count on joined subclass"); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void DeleteOnMappedJoin() - { - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - int count = s.Query().Where(x=>x.JoinedName == "joined-name").Delete(); - Assert.That(count, Is.EqualTo(1), "Incorrect deletion count on joined class"); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void DeleteUnionSubclassAbstractRoot() - { - var data = new TestData(this); - data.Prepare(); - - // These should reach out into *all* subclass tables... - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - int count = s.Query().Where(x => x.Owner == "Steve").Delete(); - Assert.That(count, Is.EqualTo(1), "incorrect restricted update count"); - - count = s.Query().Delete(); - Assert.That(count, Is.EqualTo(3), "incorrect update count"); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void DeleteUnionSubclassConcreteSubclass() - { - var data = new TestData(this); - data.Prepare(); - - // These should only affect the given table - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - int count = s.Query().Where(x=>x.Owner =="Steve").Delete(); - Assert.That(count, Is.EqualTo(1), "incorrect restricted update count"); - - count = s.Query().Delete(); - Assert.That(count, Is.EqualTo(2), "incorrect update count"); - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void DeleteUnionSubclassLeafSubclass() - { - var data = new TestData(this); - data.Prepare(); - - // These should only affect the given table - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - int count = s.Query().Where(x=>x.Owner == "Kirsten").Delete(); - Assert.That(count, Is.EqualTo(1), "incorrect restricted update count"); - - count = s.Query().Delete(); - Assert.That(count, Is.EqualTo(0), "incorrect update count"); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void DeleteRestrictedOnManyToOne() - { - var data = new TestData(this); - data.Prepare(); - - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - int count = s.Query().Where(x=>x.Mother == data.Butterfly).Delete(); - Assert.That(count, Is.EqualTo(1)); - - t.Commit(); - s.Close(); - - data.Cleanup(); - } - - [Test] - public void DeleteSyntaxWithCompositeId() - { - ISession s = OpenSession(); - ITransaction t = s.BeginTransaction(); - - s.Query().Where(x=>x.Id.Id == 1 && x.Id.OtherId == 2).Delete(); - - t.Commit(); - s.Close(); - } - - #endregion - - private class TestData - { - private readonly LinqBulkManipulationFixture tc; - public Animal Polliwog; - public Animal Catepillar; - public Animal Frog; - public Animal Butterfly; - - public Zoo Zoo; - public Zoo PettingZoo; - - public TestData(LinqBulkManipulationFixture tc) - { - this.tc = tc; - } - - public void Prepare() - { - ISession s = tc.OpenNewSession(); - ITransaction txn = s.BeginTransaction(); - - Polliwog = new Animal {BodyWeight = 12, Description = "Polliwog"}; - - Catepillar = new Animal {BodyWeight = 10, Description = "Catepillar"}; - - Frog = new Animal {BodyWeight = 34, Description = "Frog"}; - - Polliwog.Father = Frog; - Frog.AddOffspring(Polliwog); - - Butterfly = new Animal {BodyWeight = 9, Description = "Butterfly"}; - - Catepillar.Mother = Butterfly; - Butterfly.AddOffspring(Catepillar); - - s.Save(Frog); - s.Save(Polliwog); - s.Save(Butterfly); - s.Save(Catepillar); - - var dog = new Dog {BodyWeight = 200, Description = "dog"}; - s.Save(dog); - - var cat = new Cat {BodyWeight = 100, Description = "cat"}; - s.Save(cat); - - var dragon = new Dragon(); - dragon.SetFireTemperature(200); - s.Save(dragon); - - Zoo = new Zoo {Name = "Zoo"}; - var add = new Address {City = "MEL", Country = "AU", Street = "Main st", PostalCode = "3000"}; - Zoo.Address = add; - - PettingZoo = new PettingZoo {Name = "Petting Zoo"}; - var addr = new Address {City = "Sydney", Country = "AU", Street = "High st", PostalCode = "2000"}; - PettingZoo.Address = addr; - - s.Save(Zoo); - s.Save(PettingZoo); - - var joiner = new Joiner {JoinedName = "joined-name", Name = "name"}; - s.Save(joiner); - - var car = new Car {Vin = "123c", Owner = "Kirsten"}; - s.Save(car); - - var truck = new Truck {Vin = "123t", Owner = "Steve"}; - s.Save(truck); - - var suv = new SUV {Vin = "123s", Owner = "Joe"}; - s.Save(suv); - - var pickup = new Pickup {Vin = "123p", Owner = "Cecelia"}; - s.Save(pickup); - - txn.Commit(); - s.Close(); - } - - public void Cleanup() - { - ISession s = tc.OpenNewSession(); - ITransaction txn = s.BeginTransaction(); - - // workaround awesome HSQLDB "feature" - s.CreateQuery("delete from Animal where Mother is not null or Father is not null").ExecuteUpdate(); - s.CreateQuery("delete from Animal").ExecuteUpdate(); - s.CreateQuery("delete from Zoo").ExecuteUpdate(); - s.CreateQuery("delete from Joiner").ExecuteUpdate(); - s.CreateQuery("delete from Vehicle").ExecuteUpdate(); - - txn.Commit(); - s.Close(); - } - } - } -} \ No newline at end of file diff --git a/src/NHibernate/Hql/Ast/HqlTreeBuilder.cs b/src/NHibernate/Hql/Ast/HqlTreeBuilder.cs index ce6e72bbde1..bbe3517c776 100755 --- a/src/NHibernate/Hql/Ast/HqlTreeBuilder.cs +++ b/src/NHibernate/Hql/Ast/HqlTreeBuilder.cs @@ -39,12 +39,12 @@ public HqlDelete Delete(HqlFrom @from) return new HqlDelete(_factory, @from); } - public HqlUpdate Update(HqlFrom @from,HqlSet set) + public HqlUpdate Update(HqlFrom @from, HqlSet set) { - return new HqlUpdate(_factory, @from,set); + return new HqlUpdate(_factory, @from, set); } - public HqlUpdate Update(HqlVersioned versioned,HqlFrom @from, HqlSet set) + public HqlUpdate Update(HqlVersioned versioned, HqlFrom @from, HqlSet set) { return new HqlUpdate(_factory, versioned, @from, set); } @@ -53,7 +53,6 @@ public HqlInsert Insert(HqlInto into, HqlQuery query) { return new HqlInsert(_factory, into, query); } - public HqlSelectFrom SelectFrom() { diff --git a/src/NHibernate/Linq/Assignment.cs b/src/NHibernate/Linq/Assignment.cs index a905cebadc9..471c7b24a08 100644 --- a/src/NHibernate/Linq/Assignment.cs +++ b/src/NHibernate/Linq/Assignment.cs @@ -7,10 +7,16 @@ namespace NHibernate.Linq /// public class Assignment { + /// + /// The assigned property. + /// public string PropertyPath { get; set; } + /// + /// The value to assign. + /// public Expression Expression { get; set; } - public Assignment(string propertyPath,Expression expression) + public Assignment(string propertyPath, Expression expression) { PropertyPath = propertyPath; Expression = expression; diff --git a/src/NHibernate/Linq/Assignments.cs b/src/NHibernate/Linq/Assignments.cs index 37769970753..dca211b12ac 100644 --- a/src/NHibernate/Linq/Assignments.cs +++ b/src/NHibernate/Linq/Assignments.cs @@ -5,22 +5,22 @@ using System.Linq.Expressions; using System.Reflection; using NHibernate.Linq.Visitors; -using Remotion.Linq.Utilities; +using NHibernate.Util; namespace NHibernate.Linq { public abstract class Assignments { protected static readonly ConstructorInfo DictionaryConstructorInfo = typeof(Dictionary).GetConstructor(new[] { typeof(int) }); - protected static readonly MethodInfo DictionaryAddMethodInfo = typeof(Dictionary).GetMethod("Add"); + protected static readonly MethodInfo DictionaryAddMethodInfo = ReflectHelper.GetMethod>(d => d.Add(null, null)); } /// - /// Class to hold assigments used in updates and inserts + /// Class to hold assignments used in updates and inserts. /// - /// The type of the input. - /// The type of the output. - public class Assignments : Assignments + /// The type of the entity source of the insert or to update. + /// The type of the entity to insert or to update. + public class Assignments : Assignments { private readonly List _sets = new List(); @@ -30,8 +30,8 @@ public class Assignments : Assignments /// The type of the property. /// The property. /// The expression that should be assigned to the property. - /// - public Assignments Set(Expression> property, Expression> expression) + /// The current assignments list. + public Assignments Set(Expression> property, Expression> expression) { if (expression == null) throw new ArgumentNullException(nameof(expression)); @@ -46,15 +46,15 @@ public Assignments Set(Expression> /// The type of the property. /// The property. /// The value. - /// - public Assignments Set(Expression> property, TProp value) + /// The current assignments list. + public Assignments Set(Expression> property, TProp value) { var member = GetMemberExpression(property); _sets.Add(new Assignment(member.GetMemberPath(), Expression.Constant(value, typeof(TProp)))); return this; } - private static MemberExpression GetMemberExpression(Expression> property) + private static MemberExpression GetMemberExpression(Expression> property) { if (property == null) throw new ArgumentNullException(nameof(property)); @@ -65,64 +65,58 @@ private static MemberExpression GetMemberExpression(Expression - /// Converts the assignments into a to lambda expression, which creates a Dictionary<string,object%gt;. + /// Converts the assignments into a lambda expression, which creates a Dictionary<string,object%gt;. /// - /// + /// A lambda expression representing the assignments. public LambdaExpression ConvertToDictionaryExpression() { - var param = Expression.Parameter(typeof(TInput)); + var param = Expression.Parameter(typeof(TSource)); var inits = new List(); foreach (var set in _sets) { var setter = set.Expression; - var setterLambda = setter as LambdaExpression; - if (setterLambda != null) + if (setter is LambdaExpression setterLambda) { setter = setterLambda.Body.Replace(setterLambda.Parameters.First(), param); } inits.Add(Expression.ElementInit(DictionaryAddMethodInfo, Expression.Constant(set.PropertyPath), - Expression.Convert( - setter, - typeof(object)))); - + Expression.Convert(setter, typeof(object)))); } - //The ListInit is intentionally "infected" with the lambda parameter (param), in the form of an IIF. //The only relevance is to make sure that the ListInit is not evaluated by the PartialEvaluatingExpressionTreeVisitor, - //which could turn it into a Constant + //which could turn it into a Constant var listInit = Expression.ListInit( Expression.New( DictionaryConstructorInfo, Expression.Condition( - Expression.Equal(param, Expression.Constant(null, typeof(TInput))), + Expression.Equal(param, Expression.Constant(null, typeof(TSource))), Expression.Constant(_sets.Count), Expression.Constant(_sets.Count))), inits); - - return Expression.Lambda(listInit, param); } - public static Assignments FromExpression(Expression> expression) + /// + /// Converts a members initialization expression to assignments. Unset members are ignored and left untouched. + /// + /// The expression to convert. + /// The corresponding assignments. + public static Assignments FromExpression(Expression> expression) { if (expression == null) throw new ArgumentNullException(nameof(expression)); - var instance = new Assignments(); - var memberInitExpression = expression.Body as MemberInitExpression; - - if (memberInitExpression == null) - { - throw new ArgumentException("The expression must be member initialization, e.g. x => new Dog{Name = x.Name,Age = x.Age + 5}"); - } - + var instance = new Assignments(); + var memberInitExpression = expression.Body as MemberInitExpression ?? + throw new ArgumentException("The expression must be member initialization, e.g. x => new Dog { Name = x.Name, Age = x.Age + 5 }"); + AddSetsFromBindings(memberInitExpression.Bindings, instance, "", expression.Parameters); - + return instance; } - private static void AddSetsFromBindings(IEnumerable bindings, Assignments instance, string path, ReadOnlyCollection parameters) + private static void AddSetsFromBindings(IEnumerable bindings, Assignments instance, string path, ReadOnlyCollection parameters) { foreach (var binding in bindings) { @@ -132,15 +126,15 @@ private static void AddSetsFromBindings(IEnumerable bindings, Ass } else if (binding.BindingType == MemberBindingType.MemberBinding) // {Property={SubProperty="Value}} { - AddSetsFromBindings(((MemberMemberBinding) binding).Bindings, instance, path + "." + binding.Member.Name, parameters); + AddSetsFromBindings(((MemberMemberBinding)binding).Bindings, instance, path + "." + binding.Member.Name, parameters); } } } - private static void AddSetsFromAssignment(MemberAssignment assignment, Assignments instance, string path, ReadOnlyCollection parameters) + private static void AddSetsFromAssignment(MemberAssignment assignment, Assignments instance, string path, ReadOnlyCollection parameters) { - var memberInit = assignment.Expression as MemberInitExpression; // {Property=new Instance{SubProperty="Value"}} - if (memberInit!=null) + // {Property=new Instance{SubProperty="Value"}} + if (assignment.Expression is MemberInitExpression memberInit) { AddSetsFromBindings(memberInit.Bindings, instance, path, parameters); } diff --git a/src/NHibernate/Linq/DefaultQueryProvider.cs b/src/NHibernate/Linq/DefaultQueryProvider.cs index 3e84e6b611d..0878f94c0ca 100644 --- a/src/NHibernate/Linq/DefaultQueryProvider.cs +++ b/src/NHibernate/Linq/DefaultQueryProvider.cs @@ -196,9 +196,9 @@ public int ExecuteUpdate(Expression expression, Assignments assignments return query.ExecuteUpdate(); } - public int ExecuteInsert(Expression expression, Assignments assignments) + public int ExecuteInsert(Expression expression, Assignments assignments) { - var nhLinqExpression = new NhLinqInsertExpression(expression, assignments, Session.Factory); + var nhLinqExpression = new NhLinqInsertExpression(expression, assignments, Session.Factory); var query = Session.CreateQuery(nhLinqExpression); diff --git a/src/NHibernate/Linq/InsertSyntax.cs b/src/NHibernate/Linq/InsertSyntax.cs index 81bd034a5c4..da16edd0c17 100644 --- a/src/NHibernate/Linq/InsertSyntax.cs +++ b/src/NHibernate/Linq/InsertSyntax.cs @@ -3,7 +3,11 @@ namespace NHibernate.Linq { - public class InsertSyntax + /// + /// An insert object on which entities to insert can be specified. + /// + /// The type of the entities selected as source of the insert. + public class InsertSyntax { private readonly Expression _sourceExpression; private readonly INhQueryProvider _provider; @@ -17,31 +21,31 @@ internal InsertSyntax(Expression sourceExpression, INhQueryProvider provider) /// /// Executes the insert, using the specified assignments. /// - /// The type of the output. + /// The type of the entities to insert. /// The assignments. - /// - public int Into(Action> assignmentActions) + /// The number of inserted entities. + public int Into(Action> assignmentActions) { if (assignmentActions == null) throw new ArgumentNullException(nameof(assignmentActions)); - var assignments = new Assignments(); + var assignments = new Assignments(); assignmentActions.Invoke(assignments); return InsertInto(assignments); } /// - /// Executes the insert, inserting new entities as specified by the expression + /// Executes the insert, inserting new entities as specified by the expression. /// - /// The type of the output. - /// The expression. - /// - public int As(Expression> expression) + /// The type of the entities to insert. + /// The expression projecting a source entity to the entity to insert. + /// The number of inserted entities. + public int As(Expression> expression) { - var assignments = Assignments.FromExpression(expression); + var assignments = Assignments.FromExpression(expression); return InsertInto(assignments); } - private int InsertInto(Assignments assignments) + internal int InsertInto(Assignments assignments) { return _provider.ExecuteInsert(_sourceExpression, assignments); } diff --git a/src/NHibernate/Linq/IntermediateHqlTree.cs b/src/NHibernate/Linq/IntermediateHqlTree.cs index 5f0baaec0d0..fe433964b25 100644 --- a/src/NHibernate/Linq/IntermediateHqlTree.cs +++ b/src/NHibernate/Linq/IntermediateHqlTree.cs @@ -44,14 +44,19 @@ public HqlTreeNode Root get { //Strange side effects in a property getter... - ExecuteAddHavingClause(_hqlHaving); - ExecuteAddOrderBy(_orderBy); - ExecuteAddSkipClause(_skipCount); - ExecuteAddTakeClause(_takeCount); + AddPendingHqlClausesToRoot(); return _root; } } + private void AddPendingHqlClausesToRoot() + { + ExecuteAddHavingClause(_hqlHaving); + ExecuteAddOrderBy(_orderBy); + ExecuteAddSkipClause(_skipCount); + ExecuteAddTakeClause(_takeCount); + } + /// /// If execute result type does not match expected final result type (implying a post execute transformer /// will yield expected result type), the intermediate execute type. @@ -60,21 +65,21 @@ public HqlTreeNode Root public HqlTreeBuilder TreeBuilder { get; } - public IntermediateHqlTree(bool root,QueryMode mode) + public IntermediateHqlTree(bool root, QueryMode mode) { _isRoot = root; TreeBuilder = new HqlTreeBuilder(); - if (mode==QueryMode.Delete) + if (mode == QueryMode.Delete) { _root = TreeBuilder.Delete(TreeBuilder.From()); } - else if (mode==QueryMode.Update) + else if (mode == QueryMode.Update) { _root = TreeBuilder.Update(TreeBuilder.From(), TreeBuilder.Set()); } else if (mode == QueryMode.UpdateVersioned) { - _root = TreeBuilder.Update(TreeBuilder.Versioned(),TreeBuilder.From(), TreeBuilder.Set()); + _root = TreeBuilder.Update(TreeBuilder.Versioned(), TreeBuilder.From(), TreeBuilder.Set()); } else if (mode == QueryMode.Insert) { @@ -89,7 +94,8 @@ public IntermediateHqlTree(bool root,QueryMode mode) public ExpressionToHqlTranslationResults GetTranslation() { - var translationRoot = _insertRoot ?? Root; + AddPendingHqlClausesToRoot(); + var translationRoot = _insertRoot ?? _root; return new ExpressionToHqlTranslationResults(translationRoot, _itemTransformers, _listTransformers, @@ -125,7 +131,7 @@ public void AddSelectClause(HqlTreeNode select) _root.NodesPreOrder.OfType().First().AddChild(select); } - public void AddInsertClause(HqlIdent target,HqlRange columnSpec) + public void AddInsertClause(HqlIdent target, HqlRange columnSpec) { var into = _insertRoot.NodesPreOrder.OfType().Single(); into.AddChild(target); @@ -234,7 +240,7 @@ public void AddHavingClause(HqlBooleanExpression where) } else { - var currentClause = (HqlBooleanExpression) _hqlHaving.Children.Single(); + var currentClause = (HqlBooleanExpression)_hqlHaving.Children.Single(); _hqlHaving.ClearChildren(); _hqlHaving.AddChild(TreeBuilder.BooleanAnd(currentClause, where)); diff --git a/src/NHibernate/Linq/LinqExtensionMethods.cs b/src/NHibernate/Linq/LinqExtensionMethods.cs index ec3d112fcc2..6548a1d039d 100644 --- a/src/NHibernate/Linq/LinqExtensionMethods.cs +++ b/src/NHibernate/Linq/LinqExtensionMethods.cs @@ -123,7 +123,7 @@ public static IQueryable Timeout(this IQueryable query, int timeout) => query.SetOptions(o => o.SetTimeout(timeout)); /// - /// Deletes all entities in the specified query. The delete operation is performed in the database. + /// Deletes all entities selected by the specified query. The delete operation is performed in the database without reading the entities out of it. /// /// The type of the elements of . /// The query matching the entities to delete. @@ -135,7 +135,7 @@ public static int Delete(this IQueryable source) } /// - /// Updates the entities in the query, using the specified assignments. The update operation is performed in the database. + /// Initiate an update for the entities selected by the query. The update operation will be performed in the database without reading the entities out of it. /// /// The type of the elements of . /// The query matching the entities to update. @@ -147,15 +147,15 @@ public static UpdateSyntax Update(this IQueryable sou } /// - /// Inserts new entities into the database, using other stored entities as a source. Uses INSERT INTO [...] SELECT FROM [...] in the database. + /// Initiate an insert using selected entities as a source. Will use INSERT INTO [...] SELECT FROM [...] in the database. /// - /// The type of the input. + /// The type of the elements of . /// The query matching entities source of the data to insert. /// An insert builder, allowing to specify target entity class and assignments to its properties. - public static InsertSyntax Insert(this IQueryable source) + public static InsertSyntax Insert(this IQueryable source) { var provider = GetNhProvider(source); - return new InsertSyntax(source.Expression, provider); + return new InsertSyntax(source.Expression, provider); } public static T MappedAs(this T parameter, IType type) diff --git a/src/NHibernate/Linq/NhLinqDeleteExpression.cs b/src/NHibernate/Linq/NhLinqDeleteExpression.cs index bec060696ba..91eb443d1ba 100644 --- a/src/NHibernate/Linq/NhLinqDeleteExpression.cs +++ b/src/NHibernate/Linq/NhLinqDeleteExpression.cs @@ -1,22 +1,16 @@ using System.Linq.Expressions; using NHibernate.Engine; -using NHibernate.Linq.Visitors; -using Remotion.Linq; namespace NHibernate.Linq { public class NhLinqDeleteExpression : NhLinqExpression { + protected override QueryMode QueryMode => QueryMode.Delete; + public NhLinqDeleteExpression(Expression expression, ISessionFactoryImplementor sessionFactory) : base(expression, sessionFactory) { - Key = Key + "DELETE"; - } - - protected override ExpressionToHqlTranslationResults GenerateHqlQuery(QueryModel queryModel, VisitorParameters visitorParameters) - { - visitorParameters.EntityType = Type; - return QueryModelVisitor.GenerateHqlQuery(queryModel, visitorParameters, true, null, QueryMode.Delete); + Key = "DELETE " + Key; } } } \ No newline at end of file diff --git a/src/NHibernate/Linq/NhLinqExpression.cs b/src/NHibernate/Linq/NhLinqExpression.cs index a661f4ba577..566a40745dc 100644 --- a/src/NHibernate/Linq/NhLinqExpression.cs +++ b/src/NHibernate/Linq/NhLinqExpression.cs @@ -8,7 +8,6 @@ using NHibernate.Linq.Visitors; using NHibernate.Param; using NHibernate.Type; -using Remotion.Linq; namespace NHibernate.Linq { @@ -18,6 +17,11 @@ public class NhLinqExpression : IQueryExpression public System.Type Type { get; private set; } + /// + /// Entity type to insert or update when the expression is a DML. + /// + protected virtual System.Type TargetType => Type; + public IList ParameterDescriptors { get; private set; } public NhLinqExpressionReturnType ReturnType { get; } @@ -26,6 +30,8 @@ public class NhLinqExpression : IQueryExpression public ExpressionToHqlTranslationResults ExpressionToHqlTranslationResults { get; private set; } + protected virtual QueryMode QueryMode => QueryMode.Select; + private Expression _expression; private readonly IDictionary _constantToParameterMap; @@ -35,7 +41,7 @@ public NhLinqExpression(Expression expression, ISessionFactoryImplementor sessio // We want logging to be as close as possible to the original expression sent from the // application. But if we log before partial evaluation done in PreTransform, the log won't - // include e.g. subquery expressions if those are defined by the application in a variable + // include e.g. sub-query expressions if those are defined by the application in a variable // referenced from the main query. LinqLogging.LogExpression("Expression (partially evaluated)", _expression); @@ -61,24 +67,18 @@ public NhLinqExpression(Expression expression, ISessionFactoryImplementor sessio public IASTNode Translate(ISessionFactoryImplementor sessionFactory, bool filter) { var requiredHqlParameters = new List(); - var querySourceNamer = new QuerySourceNamer(); var queryModel = NhRelinqQueryParser.Parse(_expression); - var visitorParameters = new VisitorParameters(sessionFactory, _constantToParameterMap, requiredHqlParameters, querySourceNamer); + var visitorParameters = new VisitorParameters(sessionFactory, _constantToParameterMap, requiredHqlParameters, + new QuerySourceNamer(), TargetType, QueryMode); - ExpressionToHqlTranslationResults = GenerateHqlQuery(queryModel, visitorParameters); + ExpressionToHqlTranslationResults = QueryModelVisitor.GenerateHqlQuery(queryModel, visitorParameters, true, ReturnType); if (ExpressionToHqlTranslationResults.ExecuteResultTypeOverride != null) Type = ExpressionToHqlTranslationResults.ExecuteResultTypeOverride; ParameterDescriptors = requiredHqlParameters.AsReadOnly(); - - return ExpressionToHqlTranslationResults.Statement.AstNode; - } - protected virtual ExpressionToHqlTranslationResults GenerateHqlQuery(QueryModel queryModel, VisitorParameters visitorParameters) - { - visitorParameters.EntityType = Type; - return QueryModelVisitor.GenerateHqlQuery(queryModel, visitorParameters, true, ReturnType, QueryMode.Select); + return ExpressionToHqlTranslationResults.Statement.AstNode; } internal void CopyExpressionTranslation(NhLinqExpression other) diff --git a/src/NHibernate/Linq/NhLinqInsertExpression.cs b/src/NHibernate/Linq/NhLinqInsertExpression.cs index 555ff6ce20e..1bb0ad8b712 100644 --- a/src/NHibernate/Linq/NhLinqInsertExpression.cs +++ b/src/NHibernate/Linq/NhLinqInsertExpression.cs @@ -1,39 +1,31 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using NHibernate.Engine; -using NHibernate.Linq.Expressions; -using NHibernate.Linq.Visitors; -using Remotion.Linq; -using Remotion.Linq.Clauses.Expressions; -using System.Reflection; +using NHibernate.Util; namespace NHibernate.Linq { - public class NhLinqInsertExpression : NhLinqExpression + public class NhLinqInsertExpression : NhLinqExpression { - public NhLinqInsertExpression(Expression expression, Assignments assignments, ISessionFactoryImplementor sessionFactory) + protected override QueryMode QueryMode => QueryMode.Insert; + + /// + /// Entity type to insert or update when the expression is a DML. + /// + protected override System.Type TargetType => typeof(TTarget); + + public NhLinqInsertExpression(Expression expression, Assignments assignments, ISessionFactoryImplementor sessionFactory) : base(RewriteForInsert(expression, assignments), sessionFactory) { - Key = Key + "INSERT"; + Key = "INSERT " + Key; } - internal static Expression RewriteForInsert(Expression expression, Assignments assignments) + private static Expression RewriteForInsert(Expression expression, Assignments assignments) { var lambda = assignments.ConvertToDictionaryExpression(); - return - Expression.Call( - typeof(Queryable), "Select", - new System.Type[] { typeof(TInput), lambda.Body.Type }, - expression, Expression.Quote(lambda)); - } - - protected override ExpressionToHqlTranslationResults GenerateHqlQuery(QueryModel queryModel, VisitorParameters visitorParameters) - { - visitorParameters.EntityType = typeof (TOutput); - return QueryModelVisitor.GenerateHqlQuery(queryModel, visitorParameters, true, null, QueryMode.Insert); + return Expression.Call( + ReflectionCache.QueryableMethods.SelectDefinition.MakeGenericMethod(typeof(TSource), lambda.Body.Type), + expression, Expression.Quote(lambda)); } } } \ No newline at end of file diff --git a/src/NHibernate/Linq/NhLinqUpdateExpression.cs b/src/NHibernate/Linq/NhLinqUpdateExpression.cs index f19623b46e0..abc1cb43f62 100644 --- a/src/NHibernate/Linq/NhLinqUpdateExpression.cs +++ b/src/NHibernate/Linq/NhLinqUpdateExpression.cs @@ -1,43 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using NHibernate.Engine; -using NHibernate.Linq.Expressions; -using NHibernate.Linq.Visitors; -using Remotion.Linq; -using Remotion.Linq.Clauses.Expressions; +using NHibernate.Util; namespace NHibernate.Linq { public class NhLinqUpdateExpression : NhLinqExpression { + protected override QueryMode QueryMode => _versioned ? QueryMode.UpdateVersioned : QueryMode.Update; + + /// + /// Entity type to insert or update when the expression is a DML. + /// + protected override System.Type TargetType => typeof(T); + private readonly bool _versioned; - public NhLinqUpdateExpression(Expression expression, Assignments assignments, ISessionFactoryImplementor sessionFactory, bool versioned) + public NhLinqUpdateExpression(Expression expression, Assignments assignments, ISessionFactoryImplementor sessionFactory, bool versioned) : base(RewriteForUpdate(expression, assignments), sessionFactory) { _versioned = versioned; - Key = Key + "UPDATE" + versioned; - } - - protected override ExpressionToHqlTranslationResults GenerateHqlQuery(QueryModel queryModel, VisitorParameters visitorParameters) - { - visitorParameters.EntityType = typeof(T); - return QueryModelVisitor.GenerateHqlQuery(queryModel, visitorParameters, true, null, _versioned ? QueryMode.UpdateVersioned : QueryMode.Update); + Key = $"UPDATE {(versioned ? "VERSIONED " : "")}{Key}"; } - internal static Expression RewriteForUpdate(Expression expression, Assignments assignments) + private static Expression RewriteForUpdate(Expression expression, Assignments assignments) { var lambda = assignments.ConvertToDictionaryExpression(); - return - Expression.Call( - typeof(Queryable), "Select", - new System.Type[] { typeof(T), lambda.Body.Type }, - expression, Expression.Quote(lambda)); + return Expression.Call( + ReflectionCache.QueryableMethods.SelectDefinition.MakeGenericMethod(typeof(T), lambda.Body.Type), + expression, Expression.Quote(lambda)); } } - - } \ No newline at end of file diff --git a/src/NHibernate/Linq/QueryMode.cs b/src/NHibernate/Linq/QueryMode.cs index 15ed5842f81..2f3ce197ff1 100644 --- a/src/NHibernate/Linq/QueryMode.cs +++ b/src/NHibernate/Linq/QueryMode.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace NHibernate.Linq +namespace NHibernate.Linq { public enum QueryMode { diff --git a/src/NHibernate/Linq/UpdateSyntax.cs b/src/NHibernate/Linq/UpdateSyntax.cs index 53b2841d85c..29df337c764 100644 --- a/src/NHibernate/Linq/UpdateSyntax.cs +++ b/src/NHibernate/Linq/UpdateSyntax.cs @@ -3,6 +3,10 @@ namespace NHibernate.Linq { + /// + /// An update object on which values to update can be specified. + /// + /// The type of the entities to update. public class UpdateSyntax { private readonly Expression _sourceExpression; @@ -18,8 +22,8 @@ internal UpdateSyntax(Expression sourceExpression, INhQueryProvider provider) /// Specify the assignments and execute the update. /// /// The assignments. - /// if set to true [versioned]. - /// + /// If set to true [versioned]. + /// The number of updated entities. public int Assign(Action> assignments, bool versioned = false) { var u = new Assignments(); @@ -31,19 +35,16 @@ public int Assign(Action> assignments, bool versioned = false) /// /// Specify the assignments and execute the update. /// - /// - /// The query. - /// The assignments expressed as a member initialization, e.g. x => new Dog{Name = x.Name,Age = x.Age + 5}. - /// if set to true [versioned]. - /// + /// The assignments expressed as a member initialization, e.g. x => new Dog { Name = x.Name, Age = x.Age + 5 }. Unset members are ignored and left untouched. + /// If set to true [versioned]. + /// The number of updated entities. public int As(Expression> expression, bool versioned = false) { - var assignments = Assignments.FromExpression(expression); return ExecuteUpdate(versioned, assignments); } - private int ExecuteUpdate(bool versioned, Assignments assignments) + private int ExecuteUpdate(bool versioned, Assignments assignments) { return _provider.ExecuteUpdate(_sourceExpression, assignments, versioned); } diff --git a/src/NHibernate/Linq/Visitors/HqlGeneratorExpressionVisitor.cs b/src/NHibernate/Linq/Visitors/HqlGeneratorExpressionVisitor.cs index 2894e744315..cb4ec1cca66 100644 --- a/src/NHibernate/Linq/Visitors/HqlGeneratorExpressionVisitor.cs +++ b/src/NHibernate/Linq/Visitors/HqlGeneratorExpressionVisitor.cs @@ -545,7 +545,7 @@ protected HqlTreeNode VisitConditionalExpression(ConditionalExpression expressio protected HqlTreeNode VisitSubQueryExpression(SubQueryExpression expression) { - ExpressionToHqlTranslationResults query = QueryModelVisitor.GenerateHqlQuery(expression.QueryModel, _parameters, false, null, QueryMode.Select); + ExpressionToHqlTranslationResults query = QueryModelVisitor.GenerateHqlQuery(expression.QueryModel, _parameters, false, null); return query.Statement; } diff --git a/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs b/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs index c785ff1470f..0ff6bfea76e 100644 --- a/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs +++ b/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using NHibernate.Hql.Ast; @@ -26,7 +25,7 @@ public class QueryModelVisitor : NhQueryModelVisitorBase, INhQueryModelVisitor private readonly QueryMode _queryMode; public static ExpressionToHqlTranslationResults GenerateHqlQuery(QueryModel queryModel, VisitorParameters parameters, bool root, - NhLinqExpressionReturnType? rootReturnType, QueryMode queryMode) + NhLinqExpressionReturnType? rootReturnType) { NestedSelectRewriter.ReWrite(queryModel, parameters.SessionFactory); @@ -85,7 +84,7 @@ public static ExpressionToHqlTranslationResults GenerateHqlQuery(QueryModel quer // Identify and name query sources QuerySourceIdentifier.Visit(parameters.QuerySourceNamer, queryModel); - var visitor = new QueryModelVisitor(parameters, root, queryModel, rootReturnType, queryMode) + var visitor = new QueryModelVisitor(parameters, root, queryModel, rootReturnType) { RewrittenOperatorResult = result, }; @@ -135,13 +134,13 @@ static QueryModelVisitor() } private QueryModelVisitor(VisitorParameters visitorParameters, bool root, QueryModel queryModel, - NhLinqExpressionReturnType? rootReturnType, QueryMode queryMode) + NhLinqExpressionReturnType? rootReturnType) { - _queryMode = queryMode; + _queryMode = root ? visitorParameters.RootQueryMode : QueryMode.Select; VisitorParameters = visitorParameters; Model = queryModel; _rootReturnType = root ? rootReturnType : null; - _hqlTree = new IntermediateHqlTree(root, queryMode); + _hqlTree = new IntermediateHqlTree(root, _queryMode); } private void Visit() @@ -320,7 +319,6 @@ public override void VisitAdditionalFromClause(AdditionalFromClause fromClause, _hqlTree.TreeBuilder.Range( HqlGeneratorExpressionVisitor.Visit(fromClause.FromExpression, VisitorParameters), _hqlTree.TreeBuilder.Alias(querySourceName))); - } base.VisitAdditionalFromClause(fromClause, queryModel, index); @@ -382,15 +380,11 @@ public override void VisitSelectClause(SelectClause selectClause, QueryModel que return; case QueryMode.Update: case QueryMode.UpdateVersioned: - { - VisitUpdateClause(selectClause.Selector); - return; - } + VisitUpdateClause(selectClause.Selector); + return; case QueryMode.Insert: - { - VisitInsertClause(selectClause.Selector); - return; - } + VisitInsertClause(selectClause.Selector); + return; } //This is a standard select query @@ -411,33 +405,28 @@ public override void VisitSelectClause(SelectClause selectClause, QueryModel que private void VisitInsertClause(Expression expression) { - var listInit = expression as ListInitExpression; - var insertedType = VisitorParameters.EntityType; + var listInit = expression as ListInitExpression + ?? throw new QueryException("Malformed insert expression"); + var insertedType = VisitorParameters.TargetEntityType; var idents = new List(); var selectColumns = new List(); - if (listInit == null) - { - throw new QueryException("Malformed insert expression"); - } - //Extract the insert clause from the projected ListInit foreach (var assignment in listInit.Initializers) { - var member = assignment.Arguments[0] as ConstantExpression; + var member = (ConstantExpression)assignment.Arguments[0]; var value = assignment.Arguments[1]; - + //The target property idents.Add(_hqlTree.TreeBuilder.Ident((string)member.Value)); var valueHql = HqlGeneratorExpressionVisitor.Visit(value, VisitorParameters).AsExpression(); selectColumns.Add(valueHql); - }; + } //Add the insert clause ([INSERT INTO] insertedType (list of properties)) _hqlTree.AddInsertClause(_hqlTree.TreeBuilder.Ident(insertedType.FullName), - _hqlTree.TreeBuilder.Range(idents.ToArray())); - + _hqlTree.TreeBuilder.Range(idents.ToArray())); //... and then the select clause _hqlTree.AddSelectClause(_hqlTree.TreeBuilder.Select(selectColumns)); @@ -445,15 +434,16 @@ private void VisitInsertClause(Expression expression) private void VisitUpdateClause(Expression expression) { - var listInit = expression as ListInitExpression; + var listInit = expression as ListInitExpression + ?? throw new QueryException("Malformed update expression"); foreach (var initializer in listInit.Initializers) { - var member = initializer.Arguments[0] as ConstantExpression; + var member = (ConstantExpression)initializer.Arguments[0]; var setter = initializer.Arguments[1]; var setterHql = HqlGeneratorExpressionVisitor.Visit(setter, VisitorParameters).AsExpression(); _hqlTree.AddSet(_hqlTree.TreeBuilder.Equality(_hqlTree.TreeBuilder.Ident((string)member.Value), - setterHql)); + setterHql)); } } diff --git a/src/NHibernate/Linq/Visitors/VisitorParameters.cs b/src/NHibernate/Linq/Visitors/VisitorParameters.cs index 30578db83db..a4d2a1f2c65 100644 --- a/src/NHibernate/Linq/Visitors/VisitorParameters.cs +++ b/src/NHibernate/Linq/Visitors/VisitorParameters.cs @@ -16,18 +16,27 @@ public class VisitorParameters public QuerySourceNamer QuerySourceNamer { get; set; } - public System.Type EntityType { get; set; } + /// + /// Entity type to insert or update when the operation is a DML. + /// + public System.Type TargetEntityType { get; } + + public QueryMode RootQueryMode { get; } public VisitorParameters( ISessionFactoryImplementor sessionFactory, IDictionary constantToParameterMap, List requiredHqlParameters, - QuerySourceNamer querySourceNamer) + QuerySourceNamer querySourceNamer, + System.Type targetEntityType, + QueryMode rootQueryMode) { SessionFactory = sessionFactory; ConstantToParameterMap = constantToParameterMap; RequiredHqlParameters = requiredHqlParameters; QuerySourceNamer = querySourceNamer; + TargetEntityType = targetEntityType; + RootQueryMode = rootQueryMode; } } } \ No newline at end of file diff --git a/src/NHibernate/Util/ReflectionCache.cs b/src/NHibernate/Util/ReflectionCache.cs index 34f79479c80..8fae13abe83 100644 --- a/src/NHibernate/Util/ReflectionCache.cs +++ b/src/NHibernate/Util/ReflectionCache.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Reflection; namespace NHibernate.Util @@ -62,6 +63,12 @@ internal static class MethodBaseMethods ReflectHelper.GetMethod(() => MethodBase.GetMethodFromHandle(default(RuntimeMethodHandle), default(RuntimeTypeHandle))); } + internal static class QueryableMethods + { + internal static readonly MethodInfo SelectDefinition = + ReflectHelper.GetMethodDefinition(() => Queryable.Select(null, default(Expression>))); + } + internal static class TypeMethods { internal static readonly MethodInfo GetTypeFromHandle = From c32732ef22ef3dcec2c1433b52eb7fa8ad4d0bb2 Mon Sep 17 00:00:00 2001 From: Alexander Zaytsev Date: Fri, 23 Jun 2017 17:11:25 +1200 Subject: [PATCH 3/7] NH-3488 - Remove code duplication in DML queries --- doc/reference/modules/query_linq.xml | 6 +- .../LinqBulkManipulation/Fixture.cs | 4 +- src/NHibernate/Linq/Assignment.cs | 25 ---- src/NHibernate/Linq/Assignments.cs | 99 +-------------- src/NHibernate/Linq/DefaultQueryProvider.cs | 32 +---- src/NHibernate/Linq/DmlExpressionRewriter.cs | 115 ++++++++++++++++++ src/NHibernate/Linq/InsertSyntax.cs | 10 +- src/NHibernate/Linq/LinqExtensionMethods.cs | 17 ++- src/NHibernate/Linq/NhLinqDeleteExpression.cs | 16 --- src/NHibernate/Linq/NhLinqDmlExpression.cs | 22 ++++ src/NHibernate/Linq/NhLinqExpression.cs | 2 +- src/NHibernate/Linq/NhLinqInsertExpression.cs | 31 ----- src/NHibernate/Linq/NhLinqUpdateExpression.cs | 34 ------ src/NHibernate/Linq/UpdateSyntax.cs | 30 ++--- 14 files changed, 189 insertions(+), 254 deletions(-) delete mode 100644 src/NHibernate/Linq/Assignment.cs create mode 100644 src/NHibernate/Linq/DmlExpressionRewriter.cs delete mode 100644 src/NHibernate/Linq/NhLinqDeleteExpression.cs create mode 100644 src/NHibernate/Linq/NhLinqDmlExpression.cs delete mode 100644 src/NHibernate/Linq/NhLinqInsertExpression.cs delete mode 100644 src/NHibernate/Linq/NhLinqUpdateExpression.cs diff --git a/doc/reference/modules/query_linq.xml b/doc/reference/modules/query_linq.xml index a8d4016facc..bdc9f44fd0f 100644 --- a/doc/reference/modules/query_linq.xml +++ b/doc/reference/modules/query_linq.xml @@ -544,9 +544,9 @@ IList oldCats = In both cases, unspecified properties are not included in the resulting SQL update. This could be changed for version and timestamp properties: - As and Assign methods take an optional boolean parameter, - versioned, which allows incrementing the version. Custom version types - (NHibernate.Usertype.IUserVersionType) are not supported. + using UpdateVersioned instead of Update allows incrementing + the version. Custom version types (NHibernate.Usertype.IUserVersionType) are + not supported. diff --git a/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs b/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs index 63ca88c16d8..740f67e6d62 100644 --- a/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs @@ -542,7 +542,7 @@ public void IncrementCounterVersion() // Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26 var count = s.Query() - .Update().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd"), true); + .UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd")); Assert.That(count, Is.EqualTo(1), "incorrect exec count"); t.Commit(); } @@ -571,7 +571,7 @@ public void IncrementTimestampVersion() { // Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26 var count = s.Query() - .Update().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd"), true); + .UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd")); Assert.That(count, Is.EqualTo(1), "incorrect exec count"); t.Commit(); } diff --git a/src/NHibernate/Linq/Assignment.cs b/src/NHibernate/Linq/Assignment.cs deleted file mode 100644 index 471c7b24a08..00000000000 --- a/src/NHibernate/Linq/Assignment.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq.Expressions; - -namespace NHibernate.Linq -{ - /// - /// Specifies one assignment in an update or insert query - /// - public class Assignment - { - /// - /// The assigned property. - /// - public string PropertyPath { get; set; } - /// - /// The value to assign. - /// - public Expression Expression { get; set; } - - public Assignment(string propertyPath, Expression expression) - { - PropertyPath = propertyPath; - Expression = expression; - } - } -} \ No newline at end of file diff --git a/src/NHibernate/Linq/Assignments.cs b/src/NHibernate/Linq/Assignments.cs index dca211b12ac..cd8ee07bebf 100644 --- a/src/NHibernate/Linq/Assignments.cs +++ b/src/NHibernate/Linq/Assignments.cs @@ -1,28 +1,21 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using System.Linq.Expressions; -using System.Reflection; using NHibernate.Linq.Visitors; -using NHibernate.Util; namespace NHibernate.Linq { - public abstract class Assignments - { - protected static readonly ConstructorInfo DictionaryConstructorInfo = typeof(Dictionary).GetConstructor(new[] { typeof(int) }); - protected static readonly MethodInfo DictionaryAddMethodInfo = ReflectHelper.GetMethod>(d => d.Add(null, null)); - } - /// /// Class to hold assignments used in updates and inserts. /// /// The type of the entity source of the insert or to update. /// The type of the entity to insert or to update. - public class Assignments : Assignments + public class Assignments { - private readonly List _sets = new List(); + private readonly Dictionary _assignments = new Dictionary(); + + internal IReadOnlyDictionary List => _assignments; /// /// Sets the specified property. @@ -36,7 +29,7 @@ public Assignments Set(Expression> if (expression == null) throw new ArgumentNullException(nameof(expression)); var member = GetMemberExpression(property); - _sets.Add(new Assignment(member.GetMemberPath(), expression)); + _assignments.Add(member.GetMemberPath(), expression); return this; } @@ -50,7 +43,7 @@ public Assignments Set(Expression> public Assignments Set(Expression> property, TProp value) { var member = GetMemberExpression(property); - _sets.Add(new Assignment(member.GetMemberPath(), Expression.Constant(value, typeof(TProp)))); + _assignments.Add(member.GetMemberPath(), Expression.Constant(value, typeof(TProp))); return this; } @@ -63,85 +56,5 @@ private static MemberExpression GetMemberExpression(Expression - /// Converts the assignments into a lambda expression, which creates a Dictionary<string,object%gt;. - /// - /// A lambda expression representing the assignments. - public LambdaExpression ConvertToDictionaryExpression() - { - var param = Expression.Parameter(typeof(TSource)); - var inits = new List(); - foreach (var set in _sets) - { - var setter = set.Expression; - if (setter is LambdaExpression setterLambda) - { - setter = setterLambda.Body.Replace(setterLambda.Parameters.First(), param); - } - inits.Add(Expression.ElementInit(DictionaryAddMethodInfo, Expression.Constant(set.PropertyPath), - Expression.Convert(setter, typeof(object)))); - } - - //The ListInit is intentionally "infected" with the lambda parameter (param), in the form of an IIF. - //The only relevance is to make sure that the ListInit is not evaluated by the PartialEvaluatingExpressionTreeVisitor, - //which could turn it into a Constant - var listInit = Expression.ListInit( - Expression.New( - DictionaryConstructorInfo, - Expression.Condition( - Expression.Equal(param, Expression.Constant(null, typeof(TSource))), - Expression.Constant(_sets.Count), - Expression.Constant(_sets.Count))), - inits); - - return Expression.Lambda(listInit, param); - } - - /// - /// Converts a members initialization expression to assignments. Unset members are ignored and left untouched. - /// - /// The expression to convert. - /// The corresponding assignments. - public static Assignments FromExpression(Expression> expression) - { - if (expression == null) - throw new ArgumentNullException(nameof(expression)); - var instance = new Assignments(); - var memberInitExpression = expression.Body as MemberInitExpression ?? - throw new ArgumentException("The expression must be member initialization, e.g. x => new Dog { Name = x.Name, Age = x.Age + 5 }"); - - AddSetsFromBindings(memberInitExpression.Bindings, instance, "", expression.Parameters); - - return instance; - } - - private static void AddSetsFromBindings(IEnumerable bindings, Assignments instance, string path, ReadOnlyCollection parameters) - { - foreach (var binding in bindings) - { - if (binding.BindingType == MemberBindingType.Assignment) // {Property="Value"} - { - AddSetsFromAssignment((MemberAssignment)binding, instance, path + "." + binding.Member.Name, parameters); - } - else if (binding.BindingType == MemberBindingType.MemberBinding) // {Property={SubProperty="Value}} - { - AddSetsFromBindings(((MemberMemberBinding)binding).Bindings, instance, path + "." + binding.Member.Name, parameters); - } - } - } - - private static void AddSetsFromAssignment(MemberAssignment assignment, Assignments instance, string path, ReadOnlyCollection parameters) - { - // {Property=new Instance{SubProperty="Value"}} - if (assignment.Expression is MemberInitExpression memberInit) - { - AddSetsFromBindings(memberInit.Bindings, instance, path, parameters); - } - else - { - instance._sets.Add(new Assignment(path.Substring(1), Expression.Lambda(assignment.Expression, parameters))); - } - } } } \ No newline at end of file diff --git a/src/NHibernate/Linq/DefaultQueryProvider.cs b/src/NHibernate/Linq/DefaultQueryProvider.cs index 0878f94c0ca..b650a547933 100644 --- a/src/NHibernate/Linq/DefaultQueryProvider.cs +++ b/src/NHibernate/Linq/DefaultQueryProvider.cs @@ -16,9 +16,7 @@ public interface INhQueryProvider : IQueryProvider IEnumerable ExecuteFuture(Expression expression); IFutureValue ExecuteFutureValue(Expression expression); void SetResultTransformerAndAdditionalCriteria(IQuery query, NhLinqExpression nhExpression, IDictionary> parameters); - int ExecuteDelete(Expression predicate); - int ExecuteUpdate(Expression expression, Assignments assignments, bool versioned); - int ExecuteInsert(Expression expression, Assignments assignments); + int ExecuteDml(QueryMode queryMode, Expression expression); } public class DefaultQueryProvider : INhQueryProvider @@ -174,31 +172,9 @@ public virtual void SetResultTransformerAndAdditionalCriteria(IQuery query, NhLi } } - public int ExecuteDelete(Expression predicate) + public int ExecuteDml(QueryMode queryMode, Expression expression) { - var nhLinqExpression = new NhLinqDeleteExpression(predicate, Session.Factory); - - var query = Session.CreateQuery(nhLinqExpression); - - SetParameters(query, nhLinqExpression.ParameterValuesByName); - - return query.ExecuteUpdate(); - } - - public int ExecuteUpdate(Expression expression, Assignments assignments, bool versioned) - { - var nhLinqExpression = new NhLinqUpdateExpression(expression, assignments, Session.Factory, versioned); - - var query = Session.CreateQuery(nhLinqExpression); - - SetParameters(query, nhLinqExpression.ParameterValuesByName); - - return query.ExecuteUpdate(); - } - - public int ExecuteInsert(Expression expression, Assignments assignments) - { - var nhLinqExpression = new NhLinqInsertExpression(expression, assignments, Session.Factory); + var nhLinqExpression = new NhLinqDmlExpression(queryMode, expression, Session.Factory); var query = Session.CreateQuery(nhLinqExpression); @@ -207,4 +183,4 @@ public int ExecuteInsert(Expression expression, Assignments).GetConstructor(new[] {typeof(int)}); + + static readonly MethodInfo DictionaryAddMethodInfo = ReflectHelper.GetMethod>(d => d.Add(null, null)); + + readonly IReadOnlyCollection _parameters; + readonly Dictionary _assignments = new Dictionary(); + + DmlExpressionRewriter(IReadOnlyCollection parameters) + { + _parameters = parameters; + } + + void AddSettersFromBindings(IEnumerable bindings, string path) + { + foreach (var node in bindings) + switch (node.BindingType) + { + case MemberBindingType.Assignment: + AddSettersFromAssignment((MemberAssignment) node, path + "." + node.Member.Name); + break; + case MemberBindingType.MemberBinding: + AddSettersFromBindings(((MemberMemberBinding) node).Bindings, path + "." + node.Member.Name); + break; + default: + throw new InvalidOperationException($"{node.BindingType} is not supported"); + } + } + + void AddSettersFromAssignment(MemberAssignment assignment, string path) + { + // {Property=new Instance{SubProperty="Value"}} + if (assignment.Expression is MemberInitExpression memberInit) + AddSettersFromBindings(memberInit.Bindings, path); + else + _assignments.Add(path.Substring(1), Expression.Lambda(assignment.Expression, _parameters)); + } + + /// + /// Converts the assignments into a lambda expression, which creates a Dictionary<string,object%gt;. + /// + /// + /// A lambda expression representing the assignments. + static LambdaExpression ConvertAssignmentsToDictionaryExpression(IReadOnlyDictionary assignments) + { + var param = Expression.Parameter(typeof(TSource)); + var inits = new List(); + foreach (var set in assignments) + { + var setter = set.Value; + if (setter is LambdaExpression setterLambda) + setter = setterLambda.Body.Replace(setterLambda.Parameters.First(), param); + inits.Add( + Expression.ElementInit( + DictionaryAddMethodInfo, + Expression.Constant(set.Key), + Expression.Convert(setter, typeof(object)))); + } + + //The ListInit is intentionally "infected" with the lambda parameter (param), in the form of an IIF. + //The only relevance is to make sure that the ListInit is not evaluated by the PartialEvaluatingExpressionTreeVisitor, + //which could turn it into a Constant + var listInit = Expression.ListInit( + Expression.New( + DictionaryConstructorInfo, + Expression.Condition( + Expression.Equal(param, Expression.Constant(null, typeof(TSource))), + Expression.Constant(assignments.Count), + Expression.Constant(assignments.Count))), + inits); + + return Expression.Lambda(listInit, param); + } + + public static Expression PrepareExpression(Expression sourceExpression, Expression> expression) + { + if (expression == null) + throw new ArgumentNullException(nameof(expression)); + + var memberInitExpression = expression.Body as MemberInitExpression ?? + throw new ArgumentException("The expression must be member initialization, e.g. x => new Dog { Name = x.Name, Age = x.Age + 5 }"); + + var assignments = ExtractAssignments(expression, memberInitExpression); + return PrepareExpression(sourceExpression, assignments); + } + + public static Expression PrepareExpression(Expression sourceExpression, IReadOnlyDictionary assignments) + { + var lambda = ConvertAssignmentsToDictionaryExpression(assignments); + + return Expression.Call( + ReflectionCache.QueryableMethods.SelectDefinition.MakeGenericMethod(typeof(TSource), lambda.Body.Type), + sourceExpression, + Expression.Quote(lambda)); + } + + static Dictionary ExtractAssignments(Expression> expression, MemberInitExpression memberInitExpression) + { + var instance = new DmlExpressionRewriter(expression.Parameters); + instance.AddSettersFromBindings(memberInitExpression.Bindings, ""); + return instance._assignments; + } + } +} diff --git a/src/NHibernate/Linq/InsertSyntax.cs b/src/NHibernate/Linq/InsertSyntax.cs index da16edd0c17..9f0db3f43bd 100644 --- a/src/NHibernate/Linq/InsertSyntax.cs +++ b/src/NHibernate/Linq/InsertSyntax.cs @@ -30,7 +30,8 @@ public int Into(Action> assignmentActions throw new ArgumentNullException(nameof(assignmentActions)); var assignments = new Assignments(); assignmentActions.Invoke(assignments); - return InsertInto(assignments); + + return ExecuteInsert(DmlExpressionRewriter.PrepareExpression(_sourceExpression, assignments.List)); } /// @@ -41,13 +42,12 @@ public int Into(Action> assignmentActions /// The number of inserted entities. public int As(Expression> expression) { - var assignments = Assignments.FromExpression(expression); - return InsertInto(assignments); + return ExecuteInsert(DmlExpressionRewriter.PrepareExpression(_sourceExpression, expression)); } - internal int InsertInto(Assignments assignments) + private int ExecuteInsert(Expression insertExpression) { - return _provider.ExecuteInsert(_sourceExpression, assignments); + return _provider.ExecuteDml(QueryMode.Insert, insertExpression); } } } \ No newline at end of file diff --git a/src/NHibernate/Linq/LinqExtensionMethods.cs b/src/NHibernate/Linq/LinqExtensionMethods.cs index 6548a1d039d..0b0d2d75363 100644 --- a/src/NHibernate/Linq/LinqExtensionMethods.cs +++ b/src/NHibernate/Linq/LinqExtensionMethods.cs @@ -131,7 +131,7 @@ public static IQueryable Timeout(this IQueryable query, int timeout) public static int Delete(this IQueryable source) { var provider = GetNhProvider(source); - return provider.ExecuteDelete(source.Expression); + return provider.ExecuteDml(QueryMode.Delete, source.Expression); } /// @@ -143,7 +143,20 @@ public static int Delete(this IQueryable source) public static UpdateSyntax Update(this IQueryable source) { var provider = GetNhProvider(source); - return new UpdateSyntax(source.Expression, provider); + return new UpdateSyntax(source.Expression, provider, false); + } + + /// + /// Initiate a update versioned for the entities selected by the query. The update operation + /// will be performed in the database without reading the entities out of it. + /// + /// The type of the elements of . + /// The query matching the entities to update. + /// An update builder, allowing to specify assignments to the entities properties. + public static UpdateSyntax UpdateVersioned(this IQueryable source) + { + var provider = GetNhProvider(source); + return new UpdateSyntax(source.Expression, provider, true); } /// diff --git a/src/NHibernate/Linq/NhLinqDeleteExpression.cs b/src/NHibernate/Linq/NhLinqDeleteExpression.cs deleted file mode 100644 index 91eb443d1ba..00000000000 --- a/src/NHibernate/Linq/NhLinqDeleteExpression.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Linq.Expressions; -using NHibernate.Engine; - -namespace NHibernate.Linq -{ - public class NhLinqDeleteExpression : NhLinqExpression - { - protected override QueryMode QueryMode => QueryMode.Delete; - - public NhLinqDeleteExpression(Expression expression, ISessionFactoryImplementor sessionFactory) - : base(expression, sessionFactory) - { - Key = "DELETE " + Key; - } - } -} \ No newline at end of file diff --git a/src/NHibernate/Linq/NhLinqDmlExpression.cs b/src/NHibernate/Linq/NhLinqDmlExpression.cs new file mode 100644 index 00000000000..1c5bd7bb20c --- /dev/null +++ b/src/NHibernate/Linq/NhLinqDmlExpression.cs @@ -0,0 +1,22 @@ +using System.Linq.Expressions; +using NHibernate.Engine; + +namespace NHibernate.Linq +{ + public class NhLinqDmlExpression : NhLinqExpression + { + protected override QueryMode QueryMode { get; } + + /// + /// Entity type to insert or update when the expression is a DML. + /// + protected override System.Type TargetType => typeof(T); + + public NhLinqDmlExpression(QueryMode queryMode, Expression expression, ISessionFactoryImplementor sessionFactory) + : base(expression, sessionFactory) + { + Key = $"{queryMode.ToString().ToUpperInvariant()} {Key}"; + QueryMode = queryMode; + } + } +} diff --git a/src/NHibernate/Linq/NhLinqExpression.cs b/src/NHibernate/Linq/NhLinqExpression.cs index 566a40745dc..fbca270693f 100644 --- a/src/NHibernate/Linq/NhLinqExpression.cs +++ b/src/NHibernate/Linq/NhLinqExpression.cs @@ -32,7 +32,7 @@ public class NhLinqExpression : IQueryExpression protected virtual QueryMode QueryMode => QueryMode.Select; - private Expression _expression; + private readonly Expression _expression; private readonly IDictionary _constantToParameterMap; public NhLinqExpression(Expression expression, ISessionFactoryImplementor sessionFactory) diff --git a/src/NHibernate/Linq/NhLinqInsertExpression.cs b/src/NHibernate/Linq/NhLinqInsertExpression.cs deleted file mode 100644 index 1bb0ad8b712..00000000000 --- a/src/NHibernate/Linq/NhLinqInsertExpression.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Linq.Expressions; -using NHibernate.Engine; -using NHibernate.Util; - -namespace NHibernate.Linq -{ - public class NhLinqInsertExpression : NhLinqExpression - { - protected override QueryMode QueryMode => QueryMode.Insert; - - /// - /// Entity type to insert or update when the expression is a DML. - /// - protected override System.Type TargetType => typeof(TTarget); - - public NhLinqInsertExpression(Expression expression, Assignments assignments, ISessionFactoryImplementor sessionFactory) - : base(RewriteForInsert(expression, assignments), sessionFactory) - { - Key = "INSERT " + Key; - } - - private static Expression RewriteForInsert(Expression expression, Assignments assignments) - { - var lambda = assignments.ConvertToDictionaryExpression(); - - return Expression.Call( - ReflectionCache.QueryableMethods.SelectDefinition.MakeGenericMethod(typeof(TSource), lambda.Body.Type), - expression, Expression.Quote(lambda)); - } - } -} \ No newline at end of file diff --git a/src/NHibernate/Linq/NhLinqUpdateExpression.cs b/src/NHibernate/Linq/NhLinqUpdateExpression.cs deleted file mode 100644 index abc1cb43f62..00000000000 --- a/src/NHibernate/Linq/NhLinqUpdateExpression.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Linq.Expressions; -using NHibernate.Engine; -using NHibernate.Util; - -namespace NHibernate.Linq -{ - public class NhLinqUpdateExpression : NhLinqExpression - { - protected override QueryMode QueryMode => _versioned ? QueryMode.UpdateVersioned : QueryMode.Update; - - /// - /// Entity type to insert or update when the expression is a DML. - /// - protected override System.Type TargetType => typeof(T); - - private readonly bool _versioned; - - public NhLinqUpdateExpression(Expression expression, Assignments assignments, ISessionFactoryImplementor sessionFactory, bool versioned) - : base(RewriteForUpdate(expression, assignments), sessionFactory) - { - _versioned = versioned; - Key = $"UPDATE {(versioned ? "VERSIONED " : "")}{Key}"; - } - - private static Expression RewriteForUpdate(Expression expression, Assignments assignments) - { - var lambda = assignments.ConvertToDictionaryExpression(); - - return Expression.Call( - ReflectionCache.QueryableMethods.SelectDefinition.MakeGenericMethod(typeof(T), lambda.Body.Type), - expression, Expression.Quote(lambda)); - } - } -} \ No newline at end of file diff --git a/src/NHibernate/Linq/UpdateSyntax.cs b/src/NHibernate/Linq/UpdateSyntax.cs index 29df337c764..1ff1a99af6d 100644 --- a/src/NHibernate/Linq/UpdateSyntax.cs +++ b/src/NHibernate/Linq/UpdateSyntax.cs @@ -11,42 +11,44 @@ public class UpdateSyntax { private readonly Expression _sourceExpression; private readonly INhQueryProvider _provider; + private readonly bool _versioned; - internal UpdateSyntax(Expression sourceExpression, INhQueryProvider provider) + internal UpdateSyntax(Expression sourceExpression, INhQueryProvider provider, bool versioned) { _sourceExpression = sourceExpression; _provider = provider; + _versioned = versioned; } /// /// Specify the assignments and execute the update. /// - /// The assignments. - /// If set to true [versioned]. + /// The assignments. /// The number of updated entities. - public int Assign(Action> assignments, bool versioned = false) + public int Assign(Action> assignmentActions) { - var u = new Assignments(); - assignments.Invoke(u); + if (assignmentActions == null) + throw new ArgumentNullException(nameof(assignmentActions)); + var assignments = new Assignments(); + assignmentActions.Invoke(assignments); - return ExecuteUpdate(versioned, u); + return ExecuteUpdate(DmlExpressionRewriter.PrepareExpression(_sourceExpression, assignments.List)); } /// /// Specify the assignments and execute the update. /// - /// The assignments expressed as a member initialization, e.g. x => new Dog { Name = x.Name, Age = x.Age + 5 }. Unset members are ignored and left untouched. - /// If set to true [versioned]. + /// The assignments expressed as a member initialization, e.g. + /// x => new Dog { Name = x.Name, Age = x.Age + 5 }. Unset members are ignored and left untouched. /// The number of updated entities. - public int As(Expression> expression, bool versioned = false) + public int As(Expression> expression) { - var assignments = Assignments.FromExpression(expression); - return ExecuteUpdate(versioned, assignments); + return ExecuteUpdate(DmlExpressionRewriter.PrepareExpression(_sourceExpression, expression)); } - private int ExecuteUpdate(bool versioned, Assignments assignments) + private int ExecuteUpdate(Expression updateExpression) { - return _provider.ExecuteUpdate(_sourceExpression, assignments, versioned); + return _provider.ExecuteDml(_versioned ? QueryMode.UpdateVersioned : QueryMode.Update, updateExpression); } } } \ No newline at end of file From 0530838b3cea2c54d6c823911109c97149ae8b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= Date: Mon, 10 Jul 2017 13:39:57 +0200 Subject: [PATCH 4/7] NH-3488 - Protect against invalid use cases. --- .../LinqBulkManipulation/Fixture.cs | 26 +++++++++++++++++++ src/NHibernate/Linq/DmlExpressionRewriter.cs | 2 ++ .../Linq/Visitors/QueryModelVisitor.cs | 13 ++++++++++ 3 files changed, 41 insertions(+) diff --git a/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs b/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs index 740f67e6d62..74af87ff5a7 100644 --- a/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs @@ -877,6 +877,19 @@ public void UpdateSetNullOnJoinedSubclass() } } + [Test] + public void UpdateOnOtherClassThrows() + { + using (var s = OpenSession()) + using (s.BeginTransaction()) + { + var query = s + .Query().Where(x => x.Mother == _butterfly) + .Update(); + Assert.That(() => query.As(a => new Human { Description = a.Description + " humanized" }), Throws.TypeOf()); + } + } + #endregion #region DELETES @@ -1102,6 +1115,19 @@ public void DeleteSyntaxWithCompositeId() } } + [Test] + public void DeleteOnProjectionThrows() + { + using (var s = OpenSession()) + using (s.BeginTransaction()) + { + var query = s + .Query().Where(x => x.Mother == _butterfly) + .Select(x => new Car { Id = x.Id }); + Assert.That(() => query.Delete(), Throws.InvalidOperationException); + } + } + #endregion } } \ No newline at end of file diff --git a/src/NHibernate/Linq/DmlExpressionRewriter.cs b/src/NHibernate/Linq/DmlExpressionRewriter.cs index 888a676612a..06ffc1a1953 100644 --- a/src/NHibernate/Linq/DmlExpressionRewriter.cs +++ b/src/NHibernate/Linq/DmlExpressionRewriter.cs @@ -107,6 +107,8 @@ public static Expression PrepareExpression(Expression sourceExpression, static Dictionary ExtractAssignments(Expression> expression, MemberInitExpression memberInitExpression) { + if (memberInitExpression.Type != typeof(TTarget)) + throw new TypeMismatchException($"Expecting an expression of exact type {typeof(TTarget).AssemblyQualifiedName} but got {memberInitExpression.Type.AssemblyQualifiedName}"); var instance = new DmlExpressionRewriter(expression.Parameters); instance.AddSettersFromBindings(memberInitExpression.Bindings, ""); return instance._assignments; diff --git a/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs b/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs index 0ff6bfea76e..b28455363f1 100644 --- a/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs +++ b/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs @@ -377,6 +377,7 @@ public override void VisitSelectClause(SelectClause selectClause, QueryModel que switch (_queryMode) { case QueryMode.Delete: + VisitDeleteClause(selectClause.Selector); return; case QueryMode.Update: case QueryMode.UpdateVersioned: @@ -447,6 +448,18 @@ private void VisitUpdateClause(Expression expression) } } + private void VisitDeleteClause(Expression expression) + { + // We only need to check there is no unexpected select, for avoiding silently ignoring them. + var visitor = new SelectClauseVisitor(typeof(object[]), VisitorParameters); + visitor.VisitSelector(expression); + + if (visitor.ProjectionExpression != null) + { + throw new InvalidOperationException("Delete is not allowed on projections."); + } + } + public override void VisitWhereClause(WhereClause whereClause, QueryModel queryModel, int index) { var visitor = new SimplifyConditionalVisitor(); From 62cf307bd7a1f4b00a5e9a432d96c50069538bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= Date: Tue, 18 Jul 2017 23:08:46 +0200 Subject: [PATCH 5/7] NH-3488 - Anonymous selector for Linq insert/update --- doc/reference/modules/query_linq.xml | 24 ++- .../LinqBulkManipulation/Fixture.cs | 144 +++++++++++++----- src/NHibernate/Linq/DmlExpressionRewriter.cs | 73 +++++++-- src/NHibernate/Linq/InsertSyntax.cs | 12 ++ src/NHibernate/Linq/UpdateSyntax.cs | 11 ++ 5 files changed, 206 insertions(+), 58 deletions(-) diff --git a/doc/reference/modules/query_linq.xml b/doc/reference/modules/query_linq.xml index bdc9f44fd0f..4b7265c4678 100644 --- a/doc/reference/modules/query_linq.xml +++ b/doc/reference/modules/query_linq.xml @@ -474,7 +474,7 @@ IList oldCats = Beginning with NHibernate 5.0, Linq queries can be used for inserting, updating or deleting entities. The query defines the data to delete, update or insert, and then Delete, Update and Insert queryable extension methods allow to delete it, - or instruct in which way it should updated or inserted. Those queries happen entirely inside the + or instruct in which way it should be updated or inserted. Those queries happen entirely inside the database, without extracting corresponding entities out of the database. @@ -487,7 +487,7 @@ IList oldCats = Insert method extension expects a NHibernate queryable defining the data source of the insert. This data can be entities or a projection. Then it allows specifying the target entity type - to insert, and how to convert source data to those target entities. Two forms of target specification + to insert, and how to convert source data to those target entities. Three forms of target specification exist. @@ -497,6 +497,13 @@ IList oldCats = .Where(c => c.BodyWeight > 20) .Insert() .As(c => new Dog { Name = c.Name + "dog", BodyWeight = c.BodyWeight });]]> + + Projections can be done with an anonymous object too, but it requires supplying explicitly the target type: + + () + .Where(c => c.BodyWeight > 20) + .Insert() + .As(c => new { Name = c.Name + "dog", BodyWeight = c.BodyWeight });]]> Or using assignments: @@ -507,7 +514,7 @@ IList oldCats = .Set(d => d.Name, c => c.Name + "dog") .Set(d => d.BodyWeight, c => c.BodyWeight));]]> - In both cases, unspecified properties are not included in the resulting SQL insert. + In all cases, unspecified properties are not included in the resulting SQL insert. version and timestamp properties are exceptions. If not specified, they are inserted with their seed value. @@ -523,7 +530,7 @@ IList oldCats = Update method extension expects a queryable defining the entities to update. Then it allows specifying which properties should be updated with which values. As for - Insert, two forms of target specification exist. + Insert, three forms of target specification exist. Using projection to updated entity: @@ -532,6 +539,13 @@ IList oldCats = .Where(c => c.BodyWeight > 20) .Update() .As(c => new Cat { BodyWeight = c.BodyWeight / 2 });]]> + + Projections can be done with an anonymous object too: + + () + .Where(c => c.BodyWeight > 20) + .Update() + .As(c => new { BodyWeight = c.BodyWeight / 2 });]]> Or using assignments: @@ -541,7 +555,7 @@ IList oldCats = .Assign(a => a .Set(c => c.BodyWeight, c => c.BodyWeight / 2));]]> - In both cases, unspecified properties are not included in the resulting SQL update. This could + In all cases, unspecified properties are not included in the resulting SQL update. This could be changed for version and timestamp properties: using UpdateVersioned instead of Update allows incrementing diff --git a/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs b/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs index 74af87ff5a7..2779b85e175 100644 --- a/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs @@ -187,7 +187,20 @@ public void SimpleInsert() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query().Insert().As(x => new Pickup { Id = x.Id, Vin = x.Vin, Owner = x.Owner }); + var count = s.Query().Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner }); + Assert.AreEqual(1, count); + + t.Commit(); + } + } + + [Test] + public void SimpleAnonymousInsert() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query().Insert().As(x => new { Id = -x.Id, x.Vin, x.Owner }); Assert.AreEqual(1, count); t.Commit(); @@ -200,10 +213,11 @@ public void SimpleInsertFromAggregate() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query() + var count = s + .Query() .GroupBy(x => x.Id) .Select(x => new { Id = x.Key, Vin = x.Max(y => y.Vin), Owner = x.Max(y => y.Owner) }) - .Insert().As(x => new Pickup { Id = x.Id, Vin = x.Vin, Owner = x.Owner }); + .Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner }); Assert.AreEqual(1, count); t.Commit(); @@ -216,7 +230,8 @@ public void SimpleInsertFromLimited() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query() + var count = s + .Query() .Skip(1) .Take(1) .Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner }); @@ -232,8 +247,9 @@ public void SimpleInsertWithConstants() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query() - .Insert().Into(x => x.Set(y => y.Id, y => y.Id).Set(y => y.Vin, y => y.Vin).Set(y => y.Owner, "The owner")); + var count = s + .Query() + .Insert().Into(x => x.Set(y => y.Id, y => -y.Id).Set(y => y.Vin, y => y.Vin).Set(y => y.Owner, "The owner")); Assert.AreEqual(1, count); t.Commit(); @@ -246,9 +262,10 @@ public void SimpleInsertFromProjection() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query() + var count = s + .Query() .Select(x => new { x.Id, x.Owner, UpperOwner = x.Owner.ToUpper() }) - .Insert().Into(x => x.Set(y => y.Id, y => y.Id).Set(y => y.Vin, y => y.UpperOwner)); + .Insert().Into(x => x.Set(y => y.Id, y => -y.Id).Set(y => y.Vin, y => y.UpperOwner)); Assert.AreEqual(1, count); t.Commit(); @@ -261,9 +278,10 @@ public void InsertWithClientSideRequirementsThrowsException() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - Assert.Throws(() => - s.Query() - .Insert().As(x => new Pickup { Id = x.Id, Vin = x.Vin, Owner = x.Owner.PadRight(200) })); + Assert.Throws( + () => s + .Query() + .Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner.PadRight(200) })); t.Commit(); } @@ -277,7 +295,8 @@ public void InsertWithManyToOne() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query() + var count = s + .Query() .Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = x.Mother }); Assert.AreEqual(3, count); @@ -293,7 +312,8 @@ public void InsertWithManyToOneAsParameter() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query() + var count = s + .Query() .Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = _butterfly }); Assert.AreEqual(3, count); @@ -309,7 +329,8 @@ public void InsertWithManyToOneWithCompositeKey() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query() + var count = s + .Query() .Insert().As(x => new EntityReferencingEntityWithCrazyCompositeKey { Name = "Child", Parent = x }); Assert.AreEqual(1, count); @@ -324,7 +345,7 @@ public void InsertIntoSuperclassPropertiesFails() using (var t = s.BeginTransaction()) { Assert.Throws( - () => s.Query().Insert().As(x => new Human { Id = x.Id, BodyWeight = x.BodyWeight }), + () => s.Query().Insert().As(x => new Human { Id = -x.Id, BodyWeight = x.BodyWeight }), "superclass prop insertion did not error"); t.Commit(); @@ -381,10 +402,10 @@ public void InsertWithGeneratedVersionAndId() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = - s.Query() - .Where(x => x.Id == initialId) - .Insert().As(x => new IntegerVersioned { Name = x.Name, Data = x.Data }); + var count = s + .Query() + .Where(x => x.Id == initialId) + .Insert().As(x => new IntegerVersioned { Name = x.Name, Data = x.Data }); Assert.That(count, Is.EqualTo(1), "unexpected insertion count"); t.Commit(); } @@ -408,10 +429,10 @@ public void InsertWithGeneratedTimestampVersion() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = - s.Query() - .Where(x => x.Id == initialId) - .Insert().As(x => new TimestampVersioned { Name = x.Name, Data = x.Data }); + var count = s + .Query() + .Where(x => x.Id == initialId) + .Insert().As(x => new TimestampVersioned { Name = x.Name, Data = x.Data }); Assert.That(count, Is.EqualTo(1), "unexpected insertion count"); t.Commit(); @@ -438,7 +459,8 @@ public void InsertWithSelectListUsingJoins() Assert.DoesNotThrow(() => { - s.Query().Where(x => x.Mother.Mother != null) + s + .Query().Where(x => x.Mother.Mother != null) .Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight }); }); @@ -462,13 +484,25 @@ public void InsertToComponent() // https://firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-dml-insert.html#fblangref25-dml-insert-select-unstable .Where(sc => sc.Name.First != correctName) .Insert().Into(x => x.Set(y => y.Name.First, y => correctName)); - Assert.That(count, Is.EqualTo(1), "incorrect insert count"); + Assert.That(count, Is.EqualTo(1), "incorrect insert count from individual setters"); - count = - s.Query() - .Where(x => x.Name.First == correctName && x.Name.Initial != 'Z') - .Insert().As(x => new SimpleClassWithComponent { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'Z' } }); - Assert.That(count, Is.EqualTo(1), "incorrect insert from corrected count"); + count = s + .Query() + .Where(x => x.Name.First == correctName && x.Name.Initial != 'Z') + .Insert().As(x => new SimpleClassWithComponent { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'Z' } }); + Assert.That(count, Is.EqualTo(1), "incorrect insert from non anonymous selector"); + + count = s + .Query() + .Where(x => x.Name.First == correctName && x.Name.Initial == 'Z') + .Insert().As(x => new { Name = new { x.Name.First, x.Name.Last, Initial = 'W' } }); + Assert.That(count, Is.EqualTo(1), "incorrect insert from anonymous selector"); + + count = s + .Query() + .Where(x => x.Name.First == correctName && x.Name.Initial == 'Z') + .Insert().As(x => new { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'V' } }); + Assert.That(count, Is.EqualTo(1), "incorrect insert from hybrid selector"); t.Commit(); } } @@ -488,6 +522,34 @@ private void CheckSupportOfBulkInsertionWithGeneratedId() #region UPDATES + [Test] + public void SimpleUpdate() + { + using (var s = OpenSession()) + using (s.BeginTransaction()) + { + var count = s + .Query() + .Update() + .As(a => new Car { Owner = a.Owner + " a" }); + Assert.AreEqual(1, count); + } + } + + [Test] + public void SimpleAnonymousUpdate() + { + using (var s = OpenSession()) + using (s.BeginTransaction()) + { + var count = s + .Query() + .Update() + .As(a => new { Owner = a.Owner + " a" }); + Assert.AreEqual(1, count); + } + } + [Test] public void UpdateWithWhereExistsSubquery() { @@ -501,7 +563,8 @@ public void UpdateWithWhereExistsSubquery() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query() + var count = s + .Query() .Where(x => x.Friends.OfType().Any(f => f.Name.Last == "Public")) .Update().Assign(x => x.Set(y => y.Description, "updated")); Assert.That(count, Is.EqualTo(1)); @@ -513,14 +576,16 @@ public void UpdateWithWhereExistsSubquery() using (var t = s.BeginTransaction()) { // one-to-many test - var count = s.Query() + var count = s + .Query() .Where(x => x.AssociatedEntities.Any(a => a.Name == "one-to-many-association")) .Update().Assign(x => x.Set(y => y.Name, "updated")); Assert.That(count, Is.EqualTo(1)); // many-to-many test if (Dialect.SupportsSubqueryOnMutatingTable) { - count = s.Query() + count = s + .Query() .Where(x => x.ManyToManyAssociatedEntities.Any(a => a.Name == "many-to-many-association")) .Update().Assign(x => x.Set(y => y.Name, "updated")); @@ -540,9 +605,9 @@ public void IncrementCounterVersion() using (var t = s.BeginTransaction()) { // Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26 - var count = - s.Query() - .UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd")); + var count = s + .Query() + .UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd")); Assert.That(count, Is.EqualTo(1), "incorrect exec count"); t.Commit(); } @@ -570,7 +635,8 @@ public void IncrementTimestampVersion() using (var t = s.BeginTransaction()) { // Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26 - var count = s.Query() + var count = s + .Query() .UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd")); Assert.That(count, Is.EqualTo(1), "incorrect exec count"); t.Commit(); @@ -625,8 +691,8 @@ public void UpdateWithClientSideRequirementsThrowsException() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - Assert.Throws(() => - s.Query().Where(x => x.Id == _stevee.Id).Update().As(x => new Human { Name = { First = x.Name.First.PadLeft(200) } }) + Assert.Throws( + () => s.Query().Where(x => x.Id == _stevee.Id).Update().As(x => new Human { Name = { First = x.Name.First.PadLeft(200) } }) ); t.Commit(); diff --git a/src/NHibernate/Linq/DmlExpressionRewriter.cs b/src/NHibernate/Linq/DmlExpressionRewriter.cs index 06ffc1a1953..baf8e24f2c6 100644 --- a/src/NHibernate/Linq/DmlExpressionRewriter.cs +++ b/src/NHibernate/Linq/DmlExpressionRewriter.cs @@ -25,17 +25,49 @@ public class DmlExpressionRewriter void AddSettersFromBindings(IEnumerable bindings, string path) { foreach (var node in bindings) + { + var subPath = path + "." + node.Member.Name; switch (node.BindingType) { case MemberBindingType.Assignment: - AddSettersFromAssignment((MemberAssignment) node, path + "." + node.Member.Name); + AddSettersFromAssignment((MemberAssignment)node, subPath); break; case MemberBindingType.MemberBinding: - AddSettersFromBindings(((MemberMemberBinding) node).Bindings, path + "." + node.Member.Name); + AddSettersFromBindings(((MemberMemberBinding)node).Bindings, subPath); break; default: throw new InvalidOperationException($"{node.BindingType} is not supported"); } + } + } + + void AddSettersFromAnonymousConstructor(NewExpression newExpression, string path) + { + // See Members documentation, this property is specifically designed to match constructor arguments values + // in the anonymous object case. It can be null otherwise, or non-matching. + var argumentMatchingMembers = newExpression.Members; + if (argumentMatchingMembers == null || argumentMatchingMembers.Count != newExpression.Arguments.Count) + throw new ArgumentException("The expression must be an anonymous initialization, e.g. x => new { Name = x.Name, Age = x.Age + 5 }"); + + var i = 0; + foreach (var argument in newExpression.Arguments) + { + var argumentDefinition = argumentMatchingMembers[i]; + i++; + var subPath = path + "." + argumentDefinition.Name; + switch (argument.NodeType) + { + case ExpressionType.New: + AddSettersFromAnonymousConstructor((NewExpression)argument, subPath); + break; + case ExpressionType.MemberInit: + AddSettersFromBindings(((MemberInitExpression)argument).Bindings, subPath); + break; + default: + _assignments.Add(subPath.Substring(1), Expression.Lambda(argument, _parameters)); + break; + } + } } void AddSettersFromAssignment(MemberAssignment assignment, string path) @@ -89,10 +121,32 @@ public static Expression PrepareExpression(Expression sourceEx throw new ArgumentNullException(nameof(expression)); var memberInitExpression = expression.Body as MemberInitExpression ?? - throw new ArgumentException("The expression must be member initialization, e.g. x => new Dog { Name = x.Name, Age = x.Age + 5 }"); + throw new ArgumentException("The expression must be a member initialization, e.g. x => new Dog { Name = x.Name, Age = x.Age + 5 }, " + + // If someone call InsertSyntax.As(source => new {...}), the code will fail here, so we have to hint at how to correctly + // use anonymous initialization too. + "or an anonymous initialization with an explicitly specified target type when inserting"); + + if (memberInitExpression.Type != typeof(TTarget)) + throw new TypeMismatchException($"Expecting an expression of exact type {typeof(TTarget).AssemblyQualifiedName} " + + $"but got {memberInitExpression.Type.AssemblyQualifiedName}"); - var assignments = ExtractAssignments(expression, memberInitExpression); - return PrepareExpression(sourceExpression, assignments); + var instance = new DmlExpressionRewriter(expression.Parameters); + instance.AddSettersFromBindings(memberInitExpression.Bindings, ""); + return PrepareExpression(sourceExpression, instance._assignments); + } + + public static Expression PrepareExpressionFromAnonymous(Expression sourceExpression, Expression> expression) + { + if (expression == null) + throw new ArgumentNullException(nameof(expression)); + + // Anonymous initializations are not implemented as member initialization but as plain constructor call. + var newExpression = expression.Body as NewExpression ?? + throw new ArgumentException("The expression must be an anonymous initialization, e.g. x => new { Name = x.Name, Age = x.Age + 5 }"); + + var instance = new DmlExpressionRewriter(expression.Parameters); + instance.AddSettersFromAnonymousConstructor(newExpression, ""); + return PrepareExpression(sourceExpression, instance._assignments); } public static Expression PrepareExpression(Expression sourceExpression, IReadOnlyDictionary assignments) @@ -104,14 +158,5 @@ public static Expression PrepareExpression(Expression sourceExpression, sourceExpression, Expression.Quote(lambda)); } - - static Dictionary ExtractAssignments(Expression> expression, MemberInitExpression memberInitExpression) - { - if (memberInitExpression.Type != typeof(TTarget)) - throw new TypeMismatchException($"Expecting an expression of exact type {typeof(TTarget).AssemblyQualifiedName} but got {memberInitExpression.Type.AssemblyQualifiedName}"); - var instance = new DmlExpressionRewriter(expression.Parameters); - instance.AddSettersFromBindings(memberInitExpression.Bindings, ""); - return instance._assignments; - } } } diff --git a/src/NHibernate/Linq/InsertSyntax.cs b/src/NHibernate/Linq/InsertSyntax.cs index 9f0db3f43bd..90d408075bc 100644 --- a/src/NHibernate/Linq/InsertSyntax.cs +++ b/src/NHibernate/Linq/InsertSyntax.cs @@ -45,6 +45,18 @@ public int As(Expression> expression) return ExecuteInsert(DmlExpressionRewriter.PrepareExpression(_sourceExpression, expression)); } + /// + /// Executes the insert, inserting new entities as specified by the expression. + /// + /// The type of the entities to insert. + /// The expression projecting a source entity to an anonymous object representing + /// the entity to insert. + /// The number of inserted entities. + public int As(Expression> expression) + { + return ExecuteInsert(DmlExpressionRewriter.PrepareExpressionFromAnonymous(_sourceExpression, expression)); + } + private int ExecuteInsert(Expression insertExpression) { return _provider.ExecuteDml(QueryMode.Insert, insertExpression); diff --git a/src/NHibernate/Linq/UpdateSyntax.cs b/src/NHibernate/Linq/UpdateSyntax.cs index 1ff1a99af6d..39cd3c64810 100644 --- a/src/NHibernate/Linq/UpdateSyntax.cs +++ b/src/NHibernate/Linq/UpdateSyntax.cs @@ -46,6 +46,17 @@ public int As(Expression> expression) return ExecuteUpdate(DmlExpressionRewriter.PrepareExpression(_sourceExpression, expression)); } + /// + /// Specify the assignments and execute the update. + /// + /// The assignments expressed as an anonymous object, e.g. + /// x => new { Name = x.Name, Age = x.Age + 5 }. Unset members are ignored and left untouched. + /// The number of updated entities. + public int As(Expression> expression) + { + return ExecuteUpdate(DmlExpressionRewriter.PrepareExpressionFromAnonymous(_sourceExpression, expression)); + } + private int ExecuteUpdate(Expression updateExpression) { return _provider.ExecuteDml(_versioned ? QueryMode.UpdateVersioned : QueryMode.Update, updateExpression); From 02761b7d861301faf98362aa5665a9a9829a69d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= Date: Wed, 19 Jul 2017 18:28:00 +0200 Subject: [PATCH 6/7] NH-3488 - alternate Linq DML syntax, inspired by linq2db. --- .../LinqBulkManipulation/Fixture.cs | 115 +++++++------- src/NHibernate/Linq/Assignments.cs | 12 +- src/NHibernate/Linq/DmlExtensionMethods.cs | 146 ++++++++++++++++++ src/NHibernate/Linq/InsertBuilder.cs | 82 ++++++++++ src/NHibernate/Linq/InsertSyntax.cs | 65 -------- src/NHibernate/Linq/LinqExtensionMethods.cs | 52 +------ src/NHibernate/Linq/UpdateBuilder.cs | 65 ++++++++ src/NHibernate/Linq/UpdateSyntax.cs | 65 -------- 8 files changed, 359 insertions(+), 243 deletions(-) create mode 100644 src/NHibernate/Linq/DmlExtensionMethods.cs create mode 100644 src/NHibernate/Linq/InsertBuilder.cs delete mode 100644 src/NHibernate/Linq/InsertSyntax.cs create mode 100644 src/NHibernate/Linq/UpdateBuilder.cs delete mode 100644 src/NHibernate/Linq/UpdateSyntax.cs diff --git a/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs b/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs index 2779b85e175..c376163051d 100644 --- a/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs @@ -187,7 +187,7 @@ public void SimpleInsert() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query().Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner }); + var count = s.Query().InsertInto(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner }); Assert.AreEqual(1, count); t.Commit(); @@ -200,7 +200,7 @@ public void SimpleAnonymousInsert() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query().Insert().As(x => new { Id = -x.Id, x.Vin, x.Owner }); + var count = s.Query().InsertInto(x => new { Id = -x.Id, x.Vin, x.Owner }); Assert.AreEqual(1, count); t.Commit(); @@ -217,7 +217,7 @@ public void SimpleInsertFromAggregate() .Query() .GroupBy(x => x.Id) .Select(x => new { Id = x.Key, Vin = x.Max(y => y.Vin), Owner = x.Max(y => y.Owner) }) - .Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner }); + .InsertInto(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner }); Assert.AreEqual(1, count); t.Commit(); @@ -234,7 +234,7 @@ public void SimpleInsertFromLimited() .Query() .Skip(1) .Take(1) - .Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner }); + .InsertInto(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner }); Assert.AreEqual(1, count); t.Commit(); @@ -249,7 +249,8 @@ public void SimpleInsertWithConstants() { var count = s .Query() - .Insert().Into(x => x.Set(y => y.Id, y => -y.Id).Set(y => y.Vin, y => y.Vin).Set(y => y.Owner, "The owner")); + .InsertBuilder().Into().Value(y => y.Id, y => -y.Id).Value(y => y.Vin, y => y.Vin).Value(y => y.Owner, "The owner") + .Insert(); Assert.AreEqual(1, count); t.Commit(); @@ -265,7 +266,8 @@ public void SimpleInsertFromProjection() var count = s .Query() .Select(x => new { x.Id, x.Owner, UpperOwner = x.Owner.ToUpper() }) - .Insert().Into(x => x.Set(y => y.Id, y => -y.Id).Set(y => y.Vin, y => y.UpperOwner)); + .InsertBuilder().Into().Value(y => y.Id, y => -y.Id).Value(y => y.Vin, y => y.UpperOwner) + .Insert(); Assert.AreEqual(1, count); t.Commit(); @@ -281,7 +283,7 @@ public void InsertWithClientSideRequirementsThrowsException() Assert.Throws( () => s .Query() - .Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner.PadRight(200) })); + .InsertInto(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner.PadRight(200) })); t.Commit(); } @@ -297,7 +299,7 @@ public void InsertWithManyToOne() { var count = s .Query() - .Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = x.Mother }); + .InsertInto(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = x.Mother }); Assert.AreEqual(3, count); t.Commit(); @@ -314,7 +316,7 @@ public void InsertWithManyToOneAsParameter() { var count = s .Query() - .Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = _butterfly }); + .InsertInto(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = _butterfly }); Assert.AreEqual(3, count); t.Commit(); @@ -331,7 +333,7 @@ public void InsertWithManyToOneWithCompositeKey() { var count = s .Query() - .Insert().As(x => new EntityReferencingEntityWithCrazyCompositeKey { Name = "Child", Parent = x }); + .InsertInto(x => new EntityReferencingEntityWithCrazyCompositeKey { Name = "Child", Parent = x }); Assert.AreEqual(1, count); t.Commit(); @@ -345,7 +347,7 @@ public void InsertIntoSuperclassPropertiesFails() using (var t = s.BeginTransaction()) { Assert.Throws( - () => s.Query().Insert().As(x => new Human { Id = -x.Id, BodyWeight = x.BodyWeight }), + () => s.Query().InsertInto(x => new Human { Id = -x.Id, BodyWeight = x.BodyWeight }), "superclass prop insertion did not error"); t.Commit(); @@ -361,7 +363,7 @@ public void InsertAcrossMappedJoinFails() using (var t = s.BeginTransaction()) { Assert.Throws( - () => s.Query().Insert().As(x => new Joiner { Name = x.Vin, JoinedName = x.Owner }), + () => s.Query().InsertInto(x => new Joiner { Name = x.Vin, JoinedName = x.Owner }), "mapped-join insertion did not error"); t.Commit(); @@ -376,7 +378,7 @@ public void InsertWithGeneratedId() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query().Where(z => z.Id == _zoo.Id).Insert().As(x => new PettingZoo { Name = x.Name }); + var count = s.Query().Where(z => z.Id == _zoo.Id).InsertInto(x => new PettingZoo { Name = x.Name }); Assert.That(count, Is.EqualTo(1), "unexpected insertion count"); t.Commit(); } @@ -405,7 +407,7 @@ public void InsertWithGeneratedVersionAndId() var count = s .Query() .Where(x => x.Id == initialId) - .Insert().As(x => new IntegerVersioned { Name = x.Name, Data = x.Data }); + .InsertInto(x => new IntegerVersioned { Name = x.Name, Data = x.Data }); Assert.That(count, Is.EqualTo(1), "unexpected insertion count"); t.Commit(); } @@ -432,7 +434,7 @@ public void InsertWithGeneratedTimestampVersion() var count = s .Query() .Where(x => x.Id == initialId) - .Insert().As(x => new TimestampVersioned { Name = x.Name, Data = x.Data }); + .InsertInto(x => new TimestampVersioned { Name = x.Name, Data = x.Data }); Assert.That(count, Is.EqualTo(1), "unexpected insertion count"); t.Commit(); @@ -461,7 +463,7 @@ public void InsertWithSelectListUsingJoins() { s .Query().Where(x => x.Mother.Mother != null) - .Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight }); + .InsertInto(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight }); }); s.Transaction.Commit(); @@ -483,25 +485,26 @@ public void InsertToComponent() // Avoid Firebird unstable cursor bug by filtering. // https://firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-dml-insert.html#fblangref25-dml-insert-select-unstable .Where(sc => sc.Name.First != correctName) - .Insert().Into(x => x.Set(y => y.Name.First, y => correctName)); + .InsertBuilder().Into().Value(y => y.Name.First, y => correctName) + .Insert(); Assert.That(count, Is.EqualTo(1), "incorrect insert count from individual setters"); count = s .Query() .Where(x => x.Name.First == correctName && x.Name.Initial != 'Z') - .Insert().As(x => new SimpleClassWithComponent { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'Z' } }); + .InsertInto(x => new SimpleClassWithComponent { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'Z' } }); Assert.That(count, Is.EqualTo(1), "incorrect insert from non anonymous selector"); count = s .Query() .Where(x => x.Name.First == correctName && x.Name.Initial == 'Z') - .Insert().As(x => new { Name = new { x.Name.First, x.Name.Last, Initial = 'W' } }); + .InsertInto(x => new { Name = new { x.Name.First, x.Name.Last, Initial = 'W' } }); Assert.That(count, Is.EqualTo(1), "incorrect insert from anonymous selector"); count = s .Query() .Where(x => x.Name.First == correctName && x.Name.Initial == 'Z') - .Insert().As(x => new { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'V' } }); + .InsertInto(x => new { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'V' } }); Assert.That(count, Is.EqualTo(1), "incorrect insert from hybrid selector"); t.Commit(); } @@ -530,8 +533,7 @@ public void SimpleUpdate() { var count = s .Query() - .Update() - .As(a => new Car { Owner = a.Owner + " a" }); + .Update(a => new Car { Owner = a.Owner + " a" }); Assert.AreEqual(1, count); } } @@ -544,8 +546,7 @@ public void SimpleAnonymousUpdate() { var count = s .Query() - .Update() - .As(a => new { Owner = a.Owner + " a" }); + .Update(a => new { Owner = a.Owner + " a" }); Assert.AreEqual(1, count); } } @@ -566,7 +567,8 @@ public void UpdateWithWhereExistsSubquery() var count = s .Query() .Where(x => x.Friends.OfType().Any(f => f.Name.Last == "Public")) - .Update().Assign(x => x.Set(y => y.Description, "updated")); + .UpdateBuilder().Set(y => y.Description, "updated") + .Update(); Assert.That(count, Is.EqualTo(1)); t.Commit(); } @@ -579,7 +581,8 @@ public void UpdateWithWhereExistsSubquery() var count = s .Query() .Where(x => x.AssociatedEntities.Any(a => a.Name == "one-to-many-association")) - .Update().Assign(x => x.Set(y => y.Name, "updated")); + .UpdateBuilder().Set(y => y.Name, "updated") + .Update(); Assert.That(count, Is.EqualTo(1)); // many-to-many test if (Dialect.SupportsSubqueryOnMutatingTable) @@ -587,7 +590,8 @@ public void UpdateWithWhereExistsSubquery() count = s .Query() .Where(x => x.ManyToManyAssociatedEntities.Any(a => a.Name == "many-to-many-association")) - .Update().Assign(x => x.Set(y => y.Name, "updated")); + .UpdateBuilder().Set(y => y.Name, "updated") + .Update(); Assert.That(count, Is.EqualTo(1)); } @@ -607,7 +611,8 @@ public void IncrementCounterVersion() // Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26 var count = s .Query() - .UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd")); + .UpdateBuilder().Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd") + .UpdateVersioned(); Assert.That(count, Is.EqualTo(1), "incorrect exec count"); t.Commit(); } @@ -637,7 +642,8 @@ public void IncrementTimestampVersion() // Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26 var count = s .Query() - .UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd")); + .UpdateBuilder().Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd") + .UpdateVersioned(); Assert.That(count, Is.EqualTo(1), "incorrect exec count"); t.Commit(); } @@ -668,7 +674,7 @@ public void UpdateOnComponent() using (var t = s.BeginTransaction()) { var count = - s.Query().Where(x => x.Id == _stevee.Id).Update().As(x => new Human { Name = { First = correctName } }); + s.Query().Where(x => x.Id == _stevee.Id).Update(x => new Human { Name = { First = correctName } }); Assert.That(count, Is.EqualTo(1), "incorrect update count"); t.Commit(); @@ -692,7 +698,7 @@ public void UpdateWithClientSideRequirementsThrowsException() using (var t = s.BeginTransaction()) { Assert.Throws( - () => s.Query().Where(x => x.Id == _stevee.Id).Update().As(x => new Human { Name = { First = x.Name.First.PadLeft(200) } }) + () => s.Query().Where(x => x.Id == _stevee.Id).Update(x => new Human { Name = { First = x.Name.First.PadLeft(200) } }) ); t.Commit(); @@ -710,12 +716,12 @@ public void UpdateOnManyToOne() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - Assert.DoesNotThrow(() => { s.Query().Where(x => x.Id == 2).Update().Assign(x => x.Set(y => y.Mother, y => null)); }); + Assert.DoesNotThrow(() => { s.Query().Where(x => x.Id == 2).UpdateBuilder().Set(y => y.Mother, y => null).Update(); }); if (Dialect.SupportsSubqueryOnMutatingTable) { Assert.DoesNotThrow( - () => { s.Query().Where(x => x.Id == 2).Update().Assign(x => x.Set(y => y.Mother, y => s.Query().First(z => z.Id == 1))); }); + () => { s.Query().Where(x => x.Id == 2).UpdateBuilder().Set(y => y.Mother, y => s.Query().First(z => z.Id == 1)).Update(); }); } t.Commit(); @@ -729,14 +735,14 @@ public void UpdateOnDiscriminatorSubclass() { using (var t = s.BeginTransaction()) { - var count = s.Query().Update().Assign(x => x.Set(y => y.Name, y => y.Name)); + var count = s.Query().UpdateBuilder().Set(y => y.Name, y => y.Name).Update(); Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass update count"); t.Rollback(); } using (var t = s.BeginTransaction()) { - var count = s.Query().Where(x => x.Id == _pettingZoo.Id).Update().Assign(x => x.Set(y => y.Name, y => y.Name)); + var count = s.Query().Where(x => x.Id == _pettingZoo.Id).UpdateBuilder().Set(y => y.Name, y => y.Name).Update(); Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass update count"); t.Rollback(); @@ -744,7 +750,7 @@ public void UpdateOnDiscriminatorSubclass() using (var t = s.BeginTransaction()) { - var count = s.Query().Update().Assign(x => x.Set(y => y.Name, y => y.Name)); + var count = s.Query().UpdateBuilder().Set(y => y.Name, y => y.Name).Update(); Assert.That(count, Is.EqualTo(2), "Incorrect discrim subclass update count"); t.Rollback(); @@ -753,7 +759,7 @@ public void UpdateOnDiscriminatorSubclass() { // TODO : not so sure this should be allowed. Seems to me that if they specify an alias, // property-refs should be required to be qualified. - var count = s.Query().Where(x => x.Id == _zoo.Id).Update().Assign(x => x.Set(y => y.Name, y => y.Name)); + var count = s.Query().Where(x => x.Id == _zoo.Id).UpdateBuilder().Set(y => y.Name, y => y.Name).Update(); Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass update count"); t.Commit(); @@ -772,11 +778,11 @@ public void UpdateOnAnimal() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - //var count = s.Query().Where(x => x.Description == data.Frog.Description).Update(x => x.Set(y => y.Description, y => y.Description)); + //var count = s.Query().Where(x => x.Description == data.Frog.Description).Update().Set(y => y.Description, y => y.Description)); //Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); var count = - s.Query().Where(x => x.Description == _polliwog.Description).Update().Assign(x => x.Set(y => y.Description, y => "Tadpole")); + s.Query().Where(x => x.Description == _polliwog.Description).UpdateBuilder().Set(y => y.Description, y => "Tadpole").Update(); Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); var tadpole = s.Load(_polliwog.Id); @@ -784,17 +790,17 @@ public void UpdateOnAnimal() Assert.That(tadpole.Description, Is.EqualTo("Tadpole"), "Update did not take effect"); count = - s.Query().Update().Assign(x => x.Set(y => y.FireTemperature, 300)); + s.Query().UpdateBuilder().Set(y => y.FireTemperature, 300).Update(); Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); count = - s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, y => y.BodyWeight + 1 + 1)); + s.Query().UpdateBuilder().Set(y => y.BodyWeight, y => y.BodyWeight + 1 + 1).Update(); Assert.That(count, Is.EqualTo(10), "incorrect count on 'complex' update assignment"); if (Dialect.SupportsSubqueryOnMutatingTable) { - Assert.DoesNotThrow(() => { s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, y => s.Query().Max(z => z.BodyWeight))); }); + Assert.DoesNotThrow(() => { s.Query().UpdateBuilder().Set(y => y.BodyWeight, y => s.Query().Max(z => z.BodyWeight)).Update(); }); } t.Commit(); @@ -813,7 +819,7 @@ public void UpdateOnDragonWithProtectedProperty() using (var t = s.BeginTransaction()) { var count = - s.Query().Update().Assign(x => x.Set(y => y.FireTemperature, 300)); + s.Query().UpdateBuilder().Set(y => y.FireTemperature, 300).Update(); Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); t.Commit(); @@ -834,7 +840,7 @@ public void UpdateMultiplePropertyOnAnimal() var count = s.Query() .Where(x => x.Description == _polliwog.Description) - .Update().Assign(x => x.Set(y => y.Description, y => "Tadpole").Set(y => y.BodyWeight, 3)); + .UpdateBuilder().Set(y => y.Description, y => "Tadpole").Set(y => y.BodyWeight, 3).Update(); Assert.That(count, Is.EqualTo(1)); t.Commit(); @@ -860,16 +866,16 @@ public void UpdateOnMammal() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query().Update().Assign(x => x.Set(y => y.Description, y => y.Description)); + var count = s.Query().UpdateBuilder().Set(y => y.Description, y => y.Description).Update(); Assert.That(count, Is.EqualTo(5), "incorrect update count against 'middle' of joined-subclass hierarchy"); - count = s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, 25)); + count = s.Query().UpdateBuilder().Set(y => y.BodyWeight, 25).Update(); Assert.That(count, Is.EqualTo(5), "incorrect update count against 'middle' of joined-subclass hierarchy"); if (Dialect.SupportsSubqueryOnMutatingTable) { - count = s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, y => s.Query().Max(z => z.BodyWeight))); + count = s.Query().UpdateBuilder().Set(y => y.BodyWeight, y => s.Query().Max(z => z.BodyWeight)).Update(); Assert.That(count, Is.EqualTo(5), "incorrect update count against 'middle' of joined-subclass hierarchy"); } @@ -889,9 +895,9 @@ public void UpdateSetNullUnionSubclass() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query().Update().Assign(x => x.Set(y => y.Owner, "Steve")); + var count = s.Query().UpdateBuilder().Set(y => y.Owner, "Steve").Update(); Assert.That(count, Is.EqualTo(4), "incorrect restricted update count"); - count = s.Query().Where(x => x.Owner == "Steve").Update().Assign(x => x.Set(y => y.Owner, default(string))); + count = s.Query().Where(x => x.Owner == "Steve").UpdateBuilder().Set(y => y.Owner, default(string)).Update(); Assert.That(count, Is.EqualTo(4), "incorrect restricted update count"); count = s.CreateQuery("delete Vehicle where Owner is null").ExecuteUpdate(); @@ -907,13 +913,13 @@ public void UpdateSetNullOnDiscriminatorSubclass() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query().Update().Assign(x => x.Set(y => y.Address.City, default(string))); + var count = s.Query().UpdateBuilder().Set(y => y.Address.City, default(string)).Update(); Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); count = s.CreateQuery("delete Zoo where Address.City is null").ExecuteUpdate(); Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); - count = s.Query().Update().Assign(x => x.Set(y => y.Address.City, default(string))); + count = s.Query().UpdateBuilder().Set(y => y.Address.City, default(string)).Update(); Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); count = s.CreateQuery("delete Zoo where Address.City is null").ExecuteUpdate(); Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); @@ -933,7 +939,7 @@ public void UpdateSetNullOnJoinedSubclass() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, -1)); + var count = s.Query().UpdateBuilder().Set(y => y.BodyWeight, -1).Update(); Assert.That(count, Is.EqualTo(5), "Incorrect update count on joined subclass"); count = s.Query().Count(m => m.BodyWeight > -1.0001 && m.BodyWeight < -0.9999); @@ -950,9 +956,8 @@ public void UpdateOnOtherClassThrows() using (s.BeginTransaction()) { var query = s - .Query().Where(x => x.Mother == _butterfly) - .Update(); - Assert.That(() => query.As(a => new Human { Description = a.Description + " humanized" }), Throws.TypeOf()); + .Query().Where(x => x.Mother == _butterfly); + Assert.That(() => query.Update(a => new Human { Description = a.Description + " humanized" }), Throws.TypeOf()); } } diff --git a/src/NHibernate/Linq/Assignments.cs b/src/NHibernate/Linq/Assignments.cs index cd8ee07bebf..c84f2bf4ad3 100644 --- a/src/NHibernate/Linq/Assignments.cs +++ b/src/NHibernate/Linq/Assignments.cs @@ -11,40 +11,38 @@ namespace NHibernate.Linq /// /// The type of the entity source of the insert or to update. /// The type of the entity to insert or to update. - public class Assignments + internal class Assignments { private readonly Dictionary _assignments = new Dictionary(); internal IReadOnlyDictionary List => _assignments; /// - /// Sets the specified property. + /// Set the specified property. /// /// The type of the property. /// The property. /// The expression that should be assigned to the property. /// The current assignments list. - public Assignments Set(Expression> property, Expression> expression) + public void Set(Expression> property, Expression> expression) { if (expression == null) throw new ArgumentNullException(nameof(expression)); var member = GetMemberExpression(property); _assignments.Add(member.GetMemberPath(), expression); - return this; } /// - /// Sets the specified property. + /// Set the specified property. /// /// The type of the property. /// The property. /// The value. /// The current assignments list. - public Assignments Set(Expression> property, TProp value) + public void Set(Expression> property, TProp value) { var member = GetMemberExpression(property); _assignments.Add(member.GetMemberPath(), Expression.Constant(value, typeof(TProp))); - return this; } private static MemberExpression GetMemberExpression(Expression> property) diff --git a/src/NHibernate/Linq/DmlExtensionMethods.cs b/src/NHibernate/Linq/DmlExtensionMethods.cs new file mode 100644 index 00000000000..98d34b549eb --- /dev/null +++ b/src/NHibernate/Linq/DmlExtensionMethods.cs @@ -0,0 +1,146 @@ +using System; +using System.Linq; +using System.Linq.Expressions; + +namespace NHibernate.Linq +{ + /// + /// NHibernate LINQ DML extension methods. They are meant to work with . Supplied parameters + /// should at least have an . and + /// its overloads supply such queryables. + /// + public static class DmlExtensionMethods + { + /// + /// Delete all entities selected by the specified query. The delete operation is performed in the database without reading the entities out of it. + /// + /// The type of the elements of . + /// The query matching the entities to delete. + /// The number of deleted entities. + public static int Delete(this IQueryable source) + { + var provider = source.GetNhProvider(); + return provider.ExecuteDml(QueryMode.Delete, source.Expression); + } + + /// + /// Update all entities selected by the specified query. The update operation is performed in the database without reading the entities out of it. + /// + /// The type of the elements of . + /// The query matching the entities to update. + /// The update setters expressed as a member initialization of updated entities, e.g. + /// x => new Dog { Name = x.Name, Age = x.Age + 5 }. Unset members are ignored and left untouched. + /// The number of updated entities. + public static int Update(this IQueryable source, Expression> expression) + { + return ExecuteUpdate(source, DmlExpressionRewriter.PrepareExpression(source.Expression, expression), false); + } + + /// + /// Update all entities selected by the specified query, using an anonymous initializer for specifying setters. The update operation is performed + /// in the database without reading the entities out of it. + /// + /// The type of the elements of . + /// The query matching the entities to update. + /// The assignments expressed as an anonymous object, e.g. + /// x => new { Name = x.Name, Age = x.Age + 5 }. Unset members are ignored and left untouched. + /// The number of updated entities. + public static int Update(this IQueryable source, Expression> expression) + { + return ExecuteUpdate(source, DmlExpressionRewriter.PrepareExpressionFromAnonymous(source.Expression, expression), false); + } + + /// + /// Perform an update versioned on all entities selected by the specified query. The update operation is performed in the database without + /// reading the entities out of it. + /// + /// The type of the elements of . + /// The query matching the entities to update. + /// The update setters expressed as a member initialization of updated entities, e.g. + /// x => new Dog { Name = x.Name, Age = x.Age + 5 }. Unset members are ignored and left untouched. + /// The number of updated entities. + public static int UpdateVersioned(this IQueryable source, Expression> expression) + { + return ExecuteUpdate(source, DmlExpressionRewriter.PrepareExpression(source.Expression, expression), true); + } + + /// + /// Perform an update versioned on all entities selected by the specified query, using an anonymous initializer for specifying setters. + /// The update operation is performed in the database without reading the entities out of it. + /// + /// The type of the elements of . + /// The query matching the entities to update. + /// The assignments expressed as an anonymous object, e.g. + /// x => new { Name = x.Name, Age = x.Age + 5 }. Unset members are ignored and left untouched. + /// The number of updated entities. + public static int UpdateVersioned(this IQueryable source, Expression> expression) + { + return ExecuteUpdate(source, DmlExpressionRewriter.PrepareExpressionFromAnonymous(source.Expression, expression), true); + } + + /// + /// Initiate an update for the entities selected by the query. Return + /// a builder allowing to set properties and allowing to execute the update. + /// + /// The type of the elements of . + /// The query matching the entities to update. + /// An update builder. + public static UpdateBuilder UpdateBuilder(this IQueryable source) + { + return new UpdateBuilder(source); + } + + internal static int ExecuteUpdate(this IQueryable source, Expression updateExpression, bool versioned) + { + var provider = source.GetNhProvider(); + return provider.ExecuteDml(versioned ? QueryMode.UpdateVersioned : QueryMode.Update, updateExpression); + } + + /// + /// Insert all entities selected by the specified query. The insert operation is performed in the database without reading the entities out of it. + /// + /// The type of the elements of . + /// The type of the entities to insert. + /// The query matching entities source of the data to insert. + /// The expression projecting a source entity to the entity to insert. + /// The number of inserted entities. + public static int InsertInto(this IQueryable source, Expression> expression) + { + return ExecuteInsert(source, DmlExpressionRewriter.PrepareExpression(source.Expression, expression)); + } + + /// + /// Insert all entities selected by the specified query, using an anonymous initializer for specifying setters. + /// must be explicitly provided, e.g. source.InsertInto<Cat, Dog>(c => new {...}). The insert operation is performed in the + /// database without reading the entities out of it. + /// + /// The type of the elements of . + /// The type of the entities to insert. Must be explicitly provided. + /// The query matching entities source of the data to insert. + /// The expression projecting a source entity to an anonymous object representing + /// the entity to insert. + /// The number of inserted entities. + public static int InsertInto(this IQueryable source, Expression> expression) + { + return ExecuteInsert(source, DmlExpressionRewriter.PrepareExpressionFromAnonymous(source.Expression, expression)); + } + + /// + /// Initiate an insert using selected entities as a source. Return + /// a builder allowing to set properties to insert and allowing to execute the update. + /// + /// The type of the elements of . + /// The query matching the entities to update. + /// An update builder. + public static InsertBuilder InsertBuilder(this IQueryable source) + { + return new InsertBuilder(source); + } + + internal static int ExecuteInsert(this IQueryable source, Expression insertExpression) + { + var provider = source.GetNhProvider(); + return provider.ExecuteDml(QueryMode.Insert, insertExpression); + } + } +} diff --git a/src/NHibernate/Linq/InsertBuilder.cs b/src/NHibernate/Linq/InsertBuilder.cs new file mode 100644 index 00000000000..ac1d7b0cebb --- /dev/null +++ b/src/NHibernate/Linq/InsertBuilder.cs @@ -0,0 +1,82 @@ +using System; +using System.Linq; +using System.Linq.Expressions; + +namespace NHibernate.Linq +{ + /// + /// An insert builder on which entities to insert can be specified. + /// + /// The type of the entities selected as source of the insert. + public class InsertBuilder + { + private readonly IQueryable _source; + + internal InsertBuilder(IQueryable source) + { + _source = source; + } + + /// + /// Specifies the type of the entities to insert, and return an insert builder allowing to specify the values to insert. + /// + /// The type of the entities to insert. + /// An insert builder. + public InsertBuilder Into() + { + return new InsertBuilder(_source); + } + } + + /// + /// An insert builder on which entities to insert can be specified. + /// + /// The type of the entities selected as source of the insert. + /// The type of the entities to insert. + public class InsertBuilder + { + private readonly IQueryable _source; + private readonly Assignments _assignments = new Assignments(); + + internal InsertBuilder(IQueryable source) + { + _source = source; + } + + /// + /// Set the specified property value and return this builder. + /// + /// The type of the property. + /// The property. + /// The expression that should be assigned to the property. + /// This insert builder. + public InsertBuilder Value(Expression> property, Expression> expression) + { + _assignments.Set(property, expression); + return this; + } + + /// + /// Set the specified property value and return this builder. + /// + /// The type of the property. + /// The property. + /// The value. + /// This insert builder. + public InsertBuilder Value(Expression> property, TProp value) + { + _assignments.Set(property, value); + return this; + } + + /// + /// Insert the entities. The insert operation is performed in the database without reading the entities out of it. Will use + /// INSERT INTO [...] SELECT FROM [...] in the database. + /// + /// The number of inserted entities. + public int Insert() + { + return _source.ExecuteInsert(DmlExpressionRewriter.PrepareExpression(_source.Expression, _assignments.List)); + } + } +} \ No newline at end of file diff --git a/src/NHibernate/Linq/InsertSyntax.cs b/src/NHibernate/Linq/InsertSyntax.cs deleted file mode 100644 index 90d408075bc..00000000000 --- a/src/NHibernate/Linq/InsertSyntax.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Linq.Expressions; - -namespace NHibernate.Linq -{ - /// - /// An insert object on which entities to insert can be specified. - /// - /// The type of the entities selected as source of the insert. - public class InsertSyntax - { - private readonly Expression _sourceExpression; - private readonly INhQueryProvider _provider; - - internal InsertSyntax(Expression sourceExpression, INhQueryProvider provider) - { - _sourceExpression = sourceExpression; - _provider = provider; - } - - /// - /// Executes the insert, using the specified assignments. - /// - /// The type of the entities to insert. - /// The assignments. - /// The number of inserted entities. - public int Into(Action> assignmentActions) - { - if (assignmentActions == null) - throw new ArgumentNullException(nameof(assignmentActions)); - var assignments = new Assignments(); - assignmentActions.Invoke(assignments); - - return ExecuteInsert(DmlExpressionRewriter.PrepareExpression(_sourceExpression, assignments.List)); - } - - /// - /// Executes the insert, inserting new entities as specified by the expression. - /// - /// The type of the entities to insert. - /// The expression projecting a source entity to the entity to insert. - /// The number of inserted entities. - public int As(Expression> expression) - { - return ExecuteInsert(DmlExpressionRewriter.PrepareExpression(_sourceExpression, expression)); - } - - /// - /// Executes the insert, inserting new entities as specified by the expression. - /// - /// The type of the entities to insert. - /// The expression projecting a source entity to an anonymous object representing - /// the entity to insert. - /// The number of inserted entities. - public int As(Expression> expression) - { - return ExecuteInsert(DmlExpressionRewriter.PrepareExpressionFromAnonymous(_sourceExpression, expression)); - } - - private int ExecuteInsert(Expression insertExpression) - { - return _provider.ExecuteDml(QueryMode.Insert, insertExpression); - } - } -} \ No newline at end of file diff --git a/src/NHibernate/Linq/LinqExtensionMethods.cs b/src/NHibernate/Linq/LinqExtensionMethods.cs index 0b0d2d75363..d4509ec2d38 100644 --- a/src/NHibernate/Linq/LinqExtensionMethods.cs +++ b/src/NHibernate/Linq/LinqExtensionMethods.cs @@ -121,62 +121,12 @@ public static IQueryable CacheRegion(this IQueryable query, string regi [Obsolete("Please use SetOptions instead.")] public static IQueryable Timeout(this IQueryable query, int timeout) => query.SetOptions(o => o.SetTimeout(timeout)); - - /// - /// Deletes all entities selected by the specified query. The delete operation is performed in the database without reading the entities out of it. - /// - /// The type of the elements of . - /// The query matching the entities to delete. - /// The number of deleted entities. - public static int Delete(this IQueryable source) - { - var provider = GetNhProvider(source); - return provider.ExecuteDml(QueryMode.Delete, source.Expression); - } - - /// - /// Initiate an update for the entities selected by the query. The update operation will be performed in the database without reading the entities out of it. - /// - /// The type of the elements of . - /// The query matching the entities to update. - /// An update builder, allowing to specify assignments to the entities properties. - public static UpdateSyntax Update(this IQueryable source) - { - var provider = GetNhProvider(source); - return new UpdateSyntax(source.Expression, provider, false); - } - - /// - /// Initiate a update versioned for the entities selected by the query. The update operation - /// will be performed in the database without reading the entities out of it. - /// - /// The type of the elements of . - /// The query matching the entities to update. - /// An update builder, allowing to specify assignments to the entities properties. - public static UpdateSyntax UpdateVersioned(this IQueryable source) - { - var provider = GetNhProvider(source); - return new UpdateSyntax(source.Expression, provider, true); - } - - /// - /// Initiate an insert using selected entities as a source. Will use INSERT INTO [...] SELECT FROM [...] in the database. - /// - /// The type of the elements of . - /// The query matching entities source of the data to insert. - /// An insert builder, allowing to specify target entity class and assignments to its properties. - public static InsertSyntax Insert(this IQueryable source) - { - var provider = GetNhProvider(source); - return new InsertSyntax(source.Expression, provider); - } - public static T MappedAs(this T parameter, IType type) { throw new InvalidOperationException("The method should be used inside Linq to indicate a type of a parameter"); } - private static INhQueryProvider GetNhProvider(IQueryable source) + internal static INhQueryProvider GetNhProvider(this IQueryable source) { if (source == null) { diff --git a/src/NHibernate/Linq/UpdateBuilder.cs b/src/NHibernate/Linq/UpdateBuilder.cs new file mode 100644 index 00000000000..700fe32cb0d --- /dev/null +++ b/src/NHibernate/Linq/UpdateBuilder.cs @@ -0,0 +1,65 @@ +using System; +using System.Linq; +using System.Linq.Expressions; + +namespace NHibernate.Linq +{ + /// + /// An update builder on which values to update can be specified. + /// + /// The type of the entities to update. + public class UpdateBuilder + { + private readonly IQueryable _source; + private readonly Assignments _assignments = new Assignments(); + + internal UpdateBuilder(IQueryable source) + { + _source = source; + } + + /// + /// Set the specified property and return this builder. + /// + /// The type of the property. + /// The property. + /// The expression that should be assigned to the property. + /// This update builder. + public UpdateBuilder Set(Expression> property, Expression> expression) + { + _assignments.Set(property, expression); + return this; + } + + /// + /// Set the specified property and return this builder. + /// + /// The type of the property. + /// The property. + /// The value. + /// This update builder. + public UpdateBuilder Set(Expression> property, TProp value) + { + _assignments.Set(property, value); + return this; + } + + /// + /// Update the entities. The update operation is performed in the database without reading the entities out of it. + /// + /// The number of updated entities. + public int Update() + { + return _source.ExecuteUpdate(DmlExpressionRewriter.PrepareExpression(_source.Expression, _assignments.List), false); + } + + /// + /// Perform an update versioned on the entities. The update operation is performed in the database without reading the entities out of it. + /// + /// The number of updated entities. + public int UpdateVersioned() + { + return _source.ExecuteUpdate(DmlExpressionRewriter.PrepareExpression(_source.Expression, _assignments.List), true); + } + } +} \ No newline at end of file diff --git a/src/NHibernate/Linq/UpdateSyntax.cs b/src/NHibernate/Linq/UpdateSyntax.cs deleted file mode 100644 index 39cd3c64810..00000000000 --- a/src/NHibernate/Linq/UpdateSyntax.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Linq.Expressions; - -namespace NHibernate.Linq -{ - /// - /// An update object on which values to update can be specified. - /// - /// The type of the entities to update. - public class UpdateSyntax - { - private readonly Expression _sourceExpression; - private readonly INhQueryProvider _provider; - private readonly bool _versioned; - - internal UpdateSyntax(Expression sourceExpression, INhQueryProvider provider, bool versioned) - { - _sourceExpression = sourceExpression; - _provider = provider; - _versioned = versioned; - } - - /// - /// Specify the assignments and execute the update. - /// - /// The assignments. - /// The number of updated entities. - public int Assign(Action> assignmentActions) - { - if (assignmentActions == null) - throw new ArgumentNullException(nameof(assignmentActions)); - var assignments = new Assignments(); - assignmentActions.Invoke(assignments); - - return ExecuteUpdate(DmlExpressionRewriter.PrepareExpression(_sourceExpression, assignments.List)); - } - - /// - /// Specify the assignments and execute the update. - /// - /// The assignments expressed as a member initialization, e.g. - /// x => new Dog { Name = x.Name, Age = x.Age + 5 }. Unset members are ignored and left untouched. - /// The number of updated entities. - public int As(Expression> expression) - { - return ExecuteUpdate(DmlExpressionRewriter.PrepareExpression(_sourceExpression, expression)); - } - - /// - /// Specify the assignments and execute the update. - /// - /// The assignments expressed as an anonymous object, e.g. - /// x => new { Name = x.Name, Age = x.Age + 5 }. Unset members are ignored and left untouched. - /// The number of updated entities. - public int As(Expression> expression) - { - return ExecuteUpdate(DmlExpressionRewriter.PrepareExpressionFromAnonymous(_sourceExpression, expression)); - } - - private int ExecuteUpdate(Expression updateExpression) - { - return _provider.ExecuteDml(_versioned ? QueryMode.UpdateVersioned : QueryMode.Update, updateExpression); - } - } -} \ No newline at end of file From 18134d7178dd31070c446c4d8233ddcbf1104d19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= Date: Thu, 20 Jul 2017 17:07:30 +0200 Subject: [PATCH 7/7] NH-3944 - Updating the documentation for the new Linq DML syntax. --- doc/reference/modules/query_linq.xml | 52 +++++++++++++++------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/doc/reference/modules/query_linq.xml b/doc/reference/modules/query_linq.xml index 4b7265c4678..c5a03883f24 100644 --- a/doc/reference/modules/query_linq.xml +++ b/doc/reference/modules/query_linq.xml @@ -473,7 +473,8 @@ IList oldCats = Beginning with NHibernate 5.0, Linq queries can be used for inserting, updating or deleting entities. The query defines the data to delete, update or insert, and then Delete, - Update and Insert queryable extension methods allow to delete it, + Update, UpdateBuilder, InsertInto and + InsertBuilder queryable extension methods allow to delete it, or instruct in which way it should be updated or inserted. Those queries happen entirely inside the database, without extracting corresponding entities out of the database. @@ -485,34 +486,34 @@ IList oldCats = Inserting new entities - Insert method extension expects a NHibernate queryable defining the data source of - the insert. This data can be entities or a projection. Then it allows specifying the target entity type - to insert, and how to convert source data to those target entities. Three forms of target specification - exist. + InsertInto and InsertBuilder method extensions expect a NHibernate + queryable defining the data source of the insert. This data can be entities or a projection. Then they + allow specifying the target entity type to insert, and how to convert source data to those target + entities. Three forms of target specification exist. Using projection to target entity: () .Where(c => c.BodyWeight > 20) - .Insert() - .As(c => new Dog { Name = c.Name + "dog", BodyWeight = c.BodyWeight });]]> + .InsertInto(c => new Dog { Name = c.Name + "dog", BodyWeight = c.BodyWeight });]]> - Projections can be done with an anonymous object too, but it requires supplying explicitly the target type: + Projections can be done with an anonymous object too, but it requires supplying explicitly the target + type, which in turn requires re-specifying the source type: () .Where(c => c.BodyWeight > 20) - .Insert() - .As(c => new { Name = c.Name + "dog", BodyWeight = c.BodyWeight });]]> + .InsertInto(c => new { Name = c.Name + "dog", BodyWeight = c.BodyWeight });]]> Or using assignments: () .Where(c => c.BodyWeight > 20) - .Insert() - .Into(a => a - .Set(d => d.Name, c => c.Name + "dog") - .Set(d => d.BodyWeight, c => c.BodyWeight));]]> + .InsertBuilder() + .Into() + .Value(d => d.Name, c => c.Name + "dog") + .Value(d => d.BodyWeight, c => c.BodyWeight) + .Insert();]]> In all cases, unspecified properties are not included in the resulting SQL insert. version and @@ -528,32 +529,30 @@ IList oldCats = Updating entities - Update method extension expects a queryable defining the entities to update. - Then it allows specifying which properties should be updated with which values. As for - Insert, three forms of target specification exist. + Update and UpdateBuilder method extensions expect a NHibernate + queryable defining the entities to update. Then they allow specifying which properties should be + updated with which values. As for insertion, three forms of target specification exist. Using projection to updated entity: () .Where(c => c.BodyWeight > 20) - .Update() - .As(c => new Cat { BodyWeight = c.BodyWeight / 2 });]]> + .Update(c => new Cat { BodyWeight = c.BodyWeight / 2 });]]> Projections can be done with an anonymous object too: () .Where(c => c.BodyWeight > 20) - .Update() - .As(c => new { BodyWeight = c.BodyWeight / 2 });]]> + .Update(c => new { BodyWeight = c.BodyWeight / 2 });]]> Or using assignments: () .Where(c => c.BodyWeight > 20) - .Update() - .Assign(a => a - .Set(c => c.BodyWeight, c => c.BodyWeight / 2));]]> + .UpdateBuilder() + .Set(c => c.BodyWeight, c => c.BodyWeight / 2) + .Update();]]> In all cases, unspecified properties are not included in the resulting SQL update. This could be changed for version and @@ -562,6 +561,11 @@ IList oldCats = the version. Custom version types (NHibernate.Usertype.IUserVersionType) are not supported. + + When using projection to updated entity, please note that the constructed entity must have the + exact same type than the underlying queryable source type. Attempting to project to any other class + (anonymous projections excepted) will fail. +