diff --git a/integration_test/myxql/explain_test.exs b/integration_test/myxql/explain_test.exs index e51c0ad5..499975d7 100644 --- a/integration_test/myxql/explain_test.exs +++ b/integration_test/myxql/explain_test.exs @@ -10,7 +10,7 @@ defmodule Ecto.Integration.ExplainTest do explain = TestRepo.explain(:all, from(p in Post, where: p.title == "title"), timeout: 20000) assert explain =~ - "| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |" + "| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |" assert explain =~ "p0" assert explain =~ "SIMPLE" @@ -24,7 +24,9 @@ defmodule Ecto.Integration.ExplainTest do end test "update" do - explain = TestRepo.explain(:update_all, from(p in Post, update: [set: [title: "new title"]])) + explain = + TestRepo.explain(:update_all, from(p in Post, update: [set: [title: "new title"]])) + assert explain =~ "UPDATE" assert explain =~ "p0" end @@ -37,10 +39,20 @@ defmodule Ecto.Integration.ExplainTest do test "map format" do [explain] = TestRepo.explain(:all, Post, format: :map) - keys = explain["query_block"] |> Map.keys + keys = explain["query_block"] |> Map.keys() assert Enum.member?(keys, "cost_info") assert Enum.member?(keys, "select_id") assert Enum.member?(keys, "table") end + + test "explain without rolling back" do + {:ok, {:ok, explain}} = + TestRepo.transaction(fn -> + TestRepo.explain(:delete_all, Post, wrap_in_transaction: false, timeout: 20000) + end) + + assert explain =~ "DELETE" + assert explain =~ "p0" + end end end diff --git a/integration_test/pg/explain_test.exs b/integration_test/pg/explain_test.exs index cfbee0ba..cb57b9bb 100644 --- a/integration_test/pg/explain_test.exs +++ b/integration_test/pg/explain_test.exs @@ -81,4 +81,22 @@ defmodule Ecto.Integration.ExplainTest do assert explain =~ ~r/Node Type:/ assert explain =~ ~r/Relation Name:/ end + + test "explain without rolling back" do + TestRepo.insert!(%Post{}) + assert [%Post{}] = TestRepo.all(Post) + + {:ok, {:ok, explain}} = + TestRepo.transaction(fn -> + TestRepo.explain(:delete_all, Post, + analyze: true, + wrap_in_transaction: false, + timeout: 20000 + ) + end) + + assert explain =~ "Delete on posts p0" + assert explain =~ "cost=" + assert TestRepo.all(Post) == [] + end end diff --git a/integration_test/tds/explain_test.exs b/integration_test/tds/explain_test.exs index 11285453..ef326c11 100644 --- a/integration_test/tds/explain_test.exs +++ b/integration_test/tds/explain_test.exs @@ -7,7 +7,9 @@ defmodule Ecto.Integration.ExplainTest do describe "explain" do test "select" do - explain = TestRepo.explain(:all, from(p in Post, where: p.title == "explain_test", limit: 1)) + explain = + TestRepo.explain(:all, from(p in Post, where: p.title == "explain_test", limit: 1)) + assert explain =~ "| Rows | Executes |" assert explain =~ "| Parallel | EstimateExecutions |" assert explain =~ "SELECT TOP(1)" @@ -21,7 +23,9 @@ defmodule Ecto.Integration.ExplainTest do end test "update" do - explain = TestRepo.explain(:update_all, from(p in Post, update: [set: [title: "new title"]])) + explain = + TestRepo.explain(:update_all, from(p in Post, update: [set: [title: "new title"]])) + assert explain =~ "UPDATE" assert explain =~ "p0" assert explain =~ "new title" @@ -32,5 +36,15 @@ defmodule Ecto.Integration.ExplainTest do TestRepo.explain(:all, from(p in "posts", select: p.invalid, where: p.invalid == "title")) end) end + + test "explain without rolling back" do + {:ok, {:ok, explain}} = + TestRepo.transaction(fn -> + TestRepo.explain(:delete_all, Post, wrap_in_transaction: false, timeout: 20000) + end) + + assert explain =~ "DELETE" + assert explain =~ "p0" + end end end diff --git a/lib/ecto/adapters/sql.ex b/lib/ecto/adapters/sql.ex index 781009af..68604e47 100644 --- a/lib/ecto/adapters/sql.ex +++ b/lib/ecto/adapters/sql.ex @@ -434,8 +434,8 @@ defmodule Ecto.Adapters.SQL do Adapter | Supported opts ---------------- | -------------- - Postgrex | `analyze`, `verbose`, `costs`, `settings`, `buffers`, `timing`, `summary`, `format`, `plan` - MyXQL | `format` + Postgrex | `analyze`, `verbose`, `costs`, `settings`, `buffers`, `timing`, `summary`, `format`, `plan`, `wrap_in_transaction` + MyXQL | `format`, `wrap_in_transaction` All options except `format` are boolean valued and default to `false`. @@ -447,6 +447,10 @@ defmodule Ecto.Adapters.SQL do * Postgrex: `:map`, `:yaml` and `:text` * MyXQL: `:map` and `:text` + The `wrap_in_transaction` option is a boolean that controls whether the command is run inside of a + transaction that is rolled back. This is useful when, for example, you'd like to use `analyze: true` + on an update or delete query without modifying data. Defaults to `true`. + The `:plan` option in Postgrex can take the values `:custom` or `:fallback_generic`. When `:custom` is specified, the explain plan generated will consider the specific values of the query parameters that are supplied. When using `:fallback_generic`, the specific values of the query parameters will @@ -508,10 +512,11 @@ defmodule Ecto.Adapters.SQL do def explain(repo, operation, queryable, opts \\ []) def explain(repo, operation, queryable, opts) when is_atom(repo) or is_pid(repo) do - explain(Ecto.Adapter.lookup_meta(repo), operation, queryable, opts) + wrap_in_transaction? = Keyword.get(opts, :wrap_in_transaction, true) + explain(Ecto.Adapter.lookup_meta(repo), operation, queryable, wrap_in_transaction?, opts) end - def explain(%{repo: repo} = adapter_meta, operation, queryable, opts) do + def explain(%{repo: repo} = adapter_meta, operation, queryable, true, opts) do Ecto.Multi.new() |> Ecto.Multi.run(:explain, fn _, _ -> {prepared, prepared_params} = to_sql(operation, repo, queryable) @@ -528,6 +533,11 @@ defmodule Ecto.Adapters.SQL do end end + def explain(%{repo: repo} = adapter_meta, operation, queryable, false, opts) do + {prepared, prepared_params} = to_sql(operation, repo, queryable) + sql_call(adapter_meta, :explain_query, [prepared], prepared_params, opts) + end + @doc @disconnect_all_doc @spec disconnect_all( pid | Ecto.Repo.t() | Ecto.Adapter.adapter_meta(),