提交 41678ef4 编写于 作者: A Austin Drenski 提交者: Shay Rojansky

Translate LIKE ANY expressions (#430)

Adds simple support to translates LIKE ANY and LIKE ALL expressions
上级 d39db2ee
#region License
// The PostgreSQL License
//
// Copyright (C) 2016 The Npgsql Development Team
......@@ -19,16 +20,20 @@
// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS
// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#endregion
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.Expressions;
using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors;
using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
using Remotion.Linq.Clauses;
using Remotion.Linq.Clauses.Expressions;
using Remotion.Linq.Clauses.ResultOperators;
......@@ -36,8 +41,42 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionVisitors
{
public class NpgsqlSqlTranslatingExpressionVisitor : SqlTranslatingExpressionVisitor
{
private readonly RelationalQueryModelVisitor _queryModelVisitor;
/// <summary>
/// The <see cref="MethodInfo"/> for <see cref="DbFunctionsExtensions.Like(DbFunctions,string,string)"/>.
/// </summary>
[NotNull] static readonly MethodInfo Like2MethodInfo =
typeof(DbFunctionsExtensions)
.GetRuntimeMethod(nameof(DbFunctionsExtensions.Like), new[] { typeof(DbFunctions), typeof(string), typeof(string) });
/// <summary>
/// The <see cref="MethodInfo"/> for <see cref="DbFunctionsExtensions.Like(DbFunctions,string,string, string)"/>.
/// </summary>
[NotNull] static readonly MethodInfo Like3MethodInfo =
typeof(DbFunctionsExtensions)
.GetRuntimeMethod(nameof(DbFunctionsExtensions.Like), new[] { typeof(DbFunctions), typeof(string), typeof(string), typeof(string) });
// ReSharper disable once InconsistentNaming
/// <summary>
/// The <see cref="MethodInfo"/> for <see cref="NpgsqlDbFunctionsExtensions.ILike(DbFunctions,string,string)"/>.
/// </summary>
[NotNull] static readonly MethodInfo ILike2MethodInfo =
typeof(NpgsqlDbFunctionsExtensions)
.GetRuntimeMethod(nameof(NpgsqlDbFunctionsExtensions.ILike), new[] { typeof(DbFunctions), typeof(string), typeof(string) });
// ReSharper disable once InconsistentNaming
/// <summary>
/// The <see cref="MethodInfo"/> for <see cref="NpgsqlDbFunctionsExtensions.ILike(DbFunctions,string,string,string)"/>.
/// </summary>
[NotNull] static readonly MethodInfo ILike3MethodInfo =
typeof(NpgsqlDbFunctionsExtensions)
.GetRuntimeMethod(nameof(NpgsqlDbFunctionsExtensions.ILike), new[] { typeof(DbFunctions), typeof(string), typeof(string), typeof(string) });
/// <summary>
/// The query model visitor.
/// </summary>
[NotNull] readonly RelationalQueryModelVisitor _queryModelVisitor;
/// <inheritdoc />
public NpgsqlSqlTranslatingExpressionVisitor(
[NotNull] SqlTranslatingExpressionVisitorDependencies dependencies,
[NotNull] RelationalQueryModelVisitor queryModelVisitor,
......@@ -45,22 +84,51 @@ public class NpgsqlSqlTranslatingExpressionVisitor : SqlTranslatingExpressionVis
[CanBeNull] Expression topLevelPredicate = null,
bool inProjection = false)
: base(dependencies, queryModelVisitor, targetSelectExpression, topLevelPredicate, inProjection)
{
_queryModelVisitor = queryModelVisitor;
}
=> _queryModelVisitor = queryModelVisitor;
/// <inheritdoc />
protected override Expression VisitSubQuery(SubQueryExpression expression)
=> base.VisitSubQuery(expression) ?? VisitLikeAnyAll(expression) ?? VisitEqualsAny(expression);
/// <inheritdoc />
protected override Expression VisitBinary(BinaryExpression expression)
{
// Prefer the default EF Core translation if one exists
var result = base.VisitSubQuery(expression);
if (result != null)
return result;
if (expression.NodeType == ExpressionType.ArrayIndex)
{
var properties = MemberAccessBindingExpressionVisitor.GetPropertyPath(
expression.Left, _queryModelVisitor.QueryCompilationContext, out _);
if (properties.Count == 0)
return base.VisitBinary(expression);
var lastPropertyType = properties[properties.Count - 1].ClrType;
if (lastPropertyType.IsArray && lastPropertyType.GetArrayRank() == 1)
{
var left = Visit(expression.Left);
var right = Visit(expression.Right);
return left != null && right != null
? Expression.MakeBinary(ExpressionType.ArrayIndex, left, right)
: null;
}
}
return base.VisitBinary(expression);
}
/// <summary>
/// Visits a <see cref="SubQueryExpression"/> and attempts to translate a '= ANY' expression.
/// </summary>
/// <param name="expression">The expression to visit.</param>
/// <returns>
/// An '= ANY' expression or null.
/// </returns>
[CanBeNull]
protected virtual Expression VisitEqualsAny([NotNull] SubQueryExpression expression)
{
var subQueryModel = expression.QueryModel;
var fromExpression = subQueryModel.MainFromClause.FromExpression;
var properties = MemberAccessBindingExpressionVisitor.GetPropertyPath(
fromExpression, _queryModelVisitor.QueryCompilationContext, out var qsre);
fromExpression, _queryModelVisitor.QueryCompilationContext, out _);
if (properties.Count == 0)
return null;
......@@ -76,33 +144,77 @@ protected override Expression VisitSubQuery(SubQueryExpression expression)
{
var containsItem = Visit(contains.Item);
if (containsItem != null)
return new ArrayAnyExpression(containsItem, Visit(fromExpression));
return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", containsItem, Visit(fromExpression));
}
}
return null;
}
protected override Expression VisitBinary(BinaryExpression expression)
/// <summary>
/// Visits a <see cref="SubQueryExpression"/> and attempts to translate a LIKE/ILIKE ANY/ALL expression.
/// </summary>
/// <param name="expression">The expression to visit.</param>
/// <returns>
/// A 'LIKE ANY', 'LIKE ALL', 'ILIKE ANY', or 'ILIKE ALL' expression or null.
/// </returns>
[CanBeNull]
protected virtual Expression VisitLikeAnyAll([NotNull] SubQueryExpression expression)
{
if (expression.NodeType == ExpressionType.ArrayIndex)
{
var properties = MemberAccessBindingExpressionVisitor.GetPropertyPath(
expression.Left, _queryModelVisitor.QueryCompilationContext, out var qsre);
if (properties.Count == 0)
return base.VisitBinary(expression);
var lastPropertyType = properties[properties.Count - 1].ClrType;
if (lastPropertyType.IsArray && lastPropertyType.GetArrayRank() == 1)
{
var left = Visit(expression.Left);
var right = Visit(expression.Right);
var queryModel = expression.QueryModel;
var results = queryModel.ResultOperators;
var body = queryModel.BodyClauses;
return left != null && right != null
? Expression.MakeBinary(ExpressionType.ArrayIndex, left, right)
if (results.Count != 1)
return null;
ArrayComparisonType comparisonType;
MethodCallExpression call;
switch (results[0])
{
case AnyResultOperator _:
comparisonType = ArrayComparisonType.ANY;
call =
body.Count == 1 &&
body[0] is WhereClause whereClause &&
whereClause.Predicate is MethodCallExpression methocCall
? methocCall
: null;
}
break;
case AllResultOperator allResult:
comparisonType = ArrayComparisonType.ALL;
call = allResult.Predicate as MethodCallExpression;
break;
default:
return null;
}
return base.VisitBinary(expression);
if (call is null)
return null;
var source = queryModel.MainFromClause.FromExpression;
// ReSharper disable AssignNullToNotNullAttribute
switch (call.Method)
{
case MethodInfo m when m == Like2MethodInfo:
return new ArrayAnyAllExpression(comparisonType, "LIKE", Visit(call.Arguments[1]), Visit(source));
case MethodInfo m when m == Like3MethodInfo:
return new ArrayAnyAllExpression(comparisonType, "LIKE", Visit(call.Arguments[1]), Visit(source));
case MethodInfo m when m == ILike2MethodInfo:
return new ArrayAnyAllExpression(comparisonType, "ILIKE", Visit(call.Arguments[1]), Visit(source));
case MethodInfo m when m == ILike3MethodInfo:
return new ArrayAnyAllExpression(comparisonType, "ILIKE", Visit(call.Arguments[1]), Visit(source));
default:
return null;
}
// ReSharper restore AssignNullToNotNullAttribute
}
}
}
#region License
// The PostgreSQL License
//
// Copyright (C) 2016 The Npgsql Development Team
......@@ -19,10 +20,10 @@
// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS
// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#endregion
using System;
using System.Diagnostics;
using System.Linq.Expressions;
using JetBrains.Annotations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Sql.Internal;
......@@ -31,137 +32,125 @@
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal
{
/// <summary>
/// Represents a PostgreSQL ANY expression (e.g. scalar = ANY (array))
/// Represents a PostgreSQL array ANY or ALL expression.
/// </summary>
/// <example>
/// 1 = ANY ('{0,1,2}'), 'cat' LIKE ANY ('{a%,b%,c%}')
/// </example>
/// <remarks>
/// See https://www.postgresql.org/docs/current/static/functions-comparisons.html
/// </remarks>
public class ArrayAnyExpression : Expression
public class ArrayAnyAllExpression : Expression, IEquatable<ArrayAnyAllExpression>
{
/// <summary>
/// Creates a new instance of InExpression.
/// </summary>
/// <param name="operand"> The operand. </param>
/// <param name="array"> The array. </param>
public ArrayAnyExpression(
[NotNull] Expression operand,
[NotNull] Expression array)
{
Check.NotNull(operand, nameof(operand));
Check.NotNull(array, nameof(array));
Debug.Assert(array.Type.IsArray);
/// <inheritdoc />
public override ExpressionType NodeType { get; } = ExpressionType.Extension;
Operand = operand;
Array = array;
}
/// <inheritdoc />
public override Type Type { get; } = typeof(bool);
/// <summary>
/// Gets the operand.
/// The value to test against the <see cref="Array"/>.
/// </summary>
/// <value>
/// The operand.
/// </value>
public virtual Expression Operand { get; }
/// <summary>
/// Gets the array.
/// The array of values or patterns to test for the <see cref="Operand"/>.
/// </summary>
/// <value>
/// The array.
/// </value>
public virtual Expression Array { get; }
/// <summary>
/// Returns the node type of this <see cref="Expression" />. (Inherited from <see cref="Expression" />.)
/// The operator.
/// </summary>
/// <returns>The <see cref="ExpressionType" /> that represents this expression.</returns>
public override ExpressionType NodeType => ExpressionType.Extension;
public virtual string Operator { get; }
/// <summary>
/// Gets the static type of the expression that this <see cref="Expression" /> represents. (Inherited from <see cref="Expression" />.)
/// The comparison type.
/// </summary>
/// <returns>The <see cref="Type" /> that represents the static type of the expression.</returns>
public override Type Type => typeof(bool);
public virtual ArrayComparisonType ArrayComparisonType { get; }
/// <summary>
/// Dispatches to the specific visit method for this node type.
/// Constructs a <see cref="ArrayAnyAllExpression"/>.
/// </summary>
protected override Expression Accept(ExpressionVisitor visitor)
/// <param name="arrayComparisonType">The comparison type.</param>
/// <param name="operatorSymbol">The operator symbol to the array expression.</param>
/// <param name="operand">The value to find.</param>
/// <param name="array">The array to search.</param>
/// <exception cref="ArgumentNullException" />
public ArrayAnyAllExpression(
ArrayComparisonType arrayComparisonType,
[NotNull] string operatorSymbol,
[NotNull] Expression operand,
[NotNull] Expression array)
{
Check.NotNull(visitor, nameof(visitor));
Check.NotNull(array, nameof(operatorSymbol));
Check.NotNull(operand, nameof(operand));
Check.NotNull(array, nameof(array));
return visitor is NpgsqlQuerySqlGenerator npsgqlGenerator
? npsgqlGenerator.VisitArrayAny(this)
: base.Accept(visitor);
ArrayComparisonType = arrayComparisonType;
Operator = operatorSymbol;
Operand = operand;
Array = array;
}
/// <summary>
/// Reduces the node and then calls the <see cref="ExpressionVisitor.Visit(System.Linq.Expressions.Expression)" /> method passing the
/// reduced expression.
/// Throws an exception if the node isn't reducible.
/// </summary>
/// <param name="visitor"> An instance of <see cref="ExpressionVisitor" />. </param>
/// <returns> The expression being visited, or an expression which should replace it in the tree. </returns>
/// <remarks>
/// Override this method to provide logic to walk the node's children.
/// A typical implementation will call visitor.Visit on each of its
/// children, and if any of them change, should return a new copy of
/// itself with the modified children.
/// </remarks>
///
/// <inheritdoc />
protected override Expression Accept(ExpressionVisitor visitor)
=> visitor is NpgsqlQuerySqlGenerator npsgqlGenerator
? npsgqlGenerator.VisitArrayAnyAll(this)
: base.Accept(visitor);
/// <inheritdoc />
protected override Expression VisitChildren(ExpressionVisitor visitor)
{
var newOperand = visitor.Visit(Operand);
var newArray = visitor.Visit(Array);
if (!(visitor.Visit(Operand) is Expression operand))
throw new ArgumentException($"The {nameof(operand)} of a {nameof(ArrayAnyAllExpression)} cannot be null.");
return newOperand != Operand || newArray != Array
? new ArrayAnyExpression(newOperand, newArray)
: this;
if (!(visitor.Visit(Array) is Expression collection))
throw new ArgumentException($"The {nameof(collection)} of a {nameof(ArrayAnyAllExpression)} cannot be null.");
return
operand == Operand && collection == Array
? this
: new ArrayAnyAllExpression(ArrayComparisonType, Operator, operand, collection);
}
/// <summary>
/// Tests if this object is considered equal to another.
/// </summary>
/// <param name="obj"> The object to compare with the current object. </param>
/// <returns>
/// true if the objects are considered equal, false if they are not.
/// </returns>
/// <inheritdoc />
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
=> obj is ArrayAnyAllExpression likeAnyExpression && Equals(likeAnyExpression);
if (ReferenceEquals(this, obj))
{
return true;
}
/// <inheritdoc />
public bool Equals(ArrayAnyAllExpression other)
=> Operand.Equals(other?.Operand) &&
Operator.Equals(other?.Operator) &&
ArrayComparisonType.Equals(other?.ArrayComparisonType) &&
Array.Equals(other?.Array);
return obj.GetType() == GetType() && Equals((ArrayAnyExpression)obj);
}
bool Equals(ArrayAnyExpression other)
=> Operand.Equals(other.Operand) && Array.Equals(other.Array);
/// <inheritdoc />
public override int GetHashCode()
=> unchecked((397 * Operand.GetHashCode()) ^
(397 * Operator.GetHashCode()) ^
(397 * ArrayComparisonType.GetHashCode()) ^
(397 * Array.GetHashCode()));
/// <inheritdoc />
public override string ToString()
=> $"{Operand} {Operator} {ArrayComparisonType.ToString()} ({Array})";
}
/// <summary>
/// Represents whether an array comparison is ANY or ALL.
/// </summary>
public enum ArrayComparisonType
{
// ReSharper disable once InconsistentNaming
/// <summary>
/// Returns a hash code for this object.
/// Represents an ANY array comparison.
/// </summary>
/// <returns>
/// A hash code for this object.
/// </returns>
public override int GetHashCode()
{
unchecked
{
return (Operand.GetHashCode() * 397) ^ Array.GetHashCode();
}
}
ANY,
// ReSharper disable once InconsistentNaming
/// <summary>
/// Creates a <see cref="string" /> representation of the Expression.
/// Represents an ALL array comparison.
/// </summary>
/// <returns>A <see cref="string" /> representation of the Expression.</returns>
public override string ToString() => $"{Operand} = ANY ({Array})";
ALL
}
}
......@@ -41,8 +41,9 @@ public class NpgsqlQuerySqlGenerator : DefaultQuerySqlGenerator
{
readonly bool _reverseNullOrderingEnabled;
protected override string TypedTrueLiteral => "TRUE::bool";
protected override string TypedFalseLiteral => "FALSE::bool";
protected override string TypedTrueLiteral { get; } = "TRUE::bool";
protected override string TypedFalseLiteral { get; } = "FALSE::bool";
public NpgsqlQuerySqlGenerator(
[NotNull] QuerySqlGeneratorDependencies dependencies,
......@@ -176,23 +177,34 @@ protected virtual void VisitArrayIndex([NotNull] BinaryExpression expression)
Sql.Append(']');
}
public Expression VisitArrayAny(ArrayAnyExpression arrayAnyExpression)
/// <summary>
/// Produces expressions like: 1 = ANY ('{0,1,2}') or 'cat' LIKE ANY ('{a%,b%,c%}').
/// </summary>
public Expression VisitArrayAnyAll(ArrayAnyAllExpression arrayAnyAllExpression)
{
Visit(arrayAnyExpression.Operand);
Sql.Append(" = ANY (");
Visit(arrayAnyExpression.Array);
Sql.Append(")");
return arrayAnyExpression;
Visit(arrayAnyAllExpression.Operand);
Sql.Append(' ');
Sql.Append(arrayAnyAllExpression.Operator);
Sql.Append(' ');
Sql.Append(arrayAnyAllExpression.ArrayComparisonType.ToString());
Sql.Append(" (");
Visit(arrayAnyAllExpression.Array);
Sql.Append(')');
return arrayAnyAllExpression;
}
// PostgreSQL array indexing is 1-based. If the index happens to be a constant,
// just increment it. Otherwise, append a +1 in the SQL.
Expression GenerateOneBasedIndexExpression(Expression expression)
/// <summary>
/// PostgreSQL array indexing is 1-based. If the index happens to be a constant,
/// just increment it. Otherwise, append a +1 in the SQL.
/// </summary>
static Expression GenerateOneBasedIndexExpression(Expression expression)
=> expression is ConstantExpression constantExpression
? Expression.Constant(Convert.ToInt32(constantExpression.Value) + 1)
: (Expression)Expression.Add(expression, Expression.Constant(1));
// See http://www.postgresql.org/docs/current/static/functions-matching.html
/// <summary>
/// See: http://www.postgresql.org/docs/current/static/functions-matching.html
/// </summary>
public Expression VisitRegexMatch([NotNull] RegexMatchExpression regexMatchExpression)
{
Check.NotNull(regexMatchExpression, nameof(regexMatchExpression));
......
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.TestUtilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities;
using Xunit;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query
{
public class LikeAnyAllQueryNpgsqlTest : IClassFixture<LikeAnyAllQueryNpgsqlTest.LikeAnyQueryNpgsqlFixture>
{
#region Setup
/// <summary>
/// Provides resources for unit tests.
/// </summary>
LikeAnyQueryNpgsqlFixture Fixture { get; }
/// <summary>
/// Initializes resources for unit tests.
/// </summary>
/// <param name="fixture">The fixture of resources for testing.</param>
public LikeAnyAllQueryNpgsqlTest(LikeAnyQueryNpgsqlFixture fixture)
{
Fixture = fixture;
Fixture.TestSqlLoggerFactory.Clear();
}
#endregion
#region LikeTests
[Fact]
public void Array_Any_Like()
{
using (LikeAnyContext context = Fixture.CreateContext())
{
var collection = new string[] { "a", "b", "c" };
LikeAnyTestEntity[] _ =
context.LikeAnyTestEntities
.Where(x => collection.Any(y => EF.Functions.Like(x.Animal, y)))
.ToArray();
AssertContainsSql("WHERE x.\"Animal\" LIKE ANY (@__collection_0) = TRUE");
}
}
[Fact]
public void Array_All_Like()
{
using (LikeAnyContext context = Fixture.CreateContext())
{
var collection = new string[] { "a", "b", "c" };
LikeAnyTestEntity[] _ =
context.LikeAnyTestEntities
.Where(x => collection.All(y => EF.Functions.Like(x.Animal, y)))
.ToArray();
AssertContainsSql("WHERE x.\"Animal\" LIKE ALL (@__collection_0) = TRUE");
}
}
#endregion
#region ILikeTests
[Fact]
public void Array_Any_ILike()
{
using (LikeAnyContext context = Fixture.CreateContext())
{
var collection = new string[] { "a", "b", "c%" };
LikeAnyTestEntity[] _ =
context.LikeAnyTestEntities
.Where(x => collection.Any(y => EF.Functions.ILike(x.Animal, y)))
.ToArray();
AssertContainsSql("WHERE x.\"Animal\" ILIKE ANY (@__collection_0) = TRUE");
}
}
[Fact]
public void Array_All_ILike()
{
using (LikeAnyContext context = Fixture.CreateContext())
{
var collection = new string[] { "a", "b", "c%" };
LikeAnyTestEntity[] _ =
context.LikeAnyTestEntities
.Where(x => collection.All(y => EF.Functions.ILike(x.Animal, y)))
.ToArray();
AssertContainsSql("WHERE x.\"Animal\" ILIKE ALL (@__collection_0) = TRUE");
}
}
#endregion
#region Fixtures
/// <summary>
/// Represents a fixture suitable for testing LIKE ANY expressions.
/// </summary>
public class LikeAnyQueryNpgsqlFixture : IDisposable
{
/// <summary>
/// The <see cref="NpgsqlTestStore"/> used for testing.
/// </summary>
readonly NpgsqlTestStore _testStore;
/// <summary>
/// The <see cref="DbContextOptions"/> used for testing.
/// </summary>
readonly DbContextOptions _options;
/// <summary>
/// The logger factory used for testing.
/// </summary>
public TestSqlLoggerFactory TestSqlLoggerFactory { get; }
/// <summary>
/// Initializes a <see cref="LikeAnyQueryNpgsqlFixture"/>.
/// </summary>
public LikeAnyQueryNpgsqlFixture()
{
TestSqlLoggerFactory = new TestSqlLoggerFactory();
_testStore = NpgsqlTestStore.CreateScratch();
_options =
new DbContextOptionsBuilder()
.UseNpgsql(_testStore.ConnectionString, b => b.ApplyConfiguration())
.UseInternalServiceProvider(
new ServiceCollection()
.AddEntityFrameworkNpgsql()
.AddSingleton<ILoggerFactory>(TestSqlLoggerFactory)
.BuildServiceProvider())
.Options;
using (LikeAnyContext context = CreateContext())
{
context.Database.EnsureCreated();
context.LikeAnyTestEntities
.AddRange(
new LikeAnyTestEntity
{
Id = 1,
Animal = "cat"
},
new LikeAnyTestEntity
{
Id = 2,
Animal = "dog"
},
new LikeAnyTestEntity
{
Id = 3,
Animal = "turtle"
},
new LikeAnyTestEntity
{
Id = 4,
Animal = "bird"
});
context.SaveChanges();
}
}
/// <summary>
/// Creates a new <see cref="LikeAnyContext"/>.
/// </summary>
/// <returns>
/// A <see cref="LikeAnyContext"/> for testing.
/// </returns>
public LikeAnyContext CreateContext() => new LikeAnyContext(_options);
/// <inheritdoc />
public void Dispose() => _testStore.Dispose();
}
/// <summary>
/// Represents an entity suitable for testing LIKE ANY expressions.
/// </summary>
public class LikeAnyTestEntity
{
/// <summary>
/// The primary key.
/// </summary>
public int Id { get; set; }
/// <summary>
/// The value.
/// </summary>
public string Animal { get; set; }
}
/// <summary>
/// Represents a database suitable for testing range operators.
/// </summary>
public class LikeAnyContext : DbContext
{
/// <summary>
/// Represents a set of entities with a string property.
/// </summary>
public DbSet<LikeAnyTestEntity> LikeAnyTestEntities { get; set; }
/// <summary>
/// Initializes a <see cref="LikeAnyContext"/>.
/// </summary>
/// <param name="options">
/// The options to be used for configuration.
/// </param>
public LikeAnyContext(DbContextOptions options) : base(options) { }
}
#endregion
#region Helpers
/// <summary>
/// Asserts that the SQL fragment appears in the logs.
/// </summary>
/// <param name="sql">The SQL statement or fragment to search for in the logs.</param>
public void AssertContainsSql(string sql) => Assert.Contains(sql, Fixture.TestSqlLoggerFactory.Sql);
#endregion
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册