qa-db.php 23 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
	function qa_db_connect($failhandler=null)
/*
Scott committed
49 50
	Connect to the Q2A database, select the right database, optionally install the $failhandler (and call it if necessary).
	Uses mysqli as of Q2A 1.6.4.
Gideon Greenspan committed
51 52
*/
	{
Gideon Greenspan committed
53
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott Vivian committed
54

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

Gideon Greenspan committed
57 58
		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
59

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

Scott committed
63 64
		if ($qa_db_connection instanceof mysqli)
			return;
Scott Vivian committed
65

Scott committed
66 67 68 69 70
		// in mysqli we connect and select database in constructor
		if (QA_PERSISTENT_CONN_DB)
			$db = new mysqli('p:'.QA_FINAL_MYSQL_HOSTNAME, QA_FINAL_MYSQL_USERNAME, QA_FINAL_MYSQL_PASSWORD, QA_FINAL_MYSQL_DATABASE);
		else
			$db = new mysqli(QA_FINAL_MYSQL_HOSTNAME, QA_FINAL_MYSQL_USERNAME, QA_FINAL_MYSQL_PASSWORD, QA_FINAL_MYSQL_DATABASE);
Scott Vivian committed
71

Scott committed
72 73 74 75
		// must use procedural `mysqli_connect_error` here prior to 5.2.9
		$conn_error = mysqli_connect_error();
		if ($conn_error)
			qa_db_fail_error('connect', $db->connect_errno, $conn_error);
Scott Vivian committed
76

Scott committed
77 78 79 80 81 82
	/*
		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.
	*/
Scott Vivian committed
83

Scott committed
84 85
		if (!$db->set_charset('utf8'))
			qa_db_fail_error('set_charset', $db->errno, $db->error);
Scott Vivian committed
86

Scott committed
87
		qa_report_process_stage('db_connected');
Scott Vivian committed
88

Scott committed
89
		$qa_db_connection=$db;
Gideon Greenspan committed
90
	}
Scott Vivian committed
91 92


Gideon Greenspan committed
93 94 95 96 97
	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
98
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott Vivian committed
99

Gideon Greenspan committed
100
		global $qa_db_fail_handler;
Scott Vivian committed
101

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

Gideon Greenspan committed
104 105
		if (function_exists($qa_db_fail_handler))
			$qa_db_fail_handler($type, $errno, $error, $query);
Scott Vivian committed
106

Gideon Greenspan committed
107
		else {
Gideon Greenspan committed
108
			echo '<hr><font color="red">Database '.htmlspecialchars($type.' error '.$errno).'<p>'.nl2br(htmlspecialchars($error."\n\n".$query));
Gideon Greenspan committed
109 110 111 112
			qa_exit('error');
		}
	}

Scott Vivian committed
113

Gideon Greenspan committed
114 115 116 117 118
	function qa_db_connection($connect=true)
/*
	Return the current connection to the Q2A database, connecting if necessary and $connect is true
*/
	{
Gideon Greenspan committed
119
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott Vivian committed
120

Gideon Greenspan committed
121
		global $qa_db_connection;
Scott Vivian committed
122

Scott committed
123
		if ($connect && !($qa_db_connection instanceof mysqli)) {
Gideon Greenspan committed
124
			qa_db_connect();
Scott Vivian committed
125

Scott committed
126
			if (!($qa_db_connection instanceof mysqli))
Gideon Greenspan committed
127 128
				qa_fatal_error('Failed to connect to database');
		}
Scott Vivian committed
129

Gideon Greenspan committed
130 131 132
		return $qa_db_connection;
	}

Scott Vivian committed
133

Gideon Greenspan committed
134 135 136 137 138
	function qa_db_disconnect()
/*
	Disconnect from the Q2A database
*/
	{
Gideon Greenspan committed
139
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott Vivian committed
140

Gideon Greenspan committed
141
		global $qa_db_connection;
Scott Vivian committed
142

Scott committed
143
		if ($qa_db_connection instanceof mysqli) {
Gideon Greenspan committed
144
			qa_report_process_stage('db_disconnect');
Scott Vivian committed
145

Scott committed
146 147
			if (!QA_PERSISTENT_CONN_DB) {
				if (!$qa_db_connection->close())
Gideon Greenspan committed
148
					qa_fatal_error('Database disconnect failed');
Scott committed
149
			}
Scott Vivian committed
150

Gideon Greenspan committed
151 152 153 154
			$qa_db_connection=null;
		}
	}

