Skip to content

Commit bd9fd81

Browse files
committed
test(plpgsql-deparser): add round-trip AST testing for bug fixes
Upgrade deparser-fixes tests to use round-trip testing infrastructure: - parse -> deparse -> reparse -> compare cleaned ASTs - Add normalizeQueryWhitespace to cleanPlpgsqlTree to handle indentation differences in embedded SQL queries - All 17 test cases now verify AST equality after round-trip
1 parent 2769aa6 commit bd9fd81

2 files changed

Lines changed: 158 additions & 34 deletions

File tree

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

Lines changed: 57 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import { loadModule, parsePlPgSQLSync } from '@libpg-query/parser';
22
import { deparseSync, PLpgSQLParseResult } from '../src';
3+
import { PLpgSQLTestUtils } from '../test-utils';
34

45
describe('plpgsql-deparser bug fixes', () => {
6+
let testUtils: PLpgSQLTestUtils;
7+
58
beforeAll(async () => {
69
await loadModule();
10+
testUtils = new PLpgSQLTestUtils();
711
});
812

913
describe('PERFORM SELECT fix', () => {
10-
it('should strip SELECT keyword from PERFORM statements', () => {
14+
it('should strip SELECT keyword from PERFORM statements', async () => {
1115
const sql = `CREATE FUNCTION test_perform() RETURNS void
1216
LANGUAGE plpgsql
1317
AS $$
@@ -16,15 +20,18 @@ BEGIN
1620
END;
1721
$$`;
1822

23+
// Round-trip test: parse -> deparse -> reparse -> compare ASTs
24+
await testUtils.expectAstMatch('PERFORM basic', sql);
25+
26+
// Also verify specific output characteristics
1927
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
2028
const deparsed = deparseSync(parsed);
21-
2229
expect(deparsed).toMatchSnapshot();
2330
expect(deparsed).toContain('PERFORM pg_sleep');
2431
expect(deparsed).not.toMatch(/PERFORM\s+SELECT/i);
2532
});
2633

27-
it('should handle PERFORM with complex expressions', () => {
34+
it('should handle PERFORM with complex expressions', async () => {
2835
const sql = `CREATE FUNCTION test_perform_complex() RETURNS void
2936
LANGUAGE plpgsql
3037
AS $$
@@ -34,14 +41,15 @@ BEGIN
3441
END;
3542
$$`;
3643

44+
await testUtils.expectAstMatch('PERFORM complex', sql);
45+
3746
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
3847
const deparsed = deparseSync(parsed);
39-
4048
expect(deparsed).toMatchSnapshot();
4149
expect(deparsed).not.toMatch(/PERFORM\s+SELECT/i);
4250
});
4351

44-
it('should handle PERFORM with subquery', () => {
52+
it('should handle PERFORM with subquery', async () => {
4553
const sql = `CREATE FUNCTION test_perform_subquery() RETURNS void
4654
LANGUAGE plpgsql
4755
AS $$
@@ -50,16 +58,17 @@ BEGIN
5058
END;
5159
$$`;
5260

61+
await testUtils.expectAstMatch('PERFORM subquery', sql);
62+
5363
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
5464
const deparsed = deparseSync(parsed);
55-
5665
expect(deparsed).toMatchSnapshot();
5766
expect(deparsed).not.toMatch(/PERFORM\s+SELECT/i);
5867
});
5968
});
6069

6170
describe('INTO clause depth-aware scanner', () => {
62-
it('should insert INTO at correct position for simple SELECT', () => {
71+
it('should insert INTO at correct position for simple SELECT', async () => {
6372
const sql = `CREATE FUNCTION test_into_simple() RETURNS integer
6473
LANGUAGE plpgsql
6574
AS $$
@@ -71,14 +80,15 @@ BEGIN
7180
END;
7281
$$`;
7382

83+
await testUtils.expectAstMatch('INTO simple', sql);
84+
7485
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
7586
const deparsed = deparseSync(parsed);
76-
7787
expect(deparsed).toMatchSnapshot();
7888
expect(deparsed).toContain('INTO');
7989
});
8090

81-
it('should not insert INTO inside subqueries', () => {
91+
it('should not insert INTO inside subqueries', async () => {
8292
const sql = `CREATE FUNCTION test_into_subquery() RETURNS integer
8393
LANGUAGE plpgsql
8494
AS $$
@@ -90,13 +100,14 @@ BEGIN
90100
END;
91101
$$`;
92102

103+
await testUtils.expectAstMatch('INTO subquery', sql);
104+
93105
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
94106
const deparsed = deparseSync(parsed);
95-
96107
expect(deparsed).toMatchSnapshot();
97108
});
98109

99-
it('should handle INTO with CTE', () => {
110+
it('should handle INTO with CTE', async () => {
100111
const sql = `CREATE FUNCTION test_into_cte() RETURNS integer
101112
LANGUAGE plpgsql
102113
AS $$
@@ -111,13 +122,14 @@ BEGIN
111122
END;
112123
$$`;
113124

125+
await testUtils.expectAstMatch('INTO CTE', sql);
126+
114127
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
115128
const deparsed = deparseSync(parsed);
116-
117129
expect(deparsed).toMatchSnapshot();
118130
});
119131

120-
it('should handle INTO with UNION', () => {
132+
it('should handle INTO with UNION', async () => {
121133
const sql = `CREATE FUNCTION test_into_union() RETURNS integer
122134
LANGUAGE plpgsql
123135
AS $$
@@ -133,13 +145,14 @@ BEGIN
133145
END;
134146
$$`;
135147

148+
await testUtils.expectAstMatch('INTO UNION', sql);
149+
136150
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
137151
const deparsed = deparseSync(parsed);
138-
139152
expect(deparsed).toMatchSnapshot();
140153
});
141154

142-
it('should handle INTO with quoted identifiers', () => {
155+
it('should handle INTO with quoted identifiers', async () => {
143156
const sql = `CREATE FUNCTION test_into_quoted() RETURNS text
144157
LANGUAGE plpgsql
145158
AS $$
@@ -151,13 +164,14 @@ BEGIN
151164
END;
152165
$$`;
153166

167+
await testUtils.expectAstMatch('INTO quoted', sql);
168+
154169
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
155170
const deparsed = deparseSync(parsed);
156-
157171
expect(deparsed).toMatchSnapshot();
158172
});
159173

160-
it('should handle INTO with dollar-quoted strings', () => {
174+
it('should handle INTO with dollar-quoted strings', async () => {
161175
const sql = `CREATE FUNCTION test_into_dollar_quote() RETURNS text
162176
LANGUAGE plpgsql
163177
AS $$
@@ -169,13 +183,14 @@ BEGIN
169183
END;
170184
$$`;
171185

186+
await testUtils.expectAstMatch('INTO dollar-quote', sql);
187+
172188
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
173189
const deparsed = deparseSync(parsed);
174-
175190
expect(deparsed).toMatchSnapshot();
176191
});
177192

178-
it('should handle INTO STRICT', () => {
193+
it('should handle INTO STRICT', async () => {
179194
const sql = `CREATE FUNCTION test_into_strict() RETURNS integer
180195
LANGUAGE plpgsql
181196
AS $$
@@ -187,16 +202,17 @@ BEGIN
187202
END;
188203
$$`;
189204

205+
await testUtils.expectAstMatch('INTO STRICT', sql);
206+
190207
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
191208
const deparsed = deparseSync(parsed);
192-
193209
expect(deparsed).toMatchSnapshot();
194210
expect(deparsed).toContain('STRICT');
195211
});
196212
});
197213

198214
describe('Record field qualification (recfield)', () => {
199-
it('should qualify record fields with parent record name in triggers', () => {
215+
it('should qualify record fields with parent record name in triggers', async () => {
200216
const sql = `CREATE FUNCTION test_trigger() RETURNS trigger
201217
LANGUAGE plpgsql
202218
AS $$
@@ -208,13 +224,14 @@ BEGIN
208224
END;
209225
$$`;
210226

227+
await testUtils.expectAstMatch('recfield trigger', sql);
228+
211229
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
212230
const deparsed = deparseSync(parsed);
213-
214231
expect(deparsed).toMatchSnapshot();
215232
});
216233

217-
it('should handle OLD and NEW record references', () => {
234+
it('should handle OLD and NEW record references', async () => {
218235
const sql = `CREATE FUNCTION test_trigger_old_new() RETURNS trigger
219236
LANGUAGE plpgsql
220237
AS $$
@@ -226,13 +243,14 @@ BEGIN
226243
END;
227244
$$`;
228245

246+
await testUtils.expectAstMatch('recfield OLD NEW', sql);
247+
229248
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
230249
const deparsed = deparseSync(parsed);
231-
232250
expect(deparsed).toMatchSnapshot();
233251
});
234252

235-
it('should handle record field assignment', () => {
253+
it('should handle record field assignment', async () => {
236254
const sql = `CREATE FUNCTION test_record_assign() RETURNS trigger
237255
LANGUAGE plpgsql
238256
AS $$
@@ -244,13 +262,14 @@ BEGIN
244262
END;
245263
$$`;
246264

265+
await testUtils.expectAstMatch('recfield assignment', sql);
266+
247267
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
248268
const deparsed = deparseSync(parsed);
249-
250269
expect(deparsed).toMatchSnapshot();
251270
});
252271

253-
it('should handle SELECT INTO with record fields', () => {
272+
it('should handle SELECT INTO with record fields', async () => {
254273
const sql = `CREATE FUNCTION test_select_into_record() RETURNS trigger
255274
LANGUAGE plpgsql
256275
AS $$
@@ -260,13 +279,14 @@ BEGIN
260279
END;
261280
$$`;
262281

282+
await testUtils.expectAstMatch('recfield SELECT INTO', sql);
283+
263284
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
264285
const deparsed = deparseSync(parsed);
265-
266286
expect(deparsed).toMatchSnapshot();
267287
});
268288

269-
it('should handle custom record types', () => {
289+
it('should handle custom record types', async () => {
270290
const sql = `CREATE FUNCTION test_custom_record() RETURNS void
271291
LANGUAGE plpgsql
272292
AS $$
@@ -279,15 +299,16 @@ BEGIN
279299
END;
280300
$$`;
281301

302+
await testUtils.expectAstMatch('recfield custom record', sql);
303+
282304
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
283305
const deparsed = deparseSync(parsed);
284-
285306
expect(deparsed).toMatchSnapshot();
286307
});
287308
});
288309

289310
describe('combined scenarios', () => {
290-
it('should handle PERFORM with record fields', () => {
311+
it('should handle PERFORM with record fields', async () => {
291312
const sql = `CREATE FUNCTION test_perform_record() RETURNS trigger
292313
LANGUAGE plpgsql
293314
AS $$
@@ -297,14 +318,15 @@ BEGIN
297318
END;
298319
$$`;
299320

321+
await testUtils.expectAstMatch('combined PERFORM recfield', sql);
322+
300323
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
301324
const deparsed = deparseSync(parsed);
302-
303325
expect(deparsed).toMatchSnapshot();
304326
expect(deparsed).not.toMatch(/PERFORM\s+SELECT/i);
305327
});
306328

307-
it('should handle SELECT INTO with subquery and record fields', () => {
329+
it('should handle SELECT INTO with subquery and record fields', async () => {
308330
const sql = `CREATE FUNCTION test_complex_trigger() RETURNS trigger
309331
LANGUAGE plpgsql
310332
AS $$
@@ -319,9 +341,10 @@ BEGIN
319341
END;
320342
$$`;
321343

344+
await testUtils.expectAstMatch('combined INTO recfield', sql);
345+
322346
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
323347
const deparsed = deparseSync(parsed);
324-
325348
expect(deparsed).toMatchSnapshot();
326349
});
327350
});

0 commit comments

Comments
 (0)