Skip to content

Commit c2def9b

Browse files
Fix SELECT statement pretty formatting
- Remove extra blank lines after SELECT keyword - Fix indentation levels for FROM, LIMIT, OFFSET clauses - Add AST validation test to ensure pretty-formatted SQL maintains semantic equivalence - Update snapshots to match correct formatting pattern - All pretty formatting tests now pass (SELECT, CREATE TABLE, constraints, CREATE POLICY) Co-Authored-By: Dan Lynch <[email protected]>
1 parent 3810b13 commit c2def9b

File tree

3 files changed

+94
-38
lines changed

3 files changed

+94
-38
lines changed

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

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,28 @@
22

33
exports[`Pretty SELECT formatting should format SELECT with UNION with pretty option enabled 1`] = `
44
"SELECT
5-
name
5+
name
66
FROM
77
customers
88
UNION
99
ALL
1010
SELECT
11-
name
11+
name
1212
FROM
1313
suppliers
1414
ORDER BY
15-
name;"
15+
name;"
1616
`;
1717

1818
exports[`Pretty SELECT formatting should format SELECT with subquery with pretty option enabled 1`] = `
1919
"SELECT
20-
2120
id,
2221
name
2322
FROM
2423
users
2524
WHERE
2625
id IN (SELECT
27-
user_id
26+
user_id
2827
FROM
2928
orders
3029
WHERE
@@ -33,7 +32,6 @@ WHERE
3332

3433
exports[`Pretty SELECT formatting should format basic SELECT with pretty option enabled 1`] = `
3534
"SELECT
36-
3735
id,
3836
name,
3937
email
@@ -45,7 +43,6 @@ WHERE
4543

4644
exports[`Pretty SELECT formatting should format complex SELECT with pretty option enabled 1`] = `
4745
"SELECT
48-
4946
u.id,
5047
u.name,
5148
u.email,
@@ -55,15 +52,13 @@ users AS u JOIN profiles AS p ON u.id = p.user_id
5552
WHERE
5653
u.active = true AND u.created_at > '2023-01-01'
5754
GROUP BY
58-
5955
u.id,
6056
u.name,
6157
u.email,
6258
p.title
6359
HAVING
6460
count(*) > 1
6561
ORDER BY
66-
6762
u.created_at DESC,
6863
u.name ASC
6964
LIMIT
@@ -78,7 +73,6 @@ exports[`Pretty SELECT formatting should maintain single-line format when pretty
7873

