Repository navigation
2.1: node deparse, walker control, tree rewriting and comparison, fragment parsing - #24
Merged
Merged
Conversation
- Parser.DeparseNode and node.Deparse() turn a single node back into SQL: statements, expressions, FROM items, select list and ORDER BY items, WITH clauses and type names - Walk(visitor) lets the visitor skip a node's children or stop the walk; NodeVisit now reports the parent's property and list index the node came from - Parser.ParameterRefs finds $n parameter references with their offsets in the query string
…otype Closes the gaps against Ruby pg_query, pglast and @pgsql/traverse: - NodeVisit.Ancestors and FindAncestor<T>() give the path from a node up to the root of the walk - NodeVisit.Statement and StatementIndex give the top-level statement a node belongs to - ParameterRef.TypeName holds the type a parameter is cast to
…ng; bump to 2.1.0 - Rewrite keeps, replaces or removes nodes in place, children first - EqualsIgnoringLocations compares trees and nodes by structure - AsNode wraps a node for a property or list that takes any node - Parser.ParseExpression and Parser.ParseTypeName parse a single expression or type name on its own - PgIdentifier.Quote and NeedsQuoting follow PostgreSQL's quote_ident, using the scanner's keyword categories - Version 2.1.0, with a What's new in 2.1 section in the README
mysticmind
marked this pull request as ready for review
October 9, 2026 13:23
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 core additions for 2.1. All additive; nothing from 2.0 changes behaviour. These are also the pieces a schema diff library on top of this one needs.
1. Node-level deparse
Parser.DeparseNode(node), also available asnode.Deparse(), turns one node back into SQL.RangeVar,JoinExpr, subselects, functions),ResTarget,SortBy,WithClause,CommonTableExpr,TypeName, plusNodewrappers,RawStmtand a wholeParseResult.Alias,String,Integer,List). They return an error such asa Alias cannot be deparsed on its own.SELECT(in the WHERE, FROM, target list, ORDER BY or WITH position), deparsed, and the surroundingSELECTis cut off. The original tree is not modified.2. Walker control
Walk(Func<NodeVisit, WalkAction>): the visitor returnsContinue,SkipChildrenorStop. The existing enumerableWalk()could not skip a subtree.NodeVisit.FieldNameandNodeVisit.Index: which property of the parent a node came from (for examplenameof(SelectStmt.WhereClause)), and its position when that property is a list. Added as init-only properties, so the positionalNodeVisit(Node, Parent, Depth)shape is unchanged.NodeVisit.AncestorsandFindAncestor<T>(): the containing nodes, nearest first, and the nearest one of a given type. This answers questions such as "is this table reference inside a CTE?" or "inside a subquery?".NodeVisit.StatementandStatementIndex: the top-level statement a node belongs to, when the walk starts from aParseResult.NodeVisitequality is now defined explicitly: same node (by reference), same place.3. Parameter references
Parser.ParameterRefs(query)returns each$nwith its number and its start and end in the query string (UTF-16 offsets). It is built onScan, so references inside string literals and comments are left out. Async variant included.When a parameter is cast (
$1::int,CAST($2 AS numeric(10,2))),ParameterRef.TypeNameholds the type. That part needs the parse tree, so a query that scans but does not parse still returns its parameters, only without types.4. Changing and comparing trees
Rewrite(visitor): the visitor returnsNodeEdit.Keep,NodeEdit.ReplaceWith(node)orNodeEdit.Removefor each node. The tree is changed in place, children before parents, and visits carry the same context asWalk(ancestors, field, index, statement). A replacement that does not fit the property throws with a message naming the property and both types.EqualsIgnoringLocations: structural comparison of two trees or nodes that skips every location field. SQL that differs only in whitespace, comments, keyword case or redundant parentheses compares equal. It is iterative, so it is safe on deep trees.AsNode(): wraps a node for a property or list that takes any node.Together these make "normalize, then compare" possible, for example dropping the casts PostgreSQL adds when it stores an index expression and then comparing with what was written.
5. Parsing fragments and quoting names
Parser.ParseExpression: parses exactly one expression, such as a column default, a CHECK condition orpg_get_exproutput. Lists, aliases, extra clauses and extra statements are rejected.Parser.ParseTypeName: parses one type name, such asnumeric(10,2)ortext[].PgIdentifier.Quote/NeedsQuoting: PostgreSQL'squote_identrules, using the scanner's keyword categories, so no keyword list is maintained here.Compared with other bindings
I read the sources of Ruby
pg_query,pglastand@pgsql/traversefor these three features.deparse_stmtanddeparse_expr(the same wrap-in-a-SELECT approach), covering statements and expressions only. This covers more node kinds.Rewrite. Still missing compared with them: handlers per node type, and inserting sibling nodes.Verification
NodeToolsTestsandTreeEditingTests; 245 pass on .NET 8, 9 and 10 locally.Limits
Rewriteis recursive. On a very deep tree it throwsInsufficientExecutionStackExceptioninstead of overflowing.WalkandEqualsIgnoringLocationsare iterative and have no such limit.DeparseOptions(no pretty printing), because cutting off the wrapper relies on the compact output.ParseExpressionandParseTypeNamelocations are relative to an internal wrapper, not to the text passed in.ParameterRefstypes need a parse; a query that scans but does not parse returns its parameters without types.Version
Bumped to
2.1.0.