Skip to content

Commit 039e907

Browse files
feat: improve traverse with NodePath, early return, and runtime schema
- Add NodePath class for better context handling with proper TypeScript generics - Implement early return mechanism (return false to skip subtrees) - Use runtime schema for precise traversal instead of guessing which fields are nodes - Add comprehensive tests for new functionality including NodePath properties and early return - Maintain backward compatibility with existing visit API as wrapper around new walk API - Update documentation with new walk API examples and NodePath class details - Fix traversal logic to handle both schema-guided and fallback traversal modes Co-Authored-By: Dan Lynch <[email protected]>
1 parent beeb09d commit 039e907

File tree

4 files changed

+422
-138
lines changed

4 files changed

+422
-138
lines changed

packages/traverse/README.md

Lines changed: 146 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -10,102 +10,112 @@ npm install @pgsql/traverse
1010

1111
## Usage
1212

13-
### Basic Traversal
13+
### New Walk API (Recommended)
14+
15+
The new `walk` function provides improved traversal with NodePath context and early return support:
1416

1517
```typescript
16-
import { visit } from '@pgsql/traverse';
17-
import type { Visitor } from '@pgsql/traverse';
18+
import { walk, NodePath } from '@pgsql/traverse';
19+
import type { Walker, Visitor } from '@pgsql/traverse';
20+
21+
// Using a simple walker function
22+
const walker: Walker = (path: NodePath) => {
23+
console.log(`Visiting ${path.tag} at path:`, path.path);
24+
25+
// Return false to skip traversing children
26+
if (path.tag === 'SelectStmt') {
27+
return false; // Skip SELECT statement children
28+
}
29+
};
1830

31+
walk(ast, walker);
32+
33+
// Using a visitor object (recommended for multiple node types)
1934
const visitor: Visitor = {
35+
SelectStmt: (path) => {
36+
console.log('SELECT statement:', path.node);
37+
},
38+
RangeVar: (path) => {
39+
console.log('Table:', path.node.relname);
40+
console.log('Path to table:', path.path);
41+
console.log('Parent node:', path.parent?.tag);
42+
}
43+
};
44+
45+
walk(ast, visitor);
46+
```
47+
48+
### NodePath Class
49+
50+
The `NodePath` class provides rich context information:
51+
52+
```typescript
53+
class NodePath<TTag extends NodeTag = NodeTag> {
54+
tag: TTag; // Node type (e.g., 'SelectStmt', 'RangeVar')
55+
node: Node[TTag]; // The actual node data
56+
parent: NodePath | null; // Parent NodePath (null for root)
57+
keyPath: readonly (string | number)[]; // Full path array
58+
59+
get path(): (string | number)[]; // Copy of keyPath
60+
get key(): string | number; // Last element of path
61+
}
62+
```
63+
64+
### Runtime Schema Integration
65+
66+
The new implementation uses PostgreSQL's runtime schema to precisely determine which fields contain Node types that need traversal, eliminating guesswork and improving accuracy.
67+
68+
### Legacy Visit API (Backward Compatible)
69+
70+
The original `visit` function is still available for backward compatibility:
71+
72+
```typescript
73+
import { visit } from '@pgsql/traverse';
74+
75+
const visitor = {
2076
SelectStmt: (node, ctx) => {
2177
console.log('Found SELECT statement:', node);
78+
console.log('Path:', ctx.path);
2279
},
2380
RangeVar: (node, ctx) => {
2481
console.log('Found table reference:', node.relname);
2582
}
2683
};
2784

28-
const ast = {
29-
SelectStmt: {
30-
targetList: [
31-
{
32-
ResTarget: {
33-
val: {
34-
ColumnRef: {
35-
fields: [{ A_Star: {} }]
36-
}
37-
}
38-
}
39-
}
40-
],
41-
fromClause: [
42-
{
43-
RangeVar: {
44-
relname: 'users',
45-
inh: true,
46-
relpersistence: 'p'
47-
}
48-
}
49-
],
50-
limitOption: 'LIMIT_OPTION_DEFAULT',
51-
op: 'SETOP_NONE'
52-
}
53-
};
54-
85+
// Parse some SQL and traverse the AST
86+
const ast = /* your parsed AST */;
5587
visit(ast, visitor);
5688
```
5789

5890
### Working with ParseResult
5991

6092
```typescript
61-
import { visit } from '@pgsql/traverse';
62-
import type { Visitor } from '@pgsql/traverse';
93+
import { walk } from '@pgsql/traverse';
6394

64-
const visitor: Visitor = {
65-
ParseResult: (node, ctx) => {
66-
console.log('PostgreSQL version:', node.version);
95+
const visitor = {
96+
ParseResult: (path) => {
97+
console.log('Parse result version:', path.node.version);
98+
console.log('Number of statements:', path.node.stmts.length);
6799
},
68-
RawStmt: (node, ctx) => {
69-
console.log('Found statement at path:', ctx.path);
70-
}
71-
};
72-
73-
const parseResult = {
74-
ParseResult: {
75-
version: 170004,
76-
stmts: [
77-
{
78-
RawStmt: {
79-
stmt: {
80-
SelectStmt: {
81-
targetList: [],
82-
limitOption: 'LIMIT_OPTION_DEFAULT',
83-
op: 'SETOP_NONE'
84-
}
85-
}
86-
}
87-
}
88-
]
100+
SelectStmt: (path) => {
101+
console.log('SELECT statement found');
89102
}
90103
};
91104

92-
visit(parseResult, visitor);
105+
walk(parseResult, visitor);
93106
```
94107

