Skip to content

Commit

Permalink
Explain Improvements (#1670)
Browse files Browse the repository at this point in the history
* support mariadb explain

* fix for really old mysql versions (not support by laravel anymore)

* disable visual explain for mariadb

* disable visual explain for mariadb

* indentation whitespace was lost for pgsql explain

* make visual explain check only when running an explain (its not needed before)
  • Loading branch information
tpetry authored Sep 16, 2024
1 parent bae4b22 commit aa600c9
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 23 deletions.
17 changes: 12 additions & 5 deletions src/Controllers/QueriesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,21 @@ public function explain(Request $request)
}

try {
$data = match ($request->json('mode')) {
'visual' => (new Explain())->generateVisualExplain($request->json('connection'), $request->json('query'), $request->json('bindings'), $request->json('hash')),
default => (new Explain())->generateRawExplain($request->json('connection'), $request->json('query'), $request->json('bindings'), $request->json('hash')),
};
$explain = new Explain();

if ($request->json('mode') === 'visual') {
return response()->json([
'success' => true,
'data' => $explain->generateVisualExplain($request->json('connection'), $request->json('query'), $request->json('bindings'), $request->json('hash')),
]);
}

return response()->json([
'success' => true,
'data' => $data,
'data' => $explain->generateRawExplain($request->json('connection'), $request->json('query'), $request->json('bindings'), $request->json('hash')),
'visual' => $explain->isVisualExplainSupported($request->json('connection')) ? [
'confirm' => $explain->confirmVisualExplain($request->json('connection')),
] : null,
]);
} catch (Exception $e) {
return response()->json([
Expand Down
3 changes: 1 addition & 2 deletions src/DataCollector/QueryCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ public function collect()
}

$canExplainQuery = match (true) {
in_array($query['driver'], ['mysql', 'pgsql']) => $query['bindings'] !== null && preg_match('/^\s*(' . implode('|', $this->explainTypes) . ') /i', $query['query']),
in_array($query['driver'], ['mariadb', 'mysql', 'pgsql']) => $query['bindings'] !== null && preg_match('/^\s*(' . implode('|', $this->explainTypes) . ') /i', $query['query']),
default => false,
};

Expand All @@ -523,7 +523,6 @@ public function collect()
'connection' => $connectionName,
'explain' => $this->explainQuery && $canExplainQuery ? [
'url' => route('debugbar.queries.explain'),
'visual-confirm' => (new Explain())->confirm($query['connection']),
'driver' => $query['driver'],
'connection' => $query['connection'],
'query' => $query['query'],
Expand Down
21 changes: 12 additions & 9 deletions src/Resources/queries/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
return isCopied;
},

explainMysql: function ($element, statement, rows) {
explainMysql: function ($element, statement, rows, visual) {
const headings = [];
for (const key in rows[0]) {
headings.push($('<th/>').text(key));
Expand All @@ -60,27 +60,30 @@
$table.find('thead').append($('<tr/>').append(headings));
$table.find('tbody').append(values);

$element.append([$table, this.explainVisual(statement)]);
$element.append($table);
if (visual) {
$element.append(this.explainVisual(statement, visual.confirm));
}
},

explainPgsql: function ($element, statement, rows) {
explainPgsql: function ($element, statement, rows, visual) {
const $ul = $('<ul />').addClass(csscls('table-list'));
const $li = $('<li />').addClass(csscls('table-list-item'));

for (const row of rows) {
$ul.append($li.clone().append(row));
$ul.append($li.clone().html($('<span/>').text(row).text().replaceAll(' ', '&nbsp;')));
}

$element.append([$ul, this.explainVisual(statement)]);
$element.append([$ul, this.explainVisual(statement, visual.confirm)]);
},

explainVisual: function (statement) {
explainVisual: function (statement, confirmMessage) {
const $explainLink = $('<a href="#" target="_blank" rel="noopener"/>')
.addClass(csscls('visual-link'));
const $explainButton = $('<a>Visual Explain</a>')
.addClass(csscls('visual-explain'))
.on('click', () => {
if (confirm(statement.explain['visual-confirm'])) {
if (confirm(confirmMessage)) {
fetch(statement.explain.url, {
method: "POST",
body: JSON.stringify({
Expand Down Expand Up @@ -309,7 +312,7 @@
if (statement.backtrace && !$.isEmptyObject(statement.backtrace)) {
$details.append(this.renderDetailBacktrace('Backtrace', 'list-ul', statement.backtrace));
}
if (statement.explain && statement.explain.driver === 'mysql') {
if (statement.explain && ['mariadb', 'mysql'].includes(statement.explain.driver)) {
$details.append(this.renderDetailExplain('Performance', 'tachometer', statement, this.explainMysql.bind(this)));
}
if (statement.explain && statement.explain.driver === 'pgsql') {
Expand Down Expand Up @@ -399,7 +402,7 @@
response.json()
.then((json) => {
$detail.find(`.${csscls('value')}`).children().remove();
explainFn($detail.find(`.${csscls('value')}`), statement, json.data);
explainFn($detail.find(`.${csscls('value')}`), statement, json.data, json.visual);
})
.catch((err) => alert(`Response body could not be parsed. (${err})`));
} else {
Expand Down
41 changes: 34 additions & 7 deletions src/Support/Explain.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,39 @@

namespace Barryvdh\Debugbar\Support;

use DB;
use Exception;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;

class Explain
{
public function confirm(string $connection): ?string
public function isVisualExplainSupported(string $connection): bool
{
$driver = DB::connection($connection)->getDriverName();
if ($driver === 'pgsql') {
return true;
}
if ($driver === 'mysql') {
// Laravel 11 added a new MariaDB database driver but older Laravel versions handle MySQL and MariaDB with
// the same driver - and even with new versions you can use the MySQL driver while connection to a MariaDB
// database. This query uses a feature implemented only in MariaDB to differentiate them.
try {
DB::connection($connection)->select('SELECT * FROM seq_1_to_1');

return false;
} catch (QueryException) {
// This exception is expected when using MySQL as sequence tables are only available with MariaDB. So
// the exception gets silenced as the check for MySQL has succeeded.
return true;
}
}

return false;
}

public function confirmVisualExplain(string $connection): ?string
{
return match (DB::connection($connection)->getDriverName()) {
'mysql' => 'The query and EXPLAIN output is sent to mysqlexplain.com. Do you want to continue?',
Expand All @@ -23,7 +48,7 @@ public function hash(string $connection, string $sql, array $bindings): string
$bindings = json_encode($bindings);

return match (DB::connection($connection)->getDriverName()) {
'mysql', 'pgsql' => hash_hmac('sha256', "{$connection}::{$sql}::{$bindings}", config('app.key')),
'mariadb', 'mysql', 'pgsql' => hash_hmac('sha256', "{$connection}::{$sql}::{$bindings}", config('app.key')),
default => null,
};
}
Expand All @@ -42,7 +67,7 @@ public function generateRawExplain(string $connection, string $sql, array $bindi
$connection = DB::connection($connection);

return match ($driver = $connection->getDriverName()) {
'mysql' => $connection->select("EXPLAIN {$sql}", $bindings),
'mariadb', 'mysql' => $connection->select("EXPLAIN {$sql}", $bindings),
'pgsql' => array_column($connection->select("EXPLAIN {$sql}", $bindings), 'QUERY PLAN'),
default => throw new Exception("Visual explain not available for driver '{$driver}'."),
};
Expand All @@ -51,13 +76,15 @@ public function generateRawExplain(string $connection, string $sql, array $bindi
public function generateVisualExplain(string $connection, string $sql, array $bindings, string $hash): string
{
$this->verify($connection, $sql, $bindings, $hash);
if (!$this->isVisualExplainSupported($connection)) {
throw new Exception('Visual explain not available for this connection.');
}

$connection = DB::connection($connection);

return match ($driver = $connection->getDriverName()) {
return match ($connection->getDriverName()) {
'mysql' => $this->generateVisualExplainMysql($connection, $sql, $bindings),
'pgsql' => $this->generateVisualExplainPgsql($connection, $sql, $bindings),
default => throw new Exception("Visual explain not available for driver '{$driver}'."),
};
}

Expand All @@ -70,7 +97,7 @@ private function generateVisualExplainMysql(ConnectionInterface $connection, str
'bindings' => $bindings,
'version' => $connection->selectOne("SELECT VERSION()")->{'VERSION()'},
'explain_json' => $connection->selectOne("EXPLAIN FORMAT=JSON {$query}", $bindings)->EXPLAIN,
'explain_tree' => rescue(fn () => $connection->selectOne("EXPLAIN FORMAT=TREE {$query}", $bindings), report: false)->EXPLAIN,
'explain_tree' => rescue(fn () => $connection->selectOne("EXPLAIN FORMAT=TREE {$query}", $bindings)->EXPLAIN, report: false),
])->throw()->json('url');
}

Expand Down

0 comments on commit aa600c9

Please sign in to comment.