Skip to content

Commit

Permalink
Feature/common table references logic for all triggers (#83)
Browse files Browse the repository at this point in the history
* define new triggers API
* remove ArgumentTypes from the project

---------

Co-authored-by: Ilya Belyanskiy <win7user20@gmail.com>
  • Loading branch information
win7user10 and Ilya Belyanskiy authored Apr 15, 2023
1 parent 846d062 commit b5a4fe9
Show file tree
Hide file tree
Showing 250 changed files with 4,086 additions and 4,019 deletions.
37 changes: 25 additions & 12 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ After update Transaction entity, update records in the table with UserBalance en
modelBuilder.Entity<Transaction>()
.AfterUpdate(trigger => trigger
.Action(action => action
.Condition((transactionBeforeUpdate, transactionAfterUpdate) => transactionBeforeUpdate.IsVeryfied && transactionAfterUpdate.IsVeryfied) // Executes only if condition met
.Condition(tableRefs => tableRefs.Old.IsVeryfied && tableRefs.New.IsVeryfied) // Executes only if condition met
.Update<UserBalance>(
(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.
(tableRefs, userBalances) => userBalances.UserId == tableRefs.Old.UserId, // Will be updated entities with matched condition
(tableRefs, oldBalance) => new UserBalance { Balance = oldBalance.Balance + tableRefs.New.Value - tableRefs.Old.Value }))); // New values for matched entities.
```

After insert Transaction entity, upsert record in the table with UserBalance entities.
Expand All @@ -39,11 +39,11 @@ After insert Transaction entity, upsert record in the table with UserBalance ent
modelBuilder.Entity<Transaction>()
.AfterDelete(trigger => trigger
.Action(action => action
.Condition(deletedTransaction => deletedTransaction.IsVeryfied)
.Condition(tableRefs => tableRefs.Old.IsVeryfied)
.Upsert(
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
(tableRefs, balances) => tableRefs.Old.UserId == balances.UserId, // If this field will match more than 0 rows, will be executed update operation for these rows else insert
tableRefs => new UserBalance { UserId = tableRefs.Old.UserId, Balance = tableRefs.Old.Value }, // Insert, if value didn't exist
(tableRefs, oldUserBalance) => new UserBalance { Balance = oldUserBalance.Balance + tableRefs.Old.Value }))); // Update all matched values
```

After delete Transaction entity, execute raw SQL. Pass deleted entity fields as arguments.
Expand All @@ -52,7 +52,13 @@ After delete Transaction entity, execute raw SQL. Pass deleted entity fields as
modelBuilder.Entity<Transaction>()
.AfterDelete(trigger => trigger
.Action(action => action
.ExecuteRawSql("PERFORM recalc_balance({0}, {1})"), deletedEntity => deletedEntity.UserId, deletedEntity => deletedEntity.Amount)));
.ExecuteRawSql("PERFORM recalc_balance({0}, {1})"), tableRefs => tableRefs.Old.UserId, tableRefs => tableRefs.Old.Amount)));
```

Also, different trigger functions can be used to generate the SQL.
```
TriggerFunctions.GetTableName<Transaction>();
TriggerFunctions.GetColumnName<Transaction>(transaction => transaction.Value);
```

### All available triggers
Expand Down Expand Up @@ -182,7 +188,7 @@ public static class StringExtensions
{
public static bool Like(this string str, string pattern)
{
// Some code
throw new InvalidOperationException();
}
}
```
Expand All @@ -197,13 +203,13 @@ public abstract class StringExtensionsLikeConverter : MethodCallConverter
return expression.Method.ReflectedType == typeof(SomeFunctions) && MethodName == nameof(CustomFunctions.Like);
}

