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