Skip to content

Commit 704301d

Browse files
committed
Merge branch 'plpgsql'
2 parents 6924559 + 5bd8089 commit 704301d

25 files changed

Lines changed: 933 additions & 412 deletions

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,13 @@ There are also several SQL-specific options:
221221

222222
_Since 0.12.0_
223223

224+
- **`sqlExperimentalPlpgsql`**: `boolean` (default `false`)
225+
226+
When enabled, the plugin will attempt to format PL/pgSQL function bodies.
227+
This might fail, as the underlying parser support for PL/pgSQL is incomplete.
228+
229+
_Since 0.21.0_
230+
224231
## Usage inside VSCode
225232

226233
To use this plugin inside VSCode,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
],
4040
"dependencies": {
4141
"prettier": "^3.0.3",
42-
"sql-parser-cst": "^0.41.0"
42+
"sql-parser-cst": "^0.41.2"
4343
},
4444
"devDependencies": {
4545
"@types/jest": "^30.0.0",

src/embedSql.ts

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
isStringLiteral,
1414
} from "./node_utils";
1515
import { hardline, indent, stripTrailingHardline } from "./print_utils";
16+
import { AllPrettierOptions } from "./options";
1617

1718
export const embedSql: NonNullable<Printer<Node>["embed"]> = (
1819
path,
@@ -21,28 +22,48 @@ export const embedSql: NonNullable<Printer<Node>["embed"]> = (
2122
const node = path.node;
2223
const parent = path.getParentNode(0);
2324
const grandParent = path.getParentNode(1);
25+
const pluginOptions: Partial<AllPrettierOptions> = options;
2426

2527
if (
2628
isStringLiteral(node) &&
2729
isAsClause(parent) &&
28-
(isCreateFunctionStmt(grandParent) || isCreateProcedureStmt(grandParent)) &&
29-
grandParent.clauses.some(isSqlLanguageClause)
30+
(isCreateFunctionStmt(grandParent) || isCreateProcedureStmt(grandParent))
3031
) {
31-
return async (textToDoc) => {
32-
const quote = detectQuote(node);
33-
if (!quote) {
34-
return undefined;
35-
}
32+
if (grandParent.clauses.some(isSqlLanguageClause)) {
33+
return async (textToDoc) => {
34+
const quote = detectQuote(node);
35+
if (!quote) {
36+
return undefined;
37+
}
3638

37-
const sql = await textToDoc(node.value, options);
39+
const sql = await textToDoc(node.value, pluginOptions);
3840

39-
return [
40-
quote,
41-
indent([hardline, stripTrailingHardline(sql)]),
42-
hardline,
43-
quote,
44-
];
45-
};
41+
return [
42+
quote,
43+
indent([hardline, stripTrailingHardline(sql)]),
44+
hardline,
45+
quote,
46+
];
47+
};
48+
}
49+
if (
50+
grandParent.clauses.some(isPlpgsqlLanguageClause) &&
51+
pluginOptions.sqlExperimentalPlpgsql
52+
) {
53+
return async (textToDoc) => {
54+
const quote = detectQuote(node);
55+
if (!quote) {
56+
return undefined;
57+
}
58+
59+
const sql = await textToDoc(node.value, {
60+
...pluginOptions,
61+
parser: "plpgsql",
62+
});
63+
64+
return [quote, [hardline, stripTrailingHardline(sql)], hardline, quote];
65+
};
66+
}
4667
}
4768

4869
return null;
@@ -53,6 +74,11 @@ const isSqlLanguageClause = (
5374
): boolean =>
5475
isLanguageClause(clause) && clause.name.name.toLowerCase() === "sql";
5576

77+
const isPlpgsqlLanguageClause = (
78+
clause: CreateFunctionStmt["clauses"][0] | CreateProcedureStmt["clauses"][0],
79+
): boolean =>
80+
isLanguageClause(clause) && clause.name.name.toLowerCase() === "plpgsql";
81+
5682
const detectQuote = (node: StringLiteral): string | undefined => {
5783
const match = node.text.match(/^('|\$[^$]*\$)/);
5884
const quote = match?.[1];

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ export const languages: SupportLanguage[] = [
3636
name: "Experimental PostgreSQL SQL",
3737
parsers: ["postgresql"],
3838
},
39+
{
40+
extensions: [],
41+
name: "Experimental PL/pgSQL",
42+
parsers: ["plpgsql"],
43+
},
3944
];
4045

4146
const createParser = (dialect: DialectName): Parser<Node> => ({
@@ -64,6 +69,7 @@ export const parsers: Record<string, Parser<Node>> = {
6469
mysql: createParser("mysql"),
6570
mariadb: createParser("mariadb"),
6671
postgresql: createParser("postgresql"),
72+
plpgsql: createParser("plpgsql"),
6773
};
6874

6975
export const printers: Record<string, Printer> = {

src/options.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface SqlPluginOptions {
1111
sqlCanonicalSyntax: boolean;
1212
sqlFinalSemicolon: boolean;
1313
sqlAcceptUnsupportedGrammar: boolean;
14+
sqlExperimentalPlpgsql: boolean;
1415
}
1516

1617
// Prettier builtin options + options of this plugin
@@ -165,4 +166,12 @@ export const options: SupportOptions = {
165166
"Skips formatting unsupported SQL statements instead of exiting with an error",
166167
// Since 0.12.0
167168
},
169+
sqlExperimentalPlpgsql: {
170+
type: "boolean",
171+
category: "SQL",
172+
default: false,
173+
description:
174+
"Enables formatting for PL/pgSQL in PostgreSQL function bodies",
175+
// Since 0.21.0
176+
},
168177
};

src/syntax/other_clauses.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ export const otherClausesMap: CstToDocMap<AllOtherClauses> = {
1414

1515
as_clause: (print, node) => {
1616
if (isStringLiteral(node.expr) || isDynamicallyLoadedFunction(node.expr)) {
17-
return print.spaced(["asKw", "expr"]);
17+
return group(print.spaced(["asKw", "expr"]));
1818
}
19-
return [print("asKw"), indent([hardline, print("expr")])];
19+
return group([print("asKw"), indent([hardline, print("expr")])]);
2020
},
2121

22-
comma_clause: (print) => [",", print("expr")],
22+
comma_clause: (print) => group([",", indent([line, print("expr")])]),
2323

2424
// WHERE CURRENT OF clause
2525
where_current_of_clause: (print) =>
26-
print.spaced(["whereCurrentOfKw", "cursor"]),
26+
group(print.spaced(["whereCurrentOfKw", "cursor"])),
2727
};

src/syntax/prepared_statements.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import { AllPreparedStatementNodes } from "sql-parser-cst";
22
import { group, hardline, indent, join, line } from "../print_utils";
33
import { CstToDocMap } from "../CstToDocMap";
44

5-
export const preparedStatementsMap: Partial<
6-
CstToDocMap<AllPreparedStatementNodes>
7-
> = {
5+
export const preparedStatementsMap: CstToDocMap<AllPreparedStatementNodes> = {
86
// EXECUTE
97
execute_stmt: (print, node) => {
108
if (node.args?.type === "execute_using_clause") {
@@ -23,6 +21,11 @@ export const preparedStatementsMap: Partial<
2321
...print(["into", "using"]).map((clause) => group(clause)),
2422
]),
2523
),
24+
execute_expr: (print) =>
25+
group([
26+
print("executeKw"),
27+
indent([line, print.separated(line, ["expr", "using"])]),
28+
]),
2629
execute_into_clause: (print) => print.spaced(["intoKw", "variables"]),
2730
execute_using_clause: (print) =>
2831
group([print("usingKw"), indent([line, print("values")])]),

src/syntax/procedural_language.ts

Lines changed: 100 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,46 +9,79 @@ import {
99
} from "../print_utils";
1010
import { CstToDocMap } from "../CstToDocMap";
1111

12-
export const proceduralLanguageMap: Partial<CstToDocMap<AllProceduralNodes>> = {
12+
export const proceduralLanguageMap: CstToDocMap<AllProceduralNodes> = {
13+
// labels
14+
labeled_stmt: (print, node) =>
15+
group([
16+
print.spaced(["beginLabel", "statement"]),
17+
node.endLabel ? [" ", print(["endLabel"])] : [],
18+
]),
19+
colon_label: (print) => [print("label"), ":"],
20+
chevron_label: (print) => ["<<", print("label"), ">>"],
21+
1322
// BEGIN .. END
1423
block_stmt: (print, node) =>
1524
group([
16-
print.spaced(["beginKw", "atomicKw"]),
17-
indent([hardline, stripTrailingHardline(print("program"))]),
25+
stripTrailingHardline(print("declareClause")),
26+
[
27+
node.declareClause ? hardline : [],
28+
print.spaced(["beginKw", "atomicKw"]),
29+
],
30+
node.program.statements.length > 0
31+
? indent([hardline, stripTrailingHardline(print("program"))])
32+
: print("program"),
1833
node.exception ? [hardline, print("exception")] : [],
1934
hardline,
2035
print("endKw"),
2136
]),
37+
38+
// DECLARE
39+
declare_clause: (print, node) =>
40+
group([print("declareKw"), indent([hardline, print("program")])]),
41+
declare_stmt: (print) =>
42+
group(
43+
join(" ", [
44+
...print.spaced(["declareKw"]),
45+
group(print("names")),
46+
...print.spaced(["constantKw", "dataType", "constraints", "init"]),
47+
]),
48+
),
49+
declare_init: (print) => print.spaced(["operator", "expr"]),
50+
51+
// EXCEPTION
2252
exception_clause: (print, node) => {
2353
if (node.clauses.length === 1) {
2454
// Keep single exception clause on the same line as EXCEPTION keyword
2555
return group(print.spaced(["exceptionKw", "clauses"]));
2656
} else {
27-
return group([print("exceptionKw"), indent([line, print("clauses")])]);
57+
return group([
58+
print("exceptionKw"),
59+
indent([
60+
hardline,
61+
stripTrailingHardline(print("clauses").map((doc) => [doc, hardline])),
62+
]),
63+
]);
2864
}
2965
},
30-
exception_when_clause: (print) =>
66+
exception_when_clause: (print, node) =>
3167
group([
32-
print.spaced(["whenKw", "condition", "thenKw"]),
33-
indent([hardline, stripTrailingHardline(print("program"))]),
68+
join(" ", [print("whenKw"), group(print("condition")), print("thenKw")]),
69+
node.program.statements.length > 0
70+
? indent([hardline, stripTrailingHardline(print("program"))])
71+
: print("program"),
3472
]),
3573

3674
error_bigquery: (print) => print("errorKw"),
37-
38-
// DECLARE
39-
declare_stmt: (print) =>
40-
group(
41-
join(" ", [
42-
print("declareKw"),
43-
group(print("names")),
44-
...print.spaced(["dataType", "init"]),
45-
]),
46-
),
47-
declare_init: (print) => print.spaced(["operator", "expr"]),
48-
75+
error_name: (print) => print("name"),
76+
error_sqlstate: (print) => group(print.spaced(["sqlstateKw", "code"])),
77+
error_format_string: (print, node) => group(print(["format", "args"])),
4978
// SET
5079
set_stmt: (print) => group(print.spaced(["setKw", "assignments"])),
5180

81+
// assignment
82+
assignment_stmt: (print) =>
83+
group(print.spaced(["target", "operator", "expr"])),
84+
5285
// IF
5386
if_stmt: (print) =>
5487
group(join(hardline, [...print("clauses"), print.spaced("endIfKw")])),
@@ -108,6 +141,9 @@ export const proceduralLanguageMap: Partial<CstToDocMap<AllProceduralNodes>> = {
108141
hardline,
109142
print.spaced("endWhileKw"),
110143
]),
144+
// WHILE .. LOOP .. END LOOP
145+
while_loop_stmt: (print) =>
146+
group(print.spaced(["whileKw", "condition", "loop"])),
111147
// FOR .. IN
112148
for_stmt: (print) =>
113149
group([
@@ -116,26 +152,60 @@ export const proceduralLanguageMap: Partial<CstToDocMap<AllProceduralNodes>> = {
116152
hardline,
117153
print.spaced("endForKw"),
118154
]),
119-
// BREAK/CONTINUE
120-
break_stmt: (print) => group(print.spaced(["breakKw", "label"])),
121-
continue_stmt: (print) => group(print.spaced(["continueKw", "label"])),
122-
// labels
123-
labeled_stmt: (print, node) =>
155+
// FOR .. IN range LOOP .. END LOOP
156+
for_loop_stmt: (print) =>
157+
group(print.spaced(["forKw", "left", "inKw", "right", "loop"])),
158+
for_range: (print) =>
124159
group([
125-
print.spaced(["beginLabel", "statement"]),
126-
node.endLabel ? [" ", print(["endLabel"])] : [],
160+
print.spaced(["reverseKw", "from"]),
161+
"..",
162+
print.spaced(["to", "by"]),
127163
]),
128-
colon_label: (print) => [print("label"), ":"],
164+
for_by_clause: (print) => group(print.spaced(["byKw", "expr"])),
165+
// FOREACH
166+
foreach_stmt: (print) =>
167+
group([
168+
print.spaced([
169+
"foreachKw",
170+
"left",
171+
"slice",
172+
"inArrayKw",
173+
"right",
174+
"loop",
175+
]),
176+
]),
177+
foreach_slice: (print) => group(print.spaced(["sliceKw", "count"])),
178+
// BREAK/CONTINUE
179+
break_stmt: (print) => group(print.spaced(["breakKw", "label", "when"])),
180+
continue_stmt: (print) =>
181+
group(print.spaced(["continueKw", "label", "when"])),
129182

130183
// CALL
131184
call_stmt: (print) => group(print.spaced(["callKw", "func"])),
132185
// RETURN
133186
return_stmt: (print) => group(print.spaced(["returnKw", "expr"])),
187+
return_next_stmt: (print) => group(print.spaced(["returnNextKw", "expr"])),
188+
return_query_stmt: (print, node) => {
189+
if (node.expr.type === "execute_expr") {
190+
return group([print.spaced("returnQueryKw"), " ", print("expr")]);
191+
} else {
192+
return group([print.spaced("returnQueryKw"), " ", indent(print("expr"))]);
193+
}
194+
},
134195
// RAISE
135-
raise_stmt: (print) => group(print.spaced(["raiseKw", "using"])),
136-
raise_using_clause: (print) => group(print.spaced(["usingKw", "options"])),
137-
raise_option_element: (print) => [print("nameKw"), " = ", print("value")],
196+
raise_stmt: (print, node) =>
197+
group([
198+
print.spaced(["raiseKw", "level", "error"]),
199+
node.using ? indent([line, print("using")]) : [],
200+
]),
201+
raise_level: (print) => print("levelKw"),
202+
raise_using_clause: (print) =>
203+
group([print("usingKw"), indent([line, print("options")])]),
204+
raise_option_element: (print) =>
205+
group(print.spaced(["nameKw", "operator", "value"])),
138206
// ASSERT
139207
assert_stmt: (print) =>
140208
group(print.spaced(["assertKw", "condition", "message"])),
209+
// NULL
210+
null_stmt: (print) => group(print.spaced(["nullKw"])),
141211
};

src/syntax/transformMap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import { mysqlMap } from "./dialects/mysql";
5151
import { sqliteMap } from "./dialects/sqlite";
5252
import { postgresqlMap } from "./dialects/postgresql";
5353

54-
export const transformMap: Partial<CstToDocMap<Node>> = {
54+
export const transformMap: CstToDocMap<Node> = {
5555
...aliasMap,
5656
...alterActionMap,
5757
...alterTableMap,

0 commit comments

Comments
 (0)