diff --git a/DatabaseMysql.php b/DatabaseMysql.php index 3c83726..20440e8 100644 --- a/DatabaseMysql.php +++ b/DatabaseMysql.php @@ -3,7 +3,8 @@ /** * Very stable interface for MySQL - object-oriented at last :) * Select builder is inspired by MediaWiki's one. - * (c) Vitaliy Filippov, 2012 + * Also usable for querying SphinxQL. + * (c) Vitaliy Filippov, 2012-2013 */ if (!defined('MS_HASH')) @@ -31,6 +32,7 @@ class DatabaseMysql implements Database var $tableNames = array(); var $queryLogFile; var $reconnect = true; + var $ondestroy = 'commit'; var $queryCount = 0; var $link; @@ -46,8 +48,10 @@ class DatabaseMysql implements Database * dbname DB name to use * username Username * password Password - * reconnect Whether to reconnect on idle timeout [true] + * tableNames Table name mappings (virtual => real) * queryLogFile Path to query log file + * reconnect Whether to reconnect on idle timeout [true] + * ondestroy commit/rollback/none during destruction [commit] */ function __construct($options) { @@ -59,7 +63,9 @@ class DatabaseMysql implements Database 'username' => '', 'password' => '', 'reconnect' => true, + 'tableNames' => array(), 'queryLogFile' => '', + 'ondestroy' => 'commit', ); $options += $defOpts; if ($options['socket']) @@ -72,6 +78,16 @@ class DatabaseMysql implements Database } } + function __destruct() + { + $o = $this->ondestroy; + if (($o === 'commit' || $o === 'rollback') && $this->transactions) + { + $this->transactions = array(false); + $this->$o(); + } + } + function connect() { if ($this->socket !== NULL) @@ -109,6 +125,18 @@ class DatabaseMysql implements Database return "`".str_replace("`", "``", $name)."`"; } + function quoteInto($str, $params) + { + $i = 0; + $r = ''; + while (($p = strrpos($str, '?')) !== false) + { + $r = $this->quote($params[$i++]) . substr($str, $p+1) . $r; + $str = substr($str, 0, $p); + } + return $str.$r; + } + function quote($value) { if ($value === NULL) @@ -136,16 +164,73 @@ class DatabaseMysql implements Database $r = $this->link->query($sql, $fetchMode); if (!$r) { - if ($this->link->errno == 2006 && $this->reconnect) + if ($this->link->errno == 2006 && $this->reconnect && !$this->transactions) { // "MySQL server has gone away" - $this->link = false; + $this->connect(); + $r = $this->link->query($sql, $fetchMode); + if (!$r && $this->link->errno == 2006) + { + $this->link = false; + } + } + if (!$r) + { + throw new DatabaseException($this->link->error . "\nQuery: $sql", $this->link->errno); } - throw new DatabaseException($this->link->error, $this->link->errno); } return $r; } + function multi_select(array $queries, $format = 0) + { + $this->queryCount += count($queries); + $log = ''; + foreach ($queries as &$sql) + { + if (!is_string($sql)) + { + $sql = $this->select_builder($sql[0], $sql[1], $sql[2], @$sql[3]); + } + $log .= date("[Y-m-d H:i:s] ").$sql."\n"; + } + unset($sql); + if ($this->queryLogFile) + { + file_put_contents($this->queryLogFile, $log, FILE_APPEND); + } + if (!$this->link) + { + $this->connect(); + } + $sql = implode('; ', $queries); + $r = $this->link->multi_query($sql); + if (!$r) + { + if ($this->link->errno == 2006 && $this->reconnect && !$this->transactions) + { + // "MySQL server has gone away" + $this->connect(); + $r = $this->link->multi_query($sql); + if (!$r && $this->link->errno == 2006) + { + $this->link = false; + } + } + if (!$r) + { + throw new DatabaseException($this->link->error, $this->link->errno); + } + } + $results = array(); + foreach ($queries as $k => $q) + { + $results[$k] = $this->fetch_all($r, $format); + $this->link->next_result(); + } + return $results; + } + /** * Starts a transaction, supports nested calls and savepoints. * @param boolean $savepoint Creates savepoints only this parameter is true. @@ -220,6 +305,8 @@ class DatabaseMysql implements Database * 'conditional expression', * 'field_name' => 'value', * 'field_name' => array('one', 'of', 'values'), + * 'field_name < ?' => 'value', + * 'field_name < DATE_SUB(?, ?)' => array('arg1', 'arg2'), * 'field1,field2' => array(array(1, 2), array(3, 4)), * ) */ @@ -231,7 +318,21 @@ class DatabaseMysql implements Database foreach ($where as $k => $v) { if (ctype_digit("$k")) - $wh[] = $v; + { + if (is_array($v)) + { + $str = array_shift($v); + $wh[] = $this->quoteInto($str, $v); + } + else + { + $wh[] = $v; + } + } + elseif (($p = strrpos($k, '?')) !== false) + { + $wh[] = $this->quoteInto($k, (array)$v); + } elseif (is_array($v)) { if (!$v) @@ -248,6 +349,8 @@ class DatabaseMysql implements Database $wh[] = "$k IN (" . implode(",", $v) . ")"; } } + elseif (preg_match('/^-?\d+(\.\d+)?$/s', $v)) // int/float + $wh[] = "$k=$v"; elseif ($v !== NULL) $wh[] = "$k=".$this->quote($v); else @@ -255,6 +358,11 @@ class DatabaseMysql implements Database } if (!$wh) $where = '1'; + elseif (!$this->username && !$this->password && !$this->dbname) + { + // Sphinx supports neither brackets nor OR operator as of 2.0.6-release O_o + $where = join(' AND ', $wh); + } else $where = '(' . join(') AND (', $wh) . ')'; return $where; @@ -277,6 +385,9 @@ class DatabaseMysql implements Database * 'ORDER BY' => array($orderby_field1 => 'ASC', $orderby_field2 => 'DESC') * 'LIMIT' => array($limit, $offset) or array($limit) or just $limit * 'OFFSET' => $offset, for the case when 'LIMIT' is just $limit + * + * Sphinx Search extensions: + * 'WITHIN GROUP ORDER BY' => array($orderby_field => 'ASC') */ function select_builder($tables, $fields, $where, $options = NULL) { @@ -307,37 +418,16 @@ class DatabaseMysql implements Database $sql .= "$fields FROM $tables WHERE $where"; if (isset($options['GROUP BY'])) { - $g = $options['GROUP BY']; - if (is_array($g)) - { - $g1 = array(); - foreach ($g as $k => $v) - { - if (ctype_digit("$k")) - $g1[] = $v; - else - $g1[] = "$k $v"; - } - $g = join(',', $g1); - } - $sql .= " GROUP BY $g"; + $sql .= " GROUP BY ".$this->order_option($options['GROUP BY']); } if (isset($options['ORDER BY'])) { - $g = $options['ORDER BY']; - if (is_array($g)) - { - $g1 = array(); - foreach ($g as $k => $v) - { - if (ctype_digit("$k")) - $g1[] = $v; - else - $g1[] = "$k $v"; - } - $g = join(',', $g1); - } - $sql .= " ORDER BY $g"; + $sql .= " ORDER BY ".$this->order_option($options['ORDER BY']); + } + if (isset($options['WITHIN GROUP ORDER BY'])) + { + // Sphinx Search extension + $sql .= " WITHIN GROUP ORDER BY ".$this->order_option($options['WITHIN GROUP ORDER BY']); } $sql .= $this->limit_option($options); if (isset($options['FOR UPDATE'])) @@ -347,10 +437,30 @@ class DatabaseMysql implements Database return $sql; } + /** + * Handles ORDER BY or GROUP BY options + */ + protected function order_option($g) + { + if (is_array($g)) + { + $g1 = array(); + foreach ($g as $k => $v) + { + if (ctype_digit("$k")) + $g1[] = $v; + else + $g1[] = "$k $v"; + } + $g = join(',', $g1); + } + return $g; + } + /** * Handles a single LIMIT or LIMIT and OFFSET options. */ - function limit_option($options) + protected function limit_option($options) { if (isset($options['LIMIT'])) { @@ -457,10 +567,16 @@ class DatabaseMysql implements Database $sql = $this->select_builder($tables, $fields, $where, $options); if ($format & MS_RESULT) return $this->query($sql, MYSQLI_USE_RESULT); + $res = $this->query($sql); + return $this->fetch_all($res, $format); + } + + function fetch_all($res, $format = 0) + { if ($format & MS_LIST) - $r = $this->get_rows($sql); + $r = $this->get_rows($res); else - $r = $this->get_assocs($sql); + $r = $this->get_assocs($res); if (!$r) $r = array(); if ($format & MS_ROW) @@ -478,6 +594,7 @@ class DatabaseMysql implements Database } elseif ($format & MS_COL) { + $k = false; foreach ($r as $i => $v) { if (!$k) @@ -518,8 +635,9 @@ class DatabaseMysql implements Database * Builds an INSERT query. * @param string $table Table name to insert rows to. * @param array $rows Array of table rows to be inserted. - * @param boolean $onduplicatekey If true, include - * ON DUPLICATE KEY UPDATE column=VALUES(column) for all columns. + * @param boolean $onduplicatekey If true, create MySQL-specific "UPSERT" query using + * INSERT .. ON DUPLICATE KEY UPDATE column=VALUES(column) for all columns. + * @param boolean $replace If true, use REPLACE instead of INSERT */ function insert_builder($table, $rows, $onduplicatekey = false, $replace = false) { @@ -592,7 +710,7 @@ class DatabaseMysql implements Database return false; if (!is_array(@$rows[0])) $rows = array($rows); - $sql = $this->insert_builder($table, $rows, true, !empty($options['replace'])); + $sql = $this->insert_builder($table, $rows, empty($options['REPLACE']), !empty($options['REPLACE'])); } else { @@ -615,10 +733,16 @@ class DatabaseMysql implements Database return false; } - function get_rows($sql) + function replace($table, $rows, $where = NULL, $options = array()) + { + $options['REPLACE'] = true; + return $this->update($table, $rows, $where, $options); + } + + protected function get_rows($res) { $r = array(); - if ($res = $this->query($sql)) + if ($res) { while ($row = $res->fetch_row()) $r[] = $row; @@ -627,10 +751,10 @@ class DatabaseMysql implements Database return $r; } - function get_assocs($sql) + protected function get_assocs($res) { $r = array(); - if ($res = $this->query($sql)) + if ($res) { while ($row = $res->fetch_assoc()) $r[] = $row;