qa-db.php 22.9 KB
Newer Older
Gideon Greenspan committed
1 2 3 4 5 6 7
<?php

/*
	Question2Answer (c) Gideon Greenspan

	http://www.question2answer.org/

Scott Vivian committed
8

Gideon Greenspan committed
9 10 11 12 13 14 15 16 17
	File: qa-include/qa-db.php
	Version: See define()s at top of qa-include/qa-base.php
	Description: Common functions for connecting to and accessing database


	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	as published by the Free Software Foundation; either version 2
	of the License, or (at your option) any later version.
Scott Vivian committed
18

Gideon Greenspan committed
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	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
		header('Location: ../');
		exit;
	}


	function qa_db_allow_connect()
/*
	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)
*/
	{
Gideon Greenspan committed
39
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott Vivian committed
40

Gideon Greenspan committed
41 42 43 44
		global $qa_db_allow_connect;

		$qa_db_allow_connect=true;
	}
Scott Vivian committed
45 46


Gideon Greenspan committed
47 48 49 50 51
	function qa_db_connect($failhandler=null)
/*
	Connect to the Q2A database, select the right database, optionally install the $failhandler (and call it if necessary)
*/
	{
Gideon Greenspan committed
52
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott Vivian committed
53

Gideon Greenspan committed
54
		global $qa_db_connection, $qa_db_fail_handler, $qa_db_allow_connect;
Scott Vivian committed
55

Gideon Greenspan committed
56 57
		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.');
Scott Vivian committed
58

Gideon Greenspan committed
59 60
		if (isset($failhandler))
			$qa_db_fail_handler=$failhandler; // set this even if connection already opened
Scott Vivian committed
61

Gideon Greenspan committed
62 63 64 65 66
		if (!is_resource($qa_db_connection)) {
			if (QA_PERSISTENT_CONN_DB)
				$db=mysql_pconnect(QA_FINAL_MYSQL_HOSTNAME, QA_FINAL_MYSQL_USERNAME, QA_FINAL_MYSQL_PASSWORD);
			else
				$db=mysql_connect(QA_FINAL_MYSQL_HOSTNAME, QA_FINAL_MYSQL_USERNAME, QA_FINAL_MYSQL_PASSWORD);
Scott Vivian committed
67

Gideon Greenspan committed
68 69 70 71 72
			if (is_resource($db)) {
				if (!mysql_select_db(QA_FINAL_MYSQL_DATABASE, $db)) {
					mysql_close($db);
					qa_db_fail_error('select');
				}
Scott Vivian committed
73

Gideon Greenspan committed
74 75 76 77 78 79 80 81
			/*
				From Q2A 1.5, we *do* explicitly set the character encoding of the MySQL connection, instead
				of using lots of "SELECT BINARY col AS col"-style queries, as in previous versions of Q2A.
				Although this introduces the latency of another query here, testing showed that the delay is
				the same as that from calling mysql_select_db() and only a quarter of that of mysql_connect().
				So overall it adds 20% to the latency delay in qa_db_connect(), and this seems worth trading
				off against the benefit of more straightforward queries, especially for plugin developers.
			*/
Scott Vivian committed
82

Gideon Greenspan committed
83 84 85 86
				if (function_exists('mysql_set_charset'))
					mysql_set_charset('utf8', $db);
				else
					mysql_query('SET NAMES utf8', $db);
Scott Vivian committed
87

Gideon Greenspan committed
88
				qa_report_process_stage('db_connected');
Scott Vivian committed
89

Gideon Greenspan committed
90 91
			} else
				qa_db_fail_error('connect');
Scott Vivian committed
92

Gideon Greenspan committed
93 94 95
			$qa_db_connection=$db;
		}
	}
Scott Vivian committed
96 97


Gideon Greenspan committed
98 99 100 101 102
	function qa_db_fail_error($type, $errno=null, $error=null, $query=null)
