Skip to content

Commit fc99f20

Browse files
feat(vet): Add output from EXPLAIN ... for queries to the CEL program environment (#2489)
* A working prototype of sqlc vet with explain output * Some improvements to sqlc vet with explain * Clean up the PostgreSQLExplain proto message * Improve vet handling of MySQL explain output * Add a few more fields to the MySQLExplain proto message * Wrap a little more context around CEL programs in vet rules * Use proto messages for postgres and mysql cel env vars * Introduce SQLCDEBUG=dumpexplain=1 * Cleaning up a little * Adding some documentation for sqlc vet with `EXPLAIN ...` output * Disable triggering MySQL explain vet rule in authors example
1 parent bf49d88 commit fc99f20

File tree

8 files changed

+11970
-4590
lines changed

8 files changed

+11970
-4590
lines changed

docs/howto/vet.md

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ If an expression evaluates to `true`, `sqlc vet` will report an error using the
1111

1212
## Defining lint rules
1313

14-
Each lint rule's CEL expression has access to variables from your sqlc configuration and queries,
15-
defined in the following struct.
14+
Each lint rule's CEL expression has access to information from your sqlc configuration and queries
15+
via variables defined in the following proto messages.
1616

1717
```proto
1818
message Config
@@ -41,12 +41,17 @@ message Parameter
4141
}
4242
```
4343

44-
This struct will likely expand in the future to include more query information.
45-
We may also add information returned from a running database, such as the result from
46-
`EXPLAIN ...`.
44+
In addition to this basic information, when you have a PostgreSQL or MySQL
45+
[database connection configured](../reference/config.html#database)
46+
each CEL expression has access to the output from running `EXPLAIN ...` on your query
47+
via the `postgresql.explain` and `mysql.explain` variables.
48+
This output is quite complex and depends on the structure of your query but sqlc attempts
49+
to parse and provide as much information as it can. See
50+
[Rules using `EXPLAIN ...` output](#rules-using-explain-output) for more information.
4751

48-
While these examples are simplistic, they give you a flavor of the types of
49-
rules you can write.
52+
Here are a few example rules just using the basic configuration and query information available
53+
to the CEL expression environment. While these examples are simplistic, they give you a flavor
54+
of the types of rules you can write.
5055

5156
```yaml
5257
version: 2
@@ -82,6 +87,82 @@ rules:
8287
query.cmd == "exec"
8388
```
8489
90+
### Rules using `EXPLAIN ...` output
91+
92+
The CEL expression environment has two variables containing `EXPLAIN ...` output,
93+
`postgresql.explain` and `mysql.explain`. `sqlc` only populates the variable associated with
94+
your configured database engine, and only when you have a
95+
[database connection configured](../reference/config.html#database).
96+
97+
For the `postgresql` engine, `sqlc` runs
98+
99+
```sql
100+
EXPLAIN (ANALYZE false, VERBOSE, COSTS, SETTINGS, BUFFERS, FORMAT JSON) ...
101+
```
102+
103+
where `"..."` is your query string, and parses the output into a `PostgreSQLExplain` proto message.
104+
105+
For the `mysql` engine, `sqlc` runs
106+
107+
```sql
108+
EXPLAIN FORMAT=JSON ...
109+
```
110+
111+
where `"..."` is your query string, and parses the output into a `MySQLExplain` proto message.
112+
113+
These proto message definitions are too long to include here, but you can find them in the `protos`
114+
directory within the `sqlc` source tree.
115+
116+
The output from `EXPLAIN ...` depends on the structure of your query so it's a bit difficult
117+
to offer generic examples. Refer to the
118+
[PostgreSQL documentation](https://www.postgresql.org/docs/current/using-explain.html) and
119+
[MySQL documentation](https://dev.mysql.com/doc/refman/en/explain-output.html) for more
120+
information.
121+
122+
```yaml
123+
...
124+
rules:
125+
- name: postgresql-query-too-costly
126+
message: "Query cost estimate is too high"
127+
rule: "postgresql.explain.plan.total_cost > 1.0"
128+
- name: postgresql-no-seq-scan
129+
message: "Query plan results in a sequential scan"
130+
rule: "postgresql.explain.plan.node_type == 'Seq Scan'"
131+
- name: mysql-query-too-costly
132+
message: "Query cost estimate is too high"
133+
rule: "has(mysql.explain.query_block.cost_info) && double(mysql.explain.query_block.cost_info.query_cost) > 2.0"
134+
- name: mysql-must-use-primary-key
135+
message: "Query plan doesn't use primary key"
136+
rule: "has(mysql.explain.query_block.table.key) && mysql.explain.query_block.table.key != 'PRIMARY'"
137+
```
138+
139+
When building rules that depend on `EXPLAIN ...` output, it may be helpful to see the actual JSON
140+
returned from the database. `sqlc` will print it When you set the environment variable
141+
`SQLCDEBUG=dumpexplain=1`. Use this environment variable together with a dummy rule to see
142+
`EXPLAIN ...` output for all of your queries.
143+
144+
```yaml
145+
version: 2
146+
sql:
147+
- schema: "query.sql"
148+
queries: "query.sql"
149+
engine: "postgresql"
150+
gen:
151+
go:
152+
package: "db"
153+
out: "db"
154+
rules:
155+
- debug
156+
rules:
157+
- name: debug
158+
message: "Debug"
159+
rule: has(postgresql.explain)
160+
```
161+
162+
Please note that `sqlc` does not manage or migrate your database. Use your
163+
migration tool of choice to create the necessary database tables and objects
164+
before running `sqlc vet` with rules that depend on `EXPLAIN ...` output.
165+
85166
## Built-in rules
86167

87168
### sqlc/db-prepare

docs/reference/environment-variables.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,13 @@ return an error.
126126

127127
`SQLCDEBUG=processplugins=0`
128128

129+
### dumpexplain
130+
131+
The `dumpexplain` command prints the JSON-formatted result from running
132+
`EXPLAIN ...` on a query when a `sqlc vet` rule evaluation requires its output.
133+
134+
`SQLCDEBUG=dumpexplain=1`
135+
129136
## SQLCTMPDIR
130137

131138
If specified, use the given directory as the base for temporary folders. Only

examples/authors/sqlc.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ sql:
77
uri: postgresql://${PG_USER}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/authors
88
rules:
99
- sqlc/db-prepare
10+
- postgresql-query-too-costly
1011
gen:
1112
go:
1213
package: authors
@@ -18,6 +19,7 @@ sql:
1819
uri: root:${MYSQL_ROOT_PASSWORD}@tcp(${MYSQL_HOST}:${MYSQL_PORT})/authors?multiStatements=true&parseTime=true
1920
rules:
2021
- sqlc/db-prepare
22+
# - mysql-query-too-costly
2123
gen:
2224
go:
2325
package: authors
@@ -32,4 +34,11 @@ sql:
3234
gen:
3335
go:
3436
package: authors
35-
out: sqlite
37+
out: sqlite
38+
rules:
39+
- name: postgresql-query-too-costly
40+
message: "Too costly"
41+
rule: "postgresql.explain.plan.total_cost > 300.0"
42+
- name: mysql-query-too-costly
43+
message: "Too costly"
44+
rule: "has(mysql.explain.query_block.cost_info) && double(mysql.explain.query_block.cost_info.query_cost) > 2.0"

0 commit comments

Comments
 (0)