Skip to content

Commit ca3e3ce

Browse files
authored
Merge pull request #250 from constructive-io/devin/1766803722-add-pg-ast-package
feat: add pg-ast package (copy of @pgsql/utils without runtime schema)
2 parents df8eb8b + 72cfe07 commit ca3e3ce

File tree

13 files changed

+9202
-5570
lines changed

13 files changed

+9202
-5570
lines changed

packages/pg-ast/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Change Log
2+
3+
All notable changes to this project will be documented in this file.
4+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

packages/pg-ast/README.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# pg-ast
2+
3+
<p align="center" width="100%">
4+
<img height="120" src="https://github.com/constructive-io/pgsql-parser/assets/545047/6440fa7d-918b-4a3b-8d1b-755d85de8bea" />
5+
</p>
6+
7+
<p align="center" width="100%">
8+
<a href="https://github.com/constructive-io/pgsql-parser/actions/workflows/run-tests.yaml">
9+
<img height="20" src="https://github.com/constructive-io/pgsql-parser/actions/workflows/run-tests.yaml/badge.svg" />
10+
</a>
11+
<a href="https://github.com/constructive-io/pgsql-parser/blob/main/LICENSE-MIT"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a>
12+
<a href="https://www.npmjs.com/package/pg-ast"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/pgsql-parser?filename=packages%2Fpg-ast%2Fpackage.json"/></a>
13+
</p>
14+
15+
`pg-ast` is a utility library for `@pgsql/types`, offering convenient functions to work with PostgreSQL Abstract Syntax Tree (AST) nodes in a type-safe manner. This library facilitates the creation of AST nodes for building SQL queries or statements programmatically.
16+
17+
> **Note**: If you need the runtime schema for AST introspection, use [`@pgsql/utils`](https://www.npmjs.com/package/@pgsql/utils) instead.
18+
19+
# Table of Contents
20+
21+
1. [pg-ast](#pg-ast)
22+
- [Features](#features)
23+
2. [Installation](#installation)
24+
3. [Usage](#usage)
25+
- [AST Node Creation](#ast-node-creation)
26+
- [JSON AST](#json-ast)
27+
- [Select Statement](#select-statement)
28+
- [Creating Table Schemas Dynamically](#creating-table-schemas-dynamically)
29+
4. [Related Projects](#related)
30+
5. [Disclaimer](#disclaimer)
31+
32+
## Features
33+
34+
- **AST Node Creation**: Simplifies the process of constructing PostgreSQL AST nodes, allowing for easy assembly of SQL queries or statements programmatically.
35+
- **Comprehensive Coverage**: Supports all node types defined in the PostgreSQL AST.
36+
- **Seamless Integration**: Designed to be used alongside the `@pgsql/types` package for a complete AST handling solution.
37+
38+
## Installation
39+
40+
To add `pg-ast` to your project, use the following npm command:
41+
42+
```bash
43+
npm install pg-ast
44+
```
45+
46+
## Usage
47+
48+
### AST Node Creation
49+
50+
With the AST helper methods, creating complex SQL ASTs becomes straightforward and intuitive.
51+
52+
#### JSON AST
53+
54+
Explore the PostgreSQL Abstract Syntax Tree (AST) as JSON objects with ease using `pg-ast`. Below is an example of how you can generate a JSON AST using TypeScript:
55+
56+
```ts
57+
import * as t from 'pg-ast';
58+
import { SelectStmt } from '@pgsql/types';
59+
import { deparse } from 'pgsql-deparser';
60+
61+
const selectStmt: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
62+
targetList: [
63+
t.nodes.resTarget({
64+
val: t.nodes.columnRef({
65+
fields: [t.nodes.aStar()]
66+
})
67+
})
68+
],
69+
fromClause: [
70+
t.nodes.rangeVar({
71+
relname: 'some_amazing_table',
72+
inh: true,
73+
relpersistence: 'p'
74+
})
75+
],
76+
limitOption: 'LIMIT_OPTION_DEFAULT',
77+
op: 'SETOP_NONE'
78+
});
79+
console.log(selectStmt);
80+
// Output: { "SelectStmt": { "targetList": [ { "ResTarget": { "val": { "ColumnRef": { "fields": [ { "A_Star": {} } ] } } } } ], "fromClause": [ { "RangeVar": { "relname": "some_amazing_table", "inh": true, "relpersistence": "p" } } ], "limitOption": "LIMIT_OPTION_DEFAULT", "op": "SETOP_NONE" } }
81+
console.log(await deparse(stmt))
82+
// Output: SELECT * FROM some_amazing_table
83+
```
84+
85+
#### Select Statement
86+
87+
```ts
88+
import * as t from 'pg-ast';
89+
import { SelectStmt } from '@pgsql/types';
90+
import { deparse } from 'pgsql-deparser';
91+
92+
const query: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
93+
targetList: [
94+
t.nodes.resTarget({
95+
val: t.nodes.columnRef({
96+
fields: [t.nodes.string({ sval: 'name' })]
97+
})
98+
}),
99+
t.nodes.resTarget({
100+
val: t.nodes.columnRef({
101+
fields: [t.nodes.string({ sval: 'email' })]
102+
})
103+
})
104+
],
105+
fromClause: [
106+
t.nodes.rangeVar({
107+
relname: 'users',
108+
inh: true,
109+
relpersistence: 'p'
110+
})
111+
],
112+
whereClause: t.nodes.aExpr({
113+
kind: 'AEXPR_OP',
114+
name: [t.nodes.string({ sval: '>' })],
115+
lexpr: t.nodes.columnRef({
116+
fields: [t.nodes.string({ sval: 'age' })]
117+
}),
118+
rexpr: t.nodes.aConst({
119+
ival: t.ast.integer({ ival: 18 })
120+
})
121+
}),
122+
limitOption: 'LIMIT_OPTION_DEFAULT',
123+
op: 'SETOP_NONE'
124+
});
125+
126+
await deparse(createStmt);
127+
// SELECT name, email FROM users WHERE age > 18
128+
```
129+
130+
#### Creating Table Schemas Dynamically
131+
132+
```ts
133+
// Example JSON schema
134+
const schema = {
135+
"tableName": "users",
136+
"columns": [
137+
{ "name": "id", "type": "int", "constraints": ["PRIMARY KEY"] },
138+
{ "name": "username", "type": "text" },
139+
{ "name": "email", "type": "text", "constraints": ["UNIQUE"] },
140+
{ "name": "created_at", "type": "timestamp", "constraints": ["NOT NULL"] }
141+
]
142+
};
143+
144+
// Construct the CREATE TABLE statement
145+
const createStmt = t.nodes.createStmt({
146+
relation: t.ast.rangeVar({
147+
relname: schema.tableName,
148+
inh: true,
149+
relpersistence: 'p'
150+
}),
151+
tableElts: schema.columns.map(column => t.nodes.columnDef({
152+
colname: column.name,
153+
typeName: t.ast.typeName({
154+
names: [t.nodes.string({ sval: column.type })]
155+
}),
156+
constraints: column.constraints?.map(constraint =>
157+
t.nodes.constraint({
158+
contype: constraint === "PRIMARY KEY" ? "CONSTR_PRIMARY" : constraint === "UNIQUE" ? "CONSTR_UNIQUE" : "CONSTR_NOTNULL"
159+
})
160+
)
161+
}))
162+
});
163+
164+
// `deparse` function converts AST to SQL string
165+
const sql = await deparse(createStmt, { pretty: true });
166+
167+
console.log(sql);
168+
// OUTPUT:
169+
170+
// CREATE TABLE users (
171+
// id int PRIMARY KEY,
172+
// username text,
173+
// email text UNIQUE,
174+
// created_at timestamp NOT NULL
175+
// )
176+
```
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2+
3+
exports[`dynamic creation of tables 1`] = `"CREATE TABLE users (id int PRIMARY KEY, username text, email text UNIQUE, created_at timestamp NOT NULL)"`;
4+
5+
exports[`simple SelectStmt 1`] = `
6+
{
7+
"SelectStmt": {
8+
"fromClause": [
9+
{
10+
"RangeVar": {
11+
"inh": true,
12+
"relname": "another_table",
13+
"relpersistence": "p",
14+
},
15+
},
16+
],
17+
"limitOption": "LIMIT_OPTION_DEFAULT",
18+
"op": "SETOP_NONE",
19+
"targetList": [
20+
{
21+
"ResTarget": {
22+
"val": {
23+
"ColumnRef": {
24+
"fields": [
25+
{
26+
"A_Star": {},
27+
},
28+
],
29+
},
30+
},
31+
},
32+
},
33+
],
34+
},
35+
}
36+
`;
37+
38+
exports[`simple SelectStmt 2`] = `"SELECT * FROM another_table"`;
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import * as t from '../src';
2+
import { RangeVar, SelectStmt } from '@pgsql/types';
3+
import { deparseSync as deparse } from 'pgsql-deparser';
4+
5+
it('simple SelectStmt', () => {
6+
const stmt: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
7+
targetList: [
8+
t.nodes.resTarget({
9+
val: t.nodes.columnRef({
10+
fields: [t.nodes.aStar()]
11+
})
12+
})
13+
],
14+
fromClause: [
15+
t.nodes.rangeVar({
16+
relname: 'some_table',
17+
inh: true,
18+
relpersistence: 'p'
19+
})
20+
],
21+
limitOption: 'LIMIT_OPTION_DEFAULT',
22+
op: 'SETOP_NONE'
23+
});
24+
25+
(stmt.SelectStmt.fromClause[0] as {RangeVar: RangeVar}).RangeVar.relname = 'another_table';
26+
27+
expect(stmt).toMatchSnapshot();
28+
expect(deparse(stmt, { pretty: false })).toMatchSnapshot();
29+
});
30+
31+
it('SelectStmt with WHERE clause', () => {
32+
const selectStmt: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
33+
targetList: [
34+
t.nodes.resTarget({
35+
val: t.nodes.columnRef({
36+
fields: [t.nodes.aStar()]
37+
})
38+
})
39+
],
40+
fromClause: [
41+
t.nodes.rangeVar({
42+
schemaname: 'myschema',
43+
relname: 'mytable',
44+
inh: true,
45+
relpersistence: 'p'
46+
})
47+
],
48+
whereClause: t.nodes.aExpr({
49+
kind: 'AEXPR_OP',
50+
name: [t.nodes.string({ sval: '=' })],
51+
lexpr: t.nodes.columnRef({
52+
fields: [t.nodes.string({ sval: 'a' })]
53+
}),
54+
rexpr: t.nodes.typeCast({
55+
arg: t.nodes.aConst({
56+
sval: t.ast.string({ sval: 't' })
57+
}),
58+
typeName: t.ast.typeName({
59+
names: [
60+
t.nodes.string({ sval: 'pg_catalog' }),
61+
t.nodes.string({ sval: 'bool' })
62+
],
63+
typemod: -1
64+
})
65+
})
66+
}),
67+
limitOption: 'LIMIT_OPTION_DEFAULT',
68+
op: 'SETOP_NONE'
69+
});
70+
71+
expect(deparse(selectStmt, { pretty: false })).toEqual(`SELECT * FROM myschema.mytable WHERE a = CAST('t' AS boolean)`);
72+
});
73+
74+
it('queries', () => {
75+
const query: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
76+
targetList: [
77+
t.nodes.resTarget({
78+
val: t.nodes.columnRef({
79+
fields: [t.nodes.string({ sval: 'name' })]
80+
})
81+
}),
82+
t.nodes.resTarget({
83+
val: t.nodes.columnRef({
84+
fields: [t.nodes.string({ sval: 'email' })]
85+
})
86+
})
87+
],
88+
fromClause: [
89+
t.nodes.rangeVar({
90+
relname: 'users',
91+
inh: true,
92+
relpersistence: 'p'
93+
})
94+
],
95+
whereClause: t.nodes.aExpr({
96+
kind: 'AEXPR_OP',
97+
name: [t.nodes.string({ sval: '>' })],
98+
lexpr: t.nodes.columnRef({
99+
fields: [t.nodes.string({ sval: 'age' })]
100+
}),
101+
rexpr: t.nodes.aConst({
102+
ival: t.ast.integer({ ival: 18 })
103+
})
104+
}),
105+
limitOption: 'LIMIT_OPTION_DEFAULT',
106+
op: 'SETOP_NONE'
107+
});
108+
109+
expect(deparse(query, { pretty: false })).toEqual(`SELECT name, email FROM users WHERE age > 18`);
110+
111+
});
112+
it('dynamic creation of tables', () => {
113+
// Example JSON schema
114+
const schema = {
115+
"tableName": "users",
116+
"columns": [
117+
{ "name": "id", "type": "int", "constraints": ["PRIMARY KEY"] },
118+
{ "name": "username", "type": "text" },
119+
{ "name": "email", "type": "text", "constraints": ["UNIQUE"] },
120+
{ "name": "created_at", "type": "timestamp", "constraints": ["NOT NULL"] }
121+
]
122+
};
123+
124+
// Construct the CREATE TABLE statement
125+
const createStmt = t.nodes.createStmt({
126+
relation: t.ast.rangeVar({
127+
relname: schema.tableName,
128+
inh: true,
129+
relpersistence: 'p'
130+
}),
131+
tableElts: schema.columns.map(column => t.nodes.columnDef({
132+
colname: column.name,
133+
typeName: t.ast.typeName({
134+
names: [t.nodes.string({ sval: column.type })]
135+
}),
136+
constraints: column.constraints?.map(constraint =>
137+
t.nodes.constraint({
138+
contype: constraint === "PRIMARY KEY" ? "CONSTR_PRIMARY" : constraint === "UNIQUE" ? "CONSTR_UNIQUE" : "CONSTR_NOTNULL"
139+
})
140+
)
141+
}))
142+
});
143+
144+
// `deparse` function converts AST to SQL string
145+
const sql = deparse(createStmt, { pretty: false });
146+
expect(sql).toMatchSnapshot();
147+
})

packages/pg-ast/jest.config.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/** @type {import('ts-jest').JestConfigWithTsJest} */
2+
module.exports = {
3+
preset: "ts-jest",
4+
testEnvironment: "node",
5+
transform: {
6+
"^.+\\.tsx?$": [
7+
"ts-jest",
8+
{
9+
babelConfig: false,
10+
tsconfig: "tsconfig.json",
11+
},
12+
],
13+
},
14+
transformIgnorePatterns: [`/node_modules/*`],
15+
testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
16+
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
17+
modulePathIgnorePatterns: ["dist/*"]
18+
};

0 commit comments

Comments
 (0)