public override SqlBuilder BuildSql(BaseExpressionProvider provider, MethodCallExpression expression, Dictionary<string, ArgumentType> argumentTypes)
public override SqlBuilder BuildSql(BaseExpressionProvider provider, MethodCallExpression expression)
{
// Generate SQL for arguments, they can be SQL expressions
var argumentSql = provider.GetMethodCallArgumentsSql(expression, argumentTypes)[0];
var argumentSql = provider.GetMethodCallArgumentsSql(expression)[0];

// Generate SQL for this context, it also can be a SQL expression
var sqlBuilder = provider.GetExpressionSql(expression.Object, argumentTypes);
var sqlBuilder = provider.GetExpressionSql(expression.Object);

// Combine SQL for object and SQL for arguments
// Output will be like "thisValueSql LIKE 'passedArgumentValueSql'"
Expand Down Expand Up @@ -232,3 +238,10 @@ modelBuilder.Entity<Transaction>()
.Condition(oldTransaction => oldTransaction.Description.Like('%payment%'))

```

### Trigger prefix customization

You can change the standard library prefix for trigger using the next static variable
```cs
Laraue.EfCoreTriggers.Common.Constants.AnnotationKey = "MY_PREFIX"
```
27 changes: 14 additions & 13 deletions src/Laraue.EfCoreTriggers.Common/CSharpMethods/BinaryFunctions.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
using System.Linq.Expressions;

namespace Laraue.EfCoreTriggers.Common.CSharpMethods;

/// <summary>
/// Helper for binary functions translation.
/// </summary>
public static class BinaryFunctions
namespace Laraue.EfCoreTriggers.Common.CSharpMethods
{
/// <summary>
/// Translation of the <see cref="ExpressionType.Coalesce"/> binary.
/// Helper for binary functions translation.
/// </summary>
/// <param name="value"></param>
/// <param name="valueIfNull"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static T Coalesce<T>(T value, T valueIfNull)
public static class BinaryFunctions
{
return value ?? valueIfNull;
/// <summary>
/// Translation of the <see cref="ExpressionType.Coalesce"/> binary.
/// </summary>
/// <param name="value"></param>
/// <param name="valueIfNull"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static T Coalesce<T>(T value, T valueIfNull)
{
return value ?? valueIfNull;
}
}
}
7 changes: 6 additions & 1 deletion src/Laraue.EfCoreTriggers.Common/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ public static class Constants
{
/// <summary>
/// All triggers names starts with this key. Acronym from laraue core trigger.
/// Note: if triggers already created with one prefix, it's changing will be
/// a problem, because this prefix is using to find trigger annotations
/// while migrations generating.
/// The best way will be to generate a new migrations, manually fix they names
/// to start from the new <see cref="AnnotationKey"/>, only then change the value.
/// </summary>
public const string AnnotationKey = "LC_TRIGGER";
public static string AnnotationKey { get; set; } = "LC_TRIGGER";
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
using System;
using System.Linq.Expressions;
using Laraue.EfCoreTriggers.Common.Services.Impl.ExpressionVisitors;
using Laraue.EfCoreTriggers.Common.SqlGeneration;
using Laraue.EfCoreTriggers.Common.TriggerBuilders;
using Laraue.EfCoreTriggers.Common.Visitors.ExpressionVisitors;

namespace Laraue.EfCoreTriggers.Common.Converters.MethodCall
{
Expand Down Expand Up @@ -44,7 +43,6 @@ public bool IsApplicable(MethodCallExpression expression)
/// <inheritdoc />
public abstract SqlBuilder Visit(
MethodCallExpression expression,
ArgumentTypes argumentTypes,
VisitedMembers visitedMembers);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System;
using Laraue.EfCoreTriggers.Common.CSharpMethods;
using Laraue.EfCoreTriggers.Common.Services.Impl.ExpressionVisitors;
using Laraue.EfCoreTriggers.Common.Visitors.ExpressionVisitors;

namespace Laraue.EfCoreTriggers.Common.Converters.MethodCall.CSharpMethods
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
using System.Linq;
using System.Linq.Expressions;
using Laraue.EfCoreTriggers.Common.CSharpMethods;
using Laraue.EfCoreTriggers.Common.Services.Impl.ExpressionVisitors;
using Laraue.EfCoreTriggers.Common.SqlGeneration;
using Laraue.EfCoreTriggers.Common.TriggerBuilders;
using Laraue.EfCoreTriggers.Common.Visitors.ExpressionVisitors;

namespace Laraue.EfCoreTriggers.Common.Converters.MethodCall.CSharpMethods
{
/// <summary>
/// Base visitor for <see cref="BinaryFunctions"/> methods.
/// </summary>
public class CoalesceVisitor : BaseBinaryFunctionsVisitor
public sealed class CoalesceVisitor : BaseBinaryFunctionsVisitor
{
/// <inheritdoc />
protected override string MethodName => nameof(BinaryFunctions.Coalesce);

/// <inheritdoc />
Expand All @@ -20,16 +20,17 @@ public CoalesceVisitor(IExpressionVisitorFactory visitorFactory)
{
}

public override SqlBuilder Visit(MethodCallExpression expression, ArgumentTypes argumentTypes, VisitedMembers visitedMembers)
/// <inheritdoc />
public override SqlBuilder Visit(MethodCallExpression expression, VisitedMembers visitedMembers)
{
var argumentsSql = expression.Arguments
.Select(argument => VisitorFactory.Visit(argument, argumentTypes, visitedMembers))
.Select(argument => VisitorFactory.Visit(argument, visitedMembers))
.ToArray();

return GetSql(argumentsSql[0], argumentsSql[1]);
}

protected virtual SqlBuilder GetSql(SqlBuilder isNullExpressionSql, SqlBuilder whenNullExpressionSql)
private static SqlBuilder GetSql(SqlBuilder isNullExpressionSql, SqlBuilder whenNullExpressionSql)
{
return SqlBuilder.FromString($"COALESCE({isNullExpressionSql}, {whenNullExpressionSql})");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Laraue.EfCoreTriggers.Common.Services;
using Laraue.EfCoreTriggers.Common.Services.Impl.ExpressionVisitors;
using Laraue.EfCoreTriggers.Common.SqlGeneration;
using Laraue.EfCoreTriggers.Common.TriggerBuilders;
using Laraue.EfCoreTriggers.Common.TriggerBuilders.TableRefs;
using Laraue.EfCoreTriggers.Common.Visitors.ExpressionVisitors;

namespace Laraue.EfCoreTriggers.Common.Converters.MethodCall.Enumerable
{
Expand Down Expand Up @@ -34,16 +34,21 @@ protected BaseEnumerableVisitor(
_expressionVisitorFactory = visitorFactory;
}

public override SqlBuilder Visit(MethodCallExpression expression, ArgumentTypes argumentTypes, VisitedMembers visitedMembers)
/// <inheritdoc />
public override SqlBuilder Visit(MethodCallExpression expression, VisitedMembers visitedMembers)
{
var whereExpressions = new HashSet<Expression>();
var exp = GetFlattenExpressions(expression, whereExpressions);

if (exp is not MemberExpression baseMember)
{
throw new InvalidOperationException("Member expression was excepted");
}

var baseMember = exp as MemberExpression;
var enumerableMemberType = baseMember.Type;

var originalSetMemberExpression = (ParameterExpression) baseMember.Expression;
var originalSetType = originalSetMemberExpression?.Type;
var originalSetType = baseMember.Expression?.Type
?? throw new InvalidOperationException("Not null expression in the passed member excepted.");

if (!typeof(IEnumerable).IsAssignableFrom(enumerableMemberType) || !enumerableMemberType.IsGenericType)
{
Expand All @@ -56,7 +61,7 @@ public override SqlBuilder Visit(MethodCallExpression expression, ArgumentTypes
.Skip(1)
.ToArray();

var selectSql = Visit(otherArguments, argumentTypes, visitedMembers);
var selectSql = Visit(otherArguments, visitedMembers);

if (selectSql.Item2 is not null)
{
Expand All @@ -77,10 +82,15 @@ public override SqlBuilder Visit(MethodCallExpression expression, ArgumentTypes
foreach (var key in keys)
{
var column1Sql = _sqlGenerator.GetColumnSql(entityType, key.ForeignKey, ArgumentType.Default);

if (baseMember.Expression is not MemberExpression memberExpression)
{
throw new InvalidOperationException("Member expression was excepted");
}

var argument2Type = argumentTypes.Get(originalSetMemberExpression);
var argument2Type = memberExpression.Member.GetArgumentType();

var column2WhereSql = _sqlGenerator.GetVariableSql(originalSetType, key.PrincipalKey, argument2Type);
var column2WhereSql = _sqlGenerator.GetColumnValueReferenceSql(originalSetType, key.PrincipalKey, argument2Type);
visitedMembers.AddMember(argument2Type, key.PrincipalKey);

var column2JoinSql = _sqlGenerator.GetColumnSql(originalSetType, key.PrincipalKey, ArgumentType.Default);
Expand All @@ -91,7 +101,7 @@ public override SqlBuilder Visit(MethodCallExpression expression, ArgumentTypes

foreach (var e in whereExpressions)
{
joinParts.Add(_expressionVisitorFactory.Visit(e, argumentTypes, visitedMembers));
joinParts.Add(_expressionVisitorFactory.Visit(e, visitedMembers));
}

finalSql.AppendJoin(" AND ", joinParts);
Expand All @@ -117,9 +127,14 @@ private Expression GetFlattenExpressions(MethodCallExpression methodCallExpressi
}
}

/// <summary>
/// Generate pairs SqlBuilder -> Expression for all passed expressions
/// </summary>
/// <param name="arguments"></param>
/// <param name="visitedMembers"></param>
/// <returns></returns>
protected abstract (SqlBuilder, Expression) Visit(
Expression[] arguments,
ArgumentTypes argumentTypes,
IEnumerable<Expression> arguments,
VisitedMembers visitedMembers);
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Laraue.EfCoreTriggers.Common.Services;
using Laraue.EfCoreTriggers.Common.Services.Impl.ExpressionVisitors;
using Laraue.EfCoreTriggers.Common.SqlGeneration;
using Laraue.EfCoreTriggers.Common.TriggerBuilders;
using Laraue.EfCoreTriggers.Common.Visitors.ExpressionVisitors;

namespace Laraue.EfCoreTriggers.Common.Converters.MethodCall.Enumerable.Count;

public class CountVisitor : BaseEnumerableVisitor
namespace Laraue.EfCoreTriggers.Common.Converters.MethodCall.Enumerable.Count
{
protected override string MethodName => nameof(System.Linq.Enumerable.Count);

private readonly IExpressionVisitorFactory _expressionVisitorFactory;

public CountVisitor(
IExpressionVisitorFactory visitorFactory,
IDbSchemaRetriever schemaRetriever,
ISqlGenerator sqlGenerator)
: base(visitorFactory, schemaRetriever, sqlGenerator)
/// <inheritdoc />
public sealed class CountVisitor : BaseEnumerableVisitor
{
_expressionVisitorFactory = visitorFactory;
}
/// <inheritdoc />
protected override string MethodName => nameof(System.Linq.Enumerable.Count);

/// <summary>
/// Initializes a new instance of <see cref="CountVisitor"/>.
/// </summary>
/// <param name="visitorFactory"></param>
/// <param name="schemaRetriever"></param>
/// <param name="sqlGenerator"></param>
public CountVisitor(
IExpressionVisitorFactory visitorFactory,
IDbSchemaRetriever schemaRetriever,
ISqlGenerator sqlGenerator)
: base(visitorFactory, schemaRetriever, sqlGenerator)
{
}

protected override (SqlBuilder, Expression) Visit(Expression[] arguments, ArgumentTypes argumentTypes, VisitedMembers visitedMembers)
{
return (SqlBuilder.FromString("count(*)"), arguments.FirstOrDefault());
/// <inheritdoc />
protected override (SqlBuilder, Expression) Visit(IEnumerable<Expression> arguments, VisitedMembers visitedMembers)
{
return (SqlBuilder.FromString("count(*)"), arguments.FirstOrDefault());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using Laraue.EfCoreTriggers.Common.Functions;
using Laraue.EfCoreTriggers.Common.Visitors.ExpressionVisitors;

namespace Laraue.EfCoreTriggers.Common.Converters.MethodCall.Functions
{
/// <summary>
/// Base visitor for the <see cref="TriggerFunctions"/>.
/// </summary>
public abstract class BaseTriggerFunctionsVisitor : BaseMethodCallVisitor
{
/// Initializes a new instance of <see cref="BaseTriggerFunctionsVisitor"/>.
protected BaseTriggerFunctionsVisitor(IExpressionVisitorFactory visitorFactory)
: base(visitorFactory)
{
}

/// <inheritdoc />
protected override Type ReflectedType => typeof(TriggerFunctions);
}
}
Loading

0 comments on commit b5a4fe9

Please sign in to comment.