Co-Authored-By: Qstick <376117+Qstick@users.noreply.github.com> Co-authored-by: ta264 <ta264@users.noreply.github.com> (cherry picked from commit 80b1aa9a2c81617bdda7ef551c19a2f114e49204)pull/1770/head
parent
8616373f96
commit
46c2e0ba82
@ -0,0 +1,211 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.Books;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Datastore
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class WhereBuilderPostgresFixture : CoreTest
|
||||||
|
{
|
||||||
|
private WhereBuilderPostgres _subject;
|
||||||
|
|
||||||
|
[OneTimeSetUp]
|
||||||
|
public void MapTables()
|
||||||
|
{
|
||||||
|
// Generate table mapping
|
||||||
|
Mocker.Resolve<DbFactory>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private WhereBuilderPostgres Where(Expression<Func<Author, bool>> filter)
|
||||||
|
{
|
||||||
|
return new WhereBuilderPostgres(filter, true, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WhereBuilderPostgres WhereMetadata(Expression<Func<AuthorMetadata, bool>> filter)
|
||||||
|
{
|
||||||
|
return new WhereBuilderPostgres(filter, true, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void postgres_where_equal_const()
|
||||||
|
{
|
||||||
|
_subject = Where(x => x.Id == 10);
|
||||||
|
|
||||||
|
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = @Clause1_P1)");
|
||||||
|
_subject.Parameters.Get<int>("Clause1_P1").Should().Be(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void postgres_where_equal_variable()
|
||||||
|
{
|
||||||
|
var id = 10;
|
||||||
|
_subject = Where(x => x.Id == id);
|
||||||
|
|
||||||
|
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = @Clause1_P1)");
|
||||||
|
_subject.Parameters.Get<int>("Clause1_P1").Should().Be(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void postgres_where_equal_property()
|
||||||
|
{
|
||||||
|
var author = new Author { Id = 10 };
|
||||||
|
_subject = Where(x => x.Id == author.Id);
|
||||||
|
|
||||||
|
_subject.Parameters.ParameterNames.Should().HaveCount(1);
|
||||||
|
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = @Clause1_P1)");
|
||||||
|
_subject.Parameters.Get<int>("Clause1_P1").Should().Be(author.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void postgres_where_equal_joined_property()
|
||||||
|
{
|
||||||
|
_subject = Where(x => x.QualityProfile.Value.Id == 1);
|
||||||
|
|
||||||
|
_subject.Parameters.ParameterNames.Should().HaveCount(1);
|
||||||
|
_subject.ToString().Should().Be($"(\"QualityProfiles\".\"Id\" = @Clause1_P1)");
|
||||||
|
_subject.Parameters.Get<int>("Clause1_P1").Should().Be(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void postgres_where_throws_without_concrete_condition_if_requiresConcreteCondition()
|
||||||
|
{
|
||||||
|
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
|
||||||
|
_subject = new WhereBuilderPostgres(filter, true, 0);
|
||||||
|
Assert.Throws<InvalidOperationException>(() => _subject.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void postgres_where_allows_abstract_condition_if_not_requiresConcreteCondition()
|
||||||
|
{
|
||||||
|
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
|
||||||
|
_subject = new WhereBuilderPostgres(filter, false, 0);
|
||||||
|
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = \"Authors\".\"Id\")");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void postgres_where_string_is_null()
|
||||||
|
{
|
||||||
|
_subject = Where(x => x.CleanName == null);
|
||||||
|
|
||||||
|
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" IS NULL)");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void postgres_where_string_is_null_value()
|
||||||
|
{
|
||||||
|
string cleanName = null;
|
||||||
|
_subject = Where(x => x.CleanName == cleanName);
|
||||||
|
|
||||||
|
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" IS NULL)");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void postgres_where_equal_null_property()
|
||||||
|
{
|
||||||
|
var author = new Author { CleanName = null };
|
||||||
|
_subject = Where(x => x.CleanName == author.CleanName);
|
||||||
|
|
||||||
|
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" IS NULL)");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void postgres_where_column_contains_string()
|
||||||
|
{
|
||||||
|
var test = "small";
|
||||||
|
_subject = Where(x => x.CleanName.Contains(test));
|
||||||
|
|
||||||
|
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" ILIKE '%' || @Clause1_P1 || '%')");
|
||||||
|
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void postgres_where_string_contains_column()
|
||||||
|
{
|
||||||
|
var test = "small";
|
||||||
|
_subject = Where(x => test.Contains(x.CleanName));
|
||||||
|
|
||||||
|
_subject.ToString().Should().Be($"(@Clause1_P1 ILIKE '%' || \"Authors\".\"CleanName\" || '%')");
|
||||||
|
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void postgres_where_column_starts_with_string()
|
||||||
|
{
|
||||||
|
var test = "small";
|
||||||
|
_subject = Where(x => x.CleanName.StartsWith(test));
|
||||||
|
|
||||||
|
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" ILIKE @Clause1_P1 || '%')");
|
||||||
|
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void postgres_where_column_ends_with_string()
|
||||||
|
{
|
||||||
|
var test = "small";
|
||||||
|
_subject = Where(x => x.CleanName.EndsWith(test));
|
||||||
|
|
||||||
|
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" ILIKE '%' || @Clause1_P1)");
|
||||||
|
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void postgres_where_in_list()
|
||||||
|
{
|
||||||
|
var list = new List<int> { 1, 2, 3 };
|
||||||
|
_subject = Where(x => list.Contains(x.Id));
|
||||||
|
|
||||||
|
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = ANY (('{{1, 2, 3}}')))");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void postgres_where_in_list_2()
|
||||||
|
{
|
||||||
|
var list = new List<int> { 1, 2, 3 };
|
||||||
|
_subject = Where(x => x.CleanName == "test" && list.Contains(x.Id));
|
||||||
|
|
||||||
|
_subject.ToString().Should().Be($"((\"Authors\".\"CleanName\" = @Clause1_P1) AND (\"Authors\".\"Id\" = ANY (('{{1, 2, 3}}'))))");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void postgres_where_in_string_list()
|
||||||
|
{
|
||||||
|
var list = new List<string> { "first", "second", "third" };
|
||||||
|
|
||||||
|
_subject = Where(x => list.Contains(x.CleanName));
|
||||||
|
|
||||||
|
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" = ANY (@Clause1_P1))");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void enum_as_int()
|
||||||
|
{
|
||||||
|
_subject = WhereMetadata(x => x.Status == AuthorStatusType.Continuing);
|
||||||
|
|
||||||
|
_subject.ToString().Should().Be($"(\"AuthorMetadata\".\"Status\" = @Clause1_P1)");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void enum_in_list()
|
||||||
|
{
|
||||||
|
var allowed = new List<AuthorStatusType> { AuthorStatusType.Continuing, AuthorStatusType.Ended };
|
||||||
|
_subject = WhereMetadata(x => allowed.Contains(x.Status));
|
||||||
|
|
||||||
|
_subject.ToString().Should().Be($"(\"AuthorMetadata\".\"Status\" = ANY (@Clause1_P1))");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void enum_in_array()
|
||||||
|
{
|
||||||
|
var allowed = new AuthorStatusType[] { AuthorStatusType.Continuing, AuthorStatusType.Ended };
|
||||||
|
_subject = WhereMetadata(x => allowed.Contains(x.Status));
|
||||||
|
|
||||||
|
_subject.ToString().Should().Be($"(\"AuthorMetadata\".\"Status\" = ANY (@Clause1_P1))");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore
|
||||||
|
{
|
||||||
|
public class PostgresOptions
|
||||||
|
{
|
||||||
|
public string Host { get; set; }
|
||||||
|
public int Port { get; set; }
|
||||||
|
public string User { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
public string MainDb { get; set; }
|
||||||
|
public string LogDb { get; set; }
|
||||||
|
public string CacheDb { get; set; }
|
||||||
|
|
||||||
|
public static PostgresOptions GetOptions()
|
||||||
|
{
|
||||||
|
var config = new ConfigurationBuilder()
|
||||||
|
.AddEnvironmentVariables()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var postgresOptions = new PostgresOptions();
|
||||||
|
config.GetSection("Readarr:Postgres").Bind(postgresOptions);
|
||||||
|
|
||||||
|
return postgresOptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,387 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using Dapper;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore
|
||||||
|
{
|
||||||
|
public class WhereBuilderPostgres : WhereBuilder
|
||||||
|
{
|
||||||
|
protected StringBuilder _sb;
|
||||||
|
|
||||||
|
private const DbType EnumerableMultiParameter = (DbType)(-1);
|
||||||
|
private readonly string _paramNamePrefix;
|
||||||
|
private readonly bool _requireConcreteValue = false;
|
||||||
|
private int _paramCount = 0;
|
||||||
|
private bool _gotConcreteValue = false;
|
||||||
|
|
||||||
|
public WhereBuilderPostgres(Expression filter, bool requireConcreteValue, int seq)
|
||||||
|
{
|
||||||
|
_paramNamePrefix = string.Format("Clause{0}", seq + 1);
|
||||||
|
_requireConcreteValue = requireConcreteValue;
|
||||||
|
_sb = new StringBuilder();
|
||||||
|
|
||||||
|
Parameters = new DynamicParameters();
|
||||||
|
|
||||||
|
if (filter != null)
|
||||||
|
{
|
||||||
|
Visit(filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string AddParameter(object value, DbType? dbType = null)
|
||||||
|
{
|
||||||
|
_gotConcreteValue = true;
|
||||||
|
_paramCount++;
|
||||||
|
var name = _paramNamePrefix + "_P" + _paramCount;
|
||||||
|
Parameters.Add(name, value, dbType);
|
||||||
|
return '@' + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Expression VisitBinary(BinaryExpression expression)
|
||||||
|
{
|
||||||
|
_sb.Append('(');
|
||||||
|
|
||||||
|
Visit(expression.Left);
|
||||||
|
|
||||||
|
_sb.AppendFormat(" {0} ", Decode(expression));
|
||||||
|
|
||||||
|
Visit(expression.Right);
|
||||||
|
|
||||||
|
_sb.Append(')');
|
||||||
|
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Expression VisitMethodCall(MethodCallExpression expression)
|
||||||
|
{
|
||||||
|
var method = expression.Method.Name;
|
||||||
|
|
||||||
|
switch (expression.Method.Name)
|
||||||
|
{
|
||||||
|
case "Contains":
|
||||||
|
ParseContainsExpression(expression);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "StartsWith":
|
||||||
|
ParseStartsWith(expression);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "EndsWith":
|
||||||
|
ParseEndsWith(expression);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
var msg = string.Format("'{0}' expressions are not yet implemented in the where clause expression tree parser.", method);
|
||||||
|
throw new NotImplementedException(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Expression VisitMemberAccess(MemberExpression expression)
|
||||||
|
{
|
||||||
|
var tableName = expression?.Expression?.Type != null ? TableMapping.Mapper.TableNameMapping(expression.Expression.Type) : null;
|
||||||
|
var gotValue = TryGetRightValue(expression, out var value);
|
||||||
|
|
||||||
|
// Only use the SQL condition if the expression didn't resolve to an actual value
|
||||||
|
if (tableName != null && !gotValue)
|
||||||
|
{
|
||||||
|
_sb.Append($"\"{tableName}\".\"{expression.Member.Name}\"");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
// string is IEnumerable<Char> but we don't want to pick up that case
|
||||||
|
var type = value.GetType();
|
||||||
|
var typeInfo = type.GetTypeInfo();
|
||||||
|
var isEnumerable =
|
||||||
|
type != typeof(string) && (
|
||||||
|
typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) ||
|
||||||
|
(typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>)));
|
||||||
|
|
||||||
|
var paramName = isEnumerable ? AddParameter(value, EnumerableMultiParameter) : AddParameter(value);
|
||||||
|
_sb.Append(paramName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_gotConcreteValue = true;
|
||||||
|
_sb.Append("NULL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Expression VisitConstant(ConstantExpression expression)
|
||||||
|
{
|
||||||
|
if (expression.Value != null)
|
||||||
|
{
|
||||||
|
var paramName = AddParameter(expression.Value);
|
||||||
|
_sb.Append(paramName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_gotConcreteValue = true;
|
||||||
|
_sb.Append("NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetConstantValue(Expression expression, out object result)
|
||||||
|
{
|
||||||
|
result = null;
|
||||||
|
|
||||||
|
if (expression is ConstantExpression constExp)
|
||||||
|
{
|
||||||
|
result = constExp.Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetPropertyValue(MemberExpression expression, out object result)
|
||||||
|
{
|
||||||
|
result = null;
|
||||||
|
|
||||||
|
if (expression.Expression is MemberExpression nested)
|
||||||
|
{
|
||||||
|
// Value is passed in as a property on a parent entity
|
||||||
|
var container = (nested.Expression as ConstantExpression)?.Value;
|
||||||
|
|
||||||
|
if (container == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entity = GetFieldValue(container, nested.Member);
|
||||||
|
result = GetFieldValue(entity, expression.Member);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetVariableValue(MemberExpression expression, out object result)
|
||||||
|
{
|
||||||
|
result = null;
|
||||||
|
|
||||||
|
// Value is passed in as a variable
|
||||||
|
if (expression.Expression is ConstantExpression nested)
|
||||||
|
{
|
||||||
|
result = GetFieldValue(nested.Value, expression.Member);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetRightValue(Expression expression, out object value)
|
||||||
|
{
|
||||||
|
value = null;
|
||||||
|
|
||||||
|
if (TryGetConstantValue(expression, out value))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var memberExp = expression as MemberExpression;
|
||||||
|
|
||||||
|
if (TryGetPropertyValue(memberExp, out value))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryGetVariableValue(memberExp, out value))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private object GetFieldValue(object entity, MemberInfo member)
|
||||||
|
{
|
||||||
|
if (member.MemberType == MemberTypes.Field)
|
||||||
|
{
|
||||||
|
return (member as FieldInfo).GetValue(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member.MemberType == MemberTypes.Property)
|
||||||
|
{
|
||||||
|
return (member as PropertyInfo).GetValue(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ArgumentException(string.Format("WhereBuilder could not get the value for {0}.{1}.", entity.GetType().Name, member.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsNullVariable(Expression expression)
|
||||||
|
{
|
||||||
|
if (expression.NodeType == ExpressionType.Constant &&
|
||||||
|
TryGetConstantValue(expression, out var constResult) &&
|
||||||
|
constResult == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expression.NodeType == ExpressionType.MemberAccess &&
|
||||||
|
expression is MemberExpression member &&
|
||||||
|
((TryGetPropertyValue(member, out var result) && result == null) ||
|
||||||
|
(TryGetVariableValue(member, out result) && result == null)))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Decode(BinaryExpression expression)
|
||||||
|
{
|
||||||
|
if (IsNullVariable(expression.Right))
|
||||||
|
{
|
||||||
|
switch (expression.NodeType)
|
||||||
|
{
|
||||||
|
case ExpressionType.Equal: return "IS";
|
||||||
|
case ExpressionType.NotEqual: return "IS NOT";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (expression.NodeType)
|
||||||
|
{
|
||||||
|
case ExpressionType.AndAlso: return "AND";
|
||||||
|
case ExpressionType.And: return "AND";
|
||||||
|
case ExpressionType.Equal: return "=";
|
||||||
|
case ExpressionType.GreaterThan: return ">";
|
||||||
|
case ExpressionType.GreaterThanOrEqual: return ">=";
|
||||||
|
case ExpressionType.LessThan: return "<";
|
||||||
|
case ExpressionType.LessThanOrEqual: return "<=";
|
||||||
|
case ExpressionType.NotEqual: return "<>";
|
||||||
|
case ExpressionType.OrElse: return "OR";
|
||||||
|
case ExpressionType.Or: return "OR";
|
||||||
|
default: throw new NotSupportedException(string.Format("{0} statement is not supported", expression.NodeType.ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ParseContainsExpression(MethodCallExpression expression)
|
||||||
|
{
|
||||||
|
var list = expression.Object;
|
||||||
|
|
||||||
|
if (list != null && (list.Type == typeof(string)))
|
||||||
|
{
|
||||||
|
ParseStringContains(expression);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParseEnumerableContains(expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ParseEnumerableContains(MethodCallExpression body)
|
||||||
|
{
|
||||||
|
// Fish out the list and the item to compare
|
||||||
|
// It's in a different form for arrays and Lists
|
||||||
|
var list = body.Object;
|
||||||
|
Expression item;
|
||||||
|
|
||||||
|
if (list != null)
|
||||||
|
{
|
||||||
|
// Generic collection
|
||||||
|
item = body.Arguments[0];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Static method
|
||||||
|
// Must be Enumerable.Contains(source, item)
|
||||||
|
if (body.Method.DeclaringType != typeof(Enumerable) || body.Arguments.Count != 2)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Unexpected form of Enumerable.Contains");
|
||||||
|
}
|
||||||
|
|
||||||
|
list = body.Arguments[0];
|
||||||
|
item = body.Arguments[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
_sb.Append('(');
|
||||||
|
|
||||||
|
Visit(item);
|
||||||
|
|
||||||
|
_sb.Append(" = ANY (");
|
||||||
|
|
||||||
|
// hardcode the integer list if it exists to bypass parameter limit
|
||||||
|
if (item.Type == typeof(int) && TryGetRightValue(list, out var value))
|
||||||
|
{
|
||||||
|
var items = (IEnumerable<int>)value;
|
||||||
|
_sb.Append("('{");
|
||||||
|
_sb.Append(string.Join(", ", items));
|
||||||
|
_sb.Append("}')");
|
||||||
|
|
||||||
|
_gotConcreteValue = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Visit(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
_sb.Append("))");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ParseStringContains(MethodCallExpression body)
|
||||||
|
{
|
||||||
|
_sb.Append('(');
|
||||||
|
|
||||||
|
Visit(body.Object);
|
||||||
|
|
||||||
|
_sb.Append(" ILIKE '%' || ");
|
||||||
|
|
||||||
|
Visit(body.Arguments[0]);
|
||||||
|
|
||||||
|
_sb.Append(" || '%')");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ParseStartsWith(MethodCallExpression body)
|
||||||
|
{
|
||||||
|
_sb.Append('(');
|
||||||
|
|
||||||
|
Visit(body.Object);
|
||||||
|
|
||||||
|
_sb.Append(" ILIKE ");
|
||||||
|
|
||||||
|
Visit(body.Arguments[0]);
|
||||||
|
|
||||||
|
_sb.Append(" || '%')");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ParseEndsWith(MethodCallExpression body)
|
||||||
|
{
|
||||||
|
_sb.Append('(');
|
||||||
|
|
||||||
|
Visit(body.Object);
|
||||||
|
|
||||||
|
_sb.Append(" ILIKE '%' || ");
|
||||||
|
|
||||||
|
Visit(body.Arguments[0]);
|
||||||
|
|
||||||
|
_sb.Append(')');
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var sql = _sb.ToString();
|
||||||
|
|
||||||
|
if (_requireConcreteValue && !_gotConcreteValue)
|
||||||
|
{
|
||||||
|
var e = new InvalidOperationException("WhereBuilder requires a concrete condition");
|
||||||
|
e.Data.Add("sql", sql);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,387 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using Dapper;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore
|
||||||
|
{
|
||||||
|
public class WhereBuilderSqlite : WhereBuilder
|
||||||
|
{
|
||||||
|
protected StringBuilder _sb;
|
||||||
|
|
||||||
|
private const DbType EnumerableMultiParameter = (DbType)(-1);
|
||||||
|
private readonly string _paramNamePrefix;
|
||||||
|
private readonly bool _requireConcreteValue = false;
|
||||||
|
private int _paramCount = 0;
|
||||||
|
private bool _gotConcreteValue = false;
|
||||||
|
|
||||||
|
public WhereBuilderSqlite(Expression filter, bool requireConcreteValue, int seq)
|
||||||
|
{
|
||||||
|
_paramNamePrefix = string.Format("Clause{0}", seq + 1);
|
||||||
|
_requireConcreteValue = requireConcreteValue;
|
||||||
|
_sb = new StringBuilder();
|
||||||
|
|
||||||
|
Parameters = new DynamicParameters();
|
||||||
|
|
||||||
|
if (filter != null)
|
||||||
|
{
|
||||||
|
Visit(filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string AddParameter(object value, DbType? dbType = null)
|
||||||
|
{
|
||||||
|
_gotConcreteValue = true;
|
||||||
|
_paramCount++;
|
||||||
|
var name = _paramNamePrefix + "_P" + _paramCount;
|
||||||
|
Parameters.Add(name, value, dbType);
|
||||||
|
return '@' + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Expression VisitBinary(BinaryExpression expression)
|
||||||
|
{
|
||||||
|
_sb.Append('(');
|
||||||
|
|
||||||
|
Visit(expression.Left);
|
||||||
|
|
||||||
|
_sb.AppendFormat(" {0} ", Decode(expression));
|
||||||
|
|
||||||
|
Visit(expression.Right);
|
||||||
|
|
||||||
|
_sb.Append(')');
|
||||||
|
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Expression VisitMethodCall(MethodCallExpression expression)
|
||||||
|
{
|
||||||
|
var method = expression.Method.Name;
|
||||||
|
|
||||||
|
switch (expression.Method.Name)
|
||||||
|
{
|
||||||
|
case "Contains":
|
||||||
|
ParseContainsExpression(expression);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "StartsWith":
|
||||||
|
ParseStartsWith(expression);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "EndsWith":
|
||||||
|
ParseEndsWith(expression);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
var msg = string.Format("'{0}' expressions are not yet implemented in the where clause expression tree parser.", method);
|
||||||
|
throw new NotImplementedException(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Expression VisitMemberAccess(MemberExpression expression)
|
||||||
|
{
|
||||||
|
var tableName = expression?.Expression?.Type != null ? TableMapping.Mapper.TableNameMapping(expression.Expression.Type) : null;
|
||||||
|
var gotValue = TryGetRightValue(expression, out var value);
|
||||||
|
|
||||||
|
// Only use the SQL condition if the expression didn't resolve to an actual value
|
||||||
|
if (tableName != null && !gotValue)
|
||||||
|
{
|
||||||
|
_sb.Append($"\"{tableName}\".\"{expression.Member.Name}\"");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
// string is IEnumerable<Char> but we don't want to pick up that case
|
||||||
|
var type = value.GetType();
|
||||||
|
var typeInfo = type.GetTypeInfo();
|
||||||
|
var isEnumerable =
|
||||||
|
type != typeof(string) && (
|
||||||
|
typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) ||
|
||||||
|
(typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>)));
|
||||||
|
|
||||||
|
var paramName = isEnumerable ? AddParameter(value, EnumerableMultiParameter) : AddParameter(value);
|
||||||
|
_sb.Append(paramName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_gotConcreteValue = true;
|
||||||
|
_sb.Append("NULL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Expression VisitConstant(ConstantExpression expression)
|
||||||
|
{
|
||||||
|
if (expression.Value != null)
|
||||||
|
{
|
||||||
|
var paramName = AddParameter(expression.Value);
|
||||||
|
_sb.Append(paramName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_gotConcreteValue = true;
|
||||||
|
_sb.Append("NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetConstantValue(Expression expression, out object result)
|
||||||
|
{
|
||||||
|
result = null;
|
||||||
|
|
||||||
|
if (expression is ConstantExpression constExp)
|
||||||
|
{
|
||||||
|
result = constExp.Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetPropertyValue(MemberExpression expression, out object result)
|
||||||
|
{
|
||||||
|
result = null;
|
||||||
|
|
||||||
|
if (expression.Expression is MemberExpression nested)
|
||||||
|
{
|
||||||
|
// Value is passed in as a property on a parent entity
|
||||||
|
var container = (nested.Expression as ConstantExpression)?.Value;
|
||||||
|
|
||||||
|
if (container == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entity = GetFieldValue(container, nested.Member);
|
||||||
|
result = GetFieldValue(entity, expression.Member);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetVariableValue(MemberExpression expression, out object result)
|
||||||
|
{
|
||||||
|
result = null;
|
||||||
|
|
||||||
|
// Value is passed in as a variable
|
||||||
|
if (expression.Expression is ConstantExpression nested)
|
||||||
|
{
|
||||||
|
result = GetFieldValue(nested.Value, expression.Member);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetRightValue(Expression expression, out object value)
|
||||||
|
{
|
||||||
|
value = null;
|
||||||
|
|
||||||
|
if (TryGetConstantValue(expression, out value))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var memberExp = expression as MemberExpression;
|
||||||
|
|
||||||
|
if (TryGetPropertyValue(memberExp, out value))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryGetVariableValue(memberExp, out value))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private object GetFieldValue(object entity, MemberInfo member)
|
||||||
|
{
|
||||||
|
if (member.MemberType == MemberTypes.Field)
|
||||||
|
{
|
||||||
|
return (member as FieldInfo).GetValue(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member.MemberType == MemberTypes.Property)
|
||||||
|
{
|
||||||
|
return (member as PropertyInfo).GetValue(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ArgumentException(string.Format("WhereBuilder could not get the value for {0}.{1}.", entity.GetType().Name, member.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsNullVariable(Expression expression)
|
||||||
|
{
|
||||||
|
if (expression.NodeType == ExpressionType.Constant &&
|
||||||
|
TryGetConstantValue(expression, out var constResult) &&
|
||||||
|
constResult == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expression.NodeType == ExpressionType.MemberAccess &&
|
||||||
|
expression is MemberExpression member &&
|
||||||
|
((TryGetPropertyValue(member, out var result) && result == null) ||
|
||||||
|
(TryGetVariableValue(member, out result) && result == null)))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Decode(BinaryExpression expression)
|
||||||
|
{
|
||||||
|
if (IsNullVariable(expression.Right))
|
||||||
|
{
|
||||||
|
switch (expression.NodeType)
|
||||||
|
{
|
||||||
|
case ExpressionType.Equal: return "IS";
|
||||||
|
case ExpressionType.NotEqual: return "IS NOT";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (expression.NodeType)
|
||||||
|
{
|
||||||
|
case ExpressionType.AndAlso: return "AND";
|
||||||
|
case ExpressionType.And: return "AND";
|
||||||
|
case ExpressionType.Equal: return "=";
|
||||||
|
case ExpressionType.GreaterThan: return ">";
|
||||||
|
case ExpressionType.GreaterThanOrEqual: return ">=";
|
||||||
|
case ExpressionType.LessThan: return "<";
|
||||||
|
case ExpressionType.LessThanOrEqual: return "<=";
|
||||||
|
case ExpressionType.NotEqual: return "<>";
|
||||||
|
case ExpressionType.OrElse: return "OR";
|
||||||
|
case ExpressionType.Or: return "OR";
|
||||||
|
default: throw new NotSupportedException(string.Format("{0} statement is not supported", expression.NodeType.ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ParseContainsExpression(MethodCallExpression expression)
|
||||||
|
{
|
||||||
|
var list = expression.Object;
|
||||||
|
|
||||||
|
if (list != null && (list.Type == typeof(string)))
|
||||||
|
{
|
||||||
|
ParseStringContains(expression);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParseEnumerableContains(expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ParseEnumerableContains(MethodCallExpression body)
|
||||||
|
{
|
||||||
|
// Fish out the list and the item to compare
|
||||||
|
// It's in a different form for arrays and Lists
|
||||||
|
var list = body.Object;
|
||||||
|
Expression item;
|
||||||
|
|
||||||
|
if (list != null)
|
||||||
|
{
|
||||||
|
// Generic collection
|
||||||
|
item = body.Arguments[0];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Static method
|
||||||
|
// Must be Enumerable.Contains(source, item)
|
||||||
|
if (body.Method.DeclaringType != typeof(Enumerable) || body.Arguments.Count != 2)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Unexpected form of Enumerable.Contains");
|
||||||
|
}
|
||||||
|
|
||||||
|
list = body.Arguments[0];
|
||||||
|
item = body.Arguments[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
_sb.Append('(');
|
||||||
|
|
||||||
|
Visit(item);
|
||||||
|
|
||||||
|
_sb.Append(" IN ");
|
||||||
|
|
||||||
|
// hardcode the integer list if it exists to bypass parameter limit
|
||||||
|
if (item.Type == typeof(int) && TryGetRightValue(list, out var value))
|
||||||
|
{
|
||||||
|
var items = (IEnumerable<int>)value;
|
||||||
|
_sb.Append('(');
|
||||||
|
_sb.Append(string.Join(", ", items));
|
||||||
|
_sb.Append(')');
|
||||||
|
|
||||||
|
_gotConcreteValue = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Visit(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
_sb.Append(')');
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ParseStringContains(MethodCallExpression body)
|
||||||
|
{
|
||||||
|
_sb.Append('(');
|
||||||
|
|
||||||
|
Visit(body.Object);
|
||||||
|
|
||||||
|
_sb.Append(" LIKE '%' || ");
|
||||||
|
|
||||||
|
Visit(body.Arguments[0]);
|
||||||
|
|
||||||
|
_sb.Append(" || '%')");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ParseStartsWith(MethodCallExpression body)
|
||||||
|
{
|
||||||
|
_sb.Append('(');
|
||||||
|
|
||||||
|
Visit(body.Object);
|
||||||
|
|
||||||
|
_sb.Append(" LIKE ");
|
||||||
|
|
||||||
|
Visit(body.Arguments[0]);
|
||||||
|
|
||||||
|
_sb.Append(" || '%')");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ParseEndsWith(MethodCallExpression body)
|
||||||
|
{
|
||||||
|
_sb.Append('(');
|
||||||
|
|
||||||
|
Visit(body.Object);
|
||||||
|
|
||||||
|
_sb.Append(" LIKE '%' || ");
|
||||||
|
|
||||||
|
Visit(body.Arguments[0]);
|
||||||
|
|
||||||
|
_sb.Append(')');
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var sql = _sb.ToString();
|
||||||
|
|
||||||
|
if (_requireConcreteValue && !_gotConcreteValue)
|
||||||
|
{
|
||||||
|
var e = new InvalidOperationException("WhereBuilder requires a concrete condition");
|
||||||
|
e.Data.Add("sql", sql);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using Npgsql;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Test.Common.Datastore
|
||||||
|
{
|
||||||
|
public static class PostgresDatabase
|
||||||
|
{
|
||||||
|
public static PostgresOptions GetTestOptions()
|
||||||
|
{
|
||||||
|
var options = PostgresOptions.GetOptions();
|
||||||
|
|
||||||
|
var uid = TestBase.GetUID();
|
||||||
|
options.MainDb = uid + "_main";
|
||||||
|
options.LogDb = uid + "_log";
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Create(PostgresOptions options, MigrationType migrationType)
|
||||||
|
{
|
||||||
|
var db = GetDatabaseName(options, migrationType);
|
||||||
|
var connectionString = GetConnectionString(options);
|
||||||
|
using var conn = new NpgsqlConnection(connectionString);
|
||||||
|
conn.Open();
|
||||||
|
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = $"CREATE DATABASE \"{db}\" WITH OWNER = {options.User} ENCODING = 'UTF8' CONNECTION LIMIT = -1;";
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Drop(PostgresOptions options, MigrationType migrationType)
|
||||||
|
{
|
||||||
|
var db = GetDatabaseName(options, migrationType);
|
||||||
|
var connectionString = GetConnectionString(options);
|
||||||
|
using var conn = new NpgsqlConnection(connectionString);
|
||||||
|
conn.Open();
|
||||||
|
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = $"DROP DATABASE \"{db}\" WITH (FORCE);";
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetConnectionString(PostgresOptions options)
|
||||||
|
{
|
||||||
|
var builder = new NpgsqlConnectionStringBuilder()
|
||||||
|
{
|
||||||
|
Host = options.Host,
|
||||||
|
Port = options.Port,
|
||||||
|
Username = options.User,
|
||||||
|
Password = options.Password,
|
||||||
|
Enlist = false
|
||||||
|
};
|
||||||
|
|
||||||
|
return builder.ConnectionString;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetDatabaseName(PostgresOptions options, MigrationType migrationType)
|
||||||
|
{
|
||||||
|
return migrationType switch
|
||||||
|
{
|
||||||
|
MigrationType.Main => options.MainDb,
|
||||||
|
MigrationType.Log => options.LogDb,
|
||||||
|
_ => throw new NotImplementedException("Unknown migration type")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
using System.IO;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Test.Common.Datastore
|
||||||
|
{
|
||||||
|
public static class SqliteDatabase
|
||||||
|
{
|
||||||
|
public static string GetCachedDb(MigrationType type)
|
||||||
|
{
|
||||||
|
return Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_{type}.db");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RunSettings>
|
||||||
|
<RunConfiguration>
|
||||||
|
<EnvironmentVariables>
|
||||||
|
<Readarr__Postgres__Host>192.168.100.5</Readarr__Postgres__Host>
|
||||||
|
<Readarr__Postgres__Port>5432</Readarr__Postgres__Port>
|
||||||
|
<Readarr__Postgres__User>abc</Readarr__Postgres__User>
|
||||||
|
<Readarr__Postgres__Password>abc</Readarr__Postgres__Password>
|
||||||
|
</EnvironmentVariables>
|
||||||
|
</RunConfiguration>
|
||||||
|
</RunSettings>
|
Loading…
Reference in new issue