Skip to content

Commit

Permalink
Feature/refactoring tests (#14)
Browse files Browse the repository at this point in the history
* feat: A lot of test cases added and bugfixes made 

* feat: Handling exception when trigger builder uses an entity not added in a DbContext

* feat: Support of Math, String functions added

Co-authored-by: Ilya Belyanskiy <win7user20@gmail.com>
  • Loading branch information
anastazzy and Ilya Belyanskiy authored Sep 27, 2021
1 parent 6f8cad3 commit e2d9004
Show file tree
Hide file tree
Showing 112 changed files with 2,860 additions and 934 deletions.
10 changes: 5 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ After update Transaction entity, update records in the table with UserBalance en
modelBuilder.Entity<Transaction>()
.AfterUpdate(trigger => trigger
.Action(action => action
.Condition((oldTransaction, newTransaction) => oldTransaction.IsVeryfied && newTransaction.IsVeryfied) // Executes only if condition met
.Condition((transactionBeforeUpdate, transactionAfterUpdate) => transactionBeforeUpdate.IsVeryfied && transactionAfterUpdate.IsVeryfied) // Executes only if condition met
.Update<UserBalance>(
(oldTransaction, updatedTransaction, userBalances) => userBalances.UserId == oldTransaction.UserId, // Will be updated entities with matched condition
(transactionBeforeUpdate, transactionAfterUpdate, userBalances) => userBalances.UserId == oldTransaction.UserId, // Will be updated entities with matched condition
(oldTransaction, updatedTransaction, oldBalance) => new UserBalance { Balance = oldBalance.Balance + updatedTransaction.Value - oldTransaction.Value }))); // New values for matched entities.
```

Expand All @@ -40,9 +40,9 @@ modelBuilder.Entity<Transaction>()
.Action(action => action
.Condition(deletedTransaction => deletedTransaction.IsVeryfied)
.Upsert(
balance => new { balance.UserId }, // If this field is matched, will be executed update operation else insert
insertedTransaction => new UserBalance { UserId = insertedTransaction.UserId, Balance = insertedTransaction.Value }, // Insert, if value didn't exist
(insertedTransaction, oldUserBalance) => new UserBalance { Balance = oldUserBalance.Balance + insertedTransaction.Value }))); // Update if value existed
deletedTransaction => new UserBalance { UserId = deletedTransaction.UserId }, // If this field will match more than 0 rows, will be executed update operation for these rows else insert
deletedTransaction => new UserBalance { UserId = deletedTransaction.UserId, Balance = deletedTransaction.Value }, // Insert, if value didn't exist
(deletedTransaction, oldUserBalance) => new UserBalance { Balance = oldUserBalance.Balance + deletedTransaction.Value }))); // Update all matched values
```

More examples of using are available in Tests/NativeDbContext.cs.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
using Laraue.EfCoreTriggers.Common.SqlGeneration;
using Laraue.EfCoreTriggers.Common.TriggerBuilders;
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq.Expressions;
using Laraue.EfCoreTriggers.Common.Extensions;
using Laraue.EfCoreTriggers.Common.SqlGeneration;
using Laraue.EfCoreTriggers.Common.TriggerBuilders;

namespace Laraue.EfCoreTriggers.Common.Converters.MethodCall.Math.AtanTwo
namespace Laraue.EfCoreTriggers.Common.Converters.MethodCall.Math.Atan2
{
public class MathAtanTwoConverter : BaseMathConverter
public abstract class BaseAtan2Converter : BaseMathConverter
{
public override string MethodName => nameof(System.Math.Atan2);

protected abstract string SqlFunctionName { get; }

public override SqlBuilder BuildSql(BaseExpressionProvider provider, MethodCallExpression expression, Dictionary<string, ArgumentType> argumentTypes)
{
var argumentsSql = provider.GetMethodCallArgumentsSql(expression, argumentTypes);

return new SqlBuilder($"ATAN2({argumentsSql[0]}, {argumentsSql[1]})")
return new SqlBuilder($"{SqlFunctionName}({argumentsSql[0]}, {argumentsSql[1]})")
.MergeColumnsInfo(argumentsSql);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Laraue.EfCoreTriggers.Common.Converters.MethodCall.Math.Atan2
{
public class MathAtan2Converter : BaseAtan2Converter
{
protected override string SqlFunctionName => "ATAN2";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Laraue.EfCoreTriggers.Common.Converters.MethodCall.Math.Atan2
{
public class MathAtn2Converter : BaseAtan2Converter
{
protected override string SqlFunctionName => "ATN2";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Laraue.EfCoreTriggers.Common.SqlGeneration;
using Laraue.EfCoreTriggers.Common.TriggerBuilders;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace Laraue.EfCoreTriggers.Common.Converters.MethodCall.Math.Ceiling
{
public class MathCeilConverter : BaseMathConverter
{
public override string MethodName => nameof(System.Math.Ceiling);

public override SqlBuilder BuildSql(BaseExpressionProvider provider, MethodCallExpression expression, Dictionary<string, ArgumentType> argumentTypes)
{
var argument = expression.Arguments[0];
var sqlBuilder = provider.GetExpressionSql(argument, argumentTypes);
return new SqlBuilder(sqlBuilder.AffectedColumns, $"CEIL({sqlBuilder})");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ namespace Laraue.EfCoreTriggers.Common.Converters.MethodCall.String.Contains
{
public abstract class BaseStringContainsConverter : BaseStringConverter
{
public abstract string SqlFunctionName { get; }

/// <inheritdoc />
public override string MethodName => nameof(string.Contains);

/// <inheritdoc />
public override SqlBuilder BuildSql(BaseExpressionProvider provider, MethodCallExpression expression, Dictionary<string, ArgumentType> argumentTypes)
{
var argumentSql = provider.GetMethodCallArgumentsSql(expression, argumentTypes)[0];

var sqlBuilder = provider.GetExpressionSql(expression.Object, argumentTypes);
return new SqlBuilder(sqlBuilder.AffectedColumns, $"{SqlFunctionName}({sqlBuilder}, {argumentSql}) > 0")
.MergeColumnsInfo(argumentSql);
var expressionToFindSql = provider.GetMethodCallArgumentsSql(expression, argumentTypes)[0];
var expressionToSearchSql = provider.GetExpressionSql(expression.Object, argumentTypes);

return new SqlBuilder(expressionToFindSql.AffectedColumns, CombineSql(expressionToSearchSql, expressionToFindSql))
.MergeColumnsInfo(expressionToSearchSql);
}

protected abstract string CombineSql(string expressionToSearchSql, string expressionToFindSql);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
{
public class StringContainsViaCharindexFuncConverter : BaseStringContainsConverter
{
public override string SqlFunctionName => "CHARINDEX";
protected override string CombineSql(string expressionToSearchSql, string expressionToFindSql)
{
return $"CHARINDEX({expressionToFindSql}, {expressionToSearchSql}) > 0";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
{
public class StringContainsViaInstrFuncConverter : BaseStringContainsConverter
{
public override string SqlFunctionName => "INSTR";
protected override string CombineSql(string expressionToSearchSql, string expressionToFindSql)
{
return $"INSTR({expressionToSearchSql}, {expressionToFindSql}) > 0";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
{
public class StringContainsViaStrposFuncConverter : BaseStringContainsConverter
{
public override string SqlFunctionName => "STRPOS";
protected override string CombineSql(string expressionToSearchSql, string expressionToFindSql)
{
return $"STRPOS({expressionToSearchSql}, {expressionToFindSql}) > 0";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,32 @@ namespace Laraue.EfCoreTriggers.Common.Converters.MethodCall.String.Trim
{
public abstract class BaseStringTrimConverter : BaseStringConverter
{
public abstract string TrimsTypeName { get; }
/// <summary>Sequence of SQL functions which should be </summary>
protected abstract string[] SqlTrimFunctionsNamesToApply { get; }

/// <inheritdoc />
public override string MethodName => nameof(string.Trim);

/// <inheritdoc />
public override SqlBuilder BuildSql(BaseExpressionProvider provider, MethodCallExpression expression, Dictionary<string, ArgumentType> argumentTypes)
{
var sqlBuilder = provider.GetExpressionSql(expression.Object, argumentTypes);
return new(sqlBuilder.AffectedColumns, $"{TrimsTypeName}({sqlBuilder})");
var expressionSqlBuilder = provider.GetExpressionSql(expression.Object, argumentTypes);

var sqlBuilder = new SqlBuilder(expressionSqlBuilder.AffectedColumns);

foreach (var trimFunctionName in SqlTrimFunctionsNamesToApply)
{
sqlBuilder.Append(trimFunctionName).Append('(');
}

sqlBuilder.Append(expressionSqlBuilder);

foreach (var _ in SqlTrimFunctionsNamesToApply)
{
sqlBuilder.Append(')');
}

return sqlBuilder;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
{
public class StringTrimViaBtrimFuncConverter : BaseStringTrimConverter
{
public override string TrimsTypeName => "BTRIM";
protected override string[] SqlTrimFunctionsNamesToApply { get; } = { "BTRIM" };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Laraue.EfCoreTriggers.Common.Converters.MethodCall.String.Trim
{
public class StringTrimViaLtrimRtrimFuncConverter : BaseStringTrimConverter
{
protected override string[] SqlTrimFunctionsNamesToApply { get; } = { "LTRIM", "RTRIM" };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
{
public class StringTrimViaTrimFuncConverter : BaseStringTrimConverter
{
public override string TrimsTypeName => "TRIM";
protected override string[] SqlTrimFunctionsNamesToApply { get; } = { "TRIM" };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<PackageProjectUrl>https://github.com/win7user10/Laraue.EfCoreTriggers</PackageProjectUrl>
<PackageTags>Entity Framework Core;entity-framework-core;ef;efcore;triggers;sql</PackageTags>
<LangVersion>Latest</LangVersion>
<PackageVersion>1.1.0</PackageVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,18 @@ public abstract class BaseExpressionProvider : EfCoreMetadataRetriever
/// <param name="type"></param>
/// <returns></returns>
protected string GetSqlType(Type type)
{
{
type = GetNotNullableType(type);
type = type.IsEnum ? typeof(Enum) : type;
TypeMappings.TryGetValue(type, out var sqlType);
return sqlType;
}

protected Type GetNotNullableType(Type type)
{
var nullableUnderlyingType = Nullable.GetUnderlyingType(type);
return nullableUnderlyingType ?? type;
}

/// <summary>
/// Get expression operand based on <see cref="ExpressionType"/> property.
Expand Down Expand Up @@ -166,13 +173,18 @@ protected virtual SqlBuilder GetMemberExpressionSql(MemberExpression memberExpre
}

protected virtual string GetMemberExpressionSql(MemberExpression memberExpression, ArgumentType argumentType)
{
return GetColumnSql(memberExpression.Member, argumentType);
}

protected virtual string GetColumnSql(MemberInfo memberInfo, ArgumentType argumentType)
{
return argumentType switch
{
ArgumentType.New => $"{NewEntityPrefix}.{GetColumnName(memberExpression.Member)}",
ArgumentType.Old => $"{OldEntityPrefix}.{GetColumnName(memberExpression.Member)}",
ArgumentType.None => GetColumnName(memberExpression.Member),
_ => $"{GetTableName(memberExpression.Member)}.{GetColumnName(memberExpression.Member)}",
ArgumentType.New => $"{NewEntityPrefix}.{GetColumnName(memberInfo)}",
ArgumentType.Old => $"{OldEntityPrefix}.{GetColumnName(memberInfo)}",
ArgumentType.None => GetColumnName(memberInfo),
_ => $"{GetTableName(memberInfo)}.{GetColumnName(memberInfo)}",
};
}

Expand All @@ -182,11 +194,11 @@ protected virtual string GetMemberExpressionSql(MemberExpression memberExpressio
/// <param name="memberAssignment"></param>
/// <param name="argumentTypes"></param>
/// <returns></returns>
protected virtual (MemberInfo MemberInfo, SqlBuilder AssignmentSqlResult) GetMemberAssignmentParts(
protected virtual (MemberAssignment MemberAssignment, SqlBuilder AssignmentSqlResult) GetMemberAssignmentParts(
MemberAssignment memberAssignment, Dictionary<string, ArgumentType> argumentTypes)
{
var sqlExtendedResult = GetExpressionSql(memberAssignment.Expression, argumentTypes);
return (memberAssignment.Member, sqlExtendedResult);
return (memberAssignment, sqlExtendedResult);
}

protected virtual SqlBuilder GetMethodCallExpressionSql(MethodCallExpression methodCallExpression, Dictionary<string, ArgumentType> argumentTypes)
Expand All @@ -204,40 +216,32 @@ protected virtual SqlBuilder GetMethodCallExpressionSql(MethodCallExpression met

protected virtual SqlBuilder GetUnaryExpressionSql(UnaryExpression unaryExpression, Dictionary<string, ArgumentType> argumentTypes)
{
var sqlBuilder = new SqlBuilder();

var internalSql = unaryExpression.Operand switch
var internalExpressionSql = GetExpressionSql(unaryExpression.Operand, argumentTypes);
var sqlBuilder = new SqlBuilder(internalExpressionSql.AffectedColumns);

if (unaryExpression.NodeType == ExpressionType.Convert)
{
MemberExpression memberExpression => GetMemberExpressionSql(memberExpression, argumentTypes),
UnaryExpression internalUnaryExpression => GetUnaryExpressionSql(internalUnaryExpression, argumentTypes),
_ => throw new NotSupportedException($"Operand {unaryExpression.Operand.Type} is not supported for UnaryExpression")
};

sqlBuilder.MergeColumnsInfo(internalSql.AffectedColumns);
sqlBuilder.Append(GetUnaryExpressionSql(unaryExpression, internalSql.Sql));

return sqlBuilder;
}

if (IsNeedConvertion(unaryExpression))
{
sqlBuilder.Append(GetConvertExpressionSql(unaryExpression, internalExpressionSql));
}
else
{
sqlBuilder = internalExpressionSql;
}

protected virtual string GetUnaryExpressionSql(Expression expression, string member)
{
if (expression.NodeType == ExpressionType.Convert)
{
var unaryExpression = expression as UnaryExpression;
return IsNeedConvertion(unaryExpression)
? GetConvertExpressionSql(unaryExpression, member)
: member;
}
var operand = GetExpressionOperandSql(expression);
if (expression.NodeType == ExpressionType.Negate)
{
return $"{operand}{member}";
}
else
{
return $"{member} {operand}";
return sqlBuilder;
}

var operand = GetExpressionOperandSql(unaryExpression);

var sql = unaryExpression.NodeType == ExpressionType.Negate
? $"{operand}{internalExpressionSql}"
: $"{internalExpressionSql} {operand}";

sqlBuilder.Append(sql);

return sqlBuilder;
}

protected virtual SqlBuilder[] GetNewExpressionColumnsSql(NewExpression newExpression, Dictionary<string, ArgumentType> argumentTypes)
Expand Down Expand Up @@ -265,7 +269,7 @@ Expression[] GetBinaryExpressionParts()

var binaryExpressionParts = GetBinaryExpressionParts();

// Check, if one arument is null, should be generated expression "value IS NULL"
// Check, if one argument is null, should be generated expression "value IS NULL"
if (binaryExpression.NodeType is ExpressionType.Equal || binaryExpression.NodeType is ExpressionType.NotEqual)
{
if (binaryExpressionParts.Any(x => x is ConstantExpression constExpr && constExpr.Value == null))
Expand All @@ -291,13 +295,20 @@ Expression[] GetBinaryExpressionParts()
/// <param name="memberInitExpression"></param>
/// <param name="argumentTypes"></param>
/// <returns></returns>
protected Dictionary<MemberInfo, SqlBuilder> GetMemberInitExpressionAssignmentParts(MemberInitExpression memberInitExpression, Dictionary<string, ArgumentType> argumentTypes)
protected Dictionary<MemberAssignment, SqlBuilder> GetMemberInitExpressionAssignmentParts(MemberInitExpression memberInitExpression, Dictionary<string, ArgumentType> argumentTypes)
{
return memberInitExpression.Bindings.Select(memberBinding =>
{
var memberAssignmentExpression = (MemberAssignment)memberBinding;
return GetMemberAssignmentParts(memberAssignmentExpression, argumentTypes);
}).ToDictionary(x => x.MemberInfo, x => x.AssignmentSqlResult);
}).ToDictionary(x => x.MemberAssignment, x => x.AssignmentSqlResult);
}

protected Dictionary<MemberExpression, SqlBuilder> GetNewExpressionAssignmentParts(NewExpression newExpression, Dictionary<string, ArgumentType> argumentTypes)
{
return newExpression.Arguments.ToDictionary(
argument => (MemberExpression)argument,
argument => GetExpressionSql(argument, argumentTypes));
}

/// <summary>
Expand Down
Loading

0 comments on commit e2d9004

Please sign in to comment.