/*
	If a DB error occurs, call the installed fail handler (if any) otherwise report error and exit immediately
*/
	{
Gideon Greenspan committed
103
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott Vivian committed
104

Gideon Greenspan committed
105
		global $qa_db_fail_handler;
Scott Vivian committed
106

Gideon Greenspan committed
107
		@error_log('PHP Question2Answer MySQL '.$type.' error '.$errno.': '.$error.(isset($query) ? (' - Query: '.$query) : ''));
Scott Vivian committed
108

Gideon Greenspan committed
109 110
		if (function_exists($qa_db_fail_handler))
			$qa_db_fail_handler($type, $errno, $error, $query);
Scott Vivian committed
111

Gideon Greenspan committed
112
		else {
Gideon Greenspan committed
113
			echo '<hr><font color="red">Database '.htmlspecialchars($type.' error '.$errno).'<p>'.nl2br(htmlspecialchars($error."\n\n".$query));
Gideon Greenspan committed
114 115 116 117
			qa_exit('error');
		}
	}

Scott Vivian committed
118

Gideon Greenspan committed
119 120 121 122 123
	function qa_db_connection($connect=true)
/*
	Return the current connection to the Q2A database, connecting if necessary and $connect is true
*/
	{
Gideon Greenspan committed
124
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott Vivian committed
125

Gideon Greenspan committed
126
		global $qa_db_connection;
Scott Vivian committed
127

Gideon Greenspan committed
128 129
		if ($connect && !is_resource($qa_db_connection)) {
			qa_db_connect();
Scott Vivian committed
130

Gideon Greenspan committed
131 132 133
			if (!is_resource($qa_db_connection))
				qa_fatal_error('Failed to connect to database');
		}
Scott Vivian committed
134

Gideon Greenspan committed
135 136 137
		return $qa_db_connection;
	}

Scott Vivian committed
138

Gideon Greenspan committed
139 140 141 142 143
	function qa_db_disconnect()
/*
	Disconnect from the Q2A database
*/
	{
Gideon Greenspan committed
144
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott Vivian committed
145

Gideon Greenspan committed
146
		global $qa_db_connection;
Scott Vivian committed
147

Gideon Greenspan committed
148 149
		if (is_resource($qa_db_connection)) {
			qa_report_process_stage('db_disconnect');
Scott Vivian committed
150

Gideon Greenspan committed
151 152 153
			if (!QA_PERSISTENT_CONN_DB)
				if (!mysql_close($qa_db_connection))
					qa_fatal_error('Database disconnect failed');
Scott Vivian committed
154

Gideon Greenspan committed
155 156 157 158
			$qa_db_connection=null;
		}
	}

Scott Vivian committed
159

Gideon Greenspan committed
160 161 162 163 164 165
	function qa_db_query_raw($query)
/*
	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.
*/
	{
Gideon Greenspan committed
166
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott Vivian committed
167

Gideon Greenspan committed
168 169
		if (QA_DEBUG_PERFORMANCE) {
			global $qa_database_usage, $qa_database_queries;
Scott Vivian committed
170

Gideon Greenspan committed
171 172 173 174 175 176
			$oldtime=array_sum(explode(' ', microtime()));
			$result=qa_db_query_execute($query);
			$usedtime=array_sum(explode(' ', microtime()))-$oldtime;

			if (is_array($qa_database_usage)) {
				$qa_database_usage['clock']+=$usedtime;
Scott Vivian committed
177

Gideon Greenspan committed
178 179 180
				if (strlen($qa_database_queries)<1048576) { // don't keep track of big tests
					$gotrows=is_resource($result) ? mysql_num_rows($result) : null;
					$gotcolumns=is_resource($result) ? mysql_num_fields($result) : null;
Scott Vivian committed
181

Gideon Greenspan committed
182 183 184 185 186
					$qa_database_queries.=$query."\n\n".sprintf('%.2f ms', $usedtime*1000).
						(is_numeric($gotrows) ? (' - '.$gotrows.(($gotrows==1) ? ' row' : ' rows')) : '').
						(is_numeric($gotcolumns) ? (' - '.$gotcolumns.(($gotcolumns==1) ? ' column' : ' columns')) : '').
						"\n\n";
				}
Scott Vivian committed
187

Gideon Greenspan committed
188 189
				$qa_database_usage['queries']++;
			}
Scott Vivian committed
190

Gideon Greenspan committed
191 192
		} else
			$result=qa_db_query_execute($query);
Scott Vivian committed
193

Gideon Greenspan committed
194 195 196 197 198 199
	//	@error_log('Question2Answer MySQL query: '.$query);

		if ($result===false) {
			$db=qa_db_connection();
			qa_db_fail_error('query', mysql_errno($db), mysql_error($db), $query);
		}
Scott Vivian committed
200

Gideon Greenspan committed
201 202
		return $result;
	}
