Skip to content

Commit 165ba3b

Browse files
authored
Merge pull request #9 from supabase-community/feat/node-utils
feat: node utils
2 parents c49646d + 5a988a7 commit 165ba3b

File tree

9 files changed

+375
-99
lines changed

9 files changed

+375
-99
lines changed

README.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,92 @@ If the parse fails, `PgParser` will return an `error` of type `ParseError` with
231231

232232
**Note:** This is relative to the entire SQL string, not just the statement being parsed or line numbers within a statement. If you are parsing a multi-statement query, the position will be relative to the entire query string, where newlines are counted as single characters.
233233

234+
### Utility functions
235+
236+
The following utility functions are available to help with parsing:
237+
238+
#### `unwrapNode()`
239+
240+
Extracts the node type and nested value while preserving type information.
241+
242+
```typescript
243+
import { unwrapNode } from '@supabase/pg-parser';
244+
245+
const wrappedStatement = result.tree.stmts[0].stmt;
246+
// { SelectStmt: { ... } }
247+
248+
const { type, node } = unwrapNode(wrappedStatement);
249+
// { type: 'SelectStmt', node: { ... } }
250+
```
251+
252+
**Background:** The AST structure produced by Postgres ([libpg_query](https://github.com/pganalyze/libpg_query)) can be complex due to nesting. For example, a `SELECT` statement is represented as:
253+
254+
```typescript
255+
{
256+
version: 170004,
257+
stmts: [
258+
{
259+
stmt: {
260+
SelectStmt: {
261+
targetList: [ ... ],
262+
fromClause: [ ... ],
263+
whereClause: { ... },
264+
...
265+
}
266+
}
267+
}
268+
]
269+
}
270+
```
271+
272+
In order to determine which statement type is being parsed, you'd have to use the `in` operator to check for the presence of a specific key:
273+
274+
```typescript
275+
const wrappedStatement = result.tree.stmts[0].stmt;
276+
// e.g. { SelectStmt: { ... } }
277+
278+
if ('SelectStmt' in wrappedStatement) {
279+
// It's a SELECT statement
280+
const selectStmt = wrappedStatement.SelectStmt;
281+
} else if ('InsertStmt' in wrappedStatement) {
282+
// It's an INSERT statement
283+
const insertStmt = wrappedStatement.InsertStmt;
284+
} else if ('UpdateStmt' in wrappedStatement) {
285+
// It's an UPDATE statement
286+
const updateStmt = wrappedStatement.UpdateStmt;
287+
} else if ('DeleteStmt' in wrappedStatement) {
288+
// It's a DELETE statement
289+
const deleteStmt = wrappedStatement.DeleteStmt;
290+
}
291+
```
292+
293+
`unwrapNode()` simplifies this by extracting the node type and nested value in a single step:
294+
295+
```typescript
296+
const { type, node } = unwrapNode(wrappedStatement);
297+
```
298+
299+
You can then use `type` to determine which statement it is and narrow the type of `node` accordingly:
300+
301+
```typescript
302+
const { type, node } = unwrapNode(wrappedStatement);
303+
304+
switch (type) {
305+
case 'SelectStmt':
306+
// Now `node` is narrowed to `SelectStmt`
307+
break;
308+
case 'InsertStmt':
309+
// Now `node` is narrowed to `InsertStmt`
310+
break;
311+
case 'UpdateStmt':
312+
// Now `node` is narrowed to `UpdateStmt`
313+
break;
314+
case 'DeleteStmt':
315+
// Now `node` is narrowed to `DeleteStmt`
316+
break;
317+
}
318+
```
319+
234320
## Roadmap
235321

236322
- [ ] Deparse SQL queries (AST -> SQL)

packages/pg-parser/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ export {
1515
getSupportedVersions,
1616
isParseResultVersion,
1717
isSupportedVersion,
18+
unwrapNode,
1819
unwrapParseResult,
1920
} from './util.js';

packages/pg-parser/src/pg-parser.test.ts

Lines changed: 39 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import { stripIndent } from 'common-tags';
44
import { describe, expect, it } from 'vitest';
55
import { PgParser } from './pg-parser.js';
66
import type { ParseResult } from './types/index.js';
7-
import { isParseResultVersion, unwrapParseResult } from './util.js';
7+
import {
8+
assertAndUnwrapNode,
9+
assertDefined,
10+
isParseResultVersion,
11+
unwrapParseResult,
12+
} from './util.js';
813

914
import sqlDump from '../test/fixtures/dump.sql';
1015

@@ -36,10 +41,7 @@ describe('pg-parser', () => {
3641
const pgParser = new PgParser();
3742
const result = await unwrapParseResult(pgParser.parse(sqlDump));
3843

39-
if (!result.stmts) {
40-
throw new Error('stmts not found');
41-
}
42-
44+
assertDefined(result.stmts, 'stmts not found');
4345
expect(result.stmts.length).toBeGreaterThan(0);
4446
});
4547

@@ -79,106 +81,55 @@ describe('pg-parser', () => {
7981

8082
expect(result.version).toBe(170004);
8183

82-
if (!result.stmts) {
83-
throw new Error('stmts not found');
84-
}
85-
86-
const [firstStmt] = result.stmts;
87-
if (!firstStmt) {
88-
throw new Error('stmts are empty');
89-
}
90-
9184
// Use type narrowing to ensure the result is of the expected type
9285
// These should produce compile-time errors if the types are incorrect
93-
if (!firstStmt.stmt) {
94-
throw new Error('stmt not found');
95-
}
96-
97-
if (!('SelectStmt' in firstStmt.stmt) || !firstStmt.stmt.SelectStmt) {
98-
throw new Error('SelectStmt not found');
99-
}
100-
101-
if (!('targetList' in firstStmt.stmt.SelectStmt)) {
102-
throw new Error('targetList not found');
103-
}
104-
105-
if (!Array.isArray(firstStmt.stmt.SelectStmt.targetList)) {
106-
throw new Error('targetList is not an array');
107-
}
108-
109-
const [firstTarget] = firstStmt.stmt.SelectStmt.targetList;
110-
if (!firstTarget) {
111-
throw new Error('targetList is empty');
112-
}
113-
114-
if (!('ResTarget' in firstTarget) || !firstTarget.ResTarget) {
115-
throw new Error('ResTarget not found');
116-
}
86+
assertDefined(result.stmts, 'stmts not found');
11787

118-
expect(firstTarget.ResTarget.name).toBe('sum');
88+
const [firstStmt] = result.stmts;
89+
assertDefined(firstStmt, 'stmts are empty');
11990

120-
if (!firstTarget.ResTarget.val) {
121-
throw new Error('val not found');
122-
}
91+
assertDefined(firstStmt.stmt, 'stmt not found');
12392

124-
if (
125-
!('A_Expr' in firstTarget.ResTarget.val) ||
126-
!firstTarget.ResTarget.val.A_Expr
127-
) {
128-
throw new Error('A_Expr not found');
129-
}
93+
const selectStmt = assertAndUnwrapNode(firstStmt.stmt, 'SelectStmt');
94+
assertDefined(selectStmt.targetList, 'targetList not found');
13095

131-
expect(firstTarget.ResTarget.val.A_Expr.kind).toBe('AEXPR_OP');
96+
const [firstTarget] = selectStmt.targetList;
97+
assertDefined(firstTarget, 'targetList is empty');
13298

133-
if (!firstTarget.ResTarget.val.A_Expr.name) {
134-
throw new Error('name not found');
135-
}
99+
const resTarget = assertAndUnwrapNode(firstTarget, 'ResTarget');
136100

137-
const [firstName] = firstTarget.ResTarget.val.A_Expr.name;
101+
expect(resTarget.name).toBe('sum');
102+
assertDefined(resTarget.val, 'val not found');
138103

139-
if (!firstName) {
140-
throw new Error('expression name is empty');
141-
}
104+
const aExpr = assertAndUnwrapNode(resTarget.val, 'A_Expr');
142105

143-
if (!('String' in firstName) || !firstName.String) {
144-
throw new Error('expression name should be String');
145-
}
106+
expect(aExpr.kind).toBe('AEXPR_OP');
107+
assertDefined(aExpr.name, 'name not found');
146108

147-
expect(firstName.String.sval).toBe('+');
109+
const [firstName] = aExpr.name;
110+
assertDefined(firstName, 'expression name is empty');
148111

149-
if (!firstTarget.ResTarget.val.A_Expr.lexpr) {
150-
throw new Error('lexpr not found');
151-
}
112+
const name = assertAndUnwrapNode(firstName, 'String');
152113

153-
if (
154-
!('A_Const' in firstTarget.ResTarget.val.A_Expr.lexpr) ||
155-
!firstTarget.ResTarget.val.A_Expr.lexpr.A_Const
156-
) {
157-
throw new Error('left side of expression should be A_Const');
158-
}
114+
expect(name.sval).toBe('+');
159115

160-
if (!firstTarget.ResTarget.val.A_Expr.lexpr.A_Const.ival) {
161-
throw new Error('expected left side constant to be an integer');
162-
}
163-
164-
expect(firstTarget.ResTarget.val.A_Expr.lexpr.A_Const.ival.ival).toBe(1);
165-
166-
if (!firstTarget.ResTarget.val.A_Expr.rexpr) {
167-
throw new Error('rexpr not found');
168-
}
116+
assertDefined(aExpr.lexpr, 'left side of expression not found');
169117

170-
if (
171-
!('A_Const' in firstTarget.ResTarget.val.A_Expr.rexpr) ||
172-
!firstTarget.ResTarget.val.A_Expr.rexpr.A_Const
173-
) {
174-
throw new Error('right side of expression should be A_Const');
175-
}
118+
const leftConst = assertAndUnwrapNode(aExpr.lexpr, 'A_Const');
119+
assertDefined(
120+
leftConst.ival,
121+
'expected left side constant to be an integer'
122+
);
123+
expect(leftConst.ival.ival).toBe(1);
176124

177-
if (!firstTarget.ResTarget.val.A_Expr.rexpr.A_Const.ival) {
178-
throw new Error('expected right side constant to be an integer');
179-
}
125+
assertDefined(aExpr.rexpr, 'right side of expression not found');
180126

181-
expect(firstTarget.ResTarget.val.A_Expr.rexpr.A_Const.ival.ival).toBe(1);
127+
const rightConst = assertAndUnwrapNode(aExpr.rexpr, 'A_Const');
128+
assertDefined(
129+
rightConst.ival,
130+
'expected right side constant to be an integer'
131+
);
132+
expect(rightConst.ival.ival).toBe(1);
182133
});
183134

184135
it('narrows type using isParseResultVersion', async () => {

packages/pg-parser/src/types/15.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import type { ParseResult } from '../../wasm/15/pg-parser-types.js';
1+
import type { Node, ParseResult } from '../../wasm/15/pg-parser-types.js';
2+
23
export type ParseResult15 = ParseResult;
4+
export type Node15 = Node;
35

4-
export * from '../../wasm/15/pg-parser-types.js';
56
export * from '../../wasm/15/pg-parser-enums.js';
7+
export * from '../../wasm/15/pg-parser-types.js';

packages/pg-parser/src/types/16.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import type { ParseResult } from '../../wasm/16/pg-parser-types.js';
1+
import type { Node, ParseResult } from '../../wasm/16/pg-parser-types.js';
2+
23
export type ParseResult16 = ParseResult;
4+
export type Node16 = Node;
35

4-
export * from '../../wasm/16/pg-parser-types.js';
56
export * from '../../wasm/16/pg-parser-enums.js';
7+
export * from '../../wasm/16/pg-parser-types.js';

packages/pg-parser/src/types/17.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import type { ParseResult } from '../../wasm/17/pg-parser-types.js';
1+
import type { Node, ParseResult } from '../../wasm/17/pg-parser-types.js';
2+
23
export type ParseResult17 = ParseResult;
4+
export type Node17 = Node;
35

4-
export * from '../../wasm/17/pg-parser-types.js';
56
export * from '../../wasm/17/pg-parser-enums.js';
7+
export * from '../../wasm/17/pg-parser-types.js';

packages/pg-parser/src/types/index.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import type { MainModule as MainModule15 } from '../../wasm/15/pg-parser.js';
22
import type { MainModule as MainModule16 } from '../../wasm/16/pg-parser.js';
33
import type { MainModule as MainModule17 } from '../../wasm/17/pg-parser.js';
44

5-
import type { ParseResult15 } from './15.js';
6-
import type { ParseResult16 } from './16.js';
7-
import type { ParseResult17 } from './17.js';
5+
import type { Node15, ParseResult15 } from './15.js';
6+
import type { Node16, ParseResult16 } from './16.js';
7+
import type { Node17, ParseResult17 } from './17.js';
88

99
import type { SUPPORTED_VERSIONS } from '../constants.js';
1010
import type { ParseError } from '../errors.js';
@@ -23,13 +23,23 @@ type ParseResultVersionMap = {
2323
17: ParseResult17;
2424
};
2525

26+
type NodeVersionMap = {
27+
15: Node15;
28+
16: Node16;
29+
17: Node17;
30+
};
31+
2632
export type MainModule<Version extends SupportedVersion> =
2733
ModuleVersionMap[Version];
2834
export type PgParserModule<T extends SupportedVersion> = (
2935
options?: unknown
3036
) => Promise<MainModule<T>>;
3137

32-
export type ParseResult<T extends SupportedVersion> = ParseResultVersionMap[T];
38+
export type ParseResult<T extends SupportedVersion = SupportedVersion> =
39+
ParseResultVersionMap[T];
40+
41+
export type Node<Version extends SupportedVersion = SupportedVersion> =
42+
NodeVersionMap[Version];
3343

3444
export type WrappedParseSuccess<Version extends SupportedVersion> = {
3545
tree: ParseResult<Version>;

0 commit comments

Comments
 (0)