Commit 769b2d38 by Scott

Coding style (qa-db.php)

parent b507ca2f
...@@ -20,414 +20,440 @@ ...@@ -20,414 +20,440 @@
More about this license: http://www.question2answer.org/license.php More about this license: http://www.question2answer.org/license.php
*/ */
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
header('Location: ../'); header('Location: ../');
exit; exit;
} }
/**
* Indicates to the Q2A database layer that database connections are permitted fro this point forwards
* (before this point, some plugins may not have had a chance to override some database access functions).
*/
function qa_db_allow_connect()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
global $qa_db_allow_connect;
$qa_db_allow_connect=true;
}
/**
* Connect to the Q2A database, select the right database, optionally install the $failhandler (and call it if necessary).
* Uses mysqli as of Q2A 1.7.
*/
function qa_db_connect($failhandler=null)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
global $qa_db_connection, $qa_db_fail_handler, $qa_db_allow_connect;
if (!$qa_db_allow_connect)
qa_fatal_error('It appears that a plugin is trying to access the database, but this is not allowed until Q2A initialization is complete.');
if (isset($failhandler)) /**
$qa_db_fail_handler = $failhandler; // set this even if connection already opened * Indicates to the Q2A database layer that database connections are permitted fro this point forwards
* (before this point, some plugins may not have had a chance to override some database access functions).
*/
function qa_db_allow_connect()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
if ($qa_db_connection instanceof mysqli) global $qa_db_allow_connect;
return;
$host = QA_FINAL_MYSQL_HOSTNAME; $qa_db_allow_connect = true;
$port = null; }
if (defined('QA_FINAL_WORDPRESS_INTEGRATE_PATH')) {
// Wordpress allows setting port inside DB_HOST constant, like 127.0.0.1:3306
$host_and_port = explode(':', $host);
if (count($host_and_port) >= 2) {
$host = $host_and_port[0];
$port = $host_and_port[1];
}
} elseif (defined('QA_FINAL_MYSQL_PORT')) {
$port = QA_FINAL_MYSQL_PORT;
}
if (QA_PERSISTENT_CONN_DB) /**
$host = 'p:'.$host; * Connect to the Q2A database, select the right database, optionally install the $failhandler (and call it if necessary).
* Uses mysqli as of Q2A 1.7.
* @param null $failhandler
* @return mixed|void
*/
function qa_db_connect($failhandler = null)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
// in mysqli we connect and select database in constructor global $qa_db_connection, $qa_db_fail_handler, $qa_db_allow_connect;
if ($port !== null)
$db = new mysqli($host, QA_FINAL_MYSQL_USERNAME, QA_FINAL_MYSQL_PASSWORD, QA_FINAL_MYSQL_DATABASE, $port);
else
$db = new mysqli($host, QA_FINAL_MYSQL_USERNAME, QA_FINAL_MYSQL_PASSWORD, QA_FINAL_MYSQL_DATABASE);
// must use procedural `mysqli_connect_error` here prior to 5.2.9 if (!$qa_db_allow_connect)
$conn_error = mysqli_connect_error(); qa_fatal_error('It appears that a plugin is trying to access the database, but this is not allowed until Q2A initialization is complete.');
if ($conn_error)
qa_db_fail_error('connect', $db->connect_errno, $conn_error);
// From Q2A 1.5, we explicitly set the character encoding of the MySQL connection, instead of using lots of "SELECT BINARY col"-style queries. if (isset($failhandler))
// Testing showed that overhead is minimal, so this seems worth trading off against the benefit of more straightforward queries, especially $qa_db_fail_handler = $failhandler; // set this even if connection already opened
// for plugin developers.
if (!$db->set_charset('utf8'))
qa_db_fail_error('set_charset', $db->errno, $db->error);
qa_report_process_stage('db_connected'); if ($qa_db_connection instanceof mysqli)
return;
$qa_db_connection=$db;
}
$host = QA_FINAL_MYSQL_HOSTNAME;
$port = null;
/** if (defined('QA_FINAL_WORDPRESS_INTEGRATE_PATH')) {
* If a DB error occurs, call the installed fail handler (if any) otherwise report error and exit immediately. // Wordpress allows setting port inside DB_HOST constant, like 127.0.0.1:3306
*/ $host_and_port = explode(':', $host);
function qa_db_fail_error($type, $errno=null, $error=null, $query=null) if (count($host_and_port) >= 2) {
{ $host = $host_and_port[0];
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } $port = $host_and_port[1];
global $qa_db_fail_handler;
@error_log('PHP Question2Answer MySQL ' . $type . ' error ' . $errno . ': ' . $error . (isset($query) ? (' - Query: ' . $query) : ''));
if (function_exists($qa_db_fail_handler))
$qa_db_fail_handler($type, $errno, $error, $query);
else {
echo sprintf(
'<hr><div style="color: red">Database %s<p>%s</p><code>%s</code></div>',
htmlspecialchars($type . ' error ' . $errno), nl2br(htmlspecialchars($error)), nl2br(htmlspecialchars($query))
);
qa_exit('error');
} }
} elseif (defined('QA_FINAL_MYSQL_PORT')) {
$port = QA_FINAL_MYSQL_PORT;
} }
if (QA_PERSISTENT_CONN_DB)
/** $host = 'p:' . $host;
* Return the current connection to the Q2A database, connecting if necessary and $connect is true.
*/ // in mysqli we connect and select database in constructor
function qa_db_connection($connect=true) if ($port !== null)
{ $db = new mysqli($host, QA_FINAL_MYSQL_USERNAME, QA_FINAL_MYSQL_PASSWORD, QA_FINAL_MYSQL_DATABASE, $port);
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } else
$db = new mysqli($host, QA_FINAL_MYSQL_USERNAME, QA_FINAL_MYSQL_PASSWORD, QA_FINAL_MYSQL_DATABASE);
global $qa_db_connection;
// must use procedural `mysqli_connect_error` here prior to 5.2.9
if ($connect && !($qa_db_connection instanceof mysqli)) { $conn_error = mysqli_connect_error();
qa_db_connect(); if ($conn_error)
qa_db_fail_error('connect', $db->connect_errno, $conn_error);
if (!($qa_db_connection instanceof mysqli))
qa_fatal_error('Failed to connect to database'); // From Q2A 1.5, we explicitly set the character encoding of the MySQL connection, instead of using lots of "SELECT BINARY col"-style queries.
} // Testing showed that overhead is minimal, so this seems worth trading off against the benefit of more straightforward queries, especially
// for plugin developers.
return $qa_db_connection; if (!$db->set_charset('utf8'))
qa_db_fail_error('set_charset', $db->errno, $db->error);
qa_report_process_stage('db_connected');
$qa_db_connection = $db;
}
/**
* If a DB error occurs, call the installed fail handler (if any) otherwise report error and exit immediately.
* @param $type
* @param int $errno
* @param string $error
* @param string $query
* @return mixed
*/
function qa_db_fail_error($type, $errno = null, $error = null, $query = null)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
global $qa_db_fail_handler;
@error_log('PHP Question2Answer MySQL ' . $type . ' error ' . $errno . ': ' . $error . (isset($query) ? (' - Query: ' . $query) : ''));
if (function_exists($qa_db_fail_handler))
$qa_db_fail_handler($type, $errno, $error, $query);
else {
echo sprintf(
'<hr><div style="color: red">Database %s<p>%s</p><code>%s</code></div>',
htmlspecialchars($type . ' error ' . $errno), nl2br(htmlspecialchars($error)), nl2br(htmlspecialchars($query))
);
qa_exit('error');
} }
}
/** /**
* Disconnect from the Q2A database. * Return the current connection to the Q2A database, connecting if necessary and $connect is true.
*/ * @param bool $connect
function qa_db_disconnect() * @return mixed
{ */
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } function qa_db_connection($connect = true)
{
global $qa_db_connection; if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
if ($qa_db_connection instanceof mysqli) { global $qa_db_connection;
qa_report_process_stage('db_disconnect');
if (!QA_PERSISTENT_CONN_DB) { if ($connect && !($qa_db_connection instanceof mysqli)) {
if (!$qa_db_connection->close()) qa_db_connect();
qa_fatal_error('Database disconnect failed');
}
$qa_db_connection=null; if (!($qa_db_connection instanceof mysqli))
} qa_fatal_error('Failed to connect to database');
} }
return $qa_db_connection;
}
/**
* Run the raw $query, call the global failure handler if necessary, otherwise return the result resource.
* If appropriate, also track the resources used by database queries, and the queries themselves, for performance debugging.
*/
function qa_db_query_raw($query)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
if (QA_DEBUG_PERFORMANCE) { /**
global $qa_usage; * Disconnect from the Q2A database.
*/
function qa_db_disconnect()
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
// time the query global $qa_db_connection;
$oldtime = array_sum(explode(' ', microtime()));
$result = qa_db_query_execute($query);
$usedtime = array_sum(explode(' ', microtime())) - $oldtime;
// fetch counts if ($qa_db_connection instanceof mysqli) {
$gotrows = $gotcolumns = null; qa_report_process_stage('db_disconnect');
if ($result instanceof mysqli_result) {
$gotrows = $result->num_rows;
$gotcolumns = $result->field_count;
}
$qa_usage->logDatabaseQuery($query, $usedtime, $gotrows, $gotcolumns);
}
else
$result = qa_db_query_execute($query);
// @error_log('Question2Answer MySQL query: '.$query);
if ($result === false) { if (!QA_PERSISTENT_CONN_DB) {
$db = qa_db_connection(); if (!$qa_db_connection->close())
qa_db_fail_error('query', $db->errno, $db->error, $query); qa_fatal_error('Database disconnect failed');
} }
return $result; $qa_db_connection = null;
} }
}
/**
* Lower-level function to execute a query, which automatically retries if there is a MySQL deadlock error. /**
*/ * Run the raw $query, call the global failure handler if necessary, otherwise return the result resource.
function qa_db_query_execute($query) * If appropriate, also track the resources used by database queries, and the queries themselves, for performance debugging.
{ * @param $query
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } * @return mixed
*/
$db = qa_db_connection(); function qa_db_query_raw($query)
{
for ($attempt = 0; $attempt < 100; $attempt++) { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
$result = $db->query($query);
if (QA_DEBUG_PERFORMANCE) {
if ($result === false && $db->errno == 1213) global $qa_usage;
usleep(10000); // deal with InnoDB deadlock errors by waiting 0.01s then retrying
else // time the query
break; $oldtime = array_sum(explode(' ', microtime()));
$result = qa_db_query_execute($query);
$usedtime = array_sum(explode(' ', microtime())) - $oldtime;
// fetch counts
$gotrows = $gotcolumns = null;
if ($result instanceof mysqli_result) {
$gotrows = $result->num_rows;
$gotcolumns = $result->field_count;
} }
return $result; $qa_usage->logDatabaseQuery($query, $usedtime, $gotrows, $gotcolumns);
} } else
$result = qa_db_query_execute($query);
/** // @error_log('Question2Answer MySQL query: '.$query);
* Return $string escaped for use in queries to the Q2A database (to which a connection must have been made).
*/
function qa_db_escape_string($string)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
if ($result === false) {
$db = qa_db_connection(); $db = qa_db_connection();
return $db->real_escape_string($string); qa_db_fail_error('query', $db->errno, $db->error, $query);
} }
return $result;
}
/**
* Return $argument escaped for MySQL. Add quotes around it if $alwaysquote is true or it's not numeric.
* If $argument is an array, return a comma-separated list of escaped elements, with or without $arraybrackets.
*/
function qa_db_argument_to_mysql($argument, $alwaysquote, $arraybrackets=false)
{
if (is_array($argument)) {
$parts=array();
foreach ($argument as $subargument) /**
$parts[] = qa_db_argument_to_mysql($subargument, $alwaysquote, true); * Lower-level function to execute a query, which automatically retries if there is a MySQL deadlock error.
* @param $query
* @return mixed
*/
function qa_db_query_execute($query)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
if ($arraybrackets) $db = qa_db_connection();
$result = '('.implode(',', $parts).')';
else
$result = implode(',', $parts);
} for ($attempt = 0; $attempt < 100; $attempt++) {
elseif (isset($argument)) { $result = $db->query($query);
if ($alwaysquote || !is_numeric($argument))
$result = "'".qa_db_escape_string($argument)."'";
else
$result = qa_db_escape_string($argument);
}
else
$result = 'NULL';
return $result; if ($result === false && $db->errno == 1213)
usleep(10000); // deal with InnoDB deadlock errors by waiting 0.01s then retrying
else
break;
} }
return $result;
}
/**
* Return $string escaped for use in queries to the Q2A database (to which a connection must have been made).
* @param $string
* @return mixed
*/
function qa_db_escape_string($string)
{
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
$db = qa_db_connection();
return $db->real_escape_string($string);
}
/**
* Return $argument escaped for MySQL. Add quotes around it if $alwaysquote is true or it's not numeric.
* If $argument is an array, return a comma-separated list of escaped elements, with or without $arraybrackets.
* @param $argument
* @param $alwaysquote
* @param bool $arraybrackets
* @return mixed|string
*/
function qa_db_argument_to_mysql($argument, $alwaysquote, $arraybrackets = false)
{
if (is_array($argument)) {
$parts = array();
foreach ($argument as $subargument)
$parts[] = qa_db_argument_to_mysql($subargument, $alwaysquote, true);
if ($arraybrackets)
$result = '(' . implode(',', $parts) . ')';
else
$result = implode(',', $parts);
/** } elseif (isset($argument)) {
* Return the full name (with prefix) of database table $rawname, usually if it used after a ^ symbol. if ($alwaysquote || !is_numeric($argument))
*/ $result = "'" . qa_db_escape_string($argument) . "'";
function qa_db_add_table_prefix($rawname) else
{ $result = qa_db_escape_string($argument);
if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } } else
$result = 'NULL';
$prefix = QA_MYSQL_TABLE_PREFIX;
return $result;
if (defined('QA_MYSQL_USERS_PREFIX')) { }
switch (strtolower($rawname)) {
case 'users':
case 'userlogins': /**
case 'userprofile': * Return the full name (with prefix) of database table $rawname, usually if it used after a ^ symbol.
case 'userfields': * @param $rawname
case 'messages': * @return string
case 'cookies': */
case 'blobs': function qa_db_add_table_prefix($rawname)
case 'cache': {
case 'userlogins_ibfk_1': // also special cases for constraint names if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
case 'userprofile_ibfk_1':
$prefix = QA_MYSQL_USERS_PREFIX; $prefix = QA_MYSQL_TABLE_PREFIX;
break;
} if (defined('QA_MYSQL_USERS_PREFIX')) {
switch (strtolower($rawname)) {
case 'users':
case 'userlogins':
case 'userprofile':
case 'userfields':
case 'messages':
case 'cookies':
case 'blobs':
case 'cache':
case 'userlogins_ibfk_1': // also special cases for constraint names
case 'userprofile_ibfk_1':
$prefix = QA_MYSQL_USERS_PREFIX;
break;
} }
return $prefix.$rawname;
} }
return $prefix . $rawname;
/** }
* Callback function to add table prefixes, as used in qa_db_apply_sub().
*/
function qa_db_prefix_callback($matches) /**
{ * Callback function to add table prefixes, as used in qa_db_apply_sub().
return qa_db_add_table_prefix($matches[1]); * @param $matches
} * @return string
*/
function qa_db_prefix_callback($matches)
/** {
* Substitute ^, $ and # symbols in $query. ^ symbols are replaced with the table prefix set in qa-config.php. return qa_db_add_table_prefix($matches[1]);
* $ and # symbols are replaced in order by the corresponding element in $arguments (if the element is an array, }
* it is converted recursively into comma-separated list). Each element in $arguments is escaped.
* $ is replaced by the argument in quotes (even if it's a number), # only adds quotes if the argument is non-numeric.
* It's important to use $ when matching a textual column since MySQL won't use indexes to compare text against numbers. /**
*/ * Substitute ^, $ and # symbols in $query. ^ symbols are replaced with the table prefix set in qa-config.php.
function qa_db_apply_sub($query, $arguments) * $ and # symbols are replaced in order by the corresponding element in $arguments (if the element is an array,
{ * it is converted recursively into comma-separated list). Each element in $arguments is escaped.
$query = preg_replace_callback('/\^([A-Za-z_0-9]+)/', 'qa_db_prefix_callback', $query); * $ is replaced by the argument in quotes (even if it's a number), # only adds quotes if the argument is non-numeric.
* It's important to use $ when matching a textual column since MySQL won't use indexes to compare text against numbers.
if (!is_array($arguments)) * @param $query
return $query; * @param $arguments
* @return mixed
$countargs = count($arguments); */
$offset = 0; function qa_db_apply_sub($query, $arguments)
{
for ($argument = 0; $argument < $countargs; $argument++) { $query = preg_replace_callback('/\^([A-Za-z_0-9]+)/', 'qa_db_prefix_callback', $query);
$stringpos = strpos($query, '$', $offset);
$numberpos = strpos($query, '#', $offset); if (!is_array($arguments))
if ($stringpos === false || ($numberpos !== false && $numberpos < $stringpos)) {
$alwaysquote = false;
$position = $numberpos;
}
else {
$alwaysquote = true;
$position = $stringpos;
}
if (!is_numeric($position))
qa_fatal_error('Insufficient parameters in query: '.$query);
$value = qa_db_argument_to_mysql($arguments[$argument], $alwaysquote);
$query = substr_replace($query, $value, $position, 1);
$offset = $position + strlen($value); // allows inserting strings which contain #/$ character
}
return $query; return $query;
}
/**
* Run $query after substituting ^, # and $ symbols, and return the result resource (or call fail handler).
*/
function qa_db_query_sub($query) // arguments for substitution retrieved using func_get_args()
{
$funcargs=func_get_args();
return qa_db_query_raw(qa_db_apply_sub($query, array_slice($funcargs, 1)));
}
/**
* Return the number of rows in $result. (Simple wrapper for mysqli_result::num_rows.)
*/
function qa_db_num_rows($result)
{
if ($result instanceof mysqli_result)
return $result->num_rows;
return 0; $countargs = count($arguments);
} $offset = 0;
/**
* Return the value of the auto-increment column for the last inserted row.
*/
function qa_db_last_insert_id()
{
$db = qa_db_connection();
return $db->insert_id;
}
/**
* Return the number of rows affected by the last query.
*/
function qa_db_affected_rows()
{
$db = qa_db_connection();
return $db->affected_rows;
}
for ($argument = 0; $argument < $countargs; $argument++) {
$stringpos = strpos($query, '$', $offset);
$numberpos = strpos($query, '#', $offset);
/** if ($stringpos === false || ($numberpos !== false && $numberpos < $stringpos)) {
* For the previous INSERT ... ON DUPLICATE KEY UPDATE query, return whether an insert operation took place. $alwaysquote = false;
*/ $position = $numberpos;
function qa_db_insert_on_duplicate_inserted() } else {
{ $alwaysquote = true;
return (qa_db_affected_rows() == 1); $position = $stringpos;
} }
if (!is_numeric($position))
qa_fatal_error('Insufficient parameters in query: ' . $query);
/** $value = qa_db_argument_to_mysql($arguments[$argument], $alwaysquote);
* Return a random integer (as a string) for use in a BIGINT column. $query = substr_replace($query, $value, $position, 1);
* Actual limit is 18,446,744,073,709,551,615 - we aim for 18,446,743,999,999,999,999. $offset = $position + strlen($value); // allows inserting strings which contain #/$ character
*/
function qa_db_random_bigint()
{
return sprintf('%d%06d%06d', mt_rand(1, 18446743), mt_rand(0, 999999), mt_rand(0, 999999));
} }
return $query;
/** }
* Return an array of the names of all tables in the Q2A database, converted to lower case.
* No longer used by Q2A and shouldn't be needed.
*/ /**
function qa_db_list_tables_lc() * Run $query after substituting ^, # and $ symbols, and return the result resource (or call fail handler).
{ * @param $query
return array_map('strtolower', qa_db_list_tables()); * @return mixed
} */
function qa_db_query_sub($query) // arguments for substitution retrieved using func_get_args()
{
/** $funcargs = func_get_args();
* Return an array of the names of all tables in the Q2A database.
*/ return qa_db_query_raw(qa_db_apply_sub($query, array_slice($funcargs, 1)));
function qa_db_list_tables() }
{
return qa_db_read_all_values(qa_db_query_raw('SHOW TABLES'));
} /**
* Return the number of rows in $result. (Simple wrapper for mysqli_result::num_rows.)
* @param $result
* @return int
*/
function qa_db_num_rows($result)
{
if ($result instanceof mysqli_result)
return $result->num_rows;
return 0;
}
/**
* Return the value of the auto-increment column for the last inserted row.
*/
function qa_db_last_insert_id()
{
$db = qa_db_connection();
return $db->insert_id;
}
/**
* Return the number of rows affected by the last query.
*/
function qa_db_affected_rows()
{
$db = qa_db_connection();
return $db->affected_rows;
}
/**
* For the previous INSERT ... ON DUPLICATE KEY UPDATE query, return whether an insert operation took place.
*/
function qa_db_insert_on_duplicate_inserted()
{
return (qa_db_affected_rows() == 1);
}
/**
* Return a random integer (as a string) for use in a BIGINT column.
* Actual limit is 18,446,744,073,709,551,615 - we aim for 18,446,743,999,999,999,999.
*/
function qa_db_random_bigint()
{
return sprintf('%d%06d%06d', mt_rand(1, 18446743), mt_rand(0, 999999), mt_rand(0, 999999));
}
/**
* Return an array of the names of all tables in the Q2A database, converted to lower case.
* No longer used by Q2A and shouldn't be needed.
*/
function qa_db_list_tables_lc()
{
return array_map('strtolower', qa_db_list_tables());
}
/**
* Return an array of the names of all tables in the Q2A database.
*/
function qa_db_list_tables()
{
return qa_db_read_all_values(qa_db_query_raw('SHOW TABLES'));
}
/* /*
...@@ -476,300 +502,319 @@ ...@@ -476,300 +502,319 @@
*/ */
/** /**
* Return the data specified by a single $selectspec - see long comment above. * Return the data specified by a single $selectspec - see long comment above.
*/ * @param $selectspec
function qa_db_single_select($selectspec) * @return array|mixed
{ */
// check for cached results function qa_db_single_select($selectspec)
if (isset($selectspec['caching'])) { {
$cacheHandler = Q2A_Storage_CacheManager::getInstance(); // check for cached results
$cacheKey = 'query:' . $selectspec['caching']['key']; if (isset($selectspec['caching'])) {
$cacheHandler = Q2A_Storage_CacheManager::getInstance();
if ($cacheHandler->isEnabled()) { $cacheKey = 'query:' . $selectspec['caching']['key'];
$queryData = $cacheHandler->get($cacheKey);
if ($queryData !== null) if ($cacheHandler->isEnabled()) {
return $queryData; $queryData = $cacheHandler->get($cacheKey);
} if ($queryData !== null)
return $queryData;
} }
}
$query = 'SELECT '; $query = 'SELECT ';
foreach ($selectspec['columns'] as $columnas => $columnfrom) foreach ($selectspec['columns'] as $columnas => $columnfrom)
$query .= $columnfrom . (is_int($columnas) ? '' : (' AS '.$columnas)) . ', '; $query .= $columnfrom . (is_int($columnas) ? '' : (' AS ' . $columnas)) . ', ';
$results = qa_db_read_all_assoc(qa_db_query_raw(qa_db_apply_sub( $results = qa_db_read_all_assoc(qa_db_query_raw(qa_db_apply_sub(
substr($query, 0, -2).(strlen(@$selectspec['source']) ? (' FROM '.$selectspec['source']) : ''), substr($query, 0, -2) . (strlen(@$selectspec['source']) ? (' FROM ' . $selectspec['source']) : ''),
@$selectspec['arguments']) @$selectspec['arguments'])
), @$selectspec['arraykey']); // arrayvalue is applied in qa_db_post_select() ), @$selectspec['arraykey']); // arrayvalue is applied in qa_db_post_select()
qa_db_post_select($results, $selectspec); // post-processing qa_db_post_select($results, $selectspec); // post-processing
// save cached results // save cached results
if (isset($selectspec['caching'])) { if (isset($selectspec['caching'])) {
if ($cacheHandler->isEnabled()) { if ($cacheHandler->isEnabled()) {
$cacheHandler->set($cacheKey, $results, $selectspec['caching']['ttl']); $cacheHandler->set($cacheKey, $results, $selectspec['caching']['ttl']);
}
} }
return $results;
} }
return $results;
}
/**
* Return the data specified by each element of $selectspecs, where the keys of the
* returned array match the keys of the supplied $selectspecs array. See long comment above.
*/
function qa_db_multi_select($selectspecs)
{
if (!count($selectspecs))
return array();
// Perform simple queries if the database is local or there are only 0 or 1 selectspecs /**
* Return the data specified by each element of $selectspecs, where the keys of the
* returned array match the keys of the supplied $selectspecs array. See long comment above.
* @param array $selectspecs
* @return array
*/
function qa_db_multi_select($selectspecs)
{
if (!count($selectspecs))
return array();
if (!QA_OPTIMIZE_DISTANT_DB || (count($selectspecs)<=1)) { // Perform simple queries if the database is local or there are only 0 or 1 selectspecs
$outresults=array();
foreach ($selectspecs as $selectkey => $selectspec) if (!QA_OPTIMIZE_DISTANT_DB || (count($selectspecs) <= 1)) {
$outresults[$selectkey]=qa_db_single_select($selectspec); $outresults = array();
return $outresults; foreach ($selectspecs as $selectkey => $selectspec)
} $outresults[$selectkey] = qa_db_single_select($selectspec);
// Otherwise, parse columns for each spec to deal with columns without an 'AS' specification
foreach ($selectspecs as $selectkey => $selectspec) { return $outresults;
$selectspecs[$selectkey]['outcolumns']=array(); }
$selectspecs[$selectkey]['autocolumn']=array();
foreach ($selectspec['columns'] as $columnas => $columnfrom) { // Otherwise, parse columns for each spec to deal with columns without an 'AS' specification
if (is_int($columnas)) {
$periodpos=strpos($columnfrom, '.');
$columnas=is_numeric($periodpos) ? substr($columnfrom, $periodpos+1) : $columnfrom;
$selectspecs[$selectkey]['autocolumn'][$columnas]=true;
}
if (isset($selectspecs[$selectkey]['outcolumns'][$columnas])) foreach ($selectspecs as $selectkey => $selectspec) {
qa_fatal_error('Duplicate column name in qa_db_multi_select()'); $selectspecs[$selectkey]['outcolumns'] = array();
$selectspecs[$selectkey]['autocolumn'] = array();
$selectspecs[$selectkey]['outcolumns'][$columnas]=$columnfrom; foreach ($selectspec['columns'] as $columnas => $columnfrom) {
if (is_int($columnas)) {
$periodpos = strpos($columnfrom, '.');
$columnas = is_numeric($periodpos) ? substr($columnfrom, $periodpos + 1) : $columnfrom;
$selectspecs[$selectkey]['autocolumn'][$columnas] = true;
} }
if (isset($selectspec['arraykey'])) if (isset($selectspecs[$selectkey]['outcolumns'][$columnas]))
if (!isset($selectspecs[$selectkey]['outcolumns'][$selectspec['arraykey']])) qa_fatal_error('Duplicate column name in qa_db_multi_select()');
qa_fatal_error('Used arraykey not in columns in qa_db_multi_select()');
if (isset($selectspec['arrayvalue'])) $selectspecs[$selectkey]['outcolumns'][$columnas] = $columnfrom;
if (!isset($selectspecs[$selectkey]['outcolumns'][$selectspec['arrayvalue']]))
qa_fatal_error('Used arrayvalue not in columns in qa_db_multi_select()');
} }
// Work out the full list of columns used if (isset($selectspec['arraykey']))
if (!isset($selectspecs[$selectkey]['outcolumns'][$selectspec['arraykey']]))
$outcolumns=array(); qa_fatal_error('Used arraykey not in columns in qa_db_multi_select()');
foreach ($selectspecs as $selectspec)
$outcolumns=array_unique(array_merge($outcolumns, array_keys($selectspec['outcolumns'])));
// Build the query based on this full list
$query=''; if (isset($selectspec['arrayvalue']))
foreach ($selectspecs as $selectkey => $selectspec) { if (!isset($selectspecs[$selectkey]['outcolumns'][$selectspec['arrayvalue']]))
$subquery="(SELECT '".qa_db_escape_string($selectkey)."'".(empty($query) ? ' AS selectkey' : ''); qa_fatal_error('Used arrayvalue not in columns in qa_db_multi_select()');
}
foreach ($outcolumns as $columnas) {
$subquery.=', '.(isset($selectspec['outcolumns'][$columnas]) ? $selectspec['outcolumns'][$columnas] : 'NULL');
if (empty($query) && !isset($selectspec['autocolumn'][$columnas]))
$subquery.=' AS '.$columnas;
}
if (strlen(@$selectspec['source']))
$subquery.=' FROM '.$selectspec['source'];
$subquery.=')';
if (strlen($query))
$query.=' UNION ALL ';
$query.=qa_db_apply_sub($subquery, @$selectspec['arguments']);
}
// Perform query and extract results // Work out the full list of columns used
$rawresults=qa_db_read_all_assoc(qa_db_query_raw($query)); $outcolumns = array();
foreach ($selectspecs as $selectspec)
$outcolumns = array_unique(array_merge($outcolumns, array_keys($selectspec['outcolumns'])));
$outresults=array(); // Build the query based on this full list
foreach ($selectspecs as $selectkey => $selectspec)
$outresults[$selectkey]=array();
foreach ($rawresults as $rawresult) { $query = '';
$selectkey=$rawresult['selectkey']; foreach ($selectspecs as $selectkey => $selectspec) {
$selectspec=$selectspecs[$selectkey]; $subquery = "(SELECT '" . qa_db_escape_string($selectkey) . "'" . (empty($query) ? ' AS selectkey' : '');
$keepresult=array(); foreach ($outcolumns as $columnas) {
foreach ($selectspec['outcolumns'] as $columnas => $columnfrom) $subquery .= ', ' . (isset($selectspec['outcolumns'][$columnas]) ? $selectspec['outcolumns'][$columnas] : 'NULL');
$keepresult[$columnas]=$rawresult[$columnas];
if (isset($selectspec['arraykey'])) if (empty($query) && !isset($selectspec['autocolumn'][$columnas]))
$outresults[$selectkey][$keepresult[$selectspec['arraykey']]]=$keepresult; $subquery .= ' AS ' . $columnas;
else
$outresults[$selectkey][]=$keepresult;
} }
// Post-processing to apply various stuff include sorting request, since we can't rely on ORDER BY due to UNION if (strlen(@$selectspec['source']))
$subquery .= ' FROM ' . $selectspec['source'];
foreach ($selectspecs as $selectkey => $selectspec) $subquery .= ')';
qa_db_post_select($outresults[$selectkey], $selectspec);
// Return results if (strlen($query))
$query .= ' UNION ALL ';
return $outresults; $query .= qa_db_apply_sub($subquery, @$selectspec['arguments']);
} }
// Perform query and extract results
/** $rawresults = qa_db_read_all_assoc(qa_db_query_raw($query));
* Post-process $outresult according to $selectspec, applying 'sortasc', 'sortdesc', 'arrayvalue' and 'single'.
*/
function qa_db_post_select(&$outresult, $selectspec)
{
// PHP's sorting algorithm is not 'stable', so we use '_order_' element to keep stability.
// By contrast, MySQL's ORDER BY does seem to give the results in a reliable order.
if (isset($selectspec['sortasc'])) {
require_once QA_INCLUDE_DIR.'util/sort.php';
$index=0;
foreach ($outresult as $key => $value)
$outresult[$key]['_order_']=$index++;
qa_sort_by($outresult, $selectspec['sortasc'], '_order_');
} elseif (isset($selectspec['sortdesc'])) {
require_once QA_INCLUDE_DIR.'util/sort.php';
if (isset($selectspec['sortdesc_2'])) $outresults = array();
qa_sort_by($outresult, $selectspec['sortdesc'], $selectspec['sortdesc_2']); foreach ($selectspecs as $selectkey => $selectspec)
$outresults[$selectkey] = array();
else { foreach ($rawresults as $rawresult) {
$index=count($outresult); $selectkey = $rawresult['selectkey'];
foreach ($outresult as $key => $value) $selectspec = $selectspecs[$selectkey];
$outresult[$key]['_order_']=$index--;
qa_sort_by($outresult, $selectspec['sortdesc'], '_order_'); $keepresult = array();
} foreach ($selectspec['outcolumns'] as $columnas => $columnfrom)
$keepresult[$columnas] = $rawresult[$columnas];
$outresult=array_reverse($outresult, true);
}
if (isset($selectspec['arrayvalue'])) if (isset($selectspec['arraykey']))
foreach ($outresult as $key => $value) $outresults[$selectkey][$keepresult[$selectspec['arraykey']]] = $keepresult;
$outresult[$key]=$value[$selectspec['arrayvalue']]; else
$outresults[$selectkey][] = $keepresult;
if (@$selectspec['single'])
$outresult=count($outresult) ? reset($outresult) : null;
} }
// Post-processing to apply various stuff include sorting request, since we can't rely on ORDER BY due to UNION
/** foreach ($selectspecs as $selectkey => $selectspec)
* Return the full results from the $result resource as an array. The key of each element in the returned array qa_db_post_select($outresults[$selectkey], $selectspec);
* is from column $key if specified, otherwise it's integer. The value of each element in the returned array
* is from column $value if specified, otherwise it's a named array of all columns, given an array of arrays.
*/
function qa_db_read_all_assoc($result, $key=null, $value=null)
{
if (!($result instanceof mysqli_result))
qa_fatal_error('Reading all assoc from invalid result');
$assocs = array(); // Return results
while ($assoc = $result->fetch_assoc()) { return $outresults;
if (isset($key)) }
$assocs[$assoc[$key]] = isset($value) ? $assoc[$value] : $assoc;
else
$assocs[] = isset($value) ? $assoc[$value] : $assoc;
}
return $assocs;
}
/**
* Post-process $outresult according to $selectspec, applying 'sortasc', 'sortdesc', 'arrayvalue' and 'single'.
* @param array $outresult
* @param array $selectspec
*/
function qa_db_post_select(&$outresult, $selectspec)
{
// PHP's sorting algorithm is not 'stable', so we use '_order_' element to keep stability.
// By contrast, MySQL's ORDER BY does seem to give the results in a reliable order.
/** if (isset($selectspec['sortasc'])) {
* Return the first row from the $result resource as an array of [column name] => [column value]. require_once QA_INCLUDE_DIR . 'util/sort.php';
* If there's no first row, throw a fatal error unless $allowempty is true.
*/
function qa_db_read_one_assoc($result, $allowempty=false)
{
if (!($result instanceof mysqli_result))
qa_fatal_error('Reading one assoc from invalid result');
$assoc = $result->fetch_assoc(); $index = 0;
foreach ($outresult as $key => $value)
$outresult[$key]['_order_'] = $index++;
if (is_array($assoc)) qa_sort_by($outresult, $selectspec['sortasc'], '_order_');
return $assoc;
if ($allowempty)
return null;
else
qa_fatal_error('Reading one assoc from empty results');
}
} elseif (isset($selectspec['sortdesc'])) {
require_once QA_INCLUDE_DIR . 'util/sort.php';
/** if (isset($selectspec['sortdesc_2']))
* Return a numbered array containing the first (and presumably only) column from the $result resource. qa_sort_by($outresult, $selectspec['sortdesc'], $selectspec['sortdesc_2']);
*/
function qa_db_read_all_values($result)
{
if (!($result instanceof mysqli_result))
qa_fatal_error('Reading column from invalid result');
$output = array(); else {
$index = count($outresult);
foreach ($outresult as $key => $value)
$outresult[$key]['_order_'] = $index--;
while ($row = $result->fetch_row()) qa_sort_by($outresult, $selectspec['sortdesc'], '_order_');
$output[] = $row[0]; }
return $output; $outresult = array_reverse($outresult, true);
} }
if (isset($selectspec['arrayvalue']))
/** foreach ($outresult as $key => $value)
* Return the first column of the first row (and presumably only cell) from the $result resource. $outresult[$key] = $value[$selectspec['arrayvalue']];
* If there's no first row, throw a fatal error unless $allowempty is true.
*/ if (@$selectspec['single'])
function qa_db_read_one_value($result, $allowempty=false) $outresult = count($outresult) ? reset($outresult) : null;
{ }
if (!($result instanceof mysqli_result))
qa_fatal_error('Reading one value from invalid result');
/**
$row = $result->fetch_row(); * Return the full results from the $result resource as an array. The key of each element in the returned array
* is from column $key if specified, otherwise it's integer. The value of each element in the returned array
if (is_array($row)) * is from column $value if specified, otherwise it's a named array of all columns, given an array of arrays.
return $row[0]; * @param $result
* @param string $key
if ($allowempty) * @param mixed $value
return null; * @return array
*/
function qa_db_read_all_assoc($result, $key = null, $value = null)
{
if (!($result instanceof mysqli_result))
qa_fatal_error('Reading all assoc from invalid result');
$assocs = array();
while ($assoc = $result->fetch_assoc()) {
if (isset($key))
$assocs[$assoc[$key]] = isset($value) ? $assoc[$value] : $assoc;
else else
qa_fatal_error('Reading one value from empty results'); $assocs[] = isset($value) ? $assoc[$value] : $assoc;
}
/**
* Suspend the updating of counts (of many different types) in the database, to save time when making a lot of changes
* if $suspend is true, otherwise reinstate it. A counter is kept to allow multiple calls.
*/
function qa_suspend_update_counts($suspend=true)
{
global $qa_update_counts_suspended;
$qa_update_counts_suspended += ($suspend ? 1 : -1);
} }
return $assocs;
/** }
* Returns whether counts should currently be updated (i.e. if count updating has not been suspended).
*/
function qa_should_update_counts() /**
{ * Return the first row from the $result resource as an array of [column name] => [column value].
global $qa_update_counts_suspended; * If there's no first row, throw a fatal error unless $allowempty is true.
* @param $result
return ($qa_update_counts_suspended <= 0); * @param bool $allowempty
} * @return array|null
*/
function qa_db_read_one_assoc($result, $allowempty = false)
{
if (!($result instanceof mysqli_result))
qa_fatal_error('Reading one assoc from invalid result');
$assoc = $result->fetch_assoc();
if (is_array($assoc))
return $assoc;
if ($allowempty)
return null;
else
qa_fatal_error('Reading one assoc from empty results');
}
/**
* Return a numbered array containing the first (and presumably only) column from the $result resource.
* @param $result
* @return array
*/
function qa_db_read_all_values($result)
{
if (!($result instanceof mysqli_result))
qa_fatal_error('Reading column from invalid result');
$output = array();
while ($row = $result->fetch_row())
$output[] = $row[0];
return $output;
}
/**
* Return the first column of the first row (and presumably only cell) from the $result resource.
* If there's no first row, throw a fatal error unless $allowempty is true.
* @param $result
* @param bool $allowempty
* @return mixed|null
*/
function qa_db_read_one_value($result, $allowempty = false)
{
if (!($result instanceof mysqli_result))
qa_fatal_error('Reading one value from invalid result');
$row = $result->fetch_row();
if (is_array($row))
return $row[0];
if ($allowempty)
return null;
else
qa_fatal_error('Reading one value from empty results');
}
/**
* Suspend the updating of counts (of many different types) in the database, to save time when making a lot of changes
* if $suspend is true, otherwise reinstate it. A counter is kept to allow multiple calls.
* @param bool $suspend
*/
function qa_suspend_update_counts($suspend = true)
{
global $qa_update_counts_suspended;
$qa_update_counts_suspended += ($suspend ? 1 : -1);
}
/**
* Returns whether counts should currently be updated (i.e. if count updating has not been suspended).
*/
function qa_should_update_counts()
{
global $qa_update_counts_suspended;
return ($qa_update_counts_suspended <= 0);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment