Skip to content

Commit

Permalink
Support explain plan for the TDS adapter (#273)
Browse files Browse the repository at this point in the history
  • Loading branch information
leandrocp committed Oct 4, 2020
1 parent babc554 commit 0798542
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 70 deletions.
40 changes: 24 additions & 16 deletions integration_test/myxql/explain_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,34 @@ defmodule Ecto.Integration.ExplainTest do
alias Ecto.Integration.Post
import Ecto.Query, only: [from: 2]

test "explain" do
explain = TestRepo.explain(:all, from(p in Post, where: p.title == "title"), timeout: 20000)
describe "explain" do
test "select" 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 |"
assert explain =~
"| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |"

assert explain =~ "p0"
assert explain =~ "SIMPLE"
assert explain =~ "Using where"
assert explain =~ "p0"
assert explain =~ "SIMPLE"
assert explain =~ "Using where"
end

explain = TestRepo.explain(:delete_all, Post)
assert explain =~ "DELETE"
assert explain =~ "p0"
test "delete" do
explain = TestRepo.explain(:delete_all, Post)
assert explain =~ "DELETE"
assert explain =~ "p0"
end

explain = TestRepo.explain(:update_all, from(p in Post, update: [set: [title: "new title"]]))
assert explain =~ "UPDATE"
assert explain =~ "p0"
test "update" do
explain = TestRepo.explain(:update_all, from(p in Post, update: [set: [title: "new title"]]))
assert explain =~ "UPDATE"
assert explain =~ "p0"
end

assert_raise(MyXQL.Error, fn ->
TestRepo.explain(:all, from(p in "posts", select: p.invalid, where: p.invalid == "title"))
end)
test "invalid" do
assert_raise(MyXQL.Error, fn ->
TestRepo.explain(:all, from(p in "posts", select: p.invalid, where: p.invalid == "title"))
end)
end
end
end
24 changes: 24 additions & 0 deletions integration_test/sql/sql.exs
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,28 @@ defmodule Ecto.Integration.SQLTest do
test "returns false table doesn't exists" do
refute Ecto.Adapters.SQL.table_exists?(TestRepo, "unknown")
end

test "returns result as a formatted table" do
TestRepo.insert_all(Post, [%{title: "my post title", counter: 1, public: nil}])

# resolve correct query for each adapter
query = from(p in Post, select: [p.title, p.counter, p.public])
{query, _} = Ecto.Adapters.SQL.to_sql(:all, TestRepo, query)

table =
query
|> TestRepo.query!()
|> Ecto.Adapters.SQL.format_table()

assert table == "+---------------+---------+--------+\n| title | counter | public |\n+---------------+---------+--------+\n| my post title | 1 | NULL |\n+---------------+---------+--------+"
end

test "format_table edge cases" do
assert Ecto.Adapters.SQL.format_table(nil) == ""
assert Ecto.Adapters.SQL.format_table(%{columns: nil, rows: nil}) == ""
assert Ecto.Adapters.SQL.format_table(%{columns: [], rows: []}) == ""
assert Ecto.Adapters.SQL.format_table(%{columns: [], rows: [["test"]]}) == ""
assert Ecto.Adapters.SQL.format_table(%{columns: ["test"], rows: []}) == "+------+\n| test |\n+------+\n+------+"
assert Ecto.Adapters.SQL.format_table(%{columns: ["test"], rows: nil}) == "+------+\n| test |\n+------+\n+------+"
end
end
32 changes: 28 additions & 4 deletions integration_test/tds/explain_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,34 @@ defmodule Ecto.Integration.ExplainTest do

alias Ecto.Integration.TestRepo
alias Ecto.Integration.Post
import Ecto.Query, only: [from: 2]

test "explain options" do
assert_raise(Tds.Error, "EXPLAIN is not supported by Ecto.Adapters.TDS at the moment", fn ->
TestRepo.explain(:all, Post)
end)
describe "explain" do
test "select" do
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)"
assert explain =~ "explain_test"
end