Scott Vivian committed
203 204


Gideon Greenspan committed
205 206 207 208 209
	function qa_db_query_execute($query)
/*
	Lower-level function to execute a query, which automatically retries if there is a MySQL deadlock error
*/
	{
Gideon Greenspan committed
210
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott Vivian committed
211

Gideon Greenspan committed
212
		$db=qa_db_connection();
Scott Vivian committed
213

Gideon Greenspan committed
214 215
		for ($attempt=0; $attempt<100; $attempt++) {
			$result=mysql_query($query, $db);
Scott Vivian committed
216

Gideon Greenspan committed
217 218 219 220 221
			if (($result===false) && (mysql_errno($db)==1213))
				usleep(10000); // dead with InnoDB deadlock errors by waiting 0.01s then retrying
			else
				break;
		}
Scott Vivian committed
222

Gideon Greenspan committed
223 224
		return $result;
	}
Scott Vivian committed
225 226


Gideon Greenspan committed
227 228 229 230 231
	function qa_db_escape_string($string)
/*
	Return $string escaped for use in queries to the Q2A database (to which a connection must have been made)
*/
	{
Gideon Greenspan committed
232
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott Vivian committed
233

Gideon Greenspan committed
234 235 236
		return mysql_real_escape_string($string, qa_db_connection());
	}

Scott Vivian committed
237

Gideon Greenspan committed
238 239 240 241 242 243 244 245
	function qa_db_argument_to_mysql($argument, $alwaysquote, $arraybrackets=false)
/*
	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.
*/
	{
		if (is_array($argument)) {
			$parts=array();
Scott Vivian committed
246

Gideon Greenspan committed
247 248
			foreach ($argument as $subargument)
				$parts[]=qa_db_argument_to_mysql($subargument, $alwaysquote, true);
Scott Vivian committed
249

Gideon Greenspan committed
250 251 252 253
			if ($arraybrackets)
				$result='('.implode(',', $parts).')';
			else
				$result=implode(',', $parts);
Scott Vivian committed
254

Gideon Greenspan committed
255 256 257 258 259
		} elseif (isset($argument)) {
			if ($alwaysquote || !is_numeric($argument))
				$result="'".qa_db_escape_string($argument)."'";
			else
				$result=qa_db_escape_string($argument);
Scott Vivian committed
260

Gideon Greenspan committed
261 262
		} else
			$result='NULL';
Scott Vivian committed
263

Gideon Greenspan committed
264 265
		return $result;
	}
Scott Vivian committed
266 267


Gideon Greenspan committed
268 269 270 271 272
	function qa_db_add_table_prefix($rawname)
/*
	Return the full name (with prefix) of database table $rawname, usually if it used after a ^ symbol
*/
	{
Gideon Greenspan committed
273
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott Vivian committed
274

Gideon Greenspan committed
275
		$prefix=QA_MYSQL_TABLE_PREFIX;
Scott Vivian committed
276

Gideon Greenspan committed
277
		if (defined('QA_MYSQL_USERS_PREFIX')) {
Gideon Greenspan committed
278 279 280 281 282 283 284 285 286
			switch (strtolower($rawname)) {
				case 'users':
				case 'userlogins':
				case 'userprofile':
				case 'userfields':
				case 'messages':
				case 'cookies':
				case 'blobs':
				case 'cache':
Gideon Greenspan committed
287 288
				case 'userlogins_ibfk_1': // also special cases for constraint names
				case 'userprofile_ibfk_1':
Gideon Greenspan committed
289 290 291
					$prefix=QA_MYSQL_USERS_PREFIX;
					break;
			}
Gideon Greenspan committed
292
		}
Scott Vivian committed
293

Gideon Greenspan committed
294 295
		return $prefix.$rawname;
	}
