refactor(database): rename DB to Database and extract debug functionality (#1964)

- Rename DB → Database, DbFactory → DatabaseFactory for consistency
- Extract 8 debug methods from Database to dedicated DatabaseDebugger class
- Add DebugSelection wrapper for debug-enabled Nette Selection
- Update all references across codebase (common.php, Dev.php, page_footer)
- Maintain backward compatibility via magic methods (__get, __isset)
- Update documentation (README.md, UPGRADE_GUIDE.md)

No breaking changes - all existing DB() calls work unchanged
This commit is contained in:
Yury Pikhtarev 2025-06-18 17:46:12 +04:00 committed by GitHub
commit 6c0219d53c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 851 additions and 289 deletions

View file

@ -117,8 +117,10 @@ The following legacy files have been removed from the codebase:
- `src/Legacy/Dbs.php` - Original database factory - `src/Legacy/Dbs.php` - Original database factory
These were completely replaced by: These were completely replaced by:
- `src/Database/DB.php` - Modern database class with Nette Database - `src/Database/Database.php` - Modern database class with Nette Database (renamed from `DB.php`)
- `src/Database/DbFactory.php` - Modern factory with backward compatibility - `src/Database/DatabaseFactory.php` - Modern factory with backward compatibility (renamed from `DbFactory.php`)
- `src/Database/DatabaseDebugger.php` - Dedicated debug functionality extracted from Database class
- `src/Database/DebugSelection.php` - Debug-enabled wrapper for Nette Database Selection
### Verification ### Verification

View file

@ -141,17 +141,17 @@ define('FULL_URL', $server_protocol . config()->get('server_name') . $server_por
unset($server_protocol, $server_port); unset($server_protocol, $server_port);
// Initialize the new DB factory with database configuration // Initialize the new DB factory with database configuration
TorrentPier\Database\DbFactory::init(config()->get('db'), config()->get('db_alias', [])); TorrentPier\Database\DatabaseFactory::init(config()->get('db'), config()->get('db_alias', []));
/** /**
* Get the Database instance * Get the Database instance
* *
* @param string $db_alias * @param string $db_alias
* @return \TorrentPier\Database\DB * @return \TorrentPier\Database\Database
*/ */
function DB(string $db_alias = 'db'): \TorrentPier\Database\DB function DB(string $db_alias = 'db'): \TorrentPier\Database\Database
{ {
return TorrentPier\Database\DbFactory::getInstance($db_alias); return TorrentPier\Database\DatabaseFactory::getInstance($db_alias);
} }
// Initialize Unified Cache System // Initialize Unified Cache System

View file

@ -43,7 +43,7 @@ if ($show_dbg_info) {
// Get database statistics from the new system // Get database statistics from the new system
try { try {
$main_db = \TorrentPier\Database\DbFactory::getInstance('db'); $main_db = \TorrentPier\Database\DatabaseFactory::getInstance('db');
$sql_t = $main_db->sql_timetotal; $sql_t = $main_db->sql_timetotal;
$sql_time_txt = ($sql_t) ? sprintf('%.3f ' . $lang['SEC'] . ' (%d%%) · ', $sql_t, round($sql_t * 100 / $gen_time)) : ''; $sql_time_txt = ($sql_t) ? sprintf('%.3f ' . $lang['SEC'] . ' (%d%%) · ', $sql_t, round($sql_t * 100 / $gen_time)) : '';
$num_q = $main_db->num_queries; $num_q = $main_db->num_queries;

View file

@ -64,11 +64,11 @@ if (!defined('BB_ROOT')) {
<?php <?php
if (!empty($_COOKIE['explain'])) { if (!empty($_COOKIE['explain'])) {
// Get all database server instances from the new DbFactory // Get all database server instances from the new DatabaseFactory
$server_names = \TorrentPier\Database\DbFactory::getServerNames(); $server_names = \TorrentPier\Database\DatabaseFactory::getServerNames();
foreach ($server_names as $srv_name) { foreach ($server_names as $srv_name) {
try { try {
$db_obj = \TorrentPier\Database\DbFactory::getInstance($srv_name); $db_obj = \TorrentPier\Database\DatabaseFactory::getInstance($srv_name);
if (!empty($db_obj->do_explain)) { if (!empty($db_obj->do_explain)) {
$db_obj->explain('display'); $db_obj->explain('display');
} }

View file

@ -32,7 +32,7 @@ if (!$topic_id) {
} }
// Getting topic data if present // Getting topic data if present
if (!$t_data = DB()->fetch_row("SELECT * FROM " . BB_TOPICS . " WHERE topic_id = $topic_id LIMIT 1")) { if (!$t_data = DB()->table(BB_TOPICS)->where('topic_id', $topic_id)->fetch()?->toArray()) {
bb_die($lang['INVALID_TOPIC_ID_DB']); bb_die($lang['INVALID_TOPIC_ID_DB']);
} }
@ -80,20 +80,26 @@ switch ($mode) {
bb_die($lang['ALREADY_VOTED']); bb_die($lang['ALREADY_VOTED']);
} }
DB()->query(" $affected_rows = DB()->table(BB_POLL_VOTES)
UPDATE " . BB_POLL_VOTES . " SET ->where('topic_id', $topic_id)
vote_result = vote_result + 1 ->where('vote_id', $vote_id)
WHERE topic_id = $topic_id ->update(['vote_result' => new \Nette\Database\SqlLiteral('vote_result + 1')]);
AND vote_id = $vote_id
LIMIT 1
");
if (DB()->affected_rows() != 1) { if ($affected_rows != 1) {
bb_die($lang['NO_VOTE_OPTION']); bb_die($lang['NO_VOTE_OPTION']);
} }
// Voting process // Voting process
DB()->query("INSERT IGNORE INTO " . BB_POLL_USERS . " (topic_id, user_id, vote_ip, vote_dt) VALUES ($topic_id, {$userdata['user_id']}, '" . USER_IP . "', " . TIMENOW . ")"); try {
DB()->table(BB_POLL_USERS)->insert([
'topic_id' => $topic_id,
'user_id' => $userdata['user_id'],
'vote_ip' => USER_IP,
'vote_dt' => TIMENOW
]);
} catch (\Nette\Database\UniqueConstraintViolationException $e) {
// Ignore duplicate entry (equivalent to INSERT IGNORE)
}
CACHE('bb_poll_data')->rm("poll_$topic_id"); CACHE('bb_poll_data')->rm("poll_$topic_id");
bb_die($lang['VOTE_CAST']); bb_die($lang['VOTE_CAST']);
break; break;
@ -104,7 +110,9 @@ switch ($mode) {
} }
// Starting the poll // Starting the poll
DB()->query("UPDATE " . BB_TOPICS . " SET topic_vote = 1 WHERE topic_id = $topic_id"); DB()->table(BB_TOPICS)
->where('topic_id', $topic_id)
->update(['topic_vote' => 1]);
bb_die($lang['NEW_POLL_START']); bb_die($lang['NEW_POLL_START']);
break; break;
case 'poll_finish': case 'poll_finish':
@ -114,7 +122,9 @@ switch ($mode) {
} }
// Finishing the poll // Finishing the poll
DB()->query("UPDATE " . BB_TOPICS . " SET topic_vote = " . POLL_FINISHED . " WHERE topic_id = $topic_id"); DB()->table(BB_TOPICS)
->where('topic_id', $topic_id)
->update(['topic_vote' => POLL_FINISHED]);
bb_die($lang['NEW_POLL_END']); bb_die($lang['NEW_POLL_END']);
break; break;
case 'poll_delete': case 'poll_delete':

View file

@ -10,23 +10,30 @@
namespace TorrentPier\Database; namespace TorrentPier\Database;
use Nette\Database\Connection; use Nette\Database\Connection;
use Nette\Database\Explorer;
use Nette\Database\ResultSet; use Nette\Database\ResultSet;
use Nette\Database\Row; use Nette\Database\Row;
use Nette\Database\Table\Selection;
use Nette\Database\Structure;
use Nette\Database\Conventions\DiscoveredConventions;
use TorrentPier\Database\DebugSelection;
use TorrentPier\Database\DatabaseDebugger;
use TorrentPier\Dev; use TorrentPier\Dev;
use TorrentPier\Legacy\SqlDb; use TorrentPier\Legacy\SqlDb;
/** /**
* Modern DB class using Nette Database with backward compatibility * Modern Database class using Nette Database with backward compatibility
* Implements singleton pattern while maintaining all existing SqlDb methods * Implements singleton pattern while maintaining all existing SqlDb methods
*/ */
class DB class Database
{ {
private static ?DB $instance = null; private static ?Database $instance = null;
private static array $instances = []; private static array $instances = [];
private ?Connection $connection = null; public ?Connection $connection = null;
private ?Explorer $explorer = null;
private ?ResultSet $result = null; private ?ResultSet $result = null;
private int $last_affected_rows = 0; public ?DatabaseDebugger $debugger = null;
// Configuration // Configuration
public array $cfg = []; public array $cfg = [];
@ -42,21 +49,13 @@ class DB
// Statistics and debugging // Statistics and debugging
public int $num_queries = 0; public int $num_queries = 0;
private int $last_affected_rows = 0;
public float $sql_starttime = 0; public float $sql_starttime = 0;
public float $sql_inittime = 0; public float $sql_inittime = 0;
public float $sql_timetotal = 0; public float $sql_timetotal = 0;
public float $cur_query_time = 0; public float $cur_query_time = 0;
public float $slow_time = 0;
public array $dbg = [];
public int $dbg_id = 0;
public bool $dbg_enabled = false;
public ?string $cur_query = null; public ?string $cur_query = null;
public bool $do_explain = false;
public string $explain_hold = '';
public string $explain_out = '';
public array $shutdown = []; public array $shutdown = [];
public array $DBS = []; public array $DBS = [];
@ -69,9 +68,9 @@ class DB
$this->cfg = array_combine($this->cfg_keys, $cfg_values); $this->cfg = array_combine($this->cfg_keys, $cfg_values);
$this->db_server = $server_name; $this->db_server = $server_name;
$this->dbg_enabled = (dev()->checkSqlDebugAllowed() || !empty($_COOKIE['explain']));
$this->do_explain = ($this->dbg_enabled && !empty($_COOKIE['explain'])); // Initialize debugger
$this->slow_time = defined('SQL_SLOW_QUERY_TIME') ? SQL_SLOW_QUERY_TIME : 3; $this->debugger = new DatabaseDebugger($this);
// Initialize our own tracking system (replaces the old $DBS global system) // Initialize our own tracking system (replaces the old $DBS global system)
$this->DBS = [ $this->DBS = [
@ -133,8 +132,8 @@ class DB
*/ */
public function connect(): void public function connect(): void
{ {
$this->cur_query = $this->dbg_enabled ? "connect to: {$this->cfg['dbhost']}:{$this->cfg['dbport']}" : 'connect'; $this->cur_query = $this->debugger->dbg_enabled ? "connect to: {$this->cfg['dbhost']}:{$this->cfg['dbport']}" : 'connect';
$this->debug('start'); $this->debugger->debug('start');
// Build DSN // Build DSN
$dsn = "mysql:host={$this->cfg['dbhost']};port={$this->cfg['dbport']};dbname={$this->cfg['dbname']}"; $dsn = "mysql:host={$this->cfg['dbhost']};port={$this->cfg['dbport']};dbname={$this->cfg['dbname']}";
@ -149,11 +148,20 @@ class DB
$this->cfg['dbpasswd'] $this->cfg['dbpasswd']
); );
// Create Nette Database Explorer with all required dependencies
$storage = $this->getExistingCacheStorage();
$this->explorer = new Explorer(
$this->connection,
new Structure($this->connection, $storage),
new DiscoveredConventions(new Structure($this->connection, $storage)),
$storage
);
$this->selected_db = $this->cfg['dbname']; $this->selected_db = $this->cfg['dbname'];
register_shutdown_function([$this, 'close']); register_shutdown_function([$this, 'close']);
$this->debug('stop'); $this->debugger->debug('stop');
$this->cur_query = null; $this->cur_query = null;
} }
@ -170,22 +178,22 @@ class DB
$query = $this->build_sql($query); $query = $this->build_sql($query);
} }
$query = '/* ' . $this->debug_find_source() . ' */ ' . $query; $query = '/* ' . $this->debugger->debug_find_source() . ' */ ' . $query;
$this->cur_query = $query; $this->cur_query = $query;
$this->debug('start'); $this->debugger->debug('start');
try { try {
$this->result = $this->connection->query($query); $this->result = $this->connection->query($query);
// Initialize affected rows to 0 (most queries don't affect rows) // Initialize affected rows to 0 (most queries don't affect rows)
$this->last_affected_rows = 0; $this->last_affected_rows = 0;
} catch (\Exception $e) { } catch (\Exception $e) {
$this->log_error(); $this->debugger->log_error();
$this->result = null; $this->result = null;
$this->last_affected_rows = 0; $this->last_affected_rows = 0;
} }
$this->debug('stop'); $this->debugger->debug('stop');
$this->cur_query = null; $this->cur_query = null;
if ($this->inited) { if ($this->inited) {
@ -326,6 +334,42 @@ class DB
} }
} }
/**
* Get Database Explorer table access with debug logging
*/
public function table(string $table): DebugSelection
{
if (!$this->explorer) {
$this->init();
}
$selection = $this->explorer->table($table);
// Wrap the selection to capture queries for debug logging
return new DebugSelection($selection, $this);
}
/**
* Get existing cache storage from TorrentPier's unified cache system
*
* @return \Nette\Caching\Storage
*/
private function getExistingCacheStorage(): \Nette\Caching\Storage
{
// Try to use the existing cache system if available
if (function_exists('CACHE')) {
try {
$cacheManager = CACHE('database_structure');
return $cacheManager->getStorage();
} catch (\Exception $e) {
// Fall back to DevNullStorage if cache system is not available yet
}
}
// Fallback to a simple DevNullStorage if cache system is not available
return new \Nette\Caching\Storages\DevNullStorage();
}
/** /**
* Escape data used in sql query (using Nette Database) * Escape data used in sql query (using Nette Database)
*/ */
@ -688,90 +732,25 @@ class DB
} }
/** /**
* Set slow query marker * Set slow query marker (delegated to debugger)
*/ */
public function expect_slow_query(int $ignoring_time = 60, int $new_priority = 10): void public function expect_slow_query(int $ignoring_time = 60, int $new_priority = 10): void
{ {
if (function_exists('CACHE')) { $this->debugger->expect_slow_query($ignoring_time, $new_priority);
$cache = CACHE('bb_cache');
if ($old_priority = $cache->get('dont_log_slow_query')) {
if ($old_priority > $new_priority) {
return;
}
}
if (!defined('IN_FIRST_SLOW_QUERY')) {
define('IN_FIRST_SLOW_QUERY', true);
}
$cache->set('dont_log_slow_query', $new_priority, $ignoring_time);
}
} }
/** /**
* Store debug info * Store debug info (delegated to debugger)
*/ */
public function debug(string $mode): void public function debug(string $mode): void
{ {
if (!defined('SQL_DEBUG') || !SQL_DEBUG) { $this->debugger->debug($mode);
return;
}
$id =& $this->dbg_id;
$dbg =& $this->dbg[$id];
if ($mode === 'start') {
if (defined('SQL_CALC_QUERY_TIME') && SQL_CALC_QUERY_TIME || defined('SQL_LOG_SLOW_QUERIES') && SQL_LOG_SLOW_QUERIES) {
$this->sql_starttime = microtime(true);
}
if ($this->dbg_enabled) {
$dbg['sql'] = preg_replace('#^(\s*)(/\*)(.*)(\*/)(\s*)#', '', $this->cur_query);
$dbg['src'] = $this->debug_find_source();
$dbg['file'] = $this->debug_find_source('file');
$dbg['line'] = $this->debug_find_source('line');
$dbg['time'] = '';
$dbg['info'] = '';
$dbg['mem_before'] = function_exists('sys') ? sys('mem') : 0;
}
if ($this->do_explain) {
$this->explain('start');
}
} elseif ($mode === 'stop') {
if (defined('SQL_CALC_QUERY_TIME') && SQL_CALC_QUERY_TIME || defined('SQL_LOG_SLOW_QUERIES') && SQL_LOG_SLOW_QUERIES) {
$this->cur_query_time = microtime(true) - $this->sql_starttime;
$this->sql_timetotal += $this->cur_query_time;
$this->DBS['sql_timetotal'] += $this->cur_query_time;
if (defined('SQL_LOG_SLOW_QUERIES') && SQL_LOG_SLOW_QUERIES && $this->cur_query_time > $this->slow_time) {
$this->log_slow_query();
}
}
if ($this->dbg_enabled) {
$dbg['time'] = microtime(true) - $this->sql_starttime;
$dbg['info'] = $this->query_info();
$dbg['mem_after'] = function_exists('sys') ? sys('mem') : 0;
$id++;
}
if ($this->do_explain) {
$this->explain('stop');
}
// Check for logging
if ($this->DBS['log_counter'] && $this->inited) {
$this->log_query($this->DBS['log_file']);
$this->DBS['log_counter']--;
}
}
} }
/** /**
* Trigger database error * Trigger database error
*/ */
public function trigger_error(string $msg = 'DB Error'): void public function trigger_error(string $msg = 'Database Error'): void
{ {
$error = $this->sql_error(); $error = $this->sql_error();
$error_msg = "$msg: " . $error['message']; $error_msg = "$msg: " . $error['message'];
@ -784,188 +763,99 @@ class DB
} }
/** /**
* Find source of database call * Find source of database call (delegated to debugger)
*/ */
public function debug_find_source(string $mode = 'all'): string public function debug_find_source(string $mode = 'all'): string
{ {
if (!defined('SQL_PREPEND_SRC') || !SQL_PREPEND_SRC) { return $this->debugger->debug_find_source($mode);
return 'src disabled';
}
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
// Find first non-DB call
foreach ($trace as $frame) {
if (isset($frame['file']) && !str_contains($frame['file'], 'Database/DB.php')) {
switch ($mode) {
case 'file':
return $frame['file'];
case 'line':
return (string)($frame['line'] ?? '?');
case 'all':
default:
$file = function_exists('hide_bb_path') ? hide_bb_path($frame['file']) : basename($frame['file']);
$line = $frame['line'] ?? '?';
return "$file($line)";
}
}
}
return 'src not found';
} }
/** /**
* Prepare for logging * Prepare for logging (delegated to debugger)
*/ */
public function log_next_query(int $queries_count = 1, string $log_file = 'sql_queries'): void public function log_next_query(int $queries_count = 1, string $log_file = 'sql_queries'): void
{ {
$this->DBS['log_file'] = $log_file; $this->debugger->log_next_query($queries_count, $log_file);
$this->DBS['log_counter'] = $queries_count;
} }
/** /**
* Log query * Log query (delegated to debugger)
*/ */
public function log_query(string $log_file = 'sql_queries'): void public function log_query(string $log_file = 'sql_queries'): void
{ {
if (!function_exists('bb_log') || !function_exists('dev')) { $this->debugger->log_query($log_file);
return;
}
$q_time = ($this->cur_query_time >= 10) ? round($this->cur_query_time, 0) : sprintf('%.3f', $this->cur_query_time);
$msg = [];
$msg[] = round($this->sql_starttime);
$msg[] = date('m-d H:i:s', (int)$this->sql_starttime);
$msg[] = sprintf('%-6s', $q_time);
$msg[] = sprintf('%05d', getmypid());
$msg[] = $this->db_server;
$msg[] = dev()->formatShortQuery($this->cur_query);
$msg = implode(defined('LOG_SEPR') ? LOG_SEPR : ' | ', $msg);
$msg .= ($info = $this->query_info()) ? ' # ' . $info : '';
$msg .= ' # ' . $this->debug_find_source() . ' ';
$msg .= defined('IN_CRON') ? 'cron' : basename($_SERVER['REQUEST_URI'] ?? '');
bb_log($msg . (defined('LOG_LF') ? LOG_LF : "\n"), $log_file);
} }
/** /**
* Log slow query * Log slow query (delegated to debugger)
*/ */
public function log_slow_query(string $log_file = 'sql_slow_bb'): void public function log_slow_query(string $log_file = 'sql_slow_bb'): void
{ {
if (!defined('IN_FIRST_SLOW_QUERY') && function_exists('CACHE')) { $this->debugger->log_slow_query($log_file);
$cache = CACHE('bb_cache');
if ($cache && $cache->get('dont_log_slow_query')) {
return;
}
}
$this->log_query($log_file);
} }
/** /**
* Log error * Log error (delegated to debugger)
*/ */
public function log_error(): void public function log_error(): void
{ {
$error = $this->sql_error(); $this->debugger->log_error();
error_log("DB Error: " . $error['message'] . " Query: " . $this->cur_query);
} }
/** /**
* Explain queries - maintains compatibility with legacy SqlDb * Explain queries (delegated to debugger)
*/ */
public function explain($mode, $html_table = '', array $row = []): mixed public function explain($mode, $html_table = '', array $row = []): mixed
{ {
if (!$this->do_explain) { return $this->debugger->explain($mode, $html_table, $row);
return false; }
/**
* Magic method to provide backward compatibility for debug properties
*/
public function __get(string $name): mixed
{
// Delegate debug-related properties to the debugger
switch ($name) {
case 'dbg':
return $this->debugger->dbg ?? [];
case 'dbg_id':
return $this->debugger->dbg_id ?? 0;
case 'dbg_enabled':
return $this->debugger->dbg_enabled ?? false;
case 'do_explain':
return $this->debugger->do_explain ?? false;
case 'explain_hold':
return $this->debugger->explain_hold ?? '';
case 'explain_out':
return $this->debugger->explain_out ?? '';
case 'slow_time':
return $this->debugger->slow_time ?? 3.0;
case 'sql_timetotal':
return $this->sql_timetotal;
default:
throw new \InvalidArgumentException("Property '$name' does not exist");
} }
}
$query = $this->cur_query ?? ''; /**
// Remove comments * Magic method to check if debug properties exist
$query = preg_replace('#(\s*)(/\*)(.*)(\*/)(\s*)#', '', $query); */
public function __isset(string $name): bool
switch ($mode) { {
case 'start': switch ($name) {
$this->explain_hold = ''; case 'dbg':
case 'dbg_id':
if (preg_match('#UPDATE ([a-z0-9_]+).*?WHERE(.*)/#', $query, $m)) { case 'dbg_enabled':
$query = "SELECT * FROM $m[1] WHERE $m[2]"; case 'do_explain':
} elseif (preg_match('#DELETE FROM ([a-z0-9_]+).*?WHERE(.*)#s', $query, $m)) { case 'explain_hold':
$query = "SELECT * FROM $m[1] WHERE $m[2]"; case 'explain_out':
} case 'slow_time':
case 'sql_timetotal':
if (str_starts_with($query, "SELECT")) { return true;
$html_table = false; default:
return false;
try {
$result = $this->connection->query("EXPLAIN $query");
while ($row = $result->fetch()) {
$rowArray = (array)$row;
$html_table = $this->explain('add_explain_row', $html_table, $rowArray);
}
} catch (\Exception $e) {
// Skip if explain fails
}
if ($html_table) {
$this->explain_hold .= '</table>';
}
}
break;
case 'stop':
if (!$this->explain_hold) {
break;
}
$id = $this->dbg_id - 1;
$htid = 'expl-' . spl_object_hash($this->connection) . '-' . $id;
$dbg = $this->dbg[$id] ?? [];
// Ensure required keys exist with defaults
$dbg = array_merge([
'time' => $this->cur_query_time ?? 0,
'sql' => $this->cur_query ?? '',
'query' => $this->cur_query ?? '',
'src' => $this->debug_find_source(),
'trace' => $this->debug_find_source() // Backup for compatibility
], $dbg);
$this->explain_out .= '
<table width="98%" cellpadding="0" cellspacing="0" class="bodyline row2 bCenter" style="border-bottom: 0;">
<tr>
<th style="height: 22px;" align="left">&nbsp;' . ($dbg['src'] ?? $dbg['trace']) . '&nbsp; [' . sprintf('%.3f', $dbg['time']) . ' s]&nbsp; <i>' . $this->query_info() . '</i></th>
<th class="copyElement" data-clipboard-target="#' . $htid . '" style="height: 22px;" align="right" title="Copy to clipboard">' . "[$this->engine] $this->db_server.$this->selected_db" . ' :: Query #' . ($this->num_queries + 1) . '&nbsp;</th>
</tr>
<tr><td colspan="2">' . $this->explain_hold . '</td></tr>
</table>
<div class="sqlLog"><div id="' . $htid . '" class="sqlLogRow sqlExplain" style="padding: 0;">' . (function_exists('dev') ? dev()->formatShortQuery($dbg['sql'] ?? $dbg['query'], true) : htmlspecialchars($dbg['sql'] ?? $dbg['query'])) . '&nbsp;&nbsp;</div></div>
<br />';
break;
case 'add_explain_row':
if (!$html_table && $row) {
$html_table = true;
$this->explain_hold .= '<table width="100%" cellpadding="3" cellspacing="1" class="bodyline" style="border-width: 0;"><tr>';
foreach (array_keys($row) as $val) {
$this->explain_hold .= '<td class="row3 gensmall" align="center"><b>' . htmlspecialchars($val) . '</b></td>';
}
$this->explain_hold .= '</tr>';
}
$this->explain_hold .= '<tr>';
foreach (array_values($row) as $i => $val) {
$class = !($i % 2) ? 'row1' : 'row2';
$this->explain_hold .= '<td class="' . $class . ' gen">' . str_replace(["{$this->selected_db}.", ',', ';'], ['', ', ', ';<br />'], htmlspecialchars($val ?? '')) . '</td>';
}
$this->explain_hold .= '</tr>';
return $html_table;
case 'display':
echo '<a name="explain"></a><div class="med">' . $this->explain_out . '</div>';
break;
} }
return false;
} }
/** /**

View file

@ -0,0 +1,349 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier\Database;
/**
* Database Debug functionality extracted from Database class
* Handles all debug logging, timing, and query explanation features
*/
class DatabaseDebugger
{
private Database $db;
// Debug configuration
public bool $dbg_enabled = false;
public bool $do_explain = false;
public float $slow_time = 3.0;
// Timing and statistics
public float $sql_starttime = 0;
public float $cur_query_time = 0;
// Debug storage
public array $dbg = [];
public int $dbg_id = 0;
// Explain functionality
public string $explain_hold = '';
public string $explain_out = '';
public function __construct(Database $db)
{
$this->db = $db;
// Initialize debug settings more safely
$this->initializeDebugSettings();
$this->slow_time = defined('SQL_SLOW_QUERY_TIME') ? SQL_SLOW_QUERY_TIME : 3;
}
/**
* Initialize debug settings exactly like the original Database class
*/
private function initializeDebugSettings(): void
{
// Use the EXACT same logic as the original DB class
$this->dbg_enabled = (dev()->checkSqlDebugAllowed() || !empty($_COOKIE['explain']));
$this->do_explain = ($this->dbg_enabled && !empty($_COOKIE['explain']));
}
/**
* Store debug info
*/
public function debug(string $mode): void
{
$id =& $this->dbg_id;
$dbg =& $this->dbg[$id];
if ($mode === 'start') {
// Always update timing if required constants are defined
if (defined('SQL_CALC_QUERY_TIME') && SQL_CALC_QUERY_TIME || defined('SQL_LOG_SLOW_QUERIES') && SQL_LOG_SLOW_QUERIES) {
$this->sql_starttime = microtime(true);
$this->db->sql_starttime = $this->sql_starttime; // Update main Database object too
}
if ($this->dbg_enabled) {
$dbg['sql'] = preg_replace('#^(\s*)(/\*)(.*)(\*/)(\s*)#', '', $this->db->cur_query);
$dbg['src'] = $this->debug_find_source();
$dbg['file'] = $this->debug_find_source('file');
$dbg['line'] = $this->debug_find_source('line');
$dbg['time'] = '';
$dbg['info'] = '';
$dbg['mem_before'] = function_exists('sys') ? sys('mem') : 0;
}
if ($this->do_explain) {
$this->explain('start');
}
} elseif ($mode === 'stop') {
if (defined('SQL_CALC_QUERY_TIME') && SQL_CALC_QUERY_TIME || defined('SQL_LOG_SLOW_QUERIES') && SQL_LOG_SLOW_QUERIES) {
$this->cur_query_time = microtime(true) - $this->sql_starttime;
$this->db->sql_timetotal += $this->cur_query_time;
$this->db->DBS['sql_timetotal'] += $this->cur_query_time;
if (defined('SQL_LOG_SLOW_QUERIES') && SQL_LOG_SLOW_QUERIES && $this->cur_query_time > $this->slow_time) {
$this->log_slow_query();
}
}
if ($this->dbg_enabled) {
$dbg['time'] = $this->cur_query_time > 0 ? $this->cur_query_time : (microtime(true) - $this->sql_starttime);
$dbg['info'] = $this->db->query_info();
$dbg['mem_after'] = function_exists('sys') ? sys('mem') : 0;
$id++;
}
if ($this->do_explain) {
$this->explain('stop');
}
// Check for logging
if ($this->db->DBS['log_counter'] && $this->db->inited) {
$this->log_query($this->db->DBS['log_file']);
$this->db->DBS['log_counter']--;
}
}
// Update timing in main Database object
$this->db->cur_query_time = $this->cur_query_time;
}
/**
* Find source of database call
*/
public function debug_find_source(string $mode = 'all'): string
{
if (!defined('SQL_PREPEND_SRC') || !SQL_PREPEND_SRC) {
return 'src disabled';
}
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
// Find first non-DB call (skip Database.php, DebugSelection.php, and DatabaseDebugger.php)
foreach ($trace as $frame) {
if (isset($frame['file']) &&
!str_contains($frame['file'], 'Database/Database.php') &&
!str_contains($frame['file'], 'Database/DebugSelection.php') &&
!str_contains($frame['file'], 'Database/DatabaseDebugger.php')) {
switch ($mode) {
case 'file':
return $frame['file'];
case 'line':
return (string)($frame['line'] ?? '?');
case 'all':
default:
$file = function_exists('hide_bb_path') ? hide_bb_path($frame['file']) : basename($frame['file']);
$line = $frame['line'] ?? '?';
return "$file($line)";
}
}
}
return 'src not found';
}
/**
* Prepare for logging
*/
public function log_next_query(int $queries_count = 1, string $log_file = 'sql_queries'): void
{
$this->db->DBS['log_file'] = $log_file;
$this->db->DBS['log_counter'] = $queries_count;
}
/**
* Log query
*/
public function log_query(string $log_file = 'sql_queries'): void
{
if (!function_exists('bb_log') || !function_exists('dev')) {
return;
}
$q_time = ($this->cur_query_time >= 10) ? round($this->cur_query_time, 0) : sprintf('%.3f', $this->cur_query_time);
$msg = [];
$msg[] = round($this->sql_starttime);
$msg[] = date('m-d H:i:s', (int)$this->sql_starttime);
$msg[] = sprintf('%-6s', $q_time);
$msg[] = sprintf('%05d', getmypid());
$msg[] = $this->db->db_server;
$msg[] = function_exists('dev') ? dev()->formatShortQuery($this->db->cur_query) : $this->db->cur_query;
$msg = implode(defined('LOG_SEPR') ? LOG_SEPR : ' | ', $msg);
$msg .= ($info = $this->db->query_info()) ? ' # ' . $info : '';
$msg .= ' # ' . $this->debug_find_source() . ' ';
$msg .= defined('IN_CRON') ? 'cron' : basename($_SERVER['REQUEST_URI'] ?? '');
bb_log($msg . (defined('LOG_LF') ? LOG_LF : "\n"), $log_file);
}
/**
* Log slow query
*/
public function log_slow_query(string $log_file = 'sql_slow_bb'): void
{
if (!defined('IN_FIRST_SLOW_QUERY') && function_exists('CACHE')) {
$cache = CACHE('bb_cache');
if ($cache && $cache->get('dont_log_slow_query')) {
return;
}
}
$this->log_query($log_file);
}
/**
* Log error
*/
public function log_error(): void
{
$error = $this->db->sql_error();
error_log("Database Error: " . $error['message'] . " Query: " . $this->db->cur_query);
}
/**
* Set slow query marker
*/
public function expect_slow_query(int $ignoring_time = 60, int $new_priority = 10): void
{
if (function_exists('CACHE')) {
$cache = CACHE('bb_cache');
if ($old_priority = $cache->get('dont_log_slow_query')) {
if ($old_priority > $new_priority) {
return;
}
}
if (!defined('IN_FIRST_SLOW_QUERY')) {
define('IN_FIRST_SLOW_QUERY', true);
}
$cache->set('dont_log_slow_query', $new_priority, $ignoring_time);
}
}
/**
* Explain queries - maintains compatibility with legacy SqlDb
*/
public function explain($mode, $html_table = '', array $row = []): mixed
{
if (!$this->do_explain) {
return false;
}
$query = $this->db->cur_query ?? '';
// Remove comments
$query = preg_replace('#(\s*)(/\*)(.*)(\*/)(\s*)#', '', $query);
switch ($mode) {
case 'start':
$this->explain_hold = '';
if (preg_match('#UPDATE ([a-z0-9_]+).*?WHERE(.*)/#', $query, $m)) {
$query = "SELECT * FROM $m[1] WHERE $m[2]";
} elseif (preg_match('#DELETE FROM ([a-z0-9_]+).*?WHERE(.*)#s', $query, $m)) {
$query = "SELECT * FROM $m[1] WHERE $m[2]";
}
if (str_starts_with($query, "SELECT")) {
$html_table = false;
try {
$result = $this->db->connection->query("EXPLAIN $query");
while ($row = $result->fetch()) {
$rowArray = (array)$row;
$html_table = $this->explain('add_explain_row', $html_table, $rowArray);
}
} catch (\Exception $e) {
// Skip if explain fails
}
if ($html_table) {
$this->explain_hold .= '</table>';
}
}
break;
case 'stop':
if (!$this->explain_hold) {
break;
}
$id = $this->dbg_id - 1;
$htid = 'expl-' . spl_object_hash($this->db->connection) . '-' . $id;
$dbg = $this->dbg[$id] ?? [];
// Ensure required keys exist with defaults
$dbg = array_merge([
'time' => $this->cur_query_time ?? 0,
'sql' => $this->db->cur_query ?? '',
'query' => $this->db->cur_query ?? '',
'src' => $this->debug_find_source(),
'trace' => $this->debug_find_source() // Backup for compatibility
], $dbg);
$this->explain_out .= '
<table width="98%" cellpadding="0" cellspacing="0" class="bodyline row2 bCenter" style="border-bottom: 0;">
<tr>
<th style="height: 22px;" align="left">&nbsp;' . ($dbg['src'] ?? $dbg['trace']) . '&nbsp; [' . sprintf('%.3f', $dbg['time']) . ' s]&nbsp; <i>' . $this->db->query_info() . '</i></th>
<th class="copyElement" data-clipboard-target="#' . $htid . '" style="height: 22px;" align="right" title="Copy to clipboard">' . "[{$this->db->engine}] {$this->db->db_server}.{$this->db->selected_db}" . ' :: Query #' . ($this->db->num_queries + 1) . '&nbsp;</th>
</tr>
<tr><td colspan="2">' . $this->explain_hold . '</td></tr>
</table>
<div class="sqlLog"><div id="' . $htid . '" class="sqlLogRow sqlExplain" style="padding: 0;">' . (function_exists('dev') ? dev()->formatShortQuery($dbg['sql'] ?? $dbg['query'], true) : htmlspecialchars($dbg['sql'] ?? $dbg['query'])) . '&nbsp;&nbsp;</div></div>
<br />';
break;
case 'add_explain_row':
if (!$html_table && $row) {
$html_table = true;
$this->explain_hold .= '<table width="100%" cellpadding="3" cellspacing="1" class="bodyline" style="border-width: 0;"><tr>';
foreach (array_keys($row) as $val) {
$this->explain_hold .= '<td class="row3 gensmall" align="center"><b>' . htmlspecialchars($val) . '</b></td>';
}
$this->explain_hold .= '</tr>';
}
$this->explain_hold .= '<tr>';
foreach (array_values($row) as $i => $val) {
$class = !($i % 2) ? 'row1' : 'row2';
$this->explain_hold .= '<td class="' . $class . ' gen">' . str_replace(["{$this->db->selected_db}.", ',', ';'], ['', ', ', ';<br />'], htmlspecialchars($val ?? '')) . '</td>';
}
$this->explain_hold .= '</tr>';
return $html_table;
case 'display':
echo '<a name="explain"></a><div class="med">' . $this->explain_out . '</div>';
break;
}
return false;
}
/**
* Get debug statistics for display
*/
public function getDebugStats(): array
{
return [
'num_queries' => count($this->dbg),
'sql_timetotal' => $this->db->sql_timetotal,
'queries' => $this->dbg,
'explain_out' => $this->explain_out
];
}
/**
* Clear debug data
*/
public function clearDebugData(): void
{
$this->dbg = [];
$this->dbg_id = 0;
$this->explain_hold = '';
$this->explain_out = '';
}
}

View file

@ -15,7 +15,7 @@ namespace TorrentPier\Database;
* This factory completely replaces the legacy SqlDb/Dbs system with the new * This factory completely replaces the legacy SqlDb/Dbs system with the new
* Nette Database implementation while maintaining full backward compatibility. * Nette Database implementation while maintaining full backward compatibility.
*/ */
class DbFactory class DatabaseFactory
{ {
private static array $instances = []; private static array $instances = [];
private static array $server_configs = []; private static array $server_configs = [];
@ -33,7 +33,7 @@ class DbFactory
/** /**
* Get database instance (maintains compatibility with existing DB() calls) * Get database instance (maintains compatibility with existing DB() calls)
*/ */
public static function getInstance(string $srv_name_or_alias = 'db'): DB public static function getInstance(string $srv_name_or_alias = 'db'): Database
{ {
$srv_name = self::resolveSrvName($srv_name_or_alias); $srv_name = self::resolveSrvName($srv_name_or_alias);
@ -44,7 +44,7 @@ class DbFactory
throw new \RuntimeException("Database configuration not found for server: $srv_name"); throw new \RuntimeException("Database configuration not found for server: $srv_name");
} }
self::$instances[$srv_name] = DB::getInstance($cfg_values, $srv_name); self::$instances[$srv_name] = Database::getInstance($cfg_values, $srv_name);
} }
return self::$instances[$srv_name]; return self::$instances[$srv_name];
@ -96,6 +96,6 @@ class DbFactory
} }
} }
self::$instances = []; self::$instances = [];
DB::destroyInstances(); Database::destroyInstances();
} }
} }

