Skip to content

Commit d83f333

Browse files
authored
Merge pull request #281 from constructive-io/devin/1769151809-plpgsql-row-fields-out-params
fix: normalize INTO clause whitespace + add OUT parameter test fixtures
2 parents 132971c + f9012e0 commit d83f333

File tree

9 files changed

+265
-13
lines changed

9 files changed

+265
-13
lines changed

__fixtures__/plpgsql-generated/generated.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,10 @@
100100
"plpgsql_deparser_fixes-11.sql": "-- Test 11: OUT parameter function with bare RETURN\nCREATE FUNCTION test_out_params(OUT ok boolean, OUT message text)\nLANGUAGE plpgsql AS $$\nBEGIN\n ok := true;\n message := 'success';\n RETURN;\nEND$$",
101101
"plpgsql_deparser_fixes-12.sql": "-- Test 12: RETURNS TABLE function with RETURN QUERY\nCREATE FUNCTION test_returns_table(p_prefix text)\nRETURNS TABLE(id int, name text)\nLANGUAGE plpgsql AS $$\nBEGIN\n RETURN QUERY SELECT 1, p_prefix || '_one';\n RETURN QUERY SELECT 2, p_prefix || '_two';\n RETURN;\nEND$$",
102102
"plpgsql_deparser_fixes-13.sql": "-- Test 13: Trigger function with complex logic\nCREATE FUNCTION test_trigger_complex() RETURNS trigger\nLANGUAGE plpgsql AS $$\nDECLARE\n defaults_record record;\n bit_len int;\nBEGIN\n bit_len := bit_length(NEW.permissions);\n \n SELECT * INTO defaults_record\n FROM permission_defaults AS t\n LIMIT 1;\n \n IF found THEN\n NEW.is_approved := defaults_record.is_approved;\n NEW.is_verified := defaults_record.is_verified;\n END IF;\n \n IF NEW.is_owner IS TRUE THEN\n NEW.is_admin := true;\n NEW.is_approved := true;\n NEW.is_verified := true;\n END IF;\n \n SELECT\n NEW.is_approved IS TRUE\n AND NEW.is_verified IS TRUE\n AND NEW.is_disabled IS FALSE INTO NEW.is_active;\n \n RETURN NEW;\nEND$$",
103-
"plpgsql_deparser_fixes-14.sql": "-- Test 14: Procedure (implicit void return)\nCREATE PROCEDURE test_procedure(p_message text)\nLANGUAGE plpgsql AS $$\nBEGIN\n RAISE NOTICE '%', p_message;\nEND$$",
104-
"plpgsql_control-1.sql": "--\n-- Tests for PL/pgSQL control structures\n--\n\n-- integer FOR loop\n\ndo $$\nbegin\n -- basic case\n for i in 1..3 loop\n raise notice '1..3: i = %', i;\n end loop;\n -- with BY, end matches exactly\n for i in 1..10 by 3 loop\n raise notice '1..10 by 3: i = %', i;\n end loop;\n -- with BY, end does not match\n for i in 1..11 by 3 loop\n raise notice '1..11 by 3: i = %', i;\n end loop;\n -- zero iterations\n for i in 1..0 by 3 loop\n raise notice '1..0 by 3: i = %', i;\n end loop;\n -- REVERSE\n for i in reverse 10..0 by 3 loop\n raise notice 'reverse 10..0 by 3: i = %', i;\n end loop;\n -- potential overflow\n for i in 2147483620..2147483647 by 10 loop\n raise notice '2147483620..2147483647 by 10: i = %', i;\n end loop;\n -- potential overflow, reverse direction\n for i in reverse -2147483620..-2147483647 by 10 loop\n raise notice 'reverse -2147483620..-2147483647 by 10: i = %', i;\n end loop;\nend$$",
103+
"plpgsql_deparser_fixes-14.sql": "-- Test 14: Procedure (implicit void return)\nCREATE PROCEDURE test_procedure(p_message text)\nLANGUAGE plpgsql AS $$\nBEGIN\n RAISE NOTICE '%', p_message;\nEND$$",
104+
"plpgsql_deparser_fixes-15.sql": "-- Test 15: OUT parameters with SELECT INTO multiple variables\n-- This pattern is used in auth functions (sign_in, sign_up) where we need to\n-- populate multiple OUT parameters from a single SELECT statement\nCREATE FUNCTION test_out_params_select_into(\n p_user_id uuid,\n OUT id uuid,\n OUT user_id uuid,\n OUT access_token text,\n OUT access_token_expires_at timestamptz,\n OUT is_verified boolean,\n OUT totp_enabled boolean\n)\nLANGUAGE plpgsql AS $$\nDECLARE\n v_token_id uuid;\n v_plaintext_token text;\nBEGIN\n v_plaintext_token := encode(gen_random_bytes(48), 'hex');\n v_token_id := uuid_generate_v5(uuid_ns_url(), v_plaintext_token);\n \n INSERT INTO tokens (id, user_id, access_token_hash)\n VALUES (v_token_id, p_user_id, digest(v_plaintext_token, 'sha256'));\n \n SELECT tkn.id, tkn.user_id, v_plaintext_token, tkn.access_token_expires_at, tkn.is_verified, tkn.totp_enabled\n INTO id, user_id, access_token, access_token_expires_at, is_verified, totp_enabled\n FROM tokens AS tkn\n WHERE tkn.id = v_token_id;\n \n RETURN;\nEND$$",
105+
"plpgsql_deparser_fixes-16.sql": "-- Test 16: OUT parameters with SELECT INTO and STRICT\nCREATE FUNCTION test_out_params_strict(\n p_id uuid,\n OUT name text,\n OUT email text\n)\nLANGUAGE plpgsql AS $$\nBEGIN\n SELECT u.name, u.email INTO STRICT name, email\n FROM users u\n WHERE u.id = p_id;\nEND$$",
106+
"plpgsql_control-1.sql":"--\n-- Tests for PL/pgSQL control structures\n--\n\n-- integer FOR loop\n\ndo $$\nbegin\n -- basic case\n for i in 1..3 loop\n raise notice '1..3: i = %', i;\n end loop;\n -- with BY, end matches exactly\n for i in 1..10 by 3 loop\n raise notice '1..10 by 3: i = %', i;\n end loop;\n -- with BY, end does not match\n for i in 1..11 by 3 loop\n raise notice '1..11 by 3: i = %', i;\n end loop;\n -- zero iterations\n for i in 1..0 by 3 loop\n raise notice '1..0 by 3: i = %', i;\n end loop;\n -- REVERSE\n for i in reverse 10..0 by 3 loop\n raise notice 'reverse 10..0 by 3: i = %', i;\n end loop;\n -- potential overflow\n for i in 2147483620..2147483647 by 10 loop\n raise notice '2147483620..2147483647 by 10: i = %', i;\n end loop;\n -- potential overflow, reverse direction\n for i in reverse -2147483620..-2147483647 by 10 loop\n raise notice 'reverse -2147483620..-2147483647 by 10: i = %', i;\n end loop;\nend$$",
105107
"plpgsql_control-2.sql": "-- BY can't be zero or negative\ndo $$\nbegin\n for i in 1..3 by 0 loop\n raise notice '1..3 by 0: i = %', i;\n end loop;\nend$$",
106108
"plpgsql_control-3.sql": "do $$\nbegin\n for i in 1..3 by -1 loop\n raise notice '1..3 by -1: i = %', i;\n end loop;\nend$$",
107109
"plpgsql_control-4.sql": "do $$\nbegin\n for i in reverse 1..3 by -1 loop\n raise notice 'reverse 1..3 by -1: i = %', i;\n end loop;\nend$$",
@@ -189,4 +191,4 @@
189191
"plpgsql_array-21.sql": "-- some types don't support arrays\ndo $$\ndeclare\n v pg_node_tree;\n v1 v%type[];\nbegin\nend;\n$$",
190192
"plpgsql_array-22.sql": "-- check functionality\ndo $$\ndeclare\n v1 int;\n v2 varchar;\n a1 v1%type[];\n a2 v2%type[];\nbegin\n v1 := 10;\n v2 := 'Hi';\n a1 := array[v1,v1];\n a2 := array[v2,v2];\n raise notice '% %', a1, a2;\nend;\n$$",
191193
"plpgsql_array-23.sql": "do $$\ndeclare tg array_test_table%rowtype[];\nbegin\n tg := array(select array_test_table from array_test_table);\n raise notice '%', tg;\n tg := array(select row(a,b) from array_test_table);\n raise notice '%', tg;\nend;\n$$"
192-
}
194+
}