Scott Vivian committed
296 297


Gideon Greenspan committed
298 299 300 301 302 303 304 305
	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]);
	}

Scott Vivian committed
306

Gideon Greenspan committed
307 308 309 310 311 312 313 314 315 316
	function qa_db_apply_sub($query, $arguments)
/*
	Substitute ^, $ and # symbols in $query. ^ symbols are replaced with the table prefix set in qa-config.php.
	$ 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.
*/
	{
		$query=preg_replace_callback('/\^([A-Za-z_0-9]+)/', 'qa_db_prefix_callback', $query);
Scott Vivian committed
317

Gideon Greenspan committed
318 319 320
		if (is_array($arguments)) {
			$countargs=count($arguments);
			$offset=0;
Scott Vivian committed
321

Gideon Greenspan committed
322 323 324
			for ($argument=0; $argument<$countargs; $argument++) {
				$stringpos=strpos($query, '$', $offset);
				$numberpos=strpos($query, '#', $offset);
Scott Vivian committed
325

Gideon Greenspan committed
326 327 328
				if ( ($stringpos===false) || ( ($numberpos!==false) && ($numberpos<$stringpos) ) ) {
					$alwaysquote=false;
					$position=$numberpos;
Scott Vivian committed
329

Gideon Greenspan committed
330 331 332 333
				} else {
					$alwaysquote=true;
					$position=$stringpos;
				}
Scott Vivian committed
334

Gideon Greenspan committed
335 336
				if (!is_numeric($position))
					qa_fatal_error('Insufficient parameters in query: '.$query);
Scott Vivian committed
337

Gideon Greenspan committed
338 339 340 341 342
				$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
			}
		}
Scott Vivian committed
343

Gideon Greenspan committed
344 345 346
		return $query;
	}

Scott Vivian committed
347

Gideon Greenspan committed
348 349 350 351 352 353 354 355 356 357
	function qa_db_query_sub($query) // arguments for substitution retrieved using func_get_args()
/*
	Run $query after substituting ^, # and $ symbols, and return the result resource (or call fail handler)
*/
	{
		$funcargs=func_get_args();

		return qa_db_query_raw(qa_db_apply_sub($query, array_slice($funcargs, 1)));
	}

Scott Vivian committed
358

Gideon Greenspan committed
359 360 361 362 363 364 365
	function qa_db_last_insert_id()
/*
	Return the value of the auto-increment column for the last inserted row
*/
	{
		return qa_db_read_one_value(qa_db_query_raw('SELECT LAST_INSERT_ID()'));
	}
Scott Vivian committed
366

Gideon Greenspan committed
367 368 369 370 371 372 373 374

	function qa_db_affected_rows()
/*
	Does what it says on the tin
*/
	{
		return mysql_affected_rows(qa_db_connection());
	}
Scott Vivian committed
375 376


Gideon Greenspan committed
377 378 379 380 381 382 383 384
	function qa_db_insert_on_duplicate_inserted()
/*
	For the previous INSERT ... ON DUPLICATE KEY UPDATE query, return whether an insert operation took place
*/
	{
		return (qa_db_affected_rows()==1);
	}

Scott Vivian committed
385

Gideon Greenspan committed
386 387 388 389 390 391 392 393
	function qa_db_random_bigint()
/*
	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
*/
	{
		return sprintf('%d%06d%06d', mt_rand(1,18446743), mt_rand(0,999999), mt_rand(0,999999));
	}
Scott Vivian committed
394 395


Gideon Greenspan committed
396 397 398 399 400
	function qa_db_list_tables_lc()
