pquery.sql.php 14 KB

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