__fixtures__/plpgsql/plpgsql_deparser_fixes.sql

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,47 @@ LANGUAGE plpgsql AS $$
162162
BEGIN
163163
RAISE NOTICE '%', p_message;
164164
END$$;
165+
166+
-- Test 15: OUT parameters with SELECT INTO multiple variables
167+
-- This pattern is used in auth functions (sign_in, sign_up) where we need to
168+
-- populate multiple OUT parameters from a single SELECT statement
169+
CREATE FUNCTION test_out_params_select_into(
170+
p_user_id uuid,
171+
OUT id uuid,
172+
OUT user_id uuid,
173+
OUT access_token text,
174+
OUT access_token_expires_at timestamptz,
175+
OUT is_verified boolean,
176+
OUT totp_enabled boolean
177+
)
178+
LANGUAGE plpgsql AS $$
179+
DECLARE
180+
v_token_id uuid;
181+
v_plaintext_token text;
182+
BEGIN
183+
v_plaintext_token := encode(gen_random_bytes(48), 'hex');
184+
v_token_id := uuid_generate_v5(uuid_ns_url(), v_plaintext_token);
185+
186+
INSERT INTO tokens (id, user_id, access_token_hash)
187+
VALUES (v_token_id, p_user_id, digest(v_plaintext_token, 'sha256'));
188+
189+
SELECT tkn.id, tkn.user_id, v_plaintext_token, tkn.access_token_expires_at, tkn.is_verified, tkn.totp_enabled
190+
INTO id, user_id, access_token, access_token_expires_at, is_verified, totp_enabled
191+
FROM tokens AS tkn
192+
WHERE tkn.id = v_token_id;
193+
194+
RETURN;
195+
END$$;
196+
197+
-- Test 16: OUT parameters with SELECT INTO and STRICT
198+
CREATE FUNCTION test_out_params_strict(
199+
p_id uuid,
200+
OUT name text,
201+
OUT email text
202+
)
203+
LANGUAGE plpgsql AS $$
204+
BEGIN
205+
SELECT u.name, u.email INTO STRICT name, email
206+
FROM users u
207+
WHERE u.id = p_id;
208+
END$$;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# PL/pgSQL Deparser - Agent Instructions
2+
3+
## Adding Test Fixtures
4+
5+
When adding new test fixtures for the PL/pgSQL deparser, follow this workflow:
6+
7+
### Step 1: Add SQL Fixtures
8+
9+
Add your PL/pgSQL function/procedure definitions to the appropriate fixture file in `__fixtures__/plpgsql/`. For deparser-specific fixes, use `plpgsql_deparser_fixes.sql`.
10+
11+
Example fixture:
12+
```sql
13+
-- Test N: Description of what this tests
14+
CREATE FUNCTION test_example(p_input text, OUT result text)
15+
LANGUAGE plpgsql AS $$
16+
BEGIN
17+
result := p_input;
18+
RETURN;
19+
END$$;
20+
```
21+
22+
### Step 2: Generate Test Fixtures
23+
24+
Run the fixture generation script from the plpgsql-deparser package:
25+
26+
```bash
27+
cd packages/plpgsql-deparser
28+
pnpm fixtures
29+
```
30+
31+
This script (`scripts/make-fixtures.ts`):
32+
1. Reads all `.sql` files from `__fixtures__/plpgsql/`
33+
2. Parses each file to extract PL/pgSQL statements (CREATE FUNCTION, CREATE PROCEDURE, DO blocks)
34+
3. Validates each statement can be parsed by the PL/pgSQL parser
35+
4. Outputs valid fixtures to `__fixtures__/plpgsql-generated/generated.json`
36+
37+
### Step 3: Run Tests
38+
39+
Run the test suite to verify your fixtures round-trip correctly:
40+
41+
```bash
42+
cd packages/plpgsql-deparser
43+
pnpm test
44+
```
45+
46+
The round-trip test (`__tests__/plpgsql-deparser.test.ts`):
47+
1. Loads all fixtures from `generated.json`
48+
2. For each fixture: parse -> deparse -> reparse
49+
3. Compares the AST from original parse with the AST from reparsed output
50+
4. Reports any failures (AST mismatches or reparse failures)
51+
52+
### Step 4: Add Snapshot Tests (Optional but Recommended)
53+
54+
For important deparser fixes, add explicit test cases with snapshots to `__tests__/deparser-fixes.test.ts`:
55+
56+
```typescript
57+
it('should handle [description]', async () => {
58+
const sql = `CREATE FUNCTION test_example(...)
59+
LANGUAGE plpgsql AS $$
60+
BEGIN
61+
-- your test case
62+
END$$`;
63+
64+
await testUtils.expectAstMatch('description', sql);
65+
66+
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
67+
const deparsed = deparseSync(parsed);
68+
expect(deparsed).toMatchSnapshot();
69+
// Add specific assertions
70+
expect(deparsed).toContain('expected output');
71+
});
72+
```
73+
74+
Then run tests with snapshot update:
75+
76+
```bash
77+
pnpm test --updateSnapshot
78+
```
79+
80+
### Step 5: Commit All Files
81+
82+
Always commit the fixture file, generated.json, test file, AND snapshots together:
83+
84+
```bash
85+
git add __fixtures__/plpgsql/plpgsql_deparser_fixes.sql
86+
git add __fixtures__/plpgsql-generated/generated.json
87+
git add packages/plpgsql-deparser/__tests__/deparser-fixes.test.ts
88+
git add packages/plpgsql-deparser/__tests__/__snapshots__/deparser-fixes.test.ts.snap
89+
git commit -m "test: add fixtures for [description]"
90+
```
91+
92+
## Important Notes
93+
94+
- The `generated.json` file is the source of truth for tests - it must be regenerated when fixtures change
95+
- Fixtures that fail PL/pgSQL parsing are skipped (logged as warnings during generation)
96+
- The test suite has a `KNOWN_FAILING_FIXTURES` set for fixtures with known issues - avoid adding to this unless necessary
97+
- When adding fixtures for new deparser features, ensure the fixture exercises the specific AST pattern you're testing
98+
99+
## Fixture File Conventions
100+
101+
- `plpgsql_deparser_fixes.sql` - Fixtures for deparser bug fixes and edge cases
102+
- `plpgsql_*.sql` - PostgreSQL regression test fixtures (from upstream)
103+
- Each fixture should have a comment describing what it tests
104+
- Number fixtures sequentially (Test 1, Test 2, etc.) within each file