View file

@ -0,0 +1,292 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier\Database;
use Nette\Database\Table\Selection;
use ReflectionClass;
use Exception;
/**
* DebugSelection - Wraps Nette Database Selection to provide debug logging and explain functionality
*/
class DebugSelection
{
private Selection $selection;
private Database $db;
public function __construct(Selection $selection, Database $db)
{
$this->selection = $selection;
$this->db = $db;
}
/**
* Magic method to delegate calls to the wrapped Selection
*/
public function __call(string $name, array $arguments)
{
$result = call_user_func_array([$this->selection, $name], $arguments);
// If result is another Selection, wrap it too
if ($result instanceof Selection) {
return new self($result, $this->db);
}
return $result;
}
/**
* Magic method to delegate property access
*/
public function __get(string $name)
{
return $this->selection->$name;
}
/**
* Magic method to delegate property setting
*/
public function __set(string $name, $value): void
{
$this->selection->$name = $value;
}
/**
* Magic method to check if property is set
*/
public function __isset(string $name): bool
{
return isset($this->selection->$name);
}
/**
* Log query execution for debug panel
*/
private function logQuery(string $method, array $arguments): void
{
if (!defined('SQL_DEBUG') || !SQL_DEBUG) {
return;
}
// Use the actual SQL with substituted parameters for both logging and EXPLAIN
$sql = $this->generateSqlForLogging($method, $arguments, false);
// Set the query for debug logging
$this->db->cur_query = $sql;
$this->db->debug('start');
}
/**
* Complete query logging after execution
*/
private function completeQueryLogging(): void
{
if (!defined('SQL_DEBUG') || !SQL_DEBUG) {
return;
}
// Note: explain('stop') is automatically called by debug('stop') when do_explain is true
$this->db->debug('stop');
}
/**
* Generate SQL representation for logging and EXPLAIN
*/
private function generateSqlForLogging(string $method, array $arguments, bool $useRawSQL = true): string
{
// For SELECT operations, try to get the SQL from Nette
if (in_array($method, ['fetch', 'fetchAll', 'count'], true)) {
$sql = $useRawSQL ? $this->getSqlFromSelection() : $this->getSqlFromSelection(true);
// Modify the SQL based on the method
switch ($method) {
case 'fetch':
// If it doesn't already have LIMIT, add it
if (!preg_match('/LIMIT\s+\d+/i', $sql)) {
$sql .= ' LIMIT 1';
}
return $sql;
case 'count':
// Replace SELECT * with SELECT COUNT(*)
return preg_replace('/^SELECT\s+\*/i', 'SELECT COUNT(*)', $sql);
case 'fetchAll':
default:
return $sql;
}
}
// For INSERT/UPDATE/DELETE, generate appropriate SQL
$tableName = $this->selection->getName();
return match ($method) {
'insert' => $this->generateInsertSql($tableName, $arguments),
'update' => $this->generateUpdateSql($tableName, $arguments, $useRawSQL),
'delete' => $this->generateDeleteSql($tableName, $useRawSQL),
default => "-- Explorer method: {$method} on {$tableName}"
};
}
/**
* Generate INSERT SQL statement
*/
private function generateInsertSql(string $tableName, array $arguments): string
{
if (!isset($arguments[0]) || !is_array($arguments[0])) {
return "INSERT INTO {$tableName} (...) VALUES (...)";
}
$data = $arguments[0];
$columns = implode(', ', array_keys($data));
$values = implode(', ', array_map(
static fn($v) => is_string($v) ? "'$v'" : $v,
array_values($data)
));
return "INSERT INTO {$tableName} ({$columns}) VALUES ({$values})";
}
/**
* Generate UPDATE SQL statement
*/
private function generateUpdateSql(string $tableName, array $arguments, bool $useRawSQL): string
{
$setPairs = [];
if (isset($arguments[0]) && is_array($arguments[0])) {
foreach ($arguments[0] as $key => $value) {
$setPairs[] = "{$key} = " . (is_string($value) ? "'$value'" : $value);
}
}
$setClause = !empty($setPairs) ? implode(', ', $setPairs) : '...';
$sql = $this->getSqlFromSelection(!$useRawSQL);
// Extract WHERE clause from the SQL
if (preg_match('/WHERE\s+(.+?)(?:\s+ORDER\s+BY|\s+LIMIT|\s+GROUP\s+BY|$)/i', $sql, $matches)) {
return "UPDATE {$tableName} SET {$setClause} WHERE " . trim($matches[1]);
}
return "UPDATE {$tableName} SET {$setClause}";
}
/**
* Generate DELETE SQL statement
*/
private function generateDeleteSql(string $tableName, bool $useRawSQL): string
{
$sql = $this->getSqlFromSelection(!$useRawSQL);
// Extract WHERE clause from the SQL
if (preg_match('/WHERE\s+(.+?)(?:\s+ORDER\s+BY|\s+LIMIT|\s+GROUP\s+BY|$)/i', $sql, $matches)) {
return "DELETE FROM {$tableName} WHERE " . trim($matches[1]);
}
return "DELETE FROM {$tableName}";
}
/**
* Get SQL from Nette Selection with optional parameter substitution
*/
private function getSqlFromSelection(bool $replaceParameters = false): string
{
try {
$reflectionClass = new ReflectionClass($this->selection);
$sql = '';
// Try getSql() method first
if ($reflectionClass->hasMethod('getSql')) {
$getSqlMethod = $reflectionClass->getMethod('getSql');
$getSqlMethod->setAccessible(true);
$sql = $getSqlMethod->invoke($this->selection);
} else {
// Try __toString() method as fallback
$sql = (string)$this->selection;
}
// For EXPLAIN to work, we need to replace ? with actual values
if ($replaceParameters) {
$sql = preg_replace('/\?/', '1', $sql);
}
return $sql;
} catch (Exception $e) {
// Fall back to simple representation
return "SELECT * FROM " . $this->selection->getName() . " WHERE 1=1";
}
}
// Delegate common Selection methods with logging
public function where(...$args): self
{
return new self($this->selection->where(...$args), $this->db);
}
public function order(...$args): self
{
return new self($this->selection->order(...$args), $this->db);
}
public function select(...$args): self
{
return new self($this->selection->select(...$args), $this->db);
}
public function limit(...$args): self
{
return new self($this->selection->limit(...$args), $this->db);
}
public function fetch()
{
$this->logQuery('fetch', []);
$result = $this->selection->fetch();
$this->completeQueryLogging();
return $result;
}
public function fetchAll(): array
{
$this->logQuery('fetchAll', []);
$result = $this->selection->fetchAll();
$this->completeQueryLogging();
return $result;
}
public function insert($data)
{
$this->logQuery('insert', [$data]);
$result = $this->selection->insert($data);
$this->completeQueryLogging();
return $result;
}
public function update($data): int
{
$this->logQuery('update', [$data]);
$result = $this->selection->update($data);
$this->completeQueryLogging();
return $result;
}
public function delete(): int
{
$this->logQuery('delete', []);
$result = $this->selection->delete();
$this->completeQueryLogging();
return $result;
}
public function count(): int
{
$this->logQuery('count', []);
$result = $this->selection->count();
$this->completeQueryLogging();
return $result;
}
}

