pquery.sql.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. <?php
  2. /**
  3. * pQuery plugin for executing MySQL queries.
  4. *
  5. * @package pQuery
  6. */
  7. /**
  8. * @todo Documentation
  9. * @property $query The query that is being evaluated.
  10. */
  11. class pQuerySql extends pQuery {
  12. static $accepts = array('string' => 'parse_query', 'resource');
  13. /**
  14. * The pattern to use for specifying variables in a query.
  15. */
  16. const VARIABLE_PATTERN = '/\[\s*%s\s*\]/';
  17. /**
  18. * The default row fetching type, one of 'assoc', 'object' or 'array'.
  19. */
  20. const DEFAULT_FETCH_TYPE = 'assoc';
  21. /**
  22. * The MySQL link identifier.
  23. *
  24. * @var resource
  25. */
  26. static $link;
  27. /**
  28. * @see pQuery::$variable_alias
  29. * @var string|array
  30. */
  31. static $variable_alias = 'query';
  32. /**
  33. * Database login data, should be an associative array containing
  34. * values for 'host', 'username', 'password' and 'dbname'
  35. *
  36. * @var array
  37. */
  38. static $login_data = array();
  39. /**
  40. * The result of the current query.
  41. *
  42. * @var resource|bool
  43. */
  44. var $result;
  45. /**
  46. * Indicates whether the query has been executed yet.
  47. *
  48. * @var bool
  49. */
  50. var $executed;
  51. /**
  52. * Parse the given query string.
  53. */
  54. function parse_query() {
  55. $args = $this->arguments;
  56. if( !count($args) )
  57. return;
  58. // Parse arguments as variables. Arrays and
  59. // Replace variable indices by names equal to their indices
  60. $variables = array();
  61. foreach( $args as $i => $argument ) {
  62. if( is_array($argument) )
  63. $variables = array_merge($variables, $argument);
  64. else
  65. $variables[$i] = $argument;
  66. }
  67. // Replace variables by their escaped values
  68. $this->set($variables);
  69. }
  70. /**
  71. * Replace a set of variables with their (optionally escaped)
  72. * values in the current query.
  73. *
  74. * @param array $variables The variables to replace.
  75. * @param bool $escape Whether to escape the variable values.
  76. * @returns pQuerySql The current query object.
  77. */
  78. function replace_variables($variables, $escape) {
  79. $patterns = array_map('pQuerySql::variable_pattern', array_keys($variables));
  80. $escape && $variables = array_map('pQuerySql::escape', $variables);
  81. $this->variable = preg_replace($patterns, $variables, $this->variable);
  82. $this->executed = false;
  83. return $this;
  84. }
  85. /**
  86. * Replace a set of variables with their escaped values in the current query.
  87. *
  88. * @param array $variables The variables to replace.
  89. * @returns pQuerySql The current query object.
  90. */
  91. function set($variables) {
  92. return $this->replace_variables($variables, true);
  93. }
  94. /**
  95. * Replace a set of variables with their non-escaped values in the current query.
  96. *
  97. * @param array $variables The variables to replace.
  98. * @returns pQuerySql The current query object.
  99. */
  100. function set_unescaped($variables) {
  101. return $this->replace_variables($variables, false);
  102. }
  103. /**
  104. * Transform a variable name to a regex to be used as a replacement
  105. * pattern in a query.
  106. *
  107. * @param string $name The variable name to transform.
  108. * @returns string The variable's replacement pattern.
  109. */
  110. static function variable_pattern($name) {
  111. return sprintf(self::VARIABLE_PATTERN, $name);
  112. }
  113. /**
  114. * Execute the current query.
  115. *
  116. * @returns pQuerySql The current query object.
  117. */
  118. function execute() {
  119. self::assert_connection();
  120. $result = mysql_query($this->query, self::$link);
  121. if( !$result )
  122. return self::mysql_error($this->query);
  123. $this->result = $result;
  124. $this->executed = true;
  125. return $this;
  126. }
  127. /**
  128. * Find the number of resulting rows of the current query.
  129. *
  130. * @returns int The number of result rows.
  131. * @uses mysql_num_rows
  132. */
  133. function num_rows() {
  134. $this->assert_execution();
  135. return is_resource($this->result) ? mysql_num_rows($this->result) : 0;
  136. }
  137. /**
  138. * Find the number of rows affected by the current query.
  139. *
  140. * @returns int The number of affected rows.
  141. * @uses mysql_affected_rows
  142. */
  143. function affected_rows() {
  144. $this->assert_execution();
  145. return is_resource($this->result) ? mysql_affected_rows($this->result) : 0;
  146. }
  147. /**
  148. * Fetch a row from the current result.
  149. *
  150. * @param string $type The format of the result row.
  151. * @returns mixed The fetched row in the requested format.
  152. */
  153. function fetch($type=self::DEFAULT_FETCH_TYPE) {
  154. $this->assert_execution();
  155. if( !is_resource($this->result) )
  156. return self::error('Query result is not a resource.');
  157. $func = 'mysql_fetch_'.$type;
  158. if( !function_exists($func) )
  159. return self::error('Fetch type "%s" is not supported.', $type);
  160. return $func($this->result);
  161. }
  162. /**
  163. * Fetch all rows from the current result.
  164. *
  165. * @param string $type The format of the result rows.
  166. * @returns array The result set.
  167. */
  168. function fetch_all($type=self::DEFAULT_FETCH_TYPE) {
  169. $results = array();
  170. while( ($row = $this->fetch($type)) !== false )
  171. $results[] = $row;
  172. return $results;
  173. return $func($this->result);
  174. }
  175. /**
  176. * Assert that the current query has been executed.
  177. */
  178. function assert_execution() {
  179. $this->executed || $this->execute();
  180. }
  181. /**
  182. * Set database server login data.
  183. *
  184. * @param string $host The database server to connect with.
  185. * @param string $username The username to login with on the database server.
  186. * @param string $password The password to login with on the database server.
  187. * @param string $dbname The name of the database to select after connecting to the server.
  188. */
  189. static function set_login_data($host, $username, $password, $dbname) {
  190. // Close any existing connection
  191. self::disconnect();
  192. self::$login_data = array_merge(self::$login_data,
  193. compact('host', 'username', 'password', 'dbname'));
  194. }
  195. /**
  196. * Assert that the database server config has been set.
  197. */
  198. static function assert_login_data_exist() {
  199. if( !isset(self::$login_data['host']) )
  200. return self::error('No MySQL database server host is specified.');
  201. if( !isset(self::$login_data['username']) )
  202. return self::error('No username is specified for the MySQL server.');
  203. if( !isset(self::$login_data['password']) )
  204. return self::error('No password is specified for the MySQL server.');
  205. if( !isset(self::$login_data['host']) )
  206. return self::error('No MySQL database name is specified.');
  207. }
  208. /**
  209. * Assert that the MySQL connection is opened.
  210. *
  211. * @uses mysql_connect,mysql_select_db
  212. */
  213. static function assert_connection() {
  214. // Return if the connection has already been opened
  215. if( self::$link )
  216. return;
  217. self::assert_login_data_exist();
  218. // Connect to the database
  219. $c = self::$login_data;
  220. $link = @mysql_connect($c['host'], $c['username'], $c['password']);
  221. if( $link === false )
  222. return self::mysql_error();
  223. self::$link = $link;
  224. // Select the correct database
  225. if( !@mysql_select_db($c['dbname'], $link) )
  226. return self::mysql_error();
  227. }
  228. /**
  229. * Close the current connection, if any.
  230. *
  231. * @uses mysql_close
  232. */
  233. static function disconnect() {
  234. // Return if the connection has already been closed
  235. if( !self::$link )
  236. return;
  237. mysql_close(self::$link);
  238. self::$link = null;
  239. }
  240. /**
  241. * Echo the latest MySQL error.
  242. * If a query is specified and debug mode is on, add the query to the error message.
  243. *
  244. * @param string $query The query that was executed, if any.
  245. * @codeCoverageIgnore
  246. */
  247. static function mysql_error($query='') {
  248. $error = sprintf('MySQL error %d: %s.', mysql_errno(), mysql_error());
  249. PQUERY_DEBUG && $error .= "\nQuery: ".$query;
  250. self::error($error);
  251. }
  252. /**
  253. * Escape a string for safe use in a query.
  254. *
  255. * @param string $value The string to escape.
  256. * @uses mysql_real_escape_string
  257. */
  258. static function escape($value) {
  259. self::assert_connection();
  260. return mysql_real_escape_string($value, self::$link);
  261. }
  262. /**
  263. * Select all records from the given table that match the constraints.
  264. *
  265. * @param string $table The table to select from.
  266. * @param array $constraints Column names pointing to their values.
  267. * @param bool $escape Whether to escape the constraint values. Defaults to TRUE.
  268. * @returns pQuerySql The created query instance.
  269. */
  270. static function select($table, $columns, $constraints=array(), $escape=true) {
  271. return _sql("SELECT [columns] FROM `[table]` WHERE [constraints];")
  272. ->set_unescaped(array(
  273. 'columns' => self::parse_columns($columns),
  274. 'table' => $table,
  275. 'constraints' => self::parse_constraints($constraints, $escape)
  276. ));
  277. }
  278. /**
  279. * Apply the given changes to all records in the given table that
  280. * match the constraints.
  281. *
  282. * @param string $table The table to update in.
  283. * @param array $changes Column names pointing to their new values.
  284. * @param array $constraints Column names pointing to their values.
  285. * @param bool $escape Whether to escape the changed values and the
  286. * constraint values. Defaults to TRUE.
  287. * @returns pQuerySql The created query instance.
  288. */
  289. static function update($table, $changes, $constraints=array(), $escape=true) {
  290. // Parse changes
  291. $escaped_changes = array();
  292. foreach( $changes as $column => $value ) {
  293. $column = self::escape_column($column);
  294. $value = self::escape_value($value);
  295. $escaped_changes[] = "$column = $value";
  296. }
  297. return _sql("UPDATE `[table]` SET [changes] WHERE [constraints];")
  298. ->set_unescaped(array(
  299. 'table' => $table,
  300. 'changes' => implode(", ", $escaped_changes),
  301. 'constraints' => self::parse_constraints($constraints, $escape)
  302. ));
  303. }
  304. /**
  305. * Insert a record in the given table.
  306. *
  307. * @param string $table The table to insert into.
  308. * @param array $values The values to insert, pointed to by their column names.
  309. * @param bool $escape Whether to escape the values. Defaults to TRUE.
  310. * @returns pQuerySql The created query instance.
  311. */
  312. static function insert_row($table, $values, $escape=true) {
  313. $columns = array_keys($values);
  314. $escape && $values = array_map('pQuerySql::escape', $values);
  315. return _sql("INSERT INTO `[table]`([columns]) VALUES([values]);")
  316. ->set_unescaped(array(
  317. 'table' => $table,
  318. 'columns' => "`".implode("`, `", $columns)."`",
  319. 'values' => "'".implode("', '", $values)."'"
  320. ));
  321. }
  322. /**
  323. * Delete all records from the given table that match the constraints.
  324. *
  325. * @param string $table The table to delete from.
  326. * @param array $constraints Column names pointing to their values.
  327. * @param bool $escape Whether to escape the constraint values. Defaults to TRUE.
  328. * @returns pQuerySql The created query instance.
  329. */
  330. static function delete($table, $constraints, $escape=true) {
  331. return _sql("DELETE FROM `[table]` WHERE [constraints];")
  332. ->set_unescaped(array(
  333. 'table' => $table,
  334. 'constraints' => self::parse_constraints($constraints, $escape)
  335. ));
  336. }
  337. /**
  338. * Parse a list of column names.
  339. *
  340. * @param string|array $columns One of:
  341. * - '*': Returns itself.
  342. * - string: Treated as a column name or aggregate function, and
  343. * escaped as such with backticks.
  344. * - array:
  345. * @returns string The parsed columns.
  346. */
  347. static function parse_columns($columns) {
  348. if( $columns == '*' )
  349. return '*';
  350. if( is_string($columns) )
  351. return self::escape_column($columns);
  352. if( !is_array($columns) )
  353. return self::error('Unknown columns type.');
  354. $escaped_columns = array();
  355. foreach( $columns as $key => $value ) {
  356. if( is_numeric($key) ) {
  357. // Simple column name
  358. $escaped_columns[] = self::escape_column($value);
  359. } else {
  360. // MySQL 'AS' construction
  361. $escaped_columns[] = self::escape_column($key)." AS `".$value."`";
  362. }
  363. }
  364. return implode(", ", $escaped_columns);
  365. }
  366. /**
  367. * Escape a column name to be safely used (and in a tidy manner) in a column list.
  368. *
  369. * @param string $column The column name to escape.
  370. * @returns string The escaped column.
  371. */
  372. static function escape_column($column) {
  373. if( preg_match('/^`.*?`$/', $column) ) {
  374. // `column` -> `column`
  375. return $column;
  376. } elseif( preg_match('/^(\w+)\.(\w+)$/', $column, $m) ) {
  377. // table.column -> `table`.`column`
  378. list($table, $column) = array_slice($m, 1);
  379. return "`$table`.`$column`";
  380. } elseif( preg_match('/^(\w+)\(([^)]+)\)$/', $column, $m) ) {
  381. // function(name) -> FUNCTION(`name`)
  382. // function(`name`) -> FUNCTION(`name`)
  383. list($aggregate_function, $column) = array_slice($m, 1);
  384. return strtoupper($aggregate_function)."(".self::escape_column($column).")";
  385. }
  386. // column -> `column`
  387. return "`$column`";
  388. }
  389. /**
  390. * Escape a value so that it can be saved safely.
  391. *
  392. * @param string $value The value to escape.
  393. * @returns string The escaped value.
  394. */
  395. static function escape_value($value) {
  396. if( preg_match("/^'[^']*'$/", $value) ) {
  397. // 'value' -> 'value'
  398. return $value;
  399. }
  400. // value -> 'value'
  401. return "'$value'";
  402. }
  403. /**
  404. * Parse a list of constraints.
  405. *
  406. * @param mixed $constraints One of:
  407. * - A variable that evaluates as "empty", which will yield the string '1'.
  408. * - A string, which will be returned unchanged.
  409. * - A list of column names pointing to their values. A value may be
  410. * a list, wich will yield a query with the MySQL 'IN' selector.
  411. * @param bool $escape Whether to escape the values.
  412. * @returns string The parsed constraints.
  413. */
  414. static function parse_constraints($constraints, $escape) {
  415. if( empty($constraints) )
  416. return "1";
  417. if( is_string($constraints) )
  418. return $constraints;
  419. if( !is_array($constraints) )
  420. return self::error('Unknown constraints type.');
  421. $conditions = array();
  422. foreach( $constraints as $column => $value ) {
  423. $condition = "`$column` ";
  424. if( is_array($value) ) {
  425. $escape && $value = array_map('pQuerySql::escape', $value);
  426. $value = array_map('pQuerySql::escape_value', $value);
  427. $condition .= "IN (".implode(", ", $value).")";
  428. } else {
  429. $escape && $value = self::escape($value);
  430. $condition .= "= ".self::escape_value($value);
  431. }
  432. $conditions[] = $condition;
  433. }
  434. return implode(" AND ", $conditions);
  435. }
  436. }
  437. /**
  438. * Shortcut constructor for {@link pQuerySql}.
  439. *
  440. * @param string $query A MySQL query to evaluate.
  441. * @returns pQuerySql A new SQL query instance.
  442. */
  443. function _sql($query /* [ , $arg1, ... ] */) {
  444. $args = func_get_args();
  445. $query = array_shift($args);
  446. array_unshift($args, 'sql', $query);
  447. return call_user_func_array('pQuery::create', $args);
  448. }
  449. pQuery::extend('pQuerySql', 'sql');
  450. ?>