Skip to content

Commit a58e3a5

Browse files
feat(vet): Add default query parameters for explain queries (#2543)
* feat(vet): add default query parameters for vet explain with MySQL * feat(vet): Add default parameter values for PostgreSQL Before this change, all parameters to a query would be set to NULL when explaining the query. For both PostgreSQL and MySQL, this caused the explain output to either error or return unhelpful results. For MySQL, these NULL parameters would cause "Impossible WHERE clause" errors. For PostgreSQL, the EXPLAIN output would return plans without indexes. The default parameters aren't complete and won't cover all cases. We're working on a fallback mechanism that would allow you to specify the explain parameters values directly. Stay tuned! --------- Co-authored-by: Andrew Benton <[email protected]>
1 parent 3b48228 commit a58e3a5

File tree

8 files changed

+1093
-4
lines changed

8 files changed

+1093
-4
lines changed

docs/howto/vet.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ rules:
8989
9090
### Rules using `EXPLAIN ...` output
9191

92+
*Added in v1.20.0*
93+
9294
The CEL expression environment has two variables containing `EXPLAIN ...` output,
9395
`postgresql.explain` and `mysql.explain`. `sqlc` only populates the variable associated with
9496
your configured database engine, and only when you have a
@@ -100,15 +102,15 @@ For the `postgresql` engine, `sqlc` runs
100102
EXPLAIN (ANALYZE false, VERBOSE, COSTS, SETTINGS, BUFFERS, FORMAT JSON) ...
101103
```
102104

103-
where `"..."` is your query string, and parses the output into a `PostgreSQLExplain` proto message.
105+
where `"..."` is your query string, and parses the output into a [`PostgreSQLExplain`](https://buf.build/sqlc/sqlc/docs/v1.20.0:vet#vet.PostgreSQLExplain) proto message.
104106

105107
For the `mysql` engine, `sqlc` runs
106108

107109
```sql
108110
EXPLAIN FORMAT=JSON ...
109111
```
110112

111-
where `"..."` is your query string, and parses the output into a `MySQLExplain` proto message.
113+
where `"..."` is your query string, and parses the output into a [`MySQLExplain`](https://buf.build/sqlc/sqlc/docs/v1.20.0:vet#vet.MySQLExplain) proto message.
112114

113115
These proto message definitions are too long to include here, but you can find them in the `protos`
114116
directory within the `sqlc` source tree.

internal/cmd/vet.go

+134-2
Original file line numberDiff line numberDiff line change
@@ -170,16 +170,145 @@ func (p *pgxConn) Prepare(ctx context.Context, name, query string) error {
170170
return err
171171
}
172172

173+
// Return a default value for a PostgreSQL column based on its type. Returns nil
174+
// if the type is unknown.
175+
func pgDefaultValue(col *plugin.Column) any {
176+
if col == nil {
177+
return nil
178+
}
179+
if col.Type == nil {
180+
return nil
181+
}
182+
typname := strings.TrimPrefix(col.Type.Name, "pg_catalog.")
183+
switch typname {
184+
case "any", "void":
185+
if col.IsArray {
186+
return []any{}
187+
} else {
188+
return nil
189+
}
190+
case "anyarray":
191+
return []any{}
192+
case "bool", "boolean":
193+
if col.IsArray {
194+
return []bool{}
195+
} else {
196+
return false
197+
}
198+
case "double", "double precision", "real":
199+
if col.IsArray {
200+
return []float32{}
201+
} else {
202+
return 0.1
203+
}
204+
case "json", "jsonb":
205+
if col.IsArray {
206+
return []string{}
207+
} else {
208+
return "{}"
209+
}
210+
case "citext", "string", "text", "varchar":
211+
if col.IsArray {
212+
return []string{}
213+
} else {
214+
return ""
215+
}
216+
case "bigint", "bigserial", "integer", "int", "int2", "int4", "int8", "serial":
217+
if col.IsArray {
218+
return []int{}
219+
} else {
220+
return 1
221+
}
222+
case "date", "time", "timestamp", "timestamptz":
223+
if col.IsArray {
224+
return []time.Time{}
225+
} else {
226+
return time.Time{}
227+
}
228+
case "uuid":
229+
if col.IsArray {
230+
return []string{}
231+
} else {
232+
return "00000000-0000-0000-0000-000000000000"
233+
}
234+
case "numeric", "decimal":
235+
if col.IsArray {
236+
return []string{}
237+
} else {
238+
return "0.1"
239+
}
240+
case "inet":
241+
if col.IsArray {
242+
return []string{}
243+
} else {
244+
return "192.168.0.1/24"
245+
}
246+
case "cidr":
247+
if col.IsArray {
248+
return []string{}
249+
} else {
250+
return "192.168.1/24"
251+
}
252+
default:
253+
return nil
254+
}
255+
}
256+
257+
// Return a default value for a MySQL column based on its type. Returns nil
258+
// if the type is unknown.
259+
func mysqlDefaultValue(col *plugin.Column) any {
260+
if col == nil {
261+
return nil
262+
}
263+
if col.Type == nil {
264+
return nil
265+
}
266+
switch col.Type.Name {
267+
case "any":
268+
return nil
269+
case "bool":
270+
return false
271+
case "int", "bigint", "mediumint", "smallint", "tinyint", "bit":
272+
return 1
273+
case "decimal": // "numeric", "dec", "fixed"
274+
// No perfect choice here to avoid "Impossible WHERE" but I think
275+
// 0.1 is decent. It works for all cases where `scale` > 0 which
276+
// should be the majority. For more information refer to
277+
// https://dev.mysql.com/doc/refman/8.1/en/fixed-point-types.html.
278+
return 0.1
279+
case "float", "double":
280+
return 0.1
281+
case "date":
282+
return "0000-00-00"
283+
case "datetime", "timestamp":
284+
return "0000-00-00 00:00:00"
285+
case "time":
286+
return "00:00:00"
287+
case "year":
288+
return "0000"
289+
case "char", "varchar", "binary", "varbinary", "tinyblob", "blob",
290+
"mediumblob", "longblob", "tinytext", "text", "mediumtext", "longtext":
291+
return ""
292+
case "json":
293+
return "{}"
294+
default:
295+
return nil
296+
}
297+
}
298+
173299
func (p *pgxConn) Explain(ctx context.Context, query string, args ...*plugin.Parameter) (*vetEngineOutput, error) {
174300
eQuery := "EXPLAIN (ANALYZE false, VERBOSE, COSTS, SETTINGS, BUFFERS, FORMAT JSON) " + query
175301
eArgs := make([]any, len(args))
302+
for i, a := range args {
303+
eArgs[i] = pgDefaultValue(a.Column)
304+
}
176305
row := p.c.QueryRow(ctx, eQuery, eArgs...)
177306
var result []json.RawMessage
178307
if err := row.Scan(&result); err != nil {
179308
return nil, err
180309
}
181310
if debug.Debug.DumpExplain {
182-
fmt.Println(eQuery)
311+
fmt.Println(eQuery, "with args", eArgs)
183312
fmt.Println(string(result[0]))
184313
}
185314
var explain vet.PostgreSQLExplain
@@ -210,13 +339,16 @@ type mysqlExplainer struct {
210339
func (me *mysqlExplainer) Explain(ctx context.Context, query string, args ...*plugin.Parameter) (*vetEngineOutput, error) {
211340
eQuery := "EXPLAIN FORMAT=JSON " + query
212341
eArgs := make([]any, len(args))
342+
for i, a := range args {
343+
eArgs[i] = mysqlDefaultValue(a.Column)
344+
}
213345
row := me.QueryRowContext(ctx, eQuery, eArgs...)
214346
var result json.RawMessage
215347
if err := row.Scan(&result); err != nil {
216348
return nil, err
217349
}
218350
if debug.Debug.DumpExplain {
219-
fmt.Println(eQuery)
351+
fmt.Println(eQuery, "with args", eArgs)
220352
fmt.Println(string(result))
221353
}
222354
var explain vet.MySQLExplain

internal/endtoend/testdata/vet_explain/mysql/db/db.go

+31
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/endtoend/testdata/vet_explain/mysql/db/models.go

+137
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)