Skip to content

Commit 797d5db

Browse files
authored
Merge pull request #153 from launchql/feat/pretty
Feat/pretty
2 parents 61342c5 + 26b01e3 commit 797d5db

28 files changed

+1263
-129
lines changed

DEPARSER_USAGE.md

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,75 @@ const options = {
108108
const sql = deparse(parseResult, options);
109109
```
110110

111+
### Pretty Formatting Options
112+
113+
The deparser supports pretty formatting to make SQL output more readable with proper indentation and line breaks:
114+
115+
```typescript
116+
const options = {
117+
pretty: true, // Enable pretty formatting (default: false)
118+
newline: '\n', // Newline character (default: '\n')
119+
tab: ' ', // Tab/indentation character (default: ' ')
120+
functionDelimiter: '$$', // Function body delimiter (default: '$$')
121+
functionDelimiterFallback: '$EOFCODE$' // Fallback delimiter (default: '$EOFCODE$')
122+
};
123+
124+
const sql = deparse(parseResult, options);
125+
```
126+
127+
| Option | Type | Default | Description |
128+
|--------|------|---------|-------------|
129+
| `pretty` | `boolean` | `false` | Enable pretty formatting with indentation and line breaks |
130+
| `newline` | `string` | `'\n'` | Character(s) used for line breaks |
131+
| `tab` | `string` | `' '` | Character(s) used for indentation (2 spaces by default) |
132+
| `functionDelimiter` | `string` | `'$$'` | Delimiter used for function bodies |
133+
| `functionDelimiterFallback` | `string` | `'$EOFCODE$'` | Alternative delimiter when default is found in function body |
134+
135+
#### Pretty Formatting Examples
136+
137+
**Basic SELECT with pretty formatting:**
138+
```typescript
139+
// Without pretty formatting
140+
const sql1 = deparse(selectAst, { pretty: false });
141+
// Output: "SELECT id, name, email FROM users WHERE active = true;"
142+
143+
// With pretty formatting
144+
const sql2 = deparse(selectAst, { pretty: true });
145+
// Output:
146+
// SELECT
147+
// id,
148+
// name,
149+
// email
150+
// FROM users
151+
// WHERE
152+
// active = true;
153+
```
154+
155+
**Custom formatting characters:**
156+
```typescript
157+
const options = {
158+
pretty: true,
159+
newline: '\r\n', // Windows line endings
160+
tab: ' ' // 4-space indentation
161+
};
162+
163+
const sql = deparse(parseResult, options);
164+
```
165+
166+
**Supported Statements:**
167+
Pretty formatting is supported for:
168+
- `SELECT` statements with proper clause alignment
169+
- `CREATE TABLE` statements with column definitions
170+
- `CREATE POLICY` statements with clause formatting
171+
- Common Table Expressions (CTEs)
172+
- Constraint definitions
173+
- JOIN operations with proper alignment
174+
175+
**Important Notes:**
176+
- Pretty formatting preserves SQL semantics - the formatted SQL parses to the same AST
177+
- Multi-line string literals are preserved without indentation to maintain their content
178+
- Complex expressions maintain proper parentheses and operator precedence
179+
111180
## Instance Usage
112181

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

151220
```typescript
152-
import deparse from 'pgsql-deparser';
221+
import { deparse } from 'pgsql-deparser';
153222
import { parse } from 'pgsql-parser';
154223

155224
// Parse SQL
156225
const sql = 'SELECT * FROM users; INSERT INTO logs (action) VALUES ($1);';
157-
const parseResult = parse(sql);
226+
const parseResult = await parse(sql);
158227

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

186-
const sql = deparse(customSelect);
255+
const sql = await deparse(customSelect);
187256
// Output: "SELECT * FROM users"
188-
```
257+
```

README.md

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,11 @@
1414
<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>
1515
</p>
1616

17-
## PostgreSQL AST Tools
18-
17+
## PostgreSQL Parsing, Deparsing & AST Tools
1918

2019
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.
2120

22-
## 📦 Packages Overview
21+
## 📦 Packages
2322

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

3837
```bash
39-
# For parsing SQL to AST and back
38+
# For parsing SQL to AST and back (includes deparser)
4039
npm install pgsql-parser
4140

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

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

128127
# Build all packages
129128
yarn build
130-
131-
# Run tests
132-
yarn test
133129
```
134130

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

165161
// Generate the modified SQL
166-
const newSql = deparse(ast);
162+
const newSql = await deparse(ast);
167163
console.log(newSql); // SELECT * FROM customers WHERE active = TRUE
168164
```
169165

170166
### Build a Query Programmatically
171167

172168
```typescript
173169
import ast from '@pgsql/utils';
174-
import { deparse as deparseSync } from 'pgsql-deparser';
170+
import { deparse } from 'pgsql-deparser';
175171

176172
const query: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
177173
targetList: [
@@ -207,7 +203,7 @@ const query: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
207203
op: 'SETOP_NONE'
208204
});
209205