95108
### Using Visitor Context
96109

97-
The visitor context provides useful information about the current traversal state:
110+
The visitor context provides information about the current traversal state:
98111

99112
```typescript
100-
import { visit } from '@pgsql/traverse';
101-
import type { Visitor, VisitorContext } from '@pgsql/traverse';
102-
103-
const visitor: Visitor = {
104-
RangeVar: (node, ctx: VisitorContext) => {
105-
console.log('Table name:', node.relname);
106-
console.log('Path to this node:', ctx.path);
107-
console.log('Parent object:', ctx.parent);
108-
console.log('Key in parent:', ctx.key);
113+
const visitor = {
114+
RangeVar: (path) => {
115+
console.log('Table name:', path.node.relname);
116+
console.log('Path to this node:', path.path);
117+
console.log('Parent node:', path.parent?.tag);
118+
console.log('Key in parent:', path.key);
109119
}
110120
};
111121
```
@@ -144,39 +154,96 @@ console.log('Columns referenced:', columnRefs);
144154

145155
## API
146156

147-
### `visit(node, visitor, ctx?)`
157+
### `walk(root, callback, parent?, keyPath?)`
148158

149-
Recursively visits a PostgreSQL AST node, calling any matching visitor functions.
159+
Walks the tree of PostgreSQL AST nodes using runtime schema for precise traversal.
150160

151161
**Parameters:**
152-
- `node: KnownNode` - The AST node to traverse (can be a ParseResult, wrapped node, or any PostgreSQL AST node)
153-
- `visitor: Visitor` - Object containing visitor functions for different node types
154-
- `ctx?: VisitorContext` - Optional initial context (usually not needed)
162+
- `root`: The AST node to traverse
163+
- `callback`: A walker function or visitor object
164+
- `parent?`: Optional parent NodePath (for internal use)
165+
- `keyPath?`: Optional key path array (for internal use)
166+
167+
**Example:**
168+
```typescript
169+
walk(ast, {
170+
SelectStmt: (path) => {
171+
// Handle SELECT statements
172+
// Return false to skip children
173+
},
174+
RangeVar: (path) => {
175+
// Handle table references
176+
}
177+
});
178+
```
179+
180+
### `visit(node, visitor, ctx?)` (Legacy)
181+
182+
Recursively visits a PostgreSQL AST node, calling any matching visitor functions. Maintained for backward compatibility.
183+
184+
**Parameters:**
185+
- `node`: The AST node to traverse
186+
- `visitor`: An object with visitor functions for different node types
187+
- `ctx?`: Optional initial visitor context
155188

156189
### Types
157190

158191
#### `Visitor`
159192

160-
An object where keys are PostgreSQL AST node type names and values are visitor functions:
193+
An object type where keys are node type names and values are walker functions:
161194

162195
```typescript
163196
type Visitor = {
164-
[K in keyof KnownNode]?: (node: KnownNode[K], ctx: VisitorContext) => void;
197+
[TTag in NodeTag]?: Walker<NodePath<TTag>>;
165198
};
166199
```
167200

168-
#### `VisitorContext`
201+
#### `Walker`
202+
203+
A function that receives a NodePath and can return false to skip children:
204+
205+
```typescript
206+
type Walker<TNodePath extends NodePath = NodePath> = (
207+
path: TNodePath,
208+
) => boolean | void;
209+
```
210+
211+
#### `NodePath`
212+
213+
A class that encapsulates node traversal context:
214+
215+
```typescript
216+
class NodePath<TTag extends NodeTag = NodeTag> {
217+
tag: TTag; // Node type
218+
node: Node[TTag]; // Node data
219+
parent: NodePath | null; // Parent path
220+
keyPath: readonly (string | number)[]; // Full path
221+
222+
get path(): (string | number)[]; // Path copy
223+
get key(): string | number; // Current key
224+
}
225+
```
226+
227+
#### `VisitorContext` (Legacy)
169228

170-
Context information provided to each visitor function:
229+
Context information provided to legacy visitor functions:
171230

172231
```typescript
173232
type VisitorContext = {
174-
path: (string | number)[]; // Path to current node (e.g., ['SelectStmt', 'fromClause', 0])
175-
parent: any; // Parent object containing this node
176-
key: string | number; // Key or index of this node in its parent
233+
path: (string | number)[]; // Path to current node
234+
parent: any; // Parent node
235+
key: string | number; // Key in parent node
177236
};
178237
```
179238

239+
#### `NodeTag`
240+
241+
Union type of all PostgreSQL AST node type names:
242+
243+
```typescript
244+
type NodeTag = keyof Node;
245+
```
246+
180247
## Supported Node Types
181248

182249
This package works with all PostgreSQL AST node types defined in `@pgsql/types`, including:

0 commit comments

Comments
 (0)