Skip to content

Commit 56d8a7d

Browse files
committed
build: First pass at sqlc vet
1 parent 91c99b9 commit 56d8a7d

File tree

5 files changed

+154
-0
lines changed

5 files changed

+154
-0
lines changed

go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/cubicdaiya/gonp v1.0.4
99
github.com/davecgh/go-spew v1.1.1
1010
github.com/go-sql-driver/mysql v1.7.1
11+
github.com/google/cel-go v0.16.0
1112
github.com/google/go-cmp v0.5.9
1213
github.com/jackc/pgconn v1.14.0
1314
github.com/jackc/pgx/v4 v4.18.1
@@ -24,6 +25,8 @@ require (
2425
gopkg.in/yaml.v3 v3.0.1
2526
)
2627

28+
require github.com/stoewer/go-strcase v1.2.0 // indirect
29+
2730
require (
2831
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect
2932
github.com/golang/protobuf v1.5.3 // indirect

go.sum

+4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
3636
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
3737
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
3838
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
39+
github.com/google/cel-go v0.16.0 h1:DG9YQ8nFCFXAs/FDDwBxmL1tpKNrdlGUM9U3537bX/Y=
40+
github.com/google/cel-go v0.16.0/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY=
3941
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
4042
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
4143
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -151,6 +153,8 @@ github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
151153
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
152154
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
153155
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
156+
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
157+
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
154158
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
155159
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
156160
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=

internal/cmd/cmd.go

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func Do(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int
4444
rootCmd.AddCommand(initCmd)
4545
rootCmd.AddCommand(versionCmd)
4646
rootCmd.AddCommand(uploadCmd)
47+
rootCmd.AddCommand(NewCmdVet())
4748

4849
rootCmd.SetArgs(args)
4950
rootCmd.SetIn(stdin)

internal/cmd/vet.go

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"io"
8+
"os"
9+
"path/filepath"
10+
"runtime/trace"
11+
12+
"github.com/google/cel-go/cel"
13+
"github.com/spf13/cobra"
14+
15+
"github.com/kyleconroy/sqlc/internal/config"
16+
"github.com/kyleconroy/sqlc/internal/debug"
17+
"github.com/kyleconroy/sqlc/internal/opts"
18+
"github.com/kyleconroy/sqlc/internal/plugin"
19+
)
20+
21+
func NewCmdVet() *cobra.Command {
22+
return &cobra.Command{
23+
Use: "vet",
24+
Short: "Vet examines queries",
25+
RunE: func(cmd *cobra.Command, args []string) error {
26+
defer trace.StartRegion(cmd.Context(), "vet").End()
27+
stderr := cmd.ErrOrStderr()
28+
dir, name := getConfigPath(stderr, cmd.Flag("file"))
29+
if err := examine(cmd.Context(), ParseEnv(cmd), dir, name, stderr); err != nil {
30+
fmt.Fprintf(stderr, "%s\n", err)
31+
os.Exit(1)
32+
}
33+
return nil
34+
},
35+
}
36+
}
37+
38+
func examine(ctx context.Context, e Env, dir, filename string, stderr io.Writer) error {
39+
configPath, conf, err := readConfig(stderr, dir, filename)
40+
if err != nil {
41+
return err
42+
}
43+
44+
base := filepath.Base(configPath)
45+
if err := config.Validate(conf); err != nil {
46+
fmt.Fprintf(stderr, "error validating %s: %s\n", base, err)
47+
return err
48+
}
49+
50+
if err := e.Validate(conf); err != nil {
51+
fmt.Fprintf(stderr, "error validating %s: %s\n", base, err)
52+
return err
53+
}
54+
55+
env, err := cel.NewEnv(
56+
cel.StdLib(),
57+
cel.Types(&plugin.Query{}),
58+
cel.Variable("query",
59+
cel.ObjectType("plugin.Query"),
60+
),
61+
)
62+
if err != nil {
63+
return fmt.Errorf("new env; %s", err)
64+
}
65+
66+
checks := map[string]cel.Program{}
67+
68+
for _, c := range conf.Checks {
69+
// TODO: Verify check has a name
70+
// TODO: Verify that check names are unique
71+
ast, issues := env.Compile(c.Expr)
72+
if issues != nil && issues.Err() != nil {
73+
return fmt.Errorf("type-check error: %s %s", c.Name, issues.Err())
74+
}
75+
prg, err := env.Program(ast)
76+
if err != nil {
77+
return fmt.Errorf("program construction error: %s %s", c.Name, err)
78+
}
79+
checks[c.Name] = prg
80+
}
81+
82+
errored := true
83+
for _, sql := range conf.SQL {
84+
combo := config.Combine(*conf, sql)
85+
86+
// TODO: This feels like a hack that will bite us later
87+
joined := make([]string, 0, len(sql.Schema))
88+
for _, s := range sql.Schema {
89+
joined = append(joined, filepath.Join(dir, s))
90+
}
91+
sql.Schema = joined
92+
93+
joined = make([]string, 0, len(sql.Queries))
94+
for _, q := range sql.Queries {
95+
joined = append(joined, filepath.Join(dir, q))
96+
}
97+
sql.Queries = joined
98+
99+
var name string
100+
parseOpts := opts.Parser{
101+
Debug: debug.Debug,
102+
}
103+
104+
var errout bytes.Buffer
105+
result, failed := parse(ctx, name, dir, sql, combo, parseOpts, &errout)
106+
if failed {
107+
return nil
108+
}
109+
req := codeGenRequest(result, combo)
110+
for _, q := range req.Queries {
111+
for _, name := range sql.Checks {
112+
prg, ok := checks[name]
113+
if !ok {
114+
// TODO: Return a helpful error message
115+
continue
116+
}
117+
out, _, err := prg.Eval(map[string]any{
118+
"query": q,
119+
})
120+
if err != nil {
121+
return err
122+
}
123+
tripped, ok := out.Value().(bool)
124+
if !ok {
125+
return fmt.Errorf("expression returned non-bool: %s", err)
126+
}
127+
if tripped {
128+
// internal/cmd/vet.go:123:13: fmt.Errorf format %s has arg false of wrong type bool
129+
fmt.Fprintf(stderr, q.Filename+":17:1: query uses :exec\n")
130+
errored = true
131+
}
132+
}
133+
}
134+
}
135+
if errored {
136+
return fmt.Errorf("errored")
137+
}
138+
return nil
139+
}

internal/config/config.go

+7
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ type Config struct {
6262
SQL []SQL `json:"sql" yaml:"sql"`
6363
Gen Gen `json:"overrides,omitempty" yaml:"overrides"`
6464
Plugins []Plugin `json:"plugins" yaml:"plugins"`
65+
Checks []Check `json:"checks" yaml:"checks"`
6566
}
6667

6768
type Project struct {
@@ -85,6 +86,11 @@ type Plugin struct {
8586
} `json:"wasm" yaml:"wasm"`
8687
}
8788

89+
type Check struct {
90+
Name string `json:"name" yaml:"name"`
91+
Expr string `json:"expr" yaml:"expr"`
92+
}
93+
8894
type Gen struct {
8995
Go *GenGo `json:"go,omitempty" yaml:"go"`
9096
}
@@ -102,6 +108,7 @@ type SQL struct {
102108
StrictOrderBy *bool `json:"strict_order_by" yaml:"strict_order_by"`
103109
Gen SQLGen `json:"gen" yaml:"gen"`
104110
Codegen []Codegen `json:"codegen" yaml:"codegen"`
111+
Checks []string `json:"checks" yaml:"checks"`
105112
}
106113

107114
// TODO: Figure out a better name for this

0 commit comments

Comments
 (0)