test "delete" do
explain = TestRepo.explain(:delete_all, Post)
assert explain =~ "DELETE"
assert explain =~ "p0"
end

test "update" do
explain = TestRepo.explain(:update_all, from(p in Post, update: [set: [title: "new title"]]))
assert explain =~ "UPDATE"
assert explain =~ "p0"
assert explain =~ "new title"
end

test "invalid" do
assert_raise(Tds.Error, fn ->
TestRepo.explain(:all, from(p in "posts", select: p.invalid, where: p.invalid == "title"))
end)
end
end
end
50 changes: 4 additions & 46 deletions lib/ecto/adapters/myxql/connection.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
if Code.ensure_loaded?(MyXQL) do
defmodule Ecto.Adapters.MyXQL.Connection do
@moduledoc false
alias Ecto.Adapters.SQL

@behaviour Ecto.Adapters.SQL.Connection

## Connection
Expand Down Expand Up @@ -211,8 +213,8 @@ if Code.ensure_loaded?(MyXQL) do
# See Notes at https://dev.mysql.com/doc/refman/5.7/en/explain.html
def explain_query(conn, query, params, opts) do
case query(conn, build_explain_query(query), params, opts) do
{:ok, %MyXQL.Result{columns: columns, rows: rows}} ->
{:ok, format_result_as_table(columns, rows)}
{:ok, %MyXQL.Result{} = result} ->
{:ok, SQL.format_table(result)}

error ->
error
Expand All @@ -224,50 +226,6 @@ if Code.ensure_loaded?(MyXQL) do
|> IO.iodata_to_binary()
end

defp format_result_as_table(columns, rows) do
column_widths =
[columns | rows]
|> List.zip()
|> Enum.map(&Tuple.to_list/1)
|> Enum.map(fn column_with_rows ->
column_with_rows |> Enum.map(&binary_length/1) |> Enum.max()
end)

[
separator(column_widths),
"\n",
cells(columns, column_widths),
"\n",
separator(column_widths),
"\n",
Enum.map(rows, &cells(&1, column_widths) ++ ["\n"]),
separator(column_widths)
]
|> IO.iodata_to_binary()
end

defp binary_length(nil), do: 4 # NULL
defp binary_length(binary) when is_binary(binary), do: String.length(binary)
defp binary_length(other), do: other |> inspect() |> String.length()

defp separator(widths) do
Enum.map(widths, & [?+, ?-, String.duplicate("-", &1), ?-]) ++ [?+]
end

defp cells(items, widths) do
cell =
[items, widths]
|> List.zip()
|> Enum.map(fn {item, width} -> [?|, " ", format_item(item, width) , " "] end)

[cell | [?|]]
end

defp format_item(nil, width), do: String.pad_trailing("NULL", width)
defp format_item(item, width) when is_binary(item), do: String.pad_trailing(item, width)
defp format_item(item, width) when is_number(item), do: item |> inspect() |> String.pad_leading(width)
defp format_item(item, width), do: item |> inspect() |> String.pad_trailing(width)

## Query generation

binary_ops =
Expand Down
68 changes: 68 additions & 0 deletions lib/ecto/adapters/sql.ex
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,74 @@ defmodule Ecto.Adapters.SQL do
query!(adapter_meta, query, params, []).num_rows != 0
end


@doc """
Returns a formatted table for a given query `result`.
## Examples
iex> Ecto.Adapters.SQL.format_table(query) |> IO.puts()
+---------------+---------+--------+
| title | counter | public |
+---------------+---------+--------+
| My Post Title | 1 | NULL |
+---------------+---------+--------+
"""
@spec format_table(%{columns: [String.t] | nil, rows: [term()] | nil}) :: String.t
def format_table(result)

def format_table(nil), do: ""
def format_table(%{columns: nil}), do: ""
def format_table(%{columns: []}), do: ""
def format_table(%{columns: columns, rows: nil}), do: format_table(%{columns: columns, rows: []})