View file

@ -15,17 +15,21 @@ The new database system has completely replaced the legacy SqlDb/Dbs system and
### Classes ### Classes
1. **`DB`** - Main singleton database class using Nette Database Connection 1. **`Database`** - Main singleton database class using Nette Database Connection
2. **`DbFactory`** - Factory that has completely replaced the legacy SqlDb/Dbs system 2. **`DatabaseFactory`** - Factory that has completely replaced the legacy SqlDb/Dbs system
3. **`DatabaseDebugger`** - Dedicated debug functionality extracted from Database class
4. **`DebugSelection`** - Debug-enabled wrapper for Nette Database Selection
### Key Features ### Key Features
- **Singleton Pattern**: Ensures single database connection per server configuration - **Singleton Pattern**: Ensures single database connection per server configuration
- **Multiple Database Support**: Handles multiple database servers via DbFactory - **Multiple Database Support**: Handles multiple database servers via DatabaseFactory
- **Raw SQL Support**: Uses Nette Database's Connection class (SQL way) for minimal code impact - **Raw SQL Support**: Uses Nette Database's Connection class (SQL way) for minimal code impact
- **Complete Error Handling**: Maintains existing error handling behavior - **Complete Error Handling**: Maintains existing error handling behavior
- **Full Debug Support**: Preserves all debugging, logging, and explain functionality - **Full Debug Support**: Preserves all debugging, logging, and explain functionality
- **Performance Tracking**: Query timing and slow query detection - **Performance Tracking**: Query timing and slow query detection
- **Clean Architecture**: Debug functionality extracted to dedicated DatabaseDebugger class
- **Modular Design**: Single responsibility principle with separate debug and database concerns
## Implementation Status ## Implementation Status
@ -34,6 +38,8 @@ The new database system has completely replaced the legacy SqlDb/Dbs system and
- ✅ **Debug System**: Full explain(), logging, and performance tracking - ✅ **Debug System**: Full explain(), logging, and performance tracking
- ✅ **Error Handling**: Complete error handling with sql_error() support - ✅ **Error Handling**: Complete error handling with sql_error() support
- ✅ **Connection Management**: Singleton pattern with proper initialization - ✅ **Connection Management**: Singleton pattern with proper initialization
- ✅ **Clean Architecture**: Debug functionality extracted to dedicated classes
- ✅ **Class Renaming**: Renamed DB → Database, DbFactory → DatabaseFactory for consistency
## Usage ## Usage
@ -75,7 +81,7 @@ The database configuration is handled through the existing TorrentPier config sy
```php ```php
// Initialized in common.php // Initialized in common.php
TorrentPier\Database\DbFactory::init( TorrentPier\Database\DatabaseFactory::init(
config()->get('db'), // Database configurations config()->get('db'), // Database configurations
config()->get('db_alias', []) // Database aliases config()->get('db_alias', []) // Database aliases
); );
@ -155,8 +161,10 @@ This is a **complete replacement** that maintains 100% backward compatibility:
## Files ## Files
- `DB.php` - Main database class with full backward compatibility - `Database.php` - Main database class with full backward compatibility
- `DbFactory.php` - Factory for managing database instances - `DatabaseFactory.php` - Factory for managing database instances
- `DatabaseDebugger.php` - Dedicated debug functionality class
- `DebugSelection.php` - Debug-enabled Nette Selection wrapper
- `README.md` - This documentation - `README.md` - This documentation
## Future Enhancement: Gradual Migration to Nette Explorer ## Future Enhancement: Gradual Migration to Nette Explorer

