<?php
/**
* @version	$Id: db_connection.php 15242 2012-03-27 10:09:34Z alex $
* @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 {

		/**
		 * 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.
		 *
		 * @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
		 *
		 * @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)
		{
			$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 Exception($error_msg);
			}

			$this->connectionOpened = false;

			return false;
		}

		/**
		 * 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
		 *
		 * @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
		 *
		 * @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
		 *
		 * @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.
		 *
		 * @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.
		 *
		 * @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.
		 *
		 * @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')
		{
			$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
		 *
		 * @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
		 *
		 * @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
		 *
		 * @return int
		 * @access public
		 */
		public function getInsertID()
		{
			$func = $this->getMetaFunction('insert_id');
			return $func($this->connectionID);
		}

		/**
		 * 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
		 *
		 * @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
		 *
		 * @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
		 *
		 * @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)
		 *
		 * @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
		 *
		 * @return int
		 * @access public
		 */
		public function getErrorCode()
		{
			return $this->errorCode;
		}

		/**
		 * 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)
		 *
		 * @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
		 *
		 * @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
		 *
		 * @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
		 *
		 * @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
		 *
		 * @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;

	/**
	 * 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;
	}

	/**
	 * 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 ) {
				$first_cell = count($ret) == 1 && count(current($ret)) == 1 ? current(current($ret)) : 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->serverIndex);
				$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->serverIndex);
				$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.
	 *
	 * @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')
	{
		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->serverIndex);
				$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->serverIndex);
				$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);
	}
}