def format_table(%{columns: columns, rows: rows}) do
column_widths =
[columns | rows]
|> List.zip()
|> Enum.map(&Tuple.to_list/1)
|> Enum.map(fn column_with_rows ->
column_with_rows |> Enum.map(&binary_length/1) |> Enum.max()
end)

[
separator(column_widths),
"\n",
cells(columns, column_widths),
"\n",
separator(column_widths),
"\n",
Enum.map(rows, &cells(&1, column_widths) ++ ["\n"]),
separator(column_widths)
]
|> IO.iodata_to_binary()
end


defp binary_length(nil), do: 4 # NULL
defp binary_length(binary) when is_binary(binary), do: String.length(binary)
defp binary_length(other), do: other |> inspect() |> String.length()

defp separator(widths) do
Enum.map(widths, & [?+, ?-, String.duplicate("-", &1), ?-]) ++ [?+]
end

defp cells(items, widths) do
cell =
[items, widths]
|> List.zip()
|> Enum.map(fn {item, width} -> [?|, " ", format_item(item, width) , " "] end)

[cell | [?|]]
end

defp format_item(nil, width), do: String.pad_trailing("NULL", width)
defp format_item(item, width) when is_binary(item), do: String.pad_trailing(item, width)
defp format_item(item, width) when is_number(item), do: item |> inspect() |> String.pad_leading(width)
defp format_item(item, width), do: item |> inspect() |> String.pad_trailing(width)

## Callbacks

@doc false
Expand Down
25 changes: 23 additions & 2 deletions lib/ecto/adapters/tds/connection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ if Code.ensure_loaded?(Tds) do
require Logger
alias Tds.Query
alias Ecto.Query.Tagged
alias Ecto.Adapters.SQL
require Ecto.Schema

@behaviour Ecto.Adapters.SQL.Connection
Expand Down Expand Up @@ -299,8 +300,28 @@ if Code.ensure_loaded?(Tds) do
end

@impl true
def explain_query(_conn, _query, _params, _opts) do
raise Tds.Error, "EXPLAIN is not supported by Ecto.Adapters.TDS at the moment"
def explain_query(conn, query, params, opts) do
params = prepare_params(params)

case Tds.query_multi(conn, build_explain_query(query), params, opts) do
{:ok, [_, %Tds.Result{} = result, _]} ->
{:ok, SQL.format_table(result)}

error ->
error
end
end

def build_explain_query(query) do
[
"SET STATISTICS XML ON; ",
"SET STATISTICS PROFILE ON; ",
query,
"; ",
"SET STATISTICS XML OFF; ",
"SET STATISTICS PROFILE OFF;"
]
|> IO.iodata_to_binary()
end

## Query generation
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ defmodule EctoSQL.MixProject do
if path = System.get_env("TDS_PATH") do
{:tds, path: path}
else
{:tds, "~> 2.1.0", optional: true}
{:tds, "~> 2.1.1", optional: true}
end
end

Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
"poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"},
"postgrex": {:hex, :postgrex, "0.15.5", "aec40306a622d459b01bff890fa42f1430dac61593b122754144ad9033a2152f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ed90c81e1525f65a2ba2279dbcebf030d6d13328daa2f8088b9661eb9143af7f"},
"tds": {:hex, :tds, "2.1.0", "0b83c184752b0676f1f2f766dc70ad39d3fa7aab74fa70f3e3426eb3c0ff0f54", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "e0d52c1e9e452569c88d7911af69d8d21191224feed65f2d5dea9db6a166c136"},
"tds": {:hex, :tds, "2.1.1", "b6163ea716d74ed90a3a83668db2e7c74c1e722fd3538ef5758e0a084fde8d60", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "6b28e1f06a57867eb6b1a957ae5d872b09214ba771ef08cf5ca9d52d6d372876"},
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
}

0 comments on commit 0798542

Please sign in to comment.