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
79 changes: 74 additions & 5 deletions DEPARSER_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,75 @@ const options = {
const sql = deparse(parseResult, options);
```

### Pretty Formatting Options

The deparser supports pretty formatting to make SQL output more readable with proper indentation and line breaks:

```typescript
const options = {
pretty: true, // Enable pretty formatting (default: false)
newline: '\n', // Newline character (default: '\n')
tab: ' ', // Tab/indentation character (default: ' ')
functionDelimiter: '$$', // Function body delimiter (default: '$$')
functionDelimiterFallback: '$EOFCODE$' // Fallback delimiter (default: '$EOFCODE$')
};

const sql = deparse(parseResult, options);
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `pretty` | `boolean` | `false` | Enable pretty formatting with indentation and line breaks |
| `newline` | `string` | `'\n'` | Character(s) used for line breaks |
| `tab` | `string` | `' '` | Character(s) used for indentation (2 spaces by default) |
| `functionDelimiter` | `string` | `'$$'` | Delimiter used for function bodies |
| `functionDelimiterFallback` | `string` | `'$EOFCODE$'` | Alternative delimiter when default is found in function body |

#### Pretty Formatting Examples

**Basic SELECT with pretty formatting:**
```typescript
// Without pretty formatting
const sql1 = deparse(selectAst, { pretty: false });
// Output: "SELECT id, name, email FROM users WHERE active = true;"

// With pretty formatting
const sql2 = deparse(selectAst, { pretty: true });
// Output:
// SELECT
// id,
// name,
// email
// FROM users
// WHERE
// active = true;
```

**Custom formatting characters:**
```typescript
const options = {
pretty: true,
newline: '\r\n', // Windows line endings
tab: ' ' // 4-space indentation
};

const sql = deparse(parseResult, options);
```

**Supported Statements:**
Pretty formatting is supported for:
- `SELECT` statements with proper clause alignment
- `CREATE TABLE` statements with column definitions
- `CREATE POLICY` statements with clause formatting
- Common Table Expressions (CTEs)
- Constraint definitions
- JOIN operations with proper alignment

**Important Notes:**
- Pretty formatting preserves SQL semantics - the formatted SQL parses to the same AST
- Multi-line string literals are preserved without indentation to maintain their content
- Complex expressions maintain proper parentheses and operator precedence

## Instance Usage

You can also create a deparser instance:
Expand Down Expand Up @@ -149,15 +218,15 @@ These ensure proper handling of different input formats automatically.
### Complete Example

```typescript
import deparse from 'pgsql-deparser';
import { deparse } from 'pgsql-deparser';
import { parse } from 'pgsql-parser';

// Parse SQL
const sql = 'SELECT * FROM users; INSERT INTO logs (action) VALUES ($1);';
const parseResult = parse(sql);
const parseResult = await parse(sql);

// Deparse back to SQL
const regeneratedSql = deparse(parseResult);
const regeneratedSql = await deparse(parseResult);
console.log(regeneratedSql);
// Output: "SELECT * FROM users;\n\nINSERT INTO logs (action) VALUES ($1);"
```
Expand All @@ -183,6 +252,6 @@ const customSelect = {
}
};

const sql = deparse(customSelect);
const sql = await deparse(customSelect);
// Output: "SELECT * FROM users"
```
```
18 changes: 7 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@
<a href="https://github.com/launchql/pgsql-parser/blob/main/LICENSE-MIT"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a>
</p>

## PostgreSQL AST Tools

## PostgreSQL Parsing, Deparsing & AST Tools

A comprehensive monorepo for PostgreSQL Abstract Syntax Tree (AST) parsing, manipulation, and code generation. This collection of packages provides everything you need to work with PostgreSQL at the AST level, from parsing SQL queries to generating type-safe TypeScript definitions.

## 📦 Packages Overview
## 📦 Packages

| Package | Description | Key Features |
|---------|-------------|--------------|
Expand All @@ -36,7 +35,7 @@ A comprehensive monorepo for PostgreSQL Abstract Syntax Tree (AST) parsing, mani
Choose the packages you need:

```bash
# For parsing SQL to AST and back
# For parsing SQL to AST and back (includes deparser)
npm install pgsql-parser

# For only converting AST to SQL (lighter weight)
Expand Down Expand Up @@ -74,7 +73,7 @@ console.log(sql); // SELECT * FROM users WHERE id = 1
#### Build AST Programmatically
```typescript
import * as t from '@pgsql/utils';
import { RangeVar, SelectStmt } from '@pgsql/types';
import { SelectStmt } from '@pgsql/types';

