From 24bd53f91b80c9a8d1f4d80bf41e2d4315d7acf7 Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Mon, 21 Jul 2025 07:46:56 -0400 Subject: [PATCH 1/4] add rollback option to explain --- integration_test/myxql/explain_test.exs | 14 ++++++++++++++ integration_test/pg/explain_test.exs | 14 ++++++++++++++ lib/ecto/adapters/sql.ex | 18 ++++++++++++++---- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/integration_test/myxql/explain_test.exs b/integration_test/myxql/explain_test.exs index e51c0ad5..0913c12e 100644 --- a/integration_test/myxql/explain_test.exs +++ b/integration_test/myxql/explain_test.exs @@ -42,5 +42,19 @@ defmodule Ecto.Integration.ExplainTest do assert Enum.member?(keys, "select_id") assert Enum.member?(keys, "table") 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, rollback: false, timeout: 20000) + end) + + assert explain =~ "DELETE" + assert explain =~ "p0" + assert TestRepo.all(Post) == [] + end end end diff --git a/integration_test/pg/explain_test.exs b/integration_test/pg/explain_test.exs index cfbee0ba..f2c2e450 100644 --- a/integration_test/pg/explain_test.exs +++ b/integration_test/pg/explain_test.exs @@ -81,4 +81,18 @@ 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, rollback: false, timeout: 20000) + end) + + assert explain =~ "Delete on posts p0" + assert explain =~ "cost=" + assert TestRepo.all(Post) == [] + end end diff --git a/lib/ecto/adapters/sql.ex b/lib/ecto/adapters/sql.ex index 781009af..2e71c59e 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`, `rollback` + MyXQL | `format`, `rollback` 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 `rollback` 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) + rollback? = Keyword.get(opts, :rollback, true) + explain(Ecto.Adapter.lookup_meta(repo), operation, queryable, rollback?, 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(), From c20e752758435eb83e142beeed779c121d048161 Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Mon, 21 Jul 2025 07:49:03 -0400 Subject: [PATCH 2/4] add tds test --- integration_test/tds/explain_test.exs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/integration_test/tds/explain_test.exs b/integration_test/tds/explain_test.exs index 11285453..ae0f5623 100644 --- a/integration_test/tds/explain_test.exs +++ b/integration_test/tds/explain_test.exs @@ -32,5 +32,19 @@ 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 + TestRepo.insert!(%Post{}) + assert [%Post{}] = TestRepo.all(Post) + + {:ok, {:ok, explain}} = + TestRepo.transaction(fn -> + TestRepo.explain(:delete_all, Post, analyze: true, rollback: false, timeout: 20000) + end) + + assert explain =~ "DELETE" + assert explain =~ "p0" + assert TestRepo.all(Post) == [] + end end end From 4be0d9fee94e3041d991dd1916bf1d87a855223f Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Mon, 21 Jul 2025 07:58:33 -0400 Subject: [PATCH 3/4] mysql and tds don't have analyze --- integration_test/myxql/explain_test.exs | 6 +----- integration_test/tds/explain_test.exs | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/integration_test/myxql/explain_test.exs b/integration_test/myxql/explain_test.exs index 0913c12e..3491b0ce 100644 --- a/integration_test/myxql/explain_test.exs +++ b/integration_test/myxql/explain_test.exs @@ -44,17 +44,13 @@ defmodule Ecto.Integration.ExplainTest do 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, rollback: false, timeout: 20000) + TestRepo.explain(:delete_all, Post, rollback: false, timeout: 20000) end) assert explain =~ "DELETE" assert explain =~ "p0" - assert TestRepo.all(Post) == [] end end end diff --git a/integration_test/tds/explain_test.exs b/integration_test/tds/explain_test.exs index ae0f5623..aef1d5da 100644 --- a/integration_test/tds/explain_test.exs +++ b/integration_test/tds/explain_test.exs @@ -34,17 +34,13 @@ defmodule Ecto.Integration.ExplainTest do 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, rollback: false, timeout: 20000) + TestRepo.explain(:delete_all, Post, rollback: false, timeout: 20000) end) assert explain =~ "DELETE" assert explain =~ "p0" - assert TestRepo.all(Post) == [] end end end From 7aba2e41414b5ec090e642a13628f0e0b9ea411e Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Mon, 21 Jul 2025 08:55:51 -0400 Subject: [PATCH 4/4] wrap_in_transaction --- integration_test/myxql/explain_test.exs | 10 ++++++---- integration_test/pg/explain_test.exs | 6 +++++- integration_test/tds/explain_test.exs | 10 +++++++--- lib/ecto/adapters/sql.ex | 14 +++++++------- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/integration_test/myxql/explain_test.exs b/integration_test/myxql/explain_test.exs index 3491b0ce..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,7 +39,7 @@ 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") @@ -46,7 +48,7 @@ defmodule Ecto.Integration.ExplainTest do test "explain without rolling back" do {:ok, {:ok, explain}} = TestRepo.transaction(fn -> - TestRepo.explain(:delete_all, Post, rollback: false, timeout: 20000) + TestRepo.explain(:delete_all, Post, wrap_in_transaction: false, timeout: 20000) end) assert explain =~ "DELETE" diff --git a/integration_test/pg/explain_test.exs b/integration_test/pg/explain_test.exs index f2c2e450..cb57b9bb 100644 --- a/integration_test/pg/explain_test.exs +++ b/integration_test/pg/explain_test.exs @@ -88,7 +88,11 @@ defmodule Ecto.Integration.ExplainTest do {:ok, {:ok, explain}} = TestRepo.transaction(fn -> - TestRepo.explain(:delete_all, Post, analyze: true, rollback: false, timeout: 20000) + TestRepo.explain(:delete_all, Post, + analyze: true, + wrap_in_transaction: false, + timeout: 20000 + ) end) assert explain =~ "Delete on posts p0" diff --git a/integration_test/tds/explain_test.exs b/integration_test/tds/explain_test.exs index aef1d5da..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" @@ -36,7 +40,7 @@ defmodule Ecto.Integration.ExplainTest do test "explain without rolling back" do {:ok, {:ok, explain}} = TestRepo.transaction(fn -> - TestRepo.explain(:delete_all, Post, rollback: false, timeout: 20000) + TestRepo.explain(:delete_all, Post, wrap_in_transaction: false, timeout: 20000) end) assert explain =~ "DELETE" diff --git a/lib/ecto/adapters/sql.ex b/lib/ecto/adapters/sql.ex index 2e71c59e..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`, `rollback` - MyXQL | `format`, `rollback` + 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,9 +447,9 @@ defmodule Ecto.Adapters.SQL do * Postgrex: `:map`, `:yaml` and `:text` * MyXQL: `:map` and `:text` - The `rollback` 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 `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 @@ -512,8 +512,8 @@ 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 - rollback? = Keyword.get(opts, :rollback, true) - explain(Ecto.Adapter.lookup_meta(repo), operation, queryable, rollback?, 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, true, opts) do