Skip to content

Commit 57fcffe

Browse files
committed
tree: add pretty-printing for CreateRoutine
The routine body in `tree.CreateRoutine` is stored as a raw string and written as-is by its `Format` method. We implement a new `Doc` method that first re-parses the body so that it can be pretty-printed. Release note (sql change): Statement bundles for CREATE FUNCTION and CREATE PROCEDURE will now contain pretty-printed function bodies.
1 parent 86b1215 commit 57fcffe

33 files changed

Lines changed: 9207 additions & 11 deletions

pkg/sql/exec_util.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,24 @@ func DoParserInjection() {
159159
parserutils.ParseOne = parser.ParseOne
160160
parserutils.ParseQualifiedTableName = parser.ParseQualifiedTableName
161161
parserutils.PLpgSQLParse = plpgsqlparser.Parse
162+
tree.ParseSQLForDoc = func(sql string) []tree.NodeFormatter {
163+
stmts, err := parser.Parse(sql)
164+
if err != nil {
165+
return nil
166+
}
167+
result := make([]tree.NodeFormatter, len(stmts))
168+
for i, s := range stmts {
169+
result[i] = s.AST
170+
}
171+
return result
172+
}
173+
tree.ParsePLpgSQLForDoc = func(sql string) tree.NodeFormatter {
174+
stmt, err := plpgsqlparser.Parse(sql)
175+
if err != nil {
176+
return nil
177+
}
178+
return stmt.AST
179+
}
162180
}
163181

164182
// ClusterOrganization is the organization name.

pkg/sql/sem/tree/create_routine.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
1212
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
13+
"github.com/cockroachdb/cockroach/pkg/util/pretty"
1314
"github.com/cockroachdb/errors"
1415
)
1516

@@ -80,6 +81,10 @@ type CreateRoutine struct {
8081
Params RoutineParams
8182
ReturnType *RoutineReturnType
8283
Options RoutineOptions
84+
// RoutineBody is set during parsing for routines that use SQL-standard
85+
// inline body syntax: either BEGIN ATOMIC ... END or a bare RETURN expr
86+
// (as opposed to the dollar-quoted AS $$ ... $$ syntax). Currently
87+
// unimplemented in the optimizer.
8388
RoutineBody *RoutineBody
8489
// BodyStatements is not assigned during initial parsing of user input. It's
8590
// assigned during opt builder for logging purpose at the moment. It stores
@@ -165,6 +170,114 @@ func (node *CreateRoutine) Format(ctx *FmtCtx) {
165170
}
166171
}
167172

