PageRenderTime 178ms CodeModel.GetById 80ms app.highlight 50ms RepoModel.GetById 41ms app.codeStats 0ms

/lib/db.php

https://github.com/sezuan/core
PHP | 711 lines | 464 code | 56 blank | 191 comment | 96 complexity | ef13d3d15053f7fc5a157b5d1fe6d2cf MD5 | raw file
  1<?php
  2/**
  3 * ownCloud
  4 *
  5 * @author Frank Karlitschek
  6 * @copyright 2012 Frank Karlitschek frank@owncloud.org
  7 *
  8 * This library is free software; you can redistribute it and/or
  9 * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
 10 * License as published by the Free Software Foundation; either
 11 * version 3 of the License, or any later version.
 12 *
 13 * This library is distributed in the hope that it will be useful,
 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 16 * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
 17 *
 18 * You should have received a copy of the GNU Affero General Public
 19 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
 20 *
 21 */
 22
 23define('MDB2_SCHEMA_DUMP_STRUCTURE', '1');
 24
 25class DatabaseException extends Exception {
 26	private $query;
 27
 28	//FIXME getQuery seems to be unused, maybe use parent constructor with $message, $code and $previous
 29	public function __construct($message, $query = null){
 30		parent::__construct($message);
 31		$this->query = $query;
 32	}
 33
 34	public function getQuery() {
 35		return $this->query;
 36	}
 37}
 38
 39/**
 40 * This class manages the access to the database. It basically is a wrapper for
 41 * Doctrine with some adaptions.
 42 */
 43class OC_DB {
 44	const BACKEND_DOCTRINE=2;
 45
 46	static private $preparedQueries = array();
 47	static private $cachingEnabled = true;
 48
 49	/**
 50	 * @var \Doctrine\DBAL\Connection
 51	 */
 52	static private $connection; //the preferred connection to use, only Doctrine
 53	static private $backend=null;
 54	/**
 55	 * @var \Doctrine\DBAL\Connection
 56	 */
 57	static private $DOCTRINE=null;
 58
 59	static private $inTransaction=false;
 60	static private $prefix=null;
 61	static private $type=null;
 62
 63	/**
 64	 * check which backend we should use
 65	 * @return int BACKEND_DOCTRINE
 66	 */
 67	private static function getDBBackend() {
 68		return self::BACKEND_DOCTRINE;
 69	}
 70
 71	/**
 72	 * @brief connects to the database
 73	 * @param int $backend
 74	 * @return bool true if connection can be established or false on error
 75	 *
 76	 * Connects to the database as specified in config.php
 77	 */
 78	public static function connect($backend=null) {
 79		if(self::$connection) {
 80			return true;
 81		}
 82		if(is_null($backend)) {
 83			$backend=self::getDBBackend();
 84		}
 85		if($backend==self::BACKEND_DOCTRINE) {
 86			$success = self::connectDoctrine();
 87			self::$connection=self::$DOCTRINE;
 88			self::$backend=self::BACKEND_DOCTRINE;
 89		}
 90		return $success;
 91	}
 92
 93	/**
 94	 * connect to the database using doctrine
 95	 *
 96	 * @return bool
 97	 */
 98	public static function connectDoctrine() {
 99		if(self::$connection) {
100			if(self::$backend!=self::BACKEND_DOCTRINE) {
101				self::disconnect();
102			} else {
103				return true;
104			}
105		}
106		self::$preparedQueries = array();
107		// The global data we need
108		$name = OC_Config::getValue( "dbname", "owncloud" );
109		$host = OC_Config::getValue( "dbhost", "" );
110		$user = OC_Config::getValue( "dbuser", "" );
111		$pass = OC_Config::getValue( "dbpassword", "" );
112		$type = OC_Config::getValue( "dbtype", "sqlite" );
113		if(strpos($host, ':')) {
114			list($host, $port)=explode(':', $host, 2);
115		} else {
116			$port=false;
117		}
118
119		// do nothing if the connection already has been established
120		if(!self::$DOCTRINE) {
121			$config = new \Doctrine\DBAL\Configuration();
122			switch($type) {
123				case 'sqlite':
124				case 'sqlite3':
125					$datadir=OC_Config::getValue( "datadirectory", OC::$SERVERROOT.'/data' );
126					$connectionParams = array(
127							'user' => $user,
128							'password' => $pass,
129							'path' => $datadir.'/'.$name.'.db',
130							'driver' => 'pdo_sqlite',
131					);
132					break;
133				case 'mysql':
134					$connectionParams = array(
135							'user' => $user,
136							'password' => $pass,
137							'host' => $host,
138							'port' => $port,
139							'dbname' => $name,
140							'charset' => 'UTF8',
141							'driver' => 'pdo_mysql',
142					);
143					break;
144				case 'pgsql':
145					$connectionParams = array(
146							'user' => $user,
147							'password' => $pass,
148							'host' => $host,
149							'port' => $port,
150							'dbname' => $name,
151							'driver' => 'pdo_pgsql',
152					);
153					break;
154				case 'oci':
155					$connectionParams = array(
156							'user' => $user,
157							'password' => $pass,
158							'host' => $host,
159							'dbname' => $name,
160							'charset' => 'AL32UTF8',
161							'driver' => 'oci8',
162					);
163					if (!empty($port)) {
164						$connectionParams['port'] = $port;
165					}
166					break;
167				case 'mssql':
168					$connectionParams = array(
169							'user' => $user,
170							'password' => $pass,
171							'host' => $host,
172							'port' => $port,
173							'dbname' => $name,
174							'charset' => 'UTF8',
175							'driver' => 'pdo_sqlsrv',
176					);
177					break;
178				default:
179					return false;
180			}
181			try {
182				self::$DOCTRINE = \Doctrine\DBAL\DriverManager::getConnection($connectionParams, $config);
183			} catch(\Doctrine\DBAL\DBALException $e) {
184				OC_Log::write('core', $e->getMessage(), OC_Log::FATAL);
185				OC_User::setUserId(null);
186
187				// send http status 503
188				header('HTTP/1.1 503 Service Temporarily Unavailable');
189				header('Status: 503 Service Temporarily Unavailable');
190				OC_Template::printErrorPage('Failed to connect to database');
191				die();
192			}
193		}
194		return true;
195	}
196
197	/**
198	 * @brief Prepare a SQL query
199	 * @param string $query Query string
200	 * @param int $limit
201	 * @param int $offset
202	 * @param bool $isManipulation
203	 * @throws DatabaseException
204	 * @return \Doctrine\DBAL\Statement prepared SQL query
205	 *
206	 * SQL query via Doctrine prepare(), needs to be execute()'d!
207	 */
208	static public function prepare( $query , $limit = null, $offset = null, $isManipulation = null) {
209
210		if (!is_null($limit) && $limit != -1) {
211			if ($limit === -1) {
212				$limit = null;
213			}
214			$platform = self::$connection->getDatabasePlatform();
215			$query = $platform->modifyLimitQuery($query, $limit, $offset);
216		} else {
217			if (isset(self::$preparedQueries[$query]) and self::$cachingEnabled) {
218				return self::$preparedQueries[$query];
219			}
220		}
221		$rawQuery = $query;
222
223		// Optimize the query
224		$query = self::processQuery( $query );
225		if(OC_Config::getValue( "log_query", false)) {
226			OC_Log::write('core', 'DB prepare : '.$query, OC_Log::DEBUG);
227		}
228		self::connect();
229		
230		if ($isManipulation === null) {
231			//try to guess, so we return the number of rows on manipulations
232			$isManipulation = self::isManipulation($query);
233		}
234		
235		// return the result
236		if (self::$backend == self::BACKEND_DOCTRINE) {
237			try {
238				$result=self::$connection->prepare($query);
239			} catch(\Doctrine\DBAL\DBALException $e) {
240				throw new \DatabaseException($e->getMessage(), $query);
241			}
242			// differentiate between query and manipulation
243			$result=new OC_DB_StatementWrapper($result, $isManipulation);
244		}
245		if ((is_null($limit) || $limit == -1) and self::$cachingEnabled ) {
246			$type = OC_Config::getValue( "dbtype", "sqlite" );
247			if( $type != 'sqlite' && $type != 'sqlite3' ) {
248				self::$preparedQueries[$rawQuery] = $result;
249			}
250		}
251		return $result;
252	}
253	
254	/**
255	 * tries to guess the type of statement based on the first 10 characters
256	 * the current check allows some whitespace but does not work with IF EXISTS or other more complex statements
257	 * 
258	 * @param string $sql
259	 */
260	static public function isManipulation( $sql ) {
261		$selectOccurence = stripos ($sql, "SELECT");
262		if ($selectOccurence !== false && $selectOccurence < 10) {
263			return false;
264		}
265		$insertOccurence = stripos ($sql, "INSERT");
266		if ($insertOccurence !== false && $insertOccurence < 10) {
267			return true;
268		}
269		$updateOccurence = stripos ($sql, "UPDATE");
270		if ($updateOccurence !== false && $updateOccurence < 10) {
271			return true;
272		}
273		$deleteOccurance = stripos ($sql, "DELETE");
274		if ($deleteOccurance !== false && $deleteOccurance < 10) {
275			return true;
276		}
277		return false;
278	}
279	
280	/**
281	 * @brief execute a prepared statement, on error write log and throw exception
282	 * @param mixed $stmt OC_DB_StatementWrapper,
283	 *					  an array with 'sql' and optionally 'limit' and 'offset' keys
284	 *					.. or a simple sql query string
285	 * @param array $parameters
286	 * @return result
287	 * @throws DatabaseException
288	 */
289	static public function executeAudited( $stmt, array $parameters = null) {
290		if (is_string($stmt)) {
291			// convert to an array with 'sql'
292			if (stripos($stmt,'LIMIT') !== false) { //OFFSET requires LIMIT, se we only neet to check for LIMIT
293				// TODO try to convert LIMIT OFFSET notation to parameters, see fixLimitClauseForMSSQL
294				$message = 'LIMIT and OFFSET are forbidden for portability reasons,'
295						 . ' pass an array with \'limit\' and \'offset\' instead';
296				throw new DatabaseException($message);
297			}
298			$stmt = array('sql' => $stmt, 'limit' => null, 'offset' => null);
299		}
300		if (is_array($stmt)){
301			// convert to prepared statement
302			if ( ! array_key_exists('sql', $stmt) ) {
303				$message = 'statement array must at least contain key \'sql\'';
304				throw new DatabaseException($message);
305			}
306			if ( ! array_key_exists('limit', $stmt) ) {
307				$stmt['limit'] = null;
308			}
309			if ( ! array_key_exists('limit', $stmt) ) {
310				$stmt['offset'] = null;
311			}
312			$stmt = self::prepare($stmt['sql'], $stmt['limit'], $stmt['offset']);
313		}
314		self::raiseExceptionOnError($stmt, 'Could not prepare statement');
315		if ($stmt instanceof OC_DB_StatementWrapper) {
316			$result = $stmt->execute($parameters);
317			self::raiseExceptionOnError($result, 'Could not execute statement');
318		} else {
319			if (is_object($stmt)) {
320				$message = 'Expected a prepared statement or array got ' . get_class($stmt);
321			} else {
322				$message = 'Expected a prepared statement or array got ' . gettype($stmt);
323			}
324			throw new DatabaseException($message);
325		}
326		return $result;
327	}
328
329	/**
330	 * @brief gets last value of autoincrement
331	 * @param string $table The optional table name (will replace *PREFIX*) and add sequence suffix
332	 * @return int id
333	 * @throws DatabaseException
334	 *
335	 * \Doctrine\DBAL\Connection lastInsertId
336	 *
337	 * Call this method right after the insert command or other functions may
338	 * cause trouble!
339	 */
340	public static function insertid($table=null) {
341		self::connect();
342		$type = OC_Config::getValue( "dbtype", "sqlite" );
343		if( $type === 'pgsql' ) {
344			$result = self::executeAudited('SELECT lastval() AS id');
345			$row = $result->fetchRow();
346			self::raiseExceptionOnError($row, 'fetching row for insertid failed');
347			return $row['id'];
348		} else if( $type === 'mssql') {
349			if($table !== null) {
350				$prefix = OC_Config::getValue( "dbtableprefix", "oc_" );
351				$table = str_replace( '*PREFIX*', $prefix, $table );
352			}
353			return self::$connection->lastInsertId($table);
354		}
355		if( $type === 'oci' ) {
356			if($table !== null) {
357				$prefix = OC_Config::getValue( "dbtableprefix", "oc_" );
358				$suffix = '_SEQ';
359				$table = '"'.str_replace( '*PREFIX*', $prefix, $table ).$suffix.'"';
360			}
361			return self::$connection->lastInsertId($table);
362		} else {
363			if($table !== null) {
364				$prefix = OC_Config::getValue( "dbtableprefix", "oc_" );
365				$suffix = OC_Config::getValue( "dbsequencesuffix", "_id_seq" );
366				$table = str_replace( '*PREFIX*', $prefix, $table ).$suffix;
367			}
368			$result = self::$connection->lastInsertId($table);
369		}
370		self::raiseExceptionOnError($result, 'insertid failed');
371		return $result;
372	}
373
374	/**
375	 * @brief Disconnect
376	 * @return bool
377	 *
378	 * This is good bye, good bye, yeah!
379	 */
380	public static function disconnect() {
381		// Cut connection if required
382		if(self::$connection) {
383			self::$connection=false;
384			self::$DOCTRINE=false;
385		}
386
387		return true;
388	}
389
390	/** else {
391	 * @brief saves database scheme to xml file
392	 * @param string $file name of file
393	 * @param int $mode
394	 * @return bool
395	 *
396	 * TODO: write more documentation
397	 */
398	public static function getDbStructure( $file, $mode=MDB2_SCHEMA_DUMP_STRUCTURE) {
399		self::connectDoctrine();
400		return OC_DB_Schema::getDbStructure(self::$DOCTRINE, $file);
401	}
402
403	/**
404	 * @brief Creates tables from XML file
405	 * @param string $file file to read structure from
406	 * @return bool
407	 *
408	 * TODO: write more documentation
409	 */
410	public static function createDbFromStructure( $file ) {
411		self::connectDoctrine();
412		return OC_DB_Schema::createDbFromStructure(self::$DOCTRINE, $file);
413	}
414
415	/**
416	 * @brief update the database scheme
417	 * @param string $file file to read structure from
418	 * @throws Exception
419	 * @return bool
420	 */
421	public static function updateDbFromStructure($file) {
422		self::connectDoctrine();
423		try {
424			$result = OC_DB_Schema::updateDbFromStructure(self::$DOCTRINE, $file);
425		} catch (Exception $e) {
426			OC_Log::write('core', 'Failed to update database structure ('.$e.')', OC_Log::FATAL);
427			throw $e;
428		}
429		return $result;
430	}
431
432	/**
433	 * @brief Insert a row if a matching row doesn't exists.
434	 * @param string $table. The table to insert into in the form '*PREFIX*tableName'
435	 * @param array $input. An array of fieldname/value pairs
436	 * @returns int number of updated rows
437	 */
438	public static function insertIfNotExist($table, $input) {
439		self::connect();
440		$prefix = OC_Config::getValue( "dbtableprefix", "oc_" );
441		$table = str_replace( '*PREFIX*', $prefix, $table );
442
443		if(is_null(self::$type)) {
444			self::$type=OC_Config::getValue( "dbtype", "sqlite" );
445		}
446		$type = self::$type;
447
448		$query = '';
449		$inserts = array_values($input);
450		// differences in escaping of table names ('`' for mysql) and getting the current timestamp
451		if( $type == 'sqlite' || $type == 'sqlite3' ) {
452			// NOTE: For SQLite we have to use this clumsy approach
453			// otherwise all fieldnames used must have a unique key.
454			$query = 'SELECT * FROM `' . $table . '` WHERE ';
455			foreach($input as $key => $value) {
456				$query .= '`' . $key . '` = ? AND ';
457			}
458			$query = substr($query, 0, strlen($query) - 5);
459			try {
460				$result = self::executeAudited($query, $inserts);
461			} catch(DatabaseException $e) {
462				OC_Template::printExceptionErrorPage( $e );
463			}
464
465			if((int)$result->numRows() === 0) {
466				$query = 'INSERT INTO `' . $table . '` (`'
467					. implode('`,`', array_keys($input)) . '`) VALUES('
468					. str_repeat('?,', count($input)-1).'? ' . ')';
469			} else {
470				return 0; //no rows updated
471			}
472		} elseif( $type == 'pgsql' || $type == 'oci' || $type == 'mysql' || $type == 'mssql') {
473			$query = 'INSERT INTO `' .$table . '` (`'
474				. implode('`,`', array_keys($input)) . '`) SELECT '
475				. str_repeat('?,', count($input)-1).'? ' // Is there a prettier alternative?
476				. 'FROM `' . $table . '` WHERE ';
477
478			foreach($input as $key => $value) {
479				$query .= '`' . $key . '` = ? AND ';
480			}
481			$query = substr($query, 0, strlen($query) - 5);
482			$query .= ' HAVING COUNT(*) = 0';
483			$inserts = array_merge($inserts, $inserts);
484		}
485
486		try {
487			$result = self::executeAudited($query, $inserts);
488		} catch(\Doctrine\DBAL\DBALException $e) {
489			OC_Template::printExceptionErrorPage( $e );
490		}
491
492		return $result;
493	}
494
495	/**
496	 * @brief does minor changes to query
497	 * @param string $query Query string
498	 * @return string corrected query string
499	 *
500	 * This function replaces *PREFIX* with the value of $CONFIG_DBTABLEPREFIX
501	 * and replaces the ` with ' or " according to the database driver.
502	 */
503	private static function processQuery( $query ) {
504		self::connect();
505		// We need Database type and table prefix
506		if(is_null(self::$type)) {
507			self::$type=OC_Config::getValue( "dbtype", "sqlite" );
508		}
509		$type = self::$type;
510		if(is_null(self::$prefix)) {
511			self::$prefix=OC_Config::getValue( "dbtableprefix", "oc_" );
512		}
513		$prefix = self::$prefix;
514
515		// differences in escaping of table names ('`' for mysql) and getting the current timestamp
516		if( $type == 'sqlite' || $type == 'sqlite3' ) {
517			$query = str_replace( '`', '"', $query );
518			$query = str_ireplace( 'NOW()', 'datetime(\'now\')', $query );
519			$query = str_ireplace( 'UNIX_TIMESTAMP()', 'strftime(\'%s\',\'now\')', $query );
520		} elseif( $type == 'pgsql' ) {
521			$query = str_replace( '`', '"', $query );
522			$query = str_ireplace( 'UNIX_TIMESTAMP()', 'cast(extract(epoch from current_timestamp) as integer)',
523				$query );
524		} elseif( $type == 'oci'  ) {
525			$query = str_replace( '`', '"', $query );
526			$query = str_ireplace( 'NOW()', 'CURRENT_TIMESTAMP', $query );
527			$query = str_ireplace( 'UNIX_TIMESTAMP()', "(cast(sys_extract_utc(systimestamp) as date) - date'1970-01-01') * 86400", $query );
528		}elseif( $type == 'mssql' ) {
529			$query = preg_replace( "/\`(.*?)`/", "[$1]", $query );
530			$query = str_ireplace( 'NOW()', 'CURRENT_TIMESTAMP', $query );
531			$query = str_replace( 'LENGTH(', 'LEN(', $query );
532			$query = str_replace( 'SUBSTR(', 'SUBSTRING(', $query );
533			$query = str_ireplace( 'UNIX_TIMESTAMP()', 'DATEDIFF(second,{d \'1970-01-01\'},GETDATE())', $query );
534
535			$query = self::fixLimitClauseForMSSQL($query);
536		}
537
538		// replace table name prefix
539		$query = str_replace( '*PREFIX*', $prefix, $query );
540
541		return $query;
542	}
543
544	private static function fixLimitClauseForMSSQL($query) {
545		$limitLocation = stripos ($query, "LIMIT");
546
547		if ( $limitLocation === false ) {
548			return $query;
549		} 
550
551		// total == 0 means all results - not zero results
552		//
553		// First number is either total or offset, locate it by first space
554		//
555		$offset = substr ($query, $limitLocation + 5);
556		$offset = substr ($offset, 0, stripos ($offset, ' '));
557		$offset = trim ($offset);
558
559		// check for another parameter
560		if (stripos ($offset, ',') === false) {
561			// no more parameters
562			$offset = 0;
563			$total = intval ($offset);
564		} else {
565			// found another parameter
566			$offset = intval ($offset);
567
568			$total = substr ($query, $limitLocation + 5);
569			$total = substr ($total, stripos ($total, ','));
570
571			$total = substr ($total, 0, stripos ($total, ' '));
572			$total = intval ($total);
573		}
574
575		$query = trim (substr ($query, 0, $limitLocation));
576
577		if ($offset == 0 && $total !== 0) {
578			if (strpos($query, "SELECT") === false) {
579				$query = "TOP {$total} " . $query;
580			} else {
581				$query = preg_replace('/SELECT(\s*DISTINCT)?/Dsi', 'SELECT$1 TOP '.$total, $query);
582			}
583		} else if ($offset > 0) {
584			$query = preg_replace('/SELECT(\s*DISTINCT)?/Dsi', 'SELECT$1 TOP(10000000) ', $query);
585			$query = 'SELECT *
586					FROM (SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.line2) AS line3
587					FROM (SELECT 1 AS line2, sub1.* FROM (' . $query . ') AS sub1) as sub2) AS sub3';
588
589			if ($total > 0) {
590				$query .= ' WHERE line3 BETWEEN ' . ($offset + 1) . ' AND ' . ($offset + $total);
591			} else {
592				$query .= ' WHERE line3 > ' . $offset;
593			}
594		}
595		return $query;
596	}
597
598	/**
599	 * @brief drop a table
600	 * @param string $tableName the table to drop
601	 */
602	public static function dropTable($tableName) {
603		self::connectDoctrine();
604		OC_DB_Schema::dropTable(self::$DOCTRINE, $tableName);
605	}
606
607	/**
608	 * remove all tables defined in a database structure xml file
609	 * @param string $file the xml file describing the tables
610	 */
611	public static function removeDBStructure($file) {
612		self::connectDoctrine();
613		OC_DB_Schema::removeDBStructure(self::$DOCTRINE, $file);
614	}
615
616	/**
617	 * @brief replaces the ownCloud tables with a new set
618	 * @param $file string path to the MDB2 xml db export file
619	 */
620	public static function replaceDB( $file ) {
621		self::connectDoctrine();
622		OC_DB_Schema::replaceDB(self::$DOCTRINE, $file);
623	}
624
625	/**
626	 * Start a transaction
627	 * @return bool
628	 */
629	public static function beginTransaction() {
630		self::connect();
631		self::$connection->beginTransaction();
632		self::$inTransaction=true;
633		return true;
634	}
635
636	/**
637	 * Commit the database changes done during a transaction that is in progress
638	 * @return bool
639	 */
640	public static function commit() {
641		self::connect();
642		if(!self::$inTransaction) {
643			return false;
644		}
645		self::$connection->commit();
646		self::$inTransaction=false;
647		return true;
648	}
649
650	/**
651	 * check if a result is an error, works with Doctrine
652	 * @param mixed $result
653	 * @return bool
654	 */
655	public static function isError($result) {
656		//Doctrine returns false on error (and throws an exception)
657		return $result === false;
658	}
659	/**
660	 * check if a result is an error and throws an exception, works with \Doctrine\DBAL\DBALException
661	 * @param mixed $result
662	 * @param string $message
663	 * @return void
664	 * @throws DatabaseException
665	 */
666	public static function raiseExceptionOnError($result, $message = null) {
667		if(self::isError($result)) {
668			if ($message === null) {
669				$message = self::getErrorMessage($result);
670			} else {
671				$message .= ', Root cause:' . self::getErrorMessage($result);
672			}
673			throw new DatabaseException($message, self::getErrorCode($result));
674		}
675	}
676
677	public static function getErrorCode($error) {
678		$code = self::$connection->errorCode();
679		return $code;
680	}
681	/**
682	 * returns the error code and message as a string for logging
683	 * works with DoctrineException
684	 * @param mixed $error
685	 * @return string
686	 */
687	public static function getErrorMessage($error) {
688		if (self::$backend==self::BACKEND_DOCTRINE and self::$DOCTRINE) {
689			$msg = self::$DOCTRINE->errorCode() . ': ';
690			$errorInfo = self::$DOCTRINE->errorInfo();
691			if (is_array($errorInfo)) {
692				$msg .= 'SQLSTATE = '.$errorInfo[0] . ', ';
693				$msg .= 'Driver Code = '.$errorInfo[1] . ', ';
694				$msg .= 'Driver Message = '.$errorInfo[2];
695			}
696			return $msg;
697		}
698
699		return '';
700	}
701
702	/**
703	 * @param bool $enabled
704	 */
705	static public function enableCaching($enabled) {
706		if (!$enabled) {
707			self::$preparedQueries = array();
708		}
709		self::$cachingEnabled = $enabled;
710	}
711}