sql.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. <?php
  2. namespace DB;
  3. /*
  4. Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
  5. This file is part of the Fat-Free Framework (http://fatfree.sf.net).
  6. THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
  7. ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
  8. IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
  9. PURPOSE.
  10. Please see the license.txt file for more information.
  11. */
  12. //! PDO wrapper
  13. class SQL extends \PDO {
  14. private
  15. //! Database engine
  16. $engine,
  17. //! Database name
  18. $dbname,
  19. //! Transaction flag
  20. $trans=FALSE,
  21. //! Number of rows affected by query
  22. $rows=0,
  23. //! SQL log
  24. $log;
  25. /**
  26. * Begin SQL transaction
  27. * @return NULL
  28. **/
  29. function begin() {
  30. parent::begintransaction();
  31. $this->trans=TRUE;
  32. }
  33. /**
  34. * Rollback SQL transaction
  35. * @return NULL
  36. **/
  37. function rollback() {
  38. parent::rollback();
  39. $this->trans=FALSE;
  40. }
  41. /**
  42. * Commit SQL transaction
  43. * @return NULL
  44. **/
  45. function commit() {
  46. parent::commit();
  47. $this->trans=FALSE;
  48. }
  49. /**
  50. * Map data type of argument to a PDO constant
  51. * @return int
  52. * @param $val scalar
  53. **/
  54. function type($val) {
  55. switch (gettype($val)) {
  56. case 'NULL':
  57. return \PDO::PARAM_NULL;
  58. case 'boolean':
  59. return \PDO::PARAM_BOOL;
  60. case 'integer':
  61. return \PDO::PARAM_INT;
  62. default:
  63. return \PDO::PARAM_STR;
  64. }
  65. }
  66. /**
  67. * Execute SQL statement(s)
  68. * @return array|int|FALSE
  69. * @param $cmds string|array
  70. * @param $args string|array
  71. * @param $ttl int
  72. * @param $log bool
  73. **/
  74. function exec($cmds,$args=NULL,$ttl=0,$log=TRUE) {
  75. $auto=FALSE;
  76. if (is_null($args))
  77. $args=array();
  78. elseif (is_scalar($args))
  79. $args=array(1=>$args);
  80. if (is_array($cmds)) {
  81. if (count($args)<($count=count($cmds)))
  82. // Apply arguments to SQL commands
  83. $args=array_fill(0,$count,$args);
  84. if (!$this->trans) {
  85. $this->begin();
  86. $auto=TRUE;
  87. }
  88. }
  89. else {
  90. $cmds=array($cmds);
  91. $args=array($args);
  92. }
  93. $fw=\Base::instance();
  94. $cache=\Cache::instance();
  95. foreach (array_combine($cmds,$args) as $cmd=>$arg) {
  96. $now=microtime(TRUE);
  97. $keys=$vals=array();
  98. if ($fw->get('CACHE') && $ttl && ($cached=$cache->exists(
  99. $hash=$fw->hash($cmd.$fw->stringify($arg)).'.sql',
  100. $result)) && $cached[0]+$ttl>microtime(TRUE)) {
  101. foreach ($arg as $key=>$val) {
  102. $vals[]=$fw->stringify(is_array($val)?$val[0]:$val);
  103. $keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/';
  104. }
  105. }
  106. elseif (is_object($query=$this->prepare($cmd))) {
  107. foreach ($arg as $key=>$val) {
  108. if (is_array($val)) {
  109. // User-specified data type
  110. $query->bindvalue($key,$val[0],$val[1]);
  111. $vals[]=$fw->stringify($val[0]);
  112. }
  113. else {
  114. // Convert to PDO data type
  115. $query->bindvalue($key,$val,$this->type($val));
  116. $vals[]=$fw->stringify($val);
  117. }
  118. $keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/';
  119. }
  120. $query->execute();
  121. $error=$query->errorinfo();
  122. if ($error[0]!=\PDO::ERR_NONE) {
  123. // Statement-level error occurred
  124. if ($this->trans)
  125. $this->rollback();
  126. user_error('PDOStatement: '.$error[2]);
  127. }
  128. if (preg_match(
  129. '/\b(?:CALL|EXPLAIN|SELECT|PRAGMA|SHOW)\b/i',$cmd)) {
  130. $result=$query->fetchall(\PDO::FETCH_ASSOC);
  131. $this->rows=count($result);
  132. if ($fw->get('CACHE') && $ttl)
  133. // Save to cache backend
  134. $cache->set($hash,$result,$ttl);
  135. }
  136. else
  137. $this->rows=$result=$query->rowcount();
  138. $query->closecursor();
  139. unset($query);
  140. }
  141. else {
  142. $error=$this->errorinfo();
  143. if ($error[0]!=\PDO::ERR_NONE) {
  144. // PDO-level error occurred
  145. if ($this->trans)
  146. $this->rollback();
  147. user_error('PDO: '.$error[2]);
  148. }
  149. }
  150. if ($log)
  151. $this->log.=date('r').' ('.
  152. sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
  153. preg_replace($keys,$vals,$cmd,1).PHP_EOL;
  154. }
  155. if ($this->trans && $auto)
  156. $this->commit();
  157. return $result;
  158. }
  159. /**
  160. * Return number of rows affected by last query
  161. * @return int
  162. **/
  163. function count() {
  164. return $this->rows;
  165. }
  166. /**
  167. * Return SQL profiler results
  168. * @return string
  169. **/
  170. function log() {
  171. return $this->log;
  172. }
  173. /**
  174. * Retrieve schema of SQL table
  175. * @return array|FALSE
  176. * @param $table string
  177. * @param $ttl int
  178. **/
  179. function schema($table,$ttl=0) {
  180. // Supported engines
  181. $cmd=array(
  182. 'sqlite2?'=>array(
  183. 'PRAGMA table_info("'.$table.'");',
  184. 'name','type','dflt_value','notnull',0,'pk',1),
  185. 'mysql'=>array(
  186. 'SHOW columns FROM `'.$this->dbname.'`.`'.$table.'`;',
  187. 'Field','Type','Default','Null','YES','Key','PRI'),
  188. 'mssql|sqlsrv|sybase|dblib|pgsql|odbc'=>array(
  189. 'SELECT '.
  190. 'c.column_name AS field,'.
  191. 'c.data_type AS type,'.
  192. 'c.column_default AS defval,'.
  193. 'c.is_nullable AS nullable,'.
  194. 't.constraint_type AS pkey '.
  195. 'FROM information_schema.columns AS c '.
  196. 'LEFT OUTER JOIN '.
  197. 'information_schema.key_column_usage AS k '.
  198. 'ON '.
  199. 'c.table_name=k.table_name AND '.
  200. 'c.column_name=k.column_name '.
  201. ($this->dbname?
  202. ('AND '.
  203. ($this->engine=='pgsql'?
  204. 'c.table_catalog=k.table_catalog':
  205. 'c.table_schema=k.table_schema').' '):'').
  206. 'LEFT OUTER JOIN '.
  207. 'information_schema.table_constraints AS t ON '.
  208. 'k.table_name=t.table_name AND '.
  209. 'k.constraint_name=t.constraint_name '.
  210. ($this->dbname?
  211. ('AND '.
  212. ($this->engine=='pgsql'?
  213. 'k.table_catalog=t.table_catalog':
  214. 'k.table_schema=t.table_schema').' '):'').
  215. 'WHERE '.
  216. 'c.table_name='.$this->quote($table).' '.
  217. ($this->dbname?
  218. ('AND '.
  219. ($this->engine=='pgsql'?
  220. 'c.table_catalog':'c.table_schema').
  221. '='.$this->quote($this->dbname)):'').
  222. ';',
  223. 'field','type','defval','nullable','YES','pkey','PRIMARY KEY')
  224. );
  225. foreach ($cmd as $key=>$val)
  226. if (preg_match('/'.$key.'/',$this->engine)) {
  227. $rows=array();
  228. foreach ($this->exec($val[0],NULL,$ttl) as $row)
  229. $rows[$row[$val[1]]]=array(
  230. 'type'=>$row[$val[2]],
  231. 'pdo_type'=>
  232. preg_match('/int|bool/i',$row[$val[2]],$parts)?
  233. constant('\PDO::PARAM_'.strtoupper($parts[0])):
  234. \PDO::PARAM_STR,
  235. 'default'=>$row[$val[3]],
  236. 'nullable'=>$row[$val[4]]==$val[5],
  237. 'pkey'=>$row[$val[6]]==$val[7]
  238. );
  239. return $rows;
  240. }
  241. return FALSE;
  242. }
  243. /**
  244. * Quote string
  245. * @return string
  246. * @param $val mixed
  247. * @param $type int
  248. **/
  249. function quote($val,$type=\PDO::PARAM_STR) {
  250. return $this->engine=='odbc'?
  251. (is_string($val)?
  252. \Base::instance()->stringify(str_replace('\'','\'\'',$val)):
  253. $val):
  254. parent::quote($val,$type);
  255. }
  256. /**
  257. * Return database engine
  258. * @return string
  259. **/
  260. function driver() {
  261. return $this->engine;
  262. }
  263. /**
  264. * Return server version
  265. * @return string
  266. **/
  267. function version() {
  268. return parent::getattribute(parent::ATTR_SERVER_VERSION);
  269. }
  270. /**
  271. * Return database name
  272. * @return string
  273. **/
  274. function name() {
  275. return $this->dbname;
  276. }
  277. /**
  278. * Return quoted identifier name
  279. * @return string
  280. * @param $key
  281. **/
  282. function quotekey($key) {
  283. if ($this->engine=='mysql')
  284. $key="`".$key."`";
  285. elseif (preg_match('/sybase|dblib/',$this->engine))
  286. $key="'".$key."'";
  287. elseif (preg_match('/sqlite2?|pgsql/',$this->engine))
  288. $key='"'.$key.'"';
  289. elseif (preg_match('/mssql|sqlsrv|odbc/',$this->engine))
  290. $key="[".$key."]";
  291. elseif ($this->engine=='oci')
  292. $key='"'.strtoupper($key).'"';
  293. return $key;
  294. }
  295. /**
  296. * Instantiate class
  297. * @param $dsn string
  298. * @param $user string
  299. * @param $pw string
  300. * @param $options array
  301. **/
  302. function __construct($dsn,$user=NULL,$pw=NULL,array $options=NULL) {
  303. if (preg_match('/^.+?(?:dbname|database)=(.+?)(?=;|$)/i',$dsn,$parts))
  304. $this->dbname=$parts[1];
  305. if (!$options)
  306. $options=array();
  307. $options+=array(\PDO::ATTR_EMULATE_PREPARES=>FALSE);
  308. if (isset($parts[0]) && strstr($parts[0],':',TRUE)=='mysql')
  309. $options+=array(\PDO::MYSQL_ATTR_INIT_COMMAND=>'SET NAMES '.
  310. strtolower(str_replace('-','',
  311. \Base::instance()->get('ENCODING'))).';');
  312. parent::__construct($dsn,$user,$pw,$options);
  313. $this->engine=parent::getattribute(parent::ATTR_DRIVER_NAME);
  314. }
  315. }