Skip to content

Commit b3e6378

Browse files
authored
Merge pull request #195 from launchql/fix/alter
fix: correct ALTER TABLE ADD IDENTITY column generation
2 parents 4024140 + 2835933 commit b3e6378

21 files changed

+637
-178
lines changed

README.md

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,6 @@
1818

1919
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.
2020

21-
## 📦 Packages
22-
23-
| Package | Description | Key Features |
24-
|---------|-------------|--------------|
25-
| [**pgsql-parser**](./packages/parser) | The real PostgreSQL parser for Node.js | • Uses actual PostgreSQL C parser via WebAssembly<br>• Symmetric parsing and deparsing<br>• Battle-tested with 23,000+ SQL statements |
26-
| [**pgsql-deparser**](./packages/deparser) | Lightning-fast SQL generation from AST | • Pure TypeScript, zero runtime dependencies<br>• No WebAssembly overhead<br>• Perfect for AST-to-SQL conversion only |
27-
| [**@pgsql/cli**](./packages/pgsql-cli) | Unified CLI for all PostgreSQL AST operations | • Parse SQL to AST<br>• Deparse AST to SQL<br>• Generate TypeScript from protobuf<br>• Single tool for all operations |
28-
| [**@pgsql/utils**](./packages/utils) | Type-safe AST node creation utilities | • Programmatic AST construction<br>• Runtime Schema<br>• Seamless integration with types |
29-
| [**pg-proto-parser**](./packages/proto-parser) | PostgreSQL protobuf parser and code generator | • Generate TypeScript interfaces from protobuf<br>• Create enum mappings and utilities<br>• AST helper generation |
30-
3121
## 🚀 Quick Start
3222

