You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							523 lines
						
					
					
						
							13 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							523 lines
						
					
					
						
							13 KiB
						
					
					
				| <?php | |
| 
 | |
| /* | |
|  | |
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |
|  | |
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |
|  | |
| 	This 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 3 of the License, or later. | |
|  | |
| 	Fat-Free Framework 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. | |
|  | |
| 	You should have received a copy of the GNU General Public License along | |
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |
|  | |
| */ | |
| 
 | |
| namespace DB; | |
| 
 | |
| //! PDO wrapper | |
| class SQL { | |
| 
 | |
| 	//@{ Error messages | |
| 	const | |
| 		E_PKey='Table %s does not have a primary key'; | |
| 	//@} | |
|  | |
| 	const | |
| 		PARAM_FLOAT='float'; | |
| 
 | |
| 	protected | |
| 		//! UUID | |
| 		$uuid, | |
| 		//! Raw PDO | |
| 		$pdo, | |
| 		//! Data source name | |
| 		$dsn, | |
| 		//! Database engine | |
| 		$engine, | |
| 		//! Database name | |
| 		$dbname, | |
| 		//! Transaction flag | |
| 		$trans=FALSE, | |
| 		//! Number of rows affected by query | |
| 		$rows=0, | |
| 		//! SQL log | |
| 		$log; | |
| 
 | |
| 	/** | |
| 	*	Begin SQL transaction | |
| 	*	@return bool | |
| 	**/ | |
| 	function begin() { | |
| 		$out=$this->pdo->begintransaction(); | |
| 		$this->trans=TRUE; | |
| 		return $out; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Rollback SQL transaction | |
| 	*	@return bool | |
| 	**/ | |
| 	function rollback() { | |
| 		$out=$this->pdo->rollback(); | |
| 		$this->trans=FALSE; | |
| 		return $out; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Commit SQL transaction | |
| 	*	@return bool | |
| 	**/ | |
| 	function commit() { | |
| 		$out=$this->pdo->commit(); | |
| 		$this->trans=FALSE; | |
| 		return $out; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return transaction flag | |
| 	*	@return bool | |
| 	**/ | |
| 	function trans() { | |
| 		return $this->trans; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Map data type of argument to a PDO constant | |
| 	*	@return int | |
| 	*	@param $val scalar | |
| 	**/ | |
| 	function type($val) { | |
| 		switch (gettype($val)) { | |
| 			case 'NULL': | |
| 				return \PDO::PARAM_NULL; | |
| 			case 'boolean': | |
| 				return \PDO::PARAM_BOOL; | |
| 			case 'integer': | |
| 				return \PDO::PARAM_INT; | |
| 			case 'resource': | |
| 				return \PDO::PARAM_LOB; | |
| 			case 'float': | |
| 				return self::PARAM_FLOAT; | |
| 			default: | |
| 				return \PDO::PARAM_STR; | |
| 		} | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Cast value to PHP type | |
| 	*	@return mixed | |
| 	*	@param $type string | |
| 	*	@param $val mixed | |
| 	**/ | |
| 	function value($type,$val) { | |
| 		switch ($type) { | |
| 			case self::PARAM_FLOAT: | |
| 				if (!is_string($val)) | |
| 					$val=str_replace(',','.',$val); | |
| 				return $val; | |
| 			case \PDO::PARAM_NULL: | |
| 				return NULL; | |
| 			case \PDO::PARAM_INT: | |
| 				return (int)$val; | |
| 			case \PDO::PARAM_BOOL: | |
| 				return (bool)$val; | |
| 			case \PDO::PARAM_STR: | |
| 				return (string)$val; | |
| 			case \PDO::PARAM_LOB: | |
| 				return (binary)$val; | |
| 		} | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Execute SQL statement(s) | |
| 	*	@return array|int|FALSE | |
| 	*	@param $cmds string|array | |
| 	*	@param $args string|array | |
| 	*	@param $ttl int|array | |
| 	*	@param $log bool | |
| 	*	@param $stamp bool | |
| 	**/ | |
| 	function exec($cmds,$args=NULL,$ttl=0,$log=TRUE,$stamp=FALSE) { | |
| 		$tag=''; | |
| 		if (is_array($ttl)) | |
| 			list($ttl,$tag)=$ttl; | |
| 		$auto=FALSE; | |
| 		if (is_null($args)) | |
| 			$args=[]; | |
| 		elseif (is_scalar($args)) | |
| 			$args=[1=>$args]; | |
| 		if (is_array($cmds)) { | |
| 			if (count($args)<($count=count($cmds))) | |
| 				// Apply arguments to SQL commands | |
| 				$args=array_fill(0,$count,$args); | |
| 			if (!$this->trans) { | |
| 				$this->begin(); | |
| 				$auto=TRUE; | |
| 			} | |
| 		} | |
| 		else { | |
| 			$count=1; | |
| 			$cmds=[$cmds]; | |
| 			$args=[$args]; | |
| 		} | |
| 		if ($this->log===FALSE) | |
| 			$log=FALSE; | |
| 		$fw=\Base::instance(); | |
| 		$cache=\Cache::instance(); | |
| 		$result=FALSE; | |
| 		for ($i=0;$i<$count;$i++) { | |
| 			$cmd=$cmds[$i]; | |
| 			$arg=$args[$i]; | |
| 			// ensure 1-based arguments | |
| 			if (array_key_exists(0,$arg)) { | |
| 				array_unshift($arg,''); | |
| 				unset($arg[0]); | |
| 			} | |
| 			if (!preg_replace('/(^\s+|[\s;]+$)/','',$cmd)) | |
| 				continue; | |
| 			$now=microtime(TRUE); | |
| 			$keys=$vals=[]; | |
| 			if ($fw->CACHE && $ttl && ($cached=$cache->exists( | |
| 				$hash=$fw->hash($this->dsn.$cmd. | |
| 				$fw->stringify($arg)).($tag?'.'.$tag:'').'.sql',$result)) && | |
| 				$cached[0]+$ttl>microtime(TRUE)) { | |
| 				foreach ($arg as $key=>$val) { | |
| 					$vals[]=$fw->stringify(is_array($val)?$val[0]:$val); | |
| 					$keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key). | |
| 						'/'; | |
| 				} | |
| 				if ($log) | |
| 					$this->log.=($stamp?(date('r').' '):'').'('. | |
| 						sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. | |
| 						'[CACHED] '. | |
| 						preg_replace($keys,$vals, | |
| 							str_replace('?',chr(0).'?',$cmd),1).PHP_EOL; | |
| 			} | |
| 			elseif (is_object($query=$this->pdo->prepare($cmd))) { | |
| 				foreach ($arg as $key=>$val) { | |
| 					if (is_array($val)) { | |
| 						// User-specified data type | |
| 						$query->bindvalue($key,$val[0], | |
| 							$val[1]==self::PARAM_FLOAT?\PDO::PARAM_STR:$val[1]); | |
| 						$vals[]=$fw->stringify($this->value($val[1],$val[0])); | |
| 					} | |
| 					else { | |
| 						// Convert to PDO data type | |
| 						$query->bindvalue($key,$val, | |
| 							($type=$this->type($val))==self::PARAM_FLOAT? | |
| 								\PDO::PARAM_STR:$type); | |
| 						$vals[]=$fw->stringify($this->value($type,$val)); | |
| 					} | |
| 					$keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key). | |
| 						'/'; | |
| 				} | |
| 				if ($log) | |
| 					$this->log.=($stamp?(date('r').' '):'').'(-0ms) '. | |
| 						preg_replace($keys,$vals, | |
| 							str_replace('?',chr(0).'?',$cmd),1).PHP_EOL; | |
| 				$query->execute(); | |
| 				if ($log) | |
| 					$this->log=str_replace('(-0ms)', | |
| 						'('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms)', | |
| 						$this->log); | |
| 				if (($error=$query->errorinfo()) && $error[0]!=\PDO::ERR_NONE) { | |
| 					// Statement-level error occurred | |
| 					if ($this->trans) | |
| 						$this->rollback(); | |
| 					user_error('PDOStatement: '.$error[2],E_USER_ERROR); | |
| 				} | |
| 				if (preg_match('/(?:^[\s\(]*'. | |
| 					'(?:WITH|EXPLAIN|SELECT|PRAGMA|SHOW)|RETURNING)\b/is',$cmd) || | |
| 					(preg_match('/^\s*(?:CALL|EXEC)\b/is',$cmd) && | |
| 						$query->columnCount())) { | |
| 					$result=$query->fetchall(\PDO::FETCH_ASSOC); | |
| 					// Work around SQLite quote bug | |
| 					if (preg_match('/sqlite2?/',$this->engine)) | |
| 						foreach ($result as $pos=>$rec) { | |
| 							unset($result[$pos]); | |
| 							$result[$pos]=[]; | |
| 							foreach ($rec as $key=>$val) | |
| 								$result[$pos][trim($key,'\'"[]`')]=$val; | |
| 						} | |
| 					$this->rows=count($result); | |
| 					if ($fw->CACHE && $ttl) | |
| 						// Save to cache backend | |
| 						$cache->set($hash,$result,$ttl); | |
| 				} | |
| 				else | |
| 					$this->rows=$result=$query->rowcount(); | |
| 				$query->closecursor(); | |
| 				unset($query); | |
| 			} | |
| 			elseif (($error=$this->errorinfo()) && $error[0]!=\PDO::ERR_NONE) { | |
| 				// PDO-level error occurred | |
| 				if ($this->trans) | |
| 					$this->rollback(); | |
| 				user_error('PDO: '.$error[2],E_USER_ERROR); | |
| 			} | |
| 
 | |
| 		} | |
| 		if ($this->trans && $auto) | |
| 			$this->commit(); | |
| 		return $result; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return number of rows affected by last query | |
| 	*	@return int | |
| 	**/ | |
| 	function count() { | |
| 		return $this->rows; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return SQL profiler results (or disable logging) | |
| 	*	@return string | |
| 	*	@param $flag bool | |
| 	**/ | |
| 	function log($flag=TRUE) { | |
| 		if ($flag) | |
| 			return $this->log; | |
| 		$this->log=FALSE; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return TRUE if table exists | |
| 	*	@return bool | |
| 	*	@param $table string | |
| 	**/ | |
| 	function exists($table) { | |
| 		$mode=$this->pdo->getAttribute(\PDO::ATTR_ERRMODE); | |
| 		$this->pdo->setAttribute(\PDO::ATTR_ERRMODE,\PDO::ERRMODE_SILENT); | |
| 		$out=$this->pdo-> | |
| 			query('SELECT 1 FROM '.$this->quotekey($table).' LIMIT 1'); | |
| 		$this->pdo->setAttribute(\PDO::ATTR_ERRMODE,$mode); | |
| 		return is_object($out); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Retrieve schema of SQL table | |
| 	*	@return array|FALSE | |
| 	*	@param $table string | |
| 	*	@param $fields array|string | |
| 	*	@param $ttl int|array | |
| 	**/ | |
| 	function schema($table,$fields=NULL,$ttl=0) { | |
| 		$fw=\Base::instance(); | |
| 		$cache=\Cache::instance(); | |
| 		if ($fw->CACHE && $ttl && | |
| 			($cached=$cache->exists( | |
| 				$hash=$fw->hash($this->dsn.$table).'.schema',$result)) && | |
| 			$cached[0]+$ttl>microtime(TRUE)) | |
| 			return $result; | |
| 		if (strpos($table,'.')) | |
| 			list($schema,$table)=explode('.',$table); | |
| 		// Supported engines | |
| 		$cmd=[ | |
| 			'sqlite2?'=>[ | |
| 				'PRAGMA table_info(`'.$table.'`)', | |
| 				'name','type','dflt_value','notnull',0,'pk',TRUE], | |
| 			'mysql'=>[ | |
| 				'SHOW columns FROM `'.$this->dbname.'`.`'.$table.'`', | |
| 				'Field','Type','Default','Null','YES','Key','PRI'], | |
| 			'mssql|sqlsrv|sybase|dblib|pgsql|odbc'=>[ | |
| 				'SELECT '. | |
| 					'C.COLUMN_NAME AS field,'. | |
| 					'C.DATA_TYPE AS type,'. | |
| 					'C.COLUMN_DEFAULT AS defval,'. | |
| 					'C.IS_NULLABLE AS nullable,'. | |
| 					'T.CONSTRAINT_TYPE AS pkey '. | |
| 				'FROM INFORMATION_SCHEMA.COLUMNS AS C '. | |
| 				'LEFT OUTER JOIN '. | |
| 					'INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS K '. | |
| 					'ON '. | |
| 						'C.TABLE_NAME=K.TABLE_NAME AND '. | |
| 						'C.COLUMN_NAME=K.COLUMN_NAME AND '. | |
| 						'C.TABLE_SCHEMA=K.TABLE_SCHEMA '. | |
| 						($this->dbname? | |
| 							('AND C.TABLE_CATALOG=K.TABLE_CATALOG '):''). | |
| 				'LEFT OUTER JOIN '. | |
| 					'INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS T ON '. | |
| 						'K.TABLE_NAME=T.TABLE_NAME AND '. | |
| 						'K.CONSTRAINT_NAME=T.CONSTRAINT_NAME AND '. | |
| 						'K.TABLE_SCHEMA=T.TABLE_SCHEMA '. | |
| 						($this->dbname? | |
| 							('AND K.TABLE_CATALOG=T.TABLE_CATALOG '):''). | |
| 				'WHERE '. | |
| 					'C.TABLE_NAME='.$this->quote($table). | |
| 					($this->dbname? | |
| 						(' AND C.TABLE_CATALOG='. | |
| 							$this->quote($this->dbname)):''), | |
| 				'field','type','defval','nullable','YES','pkey','PRIMARY KEY'], | |
| 			'oci'=>[ | |
| 				'SELECT c.column_name AS field, '. | |
| 					'c.data_type AS type, '. | |
| 					'c.data_default AS defval, '. | |
| 					'c.nullable AS nullable, '. | |
| 					'(SELECT t.constraint_type '. | |
| 						'FROM all_cons_columns acc '. | |
| 						'LEFT OUTER JOIN all_constraints t '. | |
| 						'ON acc.constraint_name=t.constraint_name '. | |
| 						'WHERE acc.table_name='.$this->quote($table).' '. | |
| 						'AND acc.column_name=c.column_name '. | |
| 						'AND constraint_type='.$this->quote('P').') AS pkey '. | |
| 				'FROM all_tab_cols c '. | |
| 				'WHERE c.table_name='.$this->quote($table), | |
| 				'FIELD','TYPE','DEFVAL','NULLABLE','Y','PKEY','P'] | |
| 		]; | |
| 		if (is_string($fields)) | |
| 			$fields=\Base::instance()->split($fields); | |
| 		$conv=[ | |
| 			'int\b|integer'=>\PDO::PARAM_INT, | |
| 			'bool'=>\PDO::PARAM_BOOL, | |
| 			'blob|bytea|image|binary'=>\PDO::PARAM_LOB, | |
| 			'float|real|double|decimal|numeric'=>self::PARAM_FLOAT, | |
| 			'.+'=>\PDO::PARAM_STR | |
| 		]; | |
| 		foreach ($cmd as $key=>$val) | |
| 			if (preg_match('/'.$key.'/',$this->engine)) { | |
| 				$rows=[]; | |
| 				foreach ($this->exec($val[0],NULL) as $row) | |
| 					if (!$fields || in_array($row[$val[1]],$fields)) { | |
| 						foreach ($conv as $regex=>$type) | |
| 							if (preg_match('/'.$regex.'/i',$row[$val[2]])) | |
| 								break; | |
| 						$rows[$row[$val[1]]]=[ | |
| 							'type'=>$row[$val[2]], | |
| 							'pdo_type'=>$type, | |
| 							'default'=>is_string($row[$val[3]])? | |
| 								preg_replace('/^\s*([\'"])(.*)\1\s*/','\2', | |
| 								$row[$val[3]]):$row[$val[3]], | |
| 							'nullable'=>$row[$val[4]]==$val[5], | |
| 							'pkey'=>$row[$val[6]]==$val[7] | |
| 						]; | |
| 					} | |
| 				if ($fw->CACHE && $ttl) | |
| 					// Save to cache backend | |
| 					$cache->set($hash,$rows,$ttl); | |
| 				return $rows; | |
| 			} | |
| 		user_error(sprintf(self::E_PKey,$table),E_USER_ERROR); | |
| 		return FALSE; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Quote string | |
| 	*	@return string | |
| 	*	@param $val mixed | |
| 	*	@param $type int | |
| 	**/ | |
| 	function quote($val,$type=\PDO::PARAM_STR) { | |
| 		return $this->engine=='odbc'? | |
| 			(is_string($val)? | |
| 				\Base::instance()->stringify(str_replace('\'','\'\'',$val)): | |
| 				$val): | |
| 			$this->pdo->quote($val,$type); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return UUID | |
| 	*	@return string | |
| 	**/ | |
| 	function uuid() { | |
| 		return $this->uuid; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return parent object | |
| 	*	@return \PDO | |
| 	**/ | |
| 	function pdo() { | |
| 		return $this->pdo; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return database engine | |
| 	*	@return string | |
| 	**/ | |
| 	function driver() { | |
| 		return $this->engine; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return server version | |
| 	*	@return string | |
| 	**/ | |
| 	function version() { | |
| 		return $this->pdo->getattribute(\PDO::ATTR_SERVER_VERSION); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return database name | |
| 	*	@return string | |
| 	**/ | |
| 	function name() { | |
| 		return $this->dbname; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return quoted identifier name | |
| 	*	@return string | |
| 	*	@param $key | |
| 	*	@param bool $split | |
| 	 **/ | |
| 	function quotekey($key, $split=TRUE) { | |
| 		$delims=[ | |
| 			'sqlite2?|mysql'=>'``', | |
| 			'pgsql|oci'=>'""', | |
| 			'mssql|sqlsrv|odbc|sybase|dblib'=>'[]' | |
| 		]; | |
| 		$use=''; | |
| 		foreach ($delims as $engine=>$delim) | |
| 			if (preg_match('/'.$engine.'/',$this->engine)) { | |
| 				$use=$delim; | |
| 				break; | |
| 			} | |
| 		return $use[0].($split ? implode($use[1].'.'.$use[0],explode('.',$key)) | |
| 			: $key).$use[1]; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Redirect call to PDO object | |
| 	*	@return mixed | |
| 	*	@param $func string | |
| 	*	@param $args array | |
| 	**/ | |
| 	function __call($func,array $args) { | |
| 		return call_user_func_array([$this->pdo,$func],$args); | |
| 	} | |
| 
 | |
| 	//! Prohibit cloning | |
| 	private function __clone() { | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Instantiate class | |
| 	*	@param $dsn string | |
| 	*	@param $user string | |
| 	*	@param $pw string | |
| 	*	@param $options array | |
| 	**/ | |
| 	function __construct($dsn,$user=NULL,$pw=NULL,array $options=NULL) { | |
| 		$fw=\Base::instance(); | |
| 		$this->uuid=$fw->hash($this->dsn=$dsn); | |
| 		if (preg_match('/^.+?(?:dbname|database)=(.+?)(?=;|$)/is',$dsn,$parts)) | |
| 			$this->dbname=$parts[1]; | |
| 		if (!$options) | |
| 			$options=[]; | |
| 		if (isset($parts[0]) && strstr($parts[0],':',TRUE)=='mysql') | |
| 			$options+=[\PDO::MYSQL_ATTR_INIT_COMMAND=>'SET NAMES '. | |
| 				strtolower(str_replace('-','',$fw->ENCODING)).';']; | |
| 		$this->pdo=new \PDO($dsn,$user,$pw,$options); | |
| 		$this->engine=$this->pdo->getattribute(\PDO::ATTR_DRIVER_NAME); | |
| 	} | |
| 
 | |
| }
 | |
| 
 |