Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -678,13 +678,13 @@ private CountNode GetCountClause(CountExpression expression, TableAccessorNode o

public override SqlTreeNode VisitMatchText(MatchTextExpression expression, TableAccessorNode tableAccessor)
{
var column = (ColumnNode)Visit(expression.TargetAttribute, tableAccessor);
var column = (ColumnNode)Visit(expression.MatchTarget, tableAccessor);
return new LikeNode(column, expression.MatchKind, (string)expression.TextValue.TypedValue);
}

public override SqlTreeNode VisitAny(AnyExpression expression, TableAccessorNode tableAccessor)
{
var column = (ColumnNode)Visit(expression.TargetAttribute, tableAccessor);
var column = (ColumnNode)Visit(expression.MatchTarget, tableAccessor);

ReadOnlyCollection<ParameterNode> parameters =
VisitSequence<LiteralConstantExpression, ParameterNode>(expression.Constants.OrderBy(constant => constant.TypedValue), tableAccessor);
Expand Down
16 changes: 8 additions & 8 deletions src/JsonApiDotNetCore/Queries/Expressions/AnyExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,21 @@ namespace JsonApiDotNetCore.Queries.Expressions;
public class AnyExpression : FilterExpression
{
/// <summary>
/// The attribute whose value to compare. Chain format: an optional list of to-one relationships, followed by an attribute.
/// The function or attribute whose value to compare. Attribute chain format: an optional list of to-one relationships, followed by an attribute.
/// </summary>
public ResourceFieldChainExpression TargetAttribute { get; }
public QueryExpression MatchTarget { get; }

/// <summary>
/// One or more constants to compare the attribute's value against.
/// </summary>
public IImmutableSet<LiteralConstantExpression> Constants { get; }

public AnyExpression(ResourceFieldChainExpression targetAttribute, IImmutableSet<LiteralConstantExpression> constants)
public AnyExpression(QueryExpression matchTarget, IImmutableSet<LiteralConstantExpression> constants)
{
ArgumentNullException.ThrowIfNull(targetAttribute);
ArgumentNullException.ThrowIfNull(matchTarget);
ArgumentGuard.NotNullNorEmpty(constants);

TargetAttribute = targetAttribute;
MatchTarget = matchTarget;
Constants = constants;
}

Expand All @@ -56,7 +56,7 @@ private string InnerToString(bool toFullString)

builder.Append(Keywords.Any);
builder.Append('(');
builder.Append(toFullString ? TargetAttribute.ToFullString() : TargetAttribute.ToString());
builder.Append(toFullString ? MatchTarget.ToFullString() : MatchTarget.ToString());
builder.Append(',');
builder.Append(string.Join(',', Constants.Select(constant => toFullString ? constant.ToFullString() : constant.ToString()).Order()));
builder.Append(')');
Expand All @@ -78,13 +78,13 @@ public override bool Equals(object? obj)

var other = (AnyExpression)obj;

return TargetAttribute.Equals(other.TargetAttribute) && Constants.SetEquals(other.Constants);
return MatchTarget.Equals(other.MatchTarget) && Constants.SetEquals(other.Constants);
}

public override int GetHashCode()
{
var hashCode = new HashCode();
hashCode.Add(TargetAttribute);
hashCode.Add(MatchTarget);

foreach (LiteralConstantExpression constant in Constants)
{
Expand Down
20 changes: 10 additions & 10 deletions src/JsonApiDotNetCore/Queries/Expressions/MatchTextExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ namespace JsonApiDotNetCore.Queries.Expressions;
public class MatchTextExpression : FilterExpression
{
/// <summary>
/// The attribute whose value to match. Chain format: an optional list of to-one relationships, followed by an attribute.
/// The function or attribute whose value to match. Attribute chain format: an optional list of to-one relationships, followed by an attribute.
/// </summary>
public ResourceFieldChainExpression TargetAttribute { get; }
public QueryExpression MatchTarget { get; }

/// <summary>
/// The text to match the attribute's value against.
/// The text to match against.
/// </summary>
public LiteralConstantExpression TextValue { get; }

Expand All @@ -38,12 +38,12 @@ public class MatchTextExpression : FilterExpression
/// </summary>
public TextMatchKind MatchKind { get; }

public MatchTextExpression(ResourceFieldChainExpression targetAttribute, LiteralConstantExpression textValue, TextMatchKind matchKind)
public MatchTextExpression(QueryExpression matchTarget, LiteralConstantExpression textValue, TextMatchKind matchKind)
{
ArgumentNullException.ThrowIfNull(targetAttribute);
ArgumentNullException.ThrowIfNull(matchTarget);
ArgumentNullException.ThrowIfNull(textValue);

TargetAttribute = targetAttribute;
MatchTarget = matchTarget;
TextValue = textValue;
MatchKind = matchKind;
}
Expand Down Expand Up @@ -71,8 +71,8 @@ private string InnerToString(bool toFullString)
builder.Append('(');

builder.Append(toFullString
? string.Join(',', TargetAttribute.ToFullString(), TextValue.ToFullString())
: string.Join(',', TargetAttribute.ToString(), TextValue.ToString()));
? string.Join(',', MatchTarget.ToFullString(), TextValue.ToFullString())
: string.Join(',', MatchTarget.ToString(), TextValue.ToString()));

builder.Append(')');

Expand All @@ -93,11 +93,11 @@ public override bool Equals(object? obj)

var other = (MatchTextExpression)obj;

return TargetAttribute.Equals(other.TargetAttribute) && TextValue.Equals(other.TextValue) && MatchKind == other.MatchKind;
return MatchTarget.Equals(other.MatchTarget) && TextValue.Equals(other.TextValue) && MatchKind == other.MatchKind;
}

public override int GetHashCode()
{
return HashCode.Combine(TargetAttribute, TextValue, MatchKind);
return HashCode.Combine(MatchTarget, TextValue, MatchKind);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,12 @@ public override QueryExpression VisitPagination(PaginationExpression expression,

public override QueryExpression? VisitMatchText(MatchTextExpression expression, TArgument argument)
{
var newTargetAttribute = Visit(expression.TargetAttribute, argument) as ResourceFieldChainExpression;
var newMatchTarget = Visit(expression.MatchTarget, argument) as ResourceFieldChainExpression;
var newTextValue = Visit(expression.TextValue, argument) as LiteralConstantExpression;

if (newTargetAttribute != null && newTextValue != null)
if (newMatchTarget != null && newTextValue != null)
{
var newExpression = new MatchTextExpression(newTargetAttribute, newTextValue, expression.MatchKind);
var newExpression = new MatchTextExpression(newMatchTarget, newTextValue, expression.MatchKind);
return newExpression.Equals(expression) ? expression : newExpression;
}

Expand All @@ -163,12 +163,12 @@ public override QueryExpression VisitPagination(PaginationExpression expression,

public override QueryExpression? VisitAny(AnyExpression expression, TArgument argument)
{
var newTargetAttribute = Visit(expression.TargetAttribute, argument) as ResourceFieldChainExpression;
var newMatchTarget = Visit(expression.MatchTarget, argument) as ResourceFieldChainExpression;
IImmutableSet<LiteralConstantExpression> newConstants = VisitSet(expression.Constants, argument);

if (newTargetAttribute != null)
if (newMatchTarget != null)
{
var newExpression = new AnyExpression(newTargetAttribute, newConstants);
var newExpression = new AnyExpression(newMatchTarget, newConstants);
return newExpression.Equals(expression) ? expression : newExpression;
}

Expand Down
69 changes: 52 additions & 17 deletions src/JsonApiDotNetCore/Queries/Parsing/FilterParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,37 @@ protected virtual MatchTextExpression ParseTextMatch(string operatorName)
EatText(operatorName);
EatSingleCharacterToken(TokenKind.OpenParen);

QueryExpression matchTarget = ParseTextMatchLeftTerm();

EatSingleCharacterToken(TokenKind.Comma);

ConstantValueConverter constantValueConverter = GetConstantValueConverterForType(typeof(string));
LiteralConstantExpression constant = ParseConstant(constantValueConverter);

EatSingleCharacterToken(TokenKind.CloseParen);

var matchKind = Enum.Parse<TextMatchKind>(operatorName.Pascalize());
return new MatchTextExpression(matchTarget, constant, matchKind);
}

private QueryExpression ParseTextMatchLeftTerm()
{
if (TokenStack.TryPeek(out Token? nextToken) && nextToken is { Kind: TokenKind.Text } && IsFunction(nextToken.Value!))
{
FunctionExpression targetFunction = ParseFunction();

if (targetFunction.ReturnType != typeof(string))
{
throw new QueryParseException("Function that returns type 'String' expected.", nextToken.Position);
}

return targetFunction;
}

int chainStartPosition = GetNextTokenPositionOrEnd();

ResourceFieldChainExpression targetAttributeChain =
ParseFieldChain(BuiltInPatterns.ToOneChainEndingInAttribute, FieldChainPatternMatchOptions.None, ResourceTypeInScope, null);
ResourceFieldChainExpression targetAttributeChain = ParseFieldChain(BuiltInPatterns.ToOneChainEndingInAttribute, FieldChainPatternMatchOptions.None,
ResourceTypeInScope, null);

var targetAttribute = (AttrAttribute)targetAttributeChain.Fields[^1];

Expand All @@ -333,32 +360,21 @@ protected virtual MatchTextExpression ParseTextMatch(string operatorName)
throw new QueryParseException("Attribute of type 'String' expected.", position);
}

EatSingleCharacterToken(TokenKind.Comma);

ConstantValueConverter constantValueConverter = GetConstantValueConverterForAttribute(targetAttribute);
LiteralConstantExpression constant = ParseConstant(constantValueConverter);

EatSingleCharacterToken(TokenKind.CloseParen);

var matchKind = Enum.Parse<TextMatchKind>(operatorName.Pascalize());
return new MatchTextExpression(targetAttributeChain, constant, matchKind);
return targetAttributeChain;
}

protected virtual AnyExpression ParseAny()
{
EatText(Keywords.Any);
EatSingleCharacterToken(TokenKind.OpenParen);

ResourceFieldChainExpression targetAttributeChain =
ParseFieldChain(BuiltInPatterns.ToOneChainEndingInAttribute, FieldChainPatternMatchOptions.None, ResourceTypeInScope, null);

var targetAttribute = (AttrAttribute)targetAttributeChain.Fields[^1];
(QueryExpression matchTarget, Func<ConstantValueConverter> constantValueConverterFactory) = ParseAnyLeftTerm();

EatSingleCharacterToken(TokenKind.Comma);

ImmutableHashSet<LiteralConstantExpression>.Builder constantsBuilder = ImmutableHashSet.CreateBuilder<LiteralConstantExpression>();

ConstantValueConverter constantValueConverter = GetConstantValueConverterForAttribute(targetAttribute);
ConstantValueConverter constantValueConverter = constantValueConverterFactory();
LiteralConstantExpression constant = ParseConstant(constantValueConverter);
constantsBuilder.Add(constant);

Expand All @@ -374,7 +390,26 @@ protected virtual AnyExpression ParseAny()

IImmutableSet<LiteralConstantExpression> constantSet = constantsBuilder.ToImmutable();

return new AnyExpression(targetAttributeChain, constantSet);
return new AnyExpression(matchTarget, constantSet);
}

private (QueryExpression matchTarget, Func<ConstantValueConverter> constantValueConverterFactory) ParseAnyLeftTerm()
{
if (TokenStack.TryPeek(out Token? nextToken) && nextToken is { Kind: TokenKind.Text } && IsFunction(nextToken.Value!))
{
FunctionExpression targetFunction = ParseFunction();

Func<ConstantValueConverter> functionConverterFactory = () => GetConstantValueConverterForType(targetFunction.ReturnType);
return (targetFunction, functionConverterFactory);
}

ResourceFieldChainExpression targetAttributeChain =
ParseFieldChain(BuiltInPatterns.ToOneChainEndingInAttribute, FieldChainPatternMatchOptions.None, ResourceTypeInScope, null);

var targetAttribute = (AttrAttribute)targetAttributeChain.Fields[^1];

Func<ConstantValueConverter> attributeConverterFactory = () => GetConstantValueConverterForAttribute(targetAttribute);
return (targetAttributeChain, attributeConverterFactory);
}

protected virtual HasExpression ParseHas()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public override Expression VisitIsType(IsTypeExpression expression, QueryClauseB

public override Expression VisitMatchText(MatchTextExpression expression, QueryClauseBuilderContext context)
{
Expression property = Visit(expression.TargetAttribute, context);
Expression property = Visit(expression.MatchTarget, context);

if (property.Type != typeof(string))
{
Expand All @@ -109,7 +109,7 @@ public override Expression VisitMatchText(MatchTextExpression expression, QueryC

public override Expression VisitAny(AnyExpression expression, QueryClauseBuilderContext context)
{
Expression property = Visit(expression.TargetAttribute, context);
Expression property = Visit(expression.MatchTarget, context);

var valueList = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(property.Type))!;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,24 +50,30 @@ public CarExpressionRewriter(IResourceGraph resourceGraph)

public override QueryExpression? VisitAny(AnyExpression expression, object? argument)
{
PropertyInfo property = expression.TargetAttribute.Fields[^1].Property;

if (IsCarId(property))
if (expression.MatchTarget is ResourceFieldChainExpression targetAttributeChain)
{
string[] carStringIds = expression.Constants.Select(constant => (string)constant.TypedValue).ToArray();
return RewriteFilterOnCarStringIds(expression.TargetAttribute, carStringIds);
PropertyInfo property = targetAttributeChain.Fields[^1].Property;

if (IsCarId(property))
{
string[] carStringIds = expression.Constants.Select(constant => (string)constant.TypedValue).ToArray();
return RewriteFilterOnCarStringIds(targetAttributeChain, carStringIds);
}
}

return base.VisitAny(expression, argument);
}

public override QueryExpression? VisitMatchText(MatchTextExpression expression, object? argument)
{
PropertyInfo property = expression.TargetAttribute.Fields[^1].Property;

if (IsCarId(property))
if (expression.MatchTarget is ResourceFieldChainExpression targetAttributeChain)
{
throw new NotSupportedException("Partial text matching on Car IDs is not possible.");
PropertyInfo property = targetAttributeChain.Fields[^1].Property;

if (IsCarId(property))
{
throw new NotSupportedException("Partial text matching on Car IDs is not possible.");
}
}

return base.VisitMatchText(expression, argument);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Reflection;

#pragma warning disable AV1008 // Class should not be static

namespace JsonApiDotNetCoreTests.IntegrationTests.QueryStrings.CustomFunctions.Decrypt;

internal static class DatabaseFunctionStub
{
public static readonly MethodInfo DecryptMethod = typeof(DatabaseFunctionStub).GetMethod(nameof(Decrypt), [typeof(string)])!;

public static string Decrypt(string text)
{
_ = text;
throw new InvalidOperationException($"The '{nameof(Decrypt)}' user-defined SQL function cannot be called client-side.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using TestBuildingBlocks;

// @formatter:wrap_chained_method_calls chop_always

namespace JsonApiDotNetCoreTests.IntegrationTests.QueryStrings.CustomFunctions.Decrypt;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
public sealed class DecryptDbContext(DbContextOptions<DecryptDbContext> options)
: TestableDbContext(options)
{
public DbSet<Blog> Blogs => Set<Blog>();

protected override void OnModelCreating(ModelBuilder builder)
{
QueryStringDbContext.ConfigureModel(builder);

builder.HasDbFunction(DatabaseFunctionStub.DecryptMethod)
.HasName("decrypt_column_value");

base.OnModelCreating(builder);
}

internal async Task DeclareDecryptFunctionAsync()
{
// Just for demo purposes, decryption is defined as: base64-decode the incoming value.
await Database.ExecuteSqlRawAsync("""
CREATE OR REPLACE FUNCTION decrypt_column_value(value text)
RETURNS text
RETURN encode(decode(value, 'base64'), 'escape');
""");
}
}
Loading