Index: branches/5.3.x/core/kernel/db/db_load_balancer.php =================================================================== --- branches/5.3.x/core/kernel/db/db_load_balancer.php (revision 15921) +++ branches/5.3.x/core/kernel/db/db_load_balancer.php (revision 15922) @@ -1,845 +1,870 @@ <?php /** * @version $Id$ * @package In-Portal * @copyright Copyright (C) 1997 - 2011 Intechnic. All rights reserved. * @license GNU/GPL * In-Portal is Open Source software. * This means that this software may have been modified pursuant * the GNU General Public License, and as distributed it includes * or is derivative of works licensed under the GNU General Public License * or other free or open source software licenses. * See http://www.in-portal.org/license for copyright notices and details. */ defined('FULL_PATH') or die('restricted access!'); class kDBLoadBalancer extends kBase implements IDBConnection { /** * Current database type * * @var string * @access protected */ protected $dbType = 'mysql'; /** * Function to handle sql errors * * @var string * @access public */ public $errorHandler = ''; /** * Database connection settings * * @var Array * @access protected */ protected $servers = Array (); /** * Load of each slave server, given * * @var Array * @access protected */ protected $serverLoads = Array (); /** * Caches replication lag of servers * * @var Array * @access protected */ protected $serverLagTimes = Array (); /** * Connection references to opened connections * * @var Array * @access protected */ protected $connections = Array (); /** * Index of last user slave connection * * @var int * @access protected */ protected $slaveIndex = false; /** * Index of the server, that was used last * * @var int * @access protected */ protected $lastUsedIndex = 0; /** * Consider slave down if it isn't responding for N milliseconds * * @var int * @access protected */ protected $DBClusterTimeout = 10; /** * Scale load balancer polling time so that under overload conditions, the database server * receives a SHOW STATUS query at an average interval of this many microseconds * * @var int * @access protected */ protected $DBAvgStatusPoll = 2000; /** * Indicates, that next database query could be cached, when memory caching is enabled * * @var bool * @access public */ public $nextQueryCachable = false; /** * Indicates, that next query should be sent to maser database * * @var bool */ public $nextQueryFromMaster = false; /** * Creates new instance of load balancer class * * @param string $dbType * @param Array|string $errorHandler */ function __construct($dbType, $errorHandler = '') { parent::__construct(); $this->dbType = $dbType; $this->errorHandler = $errorHandler; $this->DBClusterTimeout *= 1e6; // convert to milliseconds } /** - * Setups load balancer according given configuration + * Setups load balancer according given configuration. * * @param Array $config * @return void * @access public */ public function setup($config) { $this->servers = Array (); $this->servers[0] = Array ( 'DBHost' => $config['Database']['DBHost'], 'DBUser' => $config['Database']['DBUser'], 'DBUserPassword' => $config['Database']['DBUserPassword'], 'DBName' => $config['Database']['DBName'], 'DBLoad' => 0, ); if ( isset($config['Databases']) ) { $this->servers = array_merge($this->servers, $config['Databases']); } foreach ($this->servers as $server_index => $server_setting) { $this->serverLoads[$server_index] = $server_setting['DBLoad']; } } /** * Returns connection index to master database * * @return int * @access protected */ protected function getMasterIndex() { return 0; } /** * Returns connection index to slave database. This takes into account load ratios and lag times. * Side effect: opens connections to databases * * @return int * @access protected */ protected function getSlaveIndex() { if ( count($this->servers) == 1 || $this->Application->isAdmin ) { // skip the load balancing if there's only one server OR in admin console return 0; } elseif ( $this->slaveIndex !== false ) { // shortcut if generic reader exists already return $this->slaveIndex; } $total_elapsed = 0; $non_error_loads = $this->serverLoads; $i = $found = $lagged_slave_mode = false; // first try quickly looking through the available servers for a server that meets our criteria do { $current_loads = $non_error_loads; $overloaded_servers = $total_threads_connected = 0; while ($current_loads) { if ( $lagged_slave_mode ) { // when all slave servers are too lagged, then ignore lag and pick random server $i = $this->pickRandom($current_loads); } else { $i = $this->getRandomNonLagged($current_loads); if ( $i === false && $current_loads ) { // all slaves lagged -> pick random lagged slave then $lagged_slave_mode = true; $i = $this->pickRandom( $current_loads ); } } if ( $i === false ) { // all slaves are down -> use master as a slave $this->slaveIndex = $this->getMasterIndex(); return $this->slaveIndex; } $conn =& $this->openConnection($i); if ( !$conn ) { unset($non_error_loads[$i], $current_loads[$i]); continue; } // Perform post-connection backoff $threshold = isset($this->servers[$i]['DBMaxThreads']) ? $this->servers[$i]['DBMaxThreads'] : false; $backoff = $this->postConnectionBackoff($conn, $threshold); if ( $backoff ) { // post-connection overload, don't use this server for now $total_threads_connected += $backoff; $overloaded_servers++; unset( $current_loads[$i] ); } else { // return this server break 2; } } // no server found yet $i = false; // if all servers were down, quit now if ( !$non_error_loads ) { break; } // back off for a while // scale the sleep time by the number of connected threads, to produce a roughly constant global poll rate $avg_threads = $total_threads_connected / $overloaded_servers; usleep($this->DBAvgStatusPoll * $avg_threads); $total_elapsed += $this->DBAvgStatusPoll * $avg_threads; } while ( $total_elapsed < $this->DBClusterTimeout ); if ( $i !== false ) { // slave connection successful if ( $this->slaveIndex <= 0 && $this->serverLoads[$i] > 0 && $i !== false ) { $this->slaveIndex = $i; } } return $i; } /** * Returns random non-lagged server * * @param Array $loads * @return int * @access protected */ protected function getRandomNonLagged($loads) { // unset excessively lagged servers $lags = $this->getLagTimes(); foreach ($lags as $i => $lag) { if ( $i != 0 && isset($this->servers[$i]['DBMaxLag']) ) { if ( $lag === false ) { unset( $loads[$i] ); // server is not replicating } elseif ( $lag > $this->servers[$i]['DBMaxLag'] ) { unset( $loads[$i] ); // server is excessively lagged } } } // find out if all the slaves with non-zero load are lagged if ( !$loads || array_sum($loads) == 0 ) { return false; } // return a random representative of the remainder return $this->pickRandom($loads); } /** * Select an element from an array of non-normalised probabilities * * @param Array $weights * @return int * @access protected */ protected function pickRandom($weights) { if ( !is_array($weights) || !$weights ) { return false; } $sum = array_sum($weights); if ( $sum == 0 ) { return false; } $max = mt_getrandmax(); $rand = mt_rand(0, $max) / $max * $sum; $index = $sum = 0; foreach ($weights as $index => $weight) { $sum += $weight; if ( $sum >= $rand ) { break; } } return $index; } /** * Get lag time for each server * Results are cached for a short time in memcached, and indefinitely in the process cache * * @return Array * @access protected */ protected function getLagTimes() { if ( $this->serverLagTimes ) { return $this->serverLagTimes; } $expiry = 5; $request_rate = 10; $cache_key = 'lag_times:' . $this->servers[0]['DBHost']; $times = $this->Application->getCache($cache_key); if ( $times ) { // randomly recache with probability rising over $expiry $elapsed = adodb_mktime() - $times['timestamp']; $chance = max(0, ($expiry - $elapsed) * $request_rate); if ( mt_rand(0, $chance) != 0 ) { unset( $times['timestamp'] ); $this->serverLagTimes = $times; return $times; } } // cache key missing or expired $times = Array(); foreach ($this->servers as $index => $server) { if ($index == 0) { $times[$index] = 0; // master } else { $conn =& $this->openConnection($index); if ($conn !== false) { $times[$index] = $conn->getSlaveLag(); } } } // add a timestamp key so we know when it was cached $times['timestamp'] = adodb_mktime(); $this->Application->setCache($cache_key, $times, $expiry); // but don't give the timestamp to the caller unset($times['timestamp']); $this->serverLagTimes = $times; return $this->serverLagTimes; } /** * Determines whatever server should not be used, even, when connection was made * * @param kDBConnection $conn * @param int $threshold * @return int * @access protected */ protected function postConnectionBackoff(&$conn, $threshold) { if ( !$threshold ) { return 0; } $status = $conn->getStatus('Thread%'); return $status['Threads_running'] > $threshold ? $status['Threads_connected'] : 0; } /** * Open a connection to the server given by the specified index * Index must be an actual index into the array. * If the server is already open, returns it. * * On error, returns false. * * @param integer $i Server index * @return kDBConnection|false * @access protected */ protected function &openConnection($i) { if ( isset($this->connections[$i]) ) { $conn =& $this->connections[$i]; } else { $server = $this->servers[$i]; $server['serverIndex'] = $i; $conn =& $this->reallyOpenConnection($server, $i == $this->getMasterIndex()); if ( $conn->connectionOpened ) { $this->connections[$i] =& $conn; $this->lastUsedIndex = $i; } else { $conn = false; } } if ( $this->nextQueryCachable && is_object($conn) ) { $conn->nextQueryCachable = true; $this->nextQueryCachable = false; } return $conn; } /** - * Checks if connection to the server given by the specified index is opened + * Checks if previous query execution raised an error. + * + * @return bool + * @access public + */ + public function hasError() + { + $conn =& $this->openConnection($this->lastUsedIndex); + + return $conn->hasError(); + } + + /** + * Checks if connection to the server given by the specified index is opened. * * @param integer $i Server index * @return bool * @access public */ public function connectionOpened($i = null) { $conn =& $this->openConnection(isset($i) ? $i : $this->getMasterIndex()); return $conn ? $conn->connectionOpened : false; } /** * Really opens a connection. * Returns a database object whether or not the connection was successful. * * @param Array $server * @param bool $is_master * @return kDBConnection */ protected function &reallyOpenConnection($server, $is_master) { $debug_mode = $this->Application->isDebugMode(); $db_class = $debug_mode ? 'kDBConnectionDebug' : 'kDBConnection'; $db = $this->Application->makeClass($db_class, Array ($this->dbType, $this->errorHandler, $server['serverIndex'])); /* @var $db kDBConnection */ $db->debugMode = $debug_mode; $db->Connect($server['DBHost'], $server['DBUser'], $server['DBUserPassword'], $this->servers[0]['DBName'], true, !$is_master); return $db; } /** - * Returns first field of first line of recordset if query ok or false otherwise + * Returns first field of first line of recordset if query ok or false otherwise. * * @param string $sql * @param int $offset * @return string * @access public */ public function GetOne($sql, $offset = 0) { $conn =& $this->chooseConnection($sql); return $conn->GetOne($sql, $offset); } /** - * Returns first row of recordset if query ok, false otherwise + * Returns first row of recordset if query ok, false otherwise. * * @param string $sql * @param int $offset * @return Array * @access public */ public function GetRow($sql, $offset = 0) { $conn =& $this->chooseConnection($sql); return $conn->GetRow($sql, $offset); } /** - * Returns 1st column of recordset as one-dimensional array or false otherwise - * Optional parameter $key_field can be used to set field name to be used as resulting array key + * Returns 1st column of recordset as one-dimensional array or false otherwise. + * + * Optional parameter $key_field can be used to set field name to be used as resulting array key. * * @param string $sql * @param string $key_field * @return Array * @access public */ public function GetCol($sql, $key_field = null) { $conn =& $this->chooseConnection($sql); return $conn->GetCol($sql, $key_field); } /** * Returns iterator for 1st column of a recordset or false in case of error. - * Optional parameter $key_field can be used to set field name to be used - * as resulting array key. + * + * Optional parameter $key_field can be used to set field name to be used as resulting array key. * * @param string $sql * @param string $key_field * @return bool|kMySQLQueryCol */ public function GetColIterator($sql, $key_field = null) { $conn =& $this->chooseConnection($sql); return $conn->GetColIterator($sql, $key_field); } /** - * Queries db with $sql query supplied and returns rows selected if any, false - * otherwise. Optional parameter $key_field allows to set one of the query fields - * value as key in string array. + * Queries db with $sql query supplied and returns rows selected if any, false otherwise. + * + * Optional parameter $key_field allows to set one of the query fields value as key in string array. * * @param string $sql * @param string $key_field * @param bool $no_debug * @return Array * @access public */ public function Query($sql, $key_field = null, $no_debug = false) { $conn =& $this->chooseConnection($sql); return $conn->Query($sql, $key_field, $no_debug); } /** - * Returns iterator to a recordset, produced from running $sql query Queries db with $sql query supplied and returns kMySQLQuery iterator - * or false in case of error. Optional parameter $key_field allows to - * set one of the query fields value as key in string array. + * Returns iterator to a recordset, produced from running $sql query. + * + * Queries db with $sql query supplied and returns kMySQLQuery iterator or false in case of error. + * Optional parameter $key_field allows to set one of the query fields value as key in string array. * * @param string $sql * @param string $key_field - * @param bool $no_debug + * @param bool $no_debug * @param string $iterator_class * @return kMySQLQuery|bool * @access public */ public function GetIterator($sql, $key_field = null, $no_debug = false, $iterator_class = 'kMySQLQuery') { $conn =& $this->chooseConnection($sql); return $conn->GetIterator($sql, $key_field, $no_debug, $iterator_class); } /** - * Performs sql query, that will change database content + * Free memory used to hold recordset handle. + * + * @access public + */ + public function Destroy() + { + $conn =& $this->openConnection($this->lastUsedIndex); + + $conn->Destroy(); + } + + /** + * Performs sql query, that will change database content. * * @param string $sql * @return bool * @access public */ public function ChangeQuery($sql) { $conn =& $this->chooseConnection($sql); return $conn->ChangeQuery($sql); } /** - * Returns auto increment field value from - * insert like operation if any, zero otherwise + * Returns auto increment field value from insert like operation if any, zero otherwise. * * @return int * @access public */ public function getInsertID() { $conn =& $this->openConnection($this->lastUsedIndex); return $conn->getInsertID(); } /** - * Returns row count affected by last query + * Returns row count affected by last query. * * @return int * @access public */ public function getAffectedRows() { $conn =& $this->openConnection($this->lastUsedIndex); return $conn->getAffectedRows(); } /** - * Returns LIMIT sql clause part for specific db + * Returns LIMIT sql clause part for specific db. * * @param int $offset * @param int $rows * @return string * @access public */ public function getLimitClause($offset, $rows) { $conn =& $this->openConnection($this->lastUsedIndex); return $conn->getLimitClause($offset, $rows); } /** - * If it's a string, adds quotes and backslashes (only work since PHP 4.3.0) - * Otherwise returns as-is + * If it's a string, adds quotes and backslashes. Otherwise returns as-is. * * @param mixed $string * @return string * @access public */ public function qstr($string) { $conn =& $this->openConnection($this->lastUsedIndex); return $conn->qstr($string); } /** - * Calls "qstr" function for each given array element + * Calls "qstr" function for each given array element. * * @param Array $array * @param string $function * @return Array */ public function qstrArray($array, $function = 'qstr') { $conn =& $this->openConnection($this->lastUsedIndex); return $conn->qstrArray($array, $function); } /** - * Escapes strings (only work since PHP 4.3.0) + * Escapes string. * * @param mixed $string * @return string * @access public */ public function escape($string) { $conn =& $this->openConnection($this->lastUsedIndex); return $conn->escape($string); } /** - * Returns last error code occurred + * Returns last error code occurred. * * @return int * @access public */ public function getErrorCode() { $conn =& $this->openConnection($this->lastUsedIndex); return $conn->getErrorCode(); } /** - * Returns last error message + * Returns last error message. * * @return string * @access public */ public function getErrorMsg() { $conn =& $this->openConnection($this->lastUsedIndex); return $conn->getErrorMsg(); } /** * Performs insert of given data (useful with small number of queries) - * or stores it to perform multiple insert later (useful with large number of queries) + * or stores it to perform multiple insert later (useful with large number of queries). * * @param Array $fields_hash * @param string $table * @param string $type * @param bool $insert_now * @return bool * @access public */ public function doInsert($fields_hash, $table, $type = 'INSERT', $insert_now = true) { $conn =& $this->openConnection( $this->getMasterIndex() ); return $conn->doInsert($fields_hash, $table, $type, $insert_now); } /** - * Update given field values to given record using $key_clause + * Update given field values to given record using $key_clause. * * @param Array $fields_hash * @param string $table * @param string $key_clause * @return bool * @access public */ public function doUpdate($fields_hash, $table, $key_clause) { $conn =& $this->openConnection( $this->getMasterIndex() ); return $conn->doUpdate($fields_hash, $table, $key_clause); } /** - * Allows to detect table's presence in database + * Allows to detect table's presence in database. * * @param string $table_name * @param bool $force * @return bool * @access public */ public function TableFound($table_name, $force = false) { $conn =& $this->openConnection($this->lastUsedIndex); return $conn->TableFound($table_name, $force); } /** - * Returns query processing statistics + * Returns query processing statistics. * * @return Array * @access public */ public function getQueryStatistics() { $conn =& $this->openConnection($this->lastUsedIndex); return $conn->getQueryStatistics(); } /** - * Get status information from SHOW STATUS in an associative array + * Get status information from SHOW STATUS in an associative array. * * @param string $which * @return Array * @access public */ public function getStatus($which = '%') { $conn =& $this->openConnection($this->lastUsedIndex); return $conn->getStatus($which); } /** * When undefined method is called, then send it directly to last used slave server connection * * @param string $name * @param Array $arguments * @return mixed * @access public */ public function __call($name, $arguments) { $conn =& $this->openConnection($this->lastUsedIndex); return call_user_func_array( Array (&$conn, $name), $arguments ); } /** * Returns appropriate connection based on sql * * @param string $sql * @return kDBConnection * @access protected */ protected function &chooseConnection($sql) { if ( $this->nextQueryFromMaster ) { $this->nextQueryFromMaster = false; $index = $this->getMasterIndex(); } else { $sid = isset($this->Application->Session) ? $this->Application->GetSID() : '9999999999999999999999'; if ( preg_match('/(^[ \t\r\n]*(ALTER|CREATE|DROP|RENAME|DELETE|DO|INSERT|LOAD|REPLACE|TRUNCATE|UPDATE))|ses_' . $sid . '/', $sql) ) { $index = $this->getMasterIndex(); } else { $index = $this->getSlaveIndex(); } } $this->lastUsedIndex = $index; $conn =& $this->openConnection($index); return $conn; } /** * Get slave replication lag. It will only work if the DB user has the PROCESS privilege. * * @return int * @access public */ public function getSlaveLag() { $conn =& $this->openConnection($this->lastUsedIndex); return $conn->getSlaveLag(); } } Index: branches/5.3.x/core/kernel/db/i_db_connection.php =================================================================== --- branches/5.3.x/core/kernel/db/i_db_connection.php (revision 15921) +++ branches/5.3.x/core/kernel/db/i_db_connection.php (revision 15922) @@ -1,161 +1,262 @@ <?php /** * @version $Id: i_db_connection.php 15904 2013-07-17 18:52:50Z erik $ * @package In-Portal * @copyright Copyright (C) 1997 - 2013 Intechnic. All rights reserved. * @license GNU/GPL * In-Portal is Open Source software. * This means that this software may have been modified pursuant * the GNU General Public License, and as distributed it includes * or is derivative of works licensed under the GNU General Public License * or other free or open source software licenses. * See http://www.in-portal.org/license for copyright notices and details. */ defined('FULL_PATH') or die('restricted access!'); /** * Database connection interface * */ interface IDBConnection { /** - * Performs sql query, that will change database content + * Checks if previous query execution raised an error. + * + * @return bool + * @access public + */ + public function hasError(); + + /** + * Checks if connection to database is opened. + * + * @return bool + * @access public + */ + public function connectionOpened(); + + /** + * Setups the connection according given configuration. + * + * @param Array $config + * @return bool + * @access public + */ + public function setup($config); + + /** + * Returns first field of first line of recordset if query ok or false otherwise. + * + * @param string $sql + * @param int $offset + * @return string + * @access public + */ + public function GetOne($sql, $offset = 0); + + /** + * Returns first row of recordset if query ok, false otherwise. + * + * @param string $sql + * @param int $offset + * @return Array + * @access public + */ + public function GetRow($sql, $offset = 0); + + /** + * Returns 1st column of recordset as one-dimensional array or false otherwise. + * + * Optional parameter $key_field can be used to set field name to be used as resulting array key. + * + * @param string $sql + * @param string $key_field + * @return Array + * @access public + */ + public function GetCol($sql, $key_field = null); + + /** + * Returns iterator for 1st column of a recordset or false in case of error. + * + * Optional parameter $key_field can be used to set field name to be used as resulting array key. + * + * @param string $sql + * @param string $key_field + * @return bool|kMySQLQueryCol + */ + public function GetColIterator($sql, $key_field = null); + + /** + * Queries db with $sql query supplied and returns rows selected if any, false otherwise. + * + * Optional parameter $key_field allows to set one of the query fields value as key in string array. + * + * @param string $sql + * @param string $key_field + * @param bool $no_debug + * @return Array + * @access public + */ + public function Query($sql, $key_field = null, $no_debug = false); + + /** + * Returns iterator to a recordset, produced from running $sql query. + * + * Queries db with $sql query supplied and returns kMySQLQuery iterator or false in case of error. + * Optional parameter $key_field allows to set one of the query fields value as key in string array. + * + * @param string $sql + * @param string $key_field + * @param bool $no_debug + * @param string $iterator_class + * @return kMySQLQuery|bool + * @access public + */ + public function GetIterator($sql, $key_field = null, $no_debug = false, $iterator_class = 'kMySQLQuery'); + + /** + * Free memory used to hold recordset handle. + * + * @access public + */ + public function Destroy(); + + /** + * Performs sql query, that will change database content. * * @param string $sql * @return bool * @access public */ public function ChangeQuery($sql); /** - * Returns auto increment field value from - * insert like operation if any, zero otherwise + * Returns auto increment field value from insert like operation if any, zero otherwise. * * @return int * @access public */ public function getInsertID(); /** - * Returns row count affected by last query + * Returns row count affected by last query. * * @return int * @access public */ public function getAffectedRows(); /** - * Returns LIMIT sql clause part for specific db + * Returns LIMIT sql clause part for specific db. * * @param int $offset * @param int $rows * @return string * @access public */ public function getLimitClause($offset, $rows); /** - * If it's a string, adds quotes and backslashes (only work since PHP 4.3.0) - * Otherwise returns as-is + * If it's a string, adds quotes and backslashes. Otherwise returns as-is. * * @param mixed $string * @return string * @access public */ public function qstr($string); /** - * Calls "qstr" function for each given array element + * Calls "qstr" function for each given array element. * * @param Array $array * @param string $function * @return Array */ public function qstrArray($array, $function = 'qstr'); /** - * Escapes strings (only work since PHP 4.3.0) + * Escapes string. * * @param mixed $string * @return string * @access public */ public function escape($string); /** - * Returns last error code occurred + * Returns last error code occurred. * * @return int * @access public */ public function getErrorCode(); /** - * Returns last error message + * Returns last error message. * * @return string * @access public */ public function getErrorMsg(); /** * Performs insert of given data (useful with small number of queries) - * or stores it to perform multiple insert later (useful with large number of queries) + * or stores it to perform multiple insert later (useful with large number of queries). * * @param Array $fields_hash * @param string $table * @param string $type * @param bool $insert_now * @return bool * @access public */ public function doInsert($fields_hash, $table, $type = 'INSERT', $insert_now = true); /** - * Update given field values to given record using $key_clause + * Update given field values to given record using $key_clause. * * @param Array $fields_hash * @param string $table * @param string $key_clause * @return bool * @access public */ public function doUpdate($fields_hash, $table, $key_clause); /** - * Allows to detect table's presence in database + * Allows to detect table's presence in database. * * @param string $table_name * @param bool $force * @return bool * @access public */ public function TableFound($table_name, $force = false); /** - * Returns query processing statistics + * Returns query processing statistics. * * @return Array * @access public */ public function getQueryStatistics(); /** - * Get status information from SHOW STATUS in an associative array + * Get status information from SHOW STATUS in an associative array. * * @param string $which * @return Array * @access public */ public function getStatus($which = '%'); /** * Get slave replication lag. It will only work if the DB user has the PROCESS privilege. * * @return int * @access public */ public function getSlaveLag(); } Index: branches/5.3.x/core/kernel/db/db_connection.php =================================================================== --- branches/5.3.x/core/kernel/db/db_connection.php (revision 15921) +++ branches/5.3.x/core/kernel/db/db_connection.php (revision 15922) @@ -1,1518 +1,1507 @@ <?php /** * @version $Id$ * @package In-Portal * @copyright Copyright (C) 1997 - 2009 Intechnic. All rights reserved. * @license GNU/GPL * In-Portal is Open Source software. * This means that this software may have been modified pursuant * the GNU General Public License, and as distributed it includes * or is derivative of works licensed under the GNU General Public License * or other free or open source software licenses. * See http://www.in-portal.org/license for copyright notices and details. */ defined('FULL_PATH') or die('restricted access!'); /** * Multi database connection class * */ class kDBConnection extends kBase implements IDBConnection { /** * Current database type * * @var string * @access protected */ protected $dbType = 'mysql'; /** * Created connection handle * * @var resource * @access protected */ protected $connectionID = null; /** * Remembers, that database connection was opened successfully * * @var bool * @access public */ public $connectionOpened = false; /** * Connection parameters, that were used * * @var Array * @access protected */ protected $connectionParams = Array ('host' => '', 'user' => '', 'pass' => '', 'db' => ''); /** * Index of database server * * @var int * @access protected */ protected $serverIndex = 0; /** * Handle of currently processed recordset * * @var resource * @access protected */ protected $queryID = null; /** * DB type specific function mappings * * @var Array * @access protected */ protected $metaFunctions = Array (); /** * Function to handle sql errors * * @var Array|string * @access public */ public $errorHandler = ''; /** * Error code * * @var int * @access protected */ protected $errorCode = 0; /** * Error message * * @var string * @access protected */ protected $errorMessage = ''; /** * Defines if database connection * operations should generate debug * information * * @var bool * @access public */ public $debugMode = false; /** * Save query execution statistics * * @var bool * @access protected */ protected $_captureStatistics = false; /** * Last query to database * * @var string * @access public */ public $lastQuery = ''; /** * Total processed queries count * * @var int * @access protected */ protected $_queryCount = 0; /** * Total time, used for serving queries * * @var Array * @access protected */ protected $_queryTime = 0; /** * Indicates, that next database query could be cached, when memory caching is enabled * * @var bool * @access public */ public $nextQueryCachable = false; /** * For backwards compatibility with kDBLoadBalancer class * * @var bool * @access public */ public $nextQueryFromMaster = false; /** * Initializes connection class with * db type to used in future * * @param string $dbType * @param string $errorHandler * @param int $server_index * @access public */ public function __construct($dbType, $errorHandler = '', $server_index = 0) { if ( class_exists('kApplication') ) { // prevents "Fatal Error" on 2nd installation step (when database is empty) parent::__construct(); } $this->dbType = $dbType; $this->serverIndex = $server_index; // $this->initMetaFunctions(); if (!$errorHandler) { $this->errorHandler = Array(&$this, 'handleError'); } else { $this->errorHandler = $errorHandler; } $this->_captureStatistics = defined('DBG_CAPTURE_STATISTICS') && DBG_CAPTURE_STATISTICS && !(defined('ADMIN') && ADMIN); } /** * Set's custom error * * @param int $code * @param string $msg * @access protected */ protected function setError($code, $msg) { $this->errorCode = $code; $this->errorMessage = $msg; } /** - * Checks if previous query execution - * raised an error. + * Checks if previous query execution raised an error. * * @return bool * @access public */ public function hasError() { return $this->errorCode != 0; } /** * Caches function specific to requested * db type * * @access protected */ protected function initMetaFunctions() { $ret = Array (); switch ( $this->dbType ) { case 'mysql': $ret = Array (); // only define functions, that name differs from "dbType_<meta_name>" break; } $this->metaFunctions = $ret; } /** * Gets function for specific db type * based on it's meta name * * @param string $name * @return string * @access protected */ protected function getMetaFunction($name) { /*if ( !isset($this->metaFunctions[$name]) ) { $this->metaFunctions[$name] = $name; }*/ return $this->dbType . '_' . $name; } - /** - * Try to connect to database server - * using specified parameters and set - * database to $db if connection made + * Try to connect to database server using specified parameters and set database to $db if connection made. * * @param string $host * @param string $user * @param string $pass * @param string $db - * @param bool $force_new - * @param bool $retry + * @param bool $force_new + * @param bool $retry + * * @return bool * @access public + * @throws RuntimeException When connection failed. */ public function Connect($host, $user, $pass, $db, $force_new = false, $retry = false) { $this->connectionParams = Array ('host' => $host, 'user' => $user, 'pass' => $pass, 'db' => $db); $func = $this->getMetaFunction('connect'); $this->connectionID = $func($host, $user, $pass, $force_new); if ($this->connectionID) { if (defined('DBG_SQL_MODE')) { $this->Query('SET sql_mode = \''.DBG_SQL_MODE.'\''); } if (defined('SQL_COLLATION') && defined('SQL_CHARSET')) { $this->Query('SET NAMES \''.SQL_CHARSET.'\' COLLATE \''.SQL_COLLATION.'\''); } $this->setError(0, ''); // reset error $this->setDB($db); } // process error (fatal in most cases) $func = $this->getMetaFunction('errno'); $this->errorCode = $this->connectionID ? $func($this->connectionID) : $func(); if ( is_resource($this->connectionID) && !$this->hasError() ) { $this->connectionOpened = true; return true; } $func = $this->getMetaFunction('error'); $this->errorMessage = $this->connectionID ? $func($this->connectionID) : $func(); $error_msg = 'Database connection failed, please check your connection settings.<br/>Error (' . $this->errorCode . '): ' . $this->errorMessage; if ( (defined('IS_INSTALL') && IS_INSTALL) || $retry ) { trigger_error($error_msg, E_USER_WARNING); } else { $this->Application->redirectToMaintenance(); throw new RuntimeException($error_msg); } $this->connectionOpened = false; return false; } /** - * Checks if connection to database is opened + * Checks if connection to database is opened. * * @return bool * @access public */ public function connectionOpened() { return $this->connectionOpened; } /** - * Setups the connection according given configuration + * Setups the connection according given configuration. * * @param Array $config * @return bool * @access public */ public function setup($config) { if ( is_object($this->Application) ) { $this->debugMode = $this->Application->isDebugMode(); } return $this->Connect( $config['Database']['DBHost'], $config['Database']['DBUser'], $config['Database']['DBUserPassword'], $config['Database']['DBName'] ); } /** * Performs 3 reconnect attempts in case if connection to a DB was lost in the middle of script run (e.g. server restart) * * @param bool $force_new * @return bool * @access protected */ protected function ReConnect($force_new = false) { $retry_count = 0; $connected = false; $func = $this->getMetaFunction('close'); $func($this->connectionID); while ( $retry_count < 3 ) { sleep(5); // wait 5 seconds before each reconnect attempt $connected = $this->Connect( $this->connectionParams['host'], $this->connectionParams['user'], $this->connectionParams['pass'], $this->connectionParams['db'], $force_new, true ); if ( $connected ) { break; } $retry_count++; } return $connected; } /** * Shows error message from previous operation * if it failed * * @param string $sql * @param string $key_field * @param bool $no_debug * @return bool * @access protected */ protected function showError($sql = '', $key_field = null, $no_debug = false) { static $retry_count = 0; $func = $this->getMetaFunction('errno'); if (!$this->connectionID) { // no connection while doing mysql_query $this->errorCode = $func(); if ( $this->hasError() ) { $func = $this->getMetaFunction('error'); $this->errorMessage = $func(); $ret = $this->callErrorHandler($sql); if (!$ret) { exit; } } return false; } // checking if there was an error during last mysql_query $this->errorCode = $func($this->connectionID); if ( $this->hasError() ) { $func = $this->getMetaFunction('error'); $this->errorMessage = $func($this->connectionID); $ret = $this->callErrorHandler($sql); if ( ($this->errorCode == 2006 || $this->errorCode == 2013) && ($retry_count < 3) ) { // #2006 - MySQL server has gone away // #2013 - Lost connection to MySQL server during query $retry_count++; if ( $this->ReConnect() ) { return $this->Query($sql, $key_field, $no_debug); } } if (!$ret) { exit; } } else { $retry_count = 0; } return false; } /** * Sends db error to a predefined error handler * * @param $sql * @return bool * @access protected */ protected function callErrorHandler($sql) { if (is_array($this->errorHandler)) { $func = $this->errorHandler[1]; $ret = $this->errorHandler[0]->$func($this->errorCode, $this->errorMessage, $sql); } else { $func = $this->errorHandler; $ret = $func($this->errorCode, $this->errorMessage, $sql); } return $ret; } /** * Default error handler for sql errors * * @param int $code * @param string $msg * @param string $sql * @return bool * @access public */ public function handleError($code, $msg, $sql) { echo '<strong>Processing SQL</strong>: ' . $sql . '<br/>'; echo '<strong>Error (' . $code . '):</strong> ' . $msg . '<br/>'; return false; } /** * Set's database name for connection * to $new_name * * @param string $new_name * @return bool * @access protected */ protected function setDB($new_name) { if (!$this->connectionID) return false; $func = $this->getMetaFunction('select_db'); return $func($new_name, $this->connectionID); } /** - * Returns first field of first line - * of recordset if query ok or false - * otherwise + * Returns first field of first line of recordset if query ok or false otherwise. * * @param string $sql * @param int $offset * @return string * @access public */ public function GetOne($sql, $offset = 0) { $row = $this->GetRow($sql, $offset); if ( !$row ) { return false; } return array_shift($row); } /** - * Returns first row of recordset - * if query ok, false otherwise + * Returns first row of recordset if query ok, false otherwise. * * @param string $sql * @param int $offset * @return Array * @access public */ public function GetRow($sql, $offset = 0) { $sql .= ' ' . $this->getLimitClause($offset, 1); $ret = $this->Query($sql); if ( !$ret ) { return false; } return array_shift($ret); } /** - * Returns 1st column of recordset as - * one-dimensional array or false otherwise - * Optional parameter $key_field can be used - * to set field name to be used as resulting - * array key + * Returns 1st column of recordset as one-dimensional array or false otherwise. + * + * Optional parameter $key_field can be used to set field name to be used as resulting array key. * * @param string $sql * @param string $key_field * @return Array * @access public */ public function GetCol($sql, $key_field = null) { $rows = $this->Query($sql); if ( !$rows ) { return $rows; } $i = 0; $row_count = count($rows); $ret = Array (); if ( isset($key_field) ) { while ( $i < $row_count ) { $ret[$rows[$i][$key_field]] = array_shift($rows[$i]); $i++; } } else { while ( $i < $row_count ) { $ret[] = array_shift($rows[$i]); $i++; } } return $ret; } /** * Returns iterator for 1st column of a recordset or false in case of error. - * Optional parameter $key_field can be used to set field name to be used - * as resulting array key. + * + * Optional parameter $key_field can be used to set field name to be used as resulting array key. * * @param string $sql * @param string $key_field * @return bool|kMySQLQueryCol */ public function GetColIterator($sql, $key_field = null) { return $this->GetIterator($sql, $key_field, false, 'kMySQLQueryCol'); } /** - * Queries db with $sql query supplied - * and returns rows selected if any, false - * otherwise. Optional parameter $key_field - * allows to set one of the query fields - * value as key in string array. + * Queries db with $sql query supplied and returns rows selected if any, false otherwise. + * + * Optional parameter $key_field allows to set one of the query fields value as key in string array. * * @param string $sql * @param string $key_field * @param bool $no_debug * @return Array * @access public */ public function Query($sql, $key_field = null, $no_debug = false) { $this->_queryCount++; $this->lastQuery = $sql; $query_func = $this->getMetaFunction('query'); // set 1st checkpoint: begin $start_time = $this->_captureStatistics ? microtime(true) : 0; // set 1st checkpoint: end $this->setError(0, ''); // reset error $this->queryID = $query_func($sql, $this->connectionID); if ( is_resource($this->queryID) ) { $ret = Array (); $fetch_func = $this->getMetaFunction('fetch_assoc'); if ( isset($key_field) ) { while (($row = $fetch_func($this->queryID))) { $ret[$row[$key_field]] = $row; } } else { while (($row = $fetch_func($this->queryID))) { $ret[] = $row; } } // set 2nd checkpoint: begin if ( $this->_captureStatistics ) { $query_time = microtime(true) - $start_time; if ( $query_time > DBG_MAX_SQL_TIME ) { $this->Application->logSlowQuery($sql, $query_time); } $this->_queryTime += $query_time; } // set 2nd checkpoint: end $this->Destroy(); return $ret; } else { // set 2nd checkpoint: begin if ( $this->_captureStatistics ) { $this->_queryTime += microtime(true) - $start_time; } // set 2nd checkpoint: end } return $this->showError($sql, $key_field, $no_debug); } /** - * Returns iterator to a recordset, produced from running $sql query Queries db with $sql query supplied and returns kMySQLQuery iterator - * or false in case of error. Optional parameter $key_field allows to - * set one of the query fields value as key in string array. + * Returns iterator to a recordset, produced from running $sql query. + * + * Queries db with $sql query supplied and returns kMySQLQuery iterator or false in case of error. + * Optional parameter $key_field allows to set one of the query fields value as key in string array. * * @param string $sql * @param string $key_field - * @param bool $no_debug + * @param bool $no_debug * @param string $iterator_class * @return kMySQLQuery|bool * @access public */ public function GetIterator($sql, $key_field = null, $no_debug = false, $iterator_class = 'kMySQLQuery') { $this->_queryCount++; $this->lastQuery = $sql; $query_func = $this->getMetaFunction('query'); // set 1st checkpoint: begin $start_time = $this->_captureStatistics ? microtime(true) : 0; // set 1st checkpoint: end $this->setError(0, ''); // reset error $this->queryID = $query_func($sql, $this->connectionID); if ( is_resource($this->queryID) ) { $ret = new $iterator_class($this->queryID, $key_field); /* @var $ret kMySQLQuery */ // set 2nd checkpoint: begin if ( $this->_captureStatistics ) { $query_time = microtime(true) - $start_time; if ( $query_time > DBG_MAX_SQL_TIME ) { $this->Application->logSlowQuery($sql, $query_time); } $this->_queryTime += $query_time; } // set 2nd checkpoint: end return $ret; } else { // set 2nd checkpoint: begin if ( $this->_captureStatistics ) { $this->_queryTime += microtime(true) - $start_time; } // set 2nd checkpoint: end } return $this->showError($sql, $key_field, $no_debug); } /** - * Free memory used to hold recordset handle + * Free memory used to hold recordset handle. * * @access public */ public function Destroy() { $free_func = $this->getMetaFunction('free_result'); $free_func($this->queryID); unset($this->queryID); } /** - * Performs sql query, that will change database content + * Performs sql query, that will change database content. * * @param string $sql * @return bool * @access public */ public function ChangeQuery($sql) { $this->Query($sql); return !$this->hasError(); } /** - * Returns auto increment field value from - * insert like operation if any, zero otherwise + * Returns auto increment field value from insert like operation if any, zero otherwise. * * @return int * @access public */ public function getInsertID() { $func = $this->getMetaFunction('insert_id'); return $func($this->connectionID); } /** - * Returns row count affected by last query + * Returns row count affected by last query. * * @return int * @access public */ public function getAffectedRows() { $func = $this->getMetaFunction('affected_rows'); return $func($this->connectionID); } /** - * Returns LIMIT sql clause part for specific db + * Returns LIMIT sql clause part for specific db. * * @param int $offset * @param int $rows * @return string * @access public */ public function getLimitClause($offset, $rows) { if ( !($rows > 0) ) { return ''; } switch ( $this->dbType ) { default: return 'LIMIT ' . $offset . ',' . $rows; break; } } /** - * If it's a string, adds quotes and backslashes (only work since PHP 4.3.0) - * Otherwise returns as-is + * If it's a string, adds quotes and backslashes. Otherwise returns as-is. * * @param mixed $string * @return string * @access public */ public function qstr($string) { if ( is_null($string) ) { return 'NULL'; } # This will also quote numeric values. This should be harmless, # and protects against weird problems that occur when they really # _are_ strings such as article titles and string->number->string # conversion is not 1:1. return "'" . mysql_real_escape_string($string, $this->connectionID) . "'"; } /** - * Calls "qstr" function for each given array element + * Calls "qstr" function for each given array element. * * @param Array $array * @param string $function * @return Array */ public function qstrArray($array, $function = 'qstr') { return array_map(Array (&$this, $function), $array); } /** - * Escapes strings (only work since PHP 4.3.0) + * Escapes string. * * @param mixed $string * @return string * @access public */ public function escape($string) { if ( is_null($string) ) { return 'NULL'; } $string = mysql_real_escape_string($string, $this->connectionID); // prevent double-escaping of MySQL wildcard symbols ("%" and "_") in case if they were already escaped return str_replace(Array ('\\\\%', '\\\\_'), Array ('\\%', '\\_'), $string); } /** - * Returns last error code occurred + * Returns last error code occurred. * * @return int * @access public */ public function getErrorCode() { return $this->errorCode; } /** - * Returns last error message + * Returns last error message. * * @return string * @access public */ public function getErrorMsg() { return $this->errorMessage; } /** * Performs insert of given data (useful with small number of queries) - * or stores it to perform multiple insert later (useful with large number of queries) + * or stores it to perform multiple insert later (useful with large number of queries). * * @param Array $fields_hash * @param string $table * @param string $type * @param bool $insert_now * @return bool * @access public */ public function doInsert($fields_hash, $table, $type = 'INSERT', $insert_now = true) { static $value_sqls = Array (); if ($insert_now) { $fields_sql = '`' . implode('`,`', array_keys($fields_hash)) . '`'; } $values_sql = ''; foreach ($fields_hash as $field_name => $field_value) { $values_sql .= $this->qstr($field_value) . ','; } // don't use preg here, as it may fail when string is too long $value_sqls[] = rtrim($values_sql, ','); $insert_result = true; if ($insert_now) { $insert_count = count($value_sqls); if (($insert_count > 1) && ($value_sqls[$insert_count - 1] == $value_sqls[$insert_count - 2])) { // last two records are the same array_pop($value_sqls); } $sql = strtoupper($type) . ' INTO `' . $table . '` (' . $fields_sql . ') VALUES (' . implode('),(', $value_sqls) . ')'; $value_sqls = Array (); // reset before query to prevent repeated call from error handler to insert 2 records instead of 1 $insert_result = $this->ChangeQuery($sql); } return $insert_result; } /** - * Update given field values to given record using $key_clause + * Update given field values to given record using $key_clause. * * @param Array $fields_hash * @param string $table * @param string $key_clause * @return bool * @access public */ public function doUpdate($fields_hash, $table, $key_clause) { if (!$fields_hash) return true; $fields_sql = ''; foreach ($fields_hash as $field_name => $field_value) { $fields_sql .= '`'.$field_name.'` = ' . $this->qstr($field_value) . ','; } // don't use preg here, as it may fail when string is too long $fields_sql = rtrim($fields_sql, ','); $sql = 'UPDATE `'.$table.'` SET '.$fields_sql.' WHERE '.$key_clause; return $this->ChangeQuery($sql); } /** - * Allows to detect table's presence in database + * Allows to detect table's presence in database. * * @param string $table_name * @param bool $force * @return bool * @access public */ public function TableFound($table_name, $force = false) { static $table_found = false; if ( $table_found === false ) { $table_found = array_flip($this->GetCol('SHOW TABLES')); } if ( !preg_match('/^' . preg_quote(TABLE_PREFIX, '/') . '(.*)/', $table_name) ) { $table_name = TABLE_PREFIX . $table_name; } if ( $force ) { if ( $this->Query('SHOW TABLES LIKE ' . $this->qstr($table_name)) ) { $table_found[$table_name] = 1; } else { unset($table_found[$table_name]); } } return isset($table_found[$table_name]); } /** - * Returns query processing statistics + * Returns query processing statistics. * * @return Array * @access public */ public function getQueryStatistics() { return Array ('time' => $this->_queryTime, 'count' => $this->_queryCount); } /** - * Get status information from SHOW STATUS in an associative array + * Get status information from SHOW STATUS in an associative array. * * @param string $which * @return Array * @access public */ public function getStatus($which = '%') { $status = Array (); $records = $this->Query('SHOW STATUS LIKE "' . $which . '"'); foreach ($records as $record) { $status[ $record['Variable_name'] ] = $record['Value']; } return $status; } /** * Get slave replication lag. It will only work if the DB user has the PROCESS privilege. * * @return int * @access public */ public function getSlaveLag() { // don't use kDBConnection::Query method, since it will create an array of all server processes $processes = $this->GetIterator('SHOW PROCESSLIST'); $skip_states = Array ( 'Waiting for master to send event', 'Connecting to master', 'Queueing master event to the relay log', 'Waiting for master update', 'Requesting binlog dump', ); // find slave SQL thread foreach ($processes as $process) { if ( $process['User'] == 'system user' && !in_array($process['State'], $skip_states) ) { // this is it, return the time (except -ve) return $process['Time'] > 0x7fffffff ? false : $process['Time']; } } return false; } } class kDBConnectionDebug extends kDBConnection { protected $_profileSQLs = false; /** * Info about this database connection to show in debugger report * * @var string * @access protected */ protected $serverInfoLine = ''; /** * Initializes connection class with * db type to used in future * * @param string $dbType * @param string $errorHandler * @param int $server_index * @access public */ public function __construct($dbType, $errorHandler = '', $server_index = 0) { parent::__construct($dbType, $errorHandler, $server_index); $this->_profileSQLs = defined('DBG_SQL_PROFILE') && DBG_SQL_PROFILE; } /** * Try to connect to database server * using specified parameters and set * database to $db if connection made * * @param string $host * @param string $user * @param string $pass * @param string $db * @param bool $force_new * @param bool $retry * @return bool * @access public */ public function Connect($host, $user, $pass, $db, $force_new = false, $retry = false) { if ( defined('DBG_SQL_SERVERINFO') && DBG_SQL_SERVERINFO ) { $this->serverInfoLine = $this->serverIndex . ' (' . $host . ')'; } return parent::Connect($host, $user, $pass, $db, $force_new, $retry); } /** - * Queries db with $sql query supplied - * and returns rows selected if any, false - * otherwise. Optional parameter $key_field - * allows to set one of the query fields - * value as key in string array. + * Queries db with $sql query supplied and returns rows selected if any, false otherwise. + * + * Optional parameter $key_field allows to set one of the query fields value as key in string array. * * @param string $sql * @param string $key_field * @param bool $no_debug * @return Array * @access public */ public function Query($sql, $key_field = null, $no_debug = false) { if ( $no_debug ) { return parent::Query($sql, $key_field, $no_debug); } global $debugger; $this->_queryCount++; $this->lastQuery = $sql; $query_func = $this->getMetaFunction('query'); // set 1st checkpoint: begin if ( $this->_profileSQLs ) { $queryID = $debugger->generateID(); $debugger->profileStart('sql_' . $queryID, $debugger->formatSQL($sql)); } // set 1st checkpoint: end $this->setError(0, ''); // reset error $this->queryID = $query_func($sql, $this->connectionID); if ( is_resource($this->queryID) ) { $ret = Array (); $fetch_func = $this->getMetaFunction('fetch_assoc'); if ( isset($key_field) ) { while (($row = $fetch_func($this->queryID))) { $ret[$row[$key_field]] = $row; } } else { while (($row = $fetch_func($this->queryID))) { $ret[] = $row; } } // set 2nd checkpoint: begin if ( $this->_profileSQLs ) { $current_element = current($ret); $first_cell = count($ret) == 1 && count($current_element) == 1 ? current($current_element) : null; if ( strlen($first_cell) > 200 ) { $first_cell = substr($first_cell, 0, 50) . ' ...'; } $debugger->profileFinish('sql_' . $queryID, null, null, $this->getAffectedRows(), $first_cell, $this->_queryCount, $this->nextQueryCachable, $this->serverInfoLine); $debugger->profilerAddTotal('sql', 'sql_' . $queryID); $this->nextQueryCachable = false; } // set 2nd checkpoint: end $this->Destroy(); return $ret; } else { // set 2nd checkpoint: begin if ( $this->_profileSQLs ) { $debugger->profileFinish('sql_' . $queryID, null, null, $this->getAffectedRows(), null, $this->_queryCount, $this->nextQueryCachable, $this->serverInfoLine); $debugger->profilerAddTotal('sql', 'sql_' . $queryID); $this->nextQueryCachable = false; } // set 2nd checkpoint: end } return $this->showError($sql, $key_field); } /** - * Queries db with $sql query supplied and returns kMySQLQuery iterator - * or false in case of error. Optional parameter $key_field allows to - * set one of the query fields value as key in string array. + * Returns iterator to a recordset, produced from running $sql query. + * + * Queries db with $sql query supplied and returns kMySQLQuery iterator or false in case of error. + * Optional parameter $key_field allows to set one of the query fields value as key in string array. * * @param string $sql * @param string $key_field - * @param bool $no_debug + * @param bool $no_debug * @param string $iterator_class * @return kMySQLQuery|bool * @access public */ public function GetIterator($sql, $key_field = null, $no_debug = false, $iterator_class = 'kMySQLQuery') { if ( $no_debug ) { return parent::Query($sql, $key_field, $no_debug, $iterator_class); } global $debugger; $this->_queryCount++; $this->lastQuery = $sql; $query_func = $this->getMetaFunction('query'); // set 1st checkpoint: begin if ( $this->_profileSQLs ) { $queryID = $debugger->generateID(); $debugger->profileStart('sql_' . $queryID, $debugger->formatSQL($sql)); } // set 1st checkpoint: end $this->setError(0, ''); // reset error $this->queryID = $query_func($sql, $this->connectionID); if ( is_resource($this->queryID) ) { $ret = new $iterator_class($this->queryID, $key_field); /* @var $ret kMySQLQuery */ // set 2nd checkpoint: begin if ( $this->_profileSQLs ) { $first_cell = count($ret) == 1 && $ret->fieldCount() == 1 ? current($ret->current()) : null; if ( strlen($first_cell) > 200 ) { $first_cell = substr($first_cell, 0, 50) . ' ...'; } $debugger->profileFinish('sql_' . $queryID, null, null, $this->getAffectedRows(), $first_cell, $this->_queryCount, $this->nextQueryCachable, $this->serverInfoLine); $debugger->profilerAddTotal('sql', 'sql_' . $queryID); $this->nextQueryCachable = false; } // set 2nd checkpoint: end return $ret; } else { // set 2nd checkpoint: begin if ( $this->_profileSQLs ) { $debugger->profileFinish('sql_' . $queryID, null, null, $this->getAffectedRows(), null, $this->_queryCount, $this->nextQueryCachable, $this->serverInfoLine); $debugger->profilerAddTotal('sql', 'sql_' . $queryID); $this->nextQueryCachable = false; } // set 2nd checkpoint: end } return $this->showError($sql, $key_field); } } class kMySQLQuery implements Iterator, Countable, SeekableIterator { /** * Current index in recordset * * @var int * @access protected */ protected $position = -1; /** * Query resource * * @var resource * @access protected */ protected $result; /** * Field to act as key in a resulting array * * @var string * @access protected */ protected $keyField = null; /** * Data in current row of recordset * * @var Array * @access protected */ protected $rowData = Array (); /** * Row count in a result * * @var int * @access protected */ protected $rowCount = 0; /** * Creates new instance of a class * * @param resource $result * @param null|string $key_field */ public function __construct($result, $key_field = null) { $this->result = $result; $this->keyField = $key_field; $this->rowCount = mysql_num_rows($this->result); $this->rewind(); } /** * Moves recordset pointer to first element * * @return void * @access public * @implements Iterator::rewind */ public function rewind() { $this->seek(0); } /** * Returns value at current position * * @return mixed * @access public * @implements Iterator::current */ function current() { return $this->rowData; } /** * Returns key at current position * * @return mixed * @access public * @implements Iterator::key */ function key() { return $this->keyField ? $this->rowData[$this->keyField] : $this->position; } /** * Moves recordset pointer to next position * * @return void * @access public * @implements Iterator::next */ function next() { $this->seek($this->position + 1); } /** * Detects if current position is within recordset bounds * * @return bool * @access public * @implements Iterator::valid */ public function valid() { return $this->position < $this->rowCount; } /** * Counts recordset rows * * @return int * @access public * @implements Countable::count */ public function count() { return $this->rowCount; } /** * Counts fields in current row * * @return int * @access public */ public function fieldCount() { return count($this->rowData); } /** * Moves cursor into given position within recordset * * @param int $position * @throws OutOfBoundsException * @access public * @implements SeekableIterator::seek */ public function seek($position) { if ( $this->position == $position ) { return; } $this->position = $position; if ( $this->valid() ) { mysql_data_seek($this->result, $this->position); $this->rowData = mysql_fetch_assoc($this->result); } /*if ( !$this->valid() ) { throw new OutOfBoundsException('Invalid seek position (' . $position . ')'); }*/ } /** * Returns first recordset row * * @return Array * @access public */ public function first() { $this->seek(0); return $this->rowData; } /** * Closes recordset and freese memory * * @return void * @access public */ public function close() { mysql_free_result($this->result); unset($this->result); } /** * Frees memory when object is destroyed * * @return void * @access public */ public function __destruct() { $this->close(); } /** * Returns all keys * * @return Array * @access public */ public function keys() { $ret = Array (); foreach ($this as $key => $value) { $ret[] = $key; } return $ret; } /** * Returns all values * * @return Array * @access public */ public function values() { $ret = Array (); foreach ($this as $value) { $ret[] = $value; } return $ret; } /** * Returns whole recordset as array * * @return Array * @access public */ public function toArray() { $ret = Array (); foreach ($this as $key => $value) { $ret[$key] = $value; } return $ret; } } class kMySQLQueryCol extends kMySQLQuery { /** * Returns value at current position * * @return mixed * @access public * @implements Iterator::current */ function current() { return reset($this->rowData); } /** * Returns first column of first recordset row * * @return string * @access public */ public function first() { $this->seek(0); return reset($this->rowData); } }