210-
console.log(deparse(query));
206+
console.log(await deparse(query));
211207
// SELECT name, email FROM users WHERE age > 18
212208
```
213209

TESTS.md

Lines changed: 0 additions & 11 deletions
This file was deleted.

__fixtures__/generated/generated.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,23 @@
11
{
2+
"pretty/select_statements-1.sql": "SELECT id, name, email FROM users WHERE active = true",
3+
"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",
4+
"pretty/select_statements-3.sql": "SELECT id, name FROM users WHERE id IN (\n SELECT user_id FROM orders WHERE total > 100\n)",
5+
"pretty/select_statements-4.sql": "SELECT name FROM customers\nUNION ALL\nSELECT name FROM suppliers\nORDER BY name",
6+
"pretty/select_statements-5.sql": "SELECT name, email FROM users WHERE status = 'active'",
7+
"pretty/select_statements-6.sql": "SELECT u.name, o.total FROM users u, orders o WHERE u.id = o.user_id",
8+
"pretty/create_table-1.sql": "CREATE TABLE users (\n id SERIAL PRIMARY KEY,\n name TEXT NOT NULL,\n email TEXT UNIQUE\n)",
9+
"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)",
10+
"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)",
11+
"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)",
12+
"pretty/create_table-5.sql": "CREATE TEMPORARY TABLE temp_calculations (\n id INTEGER,\n value DECIMAL(15,5),\n result TEXT\n)",
13+
"pretty/create_policy-1.sql": "CREATE POLICY user_policy ON users FOR ALL TO authenticated_users USING (user_id = current_user_id())",
14+
"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)",
15+
"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 )",
16+
"pretty/create_policy-4.sql": "CREATE POLICY simple_policy ON posts FOR SELECT TO public USING (published = true)",
17+
"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)",
18+
"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",
19+
"pretty/constraints-3.sql": "ALTER TABLE products ADD CONSTRAINT check_price CHECK (price > 0)",
20+
"pretty/constraints-4.sql": "ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email)",
221
"original/simple-1.sql": "SELECT\n *\nFROM\n table_name\nWHERE\n name = 'test' AND num > 7 AND\n last_name LIKE '%''test''%'",
322
"original/simple-2.sql": "SELECT\n *\nFROM\n table_name\nWHERE\n name = 'test' AND num > 7 AND\n last_name NOT LIKE '%''test''%'",
423
"original/simple-3.sql": "SELECT\n *\nFROM\n table_name\nWHERE\n name = 'test' AND num > 7 AND\n last_name ILIKE '%''test''%'",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
CREATE TABLE orders (
2+
id SERIAL PRIMARY KEY,
3+
user_id INTEGER NOT NULL,
4+
total DECIMAL(10,2) CHECK (total > 0),
5+
status VARCHAR(20) DEFAULT 'pending',
6+
created_at TIMESTAMP DEFAULT now(),
7+
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
8+
CONSTRAINT unique_user_date UNIQUE (user_id, created_at),
9+
CONSTRAINT check_status CHECK (status IN ('pending', 'completed', 'cancelled'))
10+
);
11+
12+
ALTER TABLE products ADD CONSTRAINT fk_category
13+
FOREIGN KEY (category_id)
14+
REFERENCES categories(id)
15+
ON UPDATE CASCADE
16+
ON DELETE SET NULL
17+
DEFERRABLE INITIALLY DEFERRED;
18+
19+
ALTER TABLE products ADD CONSTRAINT check_price CHECK (price > 0);
20+
21+
ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
CREATE POLICY user_policy ON users FOR ALL TO authenticated_users USING (user_id = current_user_id());
2+
3+
CREATE POLICY admin_policy ON sensitive_data
4+
AS RESTRICTIVE
5+
FOR SELECT
6+
TO admin_role
7+
USING (department = current_user_department())
8+
WITH CHECK (approved = true);
9+
10+
CREATE POLICY complex_policy ON documents
11+
FOR UPDATE
12+
TO document_editors
13+
USING (
14+
owner_id = current_user_id() OR
15+
(shared = true AND permissions @> '{"edit": true}')
16+
)
17+
WITH CHECK (
18+
status != 'archived' AND
19+
last_modified > now() - interval '1 day'
20+
);
21+
22+
CREATE POLICY simple_policy ON posts FOR SELECT TO public USING (published = true);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
2+
CREATE TABLE users (
3+
id SERIAL PRIMARY KEY,
4+
name TEXT NOT NULL,
5+
email TEXT UNIQUE
6+
);
7+
8+
CREATE TABLE products (
9+
id SERIAL PRIMARY KEY,
10+
name VARCHAR(255) NOT NULL,
11+
price DECIMAL(10,2) CHECK (price > 0),
12+
category_id INTEGER,
13+
description TEXT,
14+
created_at TIMESTAMP DEFAULT now(),
15+
updated_at TIMESTAMP,
16+
UNIQUE (name, category_id),
17+
FOREIGN KEY (category_id) REFERENCES categories(id)
18+
);
19+
20+
CREATE TABLE orders (
21+
id SERIAL PRIMARY KEY,
22+
subtotal DECIMAL(10,2) NOT NULL,
23+
tax_rate DECIMAL(5,4) DEFAULT 0.0825,
24+
tax_amount DECIMAL(10,2) GENERATED ALWAYS AS (subtotal * tax_rate) STORED,
25+
total DECIMAL(10,2) GENERATED ALWAYS AS (subtotal + tax_amount) STORED
26+
);
27+
28+
CREATE TABLE sales (
29+
id SERIAL,
30+
sale_date DATE NOT NULL,
31+
amount DECIMAL(10,2),
32+
region VARCHAR(50)
33+
) PARTITION BY RANGE (sale_date);
34+
35+
CREATE TEMPORARY TABLE temp_calculations (
36+
id INTEGER,
37+
value DECIMAL(15,5),
38+
result TEXT
39+
);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
SELECT id, name, email FROM users WHERE active = true;
2+
3+
SELECT
4+
u.id,
5+
u.name,
6+
u.email,
7+
p.title as profile_title
8+
FROM users u
9+
JOIN profiles p ON u.id = p.user_id
10+
WHERE u.active = true
11+
AND u.created_at > '2023-01-01'
12+
GROUP BY u.id, u.name, u.email, p.title
13+
HAVING COUNT(*) > 1
14+
ORDER BY u.created_at DESC, u.name ASC
15+
LIMIT 10
16+
OFFSET 5;
17+
18+
SELECT id, name FROM users WHERE id IN (
19+
SELECT user_id FROM orders WHERE total > 100
20+
);
21+
22+
SELECT name FROM customers
23+
UNION ALL
24+
SELECT name FROM suppliers
25+
ORDER BY name;
26+
27+
SELECT name, email FROM users WHERE status = 'active';
28+
29+
SELECT u.name, o.total FROM users u, orders o WHERE u.id = o.user_id;

packages/deparser/README.md

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ npm install pgsql-deparser
2424

2525
## Features
2626

27-
***Pure TypeScript Performance** – Zero dependencies, no WASM, no compilation - just blazing fast SQL generation
27+
***Pure TypeScript Performance** – Zero runtime dependencies, no WASM, no compilation - just blazing fast SQL generation
2828
* 🪶 **Ultra Lightweight** – Minimal footprint with laser-focused functionality for AST-to-SQL conversion only
2929
* 🧪 **Battle-Tested Reliability** – Validated against 23,000+ SQL statements ensuring production-grade stability
3030
* 🌍 **Universal Compatibility** – Runs anywhere JavaScript does - browsers, Node.js, edge functions, you name it
@@ -69,6 +69,48 @@ console.log(deparse(stmt));
6969
// Output: SELECT * FROM another_table
7070
```
7171

