Skip to content

2.1: classification, references, node builders, rewrites, typed visitors, Format - #25

Merged
mysticmind merged 2 commits into
mainfrom
capabilities-2.2
Oct 9, 2026
Merged

mysticmind merged 2 commits into
mainfrom
capabilities-2.2

Conversation

@mysticmind

@mysticmind mysticmind commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

More general-purpose capabilities for others to build on. All additive. Everything stays under 2.1.0, since 2.1.0 was merged but never published; the README has a single "What's new in 2.1" section.

Understanding a statement

Statement classification. Parser.Classify(query), also tree.Classify() and rawStmt.Classify(), returns a StatementInfo per statement.

  • Kind: Select, Insert, Update, Delete, Merge, Truncate, Copy, Ddl, Transaction, Session, Cursor, Prepared, Explain, Call, Maintenance, Lock, Other. All 117 statement node types are mapped.
  • Facts: IsUtility, HasDataModifyingCte, HasSelectInto, HasLockingClause, ExecutesInner (EXPLAIN ANALYZE), RunsProgram (COPY PROGRAM), and Inner, the statement wrapped by EXPLAIN, PREPARE, DECLARE CURSOR, CREATE TABLE AS or COPY (query).
  • IsReadOnly, derived from those facts and erring on the side of false.

These are facts, not a verdict: a function called from a SELECT can still write, which no parser can see.

Resolved references. GetReferences(), from any node, returns tables (with role Read, Write or Ddl, alias, CTE detection), functions, and columns matched to their table when the query alone settles it. Unsettled columns are left unmatched, not guessed.

Changing a statement

Node builders. Ast.Column, Star, Const, Null, Param, Op, Eq and friends, And, Or, Not, IsNull, In, Call, CountStar, Cast, Table, Target, OrderBy, Select, and Ast.Expression(sql). Every builder is tested to produce a tree structurally equal to what the parser produces for the same SQL (41 cases).

Common rewrites (QueryRewrites, on the tree, in place):

  • AddWhere: for SELECT, UPDATE, DELETE; combines with an existing WHERE using AND. Throws NotSupportedException with a clear message for a UNION, a VALUES list or other statements.
  • SetLimit, CapLimit, SetOffset. CapLimit adds a limit, lowers a larger constant one, and wraps any other expression in LEAST(..., max).
  • ToCount: a new SELECT count(*) FROM (...) q, leaving the original untouched.
  • QualifyTables, RenameSchema, RenameTable: CTE names are left alone.
  • ToDropStatement: the DROP that undoes a CREATE, for 16 object kinds, with ifExists and cascade. EnsureIfNotExists, EnsureOrReplace.

Typed visitors. NodeVisitor holds a handler per node type; tree.Walk(visitorA, visitorB, ...) runs several in one pass, and each skips a subtree or stops for itself only.

Working with text

  • Parser.Format: pretty prints a query or script and keeps its comments, one statement per paragraph. FormatOptions for indent, line length, comma placement, trailing newline and whether to keep comments.
  • Error.Format and Error.GetLineAndColumn: psql-style error rendering with a caret.

A fix to 2.1's comparison

EqualsIgnoringLocations did not ignore the start and end positions of parenthesized lists, so a IN (1, 2) and a IN ( 1,2 ) compared unequal. Found while testing the builders against the parser; fixed, with tests.

Verification

  • 142 new test cases across AnalysisTests, AstTests and RewriteAndVisitorTests; 387 pass on .NET 8, 9 and 10 locally.
  • IsUtility is checked against libpg_query's own answer for every classified statement in the tests; every AddWhere and CapLimit result is checked to be a tree the parser itself builds.
  • The package smoke test covers each addition, and a local Native AOT publish ran them with no trim or AOT warnings.
  • README sections added with example output that was run.

Limits

  • Column matching is syntactic, and CTE visibility is slightly generous (a name counts as a CTE if any WITH clause of an enclosing statement defines it).
  • RenameSchema changes table references only, not schema-qualified functions or types.
  • ToDropStatement needs a named index, and for functions uses the argument types as written.
  • Formatting a script differs from plain Deparse: Format puts each statement in its own paragraph. Deparse is unchanged.

…endering; bump to 2.2.0

- Parser.Classify and StatementInfo: kind, read-only, and facts such
  as a data-modifying CTE, SELECT INTO, a locking clause, EXPLAIN
  ANALYZE, COPY PROGRAM and the wrapped statement
- GetReferences: tables with role and CTE detection, functions, and
  columns matched to their table where the query alone settles it
- Parser.Format: pretty print a query or script, keeping comments,
  one statement per paragraph
- Error.Format and Error.GetLineAndColumn: psql-style error rendering
…g under 2.1.0

- Ast builds nodes that match what the parser produces: columns,
  constants, operators, AND/OR/NOT, IN, calls, casts, tables, select
  list and ORDER BY items, and Ast.Expression from SQL text
- QueryRewrites: AddWhere, SetLimit, CapLimit, SetOffset, ToCount,
  QualifyTables, RenameSchema, RenameTable, ToDropStatement,
  EnsureIfNotExists, EnsureOrReplace
- NodeVisitor: a handler per node type, several visitors in one walk,
  each skipping or stopping on its own
- EqualsIgnoringLocations now also ignores the start and end positions
  of parenthesized lists, which made IN lists compare unequal
- Version back to 2.1.0, since 2.1.0 was never published; one
  What's new in 2.1 section in the README
@mysticmind mysticmind changed the title 2.2: statement classification, resolved references, Format, error rendering 2.1: classification, references, node builders, rewrites, typed visitors, Format Oct 9, 2026
@mysticmind
mysticmind merged commit 13d82bd into main Oct 9, 2026
25 checks passed
@mysticmind
mysticmind deleted the capabilities-2.2 branch October 9, 2026 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant