1
0
mirror of https://github.com/phpbb/phpbb.git synced 2025-06-07 06:55:25 +02:00
David M dfe674ffa1 - changed the way we do forum accounting in phpBB, far less intensive and much faster. sync() recalculates the number of topics and posts using just the topics table instead of having to join topics and posts together. However, even this can be avoided if we know what operation is happening and an auto sync is not in action. Since MCP operations are "known", we can provide very fast MCP operations.
- changed the way we decide if a DB gets multi value support. Old method uses switch/case, new method assumes a DB can't unless the DB says it can via a property

I hope nothing is broken :P


git-svn-id: file:///svn/phpbb/trunk@7466 89ea8834-ac86-4346-8a33-228a782c2dd0
2007-05-05 04:53:43 +00:00

744 lines
18 KiB
PHP

<?php
/**
*
* @package dbal
* @version $Id$
* @copyright (c) 2005 phpBB Group
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
*
*/
/**
* Database Abstraction Layer
* @package dbal
*/
class dbal
{
var $db_connect_id;
var $query_result;
var $return_on_error = false;
var $transaction = false;
var $sql_time = 0;
var $num_queries = array();
var $open_queries = array();
var $curtime = 0;
var $query_hold = '';
var $html_hold = '';
var $sql_report = '';
var $persistency = false;
var $user = '';
var $server = '';
var $dbname = '';
// Set to true if error triggered
var $sql_error_triggered = false;
// Holding the last sql query on sql error
var $sql_error_sql = '';
// Supports multi inserts?
var $multi_insert = false;
/**
* Current sql layer
*/
var $sql_layer = '';
/**
* Constructor
*/
function dbal()
{
$this->num_queries = array(
'cached' => 0,
'normal' => 0,
'total' => 0,
);
// Fill default sql layer based on the class being called.
// This can be changed by the specified layer itself later if needed.
$this->sql_layer = substr(get_class($this), 5);
}
/**
* return on error or display error message
*/
function sql_return_on_error($fail = false)
{
$this->sql_error_triggered = false;
$this->sql_error_sql = '';
$this->return_on_error = $fail;
}
/**
* Return number of sql queries and cached sql queries used
*/
function sql_num_queries($cached = false)
{
return ($cached) ? $this->num_queries['cached'] : $this->num_queries['normal'];
}
/**
* Add to query count
*/
function sql_add_num_queries($cached = false)
{
$this->num_queries['cached'] += ($cached) ? 1 : 0;
$this->num_queries['normal'] += ($cached) ? 0 : 1;
$this->num_queries['total'] += 1;
}
/**
* DBAL garbage collection, close sql connection
*/
function sql_close()
{
if (!$this->db_connect_id)
{
return false;
}
if ($this->transaction)
{
$this->sql_transaction('commit');
}
foreach ($this->open_queries as $query_id)
{
$this->sql_freeresult($query_id);
}
return $this->_sql_close();
}
/**
* Build LIMIT query
* Doing some validation here.
*/
function sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0)
{
if (empty($query))
{
return false;
}
// Never use a negative total or offset
$total = ($total < 0) ? 0 : $total;
$offset = ($offset < 0) ? 0 : $offset;
return $this->_sql_query_limit($query, $total, $offset, $cache_ttl);
}
/**
* Fetch all rows
*/
function sql_fetchrowset($query_id = false)
{
if ($query_id === false)
{
$query_id = $this->query_result;
}
if ($query_id !== false)
{
$result = array();
while ($row = $this->sql_fetchrow($query_id))
{
$result[] = $row;
}
return $result;
}
return false;
}
/**
* Fetch field
* if rownum is false, the current row is used, else it is pointing to the row (zero-based)
*/
function sql_fetchfield($field, $rownum = false, $query_id = false)
{
global $cache;
if ($query_id === false)
{
$query_id = $this->query_result;
}
if ($query_id !== false)
{
if ($rownum !== false)
{
$this->sql_rowseek($rownum, $query_id);
}
if (!is_object($query_id) && isset($cache->sql_rowset[$query_id]))
{
return $cache->sql_fetchfield($query_id, $field);
}
$row = $this->sql_fetchrow($query_id);
return (isset($row[$field])) ? $row[$field] : false;
}
return false;
}
/**
* SQL Transaction
* @access private
*/
function sql_transaction($status = 'begin')
{
switch ($status)
{
case 'begin':
// Commit previously opened transaction before opening another transaction
if ($this->transaction)
{
$this->_sql_transaction('commit');
}
$result = $this->_sql_transaction('begin');
$this->transaction = true;
break;
case 'commit':
$result = $this->_sql_transaction('commit');
$this->transaction = false;
if (!$result)
{
$this->_sql_transaction('rollback');
}
break;
case 'rollback':
$result = $this->_sql_transaction('rollback');
$this->transaction = false;
break;
default:
$result = $this->_sql_transaction($status);
break;
}
return $result;
}
/**
* Build sql statement from array for insert/update/select statements
*
* Idea for this from Ikonboard
* Possible query values: INSERT, INSERT_SELECT, MULTI_INSERT, UPDATE, SELECT
*
*/
function sql_build_array($query, $assoc_ary = false)
{
if (!is_array($assoc_ary))
{
return false;
}
$fields = $values = array();
if ($query == 'INSERT' || $query == 'INSERT_SELECT')
{
foreach ($assoc_ary as $key => $var)
{
$fields[] = $key;
if (is_array($var) && is_string($var[0]))
{
// This is used for INSERT_SELECT(s)
$values[] = $var[0];
}
else
{
$values[] = $this->_sql_validate_value($var);
}
}
$query = ($query == 'INSERT') ? ' (' . implode(', ', $fields) . ') VALUES (' . implode(', ', $values) . ')' : ' (' . implode(', ', $fields) . ') SELECT ' . implode(', ', $values) . ' ';
}
else if ($query == 'MULTI_INSERT')
{
$ary = array();
foreach ($assoc_ary as $id => $sql_ary)
{
// If by accident the sql array is only one-dimensional we build a normal insert statement
if (!is_array($sql_ary))
{
return $this->sql_build_array('INSERT', $assoc_ary);
}
$values = array();
foreach ($sql_ary as $key => $var)
{
$values[] = $this->_sql_validate_value($var);
}
$ary[] = '(' . implode(', ', $values) . ')';
}
$query = ' (' . implode(', ', array_keys($assoc_ary[0])) . ') VALUES ' . implode(', ', $ary);
}
else if ($query == 'UPDATE' || $query == 'SELECT')
{
$values = array();
foreach ($assoc_ary as $key => $var)
{
$values[] = "$key = " . $this->_sql_validate_value($var);
}
$query = implode(($query == 'UPDATE') ? ', ' : ' AND ', $values);
}
return $query;
}
/**
* Build IN or NOT IN sql comparison string, uses <> or = on single element
* arrays to improve comparison speed
*
* @access public
* @param string $field name of the sql column that shall be compared
* @param array $array array of values that are allowed (IN) or not allowed (NOT IN)
* @param bool $negate true for NOT IN (), false for IN () (default)
* @param bool $allow_empty_set If true, allow $array to be empty, this function will return 1=1 or 1=0 then. Default to false.
*/
function sql_in_set($field, $array, $negate = false, $allow_empty_set = false)
{
if (!sizeof($array))
{
if (!$allow_empty_set)
{
// Print the backtrace to help identifying the location of the problematic code
$this->sql_error('No values specified for SQL IN comparison');
}
else
{
// NOT IN () actually means everything so use a tautology
if ($negate)
{
return '1=1';
}
// IN () actually means nothing so use a contradiction
else
{
return '1=0';
}
}
}
if (!is_array($array))
{
$array = array($array);
}
if (sizeof($array) == 1)
{
@reset($array);
$var = current($array);
return $field . ($negate ? ' <> ' : ' = ') . $this->_sql_validate_value($var);
}
else
{
return $field . ($negate ? ' NOT IN ' : ' IN ') . '(' . implode(', ', array_map(array($this, '_sql_validate_value'), $array)) . ')';
}
}
/**
* Run more than one insert statement.
*
* @param string $table table name to run the statements on
* @param array &$sql_ary multi-dimensional array holding the statement data.
*
* @return bool false if no statements were executed.
* @access public
*/
function sql_multi_insert($table, &$sql_ary)
{
if (!sizeof($sql_ary))
{
return false;
}
if ($this->multi_insert)
{
$this->sql_query('INSERT INTO ' . $table . ' ' . $this->sql_build_array('MULTI_INSERT', $sql_ary));
}
else
{
foreach ($sql_ary as $ary)
{
if (!is_array($ary))
{
return false;
}
$this->sql_query('INSERT INTO ' . $table . ' ' . $this->sql_build_array('INSERT', $ary));
}
}
return true;
}
/**
* Function for validating values
* @access private
*/
function _sql_validate_value($var)
{
if (is_null($var))
{
return 'NULL';
}
else if (is_string($var))
{
return "'" . $this->sql_escape($var) . "'";
}
else
{
return (is_bool($var)) ? intval($var) : $var;
}
}
/**
* Build sql statement from array for select and select distinct statements
*
* Possible query values: SELECT, SELECT_DISTINCT
*/
function sql_build_query($query, $array)
{
$sql = '';
switch ($query)
{
case 'SELECT':
case 'SELECT_DISTINCT';
$sql = str_replace('_', ' ', $query) . ' ' . $array['SELECT'] . ' FROM ';
$table_array = array();
foreach ($array['FROM'] as $table_name => $alias)
{
if (is_array($alias))
{
foreach ($alias as $multi_alias)
{
$table_array[] = $table_name . ' ' . $multi_alias;
}
}
else
{
$table_array[] = $table_name . ' ' . $alias;
}
}
$sql .= $this->_sql_custom_build('FROM', implode(', ', $table_array));
if (!empty($array['LEFT_JOIN']))
{
foreach ($array['LEFT_JOIN'] as $join)
{
$sql .= ' LEFT JOIN ' . key($join['FROM']) . ' ' . current($join['FROM']) . ' ON (' . $join['ON'] . ')';
}
}
if (!empty($array['WHERE']))
{
$sql .= ' WHERE ' . $this->_sql_custom_build('WHERE', $array['WHERE']);
}
if (!empty($array['GROUP_BY']))
{
$sql .= ' GROUP BY ' . $array['GROUP_BY'];
}
if (!empty($array['ORDER_BY']))
{
$sql .= ' ORDER BY ' . $array['ORDER_BY'];
}
break;
}
return $sql;
}
/**
* display sql error page
*/
function sql_error($sql = '')
{
global $auth, $user, $config;
// Set var to retrieve errored status
$this->sql_error_triggered = true;
$this->sql_error_sql = $sql;
$error = $this->_sql_error();
if (!$this->return_on_error)
{
$message = '<u>SQL ERROR</u> [ ' . $this->sql_layer . ' ]<br /><br />' . $error['message'] . ' [' . $error['code'] . ']';
// Show complete SQL error and path to administrators only
// Additionally show complete error on installation or if extended debug mode is enabled
// The DEBUG_EXTRA constant is for development only!
if (isset($auth) && ($auth->acl_get('a_') || defined('IN_INSTALL') || defined('DEBUG_EXTRA')))
{
// Print out a nice backtrace...
$backtrace = get_backtrace();
$message .= ($sql) ? '<br /><br /><u>SQL</u><br /><br />' . htmlspecialchars($sql) : '';
$message .= ($backtrace) ? '<br /><br /><u>BACKTRACE</u><br />' . $backtrace : '';
$message .= '<br />';
}
else
{
// If error occurs in initiating the session we need to use a pre-defined language string
// This could happen if the connection could not be established for example (then we are not able to grab the default language)
if (!isset($user->lang['SQL_ERROR_OCCURRED']))
{
$message .= '<br /><br />An sql error occurred while fetching this page. Please contact an administrator if this problem persists.';
}
else
{
if (!empty($config['board_contact']))
{
$message .= '<br /><br />' . sprintf($user->lang['SQL_ERROR_OCCURRED'], '<a href="mailto:' . htmlspecialchars($config['board_contact']) . '">', '</a>');
}
else
{
$message .= '<br /><br />' . sprintf($user->lang['SQL_ERROR_OCCURRED'], '', '');
}
}
}
if ($this->transaction)
{
$this->sql_transaction('rollback');
}
if (strlen($message) > 1024)
{
// We need to define $msg_long_text here to circumvent text stripping.
global $msg_long_text;
$msg_long_text = $message;
trigger_error(false, E_USER_ERROR);
}
trigger_error($message, E_USER_ERROR);
}
return $error;
}
/**
* Explain queries
*/
function sql_report($mode, $query = '')
{
global $cache, $starttime, $phpbb_root_path, $user;
if (empty($_REQUEST['explain']))
{
return false;
}
if (!$query && $this->query_hold != '')
{
$query = $this->query_hold;
}
switch ($mode)
{
case 'display':
if (!empty($cache))
{
$cache->unload();
}
$this->sql_close();
$mtime = explode(' ', microtime());
$totaltime = $mtime[0] + $mtime[1] - $starttime;
echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta http-equiv="imagetoolbar" content="no" />
<title>SQL Report</title>
<link href="' . $phpbb_root_path . 'adm/style/admin.css" rel="stylesheet" type="text/css" media="screen" />
</head>
<body id="errorpage">
<div id="wrap">
<div id="page-header">
<a href="' . build_url('explain') . '">Return to previous page</a>
</div>
<div id="page-body">
<div id="acp">
<div class="panel">
<span class="corners-top"><span></span></span>
<div id="content">
<h1>SQL Report</h1>
<br />
<p><b>Page generated in ' . round($totaltime, 4) . " seconds with {$this->num_queries['normal']} queries" . (($this->num_queries['cached']) ? " + {$this->num_queries['cached']} " . (($this->num_queries['cached'] == 1) ? 'query' : 'queries') . ' returning data from cache' : '') . '</b></p>
<p>Time spent on ' . $this->sql_layer . ' queries: <b>' . round($this->sql_time, 5) . 's</b> | Time spent on PHP: <b>' . round($totaltime - $this->sql_time, 5) . 's</b></p>
<br /><br />
' . $this->sql_report . '
</div>
<span class="corners-bottom"><span></span></span>
</div>
</div>
</div>
<div id="page-footer">
Powered by phpBB &copy; ' . date('Y') . ' <a href="http://www.phpbb.com/">phpBB Group</a>
</div>
</div>
</body>
</html>';
exit;
break;
case 'stop':
$endtime = explode(' ', microtime());
$endtime = $endtime[0] + $endtime[1];
$this->sql_report .= '
<table cellspacing="1">
<thead>
<tr>
<th>Query #' . $this->num_queries['total'] . '</th>
</tr>
</thead>
<tbody>
<tr>
<td class="row3"><textarea style="font-family:\'Courier New\',monospace;width:99%" rows="5" cols="10">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query))) . '</textarea></td>
</tr>
</tbody>
</table>
' . $this->html_hold . '
<p style="text-align: center;">
';
if ($this->query_result)
{
if (preg_match('/^(UPDATE|DELETE|REPLACE)/', $query))
{
$this->sql_report .= 'Affected rows: <b>' . $this->sql_affectedrows($this->query_result) . '</b> | ';
}
$this->sql_report .= 'Before: ' . sprintf('%.5f', $this->curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed: <b>' . sprintf('%.5f', $endtime - $this->curtime) . 's</b>';
}
else
{
$error = $this->sql_error();
$this->sql_report .= '<b style="color: red">FAILED</b> - ' . $this->sql_layer . ' Error ' . $error['code'] . ': ' . htmlspecialchars($error['message']);
}
$this->sql_report .= '</p><br /><br />';
$this->sql_time += $endtime - $this->curtime;
break;
case 'start':
$this->query_hold = $query;
$this->html_hold = '';
$this->_sql_report($mode, $query);
$this->curtime = explode(' ', microtime());
$this->curtime = $this->curtime[0] + $this->curtime[1];
break;
case 'add_select_row':
$html_table = func_get_arg(2);
$row = func_get_arg(3);
if (!$html_table && sizeof($row))
{
$html_table = true;
$this->html_hold .= '<table cellspacing="1"><tr>';
foreach (array_keys($row) as $val)
{
$this->html_hold .= '<th>' . (($val) ? ucwords(str_replace('_', ' ', $val)) : '&nbsp;') . '</th>';
}
$this->html_hold .= '</tr>';
}
$this->html_hold .= '<tr>';
$class = 'row1';
foreach (array_values($row) as $val)
{
$class = ($class == 'row1') ? 'row2' : 'row1';
$this->html_hold .= '<td class="' . $class . '">' . (($val) ? $val : '&nbsp;') . '</td>';
}
$this->html_hold .= '</tr>';
return $html_table;
break;
case 'fromcache':
$this->_sql_report($mode, $query);
break;
case 'record_fromcache':
$endtime = func_get_arg(2);
$splittime = func_get_arg(3);
$time_cache = $endtime - $this->curtime;
$time_db = $splittime - $endtime;
$color = ($time_db > $time_cache) ? 'green' : 'red';
$this->sql_report .= '<table cellspacing="1"><thead><tr><th>Query results obtained from the cache</th></tr></thead><tbody><tr>';
$this->sql_report .= '<td class="row3"><textarea style="font-family:\'Courier New\',monospace;width:99%" rows="5" cols="10">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query))) . '</textarea></td></tr></tbody></table>';
$this->sql_report .= '<p style="text-align: center;">';
$this->sql_report .= 'Before: ' . sprintf('%.5f', $this->curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed [cache]: <b style="color: ' . $color . '">' . sprintf('%.5f', ($time_cache)) . 's</b> | Elapsed [db]: <b>' . sprintf('%.5f', $time_db) . 's</b></p><br /><br />';
// Pad the start time to not interfere with page timing
$starttime += $time_db;
break;
default:
$this->_sql_report($mode, $query);
break;
}
return true;
}
}
/**
*/
if (!defined('IN_PHPBB'))
{
exit;
}
/**
* This variable holds the class name to use later
*/
$sql_db = 'dbal_' . $dbms;
?>