From 4837de3f0c74f4acebfddf6424e52e9c22a855fc Mon Sep 17 00:00:00 2001 From: maca88 Date: Sat, 21 Mar 2020 05:44:08 +0100 Subject: [PATCH 01/18] Minimize Linq query parameters --- .../Async/Linq/ParameterTests.cs | 133 ++++++++++++++++++ src/NHibernate.Test/Linq/ParameterTests.cs | 120 ++++++++++++++++ src/NHibernate/Linq/NhLinqExpression.cs | 2 +- .../Visitors/ExpressionParameterVisitor.cs | 15 +- .../ProcessContains.cs | 5 +- 5 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 src/NHibernate.Test/Async/Linq/ParameterTests.cs create mode 100644 src/NHibernate.Test/Linq/ParameterTests.cs diff --git a/src/NHibernate.Test/Async/Linq/ParameterTests.cs b/src/NHibernate.Test/Async/Linq/ParameterTests.cs new file mode 100644 index 00000000000..44abaaf3331 --- /dev/null +++ b/src/NHibernate.Test/Async/Linq/ParameterTests.cs @@ -0,0 +1,133 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using NUnit.Framework; +using NHibernate.Linq; + +namespace NHibernate.Test.Linq +{ + using System.Threading.Tasks; + using System.Threading; + [TestFixture] + public class ParameterTestsAsync : LinqTestCase + { + [Test] + public async Task UsingSameArrayParameterTwiceAsync() + { + var ids = new[] {11008, 11019, 11039}; + await (AssertTotalParametersAsync( + db.Orders.Where(o => ids.Contains(o.OrderId) && ids.Contains(o.OrderId)), + ids.Length)); + } + + [Test] + public async Task UsingDifferentArrayParametersAsync() + { + var ids = new[] { 11008, 11019, 11039 }; + var ids2 = new[] { 11008, 11019, 11039 }; + await (AssertTotalParametersAsync( + db.Orders.Where(o => ids.Contains(o.OrderId) && ids2.Contains(o.OrderId)), + ids.Length + ids2.Length)); + } + + [Test] + public async Task UsingSameListParameterTwiceAsync() + { + var ids = new List { 11008, 11019, 11039 }; + await (AssertTotalParametersAsync( + db.Orders.Where(o => ids.Contains(o.OrderId) && ids.Contains(o.OrderId)), + ids.Count)); + } + + [Test] + public async Task UsingDifferentListParametersAsync() + { + var ids = new List { 11008, 11019, 11039 }; + var ids2 = new List { 11008, 11019, 11039 }; + await (AssertTotalParametersAsync( + db.Orders.Where(o => ids.Contains(o.OrderId) && ids2.Contains(o.OrderId)), + ids.Count + ids2.Count)); + } + + [Test] + public async Task UsingSameEntityParameterTwiceAsync() + { + var order = await (db.Orders.FirstAsync()); + await (AssertTotalParametersAsync( + db.Orders.Where(o => o == order && o != order), + 1)); + } + + [Test] + public async Task UsingDifferentEntityParametersAsync() + { + var order = await (db.Orders.FirstAsync()); + var order2 = await (db.Orders.Skip(1).FirstAsync()); + await (AssertTotalParametersAsync( + db.Orders.Where(o => o == order && o != order2), + 2)); + } + + [Test] + public async Task UsingSameValueTypeParameterTwiceAsync() + { + var value = 1; + await (AssertTotalParametersAsync( + db.Orders.Where(o => o.OrderId == value && o.OrderId != value), + 1)); + } + + [Test] + public async Task UsingDifferentValueTypeParametersAsync() + { + var value = 1; + var value2 = 2; + await (AssertTotalParametersAsync( + db.Orders.Where(o => o.OrderId == value && o.OrderId != value2), + 2)); + } + + [Test] + public async Task UsingSameStringParameterTwiceAsync() + { + var value = "test"; + await (AssertTotalParametersAsync( + db.Products.Where(o => o.Name == value && o.Name != value), + 1)); + } + + [Test] + public async Task UsingDifferentStringParametersAsync() + { + var value = "test"; + var value2 = "test2"; + await (AssertTotalParametersAsync( + db.Products.Where(o => o.Name == value && o.Name != value2), + 2)); + } + + private static async Task AssertTotalParametersAsync(IQueryable query, int parameterNumber, CancellationToken cancellationToken = default(CancellationToken)) + { + using (var sqlSpy = new SqlLogSpy()) + { + await (query.ToListAsync(cancellationToken)); + var sqlParameters = sqlSpy.GetWholeLog().Split(';')[1]; + var matches = Regex.Matches(sqlParameters, @"([\d\w]+)[\s]+\=", RegexOptions.IgnoreCase); + + // Due to ODBC drivers not supporting parameter names, we have to do a distinct of parameter names. + var distinctParameters = matches.Select(m => m.Groups[1].Value.Trim()).Distinct().ToList(); + Assert.That(distinctParameters, Has.Count.EqualTo(parameterNumber)); + } + } + } +} diff --git a/src/NHibernate.Test/Linq/ParameterTests.cs b/src/NHibernate.Test/Linq/ParameterTests.cs new file mode 100644 index 00000000000..a3df9601201 --- /dev/null +++ b/src/NHibernate.Test/Linq/ParameterTests.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using NUnit.Framework; + +namespace NHibernate.Test.Linq +{ + [TestFixture] + public class ParameterTests : LinqTestCase + { + [Test] + public void UsingSameArrayParameterTwice() + { + var ids = new[] {11008, 11019, 11039}; + AssertTotalParameters( + db.Orders.Where(o => ids.Contains(o.OrderId) && ids.Contains(o.OrderId)), + ids.Length); + } + + [Test] + public void UsingDifferentArrayParameters() + { + var ids = new[] { 11008, 11019, 11039 }; + var ids2 = new[] { 11008, 11019, 11039 }; + AssertTotalParameters( + db.Orders.Where(o => ids.Contains(o.OrderId) && ids2.Contains(o.OrderId)), + ids.Length + ids2.Length); + } + + [Test] + public void UsingSameListParameterTwice() + { + var ids = new List { 11008, 11019, 11039 }; + AssertTotalParameters( + db.Orders.Where(o => ids.Contains(o.OrderId) && ids.Contains(o.OrderId)), + ids.Count); + } + + [Test] + public void UsingDifferentListParameters() + { + var ids = new List { 11008, 11019, 11039 }; + var ids2 = new List { 11008, 11019, 11039 }; + AssertTotalParameters( + db.Orders.Where(o => ids.Contains(o.OrderId) && ids2.Contains(o.OrderId)), + ids.Count + ids2.Count); + } + + [Test] + public void UsingSameEntityParameterTwice() + { + var order = db.Orders.First(); + AssertTotalParameters( + db.Orders.Where(o => o == order && o != order), + 1); + } + + [Test] + public void UsingDifferentEntityParameters() + { + var order = db.Orders.First(); + var order2 = db.Orders.Skip(1).First(); + AssertTotalParameters( + db.Orders.Where(o => o == order && o != order2), + 2); + } + + [Test] + public void UsingSameValueTypeParameterTwice() + { + var value = 1; + AssertTotalParameters( + db.Orders.Where(o => o.OrderId == value && o.OrderId != value), + 1); + } + + [Test] + public void UsingDifferentValueTypeParameters() + { + var value = 1; + var value2 = 2; + AssertTotalParameters( + db.Orders.Where(o => o.OrderId == value && o.OrderId != value2), + 2); + } + + [Test] + public void UsingSameStringParameterTwice() + { + var value = "test"; + AssertTotalParameters( + db.Products.Where(o => o.Name == value && o.Name != value), + 1); + } + + [Test] + public void UsingDifferentStringParameters() + { + var value = "test"; + var value2 = "test2"; + AssertTotalParameters( + db.Products.Where(o => o.Name == value && o.Name != value2), + 2); + } + + private static void AssertTotalParameters(IQueryable query, int parameterNumber) + { + using (var sqlSpy = new SqlLogSpy()) + { + query.ToList(); + var sqlParameters = sqlSpy.GetWholeLog().Split(';')[1]; + var matches = Regex.Matches(sqlParameters, @"([\d\w]+)[\s]+\=", RegexOptions.IgnoreCase); + + // Due to ODBC drivers not supporting parameter names, we have to do a distinct of parameter names. + var distinctParameters = matches.Select(m => m.Groups[1].Value.Trim()).Distinct().ToList(); + Assert.That(distinctParameters, Has.Count.EqualTo(parameterNumber)); + } + } + } +} diff --git a/src/NHibernate/Linq/NhLinqExpression.cs b/src/NHibernate/Linq/NhLinqExpression.cs index 918b56a37f8..146e192f9f2 100644 --- a/src/NHibernate/Linq/NhLinqExpression.cs +++ b/src/NHibernate/Linq/NhLinqExpression.cs @@ -49,7 +49,7 @@ public NhLinqExpression(Expression expression, ISessionFactoryImplementor sessio _constantToParameterMap = ExpressionParameterVisitor.Visit(ref _expression, sessionFactory); - ParameterValuesByName = _constantToParameterMap.Values.ToDictionary(p => p.Name, + ParameterValuesByName = _constantToParameterMap.Values.Distinct().ToDictionary(p => p.Name, p => System.Tuple.Create(p.Value, p.Type)); Key = ExpressionKeyVisitor.Visit(_expression, _constantToParameterMap); diff --git a/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs b/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs index a44ac2fd398..0910c9ee565 100644 --- a/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs +++ b/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs @@ -17,6 +17,7 @@ namespace NHibernate.Linq.Visitors public class ExpressionParameterVisitor : RelinqExpressionVisitor { private readonly Dictionary _parameters = new Dictionary(); + private readonly Dictionary _valueParameters = new Dictionary(); private readonly ISessionFactoryImplementor _sessionFactory; private static readonly MethodInfo QueryableSkipDefinition = @@ -122,7 +123,19 @@ protected override Expression VisitConstant(ConstantExpression expression) // comes up, it would be nice to combine the HQL parameter type determination code // and the Expression information. - _parameters.Add(expression, new NamedParameter("p" + (_parameters.Count + 1), value, type)); + // Create only one parameter for the same value + if (value == null || !_valueParameters.TryGetValue(value, out var parameter)) + { + parameter = new NamedParameter("p" + (_parameters.Count + 1), value, type); + if (value != null) + { + _valueParameters.Add(value, parameter); + } + } + + _parameters.Add(expression, parameter); + + return base.VisitConstant(expression); } return base.VisitConstant(expression); diff --git a/src/NHibernate/Linq/Visitors/ResultOperatorProcessors/ProcessContains.cs b/src/NHibernate/Linq/Visitors/ResultOperatorProcessors/ProcessContains.cs index 17fc7850425..169b1211eb5 100644 --- a/src/NHibernate/Linq/Visitors/ResultOperatorProcessors/ProcessContains.cs +++ b/src/NHibernate/Linq/Visitors/ResultOperatorProcessors/ProcessContains.cs @@ -59,8 +59,9 @@ private static HqlAlias GetFromAlias(HqlTreeNode node) private static bool IsEmptyList(HqlParameter source, VisitorParameters parameters) { var parameterName = source.NodesPreOrder.Single(n => n is HqlIdent).AstNode.Text; - var parameterValue = parameters.ConstantToParameterMap.Single(p => p.Value.Name == parameterName).Key.Value; + // Multiple constants may be linked to the same parameter, take the first matching parameter + var parameterValue = parameters.ConstantToParameterMap.First(p => p.Value.Name == parameterName).Key.Value; return !((IEnumerable)parameterValue).Cast().Any(); } } -} \ No newline at end of file +} From 5bd087e7d8c6af6ebce3b1240c257d475ac4743a Mon Sep 17 00:00:00 2001 From: maca88 Date: Sat, 21 Mar 2020 06:13:27 +0100 Subject: [PATCH 02/18] Fix build on NETFX --- src/NHibernate.Test/Async/Linq/ParameterTests.cs | 2 +- src/NHibernate.Test/Linq/ParameterTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NHibernate.Test/Async/Linq/ParameterTests.cs b/src/NHibernate.Test/Async/Linq/ParameterTests.cs index 44abaaf3331..6a5edc4982a 100644 --- a/src/NHibernate.Test/Async/Linq/ParameterTests.cs +++ b/src/NHibernate.Test/Async/Linq/ParameterTests.cs @@ -125,7 +125,7 @@ public async Task UsingDifferentStringParametersAsync() var matches = Regex.Matches(sqlParameters, @"([\d\w]+)[\s]+\=", RegexOptions.IgnoreCase); // Due to ODBC drivers not supporting parameter names, we have to do a distinct of parameter names. - var distinctParameters = matches.Select(m => m.Groups[1].Value.Trim()).Distinct().ToList(); + var distinctParameters = matches.OfType().Select(m => m.Groups[1].Value.Trim()).Distinct().ToList(); Assert.That(distinctParameters, Has.Count.EqualTo(parameterNumber)); } } diff --git a/src/NHibernate.Test/Linq/ParameterTests.cs b/src/NHibernate.Test/Linq/ParameterTests.cs index a3df9601201..f186c14e217 100644 --- a/src/NHibernate.Test/Linq/ParameterTests.cs +++ b/src/NHibernate.Test/Linq/ParameterTests.cs @@ -112,7 +112,7 @@ private static void AssertTotalParameters(IQueryable query, int parameterN var matches = Regex.Matches(sqlParameters, @"([\d\w]+)[\s]+\=", RegexOptions.IgnoreCase); // Due to ODBC drivers not supporting parameter names, we have to do a distinct of parameter names. - var distinctParameters = matches.Select(m => m.Groups[1].Value.Trim()).Distinct().ToList(); + var distinctParameters = matches.OfType().Select(m => m.Groups[1].Value.Trim()).Distinct().ToList(); Assert.That(distinctParameters, Has.Count.EqualTo(parameterNumber)); } } From 9977ee3ca27acd647b31a8b5040c96e0da462578 Mon Sep 17 00:00:00 2001 From: maca88 Date: Sun, 22 Mar 2020 19:57:02 +0100 Subject: [PATCH 03/18] Changed the logic to generate one parameter for each variable --- .../Async/Linq/ParameterTests.cs | 133 ++++++++++-- src/NHibernate.Test/Linq/ConstantTest.cs | 11 +- src/NHibernate.Test/Linq/ParameterTests.cs | 135 ++++++++++-- src/NHibernate.Test/Linq/TryGetMappedTests.cs | 4 +- src/NHibernate/Linq/NhLinqExpression.cs | 8 +- src/NHibernate/Linq/NhRelinqQueryParser.cs | 17 +- .../Visitors/ExpressionParameterVisitor.cs | 48 +++-- .../NhPartialEvaluatingExpressionVisitor.cs | 201 ++++++++++++++++-- .../Linq/Visitors/PreTransformationResult.cs | 29 +++ 9 files changed, 504 insertions(+), 82 deletions(-) create mode 100644 src/NHibernate/Linq/Visitors/PreTransformationResult.cs diff --git a/src/NHibernate.Test/Async/Linq/ParameterTests.cs b/src/NHibernate.Test/Async/Linq/ParameterTests.cs index 6a5edc4982a..670bec0d66d 100644 --- a/src/NHibernate.Test/Async/Linq/ParameterTests.cs +++ b/src/NHibernate.Test/Async/Linq/ParameterTests.cs @@ -8,9 +8,12 @@ //------------------------------------------------------------------------------ +using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Text.RegularExpressions; +using NHibernate.DomainModel.Northwind.Entities; using NUnit.Framework; using NHibernate.Linq; @@ -22,7 +25,7 @@ namespace NHibernate.Test.Linq public class ParameterTestsAsync : LinqTestCase { [Test] - public async Task UsingSameArrayParameterTwiceAsync() + public async Task UsingArrayParameterTwiceAsync() { var ids = new[] {11008, 11019, 11039}; await (AssertTotalParametersAsync( @@ -31,36 +34,36 @@ public async Task UsingSameArrayParameterTwiceAsync() } [Test] - public async Task UsingDifferentArrayParametersAsync() + public async Task UsingTwoArrayParametersAsync() { - var ids = new[] { 11008, 11019, 11039 }; - var ids2 = new[] { 11008, 11019, 11039 }; + var ids = new[] {11008, 11019, 11039}; + var ids2 = new[] {11008, 11019, 11039}; await (AssertTotalParametersAsync( db.Orders.Where(o => ids.Contains(o.OrderId) && ids2.Contains(o.OrderId)), ids.Length + ids2.Length)); } [Test] - public async Task UsingSameListParameterTwiceAsync() + public async Task UsingListParameterTwiceAsync() { - var ids = new List { 11008, 11019, 11039 }; + var ids = new List {11008, 11019, 11039}; await (AssertTotalParametersAsync( db.Orders.Where(o => ids.Contains(o.OrderId) && ids.Contains(o.OrderId)), ids.Count)); } [Test] - public async Task UsingDifferentListParametersAsync() + public async Task UsingTwoListParametersAsync() { - var ids = new List { 11008, 11019, 11039 }; - var ids2 = new List { 11008, 11019, 11039 }; + var ids = new List {11008, 11019, 11039}; + var ids2 = new List {11008, 11019, 11039}; await (AssertTotalParametersAsync( db.Orders.Where(o => ids.Contains(o.OrderId) && ids2.Contains(o.OrderId)), ids.Count + ids2.Count)); } [Test] - public async Task UsingSameEntityParameterTwiceAsync() + public async Task UsingEntityParameterTwiceAsync() { var order = await (db.Orders.FirstAsync()); await (AssertTotalParametersAsync( @@ -69,17 +72,17 @@ public async Task UsingSameEntityParameterTwiceAsync() } [Test] - public async Task UsingDifferentEntityParametersAsync() + public async Task UsingTwoEntityParametersAsync() { var order = await (db.Orders.FirstAsync()); - var order2 = await (db.Orders.Skip(1).FirstAsync()); + var order2 = await (db.Orders.FirstAsync()); await (AssertTotalParametersAsync( db.Orders.Where(o => o == order && o != order2), 2)); } [Test] - public async Task UsingSameValueTypeParameterTwiceAsync() + public async Task UsingValueTypeParameterTwiceAsync() { var value = 1; await (AssertTotalParametersAsync( @@ -88,17 +91,35 @@ public async Task UsingSameValueTypeParameterTwiceAsync() } [Test] - public async Task UsingDifferentValueTypeParametersAsync() + public async Task UsingNegateValueTypeParameterTwiceAsync() + { + var value = 1; + await (AssertTotalParametersAsync( + db.Orders.Where(o => o.OrderId == -value && o.OrderId != -value), + 1)); + } + + [Test] + public async Task UsingNegateValueTypeParameterAsync() { var value = 1; - var value2 = 2; + await (AssertTotalParametersAsync( + db.Orders.Where(o => o.OrderId == value && o.OrderId != -value), + 2)); + } + + [Test] + public async Task UsingTwoValueTypeParametersAsync() + { + var value = 1; + var value2 = 1; await (AssertTotalParametersAsync( db.Orders.Where(o => o.OrderId == value && o.OrderId != value2), 2)); } [Test] - public async Task UsingSameStringParameterTwiceAsync() + public async Task UsingStringParameterTwiceAsync() { var value = "test"; await (AssertTotalParametersAsync( @@ -107,15 +128,91 @@ public async Task UsingSameStringParameterTwiceAsync() } [Test] - public async Task UsingDifferentStringParametersAsync() + public async Task UsingTwoStringParametersAsync() { var value = "test"; - var value2 = "test2"; + var value2 = "test"; await (AssertTotalParametersAsync( db.Products.Where(o => o.Name == value && o.Name != value2), 2)); } + [Test] + public async Task UsingObjectPropertyParameterTwiceAsync() + { + var value = new Product {Name = "test"}; + await (AssertTotalParametersAsync( + db.Products.Where(o => o.Name == value.Name && o.Name != value.Name), + 1)); + } + + [Test] + public async Task UsingTwoObjectPropertyParametersAsync() + { + var value = new Product {Name = "test"}; + var value2 = new Product {Name = "test"}; + await (AssertTotalParametersAsync( + db.Products.Where(o => o.Name == value.Name && o.Name != value2.Name), + 2)); + } + + [Test] + public async Task UsingObjectNestedPropertyParameterTwiceAsync() + { + var value = new Employee {Superior = new Employee {Superior = new Employee {FirstName = "test"}}}; + await (AssertTotalParametersAsync( + db.Employees.Where(o => o.FirstName == value.Superior.Superior.FirstName && o.FirstName != value.Superior.Superior.FirstName), + 1)); + } + + [Test] + public async Task UsingDifferentObjectNestedPropertyParameterAsync() + { + var value = new Employee {Superior = new Employee {FirstName = "test", Superior = new Employee {FirstName = "test"}}}; + await (AssertTotalParametersAsync( + db.Employees.Where(o => o.FirstName == value.Superior.FirstName && o.FirstName != value.Superior.Superior.FirstName), + 2)); + } + + [Test] + public async Task UsingMethodObjectPropertyParameterTwiceAsync() + { + var value = new Product {Name = "test"}; + await (AssertTotalParametersAsync( + db.Products.Where(o => o.Name == value.Name.Trim() && o.Name != value.Name.Trim()), + 2)); + } + + [Test] + public async Task UsingStaticMethodObjectPropertyParameterTwiceAsync() + { + var value = new Product {Name = "test"}; + await (AssertTotalParametersAsync( + db.Products.Where(o => o.Name == string.Copy(value.Name) && o.Name != string.Copy(value.Name)), + 2)); + } + + [Test] + public async Task UsingObjectPropertyParameterWithSecondLevelClosureAsync() + { + var value = new Product {Name = "test"}; + Expression> predicate = o => o.Name == value.Name && o.Name != value.Name; + await (AssertTotalParametersAsync( + db.Products.Where(predicate), + 1)); + } + + [Test] + public async Task UsingObjectPropertyParameterWithThirdLevelClosureAsync() + { + var value = new Product {Name = "test"}; + Expression> orderLinePredicate = o => o.Order.ShippedTo == value.Name && o.Order.ShippedTo != value.Name; + Expression> predicate = o => o.Name == value.Name && o.OrderLines.AsQueryable().Any(orderLinePredicate); + await (AssertTotalParametersAsync( + db.Products.Where(predicate), + 1)); + } + private static async Task AssertTotalParametersAsync(IQueryable query, int parameterNumber, CancellationToken cancellationToken = default(CancellationToken)) { using (var sqlSpy = new SqlLogSpy()) diff --git a/src/NHibernate.Test/Linq/ConstantTest.cs b/src/NHibernate.Test/Linq/ConstantTest.cs index 1bb6769d6ad..2f1f9e01030 100644 --- a/src/NHibernate.Test/Linq/ConstantTest.cs +++ b/src/NHibernate.Test/Linq/ConstantTest.cs @@ -215,10 +215,13 @@ public void ConstantInWhereDoesNotCauseManyKeys() var q2 = (from c in db.Customers where c.CustomerId == "ANATR" select c); - var parameters1 = ExpressionParameterVisitor.Visit(q1.Expression, Sfi); - var k1 = ExpressionKeyVisitor.Visit(q1.Expression, parameters1); - var parameters2 = ExpressionParameterVisitor.Visit(q2.Expression, Sfi); - var k2 = ExpressionKeyVisitor.Visit(q2.Expression, parameters2); + var preTransformResult = NhRelinqQueryParser.PreTransform(q1.Expression, Sfi); + var expression = ExpressionParameterVisitor.Visit(preTransformResult, Sfi, out var parameters1); + var k1 = ExpressionKeyVisitor.Visit(expression, parameters1); + + var preTransformResult2 = NhRelinqQueryParser.PreTransform(q1.Expression, Sfi); + var expression2 = ExpressionParameterVisitor.Visit(preTransformResult2, Sfi, out var parameters2); + var k2 = ExpressionKeyVisitor.Visit(expression2, parameters2); Assert.That(parameters1, Has.Count.GreaterThan(0), "parameters1"); Assert.That(parameters2, Has.Count.GreaterThan(0), "parameters2"); diff --git a/src/NHibernate.Test/Linq/ParameterTests.cs b/src/NHibernate.Test/Linq/ParameterTests.cs index f186c14e217..11517326095 100644 --- a/src/NHibernate.Test/Linq/ParameterTests.cs +++ b/src/NHibernate.Test/Linq/ParameterTests.cs @@ -1,6 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Text.RegularExpressions; +using NHibernate.DomainModel.Northwind.Entities; using NUnit.Framework; namespace NHibernate.Test.Linq @@ -9,7 +12,7 @@ namespace NHibernate.Test.Linq public class ParameterTests : LinqTestCase { [Test] - public void UsingSameArrayParameterTwice() + public void UsingArrayParameterTwice() { var ids = new[] {11008, 11019, 11039}; AssertTotalParameters( @@ -18,36 +21,36 @@ public void UsingSameArrayParameterTwice() } [Test] - public void UsingDifferentArrayParameters() + public void UsingTwoArrayParameters() { - var ids = new[] { 11008, 11019, 11039 }; - var ids2 = new[] { 11008, 11019, 11039 }; + var ids = new[] {11008, 11019, 11039}; + var ids2 = new[] {11008, 11019, 11039}; AssertTotalParameters( db.Orders.Where(o => ids.Contains(o.OrderId) && ids2.Contains(o.OrderId)), ids.Length + ids2.Length); } [Test] - public void UsingSameListParameterTwice() + public void UsingListParameterTwice() { - var ids = new List { 11008, 11019, 11039 }; + var ids = new List {11008, 11019, 11039}; AssertTotalParameters( db.Orders.Where(o => ids.Contains(o.OrderId) && ids.Contains(o.OrderId)), ids.Count); } [Test] - public void UsingDifferentListParameters() + public void UsingTwoListParameters() { - var ids = new List { 11008, 11019, 11039 }; - var ids2 = new List { 11008, 11019, 11039 }; + var ids = new List {11008, 11019, 11039}; + var ids2 = new List {11008, 11019, 11039}; AssertTotalParameters( db.Orders.Where(o => ids.Contains(o.OrderId) && ids2.Contains(o.OrderId)), ids.Count + ids2.Count); } [Test] - public void UsingSameEntityParameterTwice() + public void UsingEntityParameterTwice() { var order = db.Orders.First(); AssertTotalParameters( @@ -56,17 +59,17 @@ public void UsingSameEntityParameterTwice() } [Test] - public void UsingDifferentEntityParameters() + public void UsingTwoEntityParameters() { var order = db.Orders.First(); - var order2 = db.Orders.Skip(1).First(); + var order2 = db.Orders.First(); AssertTotalParameters( db.Orders.Where(o => o == order && o != order2), 2); } [Test] - public void UsingSameValueTypeParameterTwice() + public void UsingValueTypeParameterTwice() { var value = 1; AssertTotalParameters( @@ -75,17 +78,35 @@ public void UsingSameValueTypeParameterTwice() } [Test] - public void UsingDifferentValueTypeParameters() + public void UsingNegateValueTypeParameterTwice() + { + var value = 1; + AssertTotalParameters( + db.Orders.Where(o => o.OrderId == -value && o.OrderId != -value), + 1); + } + + [Test] + public void UsingNegateValueTypeParameter() { var value = 1; - var value2 = 2; + AssertTotalParameters( + db.Orders.Where(o => o.OrderId == value && o.OrderId != -value), + 2); + } + + [Test] + public void UsingTwoValueTypeParameters() + { + var value = 1; + var value2 = 1; AssertTotalParameters( db.Orders.Where(o => o.OrderId == value && o.OrderId != value2), 2); } [Test] - public void UsingSameStringParameterTwice() + public void UsingStringParameterTwice() { var value = "test"; AssertTotalParameters( @@ -94,15 +115,91 @@ public void UsingSameStringParameterTwice() } [Test] - public void UsingDifferentStringParameters() + public void UsingTwoStringParameters() { var value = "test"; - var value2 = "test2"; + var value2 = "test"; AssertTotalParameters( db.Products.Where(o => o.Name == value && o.Name != value2), 2); } + [Test] + public void UsingObjectPropertyParameterTwice() + { + var value = new Product {Name = "test"}; + AssertTotalParameters( + db.Products.Where(o => o.Name == value.Name && o.Name != value.Name), + 1); + } + + [Test] + public void UsingTwoObjectPropertyParameters() + { + var value = new Product {Name = "test"}; + var value2 = new Product {Name = "test"}; + AssertTotalParameters( + db.Products.Where(o => o.Name == value.Name && o.Name != value2.Name), + 2); + } + + [Test] + public void UsingObjectNestedPropertyParameterTwice() + { + var value = new Employee {Superior = new Employee {Superior = new Employee {FirstName = "test"}}}; + AssertTotalParameters( + db.Employees.Where(o => o.FirstName == value.Superior.Superior.FirstName && o.FirstName != value.Superior.Superior.FirstName), + 1); + } + + [Test] + public void UsingDifferentObjectNestedPropertyParameter() + { + var value = new Employee {Superior = new Employee {FirstName = "test", Superior = new Employee {FirstName = "test"}}}; + AssertTotalParameters( + db.Employees.Where(o => o.FirstName == value.Superior.FirstName && o.FirstName != value.Superior.Superior.FirstName), + 2); + } + + [Test] + public void UsingMethodObjectPropertyParameterTwice() + { + var value = new Product {Name = "test"}; + AssertTotalParameters( + db.Products.Where(o => o.Name == value.Name.Trim() && o.Name != value.Name.Trim()), + 2); + } + + [Test] + public void UsingStaticMethodObjectPropertyParameterTwice() + { + var value = new Product {Name = "test"}; + AssertTotalParameters( + db.Products.Where(o => o.Name == string.Copy(value.Name) && o.Name != string.Copy(value.Name)), + 2); + } + + [Test] + public void UsingObjectPropertyParameterWithSecondLevelClosure() + { + var value = new Product {Name = "test"}; + Expression> predicate = o => o.Name == value.Name && o.Name != value.Name; + AssertTotalParameters( + db.Products.Where(predicate), + 1); + } + + [Test] + public void UsingObjectPropertyParameterWithThirdLevelClosure() + { + var value = new Product {Name = "test"}; + Expression> orderLinePredicate = o => o.Order.ShippedTo == value.Name && o.Order.ShippedTo != value.Name; + Expression> predicate = o => o.Name == value.Name && o.OrderLines.AsQueryable().Any(orderLinePredicate); + AssertTotalParameters( + db.Products.Where(predicate), + 1); + } + private static void AssertTotalParameters(IQueryable query, int parameterNumber) { using (var sqlSpy = new SqlLogSpy()) diff --git a/src/NHibernate.Test/Linq/TryGetMappedTests.cs b/src/NHibernate.Test/Linq/TryGetMappedTests.cs index b65aa43f701..7858c1d25a4 100644 --- a/src/NHibernate.Test/Linq/TryGetMappedTests.cs +++ b/src/NHibernate.Test/Linq/TryGetMappedTests.cs @@ -773,8 +773,8 @@ private void AssertResult( expectedComponentType = expectedComponentType ?? (o => o == null); var expression = query.Expression; - NhRelinqQueryParser.PreTransform(expression, Sfi); - var constantToParameterMap = ExpressionParameterVisitor.Visit(expression, Sfi); + var preTransformResult = NhRelinqQueryParser.PreTransform(expression, Sfi); + expression = ExpressionParameterVisitor.Visit(preTransformResult, Sfi, out var constantToParameterMap); var queryModel = NhRelinqQueryParser.Parse(expression); var requiredHqlParameters = new List(); var visitorParameters = new VisitorParameters( diff --git a/src/NHibernate/Linq/NhLinqExpression.cs b/src/NHibernate/Linq/NhLinqExpression.cs index 146e192f9f2..cc768f227b0 100644 --- a/src/NHibernate/Linq/NhLinqExpression.cs +++ b/src/NHibernate/Linq/NhLinqExpression.cs @@ -39,7 +39,8 @@ public class NhLinqExpression : IQueryExpression, ICacheableQueryExpression public NhLinqExpression(Expression expression, ISessionFactoryImplementor sessionFactory) { - _expression = NhRelinqQueryParser.PreTransform(expression, sessionFactory); + var preTransformResult = NhRelinqQueryParser.PreTransform(expression, sessionFactory); + _expression = preTransformResult.Expression; // We want logging to be as close as possible to the original expression sent from the // application. But if we log before partial evaluation done in PreTransform, the log won't @@ -47,7 +48,10 @@ public NhLinqExpression(Expression expression, ISessionFactoryImplementor sessio // referenced from the main query. LinqLogging.LogExpression("Expression (partially evaluated)", _expression); - _constantToParameterMap = ExpressionParameterVisitor.Visit(ref _expression, sessionFactory); + _expression = ExpressionParameterVisitor.Visit( + preTransformResult, + sessionFactory, + out _constantToParameterMap); ParameterValuesByName = _constantToParameterMap.Values.Distinct().ToDictionary(p => p.Name, p => System.Tuple.Create(p.Value, p.Type)); diff --git a/src/NHibernate/Linq/NhRelinqQueryParser.cs b/src/NHibernate/Linq/NhRelinqQueryParser.cs index bc3e89c598d..8604fd00ec9 100644 --- a/src/NHibernate/Linq/NhRelinqQueryParser.cs +++ b/src/NHibernate/Linq/NhRelinqQueryParser.cs @@ -7,6 +7,7 @@ using NHibernate.Engine; using NHibernate.Linq.ExpressionTransformers; using NHibernate.Linq.Visitors; +using NHibernate.Param; using NHibernate.Util; using Remotion.Linq; using Remotion.Linq.EagerFetching.Parsing; @@ -56,7 +57,7 @@ static NhRelinqQueryParser() [Obsolete("Use overload with an additional sessionFactory parameter")] public static Expression PreTransform(Expression expression) { - return PreTransform(expression, null); + return PreTransform(expression, null).Expression; } /// @@ -65,12 +66,16 @@ public static Expression PreTransform(Expression expression) /// /// The expression to transform. /// The session factory. - /// The transformed expression. - public static Expression PreTransform(Expression expression, ISessionFactoryImplementor sessionFactory) + /// that contains the transformed expression. + public static PreTransformationResult PreTransform(Expression expression, ISessionFactoryImplementor sessionFactory) { - var partiallyEvaluatedExpression = - NhPartialEvaluatingExpressionVisitor.EvaluateIndependentSubtrees(expression, sessionFactory); - return PreProcessor.Process(partiallyEvaluatedExpression); + var queryVariables = new Dictionary(); + var partiallyEvaluatedExpression = NhPartialEvaluatingExpressionVisitor + .EvaluateIndependentSubtrees(expression, new NhEvaluatableExpressionFilter(sessionFactory), queryVariables); + + return new PreTransformationResult( + PreProcessor.Process(partiallyEvaluatedExpression), + queryVariables); } public static QueryModel Parse(Expression expression) diff --git a/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs b/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs index 0910c9ee565..b187940431f 100644 --- a/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs +++ b/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs @@ -17,7 +17,8 @@ namespace NHibernate.Linq.Visitors public class ExpressionParameterVisitor : RelinqExpressionVisitor { private readonly Dictionary _parameters = new Dictionary(); - private readonly Dictionary _valueParameters = new Dictionary(); + private readonly Dictionary _variableParameters = new Dictionary(); + private readonly IDictionary _queryVariables; private readonly ISessionFactoryImplementor _sessionFactory; private static readonly MethodInfo QueryableSkipDefinition = @@ -35,25 +36,43 @@ public class ExpressionParameterVisitor : RelinqExpressionVisitor EnumerableSkipDefinition, EnumerableTakeDefinition }; + // Since v5.3 + [Obsolete("Please use overload with preTransformationResult parameter instead.")] public ExpressionParameterVisitor(ISessionFactoryImplementor sessionFactory) { _sessionFactory = sessionFactory; } - public static IDictionary Visit(Expression expression, ISessionFactoryImplementor sessionFactory) + public ExpressionParameterVisitor( + ISessionFactoryImplementor sessionFactory, + PreTransformationResult preTransformationResult) { - return Visit(ref expression, sessionFactory); + _sessionFactory = sessionFactory; + _queryVariables = preTransformationResult.QueryVariables; } - internal static IDictionary Visit(ref Expression expression, ISessionFactoryImplementor sessionFactory) + // Since v5.3 + [Obsolete("Please use overload with preTransformationResult parameter instead.")] + public static IDictionary Visit(Expression expression, ISessionFactoryImplementor sessionFactory) { var visitor = new ExpressionParameterVisitor(sessionFactory); - - expression = visitor.Visit(expression); + visitor.Visit(expression); return visitor._parameters; } + public static Expression Visit( + PreTransformationResult preTransformationResult, + ISessionFactoryImplementor sessionFactory, + out IDictionary parameters) + { + var visitor = new ExpressionParameterVisitor(sessionFactory, preTransformationResult); + var expression = visitor.Visit(preTransformationResult.Expression); + parameters = visitor._parameters; + + return expression; + } + protected override Expression VisitMethodCall(MethodCallExpression expression) { if (expression.Method.Name == nameof(LinqExtensionMethods.MappedAs) && expression.Method.DeclaringType == typeof(LinqExtensionMethods)) @@ -123,14 +142,19 @@ protected override Expression VisitConstant(ConstantExpression expression) // comes up, it would be nice to combine the HQL parameter type determination code // and the Expression information. - // Create only one parameter for the same value - if (value == null || !_valueParameters.TryGetValue(value, out var parameter)) + // Create only one parameter for the same variable + NamedParameter parameter = null; + if (_queryVariables != null && + _queryVariables.TryGetValue(expression, out var variable) && + !_variableParameters.TryGetValue(variable, out parameter)) + { + parameter = new NamedParameter("p" + (_parameters.Count + 1), value, type); + _variableParameters.Add(variable, parameter); + } + + if (parameter == null) { parameter = new NamedParameter("p" + (_parameters.Count + 1), value, type); - if (value != null) - { - _valueParameters.Add(value, parameter); - } } _parameters.Add(expression, parameter); diff --git a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs index 5cfb22fa178..ad86f1f82e3 100644 --- a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs +++ b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs @@ -1,51 +1,214 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; using NHibernate.Collection; using NHibernate.Engine; using NHibernate.Linq.Functions; using NHibernate.Util; -using Remotion.Linq.Clauses.Expressions; using Remotion.Linq.Parsing; -using Remotion.Linq.Parsing.ExpressionVisitors; using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation; namespace NHibernate.Linq.Visitors { - internal class NhPartialEvaluatingExpressionVisitor : RelinqExpressionVisitor, IPartialEvaluationExceptionExpressionVisitor + // Copied from Relinq and added logic for detecting and linking variables with evaluated constant expressions + /// + /// Takes an expression tree and first analyzes it for evaluatable subtrees (using ), i.e. + /// subtrees that can be pre-evaluated before actually generating the query. Examples for evaluatable subtrees are operations on constant + /// values (constant folding), access to closure variables (variables used by the LINQ query that are defined in an outer scope), or method + /// calls on known objects or their members. In a second step, it replaces all of the evaluatable subtrees (top-down and non-recursive) by + /// their evaluated counterparts. + /// + /// + /// This visitor visits each tree node at most twice: once via the for analysis and once + /// again to replace nodes if possible (unless the parent node has already been replaced). + /// + internal sealed class NhPartialEvaluatingExpressionVisitor : RelinqExpressionVisitor { - private readonly ISessionFactoryImplementor _sessionFactory; + /// + /// Takes an expression tree and finds and evaluates all its evaluatable subtrees. + /// + public static Expression EvaluateIndependentSubtrees( + Expression expressionTree, + IEvaluatableExpressionFilter evaluatableExpressionFilter, + IDictionary queryVariables) + { + var partialEvaluationInfo = EvaluatableTreeFindingExpressionVisitor.Analyze(expressionTree, evaluatableExpressionFilter); + var visitor = new NhPartialEvaluatingExpressionVisitor(partialEvaluationInfo, evaluatableExpressionFilter, queryVariables); + + return visitor.Visit(expressionTree); + } + + // _partialEvaluationInfo contains a list of the expressions that are safe to be evaluated. + private readonly PartialEvaluationInfo _partialEvaluationInfo; + private readonly IEvaluatableExpressionFilter _evaluatableExpressionFilter; + private readonly IDictionary _queryVariables; - internal NhPartialEvaluatingExpressionVisitor(ISessionFactoryImplementor sessionFactory) + private NhPartialEvaluatingExpressionVisitor( + PartialEvaluationInfo partialEvaluationInfo, + IEvaluatableExpressionFilter evaluatableExpressionFilter, + IDictionary queryVariables) { - _sessionFactory = sessionFactory; + _partialEvaluationInfo = partialEvaluationInfo; + _evaluatableExpressionFilter = evaluatableExpressionFilter; + _queryVariables = queryVariables; + } + + public override Expression Visit(Expression expression) + { + // Only evaluate expressions which do not use any of the surrounding parameter expressions. Don't evaluate + // lambda expressions (even if you could), we want to analyze those later on. + if (expression == null) + return null; + + if (expression.NodeType == ExpressionType.Lambda || !_partialEvaluationInfo.IsEvaluatableExpression(expression)) + return base.Visit(expression); + + Expression evaluatedExpression; + try + { + evaluatedExpression = EvaluateSubtree(expression); + } + catch (Exception ex) + { + // Evaluation caused an exception. Skip evaluation of this expression and proceed as if it weren't evaluable. + var baseVisitedExpression = base.Visit(expression); + + throw new HibernateException($"Evaluation failure on {baseVisitedExpression}", ex); + } + + if (evaluatedExpression != expression) + { + evaluatedExpression = EvaluateIndependentSubtrees(evaluatedExpression, _evaluatableExpressionFilter, _queryVariables); + } + + // When having multiple level closure, we have to evaluate each closure independently + if (evaluatedExpression is ConstantExpression constantExpression) + { + evaluatedExpression = VisitConstant(constantExpression); + } + + // Variables in expressions are never a constant, they are encapsulated as fields of a compiler generated class + if (expression.NodeType != ExpressionType.Constant && + evaluatedExpression is ConstantExpression variableConstant && + !_queryVariables.ContainsKey(variableConstant) && + IsVariable(expression, out var path, out var closureContext)) + { + _queryVariables.Add(variableConstant, new QueryVariable(path, closureContext)); + } + + return evaluatedExpression; } protected override Expression VisitConstant(ConstantExpression expression) { if (expression.Value is Expression value) { - return EvaluateIndependentSubtrees(value, _sessionFactory); + return EvaluateIndependentSubtrees(value, _evaluatableExpressionFilter, _queryVariables); } return base.VisitConstant(expression); } - public static Expression EvaluateIndependentSubtrees( - Expression expression, - ISessionFactoryImplementor sessionFactory) + /// + /// Evaluates an evaluatable subtree, i.e. an independent expression tree that is compilable and executable + /// without any data being passed in. The result of the evaluation is returned as a ; if the subtree + /// is already a , no evaluation is performed. + /// + /// The subtree to be evaluated. + /// A holding the result of the evaluation. + private Expression EvaluateSubtree(Expression subtree) + { + if (subtree.NodeType == ExpressionType.Constant) + { + var constantExpression = (ConstantExpression) subtree; + var valueAsIQueryable = constantExpression.Value as IQueryable; + if (valueAsIQueryable != null && valueAsIQueryable.Expression != constantExpression) + return valueAsIQueryable.Expression; + + return constantExpression; + } + else + { + Expression> lambdaWithoutParameters = Expression.Lambda>(Expression.Convert(subtree, typeof(object))); + var compiledLambda = lambdaWithoutParameters.Compile(); + + object value = compiledLambda(); + return Expression.Constant(value, subtree.Type); + } + } + + private bool IsVariable(Expression expression, out string path, out object closureContext) + { + Expression childExpression; + string currentPath; + switch (expression) + { + case MemberExpression memberExpression: + childExpression = memberExpression.Expression; + currentPath = memberExpression.Member.Name; + break; + case ConstantExpression constantExpression: + path = null; + if (constantExpression.Type.Attributes.HasFlag(TypeAttributes.NestedPrivate) && + Attribute.IsDefined(constantExpression.Type, typeof(CompilerGeneratedAttribute), inherit: true)) + { + closureContext = constantExpression.Value; + return true; + } + + closureContext = null; + return false; + case UnaryExpression unaryExpression: + childExpression = unaryExpression.Operand; + currentPath = $"({unaryExpression.NodeType})"; + break; + default: + path = null; + closureContext = null; + return false; + } + + if (!IsVariable(childExpression, out path, out closureContext)) + { + return false; + } + + path = path != null ? $"{path}_{currentPath}" : currentPath; + return true; + } + } + + internal struct QueryVariable : IEquatable + { + public QueryVariable(string path, object closureContext) { - var evaluatedExpression = PartialEvaluatingExpressionVisitor.EvaluateIndependentSubtrees( - expression, - new NhEvaluatableExpressionFilter(sessionFactory)); - return new NhPartialEvaluatingExpressionVisitor(sessionFactory).Visit(evaluatedExpression); + Path = path; + ClosureContext = closureContext; + } + + public string Path { get; } + + public object ClosureContext { get; } + + public override bool Equals(object obj) + { + return obj is QueryVariable other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return (Path.GetHashCode() * 397) ^ ClosureContext.GetHashCode(); + } } - public Expression VisitPartialEvaluationException(PartialEvaluationExceptionExpression partialEvaluationExceptionExpression) + public bool Equals(QueryVariable other) { - throw new HibernateException( - $"Evaluation failure on {partialEvaluationExceptionExpression.EvaluatedExpression}", - partialEvaluationExceptionExpression.Exception); + return Path == other.Path && ReferenceEquals(ClosureContext, other.ClosureContext); } } diff --git a/src/NHibernate/Linq/Visitors/PreTransformationResult.cs b/src/NHibernate/Linq/Visitors/PreTransformationResult.cs new file mode 100644 index 00000000000..ddd57f2d6b8 --- /dev/null +++ b/src/NHibernate/Linq/Visitors/PreTransformationResult.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace NHibernate.Linq.Visitors +{ + /// + /// The result of method. + /// + public class PreTransformationResult + { + internal PreTransformationResult( + Expression expression, + IDictionary queryVariables) + { + Expression = expression; + QueryVariables = queryVariables; + } + + /// + /// The transformed expression. + /// + public Expression Expression { get; } + + /// + /// A dictionary of that were evaluated from variables. + /// + internal IDictionary QueryVariables { get; } + } +} From aec88f9e8a6daa1d9e74d590b9c47eaa5fa4219b Mon Sep 17 00:00:00 2001 From: maca88 Date: Mon, 23 Mar 2020 22:28:43 +0100 Subject: [PATCH 04/18] Code review changes --- .../Async/Linq/ParameterTests.cs | 176 ++++++++++++- src/NHibernate.Test/Linq/ConstantTest.cs | 4 +- src/NHibernate.Test/Linq/ParameterTests.cs | 248 +++++++++++++++++- src/NHibernate.Test/Linq/TryGetMappedTests.cs | 2 +- src/NHibernate/Linq/NhLinqDmlExpression.cs | 5 +- src/NHibernate/Linq/NhLinqExpression.cs | 9 +- .../Visitors/ExpressionParameterVisitor.cs | 16 +- 7 files changed, 427 insertions(+), 33 deletions(-) diff --git a/src/NHibernate.Test/Async/Linq/ParameterTests.cs b/src/NHibernate.Test/Async/Linq/ParameterTests.cs index 670bec0d66d..fb2b33f66f5 100644 --- a/src/NHibernate.Test/Async/Linq/ParameterTests.cs +++ b/src/NHibernate.Test/Async/Linq/ParameterTests.cs @@ -14,8 +14,8 @@ using System.Linq.Expressions; using System.Text.RegularExpressions; using NHibernate.DomainModel.Northwind.Entities; -using NUnit.Framework; using NHibernate.Linq; +using NUnit.Framework; namespace NHibernate.Test.Linq { @@ -30,7 +30,8 @@ public async Task UsingArrayParameterTwiceAsync() var ids = new[] {11008, 11019, 11039}; await (AssertTotalParametersAsync( db.Orders.Where(o => ids.Contains(o.OrderId) && ids.Contains(o.OrderId)), - ids.Length)); + ids.Length, + 1)); } [Test] @@ -40,7 +41,8 @@ public async Task UsingTwoArrayParametersAsync() var ids2 = new[] {11008, 11019, 11039}; await (AssertTotalParametersAsync( db.Orders.Where(o => ids.Contains(o.OrderId) && ids2.Contains(o.OrderId)), - ids.Length + ids2.Length)); + ids.Length + ids2.Length, + 2)); } [Test] @@ -49,7 +51,8 @@ public async Task UsingListParameterTwiceAsync() var ids = new List {11008, 11019, 11039}; await (AssertTotalParametersAsync( db.Orders.Where(o => ids.Contains(o.OrderId) && ids.Contains(o.OrderId)), - ids.Count)); + ids.Count, + 1)); } [Test] @@ -59,7 +62,8 @@ public async Task UsingTwoListParametersAsync() var ids2 = new List {11008, 11019, 11039}; await (AssertTotalParametersAsync( db.Orders.Where(o => ids.Contains(o.OrderId) && ids2.Contains(o.OrderId)), - ids.Count + ids2.Count)); + ids.Count + ids2.Count, + 2)); } [Test] @@ -108,6 +112,16 @@ public async Task UsingNegateValueTypeParameterAsync() 2)); } + [Test] + public async Task UsingValueTypeParameterInArrayAsync() + { + var id = 11008; + await (AssertTotalParametersAsync( + db.Orders.Where(o => new[] {id, 11019}.Contains(o.OrderId) && new[] {id, 11019}.Contains(o.OrderId)), + 4, + 2)); + } + [Test] public async Task UsingTwoValueTypeParametersAsync() { @@ -156,6 +170,22 @@ public async Task UsingTwoObjectPropertyParametersAsync() 2)); } + [Test] + public async Task UsingParameterInWhereSkipTakeAsync() + { + var value3 = 1; + var q1 = db.Products.Where(o => o.ProductId < value3).Take(value3).Skip(value3); + await (AssertTotalParametersAsync(q1, 3)); + } + + [Test] + public async Task UsingParameterInTwoWhereAsync() + { + var value3 = 1; + var q1 = db.Products.Where(o => o.ProductId < value3).Where(o => o.ProductId < value3); + await (AssertTotalParametersAsync(q1, 1)); + } + [Test] public async Task UsingObjectNestedPropertyParameterTwiceAsync() { @@ -213,18 +243,142 @@ public async Task UsingObjectPropertyParameterWithThirdLevelClosureAsync() 1)); } - private static async Task AssertTotalParametersAsync(IQueryable query, int parameterNumber, CancellationToken cancellationToken = default(CancellationToken)) + [Test] + public async Task UsingParameterInDMLInsertIntoFourTimesAsync() + { + var value = "test"; + await (AssertTotalParametersAsync( + QueryMode.Insert, + db.Customers.Where(c => c.CustomerId == value), + x => new Customer {CustomerId = value, ContactName = value, CompanyName = value}, + 4)); + } + + [Test] + public async Task UsingFourParametersInDMLInsertIntoAsync() + { + var value = "test"; + var value2 = "test"; + var value3 = "test"; + var value4 = "test"; + await (AssertTotalParametersAsync( + QueryMode.Insert, + db.Customers.Where(c => c.CustomerId == value3), + x => new Customer {CustomerId = value4, ContactName = value2, CompanyName = value}, + 4)); + } + + [Test] + public async Task UsingParameterInDMLUpdateThreeTimesAsync() + { + var value = "test"; + await (AssertTotalParametersAsync( + QueryMode.Update, + db.Customers.Where(c => c.CustomerId == value), + x => new Customer {ContactName = value, CompanyName = value}, + 3)); + } + + [Test] + public async Task UsingThreeParametersInDMLUpdateAsync() + { + var value = "test"; + var value2 = "test"; + var value3 = "test"; + await (AssertTotalParametersAsync( + QueryMode.Update, + db.Customers.Where(c => c.CustomerId == value3), + x => new Customer { ContactName = value2, CompanyName = value }, + 3)); + } + + [Test] + public async Task UsingParameterInDMLDeleteTwiceAsync() + { + var value = "test"; + await (AssertTotalParametersAsync( + QueryMode.Delete, + db.Customers.Where(c => c.CustomerId == value && c.CompanyName == value), + 2)); + } + + [Test] + public async Task UsingTwoParametersInDMLDeleteAsync() + { + var value = "test"; + var value2 = "test"; + await (AssertTotalParametersAsync( + QueryMode.Delete, + db.Customers.Where(c => c.CustomerId == value && c.CompanyName == value2), + 2)); + } + + private async Task AssertTotalParametersAsync(IQueryable query, int parameterNumber, int? linqParameterNumber = null, CancellationToken cancellationToken = default(CancellationToken)) { using (var sqlSpy = new SqlLogSpy()) { + // In case of arrays linqParameterNumber and parameterNumber will be different + Assert.That( + GetLinqExpression(query).ParameterValuesByName.Count, + Is.EqualTo(linqParameterNumber ?? parameterNumber), + "Linq expression has different number of parameters"); + await (query.ToListAsync(cancellationToken)); - var sqlParameters = sqlSpy.GetWholeLog().Split(';')[1]; - var matches = Regex.Matches(sqlParameters, @"([\d\w]+)[\s]+\=", RegexOptions.IgnoreCase); + AssertParameters(sqlSpy, parameterNumber); + } + } + + private static Task AssertTotalParametersAsync(QueryMode queryMode, IQueryable query, int parameterNumber, CancellationToken cancellationToken = default(CancellationToken)) + { + return AssertTotalParametersAsync(queryMode, query, null, parameterNumber, cancellationToken); + } + + private static async Task AssertTotalParametersAsync(QueryMode queryMode, IQueryable query, Expression> expression, int parameterNumber, CancellationToken cancellationToken = default(CancellationToken)) + { + var provider = query.Provider as INhQueryProvider; + Assert.That(provider, Is.Not.Null); - // Due to ODBC drivers not supporting parameter names, we have to do a distinct of parameter names. - var distinctParameters = matches.OfType().Select(m => m.Groups[1].Value.Trim()).Distinct().ToList(); - Assert.That(distinctParameters, Has.Count.EqualTo(parameterNumber)); + var dmlExpression = expression != null + ? DmlExpressionRewriter.PrepareExpression(query.Expression, expression) + : query.Expression; + + using (var sqlSpy = new SqlLogSpy()) + { + Assert.That(await (provider.ExecuteDmlAsync(queryMode, dmlExpression, cancellationToken)), Is.EqualTo(0), "The DML query updated the data"); // Avoid updating the data + AssertParameters(sqlSpy, parameterNumber); } } + + private static void AssertParameters(SqlLogSpy sqlSpy, int parameterNumber) + { + var sqlParameters = sqlSpy.GetWholeLog().Split(';')[1]; + var matches = Regex.Matches(sqlParameters, @"([\d\w]+)[\s]+\=", RegexOptions.IgnoreCase); + + // Due to ODBC drivers not supporting parameter names, we have to do a distinct of parameter names. + var distinctParameters = matches.OfType().Select(m => m.Groups[1].Value.Trim()).Distinct().ToList(); + Assert.That(distinctParameters, Has.Count.EqualTo(parameterNumber)); + } + + private NhLinqExpression GetLinqExpression(QueryMode queryMode, IQueryable query, Expression> expression) + { + return GetLinqExpression(queryMode, DmlExpressionRewriter.PrepareExpression(query.Expression, expression)); + } + + private NhLinqExpression GetLinqExpression(QueryMode queryMode, IQueryable query) + { + return GetLinqExpression(queryMode, query.Expression); + } + + private NhLinqExpression GetLinqExpression(IQueryable query) + { + return GetLinqExpression(QueryMode.Select, query.Expression); + } + + private NhLinqExpression GetLinqExpression(QueryMode queryMode, Expression expression) + { + return queryMode == QueryMode.Select + ? new NhLinqExpression(expression, Sfi) + : new NhLinqDmlExpression(queryMode, expression, Sfi); + } } } diff --git a/src/NHibernate.Test/Linq/ConstantTest.cs b/src/NHibernate.Test/Linq/ConstantTest.cs index 2f1f9e01030..f1482abf984 100644 --- a/src/NHibernate.Test/Linq/ConstantTest.cs +++ b/src/NHibernate.Test/Linq/ConstantTest.cs @@ -216,11 +216,11 @@ public void ConstantInWhereDoesNotCauseManyKeys() where c.CustomerId == "ANATR" select c); var preTransformResult = NhRelinqQueryParser.PreTransform(q1.Expression, Sfi); - var expression = ExpressionParameterVisitor.Visit(preTransformResult, Sfi, out var parameters1); + var expression = ExpressionParameterVisitor.Visit(QueryMode.Select, preTransformResult, Sfi, out var parameters1); var k1 = ExpressionKeyVisitor.Visit(expression, parameters1); var preTransformResult2 = NhRelinqQueryParser.PreTransform(q1.Expression, Sfi); - var expression2 = ExpressionParameterVisitor.Visit(preTransformResult2, Sfi, out var parameters2); + var expression2 = ExpressionParameterVisitor.Visit(QueryMode.Select, preTransformResult2, Sfi, out var parameters2); var k2 = ExpressionKeyVisitor.Visit(expression2, parameters2); Assert.That(parameters1, Has.Count.GreaterThan(0), "parameters1"); diff --git a/src/NHibernate.Test/Linq/ParameterTests.cs b/src/NHibernate.Test/Linq/ParameterTests.cs index 11517326095..dd958352465 100644 --- a/src/NHibernate.Test/Linq/ParameterTests.cs +++ b/src/NHibernate.Test/Linq/ParameterTests.cs @@ -4,6 +4,7 @@ using System.Linq.Expressions; using System.Text.RegularExpressions; using NHibernate.DomainModel.Northwind.Entities; +using NHibernate.Linq; using NUnit.Framework; namespace NHibernate.Test.Linq @@ -17,7 +18,8 @@ public void UsingArrayParameterTwice() var ids = new[] {11008, 11019, 11039}; AssertTotalParameters( db.Orders.Where(o => ids.Contains(o.OrderId) && ids.Contains(o.OrderId)), - ids.Length); + ids.Length, + 1); } [Test] @@ -27,7 +29,8 @@ public void UsingTwoArrayParameters() var ids2 = new[] {11008, 11019, 11039}; AssertTotalParameters( db.Orders.Where(o => ids.Contains(o.OrderId) && ids2.Contains(o.OrderId)), - ids.Length + ids2.Length); + ids.Length + ids2.Length, + 2); } [Test] @@ -36,7 +39,8 @@ public void UsingListParameterTwice() var ids = new List {11008, 11019, 11039}; AssertTotalParameters( db.Orders.Where(o => ids.Contains(o.OrderId) && ids.Contains(o.OrderId)), - ids.Count); + ids.Count, + 1); } [Test] @@ -46,7 +50,8 @@ public void UsingTwoListParameters() var ids2 = new List {11008, 11019, 11039}; AssertTotalParameters( db.Orders.Where(o => ids.Contains(o.OrderId) && ids2.Contains(o.OrderId)), - ids.Count + ids2.Count); + ids.Count + ids2.Count, + 2); } [Test] @@ -77,6 +82,26 @@ public void UsingValueTypeParameterTwice() 1); } + [Test] + public void ValidateMixingTwoParametersCacheKeys() + { + var value = 1; + var value2 = 1; + var expression1 = GetLinqExpression(db.Orders.Where(o => o.OrderId == value && o.OrderId != value)); + var expression2 = GetLinqExpression(db.Orders.Where(o => o.OrderId == value && o.OrderId != value2)); + var expression3 = GetLinqExpression(db.Orders.Where(o => o.OrderId == value2 && o.OrderId != value)); + var expression4 = GetLinqExpression(db.Orders.Where(o => o.OrderId == value2 && o.OrderId != value2)); + + Assert.That(expression1.Key, Is.Not.EqualTo(expression2.Key)); + Assert.That(expression1.Key, Is.Not.EqualTo(expression3.Key)); + Assert.That(expression1.Key, Is.EqualTo(expression4.Key)); + + Assert.That(expression2.Key, Is.EqualTo(expression3.Key)); + Assert.That(expression2.Key, Is.Not.EqualTo(expression4.Key)); + + Assert.That(expression3.Key, Is.Not.EqualTo(expression4.Key)); + } + [Test] public void UsingNegateValueTypeParameterTwice() { @@ -95,6 +120,16 @@ public void UsingNegateValueTypeParameter() 2); } + [Test] + public void UsingValueTypeParameterInArray() + { + var id = 11008; + AssertTotalParameters( + db.Orders.Where(o => new[] {id, 11019}.Contains(o.OrderId) && new[] {id, 11019}.Contains(o.OrderId)), + 4, + 2); + } + [Test] public void UsingTwoValueTypeParameters() { @@ -143,6 +178,22 @@ public void UsingTwoObjectPropertyParameters() 2); } + [Test] + public void UsingParameterInWhereSkipTake() + { + var value3 = 1; + var q1 = db.Products.Where(o => o.ProductId < value3).Take(value3).Skip(value3); + AssertTotalParameters(q1, 3); + } + + [Test] + public void UsingParameterInTwoWhere() + { + var value3 = 1; + var q1 = db.Products.Where(o => o.ProductId < value3).Where(o => o.ProductId < value3); + AssertTotalParameters(q1, 1); + } + [Test] public void UsingObjectNestedPropertyParameterTwice() { @@ -200,18 +251,195 @@ public void UsingObjectPropertyParameterWithThirdLevelClosure() 1); } - private static void AssertTotalParameters(IQueryable query, int parameterNumber) + [Test] + public void UsingParameterInDMLInsertIntoFourTimes() + { + var value = "test"; + AssertTotalParameters( + QueryMode.Insert, + db.Customers.Where(c => c.CustomerId == value), + x => new Customer {CustomerId = value, ContactName = value, CompanyName = value}, + 4); + } + + [Test] + public void UsingFourParametersInDMLInsertInto() + { + var value = "test"; + var value2 = "test"; + var value3 = "test"; + var value4 = "test"; + AssertTotalParameters( + QueryMode.Insert, + db.Customers.Where(c => c.CustomerId == value3), + x => new Customer {CustomerId = value4, ContactName = value2, CompanyName = value}, + 4); + } + + [Test] + public void DMLInsertIntoShouldHaveSameCacheKeys() + { + var value = "test"; + var value2 = "test"; + var value3 = "test"; + var value4 = "test"; + var expression1 = GetLinqExpression( + QueryMode.Insert, + db.Customers.Where(c => c.CustomerId == value), + x => new Customer {CustomerId = value, ContactName = value, CompanyName = value}); + var expression2 = GetLinqExpression( + QueryMode.Insert, + db.Customers.Where(c => c.CustomerId == value3), + x => new Customer {CustomerId = value4, ContactName = value2, CompanyName = value}); + + Assert.That(expression1.Key, Is.EqualTo(expression2.Key)); + } + + [Test] + public void UsingParameterInDMLUpdateThreeTimes() + { + var value = "test"; + AssertTotalParameters( + QueryMode.Update, + db.Customers.Where(c => c.CustomerId == value), + x => new Customer {ContactName = value, CompanyName = value}, + 3); + } + + [Test] + public void UsingThreeParametersInDMLUpdate() + { + var value = "test"; + var value2 = "test"; + var value3 = "test"; + AssertTotalParameters( + QueryMode.Update, + db.Customers.Where(c => c.CustomerId == value3), + x => new Customer { ContactName = value2, CompanyName = value }, + 3); + } + + [TestCase(QueryMode.Update)] + [TestCase(QueryMode.UpdateVersioned)] + public void DMLUpdateIntoShouldHaveSameCacheKeys(QueryMode queryMode) + { + var value = "test"; + var value2 = "test"; + var value3 = "test"; + var expression1 = GetLinqExpression( + queryMode, + db.Customers.Where(c => c.CustomerId == value), + x => new Customer {ContactName = value, CompanyName = value}); + var expression2 = GetLinqExpression( + queryMode, + db.Customers.Where(c => c.CustomerId == value3), + x => new Customer {ContactName = value2, CompanyName = value}); + + Assert.That(expression1.Key, Is.EqualTo(expression2.Key)); + } + + [Test] + public void UsingParameterInDMLDeleteTwice() + { + var value = "test"; + AssertTotalParameters( + QueryMode.Delete, + db.Customers.Where(c => c.CustomerId == value && c.CompanyName == value), + 2); + } + + [Test] + public void UsingTwoParametersInDMLDelete() + { + var value = "test"; + var value2 = "test"; + AssertTotalParameters( + QueryMode.Delete, + db.Customers.Where(c => c.CustomerId == value && c.CompanyName == value2), + 2); + } + + [Test] + public void DMLDeleteShouldHaveSameCacheKeys() + { + var value = "test"; + var value2 = "test"; + var expression1 = GetLinqExpression( + QueryMode.Delete, + db.Customers.Where(c => c.CustomerId == value && c.CompanyName == value)); + var expression2 = GetLinqExpression( + QueryMode.Delete, + db.Customers.Where(c => c.CustomerId == value && c.CompanyName == value2)); + + Assert.That(expression1.Key, Is.EqualTo(expression2.Key)); + } + + private void AssertTotalParameters(IQueryable query, int parameterNumber, int? linqParameterNumber = null) { using (var sqlSpy = new SqlLogSpy()) { + // In case of arrays linqParameterNumber and parameterNumber will be different + Assert.That( + GetLinqExpression(query).ParameterValuesByName.Count, + Is.EqualTo(linqParameterNumber ?? parameterNumber), + "Linq expression has different number of parameters"); + query.ToList(); - var sqlParameters = sqlSpy.GetWholeLog().Split(';')[1]; - var matches = Regex.Matches(sqlParameters, @"([\d\w]+)[\s]+\=", RegexOptions.IgnoreCase); + AssertParameters(sqlSpy, parameterNumber); + } + } + + private static void AssertTotalParameters(QueryMode queryMode, IQueryable query, int parameterNumber) + { + AssertTotalParameters(queryMode, query, null, parameterNumber); + } - // Due to ODBC drivers not supporting parameter names, we have to do a distinct of parameter names. - var distinctParameters = matches.OfType().Select(m => m.Groups[1].Value.Trim()).Distinct().ToList(); - Assert.That(distinctParameters, Has.Count.EqualTo(parameterNumber)); + private static void AssertTotalParameters(QueryMode queryMode, IQueryable query, Expression> expression, int parameterNumber) + { + var provider = query.Provider as INhQueryProvider; + Assert.That(provider, Is.Not.Null); + + var dmlExpression = expression != null + ? DmlExpressionRewriter.PrepareExpression(query.Expression, expression) + : query.Expression; + + using (var sqlSpy = new SqlLogSpy()) + { + Assert.That(provider.ExecuteDml(queryMode, dmlExpression), Is.EqualTo(0), "The DML query updated the data"); // Avoid updating the data + AssertParameters(sqlSpy, parameterNumber); } } + + private static void AssertParameters(SqlLogSpy sqlSpy, int parameterNumber) + { + var sqlParameters = sqlSpy.GetWholeLog().Split(';')[1]; + var matches = Regex.Matches(sqlParameters, @"([\d\w]+)[\s]+\=", RegexOptions.IgnoreCase); + + // Due to ODBC drivers not supporting parameter names, we have to do a distinct of parameter names. + var distinctParameters = matches.OfType().Select(m => m.Groups[1].Value.Trim()).Distinct().ToList(); + Assert.That(distinctParameters, Has.Count.EqualTo(parameterNumber)); + } + + private NhLinqExpression GetLinqExpression(QueryMode queryMode, IQueryable query, Expression> expression) + { + return GetLinqExpression(queryMode, DmlExpressionRewriter.PrepareExpression(query.Expression, expression)); + } + + private NhLinqExpression GetLinqExpression(QueryMode queryMode, IQueryable query) + { + return GetLinqExpression(queryMode, query.Expression); + } + + private NhLinqExpression GetLinqExpression(IQueryable query) + { + return GetLinqExpression(QueryMode.Select, query.Expression); + } + + private NhLinqExpression GetLinqExpression(QueryMode queryMode, Expression expression) + { + return queryMode == QueryMode.Select + ? new NhLinqExpression(expression, Sfi) + : new NhLinqDmlExpression(queryMode, expression, Sfi); + } } } diff --git a/src/NHibernate.Test/Linq/TryGetMappedTests.cs b/src/NHibernate.Test/Linq/TryGetMappedTests.cs index 7858c1d25a4..523d9af5739 100644 --- a/src/NHibernate.Test/Linq/TryGetMappedTests.cs +++ b/src/NHibernate.Test/Linq/TryGetMappedTests.cs @@ -774,7 +774,7 @@ private void AssertResult( var expression = query.Expression; var preTransformResult = NhRelinqQueryParser.PreTransform(expression, Sfi); - expression = ExpressionParameterVisitor.Visit(preTransformResult, Sfi, out var constantToParameterMap); + expression = ExpressionParameterVisitor.Visit(QueryMode.Select, preTransformResult, Sfi, out var constantToParameterMap); var queryModel = NhRelinqQueryParser.Parse(expression); var requiredHqlParameters = new List(); var visitorParameters = new VisitorParameters( diff --git a/src/NHibernate/Linq/NhLinqDmlExpression.cs b/src/NHibernate/Linq/NhLinqDmlExpression.cs index 1c5bd7bb20c..b246e86caef 100644 --- a/src/NHibernate/Linq/NhLinqDmlExpression.cs +++ b/src/NHibernate/Linq/NhLinqDmlExpression.cs @@ -5,18 +5,15 @@ namespace NHibernate.Linq { public class NhLinqDmlExpression : NhLinqExpression { - protected override QueryMode QueryMode { get; } - /// /// Entity type to insert or update when the expression is a DML. /// protected override System.Type TargetType => typeof(T); public NhLinqDmlExpression(QueryMode queryMode, Expression expression, ISessionFactoryImplementor sessionFactory) - : base(expression, sessionFactory) + : base(queryMode, expression, sessionFactory) { Key = $"{queryMode.ToString().ToUpperInvariant()} {Key}"; - QueryMode = queryMode; } } } diff --git a/src/NHibernate/Linq/NhLinqExpression.cs b/src/NHibernate/Linq/NhLinqExpression.cs index cc768f227b0..dcbfac1dccd 100644 --- a/src/NHibernate/Linq/NhLinqExpression.cs +++ b/src/NHibernate/Linq/NhLinqExpression.cs @@ -32,13 +32,19 @@ public class NhLinqExpression : IQueryExpression, ICacheableQueryExpression public ExpressionToHqlTranslationResults ExpressionToHqlTranslationResults { get; private set; } - protected virtual QueryMode QueryMode => QueryMode.Select; + protected virtual QueryMode QueryMode { get; } private readonly Expression _expression; private readonly IDictionary _constantToParameterMap; public NhLinqExpression(Expression expression, ISessionFactoryImplementor sessionFactory) + : this(QueryMode.Select, expression, sessionFactory) { + } + + internal NhLinqExpression(QueryMode queryMode, Expression expression, ISessionFactoryImplementor sessionFactory) + { + QueryMode = queryMode; var preTransformResult = NhRelinqQueryParser.PreTransform(expression, sessionFactory); _expression = preTransformResult.Expression; @@ -49,6 +55,7 @@ public NhLinqExpression(Expression expression, ISessionFactoryImplementor sessio LinqLogging.LogExpression("Expression (partially evaluated)", _expression); _expression = ExpressionParameterVisitor.Visit( + QueryMode, preTransformResult, sessionFactory, out _constantToParameterMap); diff --git a/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs b/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs index b187940431f..6e5dcbc4c97 100644 --- a/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs +++ b/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs @@ -20,6 +20,7 @@ public class ExpressionParameterVisitor : RelinqExpressionVisitor private readonly Dictionary _variableParameters = new Dictionary(); private readonly IDictionary _queryVariables; private readonly ISessionFactoryImplementor _sessionFactory; + private readonly QueryMode _queryMode; private static readonly MethodInfo QueryableSkipDefinition = ReflectHelper.FastGetMethodDefinition(Queryable.Skip, default(IQueryable), 0); @@ -37,16 +38,20 @@ public class ExpressionParameterVisitor : RelinqExpressionVisitor }; // Since v5.3 - [Obsolete("Please use overload with preTransformationResult parameter instead.")] + [Obsolete("Please use overload with preTransformationResult and queryMode parameters instead.")] public ExpressionParameterVisitor(ISessionFactoryImplementor sessionFactory) { _sessionFactory = sessionFactory; + // In order to keep the old behavior use a DML query mode to generate parameters for each constant + _queryMode = QueryMode.Delete; } public ExpressionParameterVisitor( + QueryMode queryMode, ISessionFactoryImplementor sessionFactory, PreTransformationResult preTransformationResult) { + _queryMode = queryMode; _sessionFactory = sessionFactory; _queryVariables = preTransformationResult.QueryVariables; } @@ -62,11 +67,12 @@ public static IDictionary Visit(Expression e } public static Expression Visit( + QueryMode queryMode, PreTransformationResult preTransformationResult, ISessionFactoryImplementor sessionFactory, out IDictionary parameters) { - var visitor = new ExpressionParameterVisitor(sessionFactory, preTransformationResult); + var visitor = new ExpressionParameterVisitor(queryMode, sessionFactory, preTransformationResult); var expression = visitor.Visit(preTransformationResult.Expression); parameters = visitor._parameters; @@ -142,9 +148,11 @@ protected override Expression VisitConstant(ConstantExpression expression) // comes up, it would be nice to combine the HQL parameter type determination code // and the Expression information. - // Create only one parameter for the same variable + // When QueryMode.Select, create only one parameter for the same variable. HQL does not support + // reusing parameters for DML queries. NamedParameter parameter = null; - if (_queryVariables != null && + if (_queryMode == QueryMode.Select && + _queryVariables != null && _queryVariables.TryGetValue(expression, out var variable) && !_variableParameters.TryGetValue(variable, out parameter)) { From af489539f23bbbe3459a8f3df9ee64c2b2d9f048 Mon Sep 17 00:00:00 2001 From: maca88 Date: Tue, 24 Mar 2020 20:44:45 +0100 Subject: [PATCH 05/18] Pass QueryMode to PreTransform method instead --- src/NHibernate.Test/Linq/ConstantTest.cs | 9 ++-- src/NHibernate.Test/Linq/TryGetMappedTests.cs | 4 +- src/NHibernate/Linq/NhLinqExpression.cs | 10 ++--- src/NHibernate/Linq/NhRelinqQueryParser.cs | 19 +++++--- .../Visitors/ExpressionParameterVisitor.cs | 22 +++------- .../NhPartialEvaluatingExpressionVisitor.cs | 30 ++++++------- .../Visitors/PreTransformationParameters.cs | 44 +++++++++++++++++++ .../Linq/Visitors/PreTransformationResult.cs | 8 ++++ 8 files changed, 95 insertions(+), 51 deletions(-) create mode 100644 src/NHibernate/Linq/Visitors/PreTransformationParameters.cs diff --git a/src/NHibernate.Test/Linq/ConstantTest.cs b/src/NHibernate.Test/Linq/ConstantTest.cs index f1482abf984..bc70b04b27b 100644 --- a/src/NHibernate.Test/Linq/ConstantTest.cs +++ b/src/NHibernate.Test/Linq/ConstantTest.cs @@ -215,12 +215,13 @@ public void ConstantInWhereDoesNotCauseManyKeys() var q2 = (from c in db.Customers where c.CustomerId == "ANATR" select c); - var preTransformResult = NhRelinqQueryParser.PreTransform(q1.Expression, Sfi); - var expression = ExpressionParameterVisitor.Visit(QueryMode.Select, preTransformResult, Sfi, out var parameters1); + var preTransformParameters = new PreTransformationParameters(QueryMode.Select, Sfi); + var preTransformResult = NhRelinqQueryParser.PreTransform(q1.Expression, preTransformParameters); + var expression = ExpressionParameterVisitor.Visit(preTransformResult, out var parameters1); var k1 = ExpressionKeyVisitor.Visit(expression, parameters1); - var preTransformResult2 = NhRelinqQueryParser.PreTransform(q1.Expression, Sfi); - var expression2 = ExpressionParameterVisitor.Visit(QueryMode.Select, preTransformResult2, Sfi, out var parameters2); + var preTransformResult2 = NhRelinqQueryParser.PreTransform(q1.Expression, preTransformParameters); + var expression2 = ExpressionParameterVisitor.Visit(preTransformResult2, out var parameters2); var k2 = ExpressionKeyVisitor.Visit(expression2, parameters2); Assert.That(parameters1, Has.Count.GreaterThan(0), "parameters1"); diff --git a/src/NHibernate.Test/Linq/TryGetMappedTests.cs b/src/NHibernate.Test/Linq/TryGetMappedTests.cs index 523d9af5739..20610d32bad 100644 --- a/src/NHibernate.Test/Linq/TryGetMappedTests.cs +++ b/src/NHibernate.Test/Linq/TryGetMappedTests.cs @@ -773,8 +773,8 @@ private void AssertResult( expectedComponentType = expectedComponentType ?? (o => o == null); var expression = query.Expression; - var preTransformResult = NhRelinqQueryParser.PreTransform(expression, Sfi); - expression = ExpressionParameterVisitor.Visit(QueryMode.Select, preTransformResult, Sfi, out var constantToParameterMap); + var preTransformResult = NhRelinqQueryParser.PreTransform(expression, new PreTransformationParameters(QueryMode.Select, Sfi)); + expression = ExpressionParameterVisitor.Visit(preTransformResult, out var constantToParameterMap); var queryModel = NhRelinqQueryParser.Parse(expression); var requiredHqlParameters = new List(); var visitorParameters = new VisitorParameters( diff --git a/src/NHibernate/Linq/NhLinqExpression.cs b/src/NHibernate/Linq/NhLinqExpression.cs index dcbfac1dccd..817bfe459e2 100644 --- a/src/NHibernate/Linq/NhLinqExpression.cs +++ b/src/NHibernate/Linq/NhLinqExpression.cs @@ -45,7 +45,9 @@ public NhLinqExpression(Expression expression, ISessionFactoryImplementor sessio internal NhLinqExpression(QueryMode queryMode, Expression expression, ISessionFactoryImplementor sessionFactory) { QueryMode = queryMode; - var preTransformResult = NhRelinqQueryParser.PreTransform(expression, sessionFactory); + var preTransformResult = NhRelinqQueryParser.PreTransform( + expression, + new PreTransformationParameters(queryMode, sessionFactory)); _expression = preTransformResult.Expression; // We want logging to be as close as possible to the original expression sent from the @@ -54,11 +56,7 @@ internal NhLinqExpression(QueryMode queryMode, Expression expression, ISessionFa // referenced from the main query. LinqLogging.LogExpression("Expression (partially evaluated)", _expression); - _expression = ExpressionParameterVisitor.Visit( - QueryMode, - preTransformResult, - sessionFactory, - out _constantToParameterMap); + _expression = ExpressionParameterVisitor.Visit(preTransformResult, out _constantToParameterMap); ParameterValuesByName = _constantToParameterMap.Values.Distinct().ToDictionary(p => p.Name, p => System.Tuple.Create(p.Value, p.Type)); diff --git a/src/NHibernate/Linq/NhRelinqQueryParser.cs b/src/NHibernate/Linq/NhRelinqQueryParser.cs index 8604fd00ec9..56406823ad2 100644 --- a/src/NHibernate/Linq/NhRelinqQueryParser.cs +++ b/src/NHibernate/Linq/NhRelinqQueryParser.cs @@ -54,10 +54,12 @@ static NhRelinqQueryParser() /// /// The expression to transform. /// The transformed expression. - [Obsolete("Use overload with an additional sessionFactory parameter")] + [Obsolete("Use overload with PreTransformationParameters parameter")] public static Expression PreTransform(Expression expression) { - return PreTransform(expression, null).Expression; + // In order to keep the old behavior use a DML query mode to skip detecting variables, + // which will then generate parameters for each constant expression + return PreTransform(expression, new PreTransformationParameters(QueryMode.Delete, null)).Expression; } /// @@ -65,17 +67,20 @@ public static Expression PreTransform(Expression expression) /// expression key computing and parsing. /// /// The expression to transform. - /// The session factory. + /// The parameters used in the transformation process. /// that contains the transformed expression. - public static PreTransformationResult PreTransform(Expression expression, ISessionFactoryImplementor sessionFactory) + public static PreTransformationResult PreTransform(Expression expression, PreTransformationParameters parameters) { - var queryVariables = new Dictionary(); + parameters.EvaluatableExpressionFilter = new NhEvaluatableExpressionFilter(parameters.SessionFactory); + parameters.QueryVariables = new Dictionary(); + var partiallyEvaluatedExpression = NhPartialEvaluatingExpressionVisitor - .EvaluateIndependentSubtrees(expression, new NhEvaluatableExpressionFilter(sessionFactory), queryVariables); + .EvaluateIndependentSubtrees(expression, parameters); return new PreTransformationResult( PreProcessor.Process(partiallyEvaluatedExpression), - queryVariables); + parameters.SessionFactory, + parameters.QueryVariables); } public static QueryModel Parse(Expression expression) diff --git a/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs b/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs index 6e5dcbc4c97..45134248a51 100644 --- a/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs +++ b/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs @@ -20,7 +20,6 @@ public class ExpressionParameterVisitor : RelinqExpressionVisitor private readonly Dictionary _variableParameters = new Dictionary(); private readonly IDictionary _queryVariables; private readonly ISessionFactoryImplementor _sessionFactory; - private readonly QueryMode _queryMode; private static readonly MethodInfo QueryableSkipDefinition = ReflectHelper.FastGetMethodDefinition(Queryable.Skip, default(IQueryable), 0); @@ -38,21 +37,15 @@ public class ExpressionParameterVisitor : RelinqExpressionVisitor }; // Since v5.3 - [Obsolete("Please use overload with preTransformationResult and queryMode parameters instead.")] + [Obsolete("Please use overload with preTransformationResult parameter instead.")] public ExpressionParameterVisitor(ISessionFactoryImplementor sessionFactory) { _sessionFactory = sessionFactory; - // In order to keep the old behavior use a DML query mode to generate parameters for each constant - _queryMode = QueryMode.Delete; } - public ExpressionParameterVisitor( - QueryMode queryMode, - ISessionFactoryImplementor sessionFactory, - PreTransformationResult preTransformationResult) + public ExpressionParameterVisitor(PreTransformationResult preTransformationResult) { - _queryMode = queryMode; - _sessionFactory = sessionFactory; + _sessionFactory = preTransformationResult.SessionFactory; _queryVariables = preTransformationResult.QueryVariables; } @@ -67,12 +60,10 @@ public static IDictionary Visit(Expression e } public static Expression Visit( - QueryMode queryMode, PreTransformationResult preTransformationResult, - ISessionFactoryImplementor sessionFactory, out IDictionary parameters) { - var visitor = new ExpressionParameterVisitor(queryMode, sessionFactory, preTransformationResult); + var visitor = new ExpressionParameterVisitor(preTransformationResult); var expression = visitor.Visit(preTransformationResult.Expression); parameters = visitor._parameters; @@ -148,11 +139,8 @@ protected override Expression VisitConstant(ConstantExpression expression) // comes up, it would be nice to combine the HQL parameter type determination code // and the Expression information. - // When QueryMode.Select, create only one parameter for the same variable. HQL does not support - // reusing parameters for DML queries. NamedParameter parameter = null; - if (_queryMode == QueryMode.Select && - _queryVariables != null && + if (_queryVariables != null && _queryVariables.TryGetValue(expression, out var variable) && !_variableParameters.TryGetValue(variable, out parameter)) { diff --git a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs index ad86f1f82e3..458fcb21b1d 100644 --- a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs +++ b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs @@ -32,28 +32,26 @@ internal sealed class NhPartialEvaluatingExpressionVisitor : RelinqExpressionVis /// public static Expression EvaluateIndependentSubtrees( Expression expressionTree, - IEvaluatableExpressionFilter evaluatableExpressionFilter, - IDictionary queryVariables) + PreTransformationParameters preTransformationParameters) { - var partialEvaluationInfo = EvaluatableTreeFindingExpressionVisitor.Analyze(expressionTree, evaluatableExpressionFilter); - var visitor = new NhPartialEvaluatingExpressionVisitor(partialEvaluationInfo, evaluatableExpressionFilter, queryVariables); + var partialEvaluationInfo = EvaluatableTreeFindingExpressionVisitor.Analyze( + expressionTree, + preTransformationParameters.EvaluatableExpressionFilter); + var visitor = new NhPartialEvaluatingExpressionVisitor(partialEvaluationInfo, preTransformationParameters); return visitor.Visit(expressionTree); } // _partialEvaluationInfo contains a list of the expressions that are safe to be evaluated. private readonly PartialEvaluationInfo _partialEvaluationInfo; - private readonly IEvaluatableExpressionFilter _evaluatableExpressionFilter; - private readonly IDictionary _queryVariables; + private readonly PreTransformationParameters _preTransformationParameters; private NhPartialEvaluatingExpressionVisitor( PartialEvaluationInfo partialEvaluationInfo, - IEvaluatableExpressionFilter evaluatableExpressionFilter, - IDictionary queryVariables) + PreTransformationParameters preTransformationParameters) { _partialEvaluationInfo = partialEvaluationInfo; - _evaluatableExpressionFilter = evaluatableExpressionFilter; - _queryVariables = queryVariables; + _preTransformationParameters = preTransformationParameters; } public override Expression Visit(Expression expression) @@ -81,7 +79,7 @@ public override Expression Visit(Expression expression) if (evaluatedExpression != expression) { - evaluatedExpression = EvaluateIndependentSubtrees(evaluatedExpression, _evaluatableExpressionFilter, _queryVariables); + evaluatedExpression = EvaluateIndependentSubtrees(evaluatedExpression, _preTransformationParameters); } // When having multiple level closure, we have to evaluate each closure independently @@ -90,13 +88,15 @@ public override Expression Visit(Expression expression) evaluatedExpression = VisitConstant(constantExpression); } - // Variables in expressions are never a constant, they are encapsulated as fields of a compiler generated class + // Variables in expressions are never a constant, they are encapsulated as fields of a compiler generated class. + // Skip detecting variables for DML queries as HQL does not support reusing parameters for them. if (expression.NodeType != ExpressionType.Constant && + _preTransformationParameters.QueryMode == QueryMode.Select && evaluatedExpression is ConstantExpression variableConstant && - !_queryVariables.ContainsKey(variableConstant) && + !_preTransformationParameters.QueryVariables.ContainsKey(variableConstant) && IsVariable(expression, out var path, out var closureContext)) { - _queryVariables.Add(variableConstant, new QueryVariable(path, closureContext)); + _preTransformationParameters.QueryVariables.Add(variableConstant, new QueryVariable(path, closureContext)); } return evaluatedExpression; @@ -106,7 +106,7 @@ protected override Expression VisitConstant(ConstantExpression expression) { if (expression.Value is Expression value) { - return EvaluateIndependentSubtrees(value, _evaluatableExpressionFilter, _queryVariables); + return EvaluateIndependentSubtrees(value, _preTransformationParameters); } return base.VisitConstant(expression); diff --git a/src/NHibernate/Linq/Visitors/PreTransformationParameters.cs b/src/NHibernate/Linq/Visitors/PreTransformationParameters.cs new file mode 100644 index 00000000000..e73fa628746 --- /dev/null +++ b/src/NHibernate/Linq/Visitors/PreTransformationParameters.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using NHibernate.Engine; +using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation; + +namespace NHibernate.Linq.Visitors +{ + /// + /// Contains the information needed by to perform an early transformation. + /// + public class PreTransformationParameters + { + /// + /// The default constructor. + /// + /// The query mode of the expression to pre-transform. + /// The session factory used in the pre-transform process. + public PreTransformationParameters(QueryMode queryMode, ISessionFactoryImplementor sessionFactory) + { + QueryMode = queryMode; + SessionFactory = sessionFactory; + } + + /// + /// The query mode of the expression to pre-transform. + /// + public QueryMode QueryMode { get; } + + /// + /// The session factory used in the pre-transform process. + /// + public ISessionFactoryImplementor SessionFactory { get; } + + /// + /// The filter which decides whether a part of the expression will be pre-evalauted or not. + /// + internal IEvaluatableExpressionFilter EvaluatableExpressionFilter { get; set; } + + /// + /// A dictionary of that were evaluated from variables. + /// + internal IDictionary QueryVariables { get; set; } + } +} diff --git a/src/NHibernate/Linq/Visitors/PreTransformationResult.cs b/src/NHibernate/Linq/Visitors/PreTransformationResult.cs index ddd57f2d6b8..6f55ddc7bba 100644 --- a/src/NHibernate/Linq/Visitors/PreTransformationResult.cs +++ b/src/NHibernate/Linq/Visitors/PreTransformationResult.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq.Expressions; +using NHibernate.Engine; namespace NHibernate.Linq.Visitors { @@ -10,9 +11,11 @@ public class PreTransformationResult { internal PreTransformationResult( Expression expression, + ISessionFactoryImplementor sessionFactory, IDictionary queryVariables) { Expression = expression; + SessionFactory = sessionFactory; QueryVariables = queryVariables; } @@ -21,6 +24,11 @@ internal PreTransformationResult( /// public Expression Expression { get; } + /// + /// The session factory used in the pre-transform process. + /// + public ISessionFactoryImplementor SessionFactory { get; } + /// /// A dictionary of that were evaluated from variables. /// From 57caa6e15988d46342445f3925f5ffccd6dafcdf Mon Sep 17 00:00:00 2001 From: maca88 Date: Wed, 25 Mar 2020 21:24:54 +0100 Subject: [PATCH 06/18] Add query plan cache check --- src/NHibernate.Test/Async/Linq/ParameterTests.cs | 14 ++++++++++++++ src/NHibernate.Test/Linq/ParameterTests.cs | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/NHibernate.Test/Async/Linq/ParameterTests.cs b/src/NHibernate.Test/Async/Linq/ParameterTests.cs index fb2b33f66f5..81c403c2fa7 100644 --- a/src/NHibernate.Test/Async/Linq/ParameterTests.cs +++ b/src/NHibernate.Test/Async/Linq/ParameterTests.cs @@ -12,9 +12,12 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using System.Text.RegularExpressions; using NHibernate.DomainModel.Northwind.Entities; +using NHibernate.Engine.Query; using NHibernate.Linq; +using NHibernate.Util; using NUnit.Framework; namespace NHibernate.Test.Linq @@ -323,7 +326,18 @@ public async Task UsingTwoParametersInDMLDeleteAsync() Is.EqualTo(linqParameterNumber ?? parameterNumber), "Linq expression has different number of parameters"); + var queryPlanCacheType = typeof(QueryPlanCache); + var cache = (SoftLimitMRUCache) + queryPlanCacheType + .GetField("planCache", BindingFlags.Instance | BindingFlags.NonPublic) + .GetValue(Sfi.QueryPlanCache); + cache.Clear(); + await (query.ToListAsync(cancellationToken)); + + // In case of arrays two query plans will be stored, one with an one without expended parameters + Assert.That(cache, Has.Count.EqualTo(linqParameterNumber.HasValue ? 2 : 1), "Query should be cacheable"); + AssertParameters(sqlSpy, parameterNumber); } } diff --git a/src/NHibernate.Test/Linq/ParameterTests.cs b/src/NHibernate.Test/Linq/ParameterTests.cs index dd958352465..5fdd37de185 100644 --- a/src/NHibernate.Test/Linq/ParameterTests.cs +++ b/src/NHibernate.Test/Linq/ParameterTests.cs @@ -2,9 +2,12 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using System.Text.RegularExpressions; using NHibernate.DomainModel.Northwind.Entities; +using NHibernate.Engine.Query; using NHibernate.Linq; +using NHibernate.Util; using NUnit.Framework; namespace NHibernate.Test.Linq @@ -384,7 +387,18 @@ private void AssertTotalParameters(IQueryable query, int parameterNumber, Is.EqualTo(linqParameterNumber ?? parameterNumber), "Linq expression has different number of parameters"); + var queryPlanCacheType = typeof(QueryPlanCache); + var cache = (SoftLimitMRUCache) + queryPlanCacheType + .GetField("planCache", BindingFlags.Instance | BindingFlags.NonPublic) + .GetValue(Sfi.QueryPlanCache); + cache.Clear(); + query.ToList(); + + // In case of arrays two query plans will be stored, one with an one without expended parameters + Assert.That(cache, Has.Count.EqualTo(linqParameterNumber.HasValue ? 2 : 1), "Query should be cacheable"); + AssertParameters(sqlSpy, parameterNumber); } } From 3948590e21e2d80f8bd0da8ed7e6eec65d3a3b23 Mon Sep 17 00:00:00 2001 From: maca88 Date: Tue, 31 Mar 2020 17:41:30 +0200 Subject: [PATCH 07/18] Add license and copyright for NhPartialEvaluatingExpressionVisitor --- .../NhPartialEvaluatingExpressionVisitor.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs index 458fcb21b1d..be15f07f986 100644 --- a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs +++ b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs @@ -1,4 +1,21 @@ -using System; +// Copyright (c) rubicon IT GmbH, www.rubicon.eu +// +// See the NOTICE file distributed with this work for additional information +// regarding copyright ownership. rubicon licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may not use this +// file except in compliance with the License. You may obtain a copy of the +// License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// + +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; From 967199d0b6b1789d3d08574d120788feaeb191b1 Mon Sep 17 00:00:00 2001 From: maca88 Date: Sun, 26 Apr 2020 22:50:46 +0200 Subject: [PATCH 08/18] Update src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Frédéric Delaporte <12201973+fredericDelaporte@users.noreply.github.com> --- .../Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs index be15f07f986..29cce50b851 100644 --- a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs +++ b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs @@ -44,6 +44,8 @@ namespace NHibernate.Linq.Visitors /// internal sealed class NhPartialEvaluatingExpressionVisitor : RelinqExpressionVisitor { + #region Relinq adjusted code + /// /// Takes an expression tree and finds and evaluates all its evaluatable subtrees. /// From 793e0e4ea1925429e448ab667188046f8888eb95 Mon Sep 17 00:00:00 2001 From: maca88 Date: Sun, 26 Apr 2020 22:51:03 +0200 Subject: [PATCH 09/18] Update src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Frédéric Delaporte <12201973+fredericDelaporte@users.noreply.github.com> --- .../Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs index 29cce50b851..9fce6ff0f5a 100644 --- a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs +++ b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs @@ -121,10 +121,6 @@ evaluatedExpression is ConstantExpression variableConstant && return evaluatedExpression; } - protected override Expression VisitConstant(ConstantExpression expression) - { - if (expression.Value is Expression value) - { return EvaluateIndependentSubtrees(value, _preTransformationParameters); } From 4d463727b92e59a6658f8318c38917e2f3b43c2d Mon Sep 17 00:00:00 2001 From: maca88 Date: Sun, 26 Apr 2020 22:51:16 +0200 Subject: [PATCH 10/18] Update src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Frédéric Delaporte <12201973+fredericDelaporte@users.noreply.github.com> --- .../Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs index 9fce6ff0f5a..dcdbe8261ce 100644 --- a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs +++ b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs @@ -121,12 +121,6 @@ evaluatedExpression is ConstantExpression variableConstant && return evaluatedExpression; } - return EvaluateIndependentSubtrees(value, _preTransformationParameters); - } - - return base.VisitConstant(expression); - } - /// /// Evaluates an evaluatable subtree, i.e. an independent expression tree that is compilable and executable /// without any data being passed in. The result of the evaluation is returned as a ; if the subtree From aede4887ea767113d3d6170052f90176a89d7984 Mon Sep 17 00:00:00 2001 From: maca88 Date: Sun, 26 Apr 2020 22:51:29 +0200 Subject: [PATCH 11/18] Update src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Frédéric Delaporte <12201973+fredericDelaporte@users.noreply.github.com> --- .../Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs index dcdbe8261ce..e93a5ec7bc2 100644 --- a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs +++ b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs @@ -101,6 +101,8 @@ public override Expression Visit(Expression expression) evaluatedExpression = EvaluateIndependentSubtrees(evaluatedExpression, _preTransformationParameters); } + #region NH additions + // When having multiple level closure, we have to evaluate each closure independently if (evaluatedExpression is ConstantExpression constantExpression) { From cc6cc7e2838ebe435bc211c8a7d7c0a041c2dddc Mon Sep 17 00:00:00 2001 From: maca88 Date: Sun, 26 Apr 2020 22:51:47 +0200 Subject: [PATCH 12/18] Update src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Frédéric Delaporte <12201973+fredericDelaporte@users.noreply.github.com> --- .../Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs index e93a5ec7bc2..3a62e39e188 100644 --- a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs +++ b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs @@ -120,6 +120,8 @@ evaluatedExpression is ConstantExpression variableConstant && _preTransformationParameters.QueryVariables.Add(variableConstant, new QueryVariable(path, closureContext)); } + #endregion + return evaluatedExpression; } From 934e16cfb573f4dd08a5e08a588969389127702d Mon Sep 17 00:00:00 2001 From: maca88 Date: Sun, 26 Apr 2020 22:52:02 +0200 Subject: [PATCH 13/18] Update src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Frédéric Delaporte <12201973+fredericDelaporte@users.noreply.github.com> --- .../Visitors/NhPartialEvaluatingExpressionVisitor.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs index 3a62e39e188..74a1b35fb88 100644 --- a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs +++ b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs @@ -153,6 +153,17 @@ private Expression EvaluateSubtree(Expression subtree) } } + #endregion + + protected override Expression VisitConstant(ConstantExpression expression) + { + if (expression.Value is Expression value) + { + return EvaluateIndependentSubtrees(value, _preTransformationParameters); + } + return base.VisitConstant(expression); + } + private bool IsVariable(Expression expression, out string path, out object closureContext) { Expression childExpression; From 739ce82f70a5a5428cf7a9a76bc015b73c6cdd73 Mon Sep 17 00:00:00 2001 From: maca88 Date: Sun, 26 Apr 2020 23:15:29 +0200 Subject: [PATCH 14/18] Code review changes --- .../Async/Linq/ParameterTests.cs | 2 +- src/NHibernate.Test/Linq/ParameterTests.cs | 2 +- .../RemoveCharToIntConversion.cs | 22 +++++++- .../NhPartialEvaluatingExpressionVisitor.cs | 52 +++---------------- src/NHibernate/Util/ExpressionsHelper.cs | 44 ++++++++++++++++ 5 files changed, 73 insertions(+), 49 deletions(-) diff --git a/src/NHibernate.Test/Async/Linq/ParameterTests.cs b/src/NHibernate.Test/Async/Linq/ParameterTests.cs index 81c403c2fa7..4fbebe3e78b 100644 --- a/src/NHibernate.Test/Async/Linq/ParameterTests.cs +++ b/src/NHibernate.Test/Async/Linq/ParameterTests.cs @@ -112,7 +112,7 @@ public async Task UsingNegateValueTypeParameterAsync() var value = 1; await (AssertTotalParametersAsync( db.Orders.Where(o => o.OrderId == value && o.OrderId != -value), - 2)); + 1)); } [Test] diff --git a/src/NHibernate.Test/Linq/ParameterTests.cs b/src/NHibernate.Test/Linq/ParameterTests.cs index 5fdd37de185..920fa565129 100644 --- a/src/NHibernate.Test/Linq/ParameterTests.cs +++ b/src/NHibernate.Test/Linq/ParameterTests.cs @@ -120,7 +120,7 @@ public void UsingNegateValueTypeParameter() var value = 1; AssertTotalParameters( db.Orders.Where(o => o.OrderId == value && o.OrderId != -value), - 2); + 1); } [Test] diff --git a/src/NHibernate/Linq/ExpressionTransformers/RemoveCharToIntConversion.cs b/src/NHibernate/Linq/ExpressionTransformers/RemoveCharToIntConversion.cs index 3e0c19d4ca1..aabdea69916 100644 --- a/src/NHibernate/Linq/ExpressionTransformers/RemoveCharToIntConversion.cs +++ b/src/NHibernate/Linq/ExpressionTransformers/RemoveCharToIntConversion.cs @@ -35,6 +35,19 @@ public Expression Transform(BinaryExpression expression) if (!lhsIsConvertExpression && !rhsIsConvertExpression) return expression; + // Variables are not converted to constants (E.g: o.CharProperty == charVariable) + if (lhsIsConvertExpression && rhsIsConvertExpression) + { + var lhsConvertExpression = (UnaryExpression) lhs; + var rhsConvertExpression = (UnaryExpression) rhs; + if (!IsConvertCharToInt(lhsConvertExpression) || !IsConvertCharToInt(rhsConvertExpression)) + { + return expression; + } + + return Expression.MakeBinary(expression.NodeType, lhsConvertExpression.Operand, rhsConvertExpression.Operand); + } + var lhsIsConstantExpression = IsConstantExpression(lhs); var rhsIsConstantExpression = IsConstantExpression(rhs); @@ -43,7 +56,7 @@ public Expression Transform(BinaryExpression expression) var convertExpression = lhsIsConvertExpression ? (UnaryExpression)lhs : (UnaryExpression)rhs; var constantExpression = lhsIsConstantExpression ? (ConstantExpression)lhs : (ConstantExpression)rhs; - if (convertExpression.Type == typeof(int) && convertExpression.Operand.Type == typeof(char) && constantExpression.Type == typeof(int)) + if (IsConvertCharToInt(convertExpression) && constantExpression.Type == typeof(int)) { var constant = Expression.Constant(Convert.ToChar((int)constantExpression.Value)); @@ -56,6 +69,11 @@ public Expression Transform(BinaryExpression expression) return expression; } + private static bool IsConvertCharToInt(UnaryExpression expression) + { + return expression.Type == typeof(int) && expression.Operand.Type == typeof(char); + } + private static bool IsConvertExpression(Expression expression) { return (expression.NodeType == ExpressionType.Convert); @@ -71,4 +89,4 @@ public ExpressionType[] SupportedExpressionTypes get { return _supportedExpressionTypes; } } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs index 74a1b35fb88..b9284658607 100644 --- a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs +++ b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs @@ -16,11 +16,8 @@ // using System; -using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Reflection; -using System.Runtime.CompilerServices; using NHibernate.Collection; using NHibernate.Engine; using NHibernate.Linq.Functions; @@ -112,10 +109,10 @@ public override Expression Visit(Expression expression) // Variables in expressions are never a constant, they are encapsulated as fields of a compiler generated class. // Skip detecting variables for DML queries as HQL does not support reusing parameters for them. if (expression.NodeType != ExpressionType.Constant && - _preTransformationParameters.QueryMode == QueryMode.Select && + _preTransformationParameters.QueryMode == QueryMode.Select && evaluatedExpression is ConstantExpression variableConstant && !_preTransformationParameters.QueryVariables.ContainsKey(variableConstant) && - IsVariable(expression, out var path, out var closureContext)) + ExpressionsHelper.IsVariable(expression, out var path, out var closureContext)) { _preTransformationParameters.QueryVariables.Add(variableConstant, new QueryVariable(path, closureContext)); } @@ -163,46 +160,6 @@ protected override Expression VisitConstant(ConstantExpression expression) } return base.VisitConstant(expression); } - - private bool IsVariable(Expression expression, out string path, out object closureContext) - { - Expression childExpression; - string currentPath; - switch (expression) - { - case MemberExpression memberExpression: - childExpression = memberExpression.Expression; - currentPath = memberExpression.Member.Name; - break; - case ConstantExpression constantExpression: - path = null; - if (constantExpression.Type.Attributes.HasFlag(TypeAttributes.NestedPrivate) && - Attribute.IsDefined(constantExpression.Type, typeof(CompilerGeneratedAttribute), inherit: true)) - { - closureContext = constantExpression.Value; - return true; - } - - closureContext = null; - return false; - case UnaryExpression unaryExpression: - childExpression = unaryExpression.Operand; - currentPath = $"({unaryExpression.NodeType})"; - break; - default: - path = null; - closureContext = null; - return false; - } - - if (!IsVariable(childExpression, out path, out closureContext)) - { - return false; - } - - path = path != null ? $"{path}_{currentPath}" : currentPath; - return true; - } } internal struct QueryVariable : IEquatable @@ -255,6 +212,11 @@ public override bool IsEvaluatableConstant(ConstantExpression node) return base.IsEvaluatableConstant(node); } + public override bool IsEvaluatableUnary(UnaryExpression node) + { + return !ExpressionsHelper.IsVariable(node.Operand, out _, out _); + } + public override bool IsEvaluatableMember(MemberExpression node) { if (node == null) diff --git a/src/NHibernate/Util/ExpressionsHelper.cs b/src/NHibernate/Util/ExpressionsHelper.cs index 376a42ccda0..071481ecc00 100644 --- a/src/NHibernate/Util/ExpressionsHelper.cs +++ b/src/NHibernate/Util/ExpressionsHelper.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Runtime.CompilerServices; using NHibernate.Engine; using NHibernate.Linq; using NHibernate.Linq.Expressions; @@ -28,6 +29,49 @@ public static MemberInfo DecodeMemberAccessExpression(Expressi return ((MemberExpression)expression.Body).Member; } + /// + /// Check whether the given expression represent a variable. + /// + /// The expression to check. + /// The path of the variable. + /// The closure context where the variable is stored. + /// + internal static bool IsVariable(Expression expression, out string path, out object closureContext) + { + Expression childExpression; + string currentPath; + switch (expression) + { + case MemberExpression memberExpression: + childExpression = memberExpression.Expression; + currentPath = memberExpression.Member.Name; + break; + case ConstantExpression constantExpression: + path = null; + if (constantExpression.Type.Attributes.HasFlag(TypeAttributes.NestedPrivate) && + Attribute.IsDefined(constantExpression.Type, typeof(CompilerGeneratedAttribute), inherit: true)) + { + closureContext = constantExpression.Value; + return true; + } + + closureContext = null; + return false; + default: + path = null; + closureContext = null; + return false; + } + + if (!IsVariable(childExpression, out path, out closureContext)) + { + return false; + } + + path = path != null ? $"{currentPath}_{path}" : currentPath; + return true; + } + /// /// Get the mapped type for the given expression. /// From 3a2941dc8f2d7c44a91d635b8130a5c9f6951e52 Mon Sep 17 00:00:00 2001 From: maca88 Date: Mon, 27 Apr 2020 00:46:52 +0200 Subject: [PATCH 15/18] Fix missing doc --- src/NHibernate/Util/ExpressionsHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NHibernate/Util/ExpressionsHelper.cs b/src/NHibernate/Util/ExpressionsHelper.cs index 071481ecc00..92476759ce7 100644 --- a/src/NHibernate/Util/ExpressionsHelper.cs +++ b/src/NHibernate/Util/ExpressionsHelper.cs @@ -35,7 +35,7 @@ public static MemberInfo DecodeMemberAccessExpression(Expressi /// The expression to check. /// The path of the variable. /// The closure context where the variable is stored. - /// + /// Whether the expression represents a variable. internal static bool IsVariable(Expression expression, out string path, out object closureContext) { Expression childExpression; From 1602029686d0061f51ade12f97ee02f3f8573de3 Mon Sep 17 00:00:00 2001 From: maca88 Date: Mon, 4 May 2020 00:16:53 +0200 Subject: [PATCH 16/18] Fix constant test --- src/NHibernate.Test/Linq/ConstantTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NHibernate.Test/Linq/ConstantTest.cs b/src/NHibernate.Test/Linq/ConstantTest.cs index bc70b04b27b..6b693ddbc4a 100644 --- a/src/NHibernate.Test/Linq/ConstantTest.cs +++ b/src/NHibernate.Test/Linq/ConstantTest.cs @@ -220,7 +220,7 @@ public void ConstantInWhereDoesNotCauseManyKeys() var expression = ExpressionParameterVisitor.Visit(preTransformResult, out var parameters1); var k1 = ExpressionKeyVisitor.Visit(expression, parameters1); - var preTransformResult2 = NhRelinqQueryParser.PreTransform(q1.Expression, preTransformParameters); + var preTransformResult2 = NhRelinqQueryParser.PreTransform(q2.Expression, preTransformParameters); var expression2 = ExpressionParameterVisitor.Visit(preTransformResult2, out var parameters2); var k2 = ExpressionKeyVisitor.Visit(expression2, parameters2); From 295585e02ae1c54da7ad1fe3f59732e4b2ee5f30 Mon Sep 17 00:00:00 2001 From: maca88 Date: Tue, 5 May 2020 21:52:55 +0200 Subject: [PATCH 17/18] Code review changes --- src/NHibernate/Linq/NhRelinqQueryParser.cs | 1 + .../Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs | 2 +- src/NHibernate/Linq/Visitors/PreTransformationParameters.cs | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/NHibernate/Linq/NhRelinqQueryParser.cs b/src/NHibernate/Linq/NhRelinqQueryParser.cs index 56406823ad2..af4215365a1 100644 --- a/src/NHibernate/Linq/NhRelinqQueryParser.cs +++ b/src/NHibernate/Linq/NhRelinqQueryParser.cs @@ -73,6 +73,7 @@ public static PreTransformationResult PreTransform(Expression expression, PreTra { parameters.EvaluatableExpressionFilter = new NhEvaluatableExpressionFilter(parameters.SessionFactory); parameters.QueryVariables = new Dictionary(); + parameters.MinimizeParameters = parameters.QueryMode == QueryMode.Select; var partiallyEvaluatedExpression = NhPartialEvaluatingExpressionVisitor .EvaluateIndependentSubtrees(expression, parameters); diff --git a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs index b9284658607..4420571e3aa 100644 --- a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs +++ b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs @@ -109,7 +109,7 @@ public override Expression Visit(Expression expression) // Variables in expressions are never a constant, they are encapsulated as fields of a compiler generated class. // Skip detecting variables for DML queries as HQL does not support reusing parameters for them. if (expression.NodeType != ExpressionType.Constant && - _preTransformationParameters.QueryMode == QueryMode.Select && + _preTransformationParameters.MinimizeParameters && evaluatedExpression is ConstantExpression variableConstant && !_preTransformationParameters.QueryVariables.ContainsKey(variableConstant) && ExpressionsHelper.IsVariable(expression, out var path, out var closureContext)) diff --git a/src/NHibernate/Linq/Visitors/PreTransformationParameters.cs b/src/NHibernate/Linq/Visitors/PreTransformationParameters.cs index e73fa628746..f1e46b37f74 100644 --- a/src/NHibernate/Linq/Visitors/PreTransformationParameters.cs +++ b/src/NHibernate/Linq/Visitors/PreTransformationParameters.cs @@ -31,6 +31,11 @@ public PreTransformationParameters(QueryMode queryMode, ISessionFactoryImplement /// public ISessionFactoryImplementor SessionFactory { get; } + /// + /// Whether to minimize the number of parameters for variables. + /// + internal bool MinimizeParameters { get; set; } + /// /// The filter which decides whether a part of the expression will be pre-evalauted or not. /// From 6edc13cf10f9d00db1382c9eeb032030720d29a9 Mon Sep 17 00:00:00 2001 From: maca88 Date: Tue, 5 May 2020 22:22:22 +0200 Subject: [PATCH 18/18] Make MinimizeParameters public --- src/NHibernate/Linq/NhRelinqQueryParser.cs | 1 - .../Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs | 1 - src/NHibernate/Linq/Visitors/PreTransformationParameters.cs | 4 +++- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NHibernate/Linq/NhRelinqQueryParser.cs b/src/NHibernate/Linq/NhRelinqQueryParser.cs index af4215365a1..56406823ad2 100644 --- a/src/NHibernate/Linq/NhRelinqQueryParser.cs +++ b/src/NHibernate/Linq/NhRelinqQueryParser.cs @@ -73,7 +73,6 @@ public static PreTransformationResult PreTransform(Expression expression, PreTra { parameters.EvaluatableExpressionFilter = new NhEvaluatableExpressionFilter(parameters.SessionFactory); parameters.QueryVariables = new Dictionary(); - parameters.MinimizeParameters = parameters.QueryMode == QueryMode.Select; var partiallyEvaluatedExpression = NhPartialEvaluatingExpressionVisitor .EvaluateIndependentSubtrees(expression, parameters); diff --git a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs index 4420571e3aa..45ac8ffcca5 100644 --- a/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs +++ b/src/NHibernate/Linq/Visitors/NhPartialEvaluatingExpressionVisitor.cs @@ -107,7 +107,6 @@ public override Expression Visit(Expression expression) } // Variables in expressions are never a constant, they are encapsulated as fields of a compiler generated class. - // Skip detecting variables for DML queries as HQL does not support reusing parameters for them. if (expression.NodeType != ExpressionType.Constant && _preTransformationParameters.MinimizeParameters && evaluatedExpression is ConstantExpression variableConstant && diff --git a/src/NHibernate/Linq/Visitors/PreTransformationParameters.cs b/src/NHibernate/Linq/Visitors/PreTransformationParameters.cs index f1e46b37f74..3f582eef531 100644 --- a/src/NHibernate/Linq/Visitors/PreTransformationParameters.cs +++ b/src/NHibernate/Linq/Visitors/PreTransformationParameters.cs @@ -19,6 +19,8 @@ public PreTransformationParameters(QueryMode queryMode, ISessionFactoryImplement { QueryMode = queryMode; SessionFactory = sessionFactory; + // Skip detecting variables for DML queries as HQL does not support reusing parameters for them. + MinimizeParameters = QueryMode == QueryMode.Select; } /// @@ -34,7 +36,7 @@ public PreTransformationParameters(QueryMode queryMode, ISessionFactoryImplement /// /// Whether to minimize the number of parameters for variables. /// - internal bool MinimizeParameters { get; set; } + public bool MinimizeParameters { get; set; } /// /// The filter which decides whether a part of the expression will be pre-evalauted or not.