Parser and nodes
Skill matejformanek/postgres-claude/.claude/skills/parser-and-nodes
Hack the PostgreSQL parser or add a new Node type — covers scan.l flex tokenizer, gram.y bison grammar, parse_analyze in analyze.c, kwlist.h keywords, and adding/modifying Node types in parsenodes.h / primnodes.h / plannodes.h that flow through copy/equal/out/read funcs auto-generated by gen_node_support.pl. Spans the AST→Query→Plan three-tree pipeline, mutator/walker conventions (expression_tree_walker, query_tree_mutator), the node-type reference table, List/lappend/foreach idioms, and shift-reduce conflict resolution. Use whenever a PG patch edits src/backend/parser/**, edits src/include/nodes/*.h, adds a new SQL keyword to kwlist.h, extends a parse-tree node, writes a tree mutator/walker, or resolves a gram.y shift-reduce conflict. Skip for non-PG parsers (ANTLR, Babel, nom, tree-sitter, LALRPOP, Yacc/Bison on other projects, LLVM IR / Clang AST), JSON / XML / YAML parsing, and executor-node additions (use executor-and-planner instead).From its SKILL.md
npx -y skills add matejformanek/postgres-claude --skill parser-and-nodesAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
16.9 KB, ~4.2k tokens by cl100k_base, as published. Nobody here has run it
Parser & Node-types — operational
Companion docs: knowledge/idioms/parser-pipeline.md, knowledge/idioms/node-types-and-lists.md.
0. The three-tree pipeline — AST → Query → Plan
Every SQL statement flows through three distinct tree shapes; nodes in each header live or die at a specific stage. Confuse the stages and your patch will silently fail in a downstream walker.
SQL text
│
│ gram.y / scan.l / kwlist.h
▼
1. Raw parsetree (AST) — parsenodes.h utility-stmt nodes
Examples: SelectStmt, + A_Expr / A_Const / ColumnRef /
InsertStmt, A_Expr, FuncCall (raw expression shapes)
ColumnRef, FuncCall → walker: raw_expression_tree_walker
No types resolved, no relations
looked up, no operators resolved.
│
│ parser/analyze.c → transformStmt → transform<Foo>Stmt
│ parser/parse_*.c → adds rangetable, resolves names,
│ picks operator + cast OIDs,
│ applies coercions
▼
2. Query tree — parsenodes.h Query + analyzed
Examples: Query, RangeTblEntry, expression nodes (primnodes.h)
Var, OpExpr, Const, FuncExpr, Examples: Var, OpExpr, Const,
TargetEntry, FromExpr, JoinExpr FuncExpr, Aggref, WindowFunc
→ walker: expression_tree_walker
+ query_tree_walker
+ QTW_* flags for rtable/CTE descent
│
│ rewrite/ → rules, views (Query → Query)
│ optimizer/planner.c → planner: Query → Plan
▼
3. Plan tree — plannodes.h
Examples: SeqScan, Examples: SeqScan, IndexScan, HashJoin,
IndexScan, HashJoin, Sort, Agg, Limit, Gather, GatherMerge
Sort, Agg, Limit → walker: plan_tree_walker
+ planstate_tree_walker (executor-time)
Shipped to parallel workers via
nodeToString / stringToNode.
Operational rules per shape:
| Shape | Key invariants |
|---|---|
| Raw parsetree | No catalog lookups (parser runs without an open transaction in some paths); A_* and ColumnRef are the pre-resolution placeholders. Don't try to compute types here. |
| Query tree | Types, collations, operator OIDs, cast OIDs resolved. Var carries varno+varattno into the rangetable. Subqueries are nested Querys in RangeTblEntry. Stored in pg_rewrite.ev_action for views/rules — bump catversion when changing serialized fields. |
| Plan tree | Materialized from Paths by createplan.c. Plan's expression trees use Vars indexed into targetlist slots, NOT rangetable slots. Shipped to workers via the out/read funcs — bumps to plan-node fields don't need catversion (not stored on disk) but DO need correct out/read coverage. |
The executor-and-planner skill covers the Plan-tree side (createplan,
add_path costing, ExecInit/ProcNode dispatch). This skill covers raw
parsetree and Query tree work, plus the node-machinery infrastructure
(gen_node_support.pl, copy/equal/out/read funcs).
Tools for working with an existing tree
copyObject(p)— deep copy (preserves argument type viatypeof_unqualmacro,nodes.h:228-233). DispatchercopyObjectImplincopyfuncs.c:177-212, withcheck_stack_depth()guard (:185). By-ref Datums go through_copyConst→datumCopy;T_Listis deep-copied vialist_copy_deep,T_IntList/T_OidList/T_XidListshallow vialist_copy. Extensible nodes dispatch to a registerednodeCopycallback (_copyExtensibleNode). Allocated inCurrentMemoryContext— caller controls the lifetime.equal(a, b)— deep structural compare (equalfuncs.c).nodeToString(p)/stringToNode(s)— Lisp-ish text serialization; load-bearing for plan cache, rule storage (pg_rewrite.ev_action), parallel-worker plan shipping. The round-tripcopyObject(stringToNode(nodeToString(p)))underpins thedebug_write_read_parse_plan_treesGUC.expression_tree_walker/_mutator— polymorphic recursion over expression trees (post-analysis:Var,OpExpr, …).raw_expression_tree_walkerfor the pre-analysis shape (A_Expr,ColumnRef).query_tree_walker/_mutatorwrap for Query, withQTW_*flags (nodeFuncs.h:22-34) to control rtable/CTE descent.planstate_tree_walkeris the executor-time analogue. Mutator contract: returnNULLto delete a subtree; walker contract: callback returnstrueto short-circuit,falseto keep descending. The macro wrappers (nodeFuncs.h:155-183) let callbacks be typed without-Wincompatible-pointer-typeswarnings.
When you're adding a new SQL statement
Touch four layers, in order. Build between each so you fail fast.
-
Grammar —
src/backend/parser/gram.y- Add the keyword(s) to
unreserved_keyword/col_name_keyword/ etc. AND tosrc/include/parser/kwlist.h(alphabetical, with category). - Add a top-level rule producing your new
*Stmtnode, wire it intostmt:(search forstmt:ingram.y). UsemakeNode(FooStmt)in the action, set the location with@1, fill fields from$N. - Match an existing simple statement as template (e.g.
DropStmt,VariableSetStmt). Do NOT invent fresh helpers in gram.y ifparser/parse_utilcmd.calready has one.
- Add the keyword(s) to
-
Lexer —
src/backend/parser/scan.l(only if you introduced a new token shape; new keywords alone do not need scan.l changes — they go throughkwlist.h). -
Parse node —
src/include/nodes/parsenodes.h(utility stmts) orprimnodes.h(expressions) orplannodes.h(plan nodes)- First field
NodeTag type;(or another node struct, for "inheritance"). - Last field for top-level statements is typically
ParseLoc location;. - Annotate fields if needed:
pg_node_attr(...)on the struct, per-field attrs likequery_jumble_ignore,equal_ignore. Seenodes.h:43-125for the full list.
- First field
-
Analyze / execute
- Optimizable statement → add a
transformFooStmt(ParseState *, FooStmt *)inparser/analyze.cand wire it into theswitch (nodeTag(parseTree))insidetransformStmt(analyze.c:334-451, switch at :368). Caution (analyze.c:363-367): any change to that switch must also be reflected instmt_requires_parse_analysis()(:469-505) andanalyze_requires_snapshot()(:513-529) — three sites, one logical change. - Utility statement → no transform needed; just dump into a Query with
CMD_UTILITY(the default path). Execution lives insrc/backend/commands/viaProcessUtility/standard_ProcessUtility(src/backend/tcop/utility.c).
- Optimizable statement → add a
-
Run
gen_node_support.pl(done automatically by the build) and verify the generatedcopyfuncs.funcs.c,equalfuncs.funcs.c,outfuncs.funcs.c,readfuncs.funcs.cchunks for your new struct look sane. -
Add a regression test under
src/test/regress/sql/(and a matching expected file). See.claude/skills/testing/SKILL.md.
When you're adding a new Node type (no SQL change)
From src/backend/nodes/README "Steps to Add a Node":
- Put the struct in the right header. The file must be in
gen_node_support.pl's@all_input_fileslist (parsenodes.h, primnodes.h, plannodes.h, pathnodes.h, execnodes.h, value.h, etc. — seegen_node_support.pl:53-77). TheT_Footag is generated automatically intonodes/nodetags.h(do NOT hand-edit it). - First field is
NodeTag type;— unless you "inherit" by embedding another node struct as the first field (e.g.Plan plan;in a new plan node). - Build. Inspect the generated
copyfuncs.funcs.c/equalfuncs.funcs.c/outfuncs.funcs.c/readfuncs.funcs.c/queryjumblefuncs.funcs.centries for your node. Addpg_node_attr(...)and per-field attrs to fix anything wrong. - If a field needs special treatment, common annotations:
pg_node_attr(custom_copy_equal)— write your own bodies incopyfuncs.c/equalfuncs.cand the generator will skip yours.pg_node_attr(no_copy_equal, no_read, no_query_jumble)— opt out entirely (executor state nodes typically usenodetag_only).- Per-field:
array_size(otherfield),copy_as(VALUE),equal_ignore,read_write_ignore,query_jumble_ignore,query_jumble_location.
- If your node is an expression (lives in primnodes.h or has child Expr fields), add cases to every giant switch in
nodeFuncs.c:exprType(:42+),exprTypmod(:304+),exprCollation/exprSetCollation/exprInputCollation(:826+,:1140+,:1092+),exprLocation(:1403+) — type/typmod/collation/location introspection.expression_tree_walker_impl(:2111+),expression_tree_mutator_impl(:3018+) — recursion into children.raw_expression_tree_walker_impl(:4115+) — only if the node can appear in raw parsetrees.set_opfuncidfamily (:1890+) — only if your node holds an operator OID needing resolution. GrepT_<SiblingNode>(e.g.T_OpExpr) to find every site; five-to-eight maintenance points per node, miss one and walkers silently misbehave.
- Adding a node type renumbers existing tags. Recompile the whole tree (
--enable-dependhelps). No initdb needed — node numbers never go to disk. BUT if the node can appear in a stored parse tree (rule actions, view definitions), bumpCATALOG_VERSION_NOinsrc/include/catalog/catversion.h. - Test with
debug_copy_parse_plan_trees=on,debug_write_read_parse_plan_trees=on,debug_raw_expression_coverage_test=on(set viaPG_TEST_INITDB_EXTRA_OPTS).
When you're modifying an existing Node type
- Adding a field to a node that appears in stored catalog parse trees (e.g.
Query, anything in views / rules) → bump catversion. Add apg_node_attrif the new field needs special read/write handling (e.g.read_as(...)for backward-compat when reading old serialized trees from extensions — usually not needed because catversion changes invalidate stored trees). - Removing or reordering fields → also catversion bump if stored.
- Changing only Plan/Path internal fields (never serialized to catalogs) → no catversion bump needed. But Plan nodes ARE serialized to parallel workers, so you still need correct out/read funcs.
Pre-submit smoke
cd dev/build-debug && ninja && \
meson test --suite regress -q
Plus the debug-tree GUCs above on a separate initdb if you touched anything tree-shape related.
Node-type reference table
The major node families and where each lives. When in doubt about which header your new field belongs in, check this table.
| Header | Shape | Examples | When you'd add here |
|---|---|---|---|
nodes/parsenodes.h | Raw + Query | SelectStmt, InsertStmt, A_Expr, ColumnRef, Query, RangeTblEntry, TargetEntry, FromExpr, JoinExpr | New SQL statement; new fields on Query (catversion bump if serialized in pg_rewrite) |
nodes/primnodes.h | Expression (analyzed) | Var, Const, OpExpr, FuncExpr, Aggref, WindowFunc, SubLink, CaseExpr, CoalesceExpr | New expression node (must touch every nodeFuncs.c switch — see §"new Node type") |
nodes/plannodes.h | Plan | Plan, SeqScan, IndexScan, HashJoin, Sort, Agg, Limit, Gather, Append, MergeAppend | New plan node (also wire executor under nodeXxx.c — see executor-and-planner) |
nodes/pathnodes.h | Path + RelOptInfo | Path, IndexPath, JoinPath, RelOptInfo, PlannerInfo | New path shape considered by the optimizer |
nodes/execnodes.h | PlanState (runtime) | PlanState, SeqScanState, EState, ExprContext, ProjectionInfo | New runtime state for an executor node |
nodes/value.h | Leaf values | Integer, Float, Boolean, String, BitString | Almost never — the four primitive value carriers |
nodes/pg_list.h | Lists | List, IntList, OidList, XidList | Almost never (collection infrastructure) |
nodes/extensible.h | Extension nodes | ExtensibleNode, CustomScan | Custom AM / custom planner extension |
nodes/nodetags.h is auto-generated by gen_node_support.pl; never
hand-edit it. A new struct in any of the headers above (with the
file listed in gen_node_support.pl @all_input_files) gets its T_Foo
tag automatically.
Walker / mutator quick reference
| Function | Where | Use for |
|---|---|---|
raw_expression_tree_walker | nodes/nodeFuncs.c | Pre-analysis (A_Expr, ColumnRef, FuncCall); used by parser/parse_*.c |
expression_tree_walker | nodes/nodeFuncs.c | Analyzed expressions in a Query tree |
expression_tree_mutator | nodes/nodeFuncs.c | Same shape, but returns new nodes; rewriter / planner use this |
query_tree_walker | nodes/nodeFuncs.c | Wraps expression_tree_walker to also walk Query subexpressions; controlled by QTW_* flags |
query_tree_mutator | nodes/nodeFuncs.c | Mutator counterpart |
range_table_walker / _mutator | nodes/nodeFuncs.c | Just the rangetable, with per-RTE field control flags |
plan_tree_walker | nodes/nodeFuncs.c | Plan-tree (plannodes.h) recursion |
planstate_tree_walker | executor/execProcnode.c | Executor-time (PlanState) recursion; used by EXPLAIN |
Conventions to remember:
- Walker callback returns
bool. Returntrueto short-circuit the whole walk; returnfalseto continue descending. - Mutator callback returns
Node *. ReturnNULLto delete a subtree; return the input pointer to keep it unchanged; return a new node to replace. QTW_*flags (nodeFuncs.h:22-34) control rtable / CTE descent. The default skips RTE subqueries — opt in withQTW_EXAMINE_RTES_BEFORE/_AFTERwhen you actually want them.- The typed macro wrappers (
nodeFuncs.h:155-183) let your callback be typed (bool foo_walker(Node *n, MyCtx *ctx)) without-Wincompatible-pointer-typeswarnings.
Files-examined rows
| file | depth | produced |
|---|---|---|
src/backend/parser/README | full | SKILL §"new SQL statement" |
src/backend/parser/gram.y lines 13625-13706 | scanned + 1 rule | SKILL §grammar |
src/backend/parser/scan.l 1-40 | top | SKILL §lexer |
src/backend/parser/parse_node.c 1-50 | top | SKILL §analyze |
src/backend/parser/analyze.c 1-80, 334-433 | transformStmt full switch | SKILL §analyze |
src/backend/nodes/README | full | SKILL §"new Node" |
src/include/nodes/nodes.h | full | SKILL §annotations |
src/include/nodes/pg_list.h 1-120, 380-485 | List API + foreach | idioms |
src/include/nodes/value.h | full | idioms |
src/backend/nodes/gen_node_support.pl 1-140 | top + file list | SKILL §generator |
Cross-references
.claude/skills/executor-and-planner/SKILL.md— Plan-tree side of the three-tree pipeline;createplan.c,ExecInitNode/ExecProcNodefor new plan nodes..claude/skills/catalog-conventions/SKILL.md—CATALOG_VERSION_NObump rules when serialized parse-tree fields change..claude/skills/testing/SKILL.md—debug_copy_parse_plan_trees,debug_write_read_parse_plan_trees,debug_raw_expression_coverage_testGUCs for tree-shape regressions..claude/skills/coding-style/SKILL.md— node-header style,pg_node_attrplacement,T_Fooenum convention..claude/skills/fmgr-and-spi/SKILL.md— function-call node (FuncExpr) interaction with fmgr; SPI's parse-tree consumption.knowledge/idioms/parser-pipeline.md— long-form pipeline narrative.knowledge/idioms/node-types-and-lists.md—List/foreachpatterns + node-tag invariants.source/src/backend/nodes/README— canonical "Steps to Add a Node" reference.source/src/backend/parser/README— gram.y conventions, shift/reduce conflict resolution.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.