php-db-drivers/DatabaseMysql.php

894 lines
28 KiB
PHP
Raw Normal View History

2012-10-01 02:44:34 +04:00
<?php
/**
* Very stable interface for MySQL - object-oriented at last :)
* Select builder is inspired by MediaWiki's one.
* Also usable for querying SphinxQL.
2018-09-10 14:26:14 +03:00
* Version: 2018-09-10
* (c) Vitaliy Filippov, 2012-2018
2012-10-01 02:44:34 +04:00
*/
2012-10-01 02:44:37 +04:00
if (!defined('MS_HASH'))
{
define('MS_HASH', 0);
define('MS_LIST', 1);
define('MS_ROW', 2);
define('MS_ONE', 2);
define('MS_COL', 4);
define('MS_VALUE', 6);
define('MS_RESULT', 8);
}
2012-10-01 02:44:34 +04:00
2016-05-29 17:36:52 +03:00
if (!class_exists('DatabaseException'))
{
class DatabaseException extends Exception {}
}
class DatabaseMysqlException extends DatabaseException
2015-05-12 18:31:49 +03:00
{
function isDuplicateKey()
{
return $this->getCode() == 1062;
}
}
2012-10-10 02:20:40 +04:00
if (!interface_exists('Database'))
2012-10-01 02:44:34 +04:00
{
interface Database {}
}
class DatabaseMysql implements Database
{
2018-09-10 14:26:14 +03:00
var $host, $port, $socket, $username, $password, $dbname, $isSphinx;
2012-10-01 02:44:37 +04:00
2012-10-01 02:44:34 +04:00
var $tableNames = array();
2016-05-29 17:36:52 +03:00
var $init = array();
2015-05-12 18:31:49 +03:00
var $queryLogFile, $loggedQueries = '';
2012-10-10 02:20:40 +04:00
var $reconnect = true;
2013-03-30 00:57:58 +04:00
var $autoBegin;
var $ondestroy = 'commit';
2012-10-10 02:20:40 +04:00
var $queryCount = 0;
var $link;
2013-05-08 13:21:50 +04:00
var $transactions = array();
2012-10-01 02:44:34 +04:00
2012-10-01 02:44:37 +04:00
/**
* Creates a MySQL connection object.
*
* @param array $options Possible options:
* host Host name or IP address to connect to [localhost]
* socket Path to UNIX socket to connect to [/var/run/mysqld/mysqld.sock]
* port TCP port to connect to [3306]
* dbname DB name to use
* username Username
* password Password
* tableNames Table name mappings (virtual => real)
* queryLogFile Path to query log file
* reconnect Whether to reconnect on idle timeout [true]
2016-05-29 17:36:52 +03:00
* autoBegin Whether to automatically begin a transaction on first query [false]
* ondestroy commit/rollback/none during destruction [commit]
2016-05-29 17:36:52 +03:00
* init Initialisation queries (array)
2012-10-01 02:44:37 +04:00
*/
function __construct($options)
2012-10-01 02:44:34 +04:00
{
$defOpts = array(
'host' => 'localhost',
'port' => 3306,
'socket' => '/var/run/mysqld/mysqld.sock',
'dbname' => '',
'username' => '',
'password' => '',
'reconnect' => true,
'tableNames' => array(),
'queryLogFile' => '',
2013-03-30 00:57:58 +04:00
'autoBegin' => false,
'ondestroy' => 'commit',
2016-05-29 17:36:52 +03:00
'init' => array(),
);
$options += $defOpts;
if ($options['socket'])
2012-10-01 02:44:34 +04:00
{
$options['host'] = 'localhost';
2012-10-01 02:44:34 +04:00
}
foreach ($defOpts as $k => $v)
2012-10-01 02:44:34 +04:00
{
$this->$k = $options[$k];
2012-10-01 02:44:37 +04:00
}
2018-09-10 14:26:14 +03:00
$this->isSphinx = !$this->username && !$this->password && !$this->dbname;
if (!$this->isSphinx)
2016-05-29 17:36:52 +03:00
{
// READ COMMITTED is more consistent for average usage
$this->init[] = 'SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED';
}
2012-10-01 02:44:37 +04:00
}
function __destruct()
{
$o = $this->ondestroy;
if (($o === 'commit' || $o === 'rollback') && $this->transactions)
{
$this->transactions = array(false);
$this->$o();
}
2015-05-12 18:31:49 +03:00
if ($this->queryLogFile)
{
file_put_contents($this->queryLogFile, $this->loggedQueries, FILE_APPEND);
}
}
2012-10-01 02:44:37 +04:00
function connect()
{
if ($this->socket !== NULL)
$this->link = new mysqli($this->host, $this->username, $this->password, $this->dbname, $this->port, $this->socket);
elseif ($this->port !== NULL)
$this->link = new mysqli($this->host, $this->username, $this->password, $this->dbname, $this->port);
2012-10-01 02:44:34 +04:00
else
2012-10-01 02:44:37 +04:00
$this->link = new mysqli($this->host, $this->username, $this->password, $this->dbname);
$errno = $this->link->connect_errno;
$error = $this->link->connect_error;
if ($errno)
{
$this->link = NULL;
2016-05-29 17:36:52 +03:00
throw new DatabaseMysqlException($error, $errno);
}
else
{
2013-11-16 02:23:05 +04:00
$this->transactions = array();
$this->link->set_charset('utf8');
2016-05-29 17:36:52 +03:00
foreach ($this->init as $q)
$this->link->query($q);
}
2012-10-01 02:44:34 +04:00
}
function getDBName()
{
2012-10-01 02:44:37 +04:00
return $this->dbname;
2012-10-01 02:44:34 +04:00
}
function quoteId($name)
{
return "`".str_replace("`", "``", $name)."`";
}
function quoteInto($str, $params)
{
$i = 0;
$r = '';
2013-03-30 00:57:58 +04:00
while (($p = strpos($str, '?')) !== false)
{
2013-03-30 00:57:58 +04:00
$r .= substr($str, 0, $p) . $this->quote($params[$i++]);
$str = substr($str, $p+1);
}
2013-03-30 00:57:58 +04:00
return $r.$str;
}
2012-10-01 02:44:34 +04:00
function quote($value)
{
if ($value === NULL)
return "NULL";
2012-10-10 02:20:40 +04:00
if (!$this->link)
$this->connect();
2012-10-01 02:44:37 +04:00
return "'" . $this->link->real_escape_string($value) . "'";
2012-10-01 02:44:34 +04:00
}
2018-09-10 14:26:14 +03:00
function query($sql, $streamResult = false)
2012-10-01 02:44:34 +04:00
{
2013-03-30 00:57:58 +04:00
if (!$this->link)
$this->connect();
2016-05-29 17:36:52 +03:00
if ($this->autoBegin && !$this->transactions)
$this->begin();
2012-10-01 02:44:34 +04:00
$this->queryCount++;
if ($this->queryLogFile)
{
2015-05-13 14:00:14 +03:00
$begin = explode(' ', microtime(), 2);
2012-10-01 02:44:34 +04:00
}
2018-09-10 14:26:14 +03:00
$r = $this->link->query($sql, $streamResult ? MYSQLI_USE_RESULT : MYSQLI_STORE_RESULT);
2016-05-29 17:36:52 +03:00
if (!$r)
$r = $this->check_reconnect('query', [ $sql, $fetchMode ]);
2015-05-13 14:00:14 +03:00
if ($this->queryLogFile)
{
$end = explode(' ', microtime(), 2);
$this->loggedQueries .= date("[Y-m-d H:i:s.").substr($end[0], 2, 6)."] [".
sprintf("%.05fs", $end[1]-$begin[1]+$end[0]-$begin[0])."] $sql\n";
}
2016-05-29 17:36:52 +03:00
return $r;
}
protected function check_reconnect($f, $args)
{
$r = false;
if ($this->link->errno == 2006 && $this->reconnect && (!$this->transactions || $args[0] == "BEGIN"))
2012-10-10 02:20:40 +04:00
{
2016-05-29 17:36:52 +03:00
// "MySQL server has gone away"
$this->connect();
$r = call_user_func_array([ $this->link, $f ], $args);
if (!$r && $this->link->errno == 2006)
$this->link = false;
if (!$r)
2016-05-29 17:36:52 +03:00
$st = " (reconnect failed)";
}
elseif ($this->link->errno != 2006)
$st = "";
elseif (!$this->reconnect)
$st = " (reconnect disabled";
elseif ($this->transactions)
$st = " (not reconnecting because of active transactions)";
if (!$r)
throw new DatabaseMysqlException('#'.$this->link->errno.': '.$this->link->error . $st . "\nQuery: ".$args[0], $this->link->errno);
2012-10-10 02:20:40 +04:00
return $r;
}
function multi_select(array $queries, $format = 0)
{
2013-03-30 00:57:58 +04:00
if (!$this->link)
$this->connect();
2016-05-29 17:36:52 +03:00
if ($this->autoBegin && !$this->transactions)
$this->begin();
$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);
}
$sql = implode('; ', $queries);
$r = $this->link->multi_query($sql);
if (!$r)
2016-05-29 17:36:52 +03:00
$this->check_reconnect('multi_query', [ $sql ]);
$results = array();
2013-03-30 00:57:58 +04:00
$i = 0;
foreach ($queries as $k => $q)
{
2013-03-30 00:57:58 +04:00
if ($i++)
{
$this->link->next_result();
}
$r = $this->link->store_result();
$results[$k] = $this->fetch_all($r, $format);
}
return $results;
}
2012-10-10 02:20:40 +04:00
/**
* Starts a transaction, supports nested calls and savepoints.
* @param boolean $savepoint Creates savepoints only this parameter is true.
*/
function begin($savepoint = false)
{
2016-05-29 17:36:52 +03:00
$n = count($this->transactions)+1;
$this->transactions[] = $savepoint ? "sp$n" : false;
2012-10-10 02:20:40 +04:00
if ($n == 1)
return $this->query("BEGIN");
elseif ($savepoint)
return $this->query("SAVEPOINT sp$n");
return true;
}
/**
* Commits transaction or releases last savepoint.
* If there is no last savepoint, just returns true.
*/
function commit()
{
2016-05-29 17:36:52 +03:00
$r = true;
2012-10-10 02:20:40 +04:00
$savepoint = array_pop($this->transactions);
if (!$this->transactions)
2016-05-29 17:36:52 +03:00
$r = $this->query("COMMIT");
elseif ($savepoint)
$r = $this->query("RELEASE SAVEPOINT $savepoint");
return $r;
}
/**
* Commits transaction
*/
function commitAll()
{
$r = true;
if ($this->transactions)
2012-10-10 02:20:40 +04:00
{
2016-05-29 17:36:52 +03:00
$r = $this->query("COMMIT");
$this->transactions = [];
2012-10-10 02:20:40 +04:00
}
2016-05-29 17:36:52 +03:00
return $r;
}
/**
* Rollbacks transaction
*/
function rollbackAll()
{
$r = true;
if ($this->transactions)
2012-10-10 02:20:40 +04:00
{
2016-05-29 17:36:52 +03:00
$r = $this->query("ROLLBACK");
$this->transactions = [];
2012-10-10 02:20:40 +04:00
}
2016-05-29 17:36:52 +03:00
return $r;
2012-10-10 02:20:40 +04:00
}
/**
* Rollbacks transaction or last savepoint.
* If there is no savepoint, returns false.
*/
function rollback()
{
2016-05-29 17:36:52 +03:00
$r = false;
2012-10-10 02:20:40 +04:00
$savepoint = array_pop($this->transactions);
if (!$this->transactions)
2016-05-29 17:36:52 +03:00
$r = $this->query("ROLLBACK");
2012-10-10 02:20:40 +04:00
elseif ($savepoint)
2016-05-29 17:36:52 +03:00
$r = $this->query("ROLLBACK TO SAVEPOINT $savepoint");
return $r;
2012-10-10 02:20:40 +04:00
}
function errno()
{
return $this->link->errno;
2012-10-01 02:44:34 +04:00
}
2012-10-10 02:20:40 +04:00
function error()
{
return $this->link->error;
}
/**
* Builds WHERE-part of an SQL query.
* $where can also be a string - then it's passed as-is.
*
* @param array $where Query conditions:
* array(
* 'conditional expression',
* 'field_name' => 'value',
* 'field_name' => array('one', 'of', 'values'),
* 'field_name < ?' => 'value',
* 'field_name < DATE_SUB(?, ?)' => array('arg1', 'arg2'),
2012-10-10 02:20:40 +04:00
* 'field1,field2' => array(array(1, 2), array(3, 4)),
* )
*/
2012-10-01 02:44:34 +04:00
function where_builder($where)
{
if (!is_array($where))
return $where;
$wh = array();
foreach ($where as $k => $v)
{
if (ctype_digit("$k"))
{
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);
}
2012-10-01 02:44:34 +04:00
elseif (is_array($v))
{
if (!$v)
{
2016-05-29 17:36:52 +03:00
throw new DatabaseMysqlException("Error: empty array for '$k IN (...)', don't know what to do");
2012-10-01 02:44:34 +04:00
}
else
{
2013-04-09 19:10:48 +04:00
if (is_array(reset($v)))
2015-05-12 18:31:49 +03:00
{
// (a,b) IN ((1,2), (3,4)) ...
2012-10-01 02:44:34 +04:00
foreach ($v as &$l)
2015-05-12 18:31:49 +03:00
{
2012-10-01 02:44:34 +04:00
$l = "(" . implode(",", array_map(array($this, 'quote'), $l)) . ")";
2015-05-12 18:31:49 +03:00
}
$wh[] = "$k IN (" . implode(",", $v) . ")";
}
2012-10-01 02:44:34 +04:00
else
2015-05-12 18:31:49 +03:00
{
$r = '';
$null = false;
foreach ($v as $i => $l)
{
if ($l === NULL)
{
$null = true;
}
else
{
$r .= $this->quote($l).',';
}
}
$r = $r !== '' ? "$k IN (" . substr($r, 0, -1) . ")" : '';
if ($null)
{
$r = $r !== '' ? "($r OR $k IS NULL)" : "$k IS NULL";
}
$wh[] = $r;
}
2012-10-01 02:44:34 +04:00
}
}
elseif (preg_match('/^-?\d+(\.\d+)?$/s', $v)) // int/float
$wh[] = "$k=$v";
2012-10-01 02:44:34 +04:00
elseif ($v !== NULL)
$wh[] = "$k=".$this->quote($v);
else
$wh[] = "$k IS NULL";
}
2018-09-10 14:26:14 +03:00
if ($this->isSphinx)
{
// Sphinx supports neither brackets nor OR operator as of 2.0.6-release O_o
$where = join(' AND ', $wh);
}
2013-03-30 00:57:58 +04:00
elseif ($where)
2012-10-01 02:44:34 +04:00
$where = '(' . join(') AND (', $wh) . ')';
2013-03-30 00:57:58 +04:00
else
$where = '';
2012-10-01 02:44:34 +04:00
return $where;
}
2012-10-10 02:20:40 +04:00
/**
2012-10-13 18:04:56 +04:00
* Builds SQL query text.
2012-10-10 02:20:40 +04:00
*
* @param mixed $tables see $this->tablesBuilder()
* @param mixed $fields Field definitions - either a string or an array.
* Strings are passed to resulting query text as-is.
* Arrays have the following format:
* array('field1', 'alias2' => 'expression2', ...)
* @param mixed $where see $this->whereBuilder()
* @param array $options query options - array of:
* 'CALC_FOUND_ROWS'
* 'NO_CACHE' or 'CACHE'
* 'FOR UPDATE' or 'LOCK IN SHARE MODE'
* 'GROUP BY' => array($groupby_field1 => 'ASC', $groupby_field2 => 'DESC')
* 'ORDER BY' => array($orderby_field1 => 'ASC', $orderby_field2 => 'DESC')
2013-03-30 00:57:58 +04:00
* 'LIMIT' => array($offset, $limit) or array($limit) or just $limit
2012-10-10 02:20:40 +04:00
* 'OFFSET' => $offset, for the case when 'LIMIT' is just $limit
*
* Sphinx Search extensions:
* 'WITHIN GROUP ORDER BY' => array($orderby_field => 'ASC')
2013-07-18 01:07:19 +04:00
* 'FIELD_WEIGHTS' => array('field' => <weight>, ...)
2015-05-12 18:31:49 +03:00
* 'RANKER' => bm25|sph04|...|expr('...ranker expression...')
2012-10-10 02:20:40 +04:00
*/
2012-10-01 02:44:34 +04:00
function select_builder($tables, $fields, $where, $options = NULL)
{
if (!$options)
$options = array();
else
{
foreach ($options as $k => $v)
if (ctype_digit("$k"))
2012-10-01 02:44:34 +04:00
$options[$v] = true;
}
if (is_array($fields))
2012-10-10 02:20:40 +04:00
{
foreach ($fields as $k => $v)
if (!ctype_digit("$k"))
2012-10-10 02:20:40 +04:00
$fields[$k] = "$v AS ".$this->quoteId($k);
2012-10-01 02:44:34 +04:00
$fields = join(',', $fields);
2012-10-10 02:20:40 +04:00
}
2016-05-29 17:36:52 +03:00
$more = NULL;
$tables = $this->tables_builder($tables, $more);
if ($more)
$where = array_merge($where, (array)$more);
2012-10-01 02:44:34 +04:00
$where = $this->where_builder($where);
$sql = 'SELECT ';
2012-10-10 02:20:40 +04:00
if (isset($options['CALC_FOUND_ROWS']) || isset($options['SQL_CALC_FOUND_ROWS']))
2012-10-01 02:44:34 +04:00
$sql .= 'SQL_CALC_FOUND_ROWS ';
2012-10-10 02:20:40 +04:00
if (isset($options['NO_CACHE']) || isset($options['SQL_NO_CACHE']))
2012-10-01 02:44:34 +04:00
$sql .= 'SQL_NO_CACHE ';
2012-10-10 02:20:40 +04:00
elseif (isset($options['CACHE']) || isset($options['SQL_CACHE']))
2012-10-01 02:44:34 +04:00
$sql .= 'SQL_CACHE ';
2013-03-30 00:57:58 +04:00
$sql .= "$fields FROM $tables";
if ($where)
{
$sql .= " WHERE $where";
}
2013-04-09 19:12:57 +04:00
if (!empty($options['GROUP BY']) && $options['GROUP BY'] !== '0')
2012-10-01 02:44:34 +04:00
{
$sql .= " GROUP BY ".$this->order_option($options['GROUP BY']);
2012-10-01 02:44:34 +04:00
}
2013-04-10 02:18:31 +04:00
if (!empty($options['ORDER BY']) && $options['ORDER BY'] !== '0')
2012-10-01 02:44:34 +04:00
{
$sql .= " ORDER BY ".$this->order_option($options['ORDER BY']);
}
2018-09-10 14:26:14 +03:00
if ($this->isSphinx && !empty($options['WITHIN GROUP ORDER BY']) && $options['WITHIN GROUP GROUP BY'] !== '0')
{
// Sphinx Search extension
$sql .= " WITHIN GROUP ORDER BY ".$this->order_option($options['WITHIN GROUP ORDER BY']);
2012-10-01 02:44:34 +04:00
}
2012-10-10 02:20:40 +04:00
$sql .= $this->limit_option($options);
2018-09-10 14:26:14 +03:00
if ($this->isSphinx && (!empty($options['FIELD_WEIGHTS']) || !empty($options['RANKER'])))
2013-07-18 01:07:19 +04:00
{
// Sphinx Search extension
2015-05-12 18:31:49 +03:00
$opt = array();
if (!empty($options['FIELD_WEIGHTS']))
{
$weights = array();
foreach ($options['FIELD_WEIGHTS'] as $f => $w)
{
$weights[] = "`$f`=$w";
}
$opt[] = "field_weights=(".implode(', ', $weights).")";
}
if (!empty($options['RANKER']))
2013-07-18 01:07:19 +04:00
{
2015-05-12 18:31:49 +03:00
$opt[] = "ranker=".$options['RANKER'];
2013-07-18 01:07:19 +04:00
}
2015-05-12 18:31:49 +03:00
$sql .= " OPTION ".implode(', ', $opt);
2013-07-18 01:07:19 +04:00
}
2012-10-10 02:20:40 +04:00
if (isset($options['FOR UPDATE']))
$sql .= ' FOR UPDATE';
elseif (isset($options['LOCK IN SHARE MODE']))
$sql .= ' LOCK IN SHARE MODE';
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;
}
2012-10-10 02:20:40 +04:00
/**
* Handles a single LIMIT or LIMIT and OFFSET options.
*/
protected function limit_option($options)
2012-10-10 02:20:40 +04:00
{
if (isset($options['LIMIT']))
2012-10-01 02:44:34 +04:00
{
2012-10-10 02:20:40 +04:00
$g = $options['LIMIT'];
2012-10-01 02:44:34 +04:00
if (is_array($g))
$g = join(',', $g);
2012-10-10 02:20:40 +04:00
elseif ($g && isset($options['OFFSET']))
$g = "$options[OFFSET], $g";
2012-10-01 02:44:34 +04:00
if ($g)
2012-10-10 02:20:40 +04:00
return " LIMIT $g";
2012-10-01 02:44:34 +04:00
}
2012-10-10 02:20:40 +04:00
return '';
2012-10-01 02:44:34 +04:00
}
2012-10-10 02:20:40 +04:00
/**
* Builds FROM-part of an SQL query.
*
* $tables = array(
* 'table',
* 'alias' => 'table',
* 'alias' => array('INNER', 'table_name', $where_for_on_clause),
* 'alias(ignored)' => array('INNER', $nested_tables, $on_for_join_group),
2012-10-10 02:20:40 +04:00
* )
* or just a string "`table1` INNER JOIN `table2` ON ..."
* names taken into `backticks` will be transformed using $this->tableNames
*/
2016-05-29 17:36:52 +03:00
function tables_builder($tables, &$where = NULL)
2012-10-01 02:44:34 +04:00
{
if (is_array($tables))
{
$t = '';
foreach ($tables as $k => $v)
{
2016-05-29 17:36:52 +03:00
if (!is_array($v))
$v = [ 'INNER', $v, NULL ];
$join = strtolower(substr($v[0], 0, 4));
if ($join == 'righ')
$join = 'RIGHT';
elseif ($join == 'left')
$join = 'LEFT';
else /* if (!$join || $join == 'inne' || $join == 'join') */
$join = 'INNER';
if (is_array($v[1])) // nested join (left join (A join B on ...) on ...)
{
$more = NULL;
$table = $this->tables_builder($v[1], $more);
if ($more)
$v[2] = array_merge((array)$v[2], (array)$more);
if (count($v[1]) > 1)
$table = "($table)";
}
2016-05-29 17:36:52 +03:00
else
2012-10-01 02:44:34 +04:00
{
2016-05-29 17:36:52 +03:00
$table = (isset($this->tableNames[$v[1]]) ? $this->quoteId($this->tableNames[$v[1]]) : $v[1]);
if (!ctype_digit("$k"))
$table .= ' ' . $k;
2012-10-01 02:44:34 +04:00
}
if ($t)
2016-05-29 17:36:52 +03:00
$t .= " $join JOIN $table ON ".($this->where_builder($v[2]) ?: '1=1');
2012-10-01 02:44:34 +04:00
else
2016-05-29 17:36:52 +03:00
{
$t = $table;
$where = $v[2]; // extract ON to WHERE if only a single join is specified
}
2012-10-01 02:44:34 +04:00
}
$tables = $t;
}
else
$tables = preg_replace_callback('/((?:,|JOIN)\s*`)([^`]+)/s', array($this, 'tables_builder_pregcb1'), $tables);
return $tables;
}
function tables_builder_pregcb1($m)
{
2012-10-10 02:20:40 +04:00
if (isset($this->tableNames[$m[2]]))
return $m[1].$this->tableNames[$m[2]];
return $m[1].$m[2];
2012-10-01 02:44:34 +04:00
}
2012-10-10 02:20:40 +04:00
/**
* Run a SELECT query and return results.
*
* Usage: either
* $this->select($tables, $fields, $where, $options, $format)
* using $this->select_builder() or
* $this->select($sql_text, $format)
* using query text.
*
* @param int $format Return format, bitmask of MS_XX constants:
* MS_RESULT = return mysqli result object to manually fetch from
* MS_LIST = return rows as indexed arrays
* MS_HASH = return rows as associative arrays (default)
* MS_ROW = only return the first row
* MS_COL = only return the first column
* MS_VALUE = only return the first cell (just 1 value)
*/
2012-10-01 02:44:34 +04:00
function select($tables, $fields = '*', $where = 1, $options = NULL, $format = 0)
{
2013-03-30 00:57:58 +04:00
if (is_int($fields))
2012-10-01 02:44:34 +04:00
{
$sql = $tables;
$format = $fields;
}
else
$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)
{
2012-10-01 02:44:34 +04:00
if ($format & MS_LIST)
$r = $this->get_rows($res);
2012-10-01 02:44:34 +04:00
else
$r = $this->get_assocs($res);
2012-10-01 02:44:34 +04:00
if (!$r)
$r = array();
if ($format & MS_ROW)
{
2012-10-10 02:20:40 +04:00
if (!count($r))
return NULL;
2012-10-01 02:44:34 +04:00
if ($format & MS_COL)
{
$r = $r[0];
$k = array_keys($r);
$r = $r[$k[0]];
}
else
$r = $r[0];
}
elseif ($format & MS_COL)
{
$k = false;
2012-10-01 02:44:34 +04:00
foreach ($r as $i => $v)
{
if (!$k)
{
$k = array_keys($v);
$k = $k[0];
}
$r[$i] = $v[$k];
}
}
return $r;
}
function found_rows()
{
return $this->select('SELECT FOUND_ROWS()', MS_VALUE);
}
2012-10-10 02:20:40 +04:00
/**
* Delete a set of rows.
* @param mixed $tables see $this->tables_builder()
* @param mixed $where see $this->where_builder()
* @param array $options Options for query:
* 'LIMIT' => array($limit, $offset) or array($limit) or just $limit
* 'OFFSET' => $offset, for the case when 'LIMIT' is just $limit
*/
2012-10-01 02:44:34 +04:00
function delete($tables, $where, $options = NULL)
{
$tables = $this->tables_builder($tables);
2013-03-30 00:57:58 +04:00
$where = $this->where_builder($where) ?: '1=1';
2012-10-01 02:44:34 +04:00
$sql = "DELETE FROM $tables WHERE $where";
2012-10-10 02:20:40 +04:00
$sql .= $this->limit_option($options);
2012-10-01 02:44:34 +04:00
$this->query($sql);
return $this->link->affected_rows;
}
2012-10-10 02:20:40 +04:00
/**
* Builds an INSERT query.
* @param string $table Table name to insert rows to.
* @param array $rows Array of table rows to be inserted.
2016-05-29 17:36:52 +03:00
* @param string $action Conflict action: NULL, 'REPLACE', 'IGNORE' or 'UPDATE'
* REPLACE: delete matching rows, then insert all rows (MySQL REPLACE)
* IGNORE: ignore matching rows, insert missing rows (MySQL INSERT IGNORE)
* UPDATE: update matching rows, insert missing rows (MySQL INSERT ... ON DUPLICATE KEY UPDATE)
* @param array|string $uniqueKey Single unique key for conflict checking
* @param array|NULL $updateCols Columns to update in case of a conflict
2012-10-10 02:20:40 +04:00
*/
2016-05-29 17:36:52 +03:00
function insert_builder($table, $rows, $action = NULL, $uniqueKey = NULL, $updateCols = NULL)
2012-10-01 02:44:34 +04:00
{
2012-10-10 02:20:40 +04:00
if (isset($this->tableNames[$table]))
{
2012-10-01 02:44:34 +04:00
$table = $this->tableNames[$table];
2012-10-10 02:20:40 +04:00
}
2013-05-08 13:21:50 +04:00
$key = array_keys($rows[0]);
2012-10-01 02:44:34 +04:00
foreach ($rows as &$r)
{
$rs = array();
foreach ($key as &$k)
$rs[] = $this->quote($r[$k]);
$r = "(".implode(",", $rs).")";
}
foreach ($key as &$k)
2018-09-10 14:26:14 +03:00
if (strpos($k, '`') === false && (!$this->isSphinx || $k !== 'id'))
2012-10-01 02:44:34 +04:00
$k = $this->quoteId($k);
2016-05-29 17:36:52 +03:00
$sql = ($action == "REPLACE" ? "REPLACE" : "INSERT" . ($action == "IGNORE" ? " IGNORE" : "")).
" INTO $table (".implode(",",$key).") VALUES ".implode(",",$rows);
2016-05-29 17:36:52 +03:00
if ($action == "UPDATE")
2012-10-01 02:44:34 +04:00
{
2016-05-29 17:36:52 +03:00
if ($uniqueKey)
{
$uniqueKey = array_flip(is_array($uniqueKey) ? $uniqueKey : array_map('trim', explode(",", $uniqueKey)));
$cond = $uniqueKey;
foreach ($cond as $k => $v)
$v = "$k!=VALUES($k)";
// Trigger ERROR 1242 (21000): Subquery returns more than 1 row if trying to update based on different key conflict
$cond = "CASE WHEN ".implode(" OR ", $cond)." THEN (SELECT 1 UNION SELECT 2) ELSE ";
}
if ($updateCols)
$key = (array)$updateCols;
2012-10-01 02:44:34 +04:00
foreach ($key as &$k)
2016-05-29 17:36:52 +03:00
{
if ($uniqueKey && isset($uniqueKey[$k]))
$k = "$k=($cond $k END)";
else
$k = "$k=VALUES($k)";
}
2012-10-01 02:44:34 +04:00
$sql .= " ON DUPLICATE KEY UPDATE ".implode(",",$key);
}
return $sql;
}
2012-10-10 02:20:40 +04:00
/**
* Insert a single row into $table and return inserted ID.
* @param string $table Table name to insert row to.
* @param array $rows Row to be inserted.
* @return int $insert_id Autoincrement ID of inserted row (if appropriate).
*/
2012-10-01 02:44:34 +04:00
function insert_row($table, $row)
{
$sql = $this->insert_builder($table, array($row));
if ($this->query($sql))
return $this->link->insert_id;
return NULL;
}
2012-10-01 02:44:37 +04:00
function insert_id()
{
return $this->link->insert_id;
}
2012-10-10 02:20:40 +04:00
/**
2016-05-29 17:36:52 +03:00
* Update row(s) in $table.
* $this->update($table, $set, $where, $options);
2012-10-10 02:20:40 +04:00
*
* @param string $table Table name to update.
* @param array $set Assoc array with values for update query.
* @param array $where Conditions for update query, see $this->where_builder().
* @param array $options Options for update query:
* 'LIMIT' => array($limit, $offset) or array($limit) or just $limit
* 'OFFSET' => $offset, for the case when 'LIMIT' is just $limit
*/
2012-10-01 02:44:34 +04:00
function update($table, $rows, $where = NULL, $options = NULL)
{
if (!$rows)
return false;
2016-05-29 17:36:52 +03:00
if (count(func_get_args()) == 2)
throw new Exception(__CLASS__."::update(table, rows) is the old syntax, use upsert()");
$sql = array();
foreach ((array)$rows as $k => $v)
2012-10-01 02:44:34 +04:00
{
2016-05-29 17:36:52 +03:00
if (!ctype_digit("$k"))
$sql[] = $k.'='.$this->quote($v);
else
$sql[] = $v;
2012-10-01 02:44:34 +04:00
}
2016-05-29 17:36:52 +03:00
$where = $this->where_builder($where) ?: '1=1';
$sql = 'UPDATE ' . $this->tables_builder($table) . ' SET ' . implode(', ', $sql) . ' WHERE ' . $where;
$sql .= $this->limit_option($options);
if ($this->query($sql))
return $this->link->affected_rows;
return false;
}
/**
* INSERT / REPLACE / INSERT IGNORE / INSERT OR UPDATE
*/
function insert($table, $rows, $onConflict = NULL, $uniqueKey = NULL)
{
if (!$rows || !is_array($rows))
return false;
$first = reset($rows);
if (!is_array($first))
$rows = array($rows);
$sql = $this->insert_builder($table, $rows, $onConflict, $uniqueKey);
2012-10-01 02:44:34 +04:00
if ($this->query($sql))
return $this->link->affected_rows;
return false;
}
2016-05-29 17:36:52 +03:00
function insert_ignore($table, $rows, $uniqueKey = NULL)
{
return $this->insert($table, $rows, 'IGNORE', $uniqueKey);
}
function upsert($table, $rows, $uniqueKey = NULL, $updateCols = NULL)
{
return $this->insert($table, $rows, 'UPDATE', $uniqueKey, $updateCols);
}
function replace($table, $rows, $uniqueKey = NULL)
{
2016-05-29 17:36:52 +03:00
return $this->insert($table, $rows, 'REPLACE', $uniqueKey);
}
protected function get_rows($res)
2012-10-01 02:44:34 +04:00
{
$r = array();
if ($res)
2012-10-01 02:44:34 +04:00
{
while ($row = $res->fetch_row())
$r[] = $row;
$res->free();
}
return $r;
}
protected function get_assocs($res)
2012-10-01 02:44:34 +04:00
{
$r = array();
if ($res)
2012-10-01 02:44:34 +04:00
{
while ($row = $res->fetch_assoc())
$r[] = $row;
$res->free();
}
return $r;
}
}