sql.php 9.9 KB

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