const stmt: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
targetList: [
Expand Down Expand Up @@ -127,9 +126,6 @@ yarn install

# Build all packages
yarn build

# Run tests
yarn test
```

### Building Individual Packages
Expand Down Expand Up @@ -163,15 +159,15 @@ const ast = await parse('SELECT * FROM users WHERE active = true');
ast[0].RawStmt.stmt.SelectStmt.fromClause[0].RangeVar.relname = 'customers';

// Generate the modified SQL
const newSql = deparse(ast);
const newSql = await deparse(ast);
console.log(newSql); // SELECT * FROM customers WHERE active = TRUE
```

### Build a Query Programmatically

```typescript
import ast from '@pgsql/utils';
import { deparse as deparseSync } from 'pgsql-deparser';
import { deparse } from 'pgsql-deparser';

const query: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
targetList: [
Expand Down Expand Up @@ -207,7 +203,7 @@ const query: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
op: 'SETOP_NONE'
});

console.log(deparse(query));
console.log(await deparse(query));
// SELECT name, email FROM users WHERE age > 18
```

Expand Down
11 changes: 0 additions & 11 deletions TESTS.md

This file was deleted.

19 changes: 19 additions & 0 deletions __fixtures__/generated/generated.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
{
"pretty/select_statements-1.sql": "SELECT id, name, email FROM users WHERE active = true",
"pretty/select_statements-2.sql": "SELECT \n u.id,\n u.name,\n u.email,\n p.title as profile_title\nFROM users u\nJOIN profiles p ON u.id = p.user_id\nWHERE u.active = true\n AND u.created_at > '2023-01-01'\nGROUP BY u.id, u.name, u.email, p.title\nHAVING COUNT(*) > 1\nORDER BY u.created_at DESC, u.name ASC\nLIMIT 10\nOFFSET 5",
"pretty/select_statements-3.sql": "SELECT id, name FROM users WHERE id IN (\n SELECT user_id FROM orders WHERE total > 100\n)",
"pretty/select_statements-4.sql": "SELECT name FROM customers\nUNION ALL\nSELECT name FROM suppliers\nORDER BY name",
"pretty/select_statements-5.sql": "SELECT name, email FROM users WHERE status = 'active'",
"pretty/select_statements-6.sql": "SELECT u.name, o.total FROM users u, orders o WHERE u.id = o.user_id",
"pretty/create_table-1.sql": "CREATE TABLE users (\n id SERIAL PRIMARY KEY,\n name TEXT NOT NULL,\n email TEXT UNIQUE\n)",
"pretty/create_table-2.sql": "CREATE TABLE products (\n id SERIAL PRIMARY KEY,\n name VARCHAR(255) NOT NULL,\n price DECIMAL(10,2) CHECK (price > 0),\n category_id INTEGER,\n description TEXT,\n created_at TIMESTAMP DEFAULT now(),\n updated_at TIMESTAMP,\n UNIQUE (name, category_id),\n FOREIGN KEY (category_id) REFERENCES categories(id)\n)",
"pretty/create_table-3.sql": "CREATE TABLE orders (\n id SERIAL PRIMARY KEY,\n subtotal DECIMAL(10,2) NOT NULL,\n tax_rate DECIMAL(5,4) DEFAULT 0.0825,\n tax_amount DECIMAL(10,2) GENERATED ALWAYS AS (subtotal * tax_rate) STORED,\n total DECIMAL(10,2) GENERATED ALWAYS AS (subtotal + tax_amount) STORED\n)",
"pretty/create_table-4.sql": "CREATE TABLE sales (\n id SERIAL,\n sale_date DATE NOT NULL,\n amount DECIMAL(10,2),\n region VARCHAR(50)\n) PARTITION BY RANGE (sale_date)",
"pretty/create_table-5.sql": "CREATE TEMPORARY TABLE temp_calculations (\n id INTEGER,\n value DECIMAL(15,5),\n result TEXT\n)",
"pretty/create_policy-1.sql": "CREATE POLICY user_policy ON users FOR ALL TO authenticated_users USING (user_id = current_user_id())",
"pretty/create_policy-2.sql": "CREATE POLICY admin_policy ON sensitive_data \n AS RESTRICTIVE \n FOR SELECT \n TO admin_role \n USING (department = current_user_department()) \n WITH CHECK (approved = true)",
"pretty/create_policy-3.sql": "CREATE POLICY complex_policy ON documents \n FOR UPDATE \n TO document_editors \n USING (\n owner_id = current_user_id() OR \n (shared = true AND permissions @> '{\"edit\": true}')\n ) \n WITH CHECK (\n status != 'archived' AND \n last_modified > now() - interval '1 day'\n )",
"pretty/create_policy-4.sql": "CREATE POLICY simple_policy ON posts FOR SELECT TO public USING (published = true)",
"pretty/constraints-1.sql": "CREATE TABLE orders (\n id SERIAL PRIMARY KEY,\n user_id INTEGER NOT NULL,\n total DECIMAL(10,2) CHECK (total > 0),\n status VARCHAR(20) DEFAULT 'pending',\n created_at TIMESTAMP DEFAULT now(),\n CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,\n CONSTRAINT unique_user_date UNIQUE (user_id, created_at),\n CONSTRAINT check_status CHECK (status IN ('pending', 'completed', 'cancelled'))\n)",
"pretty/constraints-2.sql": "ALTER TABLE products ADD CONSTRAINT fk_category \n FOREIGN KEY (category_id) \n REFERENCES categories(id) \n ON UPDATE CASCADE \n ON DELETE SET NULL \n DEFERRABLE INITIALLY DEFERRED",
"pretty/constraints-3.sql": "ALTER TABLE products ADD CONSTRAINT check_price CHECK (price > 0)",
"pretty/constraints-4.sql": "ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email)",
"original/simple-1.sql": "SELECT\n *\nFROM\n table_name\nWHERE\n name = 'test' AND num > 7 AND\n last_name LIKE '%''test''%'",
"original/simple-2.sql": "SELECT\n *\nFROM\n table_name\nWHERE\n name = 'test' AND num > 7 AND\n last_name NOT LIKE '%''test''%'",
"original/simple-3.sql": "SELECT\n *\nFROM\n table_name\nWHERE\n name = 'test' AND num > 7 AND\n last_name ILIKE '%''test''%'",
Expand Down
21 changes: 21 additions & 0 deletions __fixtures__/kitchen-sink/pretty/constraints.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
total DECIMAL(10,2) CHECK (total > 0),
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT now(),
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT unique_user_date UNIQUE (user_id, created_at),
CONSTRAINT check_status CHECK (status IN ('pending', 'completed', 'cancelled'))
);

ALTER TABLE products ADD CONSTRAINT fk_category
FOREIGN KEY (category_id)
REFERENCES categories(id)
ON UPDATE CASCADE
ON DELETE SET NULL
DEFERRABLE INITIALLY DEFERRED;

ALTER TABLE products ADD CONSTRAINT check_price CHECK (price > 0);

ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email);
22 changes: 22 additions & 0 deletions __fixtures__/kitchen-sink/pretty/create_policy.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
CREATE POLICY user_policy ON users FOR ALL TO authenticated_users USING (user_id = current_user_id());

