Skip to content

Commit aa600c9

Browse files
authored
Explain Improvements (#1670)
* 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)
1 parent bae4b22 commit aa600c9

File tree

4 files changed

+59
-23
lines changed

4 files changed

+59
-23
lines changed

src/Controllers/QueriesController.php

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,21 @@ public function explain(Request $request)
2121
}
2222

2323
try {
24-
$data = match ($request->json('mode')) {
25-
'visual' => (new Explain())->generateVisualExplain($request->json('connection'), $request->json('query'), $request->json('bindings'), $request->json('hash')),
26-
default => (new Explain())->generateRawExplain($request->json('connection'), $request->json('query'), $request->json('bindings'), $request->json('hash')),
27-
};
24+
$explain = new Explain();
25+
26+
if ($request->json('mode') === 'visual') {
27+
return response()->json([
28+
'success' => true,
29+
'data' => $explain->generateVisualExplain($request->json('connection'), $request->json('query'), $request->json('bindings'), $request->json('hash')),
30+
]);
31+
}
2832

2933
return response()->json([
3034
'success' => true,
31-
'data' => $data,
35+
'data' => $explain->generateRawExplain($request->json('connection'), $request->json('query'), $request->json('bindings'), $request->json('hash')),
36+
'visual' => $explain->isVisualExplainSupported($request->json('connection')) ? [
37+
'confirm' => $explain->confirmVisualExplain($request->json('connection')),
38+
] : null,
3239
]);
3340
} catch (Exception $e) {
3441
return response()->json([

src/DataCollector/QueryCollector.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ public function collect()
500500
}
501501

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

@@ -523,7 +523,6 @@ public function collect()
523523
'connection' => $connectionName,
524524
'explain' => $this->explainQuery && $canExplainQuery ? [
525525
'url' => route('debugbar.queries.explain'),
526-
'visual-confirm' => (new Explain())->confirm($query['connection']),
527526
'driver' => $query['driver'],
528527
'connection' => $query['connection'],
529528
'query' => $query['query'],

src/Resources/queries/widget.js

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
return isCopied;
4242
},
4343

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

63-
$element.append([$table, this.explainVisual(statement)]);
63+
$element.append($table);
64+
if (visual) {
65+
$element.append(this.explainVisual(statement, visual.confirm));
66+
}
6467
},
6568

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

7073
for (const row of rows) {
71-
$ul.append($li.clone().append(row));
74+
$ul.append($li.clone().html($('<span/>').text(row).text().replaceAll(' ', '&nbsp;')));
7275
}
7376

74-
$element.append([$ul, this.explainVisual(statement)]);
77+
$element.append([$ul, this.explainVisual(statement, visual.confirm)]);
7578
},
7679

77-
explainVisual: function (statement) {
80+
explainVisual: function (statement, confirmMessage) {
7881
const $explainLink = $('<a href="#" target="_blank" rel="noopener"/>')
7982
.addClass(csscls('visual-link'));
8083
const $explainButton = $('<a>Visual Explain</a>')
8184
.addClass(csscls('visual-explain'))
8285
.on('click', () => {
83-
if (confirm(statement.explain['visual-confirm'])) {
86+
if (confirm(confirmMessage)) {
8487
fetch(statement.explain.url, {
8588
method: "POST",
8689
body: JSON.stringify({
@@ -309,7 +312,7 @@
309312
if (statement.backtrace && !$.isEmptyObject(statement.backtrace)) {
310313
$details.append(this.renderDetailBacktrace('Backtrace', 'list-ul', statement.backtrace));
311314
}
312-
if (statement.explain && statement.explain.driver === 'mysql') {
315+
if (statement.explain && ['mariadb', 'mysql'].includes(statement.explain.driver)) {
313316
$details.append(this.renderDetailExplain('Performance', 'tachometer', statement, this.explainMysql.bind(this)));
314317
}
315318
if (statement.explain && statement.explain.driver === 'pgsql') {
@@ -399,7 +402,7 @@
399402
response.json()
400403
.then((json) => {
401404
$detail.find(`.${csscls('value')}`).children().remove();
402-
explainFn($detail.find(`.${csscls('value')}`), statement, json.data);
405+
explainFn($detail.find(`.${csscls('value')}`), statement, json.data, json.visual);
403406
})
404407
.catch((err) => alert(`Response body could not be parsed. (${err})`));
405408
} else {

src/Support/Explain.php

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,39 @@
22

33
namespace Barryvdh\Debugbar\Support;
44

5-
use DB;
65
use Exception;
76
use Illuminate\Database\ConnectionInterface;
7+
use Illuminate\Database\QueryException;
8+
use Illuminate\Support\Facades\DB;
89
use Illuminate\Support\Facades\Http;
910

1011
class Explain
1112
{
12-
public function confirm(string $connection): ?string
13+
public function isVisualExplainSupported(string $connection): bool
14+
{
15+
$driver = DB::connection($connection)->getDriverName();
16+
if ($driver === 'pgsql') {
17+
return true;
18+
}
19+
if ($driver === 'mysql') {
20+
// Laravel 11 added a new MariaDB database driver but older Laravel versions handle MySQL and MariaDB with
21+
// the same driver - and even with new versions you can use the MySQL driver while connection to a MariaDB
22+
// database. This query uses a feature implemented only in MariaDB to differentiate them.
23+
try {
24+
DB::connection($connection)->select('SELECT * FROM seq_1_to_1');
25+
26+
return false;
27+
} catch (QueryException) {
28+
// This exception is expected when using MySQL as sequence tables are only available with MariaDB. So
29+
// the exception gets silenced as the check for MySQL has succeeded.
30+
return true;
31+
}
32+
}
33+
34+
return false;
35+
}
36+
37+
public function confirmVisualExplain(string $connection): ?string
1338
{
1439
return match (DB::connection($connection)->getDriverName()) {
1540
'mysql' => 'The query and EXPLAIN output is sent to mysqlexplain.com. Do you want to continue?',
@@ -23,7 +48,7 @@ public function hash(string $connection, string $sql, array $bindings): string
2348
$bindings = json_encode($bindings);
2449

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

4469
return match ($driver = $connection->getDriverName()) {
45-
'mysql' => $connection->select("EXPLAIN {$sql}", $bindings),
70+
'mariadb', 'mysql' => $connection->select("EXPLAIN {$sql}", $bindings),
4671
'pgsql' => array_column($connection->select("EXPLAIN {$sql}", $bindings), 'QUERY PLAN'),
4772
default => throw new Exception("Visual explain not available for driver '{$driver}'."),
4873
};
@@ -51,13 +76,15 @@ public function generateRawExplain(string $connection, string $sql, array $bindi
5176
public function generateVisualExplain(string $connection, string $sql, array $bindings, string $hash): string
5277
{
5378
$this->verify($connection, $sql, $bindings, $hash);
79+
if (!$this->isVisualExplainSupported($connection)) {
80+
throw new Exception('Visual explain not available for this connection.');
81+
}
5482

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

57-
return match ($driver = $connection->getDriverName()) {
85+
return match ($connection->getDriverName()) {
5886
'mysql' => $this->generateVisualExplainMysql($connection, $sql, $bindings),
5987
'pgsql' => $this->generateVisualExplainPgsql($connection, $sql, $bindings),
60-
default => throw new Exception("Visual explain not available for driver '{$driver}'."),
6188
};
6289
}
6390

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

0 commit comments

Comments
 (0)