Rewrite the expression parser - #92
Merged
Merged
Conversation
The parser built a MemberNode tree only to convert it into the QueryNode tree, bound lambda parameters by walking up parents and falling back to the root when no match was found, and looked at only Arguments[0] of a LINQ operator. That left three failures: - e.Nodes.First(), .Count() and .ToList() threw ArgumentOutOfRangeException, because Arguments[1] was read whenever Arguments[0] was an attributed member, whether a lambda was passed or not. - A chained operator produced an invalid query. In e.Nodes.Where(n => n.Customer.CustomerName != null).Select(n => n.OrderId) the outer Select fell through to the base visitor, so its parameter was unknown and orderId was attached to the root: "The field `orderId` does not exist on the type `OrdersConnection`". - SelectMany's result selector bound both of its parameters to the outer element: "The field `orderId` does not exist on the type `Customer`". QueryExpressionVisitor now builds QueryNode directly, so merging happens once in AddChildNode. ResolvePath handles the expressions that name a field and returns the node they select; anything else falls through to the inherited ExpressionVisitor, so fields mentioned inside initialisers, comparisons and ordinary method calls are still fetched. Lambda parameters are bound in an explicit reference keyed scope map, and a parameter that was never bound is reported instead of silently selecting from the root. LinqOperator classifies an operator as Projection, where the selection moves to the lambda result, or PassThrough, where it stays on the source and the lambda is a client side predicate or key selector whose members still have to be fetched. Where, OrderBy, Take, First, Count, Cast and the rest are supported and can be chained; the operators that combine several sequences throw a NotSupportedException naming the operator rather than quietly dropping fields. Convert, TypeAs, Quote and Unbox are unwrapped, so a cast or a Nullable lift no longer truncates a path. ExpressionEvaluator reads constants, captured locals and transparent conversions directly and compiles only as a fallback. Arguments were previously evaluated twice per call. GetArgumentsId has to stay a pure function of the argument values, because GetMethodValue recomputes it to find the field in the response. It now renders values structurally and hashes with FNV-1a/64 instead of object.GetHashCode(), which was reference based: aliases were not the same in two processes, so the query text could not be cached, and a collision silently merged two distinct argument sets into one field. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The parser built a MemberNode tree only to convert it into the QueryNode tree, bound lambda parameters by walking up parents and falling back to the root when no match was found, and looked at only Arguments[0] of a LINQ operator. That left three failures:
orderIddoes not exist on the typeOrdersConnection".orderIddoes not exist on the typeCustomer".QueryExpressionVisitor now builds QueryNode directly, so merging happens once in AddChildNode. ResolvePath handles the expressions that name a field and returns the node they select; anything else falls through to the inherited ExpressionVisitor, so fields mentioned inside initialisers, comparisons and ordinary method calls are still fetched. Lambda parameters are bound in an explicit reference keyed scope map, and a parameter that was never bound is reported instead of silently selecting from the root.
LinqOperator classifies an operator as Projection, where the selection moves to the lambda result, or PassThrough, where it stays on the source and the lambda is a client side predicate or key selector whose members still have to be fetched. Where, OrderBy, Take, First, Count, Cast and the rest are supported and can be chained; the operators that combine several sequences throw a NotSupportedException naming the operator rather than quietly dropping fields. Convert, TypeAs, Quote and Unbox are unwrapped, so a cast or a Nullable lift no longer truncates a path.
ExpressionEvaluator reads constants, captured locals and transparent conversions directly and compiles only as a fallback. Arguments were previously evaluated twice per call.
GetArgumentsId has to stay a pure function of the argument values, because GetMethodValue recomputes it to find the field in the response. It now renders values structurally and hashes with FNV-1a/64 instead of object.GetHashCode(), which was reference based: aliases were not the same in two processes, so the query text could not be cached, and a collision silently merged two distinct argument sets into one field.