diff --git a/DEPARSER_USAGE.md b/DEPARSER_USAGE.md
index a6ac6f01..70022f0d 100644
--- a/DEPARSER_USAGE.md
+++ b/DEPARSER_USAGE.md
@@ -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:
@@ -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);"
```
@@ -183,6 +252,6 @@ const customSelect = {
}
};
-const sql = deparse(customSelect);
+const sql = await deparse(customSelect);
// Output: "SELECT * FROM users"
-```
\ No newline at end of file
+```
diff --git a/README.md b/README.md
index fb117eeb..51e95e0f 100644
--- a/README.md
+++ b/README.md
@@ -14,12 +14,11 @@
-## 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 |
|---------|-------------|--------------|
@@ -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)
@@ -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: [
@@ -127,9 +126,6 @@ yarn install
# Build all packages
yarn build
-
-# Run tests
-yarn test
```
### Building Individual Packages
@@ -163,7 +159,7 @@ 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
```
@@ -171,7 +167,7 @@ console.log(newSql); // SELECT * FROM customers WHERE active = TRUE
```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: [
@@ -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
```
diff --git a/TESTS.md b/TESTS.md
deleted file mode 100644
index b9cb7869..00000000
--- a/TESTS.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# Test Results
-
-## Deparser Tests
-
-**254/254 tests passing (100%)**
-
-All deparser tests are now passing successfully, including the PostgreSQL 17 features:
-- GENERATED BY DEFAULT AS IDENTITY columns
-- UNIQUE NULLS NOT DISTINCT constraints
-
-The deparser has been updated to properly handle these new PostgreSQL 17 syntax features.
diff --git a/__fixtures__/generated/generated.json b/__fixtures__/generated/generated.json
index 16bbb3cc..dacf3324 100644
--- a/__fixtures__/generated/generated.json
+++ b/__fixtures__/generated/generated.json
@@ -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''%'",
diff --git a/__fixtures__/kitchen-sink/pretty/constraints.sql b/__fixtures__/kitchen-sink/pretty/constraints.sql
new file mode 100644
index 00000000..1ba8adee
--- /dev/null
+++ b/__fixtures__/kitchen-sink/pretty/constraints.sql
@@ -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);
diff --git a/__fixtures__/kitchen-sink/pretty/create_policy.sql b/__fixtures__/kitchen-sink/pretty/create_policy.sql
new file mode 100644
index 00000000..57f4f788
--- /dev/null
+++ b/__fixtures__/kitchen-sink/pretty/create_policy.sql
@@ -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);
diff --git a/__fixtures__/kitchen-sink/pretty/create_table.sql b/__fixtures__/kitchen-sink/pretty/create_table.sql
new file mode 100644
index 00000000..cbfff60c
--- /dev/null
+++ b/__fixtures__/kitchen-sink/pretty/create_table.sql
@@ -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
+);
diff --git a/__fixtures__/kitchen-sink/pretty/select_statements.sql b/__fixtures__/kitchen-sink/pretty/select_statements.sql
new file mode 100644
index 00000000..3d215de6
--- /dev/null
+++ b/__fixtures__/kitchen-sink/pretty/select_statements.sql
@@ -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;
diff --git a/packages/deparser/CHANGELOG.md b/packages/deparser/CHANGELOG.md
index 12bdd35f..47c9bcb8 100644
--- a/packages/deparser/CHANGELOG.md
+++ b/packages/deparser/CHANGELOG.md
@@ -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/pgsql-deparser@17.7.1...pgsql-deparser@17.7.2) (2025-06-23)
+
+**Note:** Version bump only for package pgsql-deparser
+
+
+
+
+
## [17.7.1](https://github.com/launchql/pgsql-parser/compare/pgsql-deparser@17.7.0...pgsql-deparser@17.7.1) (2025-06-23)
**Note:** Version bump only for package pgsql-deparser
diff --git a/packages/deparser/README.md b/packages/deparser/README.md
index 2d044d1c..b67e9d64 100644
--- a/packages/deparser/README.md
+++ b/packages/deparser/README.md
@@ -24,7 +24,7 @@ npm install pgsql-deparser
## Features
-* โก **Pure TypeScript Performance** โ Zero dependencies, no WASM, no compilation - just blazing fast SQL generation
+* โก **Pure TypeScript Performance** โ Zero runtime dependencies, no WASM, no compilation - just blazing fast SQL generation
* ๐ชถ **Ultra Lightweight** โ Minimal footprint with laser-focused functionality for AST-to-SQL conversion only
* ๐งช **Battle-Tested Reliability** โ Validated against 23,000+ SQL statements ensuring production-grade stability
* ๐ **Universal Compatibility** โ Runs anywhere JavaScript does - browsers, Node.js, edge functions, you name it
@@ -69,6 +69,48 @@ console.log(deparse(stmt));
// Output: SELECT * FROM another_table
```
+## Options
+
+The deparser accepts optional configuration for formatting and output control:
+
+```ts
+import { deparseSync as deparse } from 'pgsql-deparser';
+
+const options = {
+ pretty: true, // Enable pretty formatting (default: false)
+ newline: '\n', // Newline character (default: '\n')
+ tab: ' ', // Tab/indentation character (default: ' ')
+ semicolons: true // Add semicolons to statements (default: true)
+};
+
+const sql = deparse(ast, 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 |
+| `semicolons` | `boolean` | `true` | Add semicolons to SQL statements |
+
+**Pretty formatting example:**
+```ts
+// Without pretty formatting
+const sql1 = deparse(selectAst, { pretty: false });
+// "SELECT id, name FROM users WHERE active = true;"
+
+// With pretty formatting
+const sql2 = deparse(selectAst, { pretty: true });
+// SELECT
+// id,
+// name
+// FROM users
+// WHERE
+// active = true;
+```
+
+For complete documentation and advanced options, see [DEPARSER_USAGE.md](../../DEPARSER_USAGE.md).
+
## Why Use `pgsql-deparser`?
`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:
AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
-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.
\ No newline at end of file
+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.
diff --git a/packages/deparser/__tests__/kitchen-sink/pretty-constraints.test.ts b/packages/deparser/__tests__/kitchen-sink/pretty-constraints.test.ts
new file mode 100644
index 00000000..059a839d
--- /dev/null
+++ b/packages/deparser/__tests__/kitchen-sink/pretty-constraints.test.ts
@@ -0,0 +1,12 @@
+
+import { FixtureTestUtils } from '../../test-utils';
+const fixtures = new FixtureTestUtils();
+
+it('pretty-constraints', async () => {
+ await fixtures.runFixtureTests([
+ "pretty/constraints-1.sql",
+ "pretty/constraints-2.sql",
+ "pretty/constraints-3.sql",
+ "pretty/constraints-4.sql"
+]);
+});
diff --git a/packages/deparser/__tests__/kitchen-sink/pretty-create_policy.test.ts b/packages/deparser/__tests__/kitchen-sink/pretty-create_policy.test.ts
new file mode 100644
index 00000000..31999ece
--- /dev/null
+++ b/packages/deparser/__tests__/kitchen-sink/pretty-create_policy.test.ts
@@ -0,0 +1,12 @@
+
+import { FixtureTestUtils } from '../../test-utils';
+const fixtures = new FixtureTestUtils();
+
+it('pretty-create_policy', async () => {
+ await fixtures.runFixtureTests([
+ "pretty/create_policy-1.sql",
+ "pretty/create_policy-2.sql",
+ "pretty/create_policy-3.sql",
+ "pretty/create_policy-4.sql"
+]);
+});
diff --git a/packages/deparser/__tests__/kitchen-sink/pretty-create_table.test.ts b/packages/deparser/__tests__/kitchen-sink/pretty-create_table.test.ts
new file mode 100644
index 00000000..50449dd4
--- /dev/null
+++ b/packages/deparser/__tests__/kitchen-sink/pretty-create_table.test.ts
@@ -0,0 +1,13 @@
+
+import { FixtureTestUtils } from '../../test-utils';
+const fixtures = new FixtureTestUtils();
+
+it('pretty-create_table', async () => {
+ await fixtures.runFixtureTests([
+ "pretty/create_table-1.sql",
+ "pretty/create_table-2.sql",
+ "pretty/create_table-3.sql",
+ "pretty/create_table-4.sql",
+ "pretty/create_table-5.sql"
+]);
+});
diff --git a/packages/deparser/__tests__/kitchen-sink/pretty-select_statements.test.ts b/packages/deparser/__tests__/kitchen-sink/pretty-select_statements.test.ts
new file mode 100644
index 00000000..c8a76114
--- /dev/null
+++ b/packages/deparser/__tests__/kitchen-sink/pretty-select_statements.test.ts
@@ -0,0 +1,14 @@
+
+import { FixtureTestUtils } from '../../test-utils';
+const fixtures = new FixtureTestUtils();
+
+it('pretty-select_statements', async () => {
+ await fixtures.runFixtureTests([
+ "pretty/select_statements-1.sql",
+ "pretty/select_statements-2.sql",
+ "pretty/select_statements-3.sql",
+ "pretty/select_statements-4.sql",
+ "pretty/select_statements-5.sql",
+ "pretty/select_statements-6.sql"
+]);
+});
diff --git a/packages/deparser/__tests__/pretty/__snapshots__/constraints-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/constraints-pretty.test.ts.snap
new file mode 100644
index 00000000..89615060
--- /dev/null
+++ b/packages/deparser/__tests__/pretty/__snapshots__/constraints-pretty.test.ts.snap
@@ -0,0 +1,34 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Pretty constraint formatting should format check constraint with pretty option enabled 1`] = `"ALTER TABLE products ADD CONSTRAINT check_price CHECK (price > 0);"`;
+
+exports[`Pretty constraint formatting should format complex table with constraints with pretty option enabled 1`] = `
+"CREATE TABLE orders (
+ id serial PRIMARY KEY,
+ user_id int NOT NULL,
+ total numeric(10, 2) CHECK (total > 0),
+ status varchar(20) DEFAULT 'pending',
+ CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id)
+ ON DELETE CASCADE
+);"
+`;
+
+exports[`Pretty constraint formatting should format foreign key constraint with pretty option enabled 1`] = `
+"ALTER TABLE products ADD CONSTRAINT fk_category FOREIGN KEY (category_id) REFERENCES categories (id)
+ ON UPDATE CASCADE
+ ON DELETE SET NULL
+ DEFERRABLE
+ INITIALLY DEFERRED;"
+`;
+
+exports[`Pretty constraint formatting should maintain single-line format for complex table when pretty disabled 1`] = `"CREATE TABLE orders (id serial PRIMARY KEY, user_id int NOT NULL, total numeric(10, 2) CHECK (total > 0), status varchar(20) DEFAULT 'pending', CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE);"`;
+
+exports[`Pretty constraint formatting should maintain single-line format when pretty option disabled 1`] = `"ALTER TABLE products ADD CONSTRAINT fk_category FOREIGN KEY (category_id) REFERENCES categories (id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED;"`;
+
+exports[`Pretty constraint formatting should use custom newline and tab characters in pretty mode 1`] = `
+"ALTER TABLE products ADD CONSTRAINT fk_category FOREIGN KEY (category_id) REFERENCES categories (id)
+ ON UPDATE CASCADE
+ ON DELETE SET NULL
+ DEFERRABLE
+ INITIALLY DEFERRED;"
+`;
diff --git a/packages/deparser/__tests__/pretty/__snapshots__/create-policy-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/create-policy-pretty.test.ts.snap
new file mode 100644
index 00000000..e2e29089
--- /dev/null
+++ b/packages/deparser/__tests__/pretty/__snapshots__/create-policy-pretty.test.ts.snap
@@ -0,0 +1,73 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Pretty CREATE POLICY formatting should format basic CREATE POLICY with pretty option enabled 1`] = `
+"CREATE POLICY "user_policy"
+ ON users
+ AS PERMISSIVE
+ FOR ALL
+ TO authenticated_users
+ USING (
+ user_id = current_user_id()
+ );"
+`;
+
+exports[`Pretty CREATE POLICY formatting should format complex CREATE POLICY with pretty option enabled 1`] = `
+"CREATE POLICY "admin_policy"
+ ON sensitive_data
+ AS RESTRICTIVE
+ FOR SELECT
+ TO admin_role
+ USING (
+ department = current_user_department()
+ )
+ WITH CHECK (
+ approved = true
+ );"
+`;
+
+exports[`Pretty CREATE POLICY formatting should format simple CREATE POLICY with pretty option enabled 1`] = `
+"CREATE POLICY "simple_policy"
+ ON posts
+ AS PERMISSIVE
+ FOR SELECT
+ TO public
+ USING (
+ published = true
+ );"
+`;
+
+exports[`Pretty CREATE POLICY formatting should format very complex CREATE POLICY with pretty option enabled 1`] = `
+"CREATE POLICY "complex_policy"
+ ON sensitive_data
+ AS RESTRICTIVE
+ FOR SELECT
+ TO admin_role
+ USING (
+ department = current_user_department()
+ AND EXISTS (SELECT
+ 1
+ FROM user_permissions
+ WHERE
+ (user_id = current_user_id()
+ AND permission = 'read_sensitive'))
+ )
+ WITH CHECK (
+ approved = true
+ AND created_by = current_user_id()
+ );"
+`;
+
+exports[`Pretty CREATE POLICY formatting should maintain single-line format for complex policy when pretty disabled 1`] = `"CREATE POLICY "admin_policy" ON sensitive_data AS RESTRICTIVE FOR SELECT TO admin_role USING (department = current_user_department()) WITH CHECK (approved = true);"`;
+
+exports[`Pretty CREATE POLICY formatting should maintain single-line format when pretty option disabled 1`] = `"CREATE POLICY "user_policy" ON users AS PERMISSIVE FOR ALL TO authenticated_users USING (user_id = current_user_id());"`;
+
+exports[`Pretty CREATE POLICY formatting should use custom newline and tab characters in pretty mode 1`] = `
+"CREATE POLICY "user_policy"
+ ON users
+ AS PERMISSIVE
+ FOR ALL
+ TO authenticated_users
+ USING (
+ user_id = current_user_id()
+ );"
+`;
diff --git a/packages/deparser/__tests__/pretty/__snapshots__/create-table-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/create-table-pretty.test.ts.snap
new file mode 100644
index 00000000..a4053291
--- /dev/null
+++ b/packages/deparser/__tests__/pretty/__snapshots__/create-table-pretty.test.ts.snap
@@ -0,0 +1,32 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Pretty CREATE TABLE formatting should format basic CREATE TABLE with pretty option enabled 1`] = `
+"CREATE TABLE users (
+ id serial PRIMARY KEY,
+ name text NOT NULL,
+ email text UNIQUE
+);"
+`;
+
+exports[`Pretty CREATE TABLE formatting should format complex CREATE TABLE with pretty option enabled 1`] = `
+"CREATE TABLE orders (
+ id serial PRIMARY KEY,
+ user_id int NOT NULL,
+ total numeric(10, 2) CHECK (total > 0),
+ status varchar(20) DEFAULT 'pending',
+ created_at pg_catalog.timestamp DEFAULT now(),
+ FOREIGN KEY (user_id) REFERENCES users (id)
+);"
+`;
+
+exports[`Pretty CREATE TABLE formatting should maintain single-line format for complex table when pretty disabled 1`] = `"CREATE TABLE orders (id serial PRIMARY KEY, user_id int NOT NULL, total numeric(10, 2) CHECK (total > 0), status varchar(20) DEFAULT 'pending', created_at pg_catalog.timestamp DEFAULT now(), FOREIGN KEY (user_id) REFERENCES users (id));"`;
+
+exports[`Pretty CREATE TABLE formatting should maintain single-line format when pretty option disabled 1`] = `"CREATE TABLE users (id serial PRIMARY KEY, name text NOT NULL, email text UNIQUE);"`;
+
+exports[`Pretty CREATE TABLE formatting should use custom newline and tab characters in pretty mode 1`] = `
+"CREATE TABLE users (
+ id serial PRIMARY KEY,
+ name text NOT NULL,
+ email text UNIQUE
+);"
+`;
diff --git a/packages/deparser/__tests__/pretty/__snapshots__/cte-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/cte-pretty.test.ts.snap
new file mode 100644
index 00000000..132770d7
--- /dev/null
+++ b/packages/deparser/__tests__/pretty/__snapshots__/cte-pretty.test.ts.snap
@@ -0,0 +1,96 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Pretty CTE (Common Table Expressions) formatting should format basic CTE with pretty option enabled 1`] = `
+"WITH
+ regional_sales AS (SELECT
+ region,
+ sum(sales_amount) AS total_sales
+ FROM sales
+ GROUP BY
+ region)
+SELECT
+ *
+FROM regional_sales;"
+`;
+
+exports[`Pretty CTE (Common Table Expressions) formatting should format complex CTE with multiple CTEs with pretty option enabled 1`] = `
+"WITH
+ regional_sales AS (SELECT
+ region,
+ sum(sales_amount) AS total_sales
+ FROM sales
+ GROUP BY
+ region),
+ top_regions AS (SELECT
+ region
+ FROM regional_sales
+ WHERE
+ total_sales > 1000000)
+SELECT
+ *
+FROM top_regions;"
+`;
+
+exports[`Pretty CTE (Common Table Expressions) formatting should format nested CTE with complex joins with pretty option enabled 1`] = `
+"WITH
+ sales_summary AS (SELECT
+ region,
+ product_category,
+ sum(amount) AS total
+ FROM sales
+ GROUP BY
+ region,
+ product_category),
+ regional_totals AS (SELECT
+ region,
+ sum(total) AS region_total
+ FROM sales_summary
+ GROUP BY
+ region)
+SELECT
+ s.region,
+ s.product_category,
+ s.total,
+ r.region_total
+FROM sales_summary AS s
+JOIN regional_totals AS r ON s.region = r.region;"
+`;
+
+exports[`Pretty CTE (Common Table Expressions) formatting should format recursive CTE with pretty option enabled 1`] = `
+"WITH RECURSIVE
+ employee_hierarchy AS (SELECT
+ id,
+ name,
+ manager_id,
+ 1 AS level
+ FROM employees
+ WHERE
+ manager_id IS NULL
+ UNION
+ ALL
+ SELECT
+ e.id,
+ e.name,
+ e.manager_id,
+ eh.level + 1
+ FROM employees AS e
+ JOIN employee_hierarchy AS eh ON e.manager_id = eh.id)
+SELECT
+ *
+FROM employee_hierarchy;"
+`;
+
+exports[`Pretty CTE (Common Table Expressions) formatting should maintain single-line format when pretty option disabled 1`] = `"WITH regional_sales AS (SELECT region, sum(sales_amount) AS total_sales FROM sales GROUP BY region) SELECT * FROM regional_sales;"`;
+
+exports[`Pretty CTE (Common Table Expressions) formatting should use custom newline and tab characters in pretty mode 1`] = `
+"WITH
+ regional_sales AS (SELECT
+ region,
+ sum(sales_amount) AS total_sales
+ FROM sales
+ GROUP BY
+ region)
+SELECT
+ *
+FROM regional_sales;"
+`;
diff --git a/packages/deparser/__tests__/pretty/__snapshots__/select-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/select-pretty.test.ts.snap
new file mode 100644
index 00000000..7f3e6932
--- /dev/null
+++ b/packages/deparser/__tests__/pretty/__snapshots__/select-pretty.test.ts.snap
@@ -0,0 +1,90 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Pretty SELECT formatting should format SELECT with UNION with pretty option enabled 1`] = `
+"SELECT
+ name
+FROM customers
+UNION
+ALL
+SELECT
+ name
+FROM suppliers
+ORDER BY
+ name;"
+`;
+
+exports[`Pretty SELECT formatting should format SELECT with multiple JOINs with pretty option enabled 1`] = `
+"SELECT
+ u.id,
+ u.name,
+ u.email,
+ p.title
+FROM users AS u
+JOIN profiles AS p ON u.id = p.user_id
+LEFT JOIN orders AS o ON u.id = o.user_id
+RIGHT JOIN addresses AS a ON u.id = a.user_id
+WHERE
+ u.active = true;"
+`;
+
+exports[`Pretty SELECT formatting should format SELECT with subquery with pretty option enabled 1`] = `
+"SELECT
+ id,
+ name
+FROM users
+WHERE
+ id IN (SELECT
+ user_id
+FROM orders
+WHERE
+ total > 100);"
+`;
+
+exports[`Pretty SELECT formatting should format basic SELECT with pretty option enabled 1`] = `
+"SELECT
+ id,
+ name,
+ email
+FROM users
+WHERE
+ active = true;"
+`;
+
+exports[`Pretty SELECT formatting should format complex SELECT with pretty option enabled 1`] = `
+"SELECT
+ u.id,
+ u.name,
+ u.email,
+ p.title
+FROM users AS u
+JOIN profiles AS 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;"
+`;
+
+exports[`Pretty SELECT formatting should maintain single-line format for complex SELECT when pretty disabled 1`] = `"SELECT u.id, u.name, u.email, p.title FROM users AS u JOIN profiles AS 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;"`;
+
+exports[`Pretty SELECT formatting should maintain single-line format when pretty option disabled 1`] = `"SELECT id, name, email FROM users WHERE active = true;"`;
+
+exports[`Pretty SELECT formatting should use custom newline and tab characters in pretty mode 1`] = `
+"SELECT
+ id,
+ name,
+ email
+FROM users
+WHERE
+ active = true;"
+`;
diff --git a/packages/deparser/__tests__/pretty/constraints-pretty.test.ts b/packages/deparser/__tests__/pretty/constraints-pretty.test.ts
new file mode 100644
index 00000000..5ce28679
--- /dev/null
+++ b/packages/deparser/__tests__/pretty/constraints-pretty.test.ts
@@ -0,0 +1,49 @@
+import { expectParseDeparse } from '../../test-utils';
+
+describe('Pretty constraint formatting', () => {
+ const foreignKeyConstraintSql = `ALTER TABLE products ADD CONSTRAINT fk_category FOREIGN KEY (category_id) REFERENCES categories(id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED;`;
+
+ const checkConstraintSql = `ALTER TABLE products ADD CONSTRAINT check_price CHECK (price > 0);`;
+
+ const complexTableSql = `CREATE TABLE orders (
+ id SERIAL PRIMARY KEY,
+ user_id INTEGER NOT NULL,
+ total DECIMAL(10,2) CHECK (total > 0),
+ status VARCHAR(20) DEFAULT 'pending',
+ CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+ );`;
+
+ it('should format foreign key constraint with pretty option enabled', async () => {
+ const result = await expectParseDeparse(foreignKeyConstraintSql, { pretty: true });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should maintain single-line format when pretty option disabled', async () => {
+ const result = await expectParseDeparse(foreignKeyConstraintSql, { pretty: false });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should format check constraint with pretty option enabled', async () => {
+ const result = await expectParseDeparse(checkConstraintSql, { pretty: true });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should format complex table with constraints with pretty option enabled', async () => {
+ const result = await expectParseDeparse(complexTableSql, { pretty: true });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should maintain single-line format for complex table when pretty disabled', async () => {
+ const result = await expectParseDeparse(complexTableSql, { pretty: false });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should use custom newline and tab characters in pretty mode', async () => {
+ const result = await expectParseDeparse(foreignKeyConstraintSql, {
+ pretty: true,
+ newline: '\r\n',
+ tab: ' '
+ });
+ expect(result).toMatchSnapshot();
+ });
+});
diff --git a/packages/deparser/__tests__/pretty/create-policy-pretty.test.ts b/packages/deparser/__tests__/pretty/create-policy-pretty.test.ts
new file mode 100644
index 00000000..4a8b8557
--- /dev/null
+++ b/packages/deparser/__tests__/pretty/create-policy-pretty.test.ts
@@ -0,0 +1,50 @@
+import { expectParseDeparse } from '../../test-utils';
+
+describe('Pretty CREATE POLICY formatting', () => {
+ const basicPolicySql = `CREATE POLICY user_policy ON users FOR ALL TO authenticated_users USING (user_id = current_user_id());`;
+
+ const complexPolicySql = `CREATE POLICY admin_policy ON sensitive_data AS RESTRICTIVE FOR SELECT TO admin_role USING (department = current_user_department()) WITH CHECK (approved = true);`;
+
+ const veryComplexPolicySql = `CREATE POLICY complex_policy ON sensitive_data AS RESTRICTIVE FOR SELECT TO admin_role USING (department = current_user_department() AND EXISTS (SELECT 1 FROM user_permissions WHERE user_id = current_user_id() AND permission = 'read_sensitive')) WITH CHECK (approved = true AND created_by = current_user_id());`;
+
+ const simplePolicySql = `CREATE POLICY simple_policy ON posts FOR SELECT TO public USING (published = true);`;
+
+ it('should format basic CREATE POLICY with pretty option enabled', async () => {
+ const result = await expectParseDeparse(basicPolicySql, { pretty: true });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should maintain single-line format when pretty option disabled', async () => {
+ const result = await expectParseDeparse(basicPolicySql, { pretty: false });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should format complex CREATE POLICY with pretty option enabled', async () => {
+ const result = await expectParseDeparse(complexPolicySql, { pretty: true });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should maintain single-line format for complex policy when pretty disabled', async () => {
+ const result = await expectParseDeparse(complexPolicySql, { pretty: false });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should format simple CREATE POLICY with pretty option enabled', async () => {
+ const result = await expectParseDeparse(simplePolicySql, { pretty: true });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should format very complex CREATE POLICY with pretty option enabled', async () => {
+ const result = await expectParseDeparse(veryComplexPolicySql, { pretty: true });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should use custom newline and tab characters in pretty mode', async () => {
+ const result = await expectParseDeparse(basicPolicySql, {
+ pretty: true,
+ newline: '\r\n',
+ tab: ' '
+ });
+ expect(result).toMatchSnapshot();
+ });
+});
diff --git a/packages/deparser/__tests__/pretty/create-table-pretty.test.ts b/packages/deparser/__tests__/pretty/create-table-pretty.test.ts
new file mode 100644
index 00000000..5d1a810d
--- /dev/null
+++ b/packages/deparser/__tests__/pretty/create-table-pretty.test.ts
@@ -0,0 +1,43 @@
+import { expectParseDeparse } from '../../test-utils';
+
+describe('Pretty CREATE TABLE formatting', () => {
+ const basicTableSql = `CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE);`;
+
+ const complexTableSql = `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(),
+ FOREIGN KEY (user_id) REFERENCES users(id)
+ );`;
+
+ it('should format basic CREATE TABLE with pretty option enabled', async () => {
+ const result = await expectParseDeparse(basicTableSql, { pretty: true });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should maintain single-line format when pretty option disabled', async () => {
+ const result = await expectParseDeparse(basicTableSql, { pretty: false });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should format complex CREATE TABLE with pretty option enabled', async () => {
+ const result = await expectParseDeparse(complexTableSql, { pretty: true });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should maintain single-line format for complex table when pretty disabled', async () => {
+ const result = await expectParseDeparse(complexTableSql, { pretty: false });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should use custom newline and tab characters in pretty mode', async () => {
+ const result = await expectParseDeparse(basicTableSql, {
+ pretty: true,
+ newline: '\r\n',
+ tab: ' '
+ });
+ expect(result).toMatchSnapshot();
+ });
+});
diff --git a/packages/deparser/__tests__/pretty/cte-pretty.test.ts b/packages/deparser/__tests__/pretty/cte-pretty.test.ts
new file mode 100644
index 00000000..aa411e15
--- /dev/null
+++ b/packages/deparser/__tests__/pretty/cte-pretty.test.ts
@@ -0,0 +1,45 @@
+import { expectParseDeparse } from '../../test-utils';
+
+describe('Pretty CTE (Common Table Expressions) formatting', () => {
+ const basicCteSql = `WITH regional_sales AS (SELECT region, SUM(sales_amount) as total_sales FROM sales GROUP BY region) SELECT * FROM regional_sales;`;
+
+ const complexCteSql = `WITH regional_sales AS (SELECT region, SUM(sales_amount) as total_sales FROM sales GROUP BY region), top_regions AS (SELECT region FROM regional_sales WHERE total_sales > 1000000) SELECT * FROM top_regions;`;
+
+ const recursiveCteSql = `WITH RECURSIVE employee_hierarchy AS (SELECT id, name, manager_id, 1 as level FROM employees WHERE manager_id IS NULL UNION ALL SELECT e.id, e.name, e.manager_id, eh.level + 1 FROM employees e JOIN employee_hierarchy eh ON e.manager_id = eh.id) SELECT * FROM employee_hierarchy;`;
+
+ const nestedCteSql = `WITH sales_summary AS (SELECT region, product_category, SUM(amount) as total FROM sales GROUP BY region, product_category), regional_totals AS (SELECT region, SUM(total) as region_total FROM sales_summary GROUP BY region) SELECT s.region, s.product_category, s.total, r.region_total FROM sales_summary s JOIN regional_totals r ON s.region = r.region;`;
+
+ it('should format basic CTE with pretty option enabled', async () => {
+ const result = await expectParseDeparse(basicCteSql, { pretty: true });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should maintain single-line format when pretty option disabled', async () => {
+ const result = await expectParseDeparse(basicCteSql, { pretty: false });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should format complex CTE with multiple CTEs with pretty option enabled', async () => {
+ const result = await expectParseDeparse(complexCteSql, { pretty: true });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should format recursive CTE with pretty option enabled', async () => {
+ const result = await expectParseDeparse(recursiveCteSql, { pretty: true });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should format nested CTE with complex joins with pretty option enabled', async () => {
+ const result = await expectParseDeparse(nestedCteSql, { pretty: true });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should use custom newline and tab characters in pretty mode', async () => {
+ const result = await expectParseDeparse(basicCteSql, {
+ pretty: true,
+ newline: '\r\n',
+ tab: ' '
+ });
+ expect(result).toMatchSnapshot();
+ });
+});
diff --git a/packages/deparser/__tests__/pretty/select-pretty.test.ts b/packages/deparser/__tests__/pretty/select-pretty.test.ts
new file mode 100644
index 00000000..3508cc95
--- /dev/null
+++ b/packages/deparser/__tests__/pretty/select-pretty.test.ts
@@ -0,0 +1,57 @@
+import { expectParseDeparse } from '../../test-utils';
+
+describe('Pretty SELECT formatting', () => {
+ const basicSelectSql = `SELECT id, name, email FROM users WHERE active = true;`;
+
+ const complexSelectSql = `SELECT u.id, u.name, u.email, p.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;`;
+
+ const multipleJoinsSql = `SELECT u.id, u.name, u.email, p.title FROM users AS u JOIN profiles AS p ON u.id = p.user_id LEFT JOIN orders AS o ON u.id = o.user_id RIGHT JOIN addresses AS a ON u.id = a.user_id WHERE u.active = true;`;
+
+ const selectWithSubquerySql = `SELECT id, name FROM users WHERE id IN (SELECT user_id FROM orders WHERE total > 100);`;
+
+ const selectUnionSql = `SELECT name FROM customers UNION ALL SELECT name FROM suppliers ORDER BY name;`;
+
+ it('should format basic SELECT with pretty option enabled', async () => {
+ const result = await expectParseDeparse(basicSelectSql, { pretty: true });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should maintain single-line format when pretty option disabled', async () => {
+ const result = await expectParseDeparse(basicSelectSql, { pretty: false });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should format complex SELECT with pretty option enabled', async () => {
+ const result = await expectParseDeparse(complexSelectSql, { pretty: true });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should maintain single-line format for complex SELECT when pretty disabled', async () => {
+ const result = await expectParseDeparse(complexSelectSql, { pretty: false });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should format SELECT with subquery with pretty option enabled', async () => {
+ const result = await expectParseDeparse(selectWithSubquerySql, { pretty: true });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should format SELECT with UNION with pretty option enabled', async () => {
+ const result = await expectParseDeparse(selectUnionSql, { pretty: true });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should format SELECT with multiple JOINs with pretty option enabled', async () => {
+ const result = await expectParseDeparse(multipleJoinsSql, { pretty: true });
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should use custom newline and tab characters in pretty mode', async () => {
+ const result = await expectParseDeparse(basicSelectSql, {
+ pretty: true,
+ newline: '\r\n',
+ tab: ' '
+ });
+ expect(result).toMatchSnapshot();
+ });
+});
diff --git a/packages/deparser/package.json b/packages/deparser/package.json
index fd6570d7..a3792d35 100644
--- a/packages/deparser/package.json
+++ b/packages/deparser/package.json
@@ -1,6 +1,6 @@
{
"name": "pgsql-deparser",
- "version": "17.7.1",
+ "version": "17.7.2",
"author": "Dan Lynch ",
"description": "PostgreSQL AST Deparser",
"main": "index.js",
diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts
index fa7807c3..4fe7f806 100644
--- a/packages/deparser/src/deparser.ts
+++ b/packages/deparser/src/deparser.ts
@@ -12,6 +12,7 @@ export interface DeparserOptions {
functionDelimiter?: string; // Default: '$$'
// Alternative delimiter when the default is found in the body
functionDelimiterFallback?: string; // Default: '$EOFCODE$'
+ pretty?: boolean; // Default: false
}
// Type guards for better type safety
@@ -64,7 +65,7 @@ export class Deparser implements DeparserVisitor {
private options: DeparserOptions;
constructor(tree: Node | Node[] | t.ParseResult, opts: DeparserOptions = {}) {
- this.formatter = new SqlFormatter(opts.newline, opts.tab);
+ this.formatter = new SqlFormatter(opts.newline, opts.tab, opts.pretty);
// Set default options
this.options = {
@@ -218,9 +219,11 @@ export class Deparser implements DeparserVisitor {
if (!node.op || node.op === 'SETOP_NONE') {
if (node.valuesLists == null) {
- output.push('SELECT');
+ if (!this.formatter.isPretty() || !node.targetList) {
+ output.push('SELECT');
+ }
}
- } else {
+ }else {
const leftStmt = this.SelectStmt(node.larg as t.SelectStmt, context);
const rightStmt = this.SelectStmt(node.rarg as t.SelectStmt, context);
@@ -271,25 +274,52 @@ export class Deparser implements DeparserVisitor {
}
}
+ // Handle DISTINCT clause - in pretty mode, we'll include it in the SELECT clause
+ let distinctPart = '';
if (node.distinctClause) {
const distinctClause = ListUtils.unwrapList(node.distinctClause);
if (distinctClause.length > 0 && Object.keys(distinctClause[0]).length > 0) {
- output.push('DISTINCT ON');
const clause = distinctClause
.map(e => this.visit(e as Node, { ...context, select: true }))
.join(', ');
- output.push(this.formatter.parens(clause));
+ distinctPart = ' DISTINCT ON ' + this.formatter.parens(clause);
} else {
- output.push('DISTINCT');
+ distinctPart = ' DISTINCT';
+ }
+
+ if (!this.formatter.isPretty()) {
+ if (distinctClause.length > 0 && Object.keys(distinctClause[0]).length > 0) {
+ output.push('DISTINCT ON');
+ const clause = distinctClause
+ .map(e => this.visit(e as Node, { ...context, select: true }))
+ .join(', ');
+ output.push(this.formatter.parens(clause));
+ } else {
+ output.push('DISTINCT');
+ }
}
}
if (node.targetList) {
const targetList = ListUtils.unwrapList(node.targetList);
- const targets = targetList
- .map(e => this.visit(e as Node, { ...context, select: true }))
- .join(', ');
- output.push(targets);
+ if (this.formatter.isPretty()) {
+ const targetStrings = targetList
+ .map(e => {
+ const targetStr = this.visit(e as Node, { ...context, select: true });
+ if (this.containsMultilineStringLiteral(targetStr)) {
+ return targetStr;
+ }
+ return this.formatter.indent(targetStr);
+ });
+ const formattedTargets = targetStrings.join(',' + this.formatter.newline());
+ output.push('SELECT' + distinctPart);
+ output.push(formattedTargets);
+ } else {
+ const targets = targetList
+ .map(e => this.visit(e as Node, { ...context, select: true }))
+ .join(', ');
+ output.push(targets);
+ }
}
if (node.intoClause) {
@@ -298,17 +328,29 @@ export class Deparser implements DeparserVisitor {
}
if (node.fromClause) {
- output.push('FROM');
const fromList = ListUtils.unwrapList(node.fromClause);
const fromItems = fromList
.map(e => this.deparse(e as Node, { ...context, from: true }))
.join(', ');
- output.push(fromItems);
+ output.push('FROM ' + fromItems.trim());
}
if (node.whereClause) {
- output.push('WHERE');
- output.push(this.visit(node.whereClause as Node, context));
+ if (this.formatter.isPretty()) {
+ output.push('WHERE');
+ const whereExpr = this.visit(node.whereClause as Node, context);
+ const lines = whereExpr.split(this.formatter.newline());
+ const indentedLines = lines.map((line, index) => {
+ if (index === 0) {
+ return this.formatter.indent(line);
+ }
+ return line;
+ });
+ output.push(indentedLines.join(this.formatter.newline()));
+ } else {
+ output.push('WHERE');
+ output.push(this.visit(node.whereClause as Node, context));
+ }
}
if (node.valuesLists) {
@@ -321,17 +363,41 @@ export class Deparser implements DeparserVisitor {
}
if (node.groupClause) {
- output.push('GROUP BY');
const groupList = ListUtils.unwrapList(node.groupClause);
- const groupItems = groupList
- .map(e => this.visit(e as Node, { ...context, group: true }))
- .join(', ');
- output.push(groupItems);
+ if (this.formatter.isPretty()) {
+ const groupItems = groupList
+ .map(e => {
+ const groupStr = this.visit(e as Node, { ...context, group: true });
+ if (this.containsMultilineStringLiteral(groupStr)) {
+ return groupStr;
+ }
+ return this.formatter.indent(groupStr);
+ })
+ .join(',' + this.formatter.newline());
+ output.push('GROUP BY');
+ output.push(groupItems);
+ } else {
+ output.push('GROUP BY');
+ const groupItems = groupList
+ .map(e => this.visit(e as Node, { ...context, group: true }))
+ .join(', ');
+ output.push(groupItems);
+ }
}
if (node.havingClause) {
- output.push('HAVING');
- output.push(this.visit(node.havingClause as Node, context));
+ if (this.formatter.isPretty()) {
+ output.push('HAVING');
+ const havingStr = this.visit(node.havingClause as Node, context);
+ if (this.containsMultilineStringLiteral(havingStr)) {
+ output.push(havingStr);
+ } else {
+ output.push(this.formatter.indent(havingStr));
+ }
+ } else {
+ output.push('HAVING');
+ output.push(this.visit(node.havingClause as Node, context));
+ }
}
if (node.windowClause) {
@@ -344,22 +410,34 @@ export class Deparser implements DeparserVisitor {
}
if (node.sortClause) {
- output.push('ORDER BY');
const sortList = ListUtils.unwrapList(node.sortClause);
- const sortItems = sortList
- .map(e => this.visit(e as Node, { ...context, sort: true }))
- .join(', ');
- output.push(sortItems);
+ if (this.formatter.isPretty()) {
+ const sortItems = sortList
+ .map(e => {
+ const sortStr = this.visit(e as Node, { ...context, sort: true });
+ if (this.containsMultilineStringLiteral(sortStr)) {
+ return sortStr;
+ }
+ return this.formatter.indent(sortStr);
+ })
+ .join(',' + this.formatter.newline());
+ output.push('ORDER BY');
+ output.push(sortItems);
+ } else {
+ output.push('ORDER BY');
+ const sortItems = sortList
+ .map(e => this.visit(e as Node, { ...context, sort: true }))
+ .join(', ');
+ output.push(sortItems);
+ }
}
if (node.limitCount) {
- output.push('LIMIT');
- output.push(this.visit(node.limitCount as Node, context));
+ output.push('LIMIT ' + this.visit(node.limitCount as Node, context));
}
if (node.limitOffset) {
- output.push('OFFSET');
- output.push(this.visit(node.limitOffset as Node, context));
+ output.push('OFFSET ' + this.visit(node.limitOffset as Node, context));
}
if (node.lockingClause) {
@@ -370,6 +448,10 @@ export class Deparser implements DeparserVisitor {
output.push(lockingClauses);
}
+ if (this.formatter.isPretty()) {
+ const filteredOutput = output.filter(item => item.trim() !== '');
+ return filteredOutput.join(this.formatter.newline());
+ }
return output.join(' ');
}
@@ -908,9 +990,22 @@ export class Deparser implements DeparserVisitor {
output.push('RECURSIVE');
}
- const ctes = ListUtils.unwrapList(node.ctes);
- const cteStrs = ctes.map(cte => this.visit(cte, context));
- output.push(cteStrs.join(', '));
+ if (node.ctes && node.ctes.length > 0) {
+ const ctes = ListUtils.unwrapList(node.ctes);
+ if (this.formatter.isPretty()) {
+ const cteStrings = ctes.map(cte => {
+ const cteStr = this.visit(cte, context);
+ if (this.containsMultilineStringLiteral(cteStr)) {
+ return this.formatter.newline() + cteStr;
+ }
+ return this.formatter.newline() + this.formatter.indent(cteStr);
+ });
+ output.push(cteStrings.join(','));
+ } else {
+ const cteStrings = ctes.map(cte => this.visit(cte, context));
+ output.push(cteStrings.join(', '));
+ }
+ }
return output.join(' ');
}
@@ -1006,11 +1101,21 @@ export class Deparser implements DeparserVisitor {
switch (boolop) {
case 'AND_EXPR':
- const andArgs = args.map(arg => this.visit(arg, boolContext)).join(' AND ');
- return formatStr.replace('%s', () => andArgs);
+ if (this.formatter.isPretty() && args.length > 1) {
+ const andArgs = args.map(arg => this.visit(arg, boolContext)).join(this.formatter.newline() + ' AND ');
+ return formatStr.replace('%s', () => andArgs);
+ } else {
+ const andArgs = args.map(arg => this.visit(arg, boolContext)).join(' AND ');
+ return formatStr.replace('%s', () => andArgs);
+ }
case 'OR_EXPR':
- const orArgs = args.map(arg => this.visit(arg, boolContext)).join(' OR ');
- return formatStr.replace('%s', () => orArgs);
+ if (this.formatter.isPretty() && args.length > 1) {
+ const orArgs = args.map(arg => this.visit(arg, boolContext)).join(this.formatter.newline() + ' OR ');
+ return formatStr.replace('%s', () => orArgs);
+ } else {
+ const orArgs = args.map(arg => this.visit(arg, boolContext)).join(' OR ');
+ return formatStr.replace('%s', () => orArgs);
+ }
case 'NOT_EXPR':
return `NOT (${this.visit(args[0], context)})`;
default:
@@ -2139,7 +2244,15 @@ export class Deparser implements DeparserVisitor {
const elementStrs = elements.map(el => {
return this.deparse(el, context);
});
- output.push(this.formatter.parens(elementStrs.join(', ')));
+
+ if (this.formatter.isPretty()) {
+ const formattedElements = elementStrs.map(el =>
+ this.formatter.indent(el)
+ ).join(',' + this.formatter.newline());
+ output.push('(' + this.formatter.newline() + formattedElements + this.formatter.newline() + ')');
+ } else {
+ output.push(this.formatter.parens(elementStrs.join(', ')));
+ }
} else if (!node.partbound) {
output.push(this.formatter.parens(''));
}
@@ -2432,38 +2545,50 @@ export class Deparser implements DeparserVisitor {
}
}
if (node.fk_upd_action && node.fk_upd_action !== 'a') {
- output.push('ON UPDATE');
+ let updateClause = 'ON UPDATE ';
switch (node.fk_upd_action) {
case 'r':
- output.push('RESTRICT');
+ updateClause += 'RESTRICT';
break;
case 'c':
- output.push('CASCADE');
+ updateClause += 'CASCADE';
break;
case 'n':
- output.push('SET NULL');
+ updateClause += 'SET NULL';
break;
case 'd':
- output.push('SET DEFAULT');
+ updateClause += 'SET DEFAULT';
break;
}
+ if (this.formatter.isPretty()) {
+ output.push('\n' + this.formatter.indent(updateClause));
+ } else {
+ output.push('ON UPDATE');
+ output.push(updateClause.replace('ON UPDATE ', ''));
+ }
}
if (node.fk_del_action && node.fk_del_action !== 'a') {
- output.push('ON DELETE');
+ let deleteClause = 'ON DELETE ';
switch (node.fk_del_action) {
case 'r':
- output.push('RESTRICT');
+ deleteClause += 'RESTRICT';
break;
case 'c':
- output.push('CASCADE');
+ deleteClause += 'CASCADE';
break;
case 'n':
- output.push('SET NULL');
+ deleteClause += 'SET NULL';
break;
case 'd':
- output.push('SET DEFAULT');
+ deleteClause += 'SET DEFAULT';
break;
}
+ if (this.formatter.isPretty()) {
+ output.push('\n' + this.formatter.indent(deleteClause));
+ } else {
+ output.push('ON DELETE');
+ output.push(deleteClause.replace('ON DELETE ', ''));
+ }
}
// Handle NOT VALID for foreign key constraints - only for table constraints, not domain constraints
if (node.skip_validation && !context.isDomainConstraint) {
@@ -2520,17 +2645,44 @@ export class Deparser implements DeparserVisitor {
// Handle deferrable constraints for all constraint types that support it
if (node.contype === 'CONSTR_PRIMARY' || node.contype === 'CONSTR_UNIQUE' || node.contype === 'CONSTR_FOREIGN') {
if (node.deferrable) {
- output.push('DEFERRABLE');
- if (node.initdeferred === true) {
- output.push('INITIALLY DEFERRED');
- } else if (node.initdeferred === false) {
- output.push('INITIALLY IMMEDIATE');
+ if (this.formatter.isPretty() && node.contype === 'CONSTR_FOREIGN') {
+ output.push('\n' + this.formatter.indent('DEFERRABLE'));
+ if (node.initdeferred === true) {
+ output.push('\n' + this.formatter.indent('INITIALLY DEFERRED'));
+ } else if (node.initdeferred === false) {
+ output.push('\n' + this.formatter.indent('INITIALLY IMMEDIATE'));
+ }
+ } else {
+ output.push('DEFERRABLE');
+ if (node.initdeferred === true) {
+ output.push('INITIALLY DEFERRED');
+ } else if (node.initdeferred === false) {
+ output.push('INITIALLY IMMEDIATE');
+ }
}
} else if (node.deferrable === false) {
- output.push('NOT DEFERRABLE');
+ if (this.formatter.isPretty() && node.contype === 'CONSTR_FOREIGN') {
+ output.push('\n' + this.formatter.indent('NOT DEFERRABLE'));
+ } else {
+ output.push('NOT DEFERRABLE');
+ }
}
}
+ if (this.formatter.isPretty() && node.contype === 'CONSTR_FOREIGN') {
+ let result = '';
+ for (let i = 0; i < output.length; i++) {
+ if (output[i].startsWith('\n')) {
+ result += output[i];
+ } else {
+ if (i > 0 && !output[i-1].startsWith('\n')) {
+ result += ' ';
+ }
+ result += output[i];
+ }
+ }
+ return result;
+ }
return output.join(' ');
}
@@ -3352,11 +3504,9 @@ export class Deparser implements DeparserVisitor {
switch (node.jointype) {
case 'JOIN_INNER':
- // Handle NATURAL JOIN first - it has isNatural=true (NATURAL already added above)
if (node.isNatural) {
joinStr += 'JOIN';
}
- // Handle CROSS JOIN case - when there's no quals, no usingClause, and not natural
else if (!node.quals && (!node.usingClause || node.usingClause.length === 0)) {
joinStr += 'CROSS JOIN';
} else {
@@ -3376,8 +3526,6 @@ export class Deparser implements DeparserVisitor {
joinStr += 'JOIN';
}
- output.push(joinStr);
-
if (node.rarg) {
let rargStr = this.visit(node.rarg, context);
@@ -3385,22 +3533,42 @@ export class Deparser implements DeparserVisitor {
rargStr = `(${rargStr})`;
}
- output.push(rargStr);
+ if (this.formatter.isPretty()) {
+ output.push(this.formatter.newline() + joinStr + ' ' + rargStr);
+ } else {
+ output.push(joinStr + ' ' + rargStr);
+ }
+ } else {
+ if (this.formatter.isPretty()) {
+ output.push(this.formatter.newline() + joinStr);
+ } else {
+ output.push(joinStr);
+ }
}
if (node.usingClause && node.usingClause.length > 0) {
- output.push('USING');
const usingList = ListUtils.unwrapList(node.usingClause);
const columnNames = usingList.map(col => this.visit(col, context));
- output.push(`(${columnNames.join(', ')})`);
+ if (this.formatter.isPretty()) {
+ output.push(` USING (${columnNames.join(', ')})`);
+ } else {
+ output.push(`USING (${columnNames.join(', ')})`);
+ }
} else if (node.quals) {
- output.push('ON');
- output.push(this.visit(node.quals, context));
+ if (this.formatter.isPretty()) {
+ output.push(` ON ${this.visit(node.quals, context)}`);
+ } else {
+ output.push(`ON ${this.visit(node.quals, context)}`);
+ }
}
- let result = output.join(' ');
+ let result;
+ if (this.formatter.isPretty()) {
+ result = output.join('');
+ } else {
+ result = output.join(' ');
+ }
- // Handle join_using_alias first (for USING clause aliases like "AS x")
if (node.join_using_alias && node.join_using_alias.aliasname) {
let aliasStr = node.join_using_alias.aliasname;
if (node.join_using_alias.colnames && node.join_using_alias.colnames.length > 0) {
@@ -3411,7 +3579,6 @@ export class Deparser implements DeparserVisitor {
result += ` AS ${aliasStr}`;
}
- // Handle regular alias (for outer table aliases like "y")
if (node.alias && node.alias.aliasname) {
let aliasStr = node.alias.aliasname;
if (node.alias.colnames && node.alias.colnames.length > 0) {
@@ -6305,46 +6472,83 @@ export class Deparser implements DeparserVisitor {
}
CreatePolicyStmt(node: t.CreatePolicyStmt, context: DeparserContext): string {
- const output: string[] = ['CREATE', 'POLICY'];
+ const output: string[] = [];
+ const initialParts = ['CREATE', 'POLICY'];
if (node.policy_name) {
- output.push(`"${node.policy_name}"`);
+ initialParts.push(`"${node.policy_name}"`);
}
- output.push('ON');
+ output.push(initialParts.join(' '));
+ // Add ON clause on new line in pretty mode
if (node.table) {
- output.push(this.RangeVar(node.table, context));
+ if (this.formatter.isPretty()) {
+ output.push(this.formatter.newline() + this.formatter.indent(`ON ${this.RangeVar(node.table, context)}`));
+ } else {
+ output.push('ON');
+ output.push(this.RangeVar(node.table, context));
+ }
}
// Handle AS RESTRICTIVE/PERMISSIVE clause
if (node.permissive === undefined) {
- output.push('AS', 'RESTRICTIVE');
+ if (this.formatter.isPretty()) {
+ output.push(this.formatter.newline() + this.formatter.indent('AS RESTRICTIVE'));
+ } else {
+ output.push('AS', 'RESTRICTIVE');
+ }
} else if (node.permissive === true) {
- output.push('AS', 'PERMISSIVE');
+ if (this.formatter.isPretty()) {
+ output.push(this.formatter.newline() + this.formatter.indent('AS PERMISSIVE'));
+ } else {
+ output.push('AS', 'PERMISSIVE');
+ }
}
if (node.cmd_name) {
- output.push('FOR', node.cmd_name.toUpperCase());
+ if (this.formatter.isPretty()) {
+ output.push(this.formatter.newline() + this.formatter.indent(`FOR ${node.cmd_name.toUpperCase()}`));
+ } else {
+ output.push('FOR', node.cmd_name.toUpperCase());
+ }
}
if (node.roles && node.roles.length > 0) {
- output.push('TO');
const roles = ListUtils.unwrapList(node.roles).map(role => this.visit(role, context));
- output.push(roles.join(', '));
+ if (this.formatter.isPretty()) {
+ output.push(this.formatter.newline() + this.formatter.indent(`TO ${roles.join(', ')}`));
+ } else {
+ output.push('TO');
+ output.push(roles.join(', '));
+ }
}
if (node.qual) {
- output.push('USING');
- output.push(`(${this.visit(node.qual, context)})`);
+ if (this.formatter.isPretty()) {
+ const qualExpr = this.visit(node.qual, context);
+ output.push(this.formatter.newline() + this.formatter.indent('USING ('));
+ output.push(this.formatter.newline() + this.formatter.indent(this.formatter.indent(qualExpr)));
+ output.push(this.formatter.newline() + this.formatter.indent(')'));
+ } else {
+ output.push('USING');
+ output.push(`(${this.visit(node.qual, context)})`);
+ }
}
-
+
if (node.with_check) {
- output.push('WITH CHECK');
- output.push(`(${this.visit(node.with_check, context)})`);
+ if (this.formatter.isPretty()) {
+ const checkExpr = this.visit(node.with_check, context);
+ output.push(this.formatter.newline() + this.formatter.indent('WITH CHECK ('));
+ output.push(this.formatter.newline() + this.formatter.indent(this.formatter.indent(checkExpr)));
+ output.push(this.formatter.newline() + this.formatter.indent(')'));
+ } else {
+ output.push('WITH CHECK');
+ output.push(`(${this.visit(node.with_check, context)})`);
+ }
}
- return output.join(' ');
+ return this.formatter.isPretty() ? output.join('') : output.join(' ');
}
AlterPolicyStmt(node: t.AlterPolicyStmt, context: DeparserContext): string {
@@ -10722,6 +10926,8 @@ export class Deparser implements DeparserVisitor {
return output.join(' ');
}
-
-
+ private containsMultilineStringLiteral(content: string): boolean {
+ const stringLiteralRegex = /'[^']*\n[^']*'/g;
+ return stringLiteralRegex.test(content);
+ }
}
diff --git a/packages/deparser/src/utils/sql-formatter.ts b/packages/deparser/src/utils/sql-formatter.ts
index 8d0386b3..0f56504f 100644
--- a/packages/deparser/src/utils/sql-formatter.ts
+++ b/packages/deparser/src/utils/sql-formatter.ts
@@ -1,10 +1,12 @@
export class SqlFormatter {
private newlineChar: string;
private tabChar: string;
+ private prettyMode: boolean;
- constructor(newlineChar: string = '\n', tabChar: string = ' ') {
+ constructor(newlineChar: string = '\n', tabChar: string = ' ', prettyMode: boolean = false) {
this.newlineChar = newlineChar;
this.tabChar = tabChar;
+ this.prettyMode = prettyMode;
}
format(parts: string[], separator: string = ' '): string {
@@ -12,7 +14,13 @@ export class SqlFormatter {
}
indent(text: string, count: number = 1): string {
- return text;
+ if (!this.prettyMode) {
+ return text;
+ }
+ const indentation = this.tabChar.repeat(count);
+ return text.split(this.newlineChar).map(line =>
+ line.trim() ? indentation + line : line
+ ).join(this.newlineChar);
}
parens(content: string): string {
@@ -26,4 +34,8 @@ export class SqlFormatter {
tab(): string {
return this.tabChar;
}
+
+ isPretty(): boolean {
+ return this.prettyMode;
+ }
}
diff --git a/packages/deparser/test-utils/index.ts b/packages/deparser/test-utils/index.ts
index 6efb4b4e..35f41a81 100644
--- a/packages/deparser/test-utils/index.ts
+++ b/packages/deparser/test-utils/index.ts
@@ -1,18 +1,34 @@
import { parse } from 'libpg-query';
-import { deparseSync as deparse } from '../src';
+import { deparseSync as deparse, DeparserOptions } from '../src';
import { cleanTree } from '../src/utils';
import { readFileSync } from 'fs';
import * as path from 'path';
import { expect } from '@jest/globals';
import { diff } from 'jest-diff'
+export async function expectParseDeparse(sql1: string, options: DeparserOptions = { pretty: false }) {
+ const parsed = await parse(sql1);
+
+ const sql2 = deparse(parsed, options);
+
+ const ast1 = cleanTree(parsed);
+ const ast2 = cleanTree(await parse(sql2));
+
+ expect(ast2).toEqual(ast1);
+
+ return sql2;
+}
+
type ParseErrorType =
| 'PARSE_FAILED'
| 'INVALID_STATEMENT'
| 'REPARSE_FAILED'
| 'AST_MISMATCH'
| 'UNEXPECTED_ERROR'
- | 'INVALID_DEPARSED_SQL';
+ | 'INVALID_DEPARSED_SQL'
+ | 'PRETTY_INVALID_DEPARSED_SQL'
+ | 'PRETTY_REPARSE_FAILED'
+ | 'PRETTY_AST_MISMATCH';
interface ParseError extends Error {
type: ParseErrorType;
@@ -58,6 +74,12 @@ function getErrorMessage(type: ParseErrorType): string {
return 'Unexpected error during parse/deparse cycle';
case 'INVALID_DEPARSED_SQL':
return 'Invalid deparsed SQL';
+ case 'PRETTY_INVALID_DEPARSED_SQL':
+ return 'Invalid deparsed SQL (pretty)';
+ case 'PRETTY_REPARSE_FAILED':
+ return 'Reparse failed - no statements returned (pretty)';
+ case 'PRETTY_AST_MISMATCH':
+ return 'AST mismatch after parse/deparse cycle (pretty)';
}
}
@@ -99,27 +121,30 @@ export class TestUtils {
if (tree.stmts) {
for (const stmt of tree.stmts) {
if (stmt.stmt) {
- const outSql = deparse(stmt.stmt);
+ const outSql1 = deparse(stmt.stmt, { pretty: false });
+ const outSql2 = deparse(stmt.stmt, { pretty: true });
// console.log(`\n๐ DEBUGGING SQL COMPARISON for test: ${testName}`);
// console.log(`๐ฅ INPUT SQL: ${sql}`);
// console.log(`๐ค DEPARSED SQL: ${outSql}`);
// console.log(`๐ SQL MATCH: ${sql.trim() === outSql.trim() ? 'โ
EXACT MATCH' : 'โ DIFFERENT'}`);
+ // Test non-pretty version first
let reparsed;
try {
- reparsed = await parse(outSql);
+ reparsed = await parse(outSql1);
} catch (parseErr) {
throw createParseError(
'INVALID_DEPARSED_SQL',
testName,
sql,
- outSql,
+ outSql1,
cleanTree([stmt]),
undefined,
parseErr instanceof Error ? parseErr.message : String(parseErr)
);
}
+
const originalClean = cleanTree([stmt]);
const reparsedClean = cleanTree(reparsed.stmts || []);
@@ -132,13 +157,41 @@ export class TestUtils {
}
if (!reparsed.stmts) {
- throw createParseError('REPARSE_FAILED', testName, sql, outSql, originalClean);
+ throw createParseError('REPARSE_FAILED', testName, sql, outSql1, originalClean);
}
try {
expect(reparsedClean).toEqual(originalClean);
} catch (err) {
- throw createParseError('AST_MISMATCH', testName, sql, outSql, originalClean, reparsedClean);
+ throw createParseError('AST_MISMATCH', testName, sql, outSql1, originalClean, reparsedClean);
+ }
+
+ // Test pretty version if non-pretty succeeded
+ let prettyReparsed;
+ try {
+ prettyReparsed = await parse(outSql2);
+ } catch (parseErr) {
+ throw createParseError(
+ 'PRETTY_INVALID_DEPARSED_SQL',
+ testName,
+ sql,
+ outSql2,
+ cleanTree([stmt]),
+ undefined,
+ parseErr instanceof Error ? parseErr.message : String(parseErr)
+ );
+ }
+
+ const prettyReparsedClean = cleanTree(prettyReparsed.stmts || []);
+
+ if (!prettyReparsed.stmts) {
+ throw createParseError('PRETTY_REPARSE_FAILED', testName, sql, outSql2, originalClean);
+ }
+
+ try {
+ expect(prettyReparsedClean).toEqual(originalClean);
+ } catch (err) {
+ throw createParseError('PRETTY_AST_MISMATCH', testName, sql, outSql2, originalClean, prettyReparsedClean);
}
}
}
@@ -172,6 +225,23 @@ export class TestUtils {
`\nDIFF (what's missing from actual vs expected):`,
diff(parseError.originalAst, parseError.reparsedAst) || 'No diff available'
);
+ } else if (parseError.type === 'PRETTY_INVALID_DEPARSED_SQL') {
+ errorMessages.push(
+ `\nโ PRETTY DEPARSER GENERATED INVALID SQL:`,
+ `ORIGINAL AST:`,
+ JSON.stringify(parseError.originalAst, null, 2),
+ `\nPARSE ERROR: ${parseError.parseError}`
+ );
+ } else if (parseError.type === 'PRETTY_AST_MISMATCH') {
+ errorMessages.push(
+ `\nโ PRETTY AST COMPARISON:`,
+ `EXPECTED AST:`,
+ JSON.stringify(parseError.originalAst, null, 2),
+ `\nACTUAL AST:`,
+ JSON.stringify(parseError.reparsedAst, null, 2),
+ `\nDIFF (what's missing from actual vs expected):`,
+ diff(parseError.originalAst, parseError.reparsedAst) || 'No diff available'
+ );
} else if (parseError.originalAst) {
errorMessages.push(`โ AST: ${JSON.stringify(parseError.originalAst, null, 2)}`);
}
diff --git a/packages/parser/CHANGELOG.md b/packages/parser/CHANGELOG.md
index cfdfbf8b..b9c003fc 100644
--- a/packages/parser/CHANGELOG.md
+++ b/packages/parser/CHANGELOG.md
@@ -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.6.2](https://github.com/launchql/pgsql-parser/compare/pgsql-parser@17.6.1...pgsql-parser@17.6.2) (2025-06-23)
+
+**Note:** Version bump only for package pgsql-parser
+
+
+
+
+
## [17.6.1](https://github.com/launchql/pgsql-parser/compare/pgsql-parser@17.6.0...pgsql-parser@17.6.1) (2025-06-23)
**Note:** Version bump only for package pgsql-parser
diff --git a/packages/parser/README.md b/packages/parser/README.md
index 5705f4cc..9c069415 100644
--- a/packages/parser/README.md
+++ b/packages/parser/README.md
@@ -91,7 +91,7 @@ Here's how you can use the deparser in your TypeScript code, using [`@pgsql/util
```ts
import * as t from '@pgsql/utils';
import { RangeVar, SelectStmt } from '@pgsql/types';
-import { deparseSync as deparse } from 'pgsql-deparser';
+import { deparse } from 'pgsql-deparser';
// This could have been obtained from any JSON or AST, not necessarily @pgsql/utils
const stmt: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
@@ -117,7 +117,7 @@ const stmt: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
(stmt.SelectStmt.fromClause[0] as {RangeVar: RangeVar}).RangeVar.relname = 'another_table';
// Deparse the modified AST back to a SQL string
-console.log(deparse(stmt));
+console.log(await deparse(stmt));
// Output: SELECT * FROM another_table
```
diff --git a/packages/parser/package.json b/packages/parser/package.json
index 775efde6..c6f01d83 100644
--- a/packages/parser/package.json
+++ b/packages/parser/package.json
@@ -1,6 +1,6 @@
{
"name": "pgsql-parser",
- "version": "17.6.1",
+ "version": "17.6.2",
"author": "Dan Lynch ",
"description": "The real PostgreSQL query parser",
"main": "index.js",
@@ -41,6 +41,6 @@
"dependencies": {
"@pgsql/types": "^17.6.1",
"libpg-query": "17.5.2",
- "pgsql-deparser": "^17.7.1"
+ "pgsql-deparser": "^17.7.2"
}
}
diff --git a/packages/pgsql-cli/CHANGELOG.md b/packages/pgsql-cli/CHANGELOG.md
index 777b6770..f19009bf 100644
--- a/packages/pgsql-cli/CHANGELOG.md
+++ b/packages/pgsql-cli/CHANGELOG.md
@@ -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.
+## [1.30.2](https://github.com/launchql/pgsql-parser/compare/@pgsql/cli@1.30.1...@pgsql/cli@1.30.2) (2025-06-23)
+
+**Note:** Version bump only for package @pgsql/cli
+
+
+
+
+
## [1.30.1](https://github.com/launchql/pgsql-parser/compare/@pgsql/cli@1.30.0...@pgsql/cli@1.30.1) (2025-06-23)
**Note:** Version bump only for package @pgsql/cli
diff --git a/packages/pgsql-cli/package.json b/packages/pgsql-cli/package.json
index 28cfaa04..88b973ca 100644
--- a/packages/pgsql-cli/package.json
+++ b/packages/pgsql-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@pgsql/cli",
- "version": "1.30.1",
+ "version": "1.30.2",
"description": "Unified CLI for PostgreSQL AST parsing, deparsing, and code generation",
"author": "Dan Lynch ",
"main": "index.js",
@@ -53,7 +53,7 @@
"mkdirp": "3.0.1",
"nested-obj": "^0.0.1",
"pg-proto-parser": "^1.29.0",
- "pgsql-deparser": "^17.7.1",
- "pgsql-parser": "^17.6.1"
+ "pgsql-deparser": "^17.7.2",
+ "pgsql-parser": "^17.6.2"
}
}
diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md
index fa79eba9..963d1979 100644
--- a/packages/utils/CHANGELOG.md
+++ b/packages/utils/CHANGELOG.md
@@ -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.6.2](https://github.com/launchql/pgsql-parser/compare/@pgsql/utils@17.6.1...@pgsql/utils@17.6.2) (2025-06-23)
+
+**Note:** Version bump only for package @pgsql/utils
+
+
+
+
+
## [17.6.1](https://github.com/launchql/pgsql-parser/compare/@pgsql/utils@17.6.0...@pgsql/utils@17.6.1) (2025-06-23)
**Note:** Version bump only for package @pgsql/utils
diff --git a/packages/utils/README.md b/packages/utils/README.md
index 2f620015..467239fb 100644
--- a/packages/utils/README.md
+++ b/packages/utils/README.md
@@ -61,7 +61,7 @@ Explore the PostgreSQL Abstract Syntax Tree (AST) as JSON objects with ease usin
```ts
import * as t from '@pgsql/utils';
import { SelectStmt } from '@pgsql/types';
-import { deparseSync as deparse } from 'pgsql-deparser';
+import { deparse } from 'pgsql-deparser';
const selectStmt: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
targetList: [
@@ -83,7 +83,7 @@ const selectStmt: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
});
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(deparse(stmt))
+console.log(await deparse(stmt))
// Output: SELECT * FROM some_amazing_table
```
@@ -91,8 +91,8 @@ console.log(deparse(stmt))
```ts
import * as t from '@pgsql/utils';
-import { RangeVar, SelectStmt } from '@pgsql/types';
-import { deparseSync as deparse } from 'pgsql-deparser';
+import { SelectStmt } from '@pgsql/types';
+import { deparse } from 'pgsql-deparser';
const query: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
targetList: [
@@ -128,7 +128,7 @@ const query: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
op: 'SETOP_NONE'
});
-deparse(createStmt, {});
+await deparse(createStmt);
// SELECT name, email FROM users WHERE age > 18
```
@@ -167,7 +167,7 @@ const createStmt = t.nodes.createStmt({
});
// `deparse` function converts AST to SQL string
-const sql = deparse(createStmt);
+const sql = await deparse(createStmt, { pretty: true });
console.log(sql);
// OUTPUT:
diff --git a/packages/utils/package.json b/packages/utils/package.json
index 30bffc35..e92d3b32 100644
--- a/packages/utils/package.json
+++ b/packages/utils/package.json
@@ -1,6 +1,6 @@
{
"name": "@pgsql/utils",
- "version": "17.6.1",
+ "version": "17.6.2",
"author": "Dan Lynch ",
"description": "PostgreSQL AST utils for pgsql-parser",
"main": "index.js",
@@ -32,7 +32,7 @@
},
"devDependencies": {
"pg-proto-parser": "^1.29.0",
- "pgsql-deparser": "^17.7.1"
+ "pgsql-deparser": "^17.7.2"
},
"dependencies": {
"@pgsql/types": "^17.6.1",