Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60602d1de7 | ||
|
|
8d19258aa0 | ||
|
|
5cba6beeb1 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Maintainer: Hein (Warky Devs) <hein@warky.dev>
|
||||
pkgname=pgtidy-bin
|
||||
pkgver=0.0.4
|
||||
pkgver=0.0.5
|
||||
pkgrel=1
|
||||
pkgdesc="PostgreSQL SQL formatter and linter"
|
||||
arch=('x86_64' 'aarch64')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: pgtidy
|
||||
Version: 0.0.4
|
||||
Version: 0.0.5
|
||||
Release: 1%{?dist}
|
||||
Summary: PostgreSQL SQL formatter and linter
|
||||
|
||||
|
||||
+128
-7
@@ -441,13 +441,9 @@ func formatBodyStatements(text string, st config.Style) string {
|
||||
stmtLines = joinThenToCondition(stmt)
|
||||
}
|
||||
|
||||
for i, ll := range stmtLines {
|
||||
if i == 0 || ll.indent == "" {
|
||||
result.WriteString(baseIndent)
|
||||
} else {
|
||||
result.WriteString(ll.indent)
|
||||
}
|
||||
result.WriteString(ll.text)
|
||||
formattedLines := formatBodyStmtLines(stmtLines, baseIndent, st)
|
||||
for _, line := range formattedLines {
|
||||
result.WriteString(line)
|
||||
result.WriteString(nl)
|
||||
}
|
||||
|
||||
@@ -574,6 +570,131 @@ func formatBodyStatements(text string, st config.Style) string {
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// formatBodyStmtLines formats one flushed PL/pgSQL statement at its contextual
|
||||
// base indent. Multi-line UPDATE/DELETE statements inside PL/pgSQL get their
|
||||
// top-level SET/WHERE/AND/OR clauses realigned under the statement while nested
|
||||
// subqueries keep their original indentation. Non-DML statements keep
|
||||
// continuation indentation, except that standalone structural keywords such as
|
||||
// THEN are aligned with the block opener.
|
||||
func formatBodyStmtLines(lines []bline, baseIndent string, st config.Style) []string {
|
||||
if len(lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if looksLikeMultiLineBodyDML(lines) {
|
||||
return reindentBodyDML(lines, baseIndent, st)
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(lines))
|
||||
for i, ll := range lines {
|
||||
text := ll.text
|
||||
indent := baseIndent
|
||||
if i > 0 && ll.indent != "" && !isStandaloneBodyKeyword(ll.text, "then", "else", "elsif", "elseif") {
|
||||
indent = ll.indent
|
||||
}
|
||||
out = append(out, indent+text)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func looksLikeMultiLineBodyDML(lines []bline) bool {
|
||||
if len(lines) < 2 {
|
||||
return false
|
||||
}
|
||||
kw := lowerASCII(firstBodyKeyword(lines[0].text))
|
||||
return kw == "update" || kw == "delete"
|
||||
}
|
||||
|
||||
func reindentBodyDML(lines []bline, baseIndent string, st config.Style) []string {
|
||||
out := make([]string, 0, len(lines)+1)
|
||||
afterWhere := false
|
||||
parenDepth := 0
|
||||
for i, ll := range lines {
|
||||
text := strings.TrimRight(ll.text, " ")
|
||||
lineDepth := parenDepth
|
||||
kw := lowerASCII(firstBodyKeyword(text))
|
||||
if afterWhere && lineDepth == 0 && kw != "and" && kw != "or" {
|
||||
out = append(out, baseIndent+st.Indent+st.Indent+strings.TrimSpace(text))
|
||||
afterWhere = false
|
||||
updateBodyParenDepth(text, &parenDepth)
|
||||
continue
|
||||
}
|
||||
if lineDepth == 0 && (kw == "set" || kw == "where" || kw == "values" || kw == "returning") {
|
||||
if kw == "where" {
|
||||
whereText := strings.TrimSpace(text)
|
||||
fields := strings.Fields(whereText)
|
||||
nextKw := ""
|
||||
if i+1 < len(lines) {
|
||||
nextKw = lowerASCII(firstBodyKeyword(lines[i+1].text))
|
||||
}
|
||||
if len(fields) > 1 && (nextKw == "and" || nextKw == "or") {
|
||||
out = append(out, baseIndent+fields[0])
|
||||
out = append(out, baseIndent+st.Indent+st.Indent+strings.TrimSpace(whereText[len(fields[0]):]))
|
||||
afterWhere = false
|
||||
continue
|
||||
}
|
||||
afterWhere = len(fields) == 1
|
||||
}
|
||||
out = append(out, baseIndent+strings.TrimSpace(text))
|
||||
continue
|
||||
}
|
||||
if lineDepth == 0 && (kw == "and" || kw == "or") {
|
||||
out = append(out, baseIndent+st.Indent+strings.TrimSpace(text))
|
||||
afterWhere = false
|
||||
updateBodyParenDepth(text, &parenDepth)
|
||||
continue
|
||||
}
|
||||
if i == 0 {
|
||||
out = append(out, baseIndent+strings.TrimSpace(text))
|
||||
} else if ll.indent != "" {
|
||||
out = append(out, ll.indent+strings.TrimSpace(text))
|
||||
} else {
|
||||
out = append(out, baseIndent+strings.TrimSpace(text))
|
||||
}
|
||||
afterWhere = false
|
||||
updateBodyParenDepth(text, &parenDepth)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func updateBodyParenDepth(s string, depth *int) {
|
||||
for _, tok := range lexer.Lex(s) {
|
||||
switch tok.Kind {
|
||||
case lexer.LParen, lexer.LBracket:
|
||||
(*depth)++
|
||||
case lexer.RParen, lexer.RBracket:
|
||||
if *depth > 0 {
|
||||
(*depth)--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func significantBodyTokens(s string) []cst.Tok {
|
||||
var toks []cst.Tok
|
||||
for _, tok := range lexer.Lex(s) {
|
||||
if tok.IsTrivia() || tok.Kind == lexer.EOF {
|
||||
continue
|
||||
}
|
||||
toks = append(toks, cst.Tok{Tok: tok})
|
||||
}
|
||||
return toks
|
||||
}
|
||||
|
||||
func isStandaloneBodyKeyword(s string, kws ...string) bool {
|
||||
toks := significantBodyTokens(s)
|
||||
if len(toks) != 1 || toks[0].Tok.Kind != lexer.Ident {
|
||||
return false
|
||||
}
|
||||
low := lowerASCII(toks[0].Tok.Text)
|
||||
for _, kw := range kws {
|
||||
if low == kw {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// joinThenToCondition merges a THEN line (on its own bline) into the preceding
|
||||
// condition line when plpgsql_if_then_newline is false.
|
||||
func joinThenToCondition(lines []bline) []bline {
|
||||
|
||||
@@ -107,6 +107,66 @@ func TestFormatMmProcBroken(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatIssue1PLpgSQLIndenting(t *testing.T) {
|
||||
src := "CREATE FUNCTION f() RETURNS void LANGUAGE plpgsql AS $$\n" +
|
||||
"DECLARE\n" +
|
||||
" r_lp record;\n" +
|
||||
"BEGIN\n" +
|
||||
" if r_lp.total > 0\n" +
|
||||
" and r_lp.totaldone >= r_lp.total\n" +
|
||||
" then\n" +
|
||||
" update core.process u\n" +
|
||||
" set status = 'done'\n" +
|
||||
" where u.rid_process = r_lp.rid_process\n" +
|
||||
" and nv(u.status) <> 'done';\n" +
|
||||
" elsif r_lp.total > 0\n" +
|
||||
" then\n" +
|
||||
" update core.process u\n" +
|
||||
" set status = 'open'\n" +
|
||||
" where u.rid_process = r_lp.rid_process\n" +
|
||||
" and nv(u.status) <> 'open';\n" +
|
||||
"\n" +
|
||||
" end if;\n" +
|
||||
"$$;\n"
|
||||
|
||||
want := "CREATE FUNCTION f(\n" +
|
||||
")\n" +
|
||||
"RETURNS void\n" +
|
||||
"LANGUAGE plpgsql\n" +
|
||||
"AS\n" +
|
||||
"$$\n" +
|
||||
"DECLARE\n" +
|
||||
" r_lp record;\n" +
|
||||
"BEGIN\n" +
|
||||
" if r_lp.total > 0\n" +
|
||||
" and r_lp.totaldone >= r_lp.total\n" +
|
||||
" then\n" +
|
||||
" update core.process u\n" +
|
||||
" set status = 'done'\n" +
|
||||
" where\n" +
|
||||
" u.rid_process = r_lp.rid_process\n" +
|
||||
" and nv(u.status) <> 'done';\n" +
|
||||
" elsif r_lp.total > 0\n" +
|
||||
" then\n" +
|
||||
" update core.process u\n" +
|
||||
" set status = 'open'\n" +
|
||||
" where\n" +
|
||||
" u.rid_process = r_lp.rid_process\n" +
|
||||
" and nv(u.status) <> 'open';\n" +
|
||||
"\n" +
|
||||
" end if;\n" +
|
||||
"$$;\n"
|
||||
|
||||
got := format(src)
|
||||
if got != want {
|
||||
t.Errorf("issue #1 PL/pgSQL indenting\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||
}
|
||||
checkDML(t, "issue #1 PL/pgSQL indenting", got)
|
||||
if !semanticallyEqual(src, got) {
|
||||
t.Errorf("issue #1 PL/pgSQL indenting changed semantics")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorpusIdempotentAndSafe(t *testing.T) {
|
||||
dir := filepath.Join("..", "..", "testdata", "corpus")
|
||||
entries, err := os.ReadDir(dir)
|
||||
|
||||
Reference in New Issue
Block a user