From 156b3bcb28d433164cd792904e1668e42950982c Mon Sep 17 00:00:00 2001 From: ArthurFreeb <32460970+ArthurFreeb@users.noreply.github.com> Date: Sat, 27 Sep 2025 22:57:16 +0100 Subject: [PATCH 1/2] wip --- .../src/Evaluator/FilterEvaluator.cs | 144 ++++++++++++++---- 1 file changed, 114 insertions(+), 30 deletions(-) diff --git a/src/GoatQuery/src/Evaluator/FilterEvaluator.cs b/src/GoatQuery/src/Evaluator/FilterEvaluator.cs index ee38258..1cbd5b6 100644 --- a/src/GoatQuery/src/Evaluator/FilterEvaluator.cs +++ b/src/GoatQuery/src/Evaluator/FilterEvaluator.cs @@ -52,21 +52,27 @@ private static Result EvaluatePropertyPathExpression( (Expression)context.CurrentLambda.Parameter : context.RootParameter; - var propertyPathResult = BuildPropertyPath(propertyPath, baseExpression, context.PropertyMappingTree); - if (propertyPathResult.IsFailed) return Result.Fail(propertyPathResult.Errors); + var safePathResult = BuildPropertyPathWithGuard(propertyPath.Segments, baseExpression, context.PropertyMappingTree); + if (safePathResult.IsFailed) return Result.Fail(safePathResult.Errors); - var finalProperty = propertyPathResult.Value; + var (finalProperty, guard, container) = safePathResult.Value; if (exp.Right is NullLiteral) { - var nullComparison = CreateNullComparison(exp, finalProperty); - return nullComparison; + return ComposeNestedNullComparison(finalProperty, guard, exp.Operator); } var comparisonResult = EvaluateValueComparison(exp, finalProperty); if (comparisonResult.IsFailed) return comparisonResult; - return comparisonResult.Value; + var comparison = comparisonResult.Value; + + var requireFinalNotNull = RequiresFinalNotNull(exp.Operator, finalProperty, exp.Right); + var combinedGuard = requireFinalNotNull + ? Expression.AndAlso(guard, Expression.NotEqual(finalProperty, Expression.Constant(null, finalProperty.Type))) + : guard; + + return Expression.AndAlso(combinedGuard, comparison); } private static Result BuildPropertyPath( @@ -84,7 +90,6 @@ private static Result BuildPropertyPath( current = Expression.Property(current, propertyNode.ActualPropertyName); - // Navigate to nested mapping for next segment if (!isLast) { if (!propertyNode.HasNestedMapping) @@ -97,6 +102,55 @@ private static Result BuildPropertyPath( return Result.Ok((MemberExpression)current); } + private static Result<(MemberExpression Final, Expression Guard, Expression Container)> BuildPropertyPathWithGuard( + IList segments, + Expression startExpression, + PropertyMappingTree propertyMappingTree) + { + if (segments == null || segments.Count == 0) + return Result.Fail("Property path segments cannot be empty"); + + var current = startExpression; + var currentMappingTree = propertyMappingTree; + + Expression guard = Expression.Constant(true); + Expression container = startExpression; + + for (int i = 0; i < segments.Count; i++) + { + var segment = segments[i]; + var isLast = i == segments.Count - 1; + + if (!currentMappingTree.TryGetProperty(segment, out var propertyNode)) + return Result.Fail($"Invalid property '{segment}' in path"); + + var next = Expression.Property(current, propertyNode.ActualPropertyName); + + if (!isLast) + { + if (!propertyNode.HasNestedMapping) + return Result.Fail($"Property '{segment}' does not support nested navigation"); + + if (!next.Type.IsValueType || Nullable.GetUnderlyingType(next.Type) != null) + { + var notNull = Expression.NotEqual(next, Expression.Constant(null, next.Type)); + guard = Expression.AndAlso(guard, notNull); + } + + current = next; + container = current; + currentMappingTree = propertyNode.NestedMapping; + } + else + { + var final = Expression.Property(current, propertyNode.ActualPropertyName); + return Result.Ok(((MemberExpression)final, guard, container)); + } + } + + return Result.Fail("Invalid property path"); + } + private static Result ResolvePropertyPathForCollection( PropertyPath propertyPath, Expression baseExpression, @@ -113,8 +167,7 @@ private static Result ResolvePropertyPathForCollection( return Result.Fail($"Invalid property '{segment}' in lambda expression property path"); current = Expression.Property(current, propertyNode.ActualPropertyName); - - // Navigate to nested mapping for next segment + if (!isLast) if (i < propertyPath.Segments.Count - 1) { if (!propertyNode.HasNestedMapping) @@ -326,7 +379,6 @@ private static Result EvaluateLambdaExpression(QueryLambdaExpression var (collectionProperty, elementType, lambdaParameter) = setupResult.Value; - // Enter lambda scope context.EnterLambdaScope(lambdaExp.Parameter, lambdaParameter, elementType); try @@ -382,7 +434,7 @@ private static Result ResolveCollectionProperty(QueryExpressio { return Result.Fail($"Invalid property '{identifier.TokenLiteral()}' in lambda expression"); } - return Expression.Property(baseExpression, propertyNode.ActualPropertyName); + return Expression.Property(baseExpression, propertyNode.ActualPropertyName) as MemberExpression; case PropertyPath propertyPath: return ResolvePropertyPathForCollection(propertyPath, baseExpression, propertyMappingTree); @@ -392,6 +444,42 @@ private static Result ResolveCollectionProperty(QueryExpressio } } + private static bool RequiresFinalNotNull(string operatorKeyword, MemberExpression finalProperty, QueryExpression right) + { + if (operatorKeyword.Equals(Keywords.Contains, StringComparison.OrdinalIgnoreCase)) + return true; + + if (right is NullLiteral) + return false; + + var type = finalProperty.Type; + if (!type.IsValueType) + return true; + + return Nullable.GetUnderlyingType(type) != null; + } + + private static Expression ComposeNestedNullComparison(MemberExpression finalProperty, Expression guard, string operatorKeyword) + { + var isEq = operatorKeyword.Equals(Keywords.Eq, StringComparison.OrdinalIgnoreCase); + var isNe = operatorKeyword.Equals(Keywords.Ne, StringComparison.OrdinalIgnoreCase); + var nullConst = Expression.Constant(null, finalProperty.Type); + var finalEqNull = Expression.Equal(finalProperty, nullConst); + var finalNeNull = Expression.NotEqual(finalProperty, nullConst); + var notGuard = Expression.Not(guard); + + if (isEq) + { + return Expression.OrElse(notGuard, finalEqNull); + } + else if (isNe) + { + return Expression.AndAlso(guard, finalNeNull); + } + + return Expression.AndAlso(guard, finalEqNull); + } + private static Result EvaluateLambdaBody(QueryExpression expression, FilterEvaluationContext context) { return expression switch @@ -431,7 +519,6 @@ private static Result EvaluateLambdaBodyIdentifier(InfixExpression e if (identifierName.Equals(context.CurrentLambda.ParameterName, StringComparison.OrdinalIgnoreCase)) { - // For primitive types (string, int, etc.), allow direct comparisons with the lambda parameter if (IsPrimitiveType(context.CurrentLambda.ElementType)) { return EvaluateValueComparison(exp, context.CurrentLambda.Parameter); @@ -467,28 +554,29 @@ private static Result EvaluateLambdaBodyLogicalOperator(InfixExpress private static Result EvaluateLambdaPropertyPath(InfixExpression exp, PropertyPath propertyPath, ParameterExpression lambdaParameter) { - // Skip the first segment (lambda parameter name) and build property path from lambda parameter - var current = (Expression)lambdaParameter; var elementType = lambdaParameter.Type; + var mapping = PropertyMappingTreeBuilder.BuildMappingTree(elementType, GetDefaultMaxDepth()); - // Build property path from lambda parameter - var pathResult = BuildLambdaPropertyPath(current, propertyPath.Segments.Skip(1).ToList(), elementType); - if (pathResult.IsFailed) return pathResult; - - current = pathResult.Value; + var safePathResult = BuildPropertyPathWithGuard(propertyPath.Segments.Skip(1).ToList(), lambdaParameter, mapping); + if (safePathResult.IsFailed) return Result.Fail(safePathResult.Errors); - var finalProperty = (MemberExpression)current; + var (finalProperty, guard, container) = safePathResult.Value; - // Handle null comparisons if (exp.Right is NullLiteral) { - return exp.Operator == Keywords.Eq - ? Expression.Equal(finalProperty, Expression.Constant(null, finalProperty.Type)) - : Expression.NotEqual(finalProperty, Expression.Constant(null, finalProperty.Type)); + return ComposeNestedNullComparison(finalProperty, guard, exp.Operator); } - // Handle value comparisons - return EvaluateValueComparison(exp, finalProperty); + var comparisonResult = EvaluateValueComparison(exp, finalProperty); + if (comparisonResult.IsFailed) return comparisonResult; + + var comparison = comparisonResult.Value; + var requireFinalNotNull = RequiresFinalNotNull(exp.Operator, finalProperty, exp.Right); + var combinedGuard = requireFinalNotNull + ? Expression.AndAlso(guard, Expression.NotEqual(finalProperty, Expression.Constant(null, finalProperty.Type))) + : guard; + + return Expression.AndAlso(combinedGuard, comparison); } private static Expression CreateAnyExpression(MemberExpression collection, LambdaExpression lambda, Type elementType) @@ -522,7 +610,6 @@ private static Result BuildLambdaPropertyPath(Expression startExpres current = Expression.Property(current, propertyNode.ActualPropertyName); - // Update mapping tree for nested navigation if (propertyNode.HasNestedMapping) { currentMappingTree = propertyNode.NestedMapping; @@ -539,7 +626,6 @@ private static int GetDefaultMaxDepth() private static Type GetCollectionElementType(Type collectionType) { - // Handle IEnumerable if (collectionType.IsGenericType) { var genericArgs = collectionType.GetGenericArguments(); @@ -550,7 +636,6 @@ private static Type GetCollectionElementType(Type collectionType) } } - // Handle arrays if (collectionType.IsArray) { return collectionType.GetElementType(); @@ -563,7 +648,6 @@ private static Result GetIntegerExpressionConstant(int value { try { - // Fetch the underlying type if it's nullable. var underlyingType = Nullable.GetUnderlyingType(targetType); var type = underlyingType ?? targetType; From a62033f1c85592f778bee7f0809c19bac862ae77 Mon Sep 17 00:00:00 2001 From: ArthurFreeb <32460970+ArthurFreeb@users.noreply.github.com> Date: Sun, 28 Sep 2025 10:51:47 +0100 Subject: [PATCH 2/2] wip --- src/GoatQuery/src/Evaluator/FilterEvaluator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/GoatQuery/src/Evaluator/FilterEvaluator.cs b/src/GoatQuery/src/Evaluator/FilterEvaluator.cs index 1cbd5b6..d642bc8 100644 --- a/src/GoatQuery/src/Evaluator/FilterEvaluator.cs +++ b/src/GoatQuery/src/Evaluator/FilterEvaluator.cs @@ -167,7 +167,6 @@ private static Result ResolvePropertyPathForCollection( return Result.Fail($"Invalid property '{segment}' in lambda expression property path"); current = Expression.Property(current, propertyNode.ActualPropertyName); - if (!isLast) if (i < propertyPath.Segments.Count - 1) { if (!propertyNode.HasNestedMapping)