View file

@ -197,10 +197,10 @@ class Dev
$log = ''; $log = '';
// Get debug information from new database system // Get debug information from new database system
$server_names = \TorrentPier\Database\DbFactory::getServerNames(); $server_names = \TorrentPier\Database\DatabaseFactory::getServerNames();
foreach ($server_names as $srv_name) { foreach ($server_names as $srv_name) {
try { try {
$db_obj = \TorrentPier\Database\DbFactory::getInstance($srv_name); $db_obj = \TorrentPier\Database\DatabaseFactory::getInstance($srv_name);
$log .= !empty($db_obj->dbg) ? $this->getSqlLogHtml($db_obj, "database: $srv_name [{$db_obj->engine}]") : ''; $log .= !empty($db_obj->dbg) ? $this->getSqlLogHtml($db_obj, "database: $srv_name [{$db_obj->engine}]") : '';
} catch (\Exception $e) { } catch (\Exception $e) {
// Skip if server not available // Skip if server not available

View file

@ -74,11 +74,14 @@ class Poll
'vote_result' => (int)0, 'vote_result' => (int)0,
]; ];
} }
$sql_args = DB()->build_array('MULTI_INSERT', $sql_ary); // Delete existing poll data first, then insert new data
foreach ($sql_ary as $poll_vote) {
DB()->table(BB_POLL_VOTES)->insert($poll_vote);
}
DB()->query("REPLACE INTO " . BB_POLL_VOTES . $sql_args); DB()->table(BB_TOPICS)
->where('topic_id', $topic_id)
DB()->query("UPDATE " . BB_TOPICS . " SET topic_vote = 1 WHERE topic_id = $topic_id"); ->update(['topic_vote' => 1]);
} }
/** /**
@ -88,7 +91,9 @@ class Poll
*/ */
public function delete_poll($topic_id) public function delete_poll($topic_id)
{ {
DB()->query("UPDATE " . BB_TOPICS . " SET topic_vote = 0 WHERE topic_id = $topic_id"); DB()->table(BB_TOPICS)
->where('topic_id', $topic_id)
->update(['topic_vote' => 0]);
$this->delete_votes_data($topic_id); $this->delete_votes_data($topic_id);
} }
@ -99,8 +104,12 @@ class Poll
*/ */
public function delete_votes_data($topic_id) public function delete_votes_data($topic_id)
{ {
DB()->query("DELETE FROM " . BB_POLL_VOTES . " WHERE topic_id = $topic_id"); DB()->table(BB_POLL_VOTES)
DB()->query("DELETE FROM " . BB_POLL_USERS . " WHERE topic_id = $topic_id"); ->where('topic_id', $topic_id)
->delete();
DB()->table(BB_POLL_USERS)
->where('topic_id', $topic_id)
->delete();
CACHE('bb_poll_data')->rm("poll_$topic_id"); CACHE('bb_poll_data')->rm("poll_$topic_id");
} }
@ -119,12 +128,11 @@ class Poll
$items = []; $items = [];
if (!$poll_data = CACHE('bb_poll_data')->get("poll_$topic_id")) { if (!$poll_data = CACHE('bb_poll_data')->get("poll_$topic_id")) {
$poll_data = DB()->fetch_rowset(" $poll_data = DB()->table(BB_POLL_VOTES)
SELECT topic_id, vote_id, vote_text, vote_result ->select('topic_id, vote_id, vote_text, vote_result')
FROM " . BB_POLL_VOTES . " ->where('topic_id IN (?)', explode(',', $topic_id_csv))
WHERE topic_id IN($topic_id_csv) ->order('topic_id, vote_id')
ORDER BY topic_id, vote_id ->fetchAll();
");
CACHE('bb_poll_data')->set("poll_$topic_id", $poll_data); CACHE('bb_poll_data')->set("poll_$topic_id", $poll_data);
} }
@ -150,7 +158,10 @@ class Poll
*/ */
public static function userIsAlreadyVoted(int $topic_id, int $user_id): bool public static function userIsAlreadyVoted(int $topic_id, int $user_id): bool
{ {
return (bool)DB()->fetch_row("SELECT 1 FROM " . BB_POLL_USERS . " WHERE topic_id = $topic_id AND user_id = $user_id LIMIT 1"); return (bool)DB()->table(BB_POLL_USERS)
->where('topic_id', $topic_id)
->where('user_id', $user_id)
->fetch();
} }
/** /**