Skip to content

Commit 7908e75

Browse files
authored
Add explain and to-sql options to ecto.query (#746)
- add support for printing query plans with --explain - update --sql flag to --to-sql Closes #742.
1 parent c45cf69 commit 7908e75

2 files changed

Lines changed: 60 additions & 16 deletions

File tree

lib/mix/tasks/ecto.query.ex

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ defmodule Mix.Tasks.Ecto.Query do
66
@shortdoc "Runs a query against the repository"
77

88
@switches [
9+
explain: :boolean,
910
limit: :integer,
1011
repo: [:string, :keep],
11-
sql: :boolean,
12+
to_sql: :boolean,
1213
no_compile: :boolean,
1314
no_deps_check: :boolean
1415
]
@@ -24,19 +25,21 @@ defmodule Mix.Tasks.Ecto.Query do
2425
If a local `.iex.exs` file exists, only aliases from the file are made
2526
available to the query.
2627
27-
The query runs inside a read-only transaction.
28+
Unless `--to-sql` or `--explain` is given, the query runs inside a read-only transaction.
2829
2930
## Examples
3031
3132
$ mix ecto.query "from p in Post, where: p.published"
3233
$ mix ecto.query -r Custom.Repo "from p in Post, limit: 10"
33-
$ mix ecto.query --sql "from p in Post, where: p.published"
34+
$ mix ecto.query --to-sql "from p in Post, where: p.published"
35+
$ mix ecto.query --explain "from p in Post, where: p.published"
3436
3537
## Command line options
3638
3739
* `-r`, `--repo` - the repo to query
3840
* `--limit` - limits the number of printed entries. Defaults to 100.
39-
* `--sql` - prints the generated SQL and parameters instead of running the query
41+
* `--to-sql` - prints the generated SQL and parameters instead of running the query
42+
* `--explain` - prints the query plan instead of running the query
4043
4144
"""
4245

@@ -67,26 +70,37 @@ defmodule Mix.Tasks.Ecto.Query do
6770
end
6871

6972
limit = Keyword.get(opts, :limit, @default_limit)
73+
explain? = Keyword.get(opts, :explain, false)
74+
to_sql? = Keyword.get(opts, :to_sql, false)
7075

7176
if limit < 0 do
7277
Mix.raise("ecto.query expects --limit to be greater than or equal to zero")
7378
end
7479

80+
if explain? and to_sql? do
81+
Mix.raise("ecto.query expects only one of --explain or --to-sql to be given")
82+
end
83+
7584
Mix.Task.run("app.start", args)
7685
ensure_repo(repo, args)
7786

7887
query = eval_query(query)
7988

8089
result =
81-
if opts[:sql] do
82-
{:ok, format_sql(repo, query)}
83-
else
84-
read_only_transaction(repo, fn ->
85-
query
86-
|> repo.all()
87-
|> Enum.take(limit)
88-
|> inspect_entries()
89-
end)
90+
cond do
91+
explain? ->
92+
{:ok, repo.explain(:all, query)}
93+
94+
to_sql? ->
95+
{:ok, format_sql(repo, query)}
96+
97+
true ->
98+
read_only_transaction(repo, fn ->
99+
query
100+
|> repo.all()
101+
|> Enum.take(limit)
102+
|> inspect_entries()
103+
end)
90104
end
91105

92106
result

test/mix/tasks/ecto.query_test.exs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ defmodule Mix.Tasks.Ecto.QueryTest do
176176
assert Inspect.Opts.default_inspect_fun() == previous_fun
177177
end
178178

179-
test "prints SQL and params with --sql" do
179+
test "prints SQL and params with --to-sql" do
180180
Process.put(
181181
:test_repo_to_sql,
182182
{"SELECT p0.\"id\" FROM \"posts\" AS p0 WHERE (p0.\"id\" = $1)", [1]}
@@ -188,7 +188,7 @@ defmodule Mix.Tasks.Ecto.QueryTest do
188188
run([
189189
"-r",
190190
to_string(__MODULE__.PostgresRepo),
191-
"--sql",
191+
"--to-sql",
192192
"from(p in Post, where: p.id == ^1)"
193193
])
194194

@@ -202,6 +202,17 @@ defmodule Mix.Tasks.Ecto.QueryTest do
202202
end)
203203
end
204204

205+
test "prints explain plan with --explain" do
206+
Process.put(:test_repo_explain, "Seq Scan on posts p0")
207+
208+
run(["-r", to_string(__MODULE__.PostgresRepo), "--explain", inspect(Post)])
209+
210+
assert_received {:explain, :all, %Ecto.Query{}, []}
211+
refute_received {:all, _query}
212+
refute_received {:read_only_transaction, __MODULE__.PostgresRepo, _fun, _opts}
213+
assert_received {:mix_shell, :info, ["Seq Scan on posts p0"]}
214+
end
215+
205216
test "runs MySQL queries in a read-only transaction" do
206217
Process.put(:test_repo_all_results, [[1, "first", "hunter2"]])
207218

@@ -216,7 +227,7 @@ defmodule Mix.Tasks.Ecto.QueryTest do
216227
test "prints MySQL SQL without starting a transaction" do
217228
Process.put(:test_repo_to_sql, {"SELECT p0.`id` FROM `posts` AS p0", []})
218229

219-
run(["-r", to_string(__MODULE__.MyXQLRepo), "--sql", inspect(Post)])
230+
run(["-r", to_string(__MODULE__.MyXQLRepo), "--to-sql", inspect(Post)])
220231

221232
refute_received {:myxql_read_only_transaction, __MODULE__.MyXQLRepo, _fun, _opts}
222233
assert_received {:myxql_to_sql, :all, %Ecto.Query{}, []}
@@ -269,6 +280,20 @@ defmodule Mix.Tasks.Ecto.QueryTest do
269280
end
270281
end
271282

283+
test "raises when explain and to_sql are given together" do
284+
assert_raise Mix.Error,
285+
"ecto.query expects only one of --explain or --to-sql to be given",
286+
fn ->
287+
run([
288+
"-r",
289+
to_string(__MODULE__.PostgresRepo),
290+
"--explain",
291+
"--to-sql",
292+
inspect(Post)
293+
])
294+
end
295+
end
296+
272297
test "raises when the adapter does not support read-only transactions" do
273298
assert_raise ArgumentError, ~r/read-only transactions are not supported/, fn ->
274299
run(["-r", to_string(__MODULE__.NoReadOnlyRepo), inspect(Post)])
@@ -323,6 +348,11 @@ defmodule Mix.Tasks.Ecto.QueryTest do
323348
send(self(), {:to_sql, operation, queryable, opts})
324349
Process.get(:test_repo_to_sql, {"SELECT s0.\"id\" FROM \"posts\" AS s0", []})
325350
end
351+
352+
def explain(operation, queryable, opts \\ []) do
353+
send(self(), {:explain, operation, queryable, opts})
354+
Process.get(:test_repo_explain, "Seq Scan on posts p0")
355+
end
326356
end
327357

328358
defmodule PostgresAdapter do

0 commit comments

Comments
 (0)