Skip to content

Minimize Linq query parameters #2335

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
May 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
398 changes: 398 additions & 0 deletions src/NHibernate.Test/Async/Linq/ParameterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,398 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by AsyncGenerator.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------


using System;
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
{
using System.Threading.Tasks;
using System.Threading;
[TestFixture]
public class ParameterTestsAsync : LinqTestCase
{
[Test]
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,
1));
}

[Test]
public async Task UsingTwoArrayParametersAsync()
{
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,
2));
}

[Test]
public async Task UsingListParameterTwiceAsync()
{
var ids = new List<int> {11008, 11019, 11039};
await (AssertTotalParametersAsync(
db.Orders.Where(o => ids.Contains(o.OrderId) && ids.Contains(o.OrderId)),
ids.Count,
1));
}

[Test]
public async Task UsingTwoListParametersAsync()
{
var ids = new List<int> {11008, 11019, 11039};
var ids2 = new List<int> {11008, 11019, 11039};
await (AssertTotalParametersAsync(
db.Orders.Where(o => ids.Contains(o.OrderId) && ids2.Contains(o.OrderId)),
ids.Count + ids2.Count,
2));
}

[Test]
public async Task UsingEntityParameterTwiceAsync()
{
var order = await (db.Orders.FirstAsync());
await (AssertTotalParametersAsync(
db.Orders.Where(o => o == order && o != order),
1));
}

[Test]
public async Task UsingTwoEntityParametersAsync()
{
var order = await (db.Orders.FirstAsync());
var order2 = await (db.Orders.FirstAsync());
await (AssertTotalParametersAsync(
db.Orders.Where(o => o == order && o != order2),
2));
}

[Test]
public async Task UsingValueTypeParameterTwiceAsync()
{
var value = 1;
await (AssertTotalParametersAsync(
db.Orders.Where(o => o.OrderId == value && o.OrderId != value),
1));
}

[Test]
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;
await (AssertTotalParametersAsync(
db.Orders.Where(o => o.OrderId == value && o.OrderId != -value),
1));
}

[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()
{
var value = 1;
var value2 = 1;
await (AssertTotalParametersAsync(
db.Orders.Where(o => o.OrderId == value && o.OrderId != value2),
2));
}

[Test]
public async Task UsingStringParameterTwiceAsync()
{
var value = "test";
await (AssertTotalParametersAsync(
db.Products.Where(o => o.Name == value && o.Name != value),
1));
}

[Test]
public async Task UsingTwoStringParametersAsync()
{
var value = "test";
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 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()
{
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<Func<Product, bool>> 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<Func<OrderLine, bool>> orderLinePredicate = o => o.Order.ShippedTo == value.Name && o.Order.ShippedTo != value.Name;
Expression<Func<Product, bool>> predicate = o => o.Name == value.Name && o.OrderLines.AsQueryable().Any(orderLinePredicate);
await (AssertTotalParametersAsync(
db.Products.Where(predicate),
1));
}

[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<T>(IQueryable<T> 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");

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);
}
}

private static Task AssertTotalParametersAsync<T>(QueryMode queryMode, IQueryable<T> query, int parameterNumber, CancellationToken cancellationToken = default(CancellationToken))
{
return AssertTotalParametersAsync(queryMode, query, null, parameterNumber, cancellationToken);
}

private static async Task AssertTotalParametersAsync<T>(QueryMode queryMode, IQueryable<T> query, Expression<Func<T, T>> expression, int parameterNumber, CancellationToken cancellationToken = default(CancellationToken))
{
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(await (provider.ExecuteDmlAsync<T>(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<Match>().Select(m => m.Groups[1].Value.Trim()).Distinct().ToList();
Assert.That(distinctParameters, Has.Count.EqualTo(parameterNumber));
}

private NhLinqExpression GetLinqExpression<T>(QueryMode queryMode, IQueryable<T> query, Expression<Func<T, T>> expression)
{
return GetLinqExpression(queryMode, DmlExpressionRewriter.PrepareExpression(query.Expression, expression));
}

private NhLinqExpression GetLinqExpression<T>(QueryMode queryMode, IQueryable<T> query)
{
return GetLinqExpression(queryMode, query.Expression);
}

private NhLinqExpression GetLinqExpression<T>(IQueryable<T> query)
{
return GetLinqExpression(QueryMode.Select, query.Expression);
}

private NhLinqExpression GetLinqExpression(QueryMode queryMode, Expression expression)
{
return queryMode == QueryMode.Select
? new NhLinqExpression(expression, Sfi)
: new NhLinqDmlExpression<Customer>(queryMode, expression, Sfi);
}
}
}
Loading