Scott Vivian committed
155

Gideon Greenspan committed
156 157 158 159 160 161
	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
162
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott Vivian committed
163

Gideon Greenspan committed
164 165
		if (QA_DEBUG_PERFORMANCE) {
			global $qa_database_usage, $qa_database_queries;
Scott Vivian committed
166

Scott committed
167 168 169
			$oldtime = array_sum(explode(' ', microtime()));
			$result = qa_db_query_execute($query);
			$usedtime = array_sum(explode(' ', microtime())) - $oldtime;
Gideon Greenspan committed
170 171

			if (is_array($qa_database_usage)) {
Scott committed
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
				$qa_database_usage['clock'] += $usedtime;

				if (strlen($qa_database_queries) < 1048576) { // don't keep track of big tests
					$gotrows = $gotcolumns = null;
					if ($result instanceof mysqli_result) {
						$gotrows = $result->num_rows;
						$gotcolumns = $result->field_count;
					}

					$rowcolstring = '';
					if (is_numeric($gotrows))
						$rowcolstring .= ' - ' . $gotrows . ($gotrows == 1 ? ' row' : ' rows');
					if (is_numeric($gotcolumns))
						$rowcolstring .= ' - ' . $gotcolumns . ($gotcolumns == 1 ? ' column' : ' columns');

					$qa_database_queries .= $query . "\n\n" . sprintf('%.2f ms', $usedtime*1000) . $rowcolstring . "\n\n";
Gideon Greenspan committed
188
				}
Scott Vivian committed
189

Gideon Greenspan committed
190 191
				$qa_database_usage['queries']++;
			}
Scott Vivian committed
192

Scott committed
193 194 195
		}
		else
			$result = qa_db_query_execute($query);
Scott Vivian committed
196

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

Scott committed
199 200 201
		if ($result === false) {
			$db = qa_db_connection();
			qa_db_fail_error('query', $db->errno, $db->error, $query);
Gideon Greenspan committed
202
		}
Scott Vivian committed
203

Gideon Greenspan committed
204 205
		return $result;
	}
Scott Vivian committed
206 207


Gideon Greenspan committed
208 209 210 211 212
	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
213
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott Vivian committed
214

Scott committed
215
		$db = qa_db_connection();
Scott Vivian committed
216

Scott committed
217 218
		for ($attempt = 0; $attempt < 100; $attempt++) {
			$result = $db->query($query);
Scott Vivian committed
219

Scott committed
220 221
			if ($result === false && $db->errno == 1213)
				usleep(10000); // deal with InnoDB deadlock errors by waiting 0.01s then retrying
Gideon Greenspan committed
222 223 224
			else
				break;
		}
Scott Vivian committed
225

Gideon Greenspan committed
226 227
		return $result;
	}
Scott Vivian committed
228 229


Gideon Greenspan committed
230 231 232 233 234
	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
235
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott Vivian committed
236

Scott committed
237 238
		$db = qa_db_connection();
		return $db->real_escape_string($string);
Gideon Greenspan committed
239 240
	}

Scott Vivian committed
241

Gideon Greenspan committed
242 243 244 245 246 247 248 249
	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
250

Gideon Greenspan committed
251
			foreach ($argument as $subargument)
Scott committed
252
				$parts[] = qa_db_argument_to_mysql($subargument, $alwaysquote, true);
Scott Vivian committed
253

Gideon Greenspan committed
254
			if ($arraybrackets)
Scott committed
255
				$result = '('.implode(',', $parts).')';
Gideon Greenspan committed
256
			else
Scott committed
257
				$result = implode(',', $parts);
Scott Vivian committed
258

Scott committed
259 260
		}
		else if (isset($argument)) {
Gideon Greenspan committed
261
			if ($alwaysquote || !is_numeric($argument))
Scott committed
262
				$result = "'".qa_db_escape_string($argument)."'";
Gideon Greenspan committed
263
			else
Scott committed
264 265 266 267
				$result = qa_db_escape_string($argument);
		}
		else
			$result = 'NULL';
