diff --git a/UPGRADE_GUIDE.md b/UPGRADE_GUIDE.md
index e6f3724ae..5f59b3613 100644
--- a/UPGRADE_GUIDE.md
+++ b/UPGRADE_GUIDE.md
@@ -117,8 +117,10 @@ The following legacy files have been removed from the codebase:
- `src/Legacy/Dbs.php` - Original database factory
These were completely replaced by:
-- `src/Database/DB.php` - Modern database class with Nette Database
-- `src/Database/DbFactory.php` - Modern factory with backward compatibility
+- `src/Database/Database.php` - Modern database class with Nette Database (renamed from `DB.php`)
+- `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
diff --git a/common.php b/common.php
index 19fd87981..02f7ea2a5 100644
--- a/common.php
+++ b/common.php
@@ -141,17 +141,17 @@ define('FULL_URL', $server_protocol . config()->get('server_name') . $server_por
unset($server_protocol, $server_port);
// 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
*
* @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
diff --git a/library/includes/page_footer.php b/library/includes/page_footer.php
index c511e9bdb..2f0ddf0b9 100644
--- a/library/includes/page_footer.php
+++ b/library/includes/page_footer.php
@@ -43,7 +43,7 @@ if ($show_dbg_info) {
// Get database statistics from the new system
try {
- $main_db = \TorrentPier\Database\DbFactory::getInstance('db');
+ $main_db = \TorrentPier\Database\DatabaseFactory::getInstance('db');
$sql_t = $main_db->sql_timetotal;
$sql_time_txt = ($sql_t) ? sprintf('%.3f ' . $lang['SEC'] . ' (%d%%) · ', $sql_t, round($sql_t * 100 / $gen_time)) : '';
$num_q = $main_db->num_queries;
diff --git a/library/includes/page_footer_dev.php b/library/includes/page_footer_dev.php
index 08114a497..9528b479a 100644
--- a/library/includes/page_footer_dev.php
+++ b/library/includes/page_footer_dev.php
@@ -64,11 +64,11 @@ if (!defined('BB_ROOT')) {
do_explain)) {
$db_obj->explain('display');
}
diff --git a/poll.php b/poll.php
index 7181b3f4e..156b2aca0 100644
--- a/poll.php
+++ b/poll.php
@@ -32,7 +32,7 @@ if (!$topic_id) {
}
// 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']);
}
@@ -80,20 +80,26 @@ switch ($mode) {
bb_die($lang['ALREADY_VOTED']);
}
- DB()->query("
- UPDATE " . BB_POLL_VOTES . " SET
- vote_result = vote_result + 1
- WHERE topic_id = $topic_id
- AND vote_id = $vote_id
- LIMIT 1
- ");
+ $affected_rows = DB()->table(BB_POLL_VOTES)
+ ->where('topic_id', $topic_id)
+ ->where('vote_id', $vote_id)
+ ->update(['vote_result' => new \Nette\Database\SqlLiteral('vote_result + 1')]);
- if (DB()->affected_rows() != 1) {
+ if ($affected_rows != 1) {
bb_die($lang['NO_VOTE_OPTION']);
}
// 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");
bb_die($lang['VOTE_CAST']);
break;
@@ -104,7 +110,9 @@ switch ($mode) {
}
// 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']);
break;
case 'poll_finish':
@@ -114,7 +122,9 @@ switch ($mode) {
}
// 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']);
break;
case 'poll_delete':
diff --git a/src/Database/DB.php b/src/Database/Database.php
similarity index 65%
rename from src/Database/DB.php
rename to src/Database/Database.php
index 8237e51e3..51db196ac 100644
--- a/src/Database/DB.php
+++ b/src/Database/Database.php
@@ -10,23 +10,30 @@
namespace TorrentPier\Database;
use Nette\Database\Connection;
+use Nette\Database\Explorer;
use Nette\Database\ResultSet;
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\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
*/
-class DB
+class Database
{
- private static ?DB $instance = null;
+ private static ?Database $instance = null;
private static array $instances = [];
- private ?Connection $connection = null;
+ public ?Connection $connection = null;
+ private ?Explorer $explorer = null;
private ?ResultSet $result = null;
- private int $last_affected_rows = 0;
+ public ?DatabaseDebugger $debugger = null;
// Configuration
public array $cfg = [];
@@ -42,21 +49,13 @@ class DB
// Statistics and debugging
public int $num_queries = 0;
+ private int $last_affected_rows = 0;
public float $sql_starttime = 0;
public float $sql_inittime = 0;
public float $sql_timetotal = 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 bool $do_explain = false;
- public string $explain_hold = '';
- public string $explain_out = '';
-
public array $shutdown = [];
public array $DBS = [];
@@ -69,9 +68,9 @@ class DB
$this->cfg = array_combine($this->cfg_keys, $cfg_values);
$this->db_server = $server_name;
- $this->dbg_enabled = (dev()->checkSqlDebugAllowed() || !empty($_COOKIE['explain']));
- $this->do_explain = ($this->dbg_enabled && !empty($_COOKIE['explain']));
- $this->slow_time = defined('SQL_SLOW_QUERY_TIME') ? SQL_SLOW_QUERY_TIME : 3;
+
+ // Initialize debugger
+ $this->debugger = new DatabaseDebugger($this);
// Initialize our own tracking system (replaces the old $DBS global system)
$this->DBS = [
@@ -133,8 +132,8 @@ class DB
*/
public function connect(): void
{
- $this->cur_query = $this->dbg_enabled ? "connect to: {$this->cfg['dbhost']}:{$this->cfg['dbport']}" : 'connect';
- $this->debug('start');
+ $this->cur_query = $this->debugger->dbg_enabled ? "connect to: {$this->cfg['dbhost']}:{$this->cfg['dbport']}" : 'connect';
+ $this->debugger->debug('start');
// Build DSN
$dsn = "mysql:host={$this->cfg['dbhost']};port={$this->cfg['dbport']};dbname={$this->cfg['dbname']}";
@@ -149,11 +148,20 @@ class DB
$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'];
register_shutdown_function([$this, 'close']);
- $this->debug('stop');
+ $this->debugger->debug('stop');
$this->cur_query = null;
}
@@ -170,22 +178,22 @@ class DB
$query = $this->build_sql($query);
}
- $query = '/* ' . $this->debug_find_source() . ' */ ' . $query;
+ $query = '/* ' . $this->debugger->debug_find_source() . ' */ ' . $query;
$this->cur_query = $query;
- $this->debug('start');
+ $this->debugger->debug('start');
- try {
+ try {
$this->result = $this->connection->query($query);
// Initialize affected rows to 0 (most queries don't affect rows)
$this->last_affected_rows = 0;
} catch (\Exception $e) {
- $this->log_error();
+ $this->debugger->log_error();
$this->result = null;
$this->last_affected_rows = 0;
}
- $this->debug('stop');
+ $this->debugger->debug('stop');
$this->cur_query = null;
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)
*/
@@ -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
{
- 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);
- }
+ $this->debugger->expect_slow_query($ignoring_time, $new_priority);
}
/**
- * Store debug info
+ * Store debug info (delegated to debugger)
*/
public function debug(string $mode): void
{
- if (!defined('SQL_DEBUG') || !SQL_DEBUG) {
- 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']--;
- }
- }
+ $this->debugger->debug($mode);
}
/**
* 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_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
{
- if (!defined('SQL_PREPEND_SRC') || !SQL_PREPEND_SRC) {
- 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';
+ return $this->debugger->debug_find_source($mode);
}
/**
- * Prepare for logging
+ * Prepare for logging (delegated to debugger)
*/
public function log_next_query(int $queries_count = 1, string $log_file = 'sql_queries'): void
{
- $this->DBS['log_file'] = $log_file;
- $this->DBS['log_counter'] = $queries_count;
+ $this->debugger->log_next_query($queries_count, $log_file);
}
/**
- * Log query
+ * Log query (delegated to debugger)
*/
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_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);
+ $this->debugger->log_query($log_file);
}
/**
- * Log slow query
+ * Log slow query (delegated to debugger)
*/
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);
+ $this->debugger->log_slow_query($log_file);
}
/**
- * Log error
+ * Log error (delegated to debugger)
*/
public function log_error(): void
{
- $error = $this->sql_error();
- error_log("DB Error: " . $error['message'] . " Query: " . $this->cur_query);
+ $this->debugger->log_error();
}
/**
- * Explain queries - maintains compatibility with legacy SqlDb
+ * Explain queries (delegated to debugger)
*/
public function explain($mode, $html_table = '', array $row = []): mixed
{
- if (!$this->do_explain) {
- return false;
+ return $this->debugger->explain($mode, $html_table, $row);
+ }
+
+ /**
+ * 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
- $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->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 .= '';
- }
- }
- 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 .= '
-
-
- ' . ($dbg['src'] ?? $dbg['trace']) . ' [' . sprintf('%.3f', $dbg['time']) . ' s] ' . $this->query_info() . ' |
- ' . "[$this->engine] $this->db_server.$this->selected_db" . ' :: Query #' . ($this->num_queries + 1) . ' |
-
- ' . $this->explain_hold . ' |
-
- ' . (function_exists('dev') ? dev()->formatShortQuery($dbg['sql'] ?? $dbg['query'], true) : htmlspecialchars($dbg['sql'] ?? $dbg['query'])) . '
-
';
- break;
-
- case 'add_explain_row':
- if (!$html_table && $row) {
- $html_table = true;
- $this->explain_hold .= '';
- foreach (array_keys($row) as $val) {
- $this->explain_hold .= '' . htmlspecialchars($val) . ' | ';
- }
- $this->explain_hold .= '
';
- }
- $this->explain_hold .= '';
- foreach (array_values($row) as $i => $val) {
- $class = !($i % 2) ? 'row1' : 'row2';
- $this->explain_hold .= '' . str_replace(["{$this->selected_db}.", ',', ';'], ['', ', ', '; '], htmlspecialchars($val ?? '')) . ' | ';
- }
- $this->explain_hold .= '
';
-
- return $html_table;
-
- case 'display':
- echo '' . $this->explain_out . '
';
- break;
+ /**
+ * Magic method to check if debug properties exist
+ */
+ public function __isset(string $name): bool
+ {
+ switch ($name) {
+ case 'dbg':
+ case 'dbg_id':
+ case 'dbg_enabled':
+ case 'do_explain':
+ case 'explain_hold':
+ case 'explain_out':
+ case 'slow_time':
+ case 'sql_timetotal':
+ return true;
+ default:
+ return false;
}
-
- return false;
}
/**
diff --git a/src/Database/DatabaseDebugger.php b/src/Database/DatabaseDebugger.php
new file mode 100644
index 000000000..92aaafe89
--- /dev/null
+++ b/src/Database/DatabaseDebugger.php
@@ -0,0 +1,349 @@
+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 .= '
';
+ }
+ }
+ 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 .= '
+
+
+ ' . ($dbg['src'] ?? $dbg['trace']) . ' [' . sprintf('%.3f', $dbg['time']) . ' s] ' . $this->db->query_info() . ' |
+ ' . "[{$this->db->engine}] {$this->db->db_server}.{$this->db->selected_db}" . ' :: Query #' . ($this->db->num_queries + 1) . ' |
+
+ ' . $this->explain_hold . ' |
+
+ ' . (function_exists('dev') ? dev()->formatShortQuery($dbg['sql'] ?? $dbg['query'], true) : htmlspecialchars($dbg['sql'] ?? $dbg['query'])) . '
+
';
+ break;
+
+ case 'add_explain_row':
+ if (!$html_table && $row) {
+ $html_table = true;
+ $this->explain_hold .= '';
+ foreach (array_keys($row) as $val) {
+ $this->explain_hold .= '' . htmlspecialchars($val) . ' | ';
+ }
+ $this->explain_hold .= '
';
+ }
+ $this->explain_hold .= '';
+ foreach (array_values($row) as $i => $val) {
+ $class = !($i % 2) ? 'row1' : 'row2';
+ $this->explain_hold .= '' . str_replace(["{$this->db->selected_db}.", ',', ';'], ['', ', ', '; '], htmlspecialchars($val ?? '')) . ' | ';
+ }
+ $this->explain_hold .= '
';
+
+ return $html_table;
+
+ case 'display':
+ echo '' . $this->explain_out . '
';
+ 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 = '';
+ }
+}
diff --git a/src/Database/DbFactory.php b/src/Database/DatabaseFactory.php
similarity index 94%
rename from src/Database/DbFactory.php
rename to src/Database/DatabaseFactory.php
index f8b8ed8a6..203c95961 100644
--- a/src/Database/DbFactory.php
+++ b/src/Database/DatabaseFactory.php
@@ -15,7 +15,7 @@ namespace TorrentPier\Database;
* This factory completely replaces the legacy SqlDb/Dbs system with the new
* Nette Database implementation while maintaining full backward compatibility.
*/
-class DbFactory
+class DatabaseFactory
{
private static array $instances = [];
private static array $server_configs = [];
@@ -33,7 +33,7 @@ class DbFactory
/**
* 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);
@@ -44,7 +44,7 @@ class DbFactory
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];
@@ -96,6 +96,6 @@ class DbFactory
}
}
self::$instances = [];
- DB::destroyInstances();
+ Database::destroyInstances();
}
}
\ No newline at end of file
diff --git a/src/Database/DebugSelection.php b/src/Database/DebugSelection.php
new file mode 100644
index 000000000..fbcf8ca9e
--- /dev/null
+++ b/src/Database/DebugSelection.php
@@ -0,0 +1,292 @@
+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;
+ }
+}
diff --git a/src/Database/README.md b/src/Database/README.md
index 22f19ffc7..a5f47bfe2 100644
--- a/src/Database/README.md
+++ b/src/Database/README.md
@@ -15,17 +15,21 @@ The new database system has completely replaced the legacy SqlDb/Dbs system and
### Classes
-1. **`DB`** - Main singleton database class using Nette Database Connection
-2. **`DbFactory`** - Factory that has completely replaced the legacy SqlDb/Dbs system
+1. **`Database`** - Main singleton database class using Nette Database Connection
+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
- **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
- **Complete Error Handling**: Maintains existing error handling behavior
- **Full Debug Support**: Preserves all debugging, logging, and explain functionality
- **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
@@ -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
- ✅ **Error Handling**: Complete error handling with sql_error() support
- ✅ **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
@@ -75,7 +81,7 @@ The database configuration is handled through the existing TorrentPier config sy
```php
// Initialized in common.php
-TorrentPier\Database\DbFactory::init(
+TorrentPier\Database\DatabaseFactory::init(
config()->get('db'), // Database configurations
config()->get('db_alias', []) // Database aliases
);
@@ -155,8 +161,10 @@ This is a **complete replacement** that maintains 100% backward compatibility:
## Files
-- `DB.php` - Main database class with full backward compatibility
-- `DbFactory.php` - Factory for managing database instances
+- `Database.php` - Main database class with full backward compatibility
+- `DatabaseFactory.php` - Factory for managing database instances
+- `DatabaseDebugger.php` - Dedicated debug functionality class
+- `DebugSelection.php` - Debug-enabled Nette Selection wrapper
- `README.md` - This documentation
## Future Enhancement: Gradual Migration to Nette Explorer
diff --git a/src/Dev.php b/src/Dev.php
index 303e4a027..3507d30a7 100644
--- a/src/Dev.php
+++ b/src/Dev.php
@@ -197,10 +197,10 @@ class Dev
$log = '';
// 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) {
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}]") : '';
} catch (\Exception $e) {
// Skip if server not available
diff --git a/src/Legacy/Poll.php b/src/Legacy/Poll.php
index a4ece7cfe..4e677db98 100644
--- a/src/Legacy/Poll.php
+++ b/src/Legacy/Poll.php
@@ -74,11 +74,14 @@ class Poll
'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()->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]);
}
/**
@@ -88,7 +91,9 @@ class Poll
*/
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);
}
@@ -99,8 +104,12 @@ class Poll
*/
public function delete_votes_data($topic_id)
{
- DB()->query("DELETE FROM " . BB_POLL_VOTES . " WHERE topic_id = $topic_id");
- DB()->query("DELETE FROM " . BB_POLL_USERS . " WHERE topic_id = $topic_id");
+ DB()->table(BB_POLL_VOTES)
+ ->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");
}
@@ -119,12 +128,11 @@ class Poll
$items = [];
if (!$poll_data = CACHE('bb_poll_data')->get("poll_$topic_id")) {
- $poll_data = DB()->fetch_rowset("
- SELECT topic_id, vote_id, vote_text, vote_result
- FROM " . BB_POLL_VOTES . "
- WHERE topic_id IN($topic_id_csv)
- ORDER BY topic_id, vote_id
- ");
+ $poll_data = DB()->table(BB_POLL_VOTES)
+ ->select('topic_id, vote_id, vote_text, vote_result')
+ ->where('topic_id IN (?)', explode(',', $topic_id_csv))
+ ->order('topic_id, vote_id')
+ ->fetchAll();
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
{
- 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();
}
/**