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..c5a03883f24 100644 --- a/doc/reference/modules/query_linq.xml +++ b/doc/reference/modules/query_linq.xml @@ -467,6 +467,119 @@ 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, 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. + + + These operations are a Linq implementation of , with the same abilities + and limitations. + + + + Inserting new entities + + 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) + .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, which in turn requires re-specifying the source type: + + () + .Where(c => c.BodyWeight > 20) + .InsertInto(c => new { Name = c.Name + "dog", BodyWeight = c.BodyWeight });]]> + + Or using assignments: + + () + .Where(c => c.BodyWeight > 20) + .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 + 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 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(c => new Cat { BodyWeight = c.BodyWeight / 2 });]]> + + Projections can be done with an anonymous object too: + + () + .Where(c => c.BodyWeight > 20) + .Update(c => new { BodyWeight = c.BodyWeight / 2 });]]> + + Or using assignments: + + () + .Where(c => c.BodyWeight > 20) + .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 + timestamp properties: + using UpdateVersioned instead of Update allows incrementing + 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. + + + + + 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/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/LinqBulkManipulation/Domain/Address.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Address.cs new file mode 100644 index 00000000000..dfbb87f6b16 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Address.cs @@ -0,0 +1,41 @@ +namespace NHibernate.Test.LinqBulkManipulation.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/LinqBulkManipulation/Domain/Animal.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Animal.cs new file mode 100644 index 00000000000..b19e460916b --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Animal.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; + +namespace NHibernate.Test.LinqBulkManipulation.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/LinqBulkManipulation/Domain/Animal.hbm.xml b/src/NHibernate.Test/LinqBulkManipulation/Domain/Animal.hbm.xml new file mode 100644 index 00000000000..b0b88de798c --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Animal.hbm.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + human + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/NHibernate.Test/LinqBulkManipulation/Domain/Classification.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Classification.cs new file mode 100644 index 00000000000..9751b45dd05 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Classification.cs @@ -0,0 +1,8 @@ +namespace NHibernate.Test.LinqBulkManipulation.Domain +{ + public enum Classification + { + Cool = 0, + Lame = 1 + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/LinqBulkManipulation/Domain/CrazyCompositeKey.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/CrazyCompositeKey.cs new file mode 100644 index 00000000000..6558078cdc4 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/CrazyCompositeKey.cs @@ -0,0 +1,51 @@ +namespace NHibernate.Test.LinqBulkManipulation.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/LinqBulkManipulation/Domain/DomesticAnimal.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/DomesticAnimal.cs new file mode 100644 index 00000000000..48d9b122b02 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/DomesticAnimal.cs @@ -0,0 +1,16 @@ +namespace NHibernate.Test.LinqBulkManipulation.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/LinqBulkManipulation/Domain/EntityWithCrazyCompositeKey.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/EntityWithCrazyCompositeKey.cs new file mode 100644 index 00000000000..5761a8b6c06 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/EntityWithCrazyCompositeKey.cs @@ -0,0 +1,42 @@ +namespace NHibernate.Test.LinqBulkManipulation.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/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/LinqBulkManipulation/Domain/Human.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Human.cs new file mode 100644 index 00000000000..92501e7b581 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Human.cs @@ -0,0 +1,95 @@ +using System.Collections; +using System.Collections.Generic; + +namespace NHibernate.Test.LinqBulkManipulation.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/LinqBulkManipulation/Domain/IntegerVersioned.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/IntegerVersioned.cs new file mode 100644 index 00000000000..7e09ddfd978 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/IntegerVersioned.cs @@ -0,0 +1,29 @@ +namespace NHibernate.Test.LinqBulkManipulation.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/LinqBulkManipulation/Domain/Joiner.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Joiner.cs new file mode 100644 index 00000000000..a13d3f2b051 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Joiner.cs @@ -0,0 +1,27 @@ +namespace NHibernate.Test.LinqBulkManipulation.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/LinqBulkManipulation/Domain/Mammal.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Mammal.cs new file mode 100644 index 00000000000..3daf5057ac3 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Mammal.cs @@ -0,0 +1,22 @@ +using System; + +namespace NHibernate.Test.LinqBulkManipulation.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/LinqBulkManipulation/Domain/Multi.hbm.xml b/src/NHibernate.Test/LinqBulkManipulation/Domain/Multi.hbm.xml new file mode 100644 index 00000000000..694cf276f23 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Multi.hbm.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/NHibernate.Test/LinqBulkManipulation/Domain/Name.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Name.cs new file mode 100644 index 00000000000..52406bec0cb --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Name.cs @@ -0,0 +1,27 @@ +namespace NHibernate.Test.LinqBulkManipulation.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/LinqBulkManipulation/Domain/Reptile.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Reptile.cs new file mode 100644 index 00000000000..abe34f0980e --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Reptile.cs @@ -0,0 +1,29 @@ +namespace NHibernate.Test.LinqBulkManipulation.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/LinqBulkManipulation/Domain/SimpleAssociatedEntity.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleAssociatedEntity.cs new file mode 100644 index 00000000000..210dedd10ef --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleAssociatedEntity.cs @@ -0,0 +1,77 @@ +namespace NHibernate.Test.LinqBulkManipulation.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/LinqBulkManipulation/Domain/SimpleClass.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClass.cs new file mode 100644 index 00000000000..b2b88291171 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClass.cs @@ -0,0 +1,9 @@ +namespace NHibernate.Test.LinqBulkManipulation.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/LinqBulkManipulation/Domain/SimpleClass.hbm.xml b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClass.hbm.xml new file mode 100644 index 00000000000..0335a1ce786 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClass.hbm.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClassWithComponent.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClassWithComponent.cs new file mode 100644 index 00000000000..8f4b12bbc6d --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClassWithComponent.cs @@ -0,0 +1,10 @@ +namespace NHibernate.Test.LinqBulkManipulation.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/LinqBulkManipulation/Domain/SimpleClassWithComponent.hbm.xml b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClassWithComponent.hbm.xml new file mode 100644 index 00000000000..6d1aea93a16 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleClassWithComponent.hbm.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleEntityWithAssociation.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleEntityWithAssociation.cs new file mode 100644 index 00000000000..e7898f81cba --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleEntityWithAssociation.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; + +namespace NHibernate.Test.LinqBulkManipulation.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/LinqBulkManipulation/Domain/SimpleEntityWithAssociation.hbm.xml b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleEntityWithAssociation.hbm.xml new file mode 100644 index 00000000000..ff40c101ce4 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/SimpleEntityWithAssociation.hbm.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/NHibernate.Test/LinqBulkManipulation/Domain/StateProvince.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/StateProvince.cs new file mode 100644 index 00000000000..f4953f9ef5d --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/StateProvince.cs @@ -0,0 +1,27 @@ +namespace NHibernate.Test.LinqBulkManipulation.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/LinqBulkManipulation/Domain/TimestampVersioned.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/TimestampVersioned.cs new file mode 100644 index 00000000000..b599637fe4d --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/TimestampVersioned.cs @@ -0,0 +1,31 @@ +using System; + +namespace NHibernate.Test.LinqBulkManipulation.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/LinqBulkManipulation/Domain/User.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/User.cs new file mode 100644 index 00000000000..7e4f0c99704 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/User.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; + +namespace NHibernate.Test.LinqBulkManipulation.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/LinqBulkManipulation/Domain/Vehicle.hbm.xml b/src/NHibernate.Test/LinqBulkManipulation/Domain/Vehicle.hbm.xml new file mode 100644 index 00000000000..4978345625a --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/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/LinqBulkManipulation/Domain/Vehicles.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Vehicles.cs new file mode 100644 index 00000000000..6c43d24bb63 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Vehicles.cs @@ -0,0 +1,43 @@ +namespace NHibernate.Test.LinqBulkManipulation.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/LinqBulkManipulation/Domain/Versions.hbm.xml b/src/NHibernate.Test/LinqBulkManipulation/Domain/Versions.hbm.xml new file mode 100644 index 00000000000..59f3295c339 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Versions.hbm.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/NHibernate.Test/LinqBulkManipulation/Domain/Zoo.cs b/src/NHibernate.Test/LinqBulkManipulation/Domain/Zoo.cs new file mode 100644 index 00000000000..be9c239c281 --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Domain/Zoo.cs @@ -0,0 +1,53 @@ +using System.Collections; +using System.Collections.Generic; + +namespace NHibernate.Test.LinqBulkManipulation.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/LinqBulkManipulation/Fixture.cs b/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs new file mode 100644 index 00000000000..c376163051d --- /dev/null +++ b/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs @@ -0,0 +1,1204 @@ +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().InsertInto(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().InsertInto(x => new { Id = -x.Id, x.Vin, 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) }) + .InsertInto(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) + .InsertInto(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() + .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(); + } + } + + [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() }) + .InsertBuilder().Into().Value(y => y.Id, y => -y.Id).Value(y => y.Vin, y => y.UpperOwner) + .Insert(); + Assert.AreEqual(1, count); + + t.Commit(); + } + } + + [Test] + public void InsertWithClientSideRequirementsThrowsException() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + Assert.Throws( + () => s + .Query() + .InsertInto(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() + .InsertInto(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() + .InsertInto(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() + .InsertInto(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().InsertInto(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().InsertInto(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).InsertInto(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) + .InsertInto(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) + .InsertInto(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) + .InsertInto(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) + .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') + .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') + .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') + .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(); + } + } + + 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 SimpleUpdate() + { + using (var s = OpenSession()) + using (s.BeginTransaction()) + { + var count = s + .Query() + .Update(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(a => new { Owner = a.Owner + " a" }); + Assert.AreEqual(1, count); + } + } + + [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")) + .UpdateBuilder().Set(y => y.Description, "updated") + .Update(); + 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")) + .UpdateBuilder().Set(y => y.Name, "updated") + .Update(); + 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")) + .UpdateBuilder().Set(y => y.Name, "updated") + .Update(); + + 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() + .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(); + } + + 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() + .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(); + } + + 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(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(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).UpdateBuilder().Set(y => y.Mother, y => null).Update(); }); + + if (Dialect.SupportsSubqueryOnMutatingTable) + { + Assert.DoesNotThrow( + () => { s.Query().Where(x => x.Id == 2).UpdateBuilder().Set(y => y.Mother, y => s.Query().First(z => z.Id == 1)).Update(); }); + } + + t.Commit(); + } + } + + [Test] + public void UpdateOnDiscriminatorSubclass() + { + using (var s = OpenSession()) + { + using (var t = s.BeginTransaction()) + { + 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).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().UpdateBuilder().Set(y => y.Name, y => y.Name).Update(); + 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).UpdateBuilder().Set(y => y.Name, y => y.Name).Update(); + 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().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).UpdateBuilder().Set(y => y.Description, y => "Tadpole").Update(); + 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().UpdateBuilder().Set(y => y.FireTemperature, 300).Update(); + Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); + + + count = + 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().UpdateBuilder().Set(y => y.BodyWeight, y => s.Query().Max(z => z.BodyWeight)).Update(); }); + } + + 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().UpdateBuilder().Set(y => y.FireTemperature, 300).Update(); + 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) + .UpdateBuilder().Set(y => y.Description, y => "Tadpole").Set(y => y.BodyWeight, 3).Update(); + + 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().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().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().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"); + } + + 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().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").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(); + 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().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().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"); + + 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().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); + Assert.That(count, Is.EqualTo(5), "Incorrect body weight count"); + + t.Commit(); + } + } + + [Test] + public void UpdateOnOtherClassThrows() + { + using (var s = OpenSession()) + using (s.BeginTransaction()) + { + var query = s + .Query().Where(x => x.Mother == _butterfly); + Assert.That(() => query.Update(a => new Human { Description = a.Description + " humanized" }), Throws.TypeOf()); + } + } + + #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(); + } + } + + [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/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..bbe3517c776 100755 --- a/src/NHibernate/Hql/Ast/HqlTreeBuilder.cs +++ b/src/NHibernate/Hql/Ast/HqlTreeBuilder.cs @@ -34,6 +34,26 @@ 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 +84,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 +495,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/Assignments.cs b/src/NHibernate/Linq/Assignments.cs new file mode 100644 index 00000000000..c84f2bf4ad3 --- /dev/null +++ b/src/NHibernate/Linq/Assignments.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using NHibernate.Linq.Visitors; + +namespace NHibernate.Linq +{ + /// + /// 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. + internal class Assignments + { + private readonly Dictionary _assignments = new Dictionary(); + + internal IReadOnlyDictionary List => _assignments; + + /// + /// 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 void Set(Expression> property, Expression> expression) + { + if (expression == null) + throw new ArgumentNullException(nameof(expression)); + var member = GetMemberExpression(property); + _assignments.Add(member.GetMemberPath(), expression); + } + + /// + /// Set the specified property. + /// + /// The type of the property. + /// The property. + /// The value. + /// The current assignments list. + public void Set(Expression> property, TProp value) + { + var member = GetMemberExpression(property); + _assignments.Add(member.GetMemberPath(), Expression.Constant(value, typeof(TProp))); + } + + 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; + } + } +} \ No newline at end of file diff --git a/src/NHibernate/Linq/DefaultQueryProvider.cs b/src/NHibernate/Linq/DefaultQueryProvider.cs index 26c09ffe96f..b650a547933 100644 --- a/src/NHibernate/Linq/DefaultQueryProvider.cs +++ b/src/NHibernate/Linq/DefaultQueryProvider.cs @@ -16,6 +16,7 @@ public interface INhQueryProvider : IQueryProvider IEnumerable ExecuteFuture(Expression expression); IFutureValue ExecuteFutureValue(Expression expression); void SetResultTransformerAndAdditionalCriteria(IQuery query, NhLinqExpression nhExpression, IDictionary> parameters); + int ExecuteDml(QueryMode queryMode, Expression expression); } public class DefaultQueryProvider : INhQueryProvider @@ -45,14 +46,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 +95,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 +171,16 @@ public virtual void SetResultTransformerAndAdditionalCriteria(IQuery query, NhLi criteria(query, parameters); } } + + public int ExecuteDml(QueryMode queryMode, Expression expression) + { + var nhLinqExpression = new NhLinqDmlExpression(queryMode, expression, 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/DmlExpressionRewriter.cs b/src/NHibernate/Linq/DmlExpressionRewriter.cs new file mode 100644 index 00000000000..baf8e24f2c6 --- /dev/null +++ b/src/NHibernate/Linq/DmlExpressionRewriter.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using NHibernate.Linq.Visitors; +using NHibernate.Util; + +namespace NHibernate.Linq +{ + public class DmlExpressionRewriter + { + static readonly ConstructorInfo DictionaryConstructorInfo = typeof(Dictionary).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) + { + var subPath = path + "." + node.Member.Name; + switch (node.BindingType) + { + case MemberBindingType.Assignment: + AddSettersFromAssignment((MemberAssignment)node, subPath); + break; + case MemberBindingType.MemberBinding: + 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) + { + // {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 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 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) + { + var lambda = ConvertAssignmentsToDictionaryExpression(assignments); + + return Expression.Call( + ReflectionCache.QueryableMethods.SelectDefinition.MakeGenericMethod(typeof(TSource), lambda.Body.Type), + sourceExpression, + Expression.Quote(lambda)); + } + } +} 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/IntermediateHqlTree.cs b/src/NHibernate/Linq/IntermediateHqlTree.cs index 9b72eefeeb8..fe433964b25 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,14 +43,20 @@ public HqlTreeNode Root { get { - ExecuteAddHavingClause(_hqlHaving); - ExecuteAddOrderBy(_orderBy); - ExecuteAddSkipClause(_skipCount); - ExecuteAddTakeClause(_takeCount); + //Strange side effects in a property getter... + 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. @@ -58,16 +65,38 @@ 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, + AddPendingHqlClausesToRoot(); + var translationRoot = _insertRoot ?? _root; + return new ExpressionToHqlTranslationResults(translationRoot, _itemTransformers, _listTransformers, _postExecuteTransformers, @@ -102,9 +131,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) @@ -204,13 +240,27 @@ 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)); } } + 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..d4509ec2d38 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); @@ -142,10 +121,22 @@ 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)); - 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"); } + + internal static INhQueryProvider GetNhProvider(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)}"); + } + return provider; + } } } 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 c4c06f8197a..fbca270693f 100644 --- a/src/NHibernate/Linq/NhLinqExpression.cs +++ b/src/NHibernate/Linq/NhLinqExpression.cs @@ -13,10 +13,15 @@ namespace NHibernate.Linq { public class NhLinqExpression : IQueryExpression { - public string Key { get; } + public string Key { get; protected set; } 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; } @@ -25,7 +30,9 @@ public class NhLinqExpression : IQueryExpression public ExpressionToHqlTranslationResults ExpressionToHqlTranslationResults { get; private set; } - private Expression _expression; + protected virtual QueryMode QueryMode => QueryMode.Select; + + private readonly Expression _expression; private readonly IDictionary _constantToParameterMap; public NhLinqExpression(Expression expression, ISessionFactoryImplementor sessionFactory) @@ -34,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); @@ -60,9 +67,9 @@ 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 = QueryModelVisitor.GenerateHqlQuery(queryModel, visitorParameters, true, ReturnType); @@ -70,7 +77,7 @@ public IASTNode Translate(ISessionFactoryImplementor sessionFactory, bool filter Type = ExpressionToHqlTranslationResults.ExecuteResultTypeOverride; ParameterDescriptors = requiredHqlParameters.AsReadOnly(); - + return ExpressionToHqlTranslationResults.Statement.AstNode; } diff --git a/src/NHibernate/Linq/QueryMode.cs b/src/NHibernate/Linq/QueryMode.cs new file mode 100644 index 00000000000..2f3ce197ff1 --- /dev/null +++ b/src/NHibernate/Linq/QueryMode.cs @@ -0,0 +1,11 @@ +namespace NHibernate.Linq +{ + public enum QueryMode + { + Select, + Delete, + Update, + UpdateVersioned, + Insert + } +} 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/Visitors/QueryModelVisitor.cs b/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs index 4527a9ee95d..b28455363f1 100644 --- a/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs +++ b/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs @@ -22,6 +22,8 @@ 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) { @@ -134,10 +136,11 @@ static QueryModelVisitor() private QueryModelVisitor(VisitorParameters visitorParameters, bool root, QueryModel queryModel, NhLinqExpressionReturnType? rootReturnType) { + _queryMode = root ? visitorParameters.RootQueryMode : QueryMode.Select; VisitorParameters = visitorParameters; Model = queryModel; _rootReturnType = root ? rootReturnType : null; - _hqlTree = new IntermediateHqlTree(root); + _hqlTree = new IntermediateHqlTree(root, _queryMode); } private void Visit() @@ -316,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); @@ -372,6 +374,22 @@ public override void VisitSelectClause(SelectClause selectClause, QueryModel que { CurrentEvaluationType = selectClause.GetOutputDataInfo(); + switch (_queryMode) + { + case QueryMode.Delete: + VisitDeleteClause(selectClause.Selector); + 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 +404,62 @@ public override void VisitSelectClause(SelectClause selectClause, QueryModel que base.VisitSelectClause(selectClause, queryModel); } + private void VisitInsertClause(Expression expression) + { + var listInit = expression as ListInitExpression + ?? throw new QueryException("Malformed insert expression"); + var insertedType = VisitorParameters.TargetEntityType; + var idents = new List(); + var selectColumns = new List(); + + //Extract the insert clause from the projected ListInit + foreach (var assignment in listInit.Initializers) + { + 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())); + + //... and then the select clause + _hqlTree.AddSelectClause(_hqlTree.TreeBuilder.Select(selectColumns)); + } + + private void VisitUpdateClause(Expression expression) + { + var listInit = expression as ListInitExpression + ?? throw new QueryException("Malformed update expression"); + foreach (var initializer in listInit.Initializers) + { + 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)); + } + } + + 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(); diff --git a/src/NHibernate/Linq/Visitors/VisitorParameters.cs b/src/NHibernate/Linq/Visitors/VisitorParameters.cs index 27ef5de7029..a4d2a1f2c65 100644 --- a/src/NHibernate/Linq/Visitors/VisitorParameters.cs +++ b/src/NHibernate/Linq/Visitors/VisitorParameters.cs @@ -16,16 +16,27 @@ public class VisitorParameters public QuerySourceNamer QuerySourceNamer { 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/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; + } } } 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 =