Scott Vivian committed
268

Gideon Greenspan committed
269 270
		return $result;
	}
Scott Vivian committed
271 272


Gideon Greenspan committed
273 274 275 276 277
	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
278
		if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
Scott Vivian committed
279

Scott committed
280
		$prefix = QA_MYSQL_TABLE_PREFIX;
Scott Vivian committed
281

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

Gideon Greenspan committed
299 300
		return $prefix.$rawname;
	}
Scott Vivian committed
301 302


Gideon Greenspan committed
303 304 305 306 307 308 309 310
	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
311

Gideon Greenspan committed
312 313 314 315 316 317 318 319 320
	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.
*/
	{
Scott committed
321
		$query = preg_replace_callback('/\^([A-Za-z_0-9]+)/', 'qa_db_prefix_callback', $query);
Scott Vivian committed
322

Scott committed
323 324
		if (!is_array($arguments))
			return $query;
Scott Vivian committed
325

Scott committed
326 327
		$countargs = count($arguments);
		$offset = 0;
Scott Vivian committed
328

Scott committed
329 330 331
		for ($argument = 0; $argument < $countargs; $argument++) {
			$stringpos = strpos($query, '$', $offset);
			$numberpos = strpos($query, '#', $offset);
Scott Vivian committed
332

Scott committed
333 334 335 336 337 338 339 340
			if ($stringpos === false || ($numberpos !== false && $numberpos < $stringpos)) {
				$alwaysquote = false;
				$position = $numberpos;
			}
			else {
				$alwaysquote = true;
				$position = $stringpos;
			}
Scott Vivian committed
341

Scott committed
342 343
			if (!is_numeric($position))
				qa_fatal_error('Insufficient parameters in query: '.$query);
Scott Vivian committed
344

Scott committed
345 346 347
			$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
Gideon Greenspan committed
348
		}
Scott Vivian committed
349

Gideon Greenspan committed
350 351 352
		return $query;
	}

Scott Vivian committed
353

Gideon Greenspan committed
354 355 356 357 358 359 360 361 362 363
	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
364

Gideon Greenspan committed
365 366 367 368 369
	function qa_db_last_insert_id()
/*
	Return the value of the auto-increment column for the last inserted row
*/
	{
Scott committed
370 371
		$db = qa_db_connection();
		return $db->insert_id;
Gideon Greenspan committed
372
	}
Scott Vivian committed
373

Gideon Greenspan committed
374 375 376 377 378 379

	function qa_db_affected_rows()
/*
	Does what it says on the tin
*/
	{
Scott committed
380 381
		$db = qa_db_connection();
		return $db->affected_rows;
Gideon Greenspan committed
382
	}
Scott Vivian committed
383 384


Gideon Greenspan committed
385 386 387 388 389
	function qa_db_insert_on_duplicate_inserted()
/*
	For the previous INSERT ... ON DUPLICATE KEY UPDATE query, return whether an insert operation took place
*/
	{
Scott committed
390
		return (qa_db_affected_rows() == 1);
Gideon Greenspan committed
391 392
	}

Scott Vivian committed
393

Gideon Greenspan committed
394 395 396 397 398 399 400 401
	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
402 403


Gideon Greenspan committed
404 405 406 407 408
	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
409 410
		return array_map('strtolower', qa_db_list_tables());
	}
Scott Vivian committed
411 412


Gideon Greenspan committed
413
	function qa_db_list_tables()
Gideon Greenspan committed
414 415 416
/*
	Return an array of the names of all tables in the Q2A database
*/
Gideon Greenspan committed
417 418
	{
		return qa_db_read_all_values(qa_db_query_raw('SHOW TABLES'));
Gideon Greenspan committed
419 420
	}

Scott Vivian committed
421