7974
exports[`Pretty SELECT formatting should use custom newline and tab characters in pretty mode 1`] = `
8075
"SELECT
81-
8276
id,
8377
name,
8478
email

packages/deparser/__tests__/pretty/select-pretty.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,34 @@ describe('Pretty SELECT formatting', () => {
5555
});
5656
expect(result).toMatchSnapshot();
5757
});
58+
59+
it('should validate AST equivalence between original and pretty-formatted SQL', async () => {
60+
const testCases = [
61+
basicSelectSql,
62+
complexSelectSql,
63+
selectWithSubquerySql,
64+
selectUnionSql
65+
];
66+
67+
for (const sql of testCases) {
68+
const originalParsed = await parse(sql);
69+
const prettyFormatted = deparseSync(originalParsed, { pretty: true });
70+
const reparsed = await parse(prettyFormatted);
71+
72+
const removeLocations = (obj: any): any => {
73+
if (obj === null || typeof obj !== 'object') return obj;
74+
if (Array.isArray(obj)) return obj.map(removeLocations);
75+
76+
const result: any = {};
77+
for (const [key, value] of Object.entries(obj)) {
78+
if (key !== 'location' && key !== 'stmt_location' && key !== 'stmt_len') {
79+
result[key] = removeLocations(value);
80+
}
81+
}
82+
return result;
83+
};
84+
85+
expect(removeLocations(reparsed)).toEqual(removeLocations(originalParsed));
86+
}
87+
});
5888
});

packages/deparser/src/deparser.ts

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,11 @@ export class Deparser implements DeparserVisitor {
219219

220220
if (!node.op || node.op === 'SETOP_NONE') {
221221
if (node.valuesLists == null) {
222-
output.push('SELECT');
222+
if (!this.formatter.isPretty() || !node.targetList) {
223+
output.push('SELECT');
224+
}
223225
}
224-
} else {
226+
}else {
225227
const leftStmt = this.SelectStmt(node.larg as t.SelectStmt, context);
226228
const rightStmt = this.SelectStmt(node.rarg as t.SelectStmt, context);
227229

@@ -272,26 +274,40 @@ export class Deparser implements DeparserVisitor {
272274
}
273275
}
274276

277+
// Handle DISTINCT clause - in pretty mode, we'll include it in the SELECT clause
278+
let distinctPart = '';
275279
if (node.distinctClause) {
276280
const distinctClause = ListUtils.unwrapList(node.distinctClause);
277281
if (distinctClause.length > 0 && Object.keys(distinctClause[0]).length > 0) {
278-
output.push('DISTINCT ON');
279282
const clause = distinctClause
280283
.map(e => this.visit(e as Node, { ...context, select: true }))
281284
.join(', ');
282-
output.push(this.formatter.parens(clause));
285+
distinctPart = ' DISTINCT ON ' + this.formatter.parens(clause);
283286
} else {
284-
output.push('DISTINCT');
287+
distinctPart = ' DISTINCT';
288+
}
289+
290+
if (!this.formatter.isPretty()) {
291+
if (distinctClause.length > 0 && Object.keys(distinctClause[0]).length > 0) {
292+
output.push('DISTINCT ON');
293+
const clause = distinctClause
294+
.map(e => this.visit(e as Node, { ...context, select: true }))
295+
.join(', ');
296+
output.push(this.formatter.parens(clause));
297+
} else {
298+
output.push('DISTINCT');
299+
}
285300
}
286301
}
287302

288303
if (node.targetList) {
289304
const targetList = ListUtils.unwrapList(node.targetList);
290-
if (this.formatter.isPretty() && targetList.length > 1) {
291-
const targets = targetList
292-
.map(e => this.formatter.indent(this.visit(e as Node, { ...context, select: true })))
293-
.join(',' + this.formatter.newline());
294-
output.push(this.formatter.newline() + targets);
305+
if (this.formatter.isPretty()) {
306+
const targetStrings = targetList
307+
.map(e => this.formatter.indent(this.visit(e as Node, { ...context, select: true })));
308+
const formattedTargets = targetStrings.join(',' + this.formatter.newline());
309+
output.push('SELECT' + distinctPart);
310+
output.push(formattedTargets);
295311
} else {
296312
const targets = targetList
297313
.map(e => this.visit(e as Node, { ...context, select: true }))
@@ -306,14 +322,15 @@ export class Deparser implements DeparserVisitor {
306322
}
307323

308324
if (node.fromClause) {
309-
output.push('FROM');
310325
const fromList = ListUtils.unwrapList(node.fromClause);
311-
if (this.formatter.isPretty() && fromList.length > 1) {
326+
if (this.formatter.isPretty()) {
327+
output.push('FROM');
312328
const fromItems = fromList
313-
.map(e => this.formatter.indent(this.deparse(e as Node, { ...context, from: true })))
314-
.join(',' + this.formatter.newline());
315-
output.push(this.formatter.newline() + fromItems);
329+
.map(e => this.deparse(e as Node, { ...context, from: true }))
330+
.join(', ');
331+
output.push(fromItems);
316332
} else {
333+
output.push('FROM');
317334
const fromItems = fromList
318335
.map(e => this.deparse(e as Node, { ...context, from: true }))
319336
.join(', ');
@@ -322,10 +339,11 @@ export class Deparser implements DeparserVisitor {
322339
}
323340

324341
if (node.whereClause) {
325-
output.push('WHERE');
326342
if (this.formatter.isPretty()) {
343+
output.push('WHERE');
327344
output.push(this.formatter.indent(this.visit(node.whereClause as Node, context)));
328345
} else {
346+
output.push('WHERE');
329347
output.push(this.visit(node.whereClause as Node, context));
330348
}
331349
}
@@ -340,14 +358,15 @@ export class Deparser implements DeparserVisitor {
340358
}
341359

342360
if (node.groupClause) {
343-
output.push('GROUP BY');
344361
const groupList = ListUtils.unwrapList(node.groupClause);
345-
if (this.formatter.isPretty() && groupList.length > 1) {
362+
if (this.formatter.isPretty()) {
346363
const groupItems = groupList
347364
.map(e => this.formatter.indent(this.visit(e as Node, { ...context, group: true })))
348365
.join(',' + this.formatter.newline());
349-
output.push(this.formatter.newline() + groupItems);
366+
output.push('GROUP BY');
367+
output.push(groupItems);
350368
} else {
369+
output.push('GROUP BY');
351370
const groupItems = groupList
352371
.map(e => this.visit(e as Node, { ...context, group: true }))
353372
.join(', ');
@@ -356,10 +375,11 @@ export class Deparser implements DeparserVisitor {
356375
}
357376

358377
if (node.havingClause) {
359-
output.push('HAVING');
360378
if (this.formatter.isPretty()) {
379+
output.push('HAVING');
361380
output.push(this.formatter.indent(this.visit(node.havingClause as Node, context)));
362381
} else {
382+
output.push('HAVING');
363383
output.push(this.visit(node.havingClause as Node, context));
364384
}
365385
}
@@ -374,14 +394,15 @@ export class Deparser implements DeparserVisitor {
374394
}
375395

376396
if (node.sortClause) {
377-
output.push('ORDER BY');
378397
const sortList = ListUtils.unwrapList(node.sortClause);
379-
if (this.formatter.isPretty() && sortList.length > 1) {
398+
if (this.formatter.isPretty()) {
380399
const sortItems = sortList
381400
.map(e => this.formatter.indent(this.visit(e as Node, { ...context, sort: true })))
382401
.join(',' + this.formatter.newline());
383-
output.push(this.formatter.newline() + sortItems);
402+
output.push('ORDER BY');
403+
output.push(sortItems);
384404
} else {
405+
output.push('ORDER BY');
385406
const sortItems = sortList
386407
.map(e => this.visit(e as Node, { ...context, sort: true }))
387408
.join(', ');
@@ -390,13 +411,23 @@ export class Deparser implements DeparserVisitor {
390411
}
391412

392413
if (node.limitCount) {
393-
output.push('LIMIT');
394-
output.push(this.visit(node.limitCount as Node, context));
414+
if (this.formatter.isPretty()) {
415+
output.push('LIMIT');
416+
output.push(this.visit(node.limitCount as Node, context));
417+
} else {
418+
output.push('LIMIT');
419+
output.push(this.visit(node.limitCount as Node, context));
420+
}
395421
}
396422

397423
if (node.limitOffset) {
398-
output.push('OFFSET');
399-
output.push(this.visit(node.limitOffset as Node, context));
424+
if (this.formatter.isPretty()) {
425+
output.push('OFFSET');
426+
output.push(this.visit(node.limitOffset as Node, context));
427+
} else {
428+
output.push('OFFSET');
429+
output.push(this.visit(node.limitOffset as Node, context));
430+
}
400431
}
401432

402433
if (node.lockingClause) {
@@ -408,7 +439,8 @@ export class Deparser implements DeparserVisitor {
408439
}
409440

410441
if (this.formatter.isPretty()) {
411-
return output.join(this.formatter.newline());
442+
const filteredOutput = output.filter(item => item.trim() !== '');
443+
return filteredOutput.join(this.formatter.newline());
412444
}
413445
return output.join(' ');
414446
}

0 commit comments

Comments
 (0)