/*
	Return an array of the names of all tables in the Q2A database, converted to lower case
*/
	{
Gideon Greenspan committed
401 402
		return array_map('strtolower', qa_db_list_tables());
	}
Scott Vivian committed
403 404


Gideon Greenspan committed
405
	function qa_db_list_tables()
Gideon Greenspan committed
406 407 408
/*
	Return an array of the names of all tables in the Q2A database
*/
Gideon Greenspan committed
409 410
	{
		return qa_db_read_all_values(qa_db_query_raw('SHOW TABLES'));
Gideon Greenspan committed
411 412
	}

Scott Vivian committed
413

Gideon Greenspan committed
414 415 416 417 418 419
/*
	The selectspec array can contain the elements below. See qa-db-selects.php for lots of examples.

	By default, qa_db_single_select() and qa_db_multi_select() return the data for each selectspec as a numbered
	array of arrays, one per row. The array for each row has column names in the keys, and data in the values.
	But this can be changed using the 'arraykey', 'arrayvalue' and 'single' in the selectspec.
Scott Vivian committed
420

Gideon Greenspan committed
421 422 423
	Note that even if you specify ORDER BY in 'source', the final results may not be ordered. This is because
	the SELECT could be done within a UNION that (annoyingly) doesn't maintain order. Use 'sortasc' or 'sortdesc'
	to fix this. You can however rely on the combination of ORDER BY and LIMIT retrieving the appropriate records.
Scott Vivian committed
424

Gideon Greenspan committed
425 426

	'columns' => Array of names of columns to be retrieved (required)
Scott Vivian committed
427

Gideon Greenspan committed
428 429 430
		If a value in the columns array has an integer key, it is retrieved AS itself (in a SQL sense).
		If a value in the columns array has a non-integer key, it is retrieved AS that key.
		Values in the columns array can include table specifiers before the period.
Scott Vivian committed
431

Gideon Greenspan committed
432
	'source' => Any SQL after FROM, including table names, JOINs, GROUP BY, ORDER BY, WHERE, etc... (required)
Scott Vivian committed
433

Gideon Greenspan committed
434
	'arguments' => Substitutions in order for $s and #s in the query, applied in qa_db_apply_sub() above (required)
Scott Vivian committed
435

Gideon Greenspan committed
436
	'arraykey' => Name of column to use for keys of the outer-level returned array, instead of numbers by default
Scott Vivian committed
437

Gideon Greenspan committed
438
	'arrayvalue' => Name of column to use for values of outer-level returned array, instead of arrays by default
Scott Vivian committed
439

Gideon Greenspan committed
440
	'single' => If true, return the array for a single row and don't embed it within an outer-level array
Scott Vivian committed
441

Gideon Greenspan committed
442 443 444 445 446 447
	'sortasc' => Sort the output ascending by this column

	'sortdesc' => Sort the output descending by this column


	Why does qa_db_multi_select() combine usually unrelated SELECT statements into a single query?
Scott Vivian committed
448

Gideon Greenspan committed
449 450 451
	Because if the database and web servers are on different computers, there will be latency.
	This way we ensure that every read pageview on the site requires only a single DB query, so
	that we pay for this latency only one time.
Scott Vivian committed
452

Gideon Greenspan committed
453
	For writes we worry less, since the user is more likely to be expecting a delay.
Scott Vivian committed
454

Gideon Greenspan committed
455 456 457 458 459 460 461 462 463 464 465
	If QA_OPTIMIZE_LOCAL_DB is set in qa-config.php, we assume zero latency and go back to
	simple queries, since this will allow both MySQL and PHP to provide quicker results.
*/


	function qa_db_single_select($selectspec)
