Skip to content

Commit 53d1792

Browse files
authored
feat(sql/ast): Render AST to SQL (#2815)
Add a new Format method to AST nodes to support writing an AST tree back to SQL. The rendered SQL is checked to be equivalent to the input SQL, not exact. The tests current run against all query.sql files in pgx/v5 directories. As support is improved, we'll expand to all PostgreSQL queries. The hope is to eventually use this to power a sqlc fmt subcommand, but that is a ways off from happening.
1 parent 2cd364c commit 53d1792

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+1249
-108
lines changed

internal/endtoend/case_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
"testing"
10+
)
11+
12+
type Testcase struct {
13+
Name string
14+
Path string
15+
ConfigName string
16+
Stderr []byte
17+
Exec *Exec
18+
}
19+
20+
type Exec struct {
21+
Command string `json:"command"`
22+
Contexts []string `json:"contexts"`
23+
Process string `json:"process"`
24+
Env map[string]string `json:"env"`
25+
}
26+
27+
func parseStderr(t *testing.T, dir, testctx string) []byte {
28+
t.Helper()
29+
paths := []string{
30+
filepath.Join(dir, "stderr", fmt.Sprintf("%s.txt", testctx)),
31+
filepath.Join(dir, "stderr.txt"),
32+
}
33+
for _, path := range paths {
34+
if _, err := os.Stat(path); !os.IsNotExist(err) {
35+
blob, err := os.ReadFile(path)
36+
if err != nil {
37+
t.Fatal(err)
38+
}
39+
return blob
40+
}
41+
}
42+
return nil
43+
}
44+
45+
func parseExec(t *testing.T, dir string) *Exec {
46+
t.Helper()
47+
path := filepath.Join(dir, "exec.json")
48+
if _, err := os.Stat(path); os.IsNotExist(err) {
49+
return nil
50+
}
51+
var e Exec
52+
blob, err := os.ReadFile(path)
53+
if err != nil {
54+
t.Fatal(err)
55+
}
56+
if err := json.Unmarshal(blob, &e); err != nil {
57+
t.Fatal(err)
58+
}
59+
if e.Command == "" {
60+
e.Command = "generate"
61+
}
62+
return &e
63+
}
64+
65+
func FindTests(t *testing.T, root, testctx string) []*Testcase {
66+
var tcs []*Testcase
67+
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
68+
if err != nil {
69+
return err
70+
}
71+
if info.Name() == "sqlc.json" || info.Name() == "sqlc.yaml" || info.Name() == "sqlc.yml" {
72+
dir := filepath.Dir(path)
73+
tcs = append(tcs, &Testcase{
74+
Path: dir,
75+
Name: strings.TrimPrefix(dir, root+string(filepath.Separator)),
76+
ConfigName: info.Name(),
77+
Stderr: parseStderr(t, dir, testctx),
78+
Exec: parseExec(t, dir),
79+
})
80+
return filepath.SkipDir
81+
}
82+
return nil
83+
})
84+
if err != nil {
85+
t.Fatal(err)
86+
}
87+
return tcs
88+
}

internal/endtoend/endtoend_test.go