packages/plpgsql-deparser/__tests__/__snapshots__/deparser-fixes.test.ts.snap

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,30 @@ BEGIN
7070
END"
7171
`;
7272

73+
exports[`plpgsql-deparser bug fixes OUT parameters with SELECT INTO multiple variables should handle SELECT INTO STRICT with multiple OUT parameters 1`] = `
74+
"BEGIN
75+
SELECT u.name, u.email INTO STRICT name, email FROM users u
76+
WHERE u.id = p_id;
77+
RETURN;
78+
END"
79+
`;
80+
81+
exports[`plpgsql-deparser bug fixes OUT parameters with SELECT INTO multiple variables should handle SELECT INTO multiple OUT parameters 1`] = `
82+
"DECLARE
83+
v_token_id uuid;
84+
v_plaintext_token text;
85+
BEGIN
86+
v_plaintext_token := encode(gen_random_bytes(48), 'hex');
87+
v_token_id := uuid_generate_v5(uuid_ns_url(), v_plaintext_token);
88+
INSERT INTO tokens (id, user_id, access_token_hash)
89+
VALUES (v_token_id, p_user_id, digest(v_plaintext_token, 'sha256'));
90+
SELECT tkn.id, tkn.user_id, v_plaintext_token, tkn.access_token_expires_at, tkn.is_verified, tkn.totp_enabled INTO id, user_id, access_token, access_token_expires_at, is_verified, totp_enabled
91+
FROM tokens AS tkn
92+
WHERE tkn.id = v_token_id;
93+
RETURN;
94+
END"
95+
`;
96+
7397
exports[`plpgsql-deparser bug fixes PERFORM SELECT fix should handle PERFORM with complex expressions 1`] = `
7498
"BEGIN
7599
PERFORM set_config('search_path', 'public', true);