/*
	Return the data specified by a single $selectspec - see long comment above.
*/
	{
		$query='SELECT ';
Scott Vivian committed
466

Gideon Greenspan committed
467 468
		foreach ($selectspec['columns'] as $columnas => $columnfrom)
			$query.=$columnfrom.(is_int($columnas) ? '' : (' AS '.$columnas)).', ';
Scott Vivian committed
469

Gideon Greenspan committed
470 471 472 473
		$results=qa_db_read_all_assoc(qa_db_query_raw(qa_db_apply_sub(
			substr($query, 0, -2).(strlen(@$selectspec['source']) ? (' FROM '.$selectspec['source']) : ''),
			@$selectspec['arguments'])
		), @$selectspec['arraykey']); // arrayvalue is applied in qa_db_post_select()
Scott Vivian committed
474

Gideon Greenspan committed
475 476 477 478 479
		qa_db_post_select($results, $selectspec); // post-processing

		return $results;
	}

Scott Vivian committed
480

Gideon Greenspan committed
481 482 483 484 485 486 487 488
	function qa_db_multi_select($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.
*/
	{
		if (!count($selectspecs))
			return array();
Scott Vivian committed
489

Gideon Greenspan committed
490 491 492 493
	//	Perform simple queries if the database is local or there are only 0 or 1 selectspecs

		if (QA_OPTIMIZE_LOCAL_DB || (count($selectspecs)<=1)) {
			$outresults=array();
Scott Vivian committed
494

Gideon Greenspan committed
495 496
			foreach ($selectspecs as $selectkey => $selectspec)
				$outresults[$selectkey]=qa_db_single_select($selectspec);
Scott Vivian committed
497

Gideon Greenspan committed
498 499 500 501
			return $outresults;
		}

	//	Otherwise, parse columns for each spec to deal with columns without an 'AS' specification
Scott Vivian committed
502

Gideon Greenspan committed
503 504 505
		foreach ($selectspecs as $selectkey => $selectspec) {
			$selectspecs[$selectkey]['outcolumns']=array();
			$selectspecs[$selectkey]['autocolumn']=array();
Scott Vivian committed
506

Gideon Greenspan committed
507 508 509 510 511 512
			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;
				}
Scott Vivian committed
513

Gideon Greenspan committed
514 515
				if (isset($selectspecs[$selectkey]['outcolumns'][$columnas]))
					qa_fatal_error('Duplicate column name in qa_db_multi_select()');
Scott Vivian committed
516

Gideon Greenspan committed
517 518
				$selectspecs[$selectkey]['outcolumns'][$columnas]=$columnfrom;
			}
Scott Vivian committed
519

Gideon Greenspan committed
520 521 522 523 524 525 526 527
			if (isset($selectspec['arraykey']))
				if (!isset($selectspecs[$selectkey]['outcolumns'][$selectspec['arraykey']]))
					qa_fatal_error('Used arraykey not in columns in qa_db_multi_select()');

			if (isset($selectspec['arrayvalue']))
				if (!isset($selectspecs[$selectkey]['outcolumns'][$selectspec['arrayvalue']]))
					qa_fatal_error('Used arrayvalue not in columns in qa_db_multi_select()');
		}
Scott Vivian committed
528

Gideon Greenspan committed
529
	//	Work out the full list of columns used
Scott Vivian committed
530

Gideon Greenspan committed
531 532 533
		$outcolumns=array();
		foreach ($selectspecs as $selectspec)
			$outcolumns=array_unique(array_merge($outcolumns, array_keys($selectspec['outcolumns'])));
Scott Vivian committed
534

Gideon Greenspan committed
535
	//	Build the query based on this full list
Scott Vivian committed
536

Gideon Greenspan committed
537 538 539
		$query='';
		foreach ($selectspecs as $selectkey => $selectspec) {
			$subquery="(SELECT '".qa_db_escape_string($selectkey)."'".(empty($query) ? ' AS selectkey' : '');
Scott Vivian committed
540

Gideon Greenspan committed
541 542
			foreach ($outcolumns as $columnas) {
				$subquery.=', '.(isset($selectspec['outcolumns'][$columnas]) ? $selectspec['outcolumns'][$columnas] : 'NULL');
Scott Vivian committed
543

Gideon Greenspan committed
544 545 546
				if (empty($query) && !isset($selectspec['autocolumn'][$columnas]))
					$subquery.=' AS '.$columnas;
			}
Scott Vivian committed
547

Gideon Greenspan committed
548 549
			if (strlen(@$selectspec['source']))
				$subquery.=' FROM '.$selectspec['source'];
Scott Vivian committed
550

Gideon Greenspan committed
551
			$subquery.=')';
Scott Vivian committed
552

Gideon Greenspan committed
553 554
			if (strlen($query))
				$query.=' UNION ALL ';
Scott Vivian committed
555

Gideon Greenspan committed
556 557
			$query.=qa_db_apply_sub($subquery, @$selectspec['arguments']);
		}