Lines changed: 16 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package main
33
import (
44
"bytes"
55
"context"
6-
"encoding/json"
7-
"fmt"
86
"os"
97
osexec "os/exec"
108
"path/filepath"
@@ -97,20 +95,6 @@ func TestReplay(t *testing.T) {
9795

9896
// t.Parallel()
9997
ctx := context.Background()
100-
var dirs []string
101-
err := filepath.Walk("testdata", func(path string, info os.FileInfo, err error) error {
102-
if err != nil {
103-
return err
104-
}
105-
if info.Name() == "sqlc.json" || info.Name() == "sqlc.yaml" || info.Name() == "sqlc.yml" {
106-
dirs = append(dirs, filepath.Dir(path))
107-
return filepath.SkipDir
108-
}
109-
return nil
110-
})
111-
if err != nil {
112-
t.Fatal(err)
113-
}
11498

11599
contexts := map[string]textContext{
116100
"base": {
@@ -135,24 +119,29 @@ func TestReplay(t *testing.T) {
135119
},
136120
}
137121

138-
for _, replay := range dirs {
139-
tc := replay
140-
for name, testctx := range contexts {
141-
name := name
142-
testctx := testctx
122+
for name, testctx := range contexts {
123+
name := name
124+
testctx := testctx
143125

144-
if !testctx.Enabled() {
145-
continue
146-
}
126+
if !testctx.Enabled() {
127+
continue
128+
}
147129

148-
t.Run(filepath.Join(name, tc), func(t *testing.T) {
130+
for _, replay := range FindTests(t, "testdata", name) {
131+
tc := replay
132+
t.Run(filepath.Join(name, tc.Name), func(t *testing.T) {
149133
t.Parallel()
134+
150135
var stderr bytes.Buffer
151136
var output map[string]string
152137
var err error
153138

154-
path, _ := filepath.Abs(tc)
155-
args := parseExec(t, path)
139+
path, _ := filepath.Abs(tc.Path)
140+
args := tc.Exec
141+
if args == nil {
142+
args = &Exec{Command: "generate"}
143+
}
144+
expected := string(tc.Stderr)
156145

157146
if args.Process != "" {
158147
_, err := osexec.LookPath(args.Process)
@@ -167,7 +156,6 @@ func TestReplay(t *testing.T) {
167156
}
168157
}
169158

170-
expected := expectedStderr(t, path, name)
171159
opts := cmd.Options{
172160
Env: cmd.Env{
173161
Debug: opts.DebugFromString(args.Env["SQLCDEBUG"]),
@@ -263,50 +251,6 @@ func cmpDirectory(t *testing.T, dir string, actual map[string]string) {
263251
}
264252
}
265253

266-
func expectedStderr(t *testing.T, dir, testctx string) string {
267-
t.Helper()
268-
paths := []string{
269-
filepath.Join(dir, "stderr", fmt.Sprintf("%s.txt", testctx)),
270-
filepath.Join(dir, "stderr.txt"),
271-
}
272-
for _, path := range paths {
273-
if _, err := os.Stat(path); !os.IsNotExist(err) {
274-
blob, err := os.ReadFile(path)
275-
if err != nil {
276-
t.Fatal(err)
277-
}
278-
return string(blob)
279-
}
280-
}
281-
return ""
282-
}
283-
284-
type exec struct {
285-
Command string `json:"command"`
286-
Process string `json:"process"`
287-
Contexts []string `json:"contexts"`
288-
Env map[string]string `json:"env"`
289-
}
290-
291-
func parseExec(t *testing.T, dir string) exec {
292-
t.Helper()
293-
var e exec
294-
path := filepath.Join(dir, "exec.json")
295-
if _, err := os.Stat(path); !os.IsNotExist(err) {
296-
blob, err := os.ReadFile(path)
297-
if err != nil {
298-
t.Fatal(err)
299-
}
300-
if err := json.Unmarshal(blob, &e); err != nil {
301-
t.Fatal(err)
302-
}
303-
}
304-
if e.Command == "" {
305-
e.Command = "generate"
306-
}
307-
return e
308-
}
309-
310254
func BenchmarkReplay(b *testing.B) {
311255
ctx := context.Background()
312256
var dirs []string

internal/endtoend/fmt_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
"testing"
10+
11+
pg_query "github.com/pganalyze/pg_query_go/v4"
12+
"github.com/sqlc-dev/sqlc/internal/debug"
13+
"github.com/sqlc-dev/sqlc/internal/engine/postgresql"
14+
"github.com/sqlc-dev/sqlc/internal/sql/ast"
15+
)
16+
17+
func TestFormat(t *testing.T) {
18+
t.Parallel()
19+
parse := postgresql.NewParser()
20+
for _, tc := range FindTests(t, "testdata", "base") {
21+
tc := tc
22+
23+
if !strings.Contains(tc.Path, filepath.Join("pgx/v5")) {
24+
continue
25+
}
26+
27+
q := filepath.Join(tc.Path, "query.sql")
28+
if _, err := os.Stat(q); os.IsNotExist(err) {
29+
continue
30+
}
31+
32+
t.Run(tc.Name, func(t *testing.T) {
33+
contents, err := os.ReadFile(q)
34+
if err != nil {
35+
t.Fatal(err)
36+
}
37+
for i, query := range bytes.Split(bytes.TrimSpace(contents), []byte(";")) {
38+
if len(query) <= 1 {
39+
continue
40+
}
41+
query := query
42+
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
43+
expected, err := pg_query.Fingerprint(string(query))
44+
if err != nil {
45+
t.Fatal(err)
46+
}
47+
stmts, err := parse.Parse(bytes.NewReader(query))
48+
if err != nil {
49+
t.Fatal(err)
50+
}
51+
if len(stmts) != 1 {
52+
t.Fatal("expected one statement")
53+
}
54+
if false {
55+
r, err := pg_query.Parse(string(query))
56+
debug.Dump(r, err)
57+
}
58+
59+
out := ast.Format(stmts[0].Raw)
60+
actual, err := pg_query.Fingerprint(out)
61+
if err != nil {
62+
t.Error(err)
63+
}
64+
if expected != actual {
65+
debug.Dump(stmts[0].Raw)
66+
t.Errorf("- %s", expected)
67+
t.Errorf("- %s", string(query))
68+
t.Errorf("+ %s", actual)
69+
t.Errorf("+ %s", out)
70+
}
71+
})
72+
}
73+
})
74+
}
75+
}

internal/endtoend/testdata/join_where_clause/postgresql/pgx/v5/query.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ WHERE owner = $1;
1414
SELECT foo.*
1515
FROM foo
1616
CROSS JOIN bar
17-
WHERE bar.id = $2 AND owner = $1;
17+
WHERE bar.id = $2 AND owner = $1;

internal/endtoend/testdata/materialized_views/postgresql/pgx/v4/go/query.sql.go

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,2 @@
1-
CREATE TABLE authors (
2-
id BIGSERIAL PRIMARY KEY,
3-
name TEXT NOT NULL,
4-
bio TEXT
5-
);
6-
7-
ALTER TABLE authors ADD COLUMN gender INTEGER NULL;
8-
9-
CREATE MATERIALIZED VIEW authors_names as SELECT name from authors;
10-
1+
-- name: ListAuthors :many
112
SELECT * FROM authors;

internal/endtoend/testdata/materialized_views/postgresql/pgx/v5/go/query.sql.go

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)