Skip to content

Commit

Permalink
Merge pull request SimpleMachines#8174 from live627/qq
Browse files Browse the repository at this point in the history
Improve query log display
  • Loading branch information
Sesquipedalian committed Apr 26, 2024
2 parents f64fc14 + e2ada30 commit 49b1ec4
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 59 deletions.
78 changes: 37 additions & 41 deletions Sources/Actions/ViewQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

use SMF\Config;
use SMF\Db\DatabaseApi as Db;
use SMF\DebugUtils;
use SMF\ErrorHandler;
use SMF\IntegrationHook;
use SMF\Lang;
Expand Down Expand Up @@ -76,7 +77,7 @@ public function execute(): void

IntegrationHook::call('integrate_egg_nog');

$query_id = isset($_REQUEST['qq']) ? (int) $_REQUEST['qq'] - 1 : -1;
$query_id = (int) ($_REQUEST['qq'] ?? 0);

echo '<!DOCTYPE html>
<html', Utils::$context['right_to_left'] ? ' dir="rtl"' : '', '>
Expand All @@ -103,56 +104,42 @@ public function execute(): void

foreach ($_SESSION['debug'] as $q => $query_data) {
// Fix the indentation....
$query_data['q'] = ltrim(str_replace("\r", '', $query_data['q']), "\n");
$query = explode("\n", $query_data['q']);
$min_indent = 0;

foreach ($query as $line) {
preg_match('/^(\t*)/', $line, $temp);

if (strlen($temp[0]) < $min_indent || $min_indent == 0) {
$min_indent = strlen($temp[0]);
}
}

foreach ($query as $l => $dummy) {
$query[$l] = substr($dummy, $min_indent);
}

$query_data['q'] = implode("\n", $query);
$query_data['q'] = DebugUtils::trimIndent($query_data['q']);

// Make the filenames look a bit better.
if (isset($query_data['f'])) {
$query_data['f'] = preg_replace('~^' . preg_quote(Config::$boarddir, '~') . '~', '...', $query_data['f']);
$query_data['f'] = preg_replace('/^' . preg_quote(Config::$boarddir, '/') . '/', '...', strtr($query_data['f'], '\\', '/'));
}

$is_select_query = substr(trim($query_data['q']), 0, 6) == 'SELECT' || substr(trim($query_data['q']), 0, 4) == 'WITH';
$is_select_query = preg_match('/^\s*(?:SELECT|WITH)/i', $query_data['q']) != 0;

if ($is_select_query) {
$select = $query_data['q'];
} elseif (preg_match('~^INSERT(?: IGNORE)? INTO \w+(?:\s+\([^)]+\))?\s+(SELECT .+)$~s', trim($query_data['q']), $matches) != 0) {
$is_select_query = true;
$select = $matches[1];
} elseif (preg_match('~^CREATE TEMPORARY TABLE .+?(SELECT .+)$~s', trim($query_data['q']), $matches) != 0) {
} elseif (preg_match('/^\s*(?:INSERT(?: IGNORE)? INTO \w+|CREATE TEMPORARY TABLE .+?)\KSELECT .+$/is', trim($query_data['q']), $matches) != 0) {
$is_select_query = true;
$select = $matches[1];
$select = $matches[0];
}

// Temporary tables created in earlier queries are not explainable.
if ($is_select_query && preg_match('/log_topics_unread|topics_posted_in|tmp_log_search_(?:topics|messages)/i', $select) != 0) {
$is_select_query = false;
}

echo '
<div id="qq', $q, '" style="margin-bottom: 2ex;">';

if ($is_select_query) {
foreach (['log_topics_unread', 'topics_posted_in', 'tmp_log_search_topics', 'tmp_log_search_messages'] as $tmp) {
if (strpos($select, $tmp) !== false) {
$is_select_query = false;
break;
}
}
echo '
<a href="' . Config::$scripturl . '?action=viewquery;qq=' . $q . '#qq' . $q . '" style="font-weight: bold; text-decoration: none;">';
}

echo '
<div id="qq', $q, '" style="margin-bottom: 2ex;">
<a', $is_select_query ? ' href="' . Config::$scripturl . '?action=viewquery;qq=' . ($q + 1) . '#qq' . $q . '"' : '', ' style="font-weight: bold; text-decoration: none;">
', nl2br(str_replace("\t", '&nbsp;&nbsp;&nbsp;', Utils::htmlspecialchars($query_data['q']))), '
</a><br>';
<pre style="tab-size: 2;">', DebugUtils::highlightSql($query_data['q']), '</pre>';

if ($is_select_query) {
echo '
</a>';
}

if (!empty($query_data['f']) && !empty($query_data['l'])) {
echo Lang::getTxt('debug_query_in_line', ['file' => $query_data['f'], 'line' => $query_data['l']]);
Expand All @@ -169,12 +156,7 @@ public function execute(): void

// Explain the query.
if ($query_id == $q && $is_select_query) {
$result = Db::$db->query(
'',
'EXPLAIN ' . (Db::$db->title === POSTGRE_TITLE ? 'ANALYZE ' : '') . $select,
[
],
);
$result = Db::$db->query('', 'EXPLAIN ' . $select);

if ($result === false) {
echo '
Expand Down Expand Up @@ -209,6 +191,20 @@ public function execute(): void

echo '
</table>';

$vendor = Db::$db->get_vendor();

if ($vendor == 'MariaDB') {
$result = Db::$db->query('', 'ANALYZE FORMAT=JSON ' . $select);
} else {
$result = Db::$db->query(
'',
'EXPLAIN ' . ($vendor == 'PostgreSQL' ? '(ANALYZE, FORMAT JSON) ' : 'ANALYZE FORMAT=JSON ') . $select,
);
}

echo '
<pre>' . DebugUtils::highlightJson(Db::$db->fetch_row($result)[0]) . '</pre>';
}
}

Expand Down
106 changes: 106 additions & 0 deletions Sources/DebugUtils.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2024 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 3.0 Alpha 1
*/

declare(strict_types=1);

namespace SMF;/**
* Contains functions that aid in debugging and are generally
* useful to developers.
*/

class DebugUtils
{
/***********************
* Public static methods
***********************/

/**
* Trims excess indentation off a string.
*
* Example: If both the first and the third lines are indeented
* thrice but the second one has four indents, the returned string
* will have three less indents, where only the second line has any
* indentation left.
*
* Ignores lines with no leading whitrespace.
*
* @param string $string Query with indentation to remove.
* @return string Query without excess indentation.
*/
public static function trimIndent(string $string): string
{
preg_match_all('/^[ \t]+(?=\S)/m', $string, $matches);
$min_indent = PHP_INT_MAX;

foreach ($matches[0] as $match) {
$min_indent = min($min_indent, strlen($match));
}

if ($min_indent != PHP_INT_MAX) {
$string = preg_replace('/^[ \t]{' . $min_indent . '}/m', '', $string);
}

return $string;
}

/**
* Highlights a well-formecd JSON string as HTML.
*
* @param string $string Well-formed JSON.
* @return string Highlighted JSON.
*/
public static function highlightJson(string $string): string
{
$colors = [
'STRING' => '#567A0D',
'NUMBER' => '#015493',
'NULL' => '#B75301',
'KEY' => '#803378',
'COMMENT' => '#666F78',
];

return preg_replace_callback(
'/"[^"]+"(?(?=\s*:)(*MARK:KEY)|(*MARK:STRING))|\b(?:true|false|null)\b(*MARK:NULL)|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?(*MARK:NUMBER)/',
fn (array $matches): string => '<span style=\'color:' . $colors[$matches['MARK']] . '\'>' . $matches[0] . '</span>',
str_replace(['<', '>', '&'], ['&lt;', '&gt;', '&amp;'], $string),
) ?? $string;
}

/**
* Highlights a SQL string as HTML.
*
* @param string $string SQL.
* @return string Highlighted SQL.
*/
public static function highlightSql(string $string): string
{
$keyword_regex = '(?>HAVING|GROUP(?> BY|)|MATCH|JOIN|KEY(?>S|)|PR(?>OCEDURE|AGMA|I(?>MARY(?> KEY|)|NT))|A(?>UTO_INCREMENT|DD(?> CONSTRAINT|)|L(?>TER(?> (?>COLUMN|TABLE)|)|L)|N(?>[DY])|S(?>C|))|B(?>ACKUP DATABASE|INARY|LOB|E(?>TWEEN|GIN)|Y)|C(?>URRENT_(?>DATE|TIME)|REATE(?> (?>OR REPLACE VIEW|UNIQUE INDEX|PROCEDURE|DATABASE|INDEX|TABLE|VIEW)|)|AS(?>CADE|E)|H(?>ECK|AR)|O(?>NSTRAINT|LUMN))|D(?>ISTINCT|ROP(?> (?>INDEX|TABLE|VIEW|CO(?>NSTRAINT|LUMN)|D(?>ATABASE|EFAULT))|)|AT(?>ABASE|ETIME)|E(?>CIMAL|FAULT|LETE|SC))|E(?>ACH|LSE(?>IF|)|N(?>GINE|D)|X(?>ISTS|EC))|F(?>ULL OUTER JOIN|ALSE|ROM|OR(?>EIGN KEY|))|I(?>F(?>NULL|)|N(?>NER JOIN|SERT(?> INTO(?> SELECT|)|)|DEX(?>_LIST|)|T(?>E(?>RVAL|GER)|O)|)|S(?> N(?>OT NULL|ULL)|))|L(?>ONGTEXT|E(?>ADING|FT(?> JOIN|))|I(?>MIT|KE))|N(?>ULL|OT(?> NULL|))|O(?>VERLAPS|PTION|UT(?>ER(?> JOIN|)|)|N|R(?>DER(?> BY|)|))|R(?>OWNUM|IGHT(?> JOIN|)|E(?>FERENCES|PLACE))|S(?>HOW|E(?>LECT(?> (?>DISTINCT|INTO|TOP)|)|T))|T(?>ABLE|EXT|HEN|I(?>MESTAMP|NY(?>BLOB|TEXT|INT))|O(?>P|)|R(?>AILING|U(?>NCATE TABLE|E)))|U(?>PDATE|N(?>SIGNED|I(?>QUE|ON(?> ALL|))))|V(?>IEW|A(?>LUES|R(?>BINARY|CHAR)))|W(?>ITH|HE(?>RE|N)))';

$colors = [
'STRING' => '#567A0D',
'NUMBER' => '#015493',
'FUNCTION' => '#015493',
'OPERATOR' => '#B75301',
'KEY' => '#803378',
'COMMENT' => '#666F78',
];

return preg_replace_callback(
'/(["\'])?(?(1)(?:(?!\1).)*+\1(*MARK:STRING)|(?:\b' . $keyword_regex . '\b(*MARK:KEY)|--.*$|\/\*[\s\S]*?\*\/(*MARK:COMMENT)|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?(*MARK:NUMBER)|[!=%\/*-,;:<>](*MARK:OPERATOR)|\w+\((?:[^)(]+|(?R))*\)(*MARK:FUNCTION)))/',
fn (array $matches): string => '<span style=\'color:' . $colors[$matches['MARK']] . '\'>' . $matches[0] . '</span>',
$string,
) ?? $string;
}
}

?>
46 changes: 28 additions & 18 deletions Sources/Logging.php
Original file line number Diff line number Diff line change
Expand Up @@ -933,30 +933,40 @@ public static function displayDebug(): void

if ($_SESSION['view_queries'] == 1 && !empty(Db::$cache)) {
foreach (Db::$cache as $q => $query_data) {
$is_select = strpos(trim($query_data['q']), 'SELECT') === 0 || preg_match('~^INSERT(?: IGNORE)? INTO \w+(?:\s+\([^)]+\))?\s+SELECT .+$~s', trim($query_data['q'])) != 0 || strpos(trim($query_data['q']), 'WITH') === 0;
// Fix the indentation....
$query_data['q'] = DebugUtils::trimIndent($query_data['q']);

// Temporary tables created in earlier queries are not explainable.
if ($is_select) {
foreach (['log_topics_unread', 'topics_posted_in', 'tmp_log_search_topics', 'tmp_log_search_messages'] as $tmp) {
if (strpos(trim($query_data['q']), $tmp) !== false) {
$is_select = false;
break;
}
}
// Make the filenames look a bit better.
if (isset($query_data['f'])) {
$query_data['f'] = preg_replace('/^' . preg_quote(Config::$boarddir, '/') . '/', '...', strtr($query_data['f'], '\\', '/'));
}
// But actual creation of the temporary tables are.
elseif (preg_match('~^CREATE TEMPORARY TABLE .+?SELECT .+$~s', trim($query_data['q'])) != 0) {
$is_select = true;

$is_select_query = preg_match('/^\s*(?:SELECT|WITH)/i', $query_data['q']) != 0;

if ($is_select_query) {
$select = $query_data['q'];
} elseif (preg_match('/^\s*(?:INSERT(?: IGNORE)? INTO \w+|CREATE TEMPORARY TABLE .+?)\KSELECT .+$/is', trim($query_data['q']), $matches) != 0) {
$is_select_query = true;
$select = $matches[0];
}

// Make the filenames look a bit better.
if (isset($query_data['f'])) {
$query_data['f'] = preg_replace('~^' . preg_quote(Config::$boarddir, '~') . '~', '...', $query_data['f']);
// Temporary tables created in earlier queries are not explainable.
if ($is_select_query && preg_match('/log_topics_unread|topics_posted_in|tmp_log_search_(?:topics|messages)/i', $select) != 0) {
$is_select_query = false;
}

echo '
<strong>', $is_select ? '<a href="' . Config::$scripturl . '?action=viewquery;qq=' . ($q + 1) . '#qq' . $q . '" target="_blank" rel="noopener" style="text-decoration: none;">' : '', nl2br(str_replace("\t", '&nbsp;&nbsp;&nbsp;', Utils::htmlspecialchars(ltrim($query_data['q'], "\n\r")))) . ($is_select ? '</a></strong>' : '</strong>') . '<br>
&nbsp;&nbsp;&nbsp;';
if ($is_select_query) {
echo '
<a href="' . Config::$scripturl . '?action=viewquery;qq=' . $q . '#qq' . $q . '" target="_blank" rel="noopener" target="_blank" rel="noopener" style="font-weight: bold; text-decoration: none;">';
}

echo '
<pre style="tab-size: 2;">', $query_data['q'], '</pre>';

if ($is_select_query) {
echo '
</a>';
}

if (!empty($query_data['f']) && !empty($query_data['l'])) {
echo Lang::getTxt('debug_query_in_line', ['file' => $query_data['f'], 'line' => $query_data['l']]);
Expand Down

0 comments on commit 49b1ec4

Please sign in to comment.