Scott Vivian committed
558

Gideon Greenspan committed
559
	//	Perform query and extract results
Scott Vivian committed
560

Gideon Greenspan committed
561 562 563 564 565 566 567 568 569
		$rawresults=qa_db_read_all_assoc(qa_db_query_raw($query));

		$outresults=array();
		foreach ($selectspecs as $selectkey => $selectspec)
			$outresults[$selectkey]=array();

		foreach ($rawresults as $rawresult) {
			$selectkey=$rawresult['selectkey'];
			$selectspec=$selectspecs[$selectkey];
Scott Vivian committed
570

Gideon Greenspan committed
571 572 573 574 575 576 577 578 579 580 581
			$keepresult=array();
			foreach ($selectspec['outcolumns'] as $columnas => $columnfrom)
				$keepresult[$columnas]=$rawresult[$columnas];

			if (isset($selectspec['arraykey']))
				$outresults[$selectkey][$keepresult[$selectspec['arraykey']]]=$keepresult;
			else
				$outresults[$selectkey][]=$keepresult;
		}

	//	Post-processing to apply various stuff include sorting request, since we can't rely on ORDER BY due to UNION
Scott Vivian committed
582

Gideon Greenspan committed
583 584
		foreach ($selectspecs as $selectkey => $selectspec)
			qa_db_post_select($outresults[$selectkey], $selectspec);
Scott Vivian committed
585

Gideon Greenspan committed
586
	//	Return results
Scott Vivian committed
587

Gideon Greenspan committed
588 589
		return $outresults;
	}
Scott Vivian committed
590

Gideon Greenspan committed
591 592 593 594 595 596 597 598 599 600 601

	function qa_db_post_select(&$outresult, $selectspec)
/*
	Post-process $outresult according to $selectspec, applying 'sortasc', 'sortdesc', 'arrayvalue' and 'single'
*/
	{
		// 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.'qa-util-sort.php';
Scott Vivian committed
602

Gideon Greenspan committed
603 604 605
			$index=0;
			foreach ($outresult as $key => $value)
				$outresult[$key]['_order_']=$index++;
Scott Vivian committed
606

Gideon Greenspan committed
607 608 609 610
			qa_sort_by($outresult, $selectspec['sortasc'], '_order_');

		} elseif (isset($selectspec['sortdesc'])) {
			require_once QA_INCLUDE_DIR.'qa-util-sort.php';
Scott Vivian committed
611

Gideon Greenspan committed
612 613
			if (isset($selectspec['sortdesc_2']))
				qa_sort_by($outresult, $selectspec['sortdesc'], $selectspec['sortdesc_2']);
Scott Vivian committed
614

Gideon Greenspan committed
615 616 617 618
			else {
				$index=count($outresult);
				foreach ($outresult as $key => $value)
					$outresult[$key]['_order_']=$index--;
Scott Vivian committed
619

Gideon Greenspan committed
620 621
				qa_sort_by($outresult, $selectspec['sortdesc'], '_order_');
			}
Scott Vivian committed
622

Gideon Greenspan committed
623 624
			$outresult=array_reverse($outresult, true);
		}
Scott Vivian committed
625

Gideon Greenspan committed
626 627 628
		if (isset($selectspec['arrayvalue']))
			foreach ($outresult as $key => $value)
				$outresult[$key]=$value[$selectspec['arrayvalue']];
Scott Vivian committed
629

