Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/pg-ast/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
176 changes: 176 additions & 0 deletions packages/pg-ast/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# pg-ast

<p align="center" width="100%">
<img height="120" src="https://github.com/constructive-io/pgsql-parser/assets/545047/6440fa7d-918b-4a3b-8d1b-755d85de8bea" />
</p>

<p align="center" width="100%">
<a href="https://github.com/constructive-io/pgsql-parser/actions/workflows/run-tests.yaml">
<img height="20" src="https://github.com/constructive-io/pgsql-parser/actions/workflows/run-tests.yaml/badge.svg" />
</a>
<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>
<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>
</p>

`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.

> **Note**: If you need the runtime schema for AST introspection, use [`@pgsql/utils`](https://www.npmjs.com/package/@pgsql/utils) instead.

# Table of Contents

1. [pg-ast](#pg-ast)
- [Features](#features)
2. [Installation](#installation)
3. [Usage](#usage)
- [AST Node Creation](#ast-node-creation)
- [JSON AST](#json-ast)
- [Select Statement](#select-statement)
- [Creating Table Schemas Dynamically](#creating-table-schemas-dynamically)
4. [Related Projects](#related)
5. [Disclaimer](#disclaimer)

## Features

- **AST Node Creation**: Simplifies the process of constructing PostgreSQL AST nodes, allowing for easy assembly of SQL queries or statements programmatically.
- **Comprehensive Coverage**: Supports all node types defined in the PostgreSQL AST.
- **Seamless Integration**: Designed to be used alongside the `@pgsql/types` package for a complete AST handling solution.

## Installation

To add `pg-ast` to your project, use the following npm command:

```bash
npm install pg-ast
```

## Usage

### AST Node Creation

With the AST helper methods, creating complex SQL ASTs becomes straightforward and intuitive.

#### JSON AST

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:

```ts
import * as t from 'pg-ast';
import { SelectStmt } from '@pgsql/types';
import { deparse } from 'pgsql-deparser';

const selectStmt: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
targetList: [
t.nodes.resTarget({
val: t.nodes.columnRef({
fields: [t.nodes.aStar()]
})
})
],
fromClause: [
t.nodes.rangeVar({
relname: 'some_amazing_table',
inh: true,
relpersistence: 'p'
})
],
limitOption: 'LIMIT_OPTION_DEFAULT',
op: 'SETOP_NONE'
});
console.log(selectStmt);
// 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" } }
console.log(await deparse(stmt))
// Output: SELECT * FROM some_amazing_table
```

#### Select Statement

```ts
import * as t from 'pg-ast';
import { SelectStmt } from '@pgsql/types';
import { deparse } from 'pgsql-deparser';

const query: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
targetList: [
t.nodes.resTarget({
val: t.nodes.columnRef({
fields: [t.nodes.string({ sval: 'name' })]
})
}),
t.nodes.resTarget({
val: t.nodes.columnRef({
fields: [t.nodes.string({ sval: 'email' })]
})
})
],
fromClause: [
t.nodes.rangeVar({
relname: 'users',
inh: true,
relpersistence: 'p'
})
],
whereClause: t.nodes.aExpr({
kind: 'AEXPR_OP',
name: [t.nodes.string({ sval: '>' })],
lexpr: t.nodes.columnRef({
fields: [t.nodes.string({ sval: 'age' })]
}),
rexpr: t.nodes.aConst({
ival: t.ast.integer({ ival: 18 })
})
}),
limitOption: 'LIMIT_OPTION_DEFAULT',
op: 'SETOP_NONE'
});

await deparse(createStmt);
// SELECT name, email FROM users WHERE age > 18
```

#### Creating Table Schemas Dynamically

```ts
// Example JSON schema
const schema = {
"tableName": "users",
"columns": [
{ "name": "id", "type": "int", "constraints": ["PRIMARY KEY"] },
{ "name": "username", "type": "text" },
{ "name": "email", "type": "text", "constraints": ["UNIQUE"] },
{ "name": "created_at", "type": "timestamp", "constraints": ["NOT NULL"] }
]
};

// Construct the CREATE TABLE statement
const createStmt = t.nodes.createStmt({
relation: t.ast.rangeVar({
relname: schema.tableName,
inh: true,
relpersistence: 'p'
}),
tableElts: schema.columns.map(column => t.nodes.columnDef({
colname: column.name,
typeName: t.ast.typeName({
names: [t.nodes.string({ sval: column.type })]
}),
constraints: column.constraints?.map(constraint =>
t.nodes.constraint({
contype: constraint === "PRIMARY KEY" ? "CONSTR_PRIMARY" : constraint === "UNIQUE" ? "CONSTR_UNIQUE" : "CONSTR_NOTNULL"
})
)
}))
});

// `deparse` function converts AST to SQL string
const sql = await deparse(createStmt, { pretty: true });

console.log(sql);
// OUTPUT:

