Parser and nodes
Skill matejformanek/postgres-claude/.claude/skills/parser-and-nodes
Turn Claude Code into a long-term collaborator on PostgreSQL internals — cited knowledge corpus, agent skills, slash commands, and task-shaped scenarios for backend hacking.
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.
What its author says it does
Copied from the file, not written here
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).
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.