packages/plpgsql-deparser/__tests__/__snapshots__/hydrate-demo.test.ts.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ BEGIN
9898
t.orders_scanned,
9999
t.gross_total,
100100
t.avg_total INTO v_orders_scanned, v_gross, v_avg
101-
FROM totals AS t;
101+
FROM totals AS t;
102102
IF p_apply_discount THEN
103103
v_rebate := round(v_gross * GREATEST(LEAST(v_discount_rate + v_jitter, 0.50), 0), p_round_to);
104104
ELSE
@@ -109,7 +109,7 @@ BEGIN
109109
SELECT
110110
oi.sku,
111111
CAST(sum(oi.quantity) AS bigint) AS qty INTO v_top_sku, v_top_sku_qty
112-
FROM app_public.order_item AS oi
112+
FROM app_public.order_item AS oi
113113
JOIN app_public.app_order AS o ON o.id = oi.order_id
114114
WHERE
115115
o.org_id = p_org_id

packages/plpgsql-deparser/__tests__/__snapshots__/schema-rename-mapped.test.ts.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ exports[`schema rename mapped should transform schema names and snapshot schema
194194
total_count int;
195195
BEGIN
196196
SELECT count(*) INTO total_count
197-
FROM myapp_v2.users AS u
197+
FROM myapp_v2.users AS u
198198
JOIN myapp_v2.orders AS o ON o.user_id = u.id
199199
WHERE
200200
u.id = p_user_id;
@@ -270,7 +270,7 @@ CREATE FUNCTION myapp_v2.calculate_order_total(
270270
discount numeric;
271271
BEGIN
272272
SELECT sum(quantity * price) INTO subtotal
273-
FROM myapp_v2.order_items
273+
FROM myapp_v2.order_items
274274
WHERE
275275
order_id = p_order_id;
276276
tax_amount := myapp_v2.get_tax_rate() * subtotal;

packages/plpgsql-deparser/__tests__/deparser-fixes.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,69 @@ $$`;
307307
});
308308
});
309309