Gideon Greenspan committed
422 423 424 425 426 427
/*
	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
428

Gideon Greenspan committed
429 430 431
	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
432

Gideon Greenspan committed
433 434

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

Gideon Greenspan committed
436 437 438
		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
439

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

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

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

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

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

Gideon Greenspan committed
450 451 452 453 454 455
	'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
456

Gideon Greenspan committed
457 458 459
	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
460

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

Gideon Greenspan committed
463 464 465 466 467 468 469 470 471 472
	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.
*/
	{
Scott committed
473
		$query = 'SELECT ';
Scott Vivian committed
474

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

Scott committed
478
		$results = qa_db_read_all_assoc(qa_db_query_raw(qa_db_apply_sub(
Gideon Greenspan committed
479 480 481
			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
482

Gideon Greenspan committed
483 484 485 486 487
		qa_db_post_select($results, $selectspec); // post-processing

		return $results;
	}

Scott Vivian committed
488

Gideon Greenspan committed
489 490 491 492 493 494 495 496
	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
497

Gideon Greenspan committed
498 499 500 501
	//	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
502

Gideon Greenspan committed
503 504
			foreach ($selectspecs as $selectkey => $selectspec)
				$outresults[$selectkey]=qa_db_single_select($selectspec);
Scott Vivian committed
505

Gideon Greenspan committed
506 507 508 509
			return $outresults;
		}

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

Gideon Greenspan committed
511 512 513
		foreach ($selectspecs as $selectkey => $selectspec) {
			$selectspecs[$selectkey]['outcolumns']=array();
			$selectspecs[$selectkey]['autocolumn']=array();
Scott Vivian committed
514

Gideon Greenspan committed
515 516 517 518 519 520
			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
521

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

Gideon Greenspan committed
525 526
				$selectspecs[$selectkey]['outcolumns'][$columnas]=$columnfrom;
			}
Scott Vivian committed
527

Gideon Greenspan committed
528 529 530 531 532 533 534 535
			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
536

Gideon Greenspan committed
537
	//	Work out the full list of columns used
Scott Vivian committed
538

Gideon Greenspan committed
539 540 541
		$outcolumns=array();
		foreach ($selectspecs as $selectspec)
			$outcolumns=array_unique(array_merge($outcolumns, array_keys($selectspec['outcolumns'])));
Scott Vivian committed
542

Gideon Greenspan committed
543
	//	Build the query based on this full list
Scott Vivian committed
544

Gideon Greenspan committed
545 546 547
		$query='';
		foreach ($selectspecs as $selectkey => $selectspec) {
			$subquery="(SELECT '".qa_db_escape_string($selectkey)."'".(empty($query) ? ' AS selectkey' : '');
Scott Vivian committed
548

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

Gideon Greenspan committed
552 553 554
				if (empty($query) && !isset($selectspec['autocolumn'][$columnas]))
					$subquery.=' AS '.$columnas;
			}
Scott Vivian committed
555

Gideon Greenspan committed
556 557
			if (strlen(@$selectspec['source']))
				$subquery.=' FROM '.$selectspec['source'];
Scott Vivian committed
558

Gideon Greenspan committed
559
			$subquery.=')';
Scott Vivian committed
560

Gideon Greenspan committed
561 562
			if (strlen($query))
				$query.=' UNION ALL ';
Scott Vivian committed
563

Gideon Greenspan committed
564 565
			$query.=qa_db_apply_sub($subquery, @$selectspec['arguments']);
		}
Scott Vivian committed
566

Gideon Greenspan committed
567
	//	Perform query and extract results
Scott Vivian committed
568

Gideon Greenspan committed
569 570 571 572 573 574 575 576 577
		$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
578

Gideon Greenspan committed
579 580 581 582 583 584 585 586 587 588 589
			$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
590

Gideon Greenspan committed
591 592
		foreach ($selectspecs as $selectkey => $selectspec)
			qa_db_post_select($outresults[$selectkey], $selectspec);
Scott Vivian committed
593

Gideon Greenspan committed
594
	//	Return results
Scott Vivian committed
595

Gideon Greenspan committed
596 597
		return $outresults;
	}
Scott Vivian committed
598

Gideon Greenspan committed
599 600 601 602 603 604 605 606 607 608 609

	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
610

Gideon Greenspan committed
611 612 613
			$index=0;
			foreach ($outresult as $key => $value)
				$outresult[$key]['_order_']=$index++;
Scott Vivian committed
614

Gideon Greenspan committed
615 616 617 618
			qa_sort_by($outresult, $selectspec['sortasc'], '_order_');

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

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