Gideon Greenspan committed
630 631 632 633
		if (@$selectspec['single'])
			$outresult=count($outresult) ? reset($outresult) : null;
	}

Scott Vivian committed
634

Gideon Greenspan committed
635 636 637 638 639 640 641 642 643
	function qa_db_read_all_assoc($result, $key=null, $value=null)
/*
	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
	is from column $value if specified, otherwise it's a named array of all columns, given an array of arrays.
*/
	{
		if (!is_resource($result))
			qa_fatal_error('Reading all assoc from invalid result');
Scott Vivian committed
644

Gideon Greenspan committed
645
		$assocs=array();
Scott Vivian committed
646

Gideon Greenspan committed
647 648 649 650 651 652
		while ($assoc=mysql_fetch_assoc($result)) {
			if (isset($key))
				$assocs[$assoc[$key]]=isset($value) ? $assoc[$value] : $assoc;
			else
				$assocs[]=isset($value) ? $assoc[$value] : $assoc;
		}
Scott Vivian committed
653

Gideon Greenspan committed
654 655
		return $assocs;
	}
Scott Vivian committed
656

Gideon Greenspan committed
657 658 659 660 661 662 663 664 665 666 667

	function qa_db_read_one_assoc($result, $allowempty=false)
/*
	Return the first row from the $result resource as an array of [column name] => [column value].
	If there's no first row, throw a fatal error unless $allowempty is true.
*/
	{
		if (!is_resource($result))
			qa_fatal_error('Reading one assoc from invalid result');

		$assoc=mysql_fetch_assoc($result);
Scott Vivian committed
668

Gideon Greenspan committed
669 670 671 672 673 674
		if (!is_array($assoc)) {
			if ($allowempty)
				return null;
			else
				qa_fatal_error('Reading one assoc from empty results');
		}
Scott Vivian committed
675

Gideon Greenspan committed
676 677 678
		return $assoc;
	}

Scott Vivian committed
679

Gideon Greenspan committed
680 681 682 683 684 685 686
	function qa_db_read_all_values($result)
/*
	Return a numbered array containing the first (and presumably only) column from the $result resource
*/
	{
		if (!is_resource($result))
			qa_fatal_error('Reading column from invalid result');
Scott Vivian committed
687

Gideon Greenspan committed
688
		$output=array();
Scott Vivian committed
689

Gideon Greenspan committed
690 691
		while ($row=mysql_fetch_row($result))
			$output[]=$row[0];
Scott Vivian committed
692

Gideon Greenspan committed
693 694 695
		return $output;
	}

Scott Vivian committed
696

Gideon Greenspan committed
697 698 699 700 701 702 703 704 705 706
	function qa_db_read_one_value($result, $allowempty=false)
/*
	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.
*/
	{
		if (!is_resource($result))
			qa_fatal_error('Reading one value from invalid result');

		$row=mysql_fetch_row($result);
Scott Vivian committed
707

Gideon Greenspan committed
708 709 710 711 712 713
		if (!is_array($row)) {
			if ($allowempty)
				return null;
			else
				qa_fatal_error('Reading one value from empty results');
		}
Scott Vivian committed
714

Gideon Greenspan committed
715 716
		return $row[0];
	}
Scott Vivian committed
717 718


Gideon Greenspan committed
719 720 721 722 723 724 725
	function qa_suspend_update_counts($suspend=true)
/*
	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.
*/
	{
		global $qa_update_counts_suspended;
Scott Vivian committed
726

Gideon Greenspan committed
727 728 729
		$qa_update_counts_suspended+=($suspend ? 1 : -1);
	}

Scott Vivian committed
730

Gideon Greenspan committed
731 732 733 734 735 736
	function qa_should_update_counts()
/*
	Returns whether counts should currently be updated (i.e. if count updating has not been suspended)
*/
	{
		global $qa_update_counts_suspended;
Scott Vivian committed
737

Gideon Greenspan committed
738 739 740 741 742 743 744
		return ($qa_update_counts_suspended<=0);
	}


/*
	Omit PHP closing tag to help avoid accidental output
*/