72+
## Options
73+
74+
The deparser accepts optional configuration for formatting and output control:
75+
76+
```ts
77+
import { deparseSync as deparse } from 'pgsql-deparser';
78+
79+
const options = {
80+
pretty: true, // Enable pretty formatting (default: false)
81+
newline: '\n', // Newline character (default: '\n')
82+
tab: ' ', // Tab/indentation character (default: ' ')
83+
semicolons: true // Add semicolons to statements (default: true)
84+
};
85+
86+
const sql = deparse(ast, options);
87+
```
88+
89+
| Option | Type | Default | Description |
90+
|--------|------|---------|-------------|
91+
| `pretty` | `boolean` | `false` | Enable pretty formatting with indentation and line breaks |
92+
| `newline` | `string` | `'\n'` | Character(s) used for line breaks |
93+
| `tab` | `string` | `' '` | Character(s) used for indentation |
94+
| `semicolons` | `boolean` | `true` | Add semicolons to SQL statements |
95+
96+
**Pretty formatting example:**
97+
```ts
98+
// Without pretty formatting
99+
const sql1 = deparse(selectAst, { pretty: false });
100+
// "SELECT id, name FROM users WHERE active = true;"
101+
102+
// With pretty formatting
103+
const sql2 = deparse(selectAst, { pretty: true });
104+
// SELECT
105+
// id,
106+
// name
107+
// FROM users
108+
// WHERE
109+
// active = true;
110+
```
111+
112+
For complete documentation and advanced options, see [DEPARSER_USAGE.md](../../DEPARSER_USAGE.md).
113+
72114
## Why Use `pgsql-deparser`?
73115

74116
`pgsql-deparser` is particularly useful in development environments where native dependencies are problematic or in applications where only the deparser functionality is required. Its independence from the full `pgsql-parser` package allows for more focused and lightweight SQL generation tasks.
@@ -98,4 +140,4 @@ Built on the excellent work of several contributors:
98140

99141
AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
100142

101-
No developer or entity involved in creating Software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the Software code or Software CLI, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
143+
No developer or entity involved in creating Software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the Software code or Software CLI, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.

0 commit comments

Comments
 (0)