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 via typeof_unqual macro, nodes.h:228-233). Dispatcher copyObjectImpl in copyfuncs.c:177-212, with check_stack_depth() guard (:185). By-ref Datums go through _copyConst → datumCopy; T_List is deep-copied via list_copy_deep, T_IntList/T_OidList/T_XidList shallow via list_copy. Extensible nodes dispatch to a registered nodeCopy callback (_copyExtensibleNode). Allocated in CurrentMemoryContext — 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-trip copyObject(stringToNode(nodeToString(p))) underpins the debug_write_read_parse_plan_trees GUC.
expression_tree_walker / _mutator — polymorphic recursion over expression trees (post-analysis: Var, OpExpr, …). raw_expression_tree_walker for the pre-analysis shape (A_Expr, ColumnRef). query_tree_walker / _mutator wrap for Query, with QTW_* flags (nodeFuncs.h:22-34) to control rtable/CTE descent. planstate_tree_walker is the executor-time analogue. Mutator contract: return NULL to delete a subtree; walker contract: callback returns true to short-circuit, false to keep descending. The macro wrappers (nodeFuncs.h:155-183) let callbacks be typed without -Wincompatible-pointer-types warnings.
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 to src/include/parser/kwlist.h (alphabetical, with category).
- Add a top-level rule producing your new
*Stmt node, wire it into stmt: (search for stmt: in gram.y). Use makeNode(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 if parser/parse_utilcmd.c already has one.
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 through kwlist.h).
Parse node — src/include/nodes/parsenodes.h (utility stmts) or primnodes.h (expressions) or plannodes.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 like query_jumble_ignore, equal_ignore. See nodes.h:43-125 for the full list.
Analyze / execute
- Optimizable statement → add a
transformFooStmt(ParseState *, FooStmt *) in parser/analyze.c and wire it into the switch (nodeTag(parseTree)) inside transformStmt (analyze.c:334-451, switch at :368). Caution (analyze.c:363-367): any change to that switch must also be reflected in stmt_requires_parse_analysis() (:469-505) and analyze_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 in src/backend/commands/ via ProcessUtility / standard_ProcessUtility (src/backend/tcop/utility.c).
Run gen_node_support.pl (done automatically by the build) and verify the generated copyfuncs.funcs.c, equalfuncs.funcs.c, outfuncs.funcs.c, readfuncs.funcs.c chunks 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_files list (parsenodes.h, primnodes.h, plannodes.h, pathnodes.h, execnodes.h, value.h, etc. — see gen_node_support.pl:53-77). The T_Foo tag is generated automatically into nodes/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.c entries for your node. Add pg_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 in copyfuncs.c / equalfuncs.c and the generator will skip yours.
pg_node_attr(no_copy_equal, no_read, no_query_jumble) — opt out entirely (executor state nodes typically use nodetag_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_opfuncid family (:1890+) — only if your node holds an operator OID needing resolution.
Grep T_<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-depend helps). No initdb needed — node numbers never go to disk. BUT if the node can appear in a stored parse tree (rule actions, view definitions), bump CATALOG_VERSION_NO in src/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 via PG_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 a pg_node_attr if 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. Return true to short-circuit
the whole walk; return false to continue descending.
- Mutator callback returns
Node *. Return NULL to 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 with
QTW_EXAMINE_RTES_BEFORE / _AFTER when 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-types warnings.
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/ExecProcNode for new plan nodes.
.claude/skills/catalog-conventions/SKILL.md — CATALOG_VERSION_NO bump 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_test GUCs for tree-shape regressions.
.claude/skills/coding-style/SKILL.md — node-header style, pg_node_attr placement, T_Foo enum 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 / foreach patterns + 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.
1---2name: parser-and-nodes3description: 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).4---56# Parser & Node-types — operational78Companion docs: `knowledge/idioms/parser-pipeline.md`, `knowledge/idioms/node-types-and-lists.md`.910## 0. The three-tree pipeline — AST → Query → Plan1112Every SQL statement flows through three distinct tree shapes; nodes13in each header live or die at a specific stage. Confuse the stages14and your patch will silently fail in a downstream walker.1516```17SQL text18 │19 │ gram.y / scan.l / kwlist.h20 ▼211. Raw parsetree (AST) — parsenodes.h utility-stmt nodes22 Examples: SelectStmt, + A_Expr / A_Const / ColumnRef /23 InsertStmt, A_Expr, FuncCall (raw expression shapes)24 ColumnRef, FuncCall → walker: raw_expression_tree_walker25 No types resolved, no relations26 looked up, no operators resolved.27 │28 │ parser/analyze.c → transformStmt → transform<Foo>Stmt29 │ parser/parse_*.c → adds rangetable, resolves names,30 │ picks operator + cast OIDs,31 │ applies coercions32 ▼332. Query tree — parsenodes.h Query + analyzed34 Examples: Query, RangeTblEntry, expression nodes (primnodes.h)35 Var, OpExpr, Const, FuncExpr, Examples: Var, OpExpr, Const,36 TargetEntry, FromExpr, JoinExpr FuncExpr, Aggref, WindowFunc37 → walker: expression_tree_walker38 + query_tree_walker39 + QTW_* flags for rtable/CTE descent40 │41 │ rewrite/ → rules, views (Query → Query)42 │ optimizer/planner.c → planner: Query → Plan43 ▼443. Plan tree — plannodes.h45 Examples: SeqScan, Examples: SeqScan, IndexScan, HashJoin,46 IndexScan, HashJoin, Sort, Agg, Limit, Gather, GatherMerge47 Sort, Agg, Limit → walker: plan_tree_walker48 + planstate_tree_walker (executor-time)49 Shipped to parallel workers via50 nodeToString / stringToNode.51```5253Operational rules per shape:5455| Shape | Key invariants |56|---|---|57| **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. |58| **Query tree** | Types, collations, operator OIDs, cast OIDs resolved. `Var` carries `varno`+`varattno` into the rangetable. Subqueries are nested `Query`s in `RangeTblEntry`. Stored in `pg_rewrite.ev_action` for views/rules — bump catversion when changing serialized fields. |59| **Plan tree** | Materialized from `Path`s by `createplan.c`. `Plan`'s expression trees use `Var`s 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. |6061The `executor-and-planner` skill covers the Plan-tree side (createplan,62add_path costing, ExecInit/ProcNode dispatch). This skill covers raw63parsetree and Query tree work, plus the node-machinery infrastructure64(`gen_node_support.pl`, copy/equal/out/read funcs).6566## Tools for working with an existing tree6768- `copyObject(p)` — deep copy (preserves argument type via `typeof_unqual` macro, `nodes.h:228-233`). Dispatcher `copyObjectImpl` in `copyfuncs.c:177-212`, with `check_stack_depth()` guard (`:185`). By-ref Datums go through `_copyConst` → `datumCopy`; `T_List` is deep-copied via `list_copy_deep`, `T_IntList`/`T_OidList`/`T_XidList` shallow via `list_copy`. Extensible nodes dispatch to a registered `nodeCopy` callback (`_copyExtensibleNode`). Allocated in `CurrentMemoryContext` — caller controls the lifetime.69- `equal(a, b)` — deep structural compare (`equalfuncs.c`).70- `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-trip `copyObject(stringToNode(nodeToString(p)))` underpins the `debug_write_read_parse_plan_trees` GUC.71- `expression_tree_walker` / `_mutator` — polymorphic recursion over expression trees (post-analysis: `Var`, `OpExpr`, …). `raw_expression_tree_walker` for the pre-analysis shape (`A_Expr`, `ColumnRef`). `query_tree_walker` / `_mutator` wrap for Query, with `QTW_*` flags (`nodeFuncs.h:22-34`) to control rtable/CTE descent. `planstate_tree_walker` is the executor-time analogue. Mutator contract: return `NULL` to delete a subtree; walker contract: callback returns `true` to short-circuit, `false` to keep descending. The macro wrappers (`nodeFuncs.h:155-183`) let callbacks be typed without `-Wincompatible-pointer-types` warnings.7273## When you're adding a new SQL statement7475Touch four layers, in order. Build between each so you fail fast.76771. **Grammar — `src/backend/parser/gram.y`**78 - Add the keyword(s) to `unreserved_keyword` / `col_name_keyword` / etc. AND to `src/include/parser/kwlist.h` (alphabetical, with category).79 - Add a top-level rule producing your new `*Stmt` node, wire it into `stmt:` (search for `stmt:` in `gram.y`). Use `makeNode(FooStmt)` in the action, set the location with `@1`, fill fields from `$N`.80 - Match an existing simple statement as template (e.g. `DropStmt`, `VariableSetStmt`). Do NOT invent fresh helpers in gram.y if `parser/parse_utilcmd.c` already has one.81822. **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 through `kwlist.h`)*.83843. **Parse node — `src/include/nodes/parsenodes.h`** *(utility stmts)* or `primnodes.h` *(expressions)* or `plannodes.h` *(plan nodes)*85 - First field `NodeTag type;` (or another node struct, for "inheritance").86 - Last field for top-level statements is typically `ParseLoc location;`.87 - Annotate fields if needed: `pg_node_attr(...)` on the struct, per-field attrs like `query_jumble_ignore`, `equal_ignore`. See `nodes.h:43-125` for the full list.88894. **Analyze / execute**90 - Optimizable statement → add a `transformFooStmt(ParseState *, FooStmt *)` in `parser/analyze.c` and wire it into the `switch (nodeTag(parseTree))` inside `transformStmt` (analyze.c:334-451, switch at :368). **Caution (analyze.c:363-367)**: any change to that switch must also be reflected in `stmt_requires_parse_analysis()` (`:469-505`) and `analyze_requires_snapshot()` (`:513-529`) — three sites, one logical change.91 - Utility statement → no transform needed; just dump into a Query with `CMD_UTILITY` (the default path). Execution lives in `src/backend/commands/` via `ProcessUtility` / `standard_ProcessUtility` (`src/backend/tcop/utility.c`).92935. Run `gen_node_support.pl` (done automatically by the build) and verify the generated `copyfuncs.funcs.c`, `equalfuncs.funcs.c`, `outfuncs.funcs.c`, `readfuncs.funcs.c` chunks for your new struct look sane.94956. Add a regression test under `src/test/regress/sql/` (and a matching expected file). See `.claude/skills/testing/SKILL.md`.9697## When you're adding a new Node type (no SQL change)9899From `src/backend/nodes/README` "Steps to Add a Node":1001011. Put the struct in the right header. The file must be in `gen_node_support.pl`'s `@all_input_files` list (parsenodes.h, primnodes.h, plannodes.h, pathnodes.h, execnodes.h, value.h, etc. — see `gen_node_support.pl:53-77`). The `T_Foo` tag is generated automatically into `nodes/nodetags.h` (do NOT hand-edit it).1022. 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).1033. Build. Inspect the generated `copyfuncs.funcs.c` / `equalfuncs.funcs.c` / `outfuncs.funcs.c` / `readfuncs.funcs.c` / `queryjumblefuncs.funcs.c` entries for your node. Add `pg_node_attr(...)` and per-field attrs to fix anything wrong.1044. If a field needs special treatment, common annotations:105 - `pg_node_attr(custom_copy_equal)` — write your own bodies in `copyfuncs.c` / `equalfuncs.c` and the generator will skip yours.106 - `pg_node_attr(no_copy_equal, no_read, no_query_jumble)` — opt out entirely (executor state nodes typically use `nodetag_only`).107 - Per-field: `array_size(otherfield)`, `copy_as(VALUE)`, `equal_ignore`, `read_write_ignore`, `query_jumble_ignore`, `query_jumble_location`.1085. If your node is an **expression** (lives in primnodes.h or has child Expr fields), add cases to **every** giant switch in `nodeFuncs.c`:109 - `exprType` (`:42+`), `exprTypmod` (`:304+`), `exprCollation` / `exprSetCollation` / `exprInputCollation` (`:826+`, `:1140+`, `:1092+`), `exprLocation` (`:1403+`) — type/typmod/collation/location introspection.110 - `expression_tree_walker_impl` (`:2111+`), `expression_tree_mutator_impl` (`:3018+`) — recursion into children.111 - `raw_expression_tree_walker_impl` (`:4115+`) — only if the node can appear in raw parsetrees.112 - `set_opfuncid` family (`:1890+`) — only if your node holds an operator OID needing resolution.113 Grep `T_<SiblingNode>` (e.g. `T_OpExpr`) to find every site; five-to-eight maintenance points per node, miss one and walkers silently misbehave.1146. **Adding a node type renumbers existing tags.** Recompile the whole tree (`--enable-depend` helps). No initdb needed — node numbers never go to disk. BUT if the node can appear in a *stored* parse tree (rule actions, view definitions), bump `CATALOG_VERSION_NO` in `src/include/catalog/catversion.h`.1157. Test with `debug_copy_parse_plan_trees=on`, `debug_write_read_parse_plan_trees=on`, `debug_raw_expression_coverage_test=on` (set via `PG_TEST_INITDB_EXTRA_OPTS`).116117## When you're modifying an existing Node type118119- **Adding a field** to a node that appears in stored catalog parse trees (e.g. `Query`, anything in views / rules) → bump catversion. Add a `pg_node_attr` if 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).120- **Removing or reordering fields** → also catversion bump if stored.121- **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.122123## Pre-submit smoke124125```126cd dev/build-debug && ninja && \127 meson test --suite regress -q128```129130Plus the debug-tree GUCs above on a separate initdb if you touched anything tree-shape related.131132## Node-type reference table133134The major node families and where each lives. When in doubt about135which header your new field belongs in, check this table.136137| Header | Shape | Examples | When you'd add here |138|---|---|---|---|139| `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`) |140| `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") |141| `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`) |142| `nodes/pathnodes.h` | Path + RelOptInfo | `Path`, `IndexPath`, `JoinPath`, `RelOptInfo`, `PlannerInfo` | New path shape considered by the optimizer |143| `nodes/execnodes.h` | PlanState (runtime) | `PlanState`, `SeqScanState`, `EState`, `ExprContext`, `ProjectionInfo` | New runtime state for an executor node |144| `nodes/value.h` | Leaf values | `Integer`, `Float`, `Boolean`, `String`, `BitString` | Almost never — the four primitive value carriers |145| `nodes/pg_list.h` | Lists | `List`, `IntList`, `OidList`, `XidList` | Almost never (collection infrastructure) |146| `nodes/extensible.h` | Extension nodes | `ExtensibleNode`, `CustomScan` | Custom AM / custom planner extension |147148`nodes/nodetags.h` is auto-generated by `gen_node_support.pl`; **never149hand-edit it.** A new struct in any of the headers above (with the150file listed in `gen_node_support.pl @all_input_files`) gets its `T_Foo`151tag automatically.152153## Walker / mutator quick reference154155| Function | Where | Use for |156|---|---|---|157| `raw_expression_tree_walker` | `nodes/nodeFuncs.c` | Pre-analysis (`A_Expr`, `ColumnRef`, `FuncCall`); used by `parser/parse_*.c` |158| `expression_tree_walker` | `nodes/nodeFuncs.c` | Analyzed expressions in a Query tree |159| `expression_tree_mutator` | `nodes/nodeFuncs.c` | Same shape, but returns new nodes; rewriter / planner use this |160| `query_tree_walker` | `nodes/nodeFuncs.c` | Wraps `expression_tree_walker` to also walk Query subexpressions; controlled by `QTW_*` flags |161| `query_tree_mutator` | `nodes/nodeFuncs.c` | Mutator counterpart |162| `range_table_walker` / `_mutator` | `nodes/nodeFuncs.c` | Just the rangetable, with per-RTE field control flags |163| `plan_tree_walker` | `nodes/nodeFuncs.c` | Plan-tree (`plannodes.h`) recursion |164| `planstate_tree_walker` | `executor/execProcnode.c` | Executor-time (`PlanState`) recursion; used by EXPLAIN |165166Conventions to remember:167168- **Walker callback** returns `bool`. Return `true` to short-circuit169 the whole walk; return `false` to continue descending.170- **Mutator callback** returns `Node *`. Return `NULL` to delete a171 subtree; return the input pointer to keep it unchanged; return a172 new node to replace.173- **`QTW_*` flags** (`nodeFuncs.h:22-34`) control rtable / CTE174 descent. The default skips RTE subqueries — opt in with175 `QTW_EXAMINE_RTES_BEFORE` / `_AFTER` when you actually want them.176- The typed macro wrappers (`nodeFuncs.h:155-183`) let your177 callback be typed (`bool foo_walker(Node *n, MyCtx *ctx)`) without178 `-Wincompatible-pointer-types` warnings.179180## Files-examined rows181182| file | depth | produced |183| --- | --- | --- |184| `src/backend/parser/README` | full | SKILL §"new SQL statement" |185| `src/backend/parser/gram.y` lines 13625-13706 | scanned + 1 rule | SKILL §grammar |186| `src/backend/parser/scan.l` 1-40 | top | SKILL §lexer |187| `src/backend/parser/parse_node.c` 1-50 | top | SKILL §analyze |188| `src/backend/parser/analyze.c` 1-80, 334-433 | transformStmt full switch | SKILL §analyze |189| `src/backend/nodes/README` | full | SKILL §"new Node" |190| `src/include/nodes/nodes.h` | full | SKILL §annotations |191| `src/include/nodes/pg_list.h` 1-120, 380-485 | List API + foreach | idioms |192| `src/include/nodes/value.h` | full | idioms |193| `src/backend/nodes/gen_node_support.pl` 1-140 | top + file list | SKILL §generator |194195## Cross-references196197- `.claude/skills/executor-and-planner/SKILL.md` — Plan-tree side of the three-tree pipeline; `createplan.c`, `ExecInitNode`/`ExecProcNode` for new plan nodes.198- `.claude/skills/catalog-conventions/SKILL.md` — `CATALOG_VERSION_NO` bump rules when serialized parse-tree fields change.199- `.claude/skills/testing/SKILL.md` — `debug_copy_parse_plan_trees`, `debug_write_read_parse_plan_trees`, `debug_raw_expression_coverage_test` GUCs for tree-shape regressions.200- `.claude/skills/coding-style/SKILL.md` — node-header style, `pg_node_attr` placement, `T_Foo` enum convention.201- `.claude/skills/fmgr-and-spi/SKILL.md` — function-call node (`FuncExpr`) interaction with fmgr; SPI's parse-tree consumption.202- `knowledge/idioms/parser-pipeline.md` — long-form pipeline narrative.203- `knowledge/idioms/node-types-and-lists.md` — `List` / `foreach` patterns + node-tag invariants.204- `source/src/backend/nodes/README` — canonical "Steps to Add a Node" reference.205- `source/src/backend/parser/README` — gram.y conventions, shift/reduce conflict resolution.