CREATE POLICY admin_policy ON sensitive_data
AS RESTRICTIVE
FOR SELECT
TO admin_role
USING (department = current_user_department())
WITH CHECK (approved = true);

CREATE POLICY complex_policy ON documents
FOR UPDATE
TO document_editors
USING (
owner_id = current_user_id() OR
(shared = true AND permissions @> '{"edit": true}')
)
WITH CHECK (
status != 'archived' AND
last_modified > now() - interval '1 day'
);

CREATE POLICY simple_policy ON posts FOR SELECT TO public USING (published = true);
39 changes: 39 additions & 0 deletions __fixtures__/kitchen-sink/pretty/create_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE
);

CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10,2) CHECK (price > 0),
category_id INTEGER,
description TEXT,
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP,
UNIQUE (name, category_id),
FOREIGN KEY (category_id) REFERENCES categories(id)
);

CREATE TABLE orders (
id SERIAL PRIMARY KEY,
subtotal DECIMAL(10,2) NOT NULL,
tax_rate DECIMAL(5,4) DEFAULT 0.0825,
tax_amount DECIMAL(10,2) GENERATED ALWAYS AS (subtotal * tax_rate) STORED,
total DECIMAL(10,2) GENERATED ALWAYS AS (subtotal + tax_amount) STORED
);

CREATE TABLE sales (
id SERIAL,
sale_date DATE NOT NULL,
amount DECIMAL(10,2),
region VARCHAR(50)
) PARTITION BY RANGE (sale_date);

CREATE TEMPORARY TABLE temp_calculations (
id INTEGER,
value DECIMAL(15,5),
result TEXT
);
29 changes: 29 additions & 0 deletions __fixtures__/kitchen-sink/pretty/select_statements.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
SELECT id, name, email FROM users WHERE active = true;

SELECT
u.id,
u.name,
u.email,
p.title as profile_title
FROM users u
JOIN profiles p ON u.id = p.user_id
WHERE u.active = true
AND u.created_at > '2023-01-01'
GROUP BY u.id, u.name, u.email, p.title
HAVING COUNT(*) > 1
ORDER BY u.created_at DESC, u.name ASC
LIMIT 10
OFFSET 5;

SELECT id, name FROM users WHERE id IN (
SELECT user_id FROM orders WHERE total > 100
);

SELECT name FROM customers
UNION ALL
SELECT name FROM suppliers
ORDER BY name;

SELECT name, email FROM users WHERE status = 'active';

SELECT u.name, o.total FROM users u, orders o WHERE u.id = o.user_id;
8 changes: 8 additions & 0 deletions packages/deparser/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

## [17.7.2](https://github.com/launchql/pgsql-parser/compare/[email protected]@17.7.2) (2025-06-23)

**Note:** Version bump only for package pgsql-deparser





## [17.7.1](https://github.com/launchql/pgsql-parser/compare/[email protected]@17.7.1) (2025-06-23)

**Note:** Version bump only for package pgsql-deparser
Expand Down
Loading