Gideon Greenspan committed
623 624 625 626
			else {
				$index=count($outresult);
				foreach ($outresult as $key => $value)
					$outresult[$key]['_order_']=$index--;
Scott Vivian committed
627

Gideon Greenspan committed
628 629
				qa_sort_by($outresult, $selectspec['sortdesc'], '_order_');
			}
Scott Vivian committed
630

Gideon Greenspan committed
631 632
			$outresult=array_reverse($outresult, true);
		}
Scott Vivian committed
633

Gideon Greenspan committed
634 635 636
		if (isset($selectspec['arrayvalue']))
			foreach ($outresult as $key => $value)
				$outresult[$key]=$value[$selectspec['arrayvalue']];
Scott Vivian committed
637

Gideon Greenspan committed
638 639 640 641
		if (@$selectspec['single'])
			$outresult=count($outresult) ? reset($outresult) : null;
	}

Scott Vivian committed
642

Gideon Greenspan committed
643 644 645 646 647 648 649
	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.
*/
	{
Scott committed
650
		if (!($result instanceof mysqli_result))
Gideon Greenspan committed
651
			qa_fatal_error('Reading all assoc from invalid result');
Scott Vivian committed
652

Scott committed
653
		$assocs = array();
Scott Vivian committed
654

Scott committed
655
		while ($assoc = $result->fetch_assoc()) {
Gideon Greenspan committed
656
			if (isset($key))
Scott committed
657
				$assocs[$assoc[$key]] = isset($value) ? $assoc[$value] : $assoc;
Gideon Greenspan committed
658
			else
Scott committed
659
				$assocs[] = isset($value) ? $assoc[$value] : $assoc;
Gideon Greenspan committed
660
		}
Scott Vivian committed
661

Gideon Greenspan committed
662 663
		return $assocs;
	}
Scott Vivian committed
664

Gideon Greenspan committed
665 666 667 668 669 670 671

	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.
*/
	{
Scott committed
672
		if (!($result instanceof mysqli_result))
Gideon Greenspan committed
673 674
			qa_fatal_error('Reading one assoc from invalid result');

Scott committed
675
		$assoc = $result->fetch_assoc();
Scott Vivian committed
676

Scott committed
677 678
		if (is_array($assoc))
			return $assoc;
Scott Vivian committed
679

Scott committed
680 681 682 683
		if ($allowempty)
			return null;
		else
			qa_fatal_error('Reading one assoc from empty results');
Gideon Greenspan committed
684 685
	}

Scott Vivian committed
686

Gideon Greenspan committed
687 688 689 690 691
	function qa_db_read_all_values($result)
/*
	Return a numbered array containing the first (and presumably only) column from the $result resource
*/
	{
Scott committed
692
		if (!($result instanceof mysqli_result))
Gideon Greenspan committed
693
			qa_fatal_error('Reading column from invalid result');
Scott Vivian committed
694

Scott committed
695
		$output = array();
Scott Vivian committed
696

Scott committed
697 698
		while ($row = $result->fetch_row())
			$output[] = $row[0];
Scott Vivian committed
699

Gideon Greenspan committed
700 701 702
		return $output;
	}

Scott Vivian committed
703

Gideon Greenspan committed
704 705 706 707 708 709
	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.
*/
	{
Scott committed
710
		if (!($result instanceof mysqli_result))
Gideon Greenspan committed
711 712
			qa_fatal_error('Reading one value from invalid result');

Scott committed
713
		$row = $result->fetch_row();
Scott Vivian committed
714

Scott committed
715 716
		if (is_array($row))
			return $row[0];
Scott Vivian committed
717

Scott committed
718 719 720 721
		if ($allowempty)
			return null;
		else
			qa_fatal_error('Reading one value from empty results');
Gideon Greenspan committed
722
	}
Scott Vivian committed
723 724


Gideon Greenspan committed
725 726 727 728 729 730 731
	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
732

Scott committed
733
		$qa_update_counts_suspended += ($suspend ? 1 : -1);
Gideon Greenspan committed
734 735
	}

Scott Vivian committed
736

Gideon Greenspan committed
737 738 739 740 741 742
	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
743

Scott committed
744
		return ($qa_update_counts_suspended <= 0);
Gideon Greenspan committed
745 746 747 748 749 750
	}


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