// CREATE TABLE users (
// id int PRIMARY KEY,
// username text,
// email text UNIQUE,
// created_at timestamp NOT NULL
// )
```
38 changes: 38 additions & 0 deletions packages/pg-ast/__test__/__snapshots__/utils.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`dynamic creation of tables 1`] = `"CREATE TABLE users (id int PRIMARY KEY, username text, email text UNIQUE, created_at timestamp NOT NULL)"`;

exports[`simple SelectStmt 1`] = `
{
"SelectStmt": {
"fromClause": [
{
"RangeVar": {
"inh": true,
"relname": "another_table",
"relpersistence": "p",
},
},
],
"limitOption": "LIMIT_OPTION_DEFAULT",
"op": "SETOP_NONE",
"targetList": [
{
"ResTarget": {
"val": {
"ColumnRef": {
"fields": [
{
"A_Star": {},
},
],
},
},
},
},
],
},
}
`;

exports[`simple SelectStmt 2`] = `"SELECT * FROM another_table"`;
147 changes: 147 additions & 0 deletions packages/pg-ast/__test__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import * as t from '../src';
import { RangeVar, SelectStmt } from '@pgsql/types';
import { deparseSync as deparse } from 'pgsql-deparser';

it('simple SelectStmt', () => {
const stmt: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
targetList: [
t.nodes.resTarget({
val: t.nodes.columnRef({
fields: [t.nodes.aStar()]
})
})
],
fromClause: [
t.nodes.rangeVar({
relname: 'some_table',
inh: true,
relpersistence: 'p'
})
],
limitOption: 'LIMIT_OPTION_DEFAULT',
op: 'SETOP_NONE'
});

(stmt.SelectStmt.fromClause[0] as {RangeVar: RangeVar}).RangeVar.relname = 'another_table';

expect(stmt).toMatchSnapshot();
expect(deparse(stmt, { pretty: false })).toMatchSnapshot();
});

it('SelectStmt with WHERE clause', () => {
const selectStmt: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
targetList: [
t.nodes.resTarget({
val: t.nodes.columnRef({
fields: [t.nodes.aStar()]
})
})
],
fromClause: [
t.nodes.rangeVar({
schemaname: 'myschema',
relname: 'mytable',
inh: true,
relpersistence: 'p'
})
],
whereClause: t.nodes.aExpr({
kind: 'AEXPR_OP',
name: [t.nodes.string({ sval: '=' })],
lexpr: t.nodes.columnRef({
fields: [t.nodes.string({ sval: 'a' })]
}),
rexpr: t.nodes.typeCast({
arg: t.nodes.aConst({
sval: t.ast.string({ sval: 't' })
}),
typeName: t.ast.typeName({
names: [
t.nodes.string({ sval: 'pg_catalog' }),
t.nodes.string({ sval: 'bool' })
],
typemod: -1
})
})
}),
limitOption: 'LIMIT_OPTION_DEFAULT',
op: 'SETOP_NONE'
});

expect(deparse(selectStmt, { pretty: false })).toEqual(`SELECT * FROM myschema.mytable WHERE a = CAST('t' AS boolean)`);
});

it('queries', () => {
const query: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
targetList: [
t.nodes.resTarget({
val: t.nodes.columnRef({
fields: [t.nodes.string({ sval: 'name' })]
})
}),
t.nodes.resTarget({
val: t.nodes.columnRef({
fields: [t.nodes.string({ sval: 'email' })]
})
})
],
fromClause: [
t.nodes.rangeVar({
relname: 'users',
inh: true,
relpersistence: 'p'
})
],
whereClause: t.nodes.aExpr({
kind: 'AEXPR_OP',
name: [t.nodes.string({ sval: '>' })],
lexpr: t.nodes.columnRef({
fields: [t.nodes.string({ sval: 'age' })]
}),
rexpr: t.nodes.aConst({
ival: t.ast.integer({ ival: 18 })
})
}),
limitOption: 'LIMIT_OPTION_DEFAULT',
op: 'SETOP_NONE'
});

expect(deparse(query, { pretty: false })).toEqual(`SELECT name, email FROM users WHERE age > 18`);

});
it('dynamic creation of tables', () => {
// Example JSON schema
const schema = {
"tableName": "users",
"columns": [
{ "name": "id", "type": "int", "constraints": ["PRIMARY KEY"] },
{ "name": "username", "type": "text" },
{ "name": "email", "type": "text", "constraints": ["UNIQUE"] },
{ "name": "created_at", "type": "timestamp", "constraints": ["NOT NULL"] }
]
};

// Construct the CREATE TABLE statement
const createStmt = t.nodes.createStmt({
relation: t.ast.rangeVar({
relname: schema.tableName,
inh: true,
relpersistence: 'p'
}),
tableElts: schema.columns.map(column => t.nodes.columnDef({
colname: column.name,
typeName: t.ast.typeName({
names: [t.nodes.string({ sval: column.type })]
}),
constraints: column.constraints?.map(constraint =>
t.nodes.constraint({
contype: constraint === "PRIMARY KEY" ? "CONSTR_PRIMARY" : constraint === "UNIQUE" ? "CONSTR_UNIQUE" : "CONSTR_NOTNULL"
})
)
}))
});

// `deparse` function converts AST to SQL string
const sql = deparse(createStmt, { pretty: false });
expect(sql).toMatchSnapshot();
})
18 changes: 18 additions & 0 deletions packages/pg-ast/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
transform: {
"^.+\\.tsx?$": [
"ts-jest",
{
babelConfig: false,
tsconfig: "tsconfig.json",
},
],
},
transformIgnorePatterns: [`/node_modules/*`],
testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
modulePathIgnorePatterns: ["dist/*"]
};
Loading