173+
// Doc implements the Docer interface.
174+
func (node *CreateRoutine) Doc(p *PrettyCfg) pretty.Doc {
175+
header := pretty.Keyword("CREATE")
176+
if node.Replace {
177+
header = pretty.ConcatSpace(header, pretty.Keyword("OR REPLACE"))
178+
}
179+
if node.IsProcedure {
180+
header = pretty.ConcatSpace(header, pretty.Keyword("PROCEDURE"))
181+
} else {
182+
header = pretty.ConcatSpace(header, pretty.Keyword("FUNCTION"))
183+
}
184+
header = pretty.ConcatSpace(header, p.Doc(&node.Name))
185+
paramDocs := make([]pretty.Doc, len(node.Params))
186+
for i := range node.Params {
187+
paramDocs[i] = p.Doc(&node.Params[i])
188+
}
189+
header = pretty.Concat(header, p.bracket("(", p.commaSeparated(paramDocs...), ")"))
190+
191+
var clauses []pretty.Doc
192+
if !node.IsProcedure && node.ReturnType != nil {
193+
ret := pretty.Keyword("RETURNS")
194+
if node.ReturnType.SetOf {
195+
ret = pretty.ConcatSpace(ret, pretty.Keyword("SETOF"))
196+
}
197+
ret = pretty.ConcatSpace(ret, p.FormatType(node.ReturnType.Type))
198+
clauses = append(clauses, ret)
199+
}
200+
201+
// Extract RoutineBodyStr from options; it is formatted separately as the body.
202+
var isPLpgSQL bool
203+
var funcBody RoutineBodyStr
204+
for _, option := range node.Options {
205+
switch t := option.(type) {
206+
case RoutineBodyStr:
207+
funcBody = t
208+
continue
209+
case RoutineLeakproof, RoutineVolatility, RoutineNullInputBehavior:
210+
if node.IsProcedure {
211+
continue
212+
}
213+
case RoutineLanguage:
214+
isPLpgSQL = t == RoutineLangPLpgSQL
215+
}
216+
clauses = append(clauses, p.Doc(option))
217+
}
218+
219+
var bodyDoc pretty.Doc
220+
if node.RoutineBody != nil {
221+
stmtDocs := make([]pretty.Doc, len(node.RoutineBody.Stmts))
222+
for i, stmt := range node.RoutineBody.Stmts {
223+
d := p.Doc(stmt)
224+
// DoBlock.Doc already includes a trailing semicolon.
225+
if _, ok := stmt.(*DoBlock); !ok {
226+
d = pretty.Concat(d, pretty.Text(";"))
227+
}
228+
stmtDocs[i] = d
229+
}
230+
bodyDoc = pretty.Fold(pretty.Concat,
231+
pretty.Keyword("BEGIN ATOMIC"),
232+
pretty.NestT(pretty.Concat(pretty.HardLine, pretty.Stack(stmtDocs...))),
233+
pretty.HardLine,
234+
pretty.Keyword("END"))
235+
} else if !isPLpgSQL && ParseSQLForDoc != nil {
236+
if stmts := ParseSQLForDoc(string(funcBody)); len(stmts) > 0 {
237+
stmtDocs := make([]pretty.Doc, len(stmts))
238+
for i, stmt := range stmts {
239+
d := p.Doc(stmt)
240+
// DoBlock.Doc already includes a trailing semicolon.
241+
if _, ok := stmt.(*DoBlock); !ok {
242+
d = pretty.Concat(d, pretty.Text(";"))
243+
}
244+
stmtDocs[i] = d
245+
}
246+
// Render the body to find a dollar-quote tag that doesn't
247+
// collide with nested dollar quotes (e.g. from DO blocks).
248+
fmtCtx := NewFmtCtx(p.FmtFlagsWithDefaults())
249+
for _, stmt := range stmts {
250+
fmtCtx.FormatNode(stmt)
251+
}
252+
bodyStr := fmtCtx.CloseAndGetString()
253+
tag := "$" + DollarQuoteDelimiter(bodyStr) + "$"
254+
bodyDoc = pretty.Fold(pretty.Concat,
255+
pretty.ConcatSpace(pretty.Keyword("AS"), pretty.Text(tag)),
256+
pretty.NestT(pretty.Concat(pretty.HardLine, pretty.Stack(stmtDocs...))),
257+
pretty.HardLine,
258+
pretty.Text(tag))
259+
}
260+
} else if isPLpgSQL && ParsePLpgSQLForDoc != nil {
261+
if parsed := ParsePLpgSQLForDoc(string(funcBody)); parsed != nil {
262+
// Use the formatted output to pick a dollar-quote tag that
263+
// doesn't collide with nested dollar quotes (e.g. DO blocks).
264+
bodyStr := AsStringWithFlags(parsed, p.FmtFlagsWithDefaults())
265+
tag := "$" + DollarQuoteDelimiter(bodyStr) + "$"
266+
bodyDoc = pretty.Fold(pretty.Concat,
267+
pretty.ConcatSpace(pretty.Keyword("AS"), pretty.Text(tag)),
268+
pretty.NestT(pretty.Concat(pretty.HardLine, p.Doc(parsed))),
269+
pretty.HardLine,
270+
pretty.Text(tag))
271+
}
272+
}
273+
if bodyDoc == nil {
274+
bodyDoc = p.docAsString(funcBody)
275+
}
276+
clauses = append(clauses, bodyDoc)
277+
278+
return p.nestUnder(header, pretty.Stack(clauses...))
279+
}
280+
168281
// RoutineBody represent a list of statements in a UDF body.
169282
type RoutineBody struct {
170283
// Stmts is populated during parsing. Unlike BodyStatements, we don't need

pkg/sql/sem/tree/pretty.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,22 @@ import (
2020
"github.com/cockroachdb/errors"
2121
)
2222

23+
var (
24+
// ParseSQLForDoc parses SQL statements stored as raw strings (e.g.
25+
// RoutineBodyStr) into AST nodes for structured pretty-printing.
26+
// Injected at init time to avoid a circular dependency on the parser
27+
// package (the parserutils package unfortunately has the same issue).
28+
// Returns nil if parsing fails.
29+
ParseSQLForDoc func(sql string) []NodeFormatter
30+
31+
// ParsePLpgSQLForDoc parses a PL/pgSQL block stored as a raw string
32+
// into an AST node for structured pretty-printing. Injected at init
33+
// time to avoid a circular dependency on the plpgsqlparser package
34+
// (the parserutils package unfortunately has the same issue).
35+
// Returns nil if parsing fails.
36+
ParsePLpgSQLForDoc func(sql string) NodeFormatter
37+
)
38+
2339
// This file contains methods that convert statements to pretty Docs (a tree
2440
// structure that can be pretty printed at a specific line width). Nodes
2541
// implement the Docer interface to allow this conversion. In general,

0 commit comments

Comments
 (0)