3323
### Installation
@@ -70,6 +60,41 @@ const sql = await deparse(ast);
7060
console.log(sql); // SELECT * FROM users WHERE id = 1
7161
```
7262

63+
#### Build AST with Types
64+
```typescript
65+
import { deparse } from 'pgsql-deparser';
66+
import { SelectStmt } from '@pgsql/types';
67+
68+
const stmt: { SelectStmt: SelectStmt } = {
69+
SelectStmt: {
70+
targetList: [
71+
{
72+
ResTarget: {
73+
val: {
74+
ColumnRef: {
75+
fields: [{ A_Star: {} }]
76+
}
77+
}
78+
}
79+
}
80+
],
81+
fromClause: [
82+
{
83+
RangeVar: {
84+
relname: 'some_table',
85+
inh: true,
86+
relpersistence: 'p'
87+
}
88+
}
89+
],
90+
limitOption: 'LIMIT_OPTION_DEFAULT',
91+
op: 'SETOP_NONE'
92+
}
93+
};
94+
95+
await deparse(stmt);
96+
```
97+
7398
#### Build AST Programmatically
7499
```typescript
75100
import * as t from '@pgsql/utils';
@@ -98,19 +123,16 @@ const stmt: { SelectStmt: SelectStmt } = t.nodes.selectStmt({
98123
await deparse(stmt);
99124
```
100125

101-
#### Use the CLI
102-
```bash
103-
npm install -g @pgsql/cli
104-
105-
# Parse SQL file
106-
pgsql parse query.sql
126+
## 📦 Packages
107127

108-
# Convert AST to SQL
109-
pgsql deparse ast.json
128+
| Package | Description | Key Features |
129+
|---------|-------------|--------------|
130+
| [**pgsql-parser**](./packages/parser) | The real PostgreSQL parser for Node.js | • Uses actual PostgreSQL C parser via WebAssembly<br>• Symmetric parsing and deparsing<br>• Battle-tested with 23,000+ SQL statements |
131+
| [**pgsql-deparser**](./packages/deparser) | Lightning-fast SQL generation from AST | • Pure TypeScript, zero runtime dependencies<br>• No WebAssembly overhead<br>• Perfect for AST-to-SQL conversion only |
132+
| [**@pgsql/cli**](./packages/pgsql-cli) | Unified CLI for all PostgreSQL AST operations | • Parse SQL to AST<br>• Deparse AST to SQL<br>• Generate TypeScript from protobuf<br>• Single tool for all operations |
133+
| [**@pgsql/utils**](./packages/utils) | Type-safe AST node creation utilities | • Programmatic AST construction<br>• Runtime Schema<br>• Seamless integration with types |
134+
| [**pg-proto-parser**](./packages/proto-parser) | PostgreSQL protobuf parser and code generator | • Generate TypeScript interfaces from protobuf<br>• Create enum mappings and utilities<br>• AST helper generation |
110135

111-
# Generate TypeScript from protobuf
112-
pgsql proto-gen --inFile pg_query.proto --outDir out --types --enums
113-
```
114136

115137
## 🛠️ Development
116138

__fixtures__/generated/generated.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21181,6 +21181,15 @@
2118121181
"original/alter/alter-95.sql": "ALTER TABLE mytable ADD COLUMN height_in numeric GENERATED ALWAYS AS (height_cm / 2.54) STORED",
2118221182
"original/alter/alter-96.sql": "ALTER SCHEMA schemaname RENAME TO newname",
2118321183
"original/alter/alter-97.sql": "ALTER SCHEMA schemaname OWNER TO newowner",
21184+
"original/alter/alter-table-column-1.sql": "ALTER TABLE public.table1 ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (\n SEQUENCE NAME public.table1\n START WITH 1\n INCREMENT BY 1\n NO MINVALUE\n NO MAXVALUE\n CACHE 1\n)",
21185+
"original/alter/alter-table-column-2.sql": "ALTER TABLE public.sales\nADD COLUMN total_price NUMERIC GENERATED ALWAYS AS (quantity * unit_price) STORED",
21186+
"original/alter/alter-table-column-3.sql": "ALTER TABLE public.comments\nADD COLUMN post_id INTEGER NOT NULL REFERENCES public.posts(id) ON DELETE CASCADE",
21187+
"original/alter/alter-table-column-4.sql": "ALTER TABLE public.devices\nADD COLUMN device_token UUID UNIQUE DEFAULT gen_random_uuid()",
21188+
"original/alter/alter-table-column-5.sql": "ALTER TABLE public.products\nADD COLUMN product_id BIGINT GENERATED BY DEFAULT AS IDENTITY (\n START WITH 5000\n INCREMENT BY 10\n)",
21189+
"original/alter/alter-table-column-6.sql": "ALTER TABLE public.users\nADD COLUMN name TEXT COLLATE \"fr_FR\"",
21190+
"original/alter/alter-table-column-7.sql": "ALTER TABLE public.books\nADD COLUMN tags TEXT[] DEFAULT '{}'",
21191+
"original/alter/alter-table-column-8.sql": "CREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral')",
21192+
"original/alter/alter-table-column-9.sql": "ALTER TABLE public.profiles\nADD COLUMN current_mood mood DEFAULT 'neutral'",
2118421193
"misc/quotes_etc-1.sql": "CREATE USER MAPPING FOR local_user SERVER \"foreign_server\" OPTIONS (user 'remote_user', password 'secret123')",
2118521194
"misc/quotes_etc-2.sql": "CREATE USER MAPPING FOR local_user SERVER foreign_server OPTIONS (user 'remote_user', password 'secret123')",
2118621195
"misc/quotes_etc-3.sql": "SELECT E'Line 1\\nLine 2'",
@@ -21211,6 +21220,23 @@
2121121220
"misc/quotes_etc-28.sql": "DO $$\nBEGIN\n RAISE NOTICE 'Line one\\nLine two';\nEND;\n$$ LANGUAGE plpgsql",
2121221221
"misc/quotes_etc-29.sql": "CREATE USER MAPPING FOR local_user SERVER \"foreign_server\" OPTIONS (user 'remote_user', password 'secret123')",
2121321222
"misc/quotes_etc-30.sql": "CREATE USER MAPPING FOR local_user SERVER foreign_server OPTIONS (user 'remote_user', password 'secret123')",
21223+
"misc/pg_catalog-1.sql": "SELECT json_object('{}')",
21224+
"misc/pg_catalog-2.sql": "SELECT * FROM generate_series(1, 5)",
21225+
"misc/pg_catalog-3.sql": "SELECT get_byte(E'\\\\xDEADBEEF'::bytea, 1)",
21226+
"misc/pg_catalog-4.sql": "SELECT now()",
21227+
"misc/pg_catalog-5.sql": "SELECT clock_timestamp()",
21228+
"misc/pg_catalog-6.sql": "SELECT to_char(now(), 'YYYY-MM-DD HH24:MI:SS')",
21229+
"misc/pg_catalog-7.sql": "SELECT json_build_object('name', 'Alice', 'age', 30)",
21230+
"misc/pg_catalog-8.sql": "SELECT pg_typeof(42), pg_typeof('hello'), pg_typeof(now())",
21231+
"misc/pg_catalog-9.sql": "SELECT substring('abcdefg' FROM 2 FOR 3)",
21232+
"misc/pg_catalog-10.sql": "SELECT replace('hello world', 'l', 'L')",
21233+
"misc/pg_catalog-11.sql": "SELECT length('yolo')",
21234+
"misc/pg_catalog-12.sql": "SELECT position('G' IN 'ChatGPT')",
21235+
"misc/pg_catalog-13.sql": "SELECT trim(' padded text ')",
21236+
"misc/pg_catalog-14.sql": "SELECT ltrim('---abc', '-')",
21237+
"misc/pg_catalog-15.sql": "SELECT array_agg(id) FROM (VALUES (1), (2), (3)) AS t(id)",
21238+
"misc/pg_catalog-16.sql": "SELECT string_agg(name, ', ') FROM (VALUES ('Alice'), ('Bob'), ('Carol')) AS t(name)",
21239+
"misc/pg_catalog-17.sql": "SELECT json_agg(name) FROM (VALUES ('A'), ('B')) AS t(name)",
2121421240
"misc/launchql-ext-types-1.sql": "CREATE DOMAIN attachment AS jsonb CHECK ( value ?& ARRAY['url', 'mime'] AND (value->>'url') ~ '^(https?)://[^\\s/$.?#].[^\\s]*$' )",
2121521241
"misc/launchql-ext-types-2.sql": "COMMENT ON DOMAIN attachment IS E'@name launchqlInternalTypeAttachment'",
2121621242
"misc/launchql-ext-types-3.sql": "CREATE DOMAIN email AS citext CHECK ( value ~ '^[a-zA-Z0-9.!#$%&''*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$' )",
@@ -21889,6 +21915,10 @@
2188921915
"latest/postgres/create_table-39.sql": "DROP FUNCTION plusone(INT)",
2189021916
"latest/postgres/create_table-40.sql": "DROP TYPE comp_type",
2189121917
"latest/postgres/create_table-41.sql": "DROP DOMAIN posint",
21918+
"latest/postgres/create_table-42.sql": "CREATE TABLE generated_cols (\n a INT,\n b INT GENERATED ALWAYS AS (a * 2) STORED\n)",
21919+
"latest/postgres/create_table-43.sql": "CREATE TYPE comp_type AS (x INT, y TEXT)",
21920+
"latest/postgres/create_table-44.sql": "CREATE TABLE uses_comp (\n id INT,\n data comp_type\n)",
21921+
"latest/postgres/create_table-45.sql": "CREATE TABLE public.users (\n user_id INTEGER GENERATED ALWAYS AS IDENTITY (\n START WITH 1000\n INCREMENT BY 5\n CACHE 10\n ),\n username TEXT NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n)",
2189221922
"latest/postgres/create_schema-1.sql": "CREATE ROLE regress_create_schema_role SUPERUSER",
2189321923
"latest/postgres/create_schema-2.sql": "CREATE SCHEMA AUTHORIZATION regress_create_schema_role\n CREATE SEQUENCE schema_not_existing.seq",
2189421924
"latest/postgres/create_schema-3.sql": "CREATE SCHEMA AUTHORIZATION regress_create_schema_role\n CREATE TABLE schema_not_existing.tab (id int)",

__fixtures__/kitchen-sink/latest/postgres/create_table.sql

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,28 @@ CREATE TABLE mlvl_leaf PARTITION OF mlvl_sub FOR VALUES FROM (1) TO (10);
126126
DROP FUNCTION plusone(INT);
127127
DROP TYPE comp_type;
128128
DROP DOMAIN posint;
129+
130+
-- generated columns
131+
CREATE TABLE generated_cols (
132+
a INT,
133+
b INT GENERATED ALWAYS AS (a * 2) STORED
134+
);
135+
136+
-- composite types
137+
CREATE TYPE comp_type AS (x INT, y TEXT);
138+
CREATE TABLE uses_comp (
139+
id INT,
140+
data comp_type
141+
);
142+
143+
-- generated columns
144+
CREATE TABLE public.users (
145+
user_id INTEGER GENERATED ALWAYS AS IDENTITY (
146+
START WITH 1000
147+
INCREMENT BY 5
148+
CACHE 10
149+
),
150+
username TEXT NOT NULL,
151+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
152+
);
153+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
ALTER TABLE public.table1 ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
2+
SEQUENCE NAME public.table1
3+
START WITH 1
4+
INCREMENT BY 1
5+
NO MINVALUE
6+
NO MAXVALUE
7+
CACHE 1
8+
);
9+
10+
ALTER TABLE public.sales
11+
ADD COLUMN total_price NUMERIC GENERATED ALWAYS AS (quantity * unit_price) STORED;
12+
13+
ALTER TABLE public.comments
14+
ADD COLUMN post_id INTEGER NOT NULL REFERENCES public.posts(id) ON DELETE CASCADE;
15+
16+
ALTER TABLE public.devices
17+
ADD COLUMN device_token UUID UNIQUE DEFAULT gen_random_uuid();
18+
19+
ALTER TABLE public.products
20+
ADD COLUMN product_id BIGINT GENERATED BY DEFAULT AS IDENTITY (
21+
START WITH 5000
22+
INCREMENT BY 10
23+
);
24+
25+
ALTER TABLE public.users
26+
ADD COLUMN name TEXT COLLATE "fr_FR";
27+
28+
ALTER TABLE public.books
29+
ADD COLUMN tags TEXT[] DEFAULT '{}';
30+
31+
CREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral');
32+
33+
ALTER TABLE public.profiles
34+
ADD COLUMN current_mood mood DEFAULT 'neutral';

packages/deparser/__tests__/kitchen-sink/latest-postgres-create_table.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ it('latest-postgres-create_table', async () => {
4444
"latest/postgres/create_table-38.sql",
4545
"latest/postgres/create_table-39.sql",
4646
"latest/postgres/create_table-40.sql",
47-
"latest/postgres/create_table-41.sql"
47+
"latest/postgres/create_table-41.sql",
48+
"latest/postgres/create_table-42.sql",
49+
"latest/postgres/create_table-43.sql",
50+
"latest/postgres/create_table-44.sql",
51+
"latest/postgres/create_table-45.sql"
4852
]);
4953
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
import { FixtureTestUtils } from '../../test-utils';
3+
const fixtures = new FixtureTestUtils();
4+
5+
it('misc-pg_catalog', async () => {
6+
await fixtures.runFixtureTests([
7+
"misc/pg_catalog-1.sql",
8+
"misc/pg_catalog-2.sql",
9+
"misc/pg_catalog-3.sql",
10+
"misc/pg_catalog-4.sql",
11+
"misc/pg_catalog-5.sql",
12+
"misc/pg_catalog-6.sql",
13+
"misc/pg_catalog-7.sql",
14+
"misc/pg_catalog-8.sql",
15+
"misc/pg_catalog-9.sql",
16+
"misc/pg_catalog-10.sql",
17+
"misc/pg_catalog-11.sql",
18+
"misc/pg_catalog-12.sql",
19+
"misc/pg_catalog-13.sql",
20+
"misc/pg_catalog-14.sql",
21+
"misc/pg_catalog-15.sql",
22+
"misc/pg_catalog-16.sql",
23+
"misc/pg_catalog-17.sql"
24+
]);
25+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
import { FixtureTestUtils } from '../../test-utils';
3+
const fixtures = new FixtureTestUtils();
4+
5+
it('original-alter-alter-table-column', async () => {
6+
await fixtures.runFixtureTests([
7+
"original/alter/alter-table-column-1.sql",
8+
"original/alter/alter-table-column-2.sql",
9+
"original/alter/alter-table-column-3.sql",
10+
"original/alter/alter-table-column-4.sql",
11+
"original/alter/alter-table-column-5.sql",
12+
"original/alter/alter-table-column-6.sql",
13+
"original/alter/alter-table-column-7.sql",
14+
"original/alter/alter-table-column-8.sql",
15+
"original/alter/alter-table-column-9.sql"
16+
]);
17+
});
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`non-pretty: original/alter/alter-table-column-1.sql 1`] = `"ALTER TABLE public.table1 ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (SEQUENCE NAME public.table1 START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1)"`;
4+
5+
exports[`non-pretty: original/alter/alter-table-column-2.sql 1`] = `"ALTER TABLE public.sales ADD COLUMN total_price numeric GENERATED ALWAYS AS (quantity * unit_price) STORED"`;
6+
7+
exports[`non-pretty: original/alter/alter-table-column-3.sql 1`] = `"ALTER TABLE public.comments ADD COLUMN post_id int NOT NULL REFERENCES public.posts (id) ON DELETE CASCADE"`;
8+
9+
exports[`non-pretty: original/alter/alter-table-column-4.sql 1`] = `"ALTER TABLE public.devices ADD COLUMN device_token uuid UNIQUE DEFAULT gen_random_uuid()"`;
10+
11+
exports[`non-pretty: original/alter/alter-table-column-5.sql 1`] = `"ALTER TABLE public.products ADD COLUMN product_id bigint GENERATED BY DEFAULT AS IDENTITY (START WITH 5000 INCREMENT BY 10)"`;
12+
13+
exports[`non-pretty: original/alter/alter-table-column-6.sql 1`] = `"ALTER TABLE public.users ADD COLUMN name text COLLATE "fr_FR""`;
14+
15+
exports[`non-pretty: original/alter/alter-table-column-7.sql 1`] = `"ALTER TABLE public.books ADD COLUMN tags text[] DEFAULT '{}'"`;
16+
17+
exports[`non-pretty: original/alter/alter-table-column-8.sql 1`] = `"CREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral')"`;
18+
19+
exports[`non-pretty: original/alter/alter-table-column-9.sql 1`] = `"ALTER TABLE public.profiles ADD COLUMN current_mood mood DEFAULT 'neutral'"`;
20+
21+
exports[`pretty: original/alter/alter-table-column-1.sql 1`] = `
22+
"ALTER TABLE public.table1
23+
ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
24+
SEQUENCE NAME public.table1
25+
START WITH 1
26+
INCREMENT BY 1
27+
NO MINVALUE
28+
NO MAXVALUE
29+
CACHE 1
30+
)"
31+
`;
32+
33+
exports[`pretty: original/alter/alter-table-column-2.sql 1`] = `
34+
"ALTER TABLE public.sales
35+
ADD COLUMN total_price numeric
36+
GENERATED ALWAYS AS (quantity * unit_price) STORED"
37+
`;
38+
39+
exports[`pretty: original/alter/alter-table-column-3.sql 1`] = `
40+
"ALTER TABLE public.comments
41+
ADD COLUMN post_id int
42+
NOT NULL
43+
REFERENCES public.posts (id)
44+
ON DELETE CASCADE"
45+
`;
46+
47+
exports[`pretty: original/alter/alter-table-column-4.sql 1`] = `
48+
"ALTER TABLE public.devices
49+
ADD COLUMN device_token uuid
50+
UNIQUE
51+
DEFAULT gen_random_uuid()"
52+
`;
53+
54+
exports[`pretty: original/alter/alter-table-column-5.sql 1`] = `
55+
"ALTER TABLE public.products
56+
ADD COLUMN product_id bigint
57+
GENERATED BY DEFAULT AS IDENTITY (
58+
START WITH 5000
59+
INCREMENT BY 10
60+
)"
61+
`;
62+
63+
exports[`pretty: original/alter/alter-table-column-6.sql 1`] = `
64+
"ALTER TABLE public.users
65+
ADD COLUMN name text
66+
COLLATE "fr_FR""
67+
`;
68+
69+
exports[`pretty: original/alter/alter-table-column-7.sql 1`] = `
70+
"ALTER TABLE public.books
71+
ADD COLUMN tags text[]
72+
DEFAULT '{}'"
73+
`;
74+
75+
exports[`pretty: original/alter/alter-table-column-8.sql 1`] = `"CREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral')"`;
76+
77+
exports[`pretty: original/alter/alter-table-column-9.sql 1`] = `
78+
"ALTER TABLE public.profiles
79+
ADD COLUMN current_mood mood
80+
DEFAULT 'neutral'"
81+
`;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { PrettyTest } from '../../test-utils/PrettyTest';
2+
const prettyTest = new PrettyTest([
3+
"original/alter/alter-table-column-1.sql",
4+
"original/alter/alter-table-column-2.sql",
5+
"original/alter/alter-table-column-3.sql",
6+
"original/alter/alter-table-column-4.sql",
7+
"original/alter/alter-table-column-5.sql",
8+
"original/alter/alter-table-column-6.sql",
9+
"original/alter/alter-table-column-7.sql",
10+
"original/alter/alter-table-column-8.sql",
11+
"original/alter/alter-table-column-9.sql"
12+
]);
13+
14+
prettyTest.generateTests();

0 commit comments

Comments
 (0)