310+
describe('OUT parameters with SELECT INTO multiple variables', () => {
311+
it('should handle SELECT INTO multiple OUT parameters', async () => {
312+
const sql = `CREATE FUNCTION test_out_params_select_into(
313+
p_user_id uuid,
314+
OUT id uuid,
315+
OUT user_id uuid,
316+
OUT access_token text,
317+
OUT access_token_expires_at timestamptz,
318+
OUT is_verified boolean,
319+
OUT totp_enabled boolean
320+
)
321+
LANGUAGE plpgsql AS $$
322+
DECLARE
323+
v_token_id uuid;
324+
v_plaintext_token text;
325+
BEGIN
326+
v_plaintext_token := encode(gen_random_bytes(48), 'hex');
327+
v_token_id := uuid_generate_v5(uuid_ns_url(), v_plaintext_token);
328+
329+
INSERT INTO tokens (id, user_id, access_token_hash)
330+
VALUES (v_token_id, p_user_id, digest(v_plaintext_token, 'sha256'));
331+
332+
SELECT tkn.id, tkn.user_id, v_plaintext_token, tkn.access_token_expires_at, tkn.is_verified, tkn.totp_enabled
333+
INTO id, user_id, access_token, access_token_expires_at, is_verified, totp_enabled
334+
FROM tokens AS tkn
335+
WHERE tkn.id = v_token_id;
336+
337+
RETURN;
338+
END$$`;
339+
340+
await testUtils.expectAstMatch('OUT params SELECT INTO', sql);
341+
342+
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
343+
const deparsed = deparseSync(parsed);
344+
expect(deparsed).toMatchSnapshot();
345+
// Verify multiple INTO targets are present
346+
expect(deparsed).toMatch(/INTO\s+id\s*,\s*user_id\s*,\s*access_token/i);
347+
});
348+
349+
it('should handle SELECT INTO STRICT with multiple OUT parameters', async () => {
350+
const sql = `CREATE FUNCTION test_out_params_strict(
351+
p_id uuid,
352+
OUT name text,
353+
OUT email text
354+
)
355+
LANGUAGE plpgsql AS $$
356+
BEGIN
357+
SELECT u.name, u.email INTO STRICT name, email
358+
FROM users u
359+
WHERE u.id = p_id;
360+
END$$`;
361+
362+
await testUtils.expectAstMatch('OUT params STRICT', sql);
363+
364+
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
365+
const deparsed = deparseSync(parsed);
366+
expect(deparsed).toMatchSnapshot();
367+
expect(deparsed).toContain('STRICT');
368+
// Verify multiple INTO targets are present
369+
expect(deparsed).toMatch(/INTO\s+STRICT\s+name\s*,\s*email/i);
370+
});
371+
});
372+
310373
describe('combined scenarios', () => {
311374
it('should handle PERFORM with record fields', async () => {
312375
const sql = `CREATE FUNCTION test_perform_record() RETURNS trigger

packages/plpgsql-deparser/__tests__/pretty/__snapshots__/plpgsql-pretty.test.ts.snap

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ begin
6666
t.orders_scanned,
6767
t.gross_total,
6868
t.avg_total into v_orders_scanned, v_gross, v_avg
69-
FROM totals t;
69+
FROM totals t;
7070
if p_apply_discount then
7171
v_discount := round(v_gross * GREATEST(LEAST(v_discount_rate + v_jitter, 0.50), 0), p_round_to);
7272
else
@@ -77,7 +77,7 @@ begin
7777
SELECT
7878
oi.sku,
7979
sum(oi.quantity)::bigint AS qty into v_top_sku, v_top_sku_qty
80-
FROM app_public.order_item oi
80+
FROM app_public.order_item oi
8181
JOIN app_public.app_order o ON o.id = oi.order_id
8282
WHERE o.org_id = p_org_id
8383
AND o.user_id = p_user_id
@@ -295,7 +295,7 @@ BEGIN
295295
t.orders_scanned,
296296
t.gross_total,
297297
t.avg_total INTO v_orders_scanned, v_gross, v_avg
298-
FROM totals t;
298+
FROM totals t;
299299
IF p_apply_discount THEN
300300
v_discount := round(v_gross * GREATEST(LEAST(v_discount_rate + v_jitter, 0.50), 0), p_round_to);
301301
ELSE
@@ -306,7 +306,7 @@ BEGIN
306306
SELECT
307307
oi.sku,
308308
sum(oi.quantity)::bigint AS qty INTO v_top_sku, v_top_sku_qty
309-
FROM app_public.order_item oi
309+
FROM app_public.order_item oi
310310
JOIN app_public.app_order o ON o.id = oi.order_id
311311
WHERE o.org_id = p_org_id
312312
AND o.user_id = p_user_id

packages/plpgsql-deparser/src/plpgsql-deparser.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1485,8 +1485,23 @@ export class PLpgSQLDeparser {
14851485
// large gaps like "SELECT x INTO y FROM z"
14861486
const before = sql.slice(0, insertPos);
14871487
let after = sql.slice(insertPos);
1488-
// Collapse leading whitespace (but preserve a single space before the next keyword)
1489-
after = after.replace(/^[ \t]+/, ' ');
1488+
// Normalize whitespace after INTO insertion
1489+
// The parser strips "INTO <target>" but leaves whitespace behind, which can cause
1490+
// weird formatting like "SELECT x INTO y FROM z"
1491+
// We collapse all whitespace to either a single space or a newline with standard indent
1492+
const leadingWsMatch = after.match(/^(\s+)/);
1493+
if (leadingWsMatch) {
1494+
const ws = leadingWsMatch[1];
1495+
const hasNewline = /\n/.test(ws);
1496+
if (hasNewline) {
1497+
// If original had newlines, use a newline with standard 4-space indent
1498+
// This normalizes any weird indentation left by the parser
1499+
after = after.replace(/^\s+/, '\n ');
1500+
} else {
1501+
// If original was just spaces/tabs, collapse to single space
1502+
after = after.replace(/^[ \t]+/, ' ');
1503+
}
1504+
}
14901505
sql = before + intoClause + after;
14911506
} else {
14921507
// -1 means INTO already exists at depth 0, don't add another one

0 commit comments

Comments
 (0)