2016-09-16 03:14:58 +00:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* @defgroup Database Database
|
|
|
|
|
*
|
|
|
|
|
* This file deals with database interface functions
|
|
|
|
|
* and query specifics/optimisations.
|
|
|
|
|
*
|
|
|
|
|
* This program is free software; you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License along
|
|
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
|
|
|
*
|
|
|
|
|
* @file
|
|
|
|
|
* @ingroup Database
|
|
|
|
|
*/
|
2017-02-07 04:49:57 +00:00
|
|
|
namespace Wikimedia\Rdbms;
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
use Psr\Log\LoggerAwareInterface;
|
|
|
|
|
use Psr\Log\LoggerInterface;
|
2018-02-27 21:44:14 +00:00
|
|
|
use Psr\Log\NullLogger;
|
2016-10-12 05:36:03 +00:00
|
|
|
use Wikimedia\ScopedCallback;
|
2016-10-02 04:51:51 +00:00
|
|
|
use Wikimedia\Timestamp\ConvertibleTimestamp;
|
2018-02-10 07:52:26 +00:00
|
|
|
use Wikimedia;
|
2017-02-07 04:49:57 +00:00
|
|
|
use BagOStuff;
|
|
|
|
|
use HashBagOStuff;
|
2018-02-28 20:56:34 +00:00
|
|
|
use LogicException;
|
2017-02-07 04:49:57 +00:00
|
|
|
use InvalidArgumentException;
|
2018-03-24 12:16:29 +00:00
|
|
|
use UnexpectedValueException;
|
2017-02-07 04:49:57 +00:00
|
|
|
use Exception;
|
|
|
|
|
use RuntimeException;
|
2016-09-16 03:14:58 +00:00
|
|
|
|
|
|
|
|
/**
|
2016-09-23 03:46:16 +00:00
|
|
|
* Relational database abstraction object
|
|
|
|
|
*
|
2016-09-16 03:14:58 +00:00
|
|
|
* @ingroup Database
|
2016-09-23 03:46:16 +00:00
|
|
|
* @since 1.28
|
2016-09-16 03:14:58 +00:00
|
|
|
*/
|
2016-09-23 03:46:16 +00:00
|
|
|
abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAwareInterface {
|
2016-09-16 03:14:58 +00:00
|
|
|
/** Number of times to re-try an operation in case of deadlock */
|
|
|
|
|
const DEADLOCK_TRIES = 4;
|
|
|
|
|
/** Minimum time to wait before retry, in microseconds */
|
|
|
|
|
const DEADLOCK_DELAY_MIN = 500000;
|
|
|
|
|
/** Maximum time to wait before retry */
|
|
|
|
|
const DEADLOCK_DELAY_MAX = 1500000;
|
|
|
|
|
|
|
|
|
|
/** How long before it is worth doing a dummy query to test the connection */
|
|
|
|
|
const PING_TTL = 1.0;
|
|
|
|
|
const PING_QUERY = 'SELECT 1 AS ping';
|
|
|
|
|
|
2017-09-10 19:11:37 +00:00
|
|
|
const TINY_WRITE_SEC = 0.010;
|
|
|
|
|
const SLOW_WRITE_SEC = 0.500;
|
2016-09-16 03:14:58 +00:00
|
|
|
const SMALL_WRITE_ROWS = 100;
|
|
|
|
|
|
2018-02-28 00:00:05 +00:00
|
|
|
/** @var string Whether lock granularity is on the level of the entire database */
|
|
|
|
|
const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
|
|
|
|
|
|
2018-02-28 20:56:34 +00:00
|
|
|
/** @var int New Database instance will not be connected yet when returned */
|
|
|
|
|
const NEW_UNCONNECTED = 0;
|
|
|
|
|
/** @var int New Database instance will already be connected when returned */
|
|
|
|
|
const NEW_CONNECTED = 1;
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
/** @var string SQL query */
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $lastQuery = '';
|
2016-10-14 09:17:25 +00:00
|
|
|
/** @var float|bool UNIX timestamp of last write query */
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $lastWriteTime = false;
|
2016-09-16 03:14:58 +00:00
|
|
|
/** @var string|bool */
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $phpError = false;
|
2018-02-28 20:56:34 +00:00
|
|
|
/** @var string Server that this instance is currently connected to */
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $server;
|
2018-02-28 20:56:34 +00:00
|
|
|
/** @var string User that this instance is currently connected under the name of */
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $user;
|
2018-02-28 20:56:34 +00:00
|
|
|
/** @var string Password used to establish the current connection */
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $password;
|
2018-02-17 22:09:02 +00:00
|
|
|
/** @var array[] Map of (table => (dbname, schema, prefix) map) */
|
2016-09-16 03:14:58 +00:00
|
|
|
protected $tableAliases = [];
|
2018-02-17 22:09:02 +00:00
|
|
|
/** @var string[] Map of (index alias => index) */
|
|
|
|
|
protected $indexAliases = [];
|
2016-09-16 03:14:58 +00:00
|
|
|
/** @var bool Whether this PHP instance is for a CLI script */
|
|
|
|
|
protected $cliMode;
|
|
|
|
|
/** @var string Agent name for query profiling */
|
|
|
|
|
protected $agent;
|
2018-02-28 20:56:34 +00:00
|
|
|
/** @var array Parameters used by initConnection() to establish a connection */
|
|
|
|
|
protected $connectionParams = [];
|
2016-09-16 03:14:58 +00:00
|
|
|
/** @var BagOStuff APC cache */
|
|
|
|
|
protected $srvCache;
|
|
|
|
|
/** @var LoggerInterface */
|
|
|
|
|
protected $connLogger;
|
|
|
|
|
/** @var LoggerInterface */
|
|
|
|
|
protected $queryLogger;
|
2018-12-01 09:02:48 +00:00
|
|
|
/** @var callable Error logging callback */
|
2016-09-16 03:14:58 +00:00
|
|
|
protected $errorLogger;
|
2018-12-01 09:02:48 +00:00
|
|
|
/** @var callable Deprecation logging callback */
|
2018-04-05 16:13:08 +00:00
|
|
|
protected $deprecationLogger;
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2016-10-17 18:21:40 +00:00
|
|
|
/** @var resource|null Database connection */
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $conn = null;
|
2016-09-16 03:14:58 +00:00
|
|
|
/** @var bool */
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $opened = false;
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2018-03-24 07:02:24 +00:00
|
|
|
/** @var array[] List of (callable, method name, atomic section id) */
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $trxIdleCallbacks = [];
|
2018-03-24 07:02:24 +00:00
|
|
|
/** @var array[] List of (callable, method name, atomic section id) */
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $trxPreCommitCallbacks = [];
|
2018-03-24 07:02:24 +00:00
|
|
|
/** @var array[] List of (callable, method name, atomic section id) */
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $trxEndCallbacks = [];
|
2016-09-16 03:14:58 +00:00
|
|
|
/** @var callable[] Map of (name => callable) */
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $trxRecurringCallbacks = [];
|
2016-09-16 03:14:58 +00:00
|
|
|
/** @var bool Whether to suppress triggering of transaction end callbacks */
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $trxEndCallbacksSuppressed = false;
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2017-08-20 11:20:59 +00:00
|
|
|
/** @var int */
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $flags;
|
2016-09-16 03:14:58 +00:00
|
|
|
/** @var array */
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $lbInfo = [];
|
2016-09-16 03:14:58 +00:00
|
|
|
/** @var array|bool */
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $schemaVars = false;
|
2016-09-16 03:14:58 +00:00
|
|
|
/** @var array */
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $sessionVars = [];
|
2016-09-16 03:14:58 +00:00
|
|
|
/** @var array|null */
|
|
|
|
|
protected $preparedArgs;
|
|
|
|
|
/** @var string|bool|null Stashed value of html_errors INI setting */
|
|
|
|
|
protected $htmlErrors;
|
|
|
|
|
/** @var string */
|
|
|
|
|
protected $delimiter = ';';
|
2016-09-17 04:39:57 +00:00
|
|
|
/** @var DatabaseDomain */
|
|
|
|
|
protected $currentDomain;
|
2018-01-28 14:10:39 +00:00
|
|
|
/** @var integer|null Rows affected by the last query to query() or its CRUD wrappers */
|
|
|
|
|
protected $affectedRowCount;
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2018-03-23 09:57:21 +00:00
|
|
|
/**
|
|
|
|
|
* @var int Transaction status
|
|
|
|
|
*/
|
|
|
|
|
protected $trxStatus = self::STATUS_TRX_NONE;
|
|
|
|
|
/**
|
|
|
|
|
* @var Exception|null The last error that caused the status to become STATUS_TRX_ERROR
|
|
|
|
|
*/
|
|
|
|
|
protected $trxStatusCause;
|
2018-04-05 18:17:09 +00:00
|
|
|
/**
|
|
|
|
|
* @var array|null If wasKnownStatementRollbackError() prevented trxStatus from being set,
|
|
|
|
|
* the relevant details are stored here.
|
|
|
|
|
*/
|
|
|
|
|
protected $trxStatusIgnoredCause;
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
|
|
|
|
* Either 1 if a transaction is active or 0 otherwise.
|
|
|
|
|
* The other Trx fields may not be meaningfull if this is 0.
|
|
|
|
|
*
|
|
|
|
|
* @var int
|
|
|
|
|
*/
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $trxLevel = 0;
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
|
|
|
|
* Either a short hexidecimal string if a transaction is active or ""
|
|
|
|
|
*
|
|
|
|
|
* @var string
|
2018-02-13 06:58:57 +00:00
|
|
|
* @see Database::trxLevel
|
2016-09-16 03:14:58 +00:00
|
|
|
*/
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $trxShortId = '';
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
|
|
|
|
* The UNIX time that the transaction started. Callers can assume that if
|
|
|
|
|
* snapshot isolation is used, then the data is *at least* up to date to that
|
|
|
|
|
* point (possibly more up-to-date since the first SELECT defines the snapshot).
|
|
|
|
|
*
|
|
|
|
|
* @var float|null
|
2018-02-13 06:58:57 +00:00
|
|
|
* @see Database::trxLevel
|
2016-09-16 03:14:58 +00:00
|
|
|
*/
|
2018-02-13 06:58:57 +00:00
|
|
|
private $trxTimestamp = null;
|
2016-09-16 03:14:58 +00:00
|
|
|
/** @var float Lag estimate at the time of BEGIN */
|
2018-02-13 06:58:57 +00:00
|
|
|
private $trxReplicaLag = null;
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
|
|
|
|
* Remembers the function name given for starting the most recent transaction via begin().
|
|
|
|
|
* Used to provide additional context for error reporting.
|
|
|
|
|
*
|
|
|
|
|
* @var string
|
2018-02-13 06:58:57 +00:00
|
|
|
* @see Database::trxLevel
|
2016-09-16 03:14:58 +00:00
|
|
|
*/
|
2018-02-13 06:58:57 +00:00
|
|
|
private $trxFname = null;
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
|
|
|
|
* Record if possible write queries were done in the last transaction started
|
|
|
|
|
*
|
|
|
|
|
* @var bool
|
2018-02-13 06:58:57 +00:00
|
|
|
* @see Database::trxLevel
|
2016-09-16 03:14:58 +00:00
|
|
|
*/
|
2018-02-13 06:58:57 +00:00
|
|
|
private $trxDoneWrites = false;
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
|
|
|
|
* Record if the current transaction was started implicitly due to DBO_TRX being set.
|
|
|
|
|
*
|
|
|
|
|
* @var bool
|
2018-02-13 06:58:57 +00:00
|
|
|
* @see Database::trxLevel
|
2016-09-16 03:14:58 +00:00
|
|
|
*/
|
2018-02-13 06:58:57 +00:00
|
|
|
private $trxAutomatic = false;
|
2018-03-17 21:59:56 +00:00
|
|
|
/**
|
|
|
|
|
* Counter for atomic savepoint identifiers. Reset when a new transaction begins.
|
|
|
|
|
*
|
|
|
|
|
* @var int
|
|
|
|
|
*/
|
|
|
|
|
private $trxAtomicCounter = 0;
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
|
|
|
|
* Array of levels of atomicity within transactions
|
|
|
|
|
*
|
2018-03-29 07:23:10 +00:00
|
|
|
* @var array List of (name, unique ID, savepoint ID)
|
2016-09-16 03:14:58 +00:00
|
|
|
*/
|
2018-02-13 06:58:57 +00:00
|
|
|
private $trxAtomicLevels = [];
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
2016-09-26 22:40:07 +00:00
|
|
|
* Record if the current transaction was started implicitly by Database::startAtomic
|
2016-09-16 03:14:58 +00:00
|
|
|
*
|
|
|
|
|
* @var bool
|
|
|
|
|
*/
|
2018-02-13 06:58:57 +00:00
|
|
|
private $trxAutomaticAtomic = false;
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
|
|
|
|
* Track the write query callers of the current transaction
|
|
|
|
|
*
|
|
|
|
|
* @var string[]
|
|
|
|
|
*/
|
2018-02-13 06:58:57 +00:00
|
|
|
private $trxWriteCallers = [];
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
|
|
|
|
* @var float Seconds spent in write queries for the current transaction
|
|
|
|
|
*/
|
2018-02-13 06:58:57 +00:00
|
|
|
private $trxWriteDuration = 0.0;
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
2017-08-20 11:20:59 +00:00
|
|
|
* @var int Number of write queries for the current transaction
|
2016-09-16 03:14:58 +00:00
|
|
|
*/
|
2018-02-13 06:58:57 +00:00
|
|
|
private $trxWriteQueryCount = 0;
|
2017-05-26 18:42:05 +00:00
|
|
|
/**
|
2017-08-20 11:20:59 +00:00
|
|
|
* @var int Number of rows affected by write queries for the current transaction
|
2017-05-26 18:42:05 +00:00
|
|
|
*/
|
2018-02-13 06:58:57 +00:00
|
|
|
private $trxWriteAffectedRows = 0;
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
2018-02-13 06:58:57 +00:00
|
|
|
* @var float Like trxWriteQueryCount but excludes lock-bound, easy to replicate, queries
|
2016-09-16 03:14:58 +00:00
|
|
|
*/
|
2018-02-13 06:58:57 +00:00
|
|
|
private $trxWriteAdjDuration = 0.0;
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
2018-02-13 06:58:57 +00:00
|
|
|
* @var int Number of write queries counted in trxWriteAdjDuration
|
2016-09-16 03:14:58 +00:00
|
|
|
*/
|
2018-02-13 06:58:57 +00:00
|
|
|
private $trxWriteAdjQueryCount = 0;
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
|
|
|
|
* @var float RTT time estimate
|
|
|
|
|
*/
|
2018-02-13 06:58:57 +00:00
|
|
|
private $rttEstimate = 0.0;
|
2016-09-16 03:14:58 +00:00
|
|
|
|
|
|
|
|
/** @var array Map of (name => 1) for locks obtained via lock() */
|
2018-02-13 06:58:57 +00:00
|
|
|
private $namedLocksHeld = [];
|
2016-09-19 21:15:05 +00:00
|
|
|
/** @var array Map of (table name => 1) for TEMPORARY tables */
|
2018-02-13 06:58:57 +00:00
|
|
|
protected $sessionTempTables = [];
|
2016-09-16 03:14:58 +00:00
|
|
|
|
|
|
|
|
/** @var IDatabase|null Lazy handle to the master DB this server replicates from */
|
|
|
|
|
private $lazyMasterHandle;
|
|
|
|
|
|
|
|
|
|
/** @var float UNIX timestamp */
|
|
|
|
|
protected $lastPing = 0.0;
|
|
|
|
|
|
2018-02-13 06:58:57 +00:00
|
|
|
/** @var int[] Prior flags member variable values */
|
2016-09-16 03:14:58 +00:00
|
|
|
private $priorFlags = [];
|
|
|
|
|
|
2018-08-22 04:25:48 +00:00
|
|
|
/** @var mixed Class name or object With profileIn/profileOut methods */
|
2016-09-16 03:14:58 +00:00
|
|
|
protected $profiler;
|
|
|
|
|
/** @var TransactionProfiler */
|
|
|
|
|
protected $trxProfiler;
|
|
|
|
|
|
2018-02-08 19:16:29 +00:00
|
|
|
/** @var int */
|
|
|
|
|
protected $nonNativeInsertSelectBatchSize = 10000;
|
|
|
|
|
|
2018-03-24 07:02:24 +00:00
|
|
|
/** @var string Idiom used when a cancelable atomic section started the transaction */
|
|
|
|
|
private static $NOT_APPLICABLE = 'n/a';
|
|
|
|
|
/** @var string Prefix to the atomic section counter used to make savepoint IDs */
|
|
|
|
|
private static $SAVEPOINT_PREFIX = 'wikimedia_rdbms_atomic';
|
|
|
|
|
|
2018-03-23 09:57:21 +00:00
|
|
|
/** @var int Transaction is in a error state requiring a full or savepoint rollback */
|
|
|
|
|
const STATUS_TRX_ERROR = 1;
|
|
|
|
|
/** @var int Transaction is active and in a normal state */
|
|
|
|
|
const STATUS_TRX_OK = 2;
|
|
|
|
|
/** @var int No transaction is active */
|
|
|
|
|
const STATUS_TRX_NONE = 3;
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
2018-07-26 16:31:49 +00:00
|
|
|
* @note exceptions for missing libraries/drivers should be thrown in initConnection()
|
2016-09-19 23:34:32 +00:00
|
|
|
* @param array $params Parameters passed from Database::factory()
|
2016-09-16 03:14:58 +00:00
|
|
|
*/
|
2018-02-28 20:56:34 +00:00
|
|
|
protected function __construct( array $params ) {
|
2018-08-14 23:44:41 +00:00
|
|
|
foreach ( [ 'host', 'user', 'password', 'dbname', 'schema', 'tablePrefix' ] as $name ) {
|
2018-02-28 20:56:34 +00:00
|
|
|
$this->connectionParams[$name] = $params[$name];
|
|
|
|
|
}
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2016-09-20 18:28:51 +00:00
|
|
|
$this->cliMode = $params['cliMode'];
|
|
|
|
|
// Agent name is added to SQL queries in a comment, so make sure it can't break out
|
|
|
|
|
$this->agent = str_replace( '/', '-', $params['agent'] );
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->flags = $params['flags'];
|
|
|
|
|
if ( $this->flags & self::DBO_DEFAULT ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( $this->cliMode ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->flags &= ~self::DBO_TRX;
|
2016-09-16 03:14:58 +00:00
|
|
|
} else {
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->flags |= self::DBO_TRX;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
2018-03-24 12:17:12 +00:00
|
|
|
// Disregard deprecated DBO_IGNORE flag (T189999)
|
|
|
|
|
$this->flags &= ~self::DBO_IGNORE;
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->sessionVars = $params['variables'];
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2017-10-06 22:17:58 +00:00
|
|
|
$this->srvCache = $params['srvCache'] ?? new HashBagOStuff();
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2016-09-20 18:28:51 +00:00
|
|
|
$this->profiler = $params['profiler'];
|
|
|
|
|
$this->trxProfiler = $params['trxProfiler'];
|
|
|
|
|
$this->connLogger = $params['connLogger'];
|
|
|
|
|
$this->queryLogger = $params['queryLogger'];
|
2016-09-22 05:22:04 +00:00
|
|
|
$this->errorLogger = $params['errorLogger'];
|
2018-04-05 16:13:08 +00:00
|
|
|
$this->deprecationLogger = $params['deprecationLogger'];
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2018-02-08 19:16:29 +00:00
|
|
|
if ( isset( $params['nonNativeInsertSelectBatchSize'] ) ) {
|
|
|
|
|
$this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'];
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-20 15:15:39 +00:00
|
|
|
// Set initial dummy domain until open() sets the final DB/prefix
|
2018-08-14 23:44:41 +00:00
|
|
|
$this->currentDomain = new DatabaseDomain(
|
|
|
|
|
$params['dbname'] != '' ? $params['dbname'] : null,
|
|
|
|
|
$params['schema'] != '' ? $params['schema'] : null,
|
|
|
|
|
$params['tablePrefix']
|
|
|
|
|
);
|
2018-02-28 20:56:34 +00:00
|
|
|
}
|
2016-09-20 15:15:39 +00:00
|
|
|
|
2018-02-28 20:56:34 +00:00
|
|
|
/**
|
|
|
|
|
* Initialize the connection to the database over the wire (or to local files)
|
|
|
|
|
*
|
|
|
|
|
* @throws LogicException
|
|
|
|
|
* @throws InvalidArgumentException
|
|
|
|
|
* @throws DBConnectionError
|
|
|
|
|
* @since 1.31
|
|
|
|
|
*/
|
|
|
|
|
final public function initConnection() {
|
|
|
|
|
if ( $this->isOpen() ) {
|
|
|
|
|
throw new LogicException( __METHOD__ . ': already connected.' );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
2018-02-28 20:56:34 +00:00
|
|
|
// Establish the connection
|
|
|
|
|
$this->doInitConnection();
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-02-28 20:56:34 +00:00
|
|
|
/**
|
|
|
|
|
* Actually connect to the database over the wire (or to local files)
|
|
|
|
|
*
|
|
|
|
|
* @throws InvalidArgumentException
|
|
|
|
|
* @throws DBConnectionError
|
|
|
|
|
* @since 1.31
|
|
|
|
|
*/
|
|
|
|
|
protected function doInitConnection() {
|
|
|
|
|
if ( strlen( $this->connectionParams['user'] ) ) {
|
|
|
|
|
$this->open(
|
|
|
|
|
$this->connectionParams['host'],
|
|
|
|
|
$this->connectionParams['user'],
|
|
|
|
|
$this->connectionParams['password'],
|
2018-08-14 23:44:41 +00:00
|
|
|
$this->connectionParams['dbname'],
|
|
|
|
|
$this->connectionParams['schema'],
|
|
|
|
|
$this->connectionParams['tablePrefix']
|
2018-02-28 20:56:34 +00:00
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
throw new InvalidArgumentException( "No database user provided." );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 01:34:51 +00:00
|
|
|
/**
|
|
|
|
|
* Open a new connection to the database (closing any existing one)
|
|
|
|
|
*
|
|
|
|
|
* @param string $server Database server host
|
|
|
|
|
* @param string $user Database user name
|
|
|
|
|
* @param string $password Database user password
|
|
|
|
|
* @param string $dbName Database name
|
2018-08-14 23:44:41 +00:00
|
|
|
* @param string|null $schema Database schema name
|
|
|
|
|
* @param string $tablePrefix Table prefix
|
2018-08-22 01:34:51 +00:00
|
|
|
* @return bool
|
|
|
|
|
* @throws DBConnectionError
|
|
|
|
|
*/
|
2018-08-14 23:44:41 +00:00
|
|
|
abstract protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix );
|
2018-08-22 01:34:51 +00:00
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
2016-09-19 23:34:32 +00:00
|
|
|
* Construct a Database subclass instance given a database type and parameters
|
|
|
|
|
*
|
|
|
|
|
* This also connects to the database immediately upon object construction
|
|
|
|
|
*
|
2018-02-27 21:44:14 +00:00
|
|
|
* @param string $dbType A possible DB type (sqlite, mysql, postgres,...)
|
2016-09-19 23:34:32 +00:00
|
|
|
* @param array $p Parameter map with keys:
|
|
|
|
|
* - host : The hostname of the DB server
|
|
|
|
|
* - user : The name of the database user the client operates under
|
|
|
|
|
* - password : The password for the database user
|
|
|
|
|
* - dbname : The name of the database to use where queries do not specify one.
|
|
|
|
|
* The database must exist or an error might be thrown. Setting this to the empty string
|
|
|
|
|
* will avoid any such errors and make the handle have no implicit database scope. This is
|
|
|
|
|
* useful for queries like SHOW STATUS, CREATE DATABASE, or DROP DATABASE. Note that a
|
|
|
|
|
* "database" in Postgres is rougly equivalent to an entire MySQL server. This the domain
|
|
|
|
|
* in which user names and such are defined, e.g. users are database-specific in Postgres.
|
|
|
|
|
* - schema : The database schema to use (if supported). A "schema" in Postgres is roughly
|
|
|
|
|
* equivalent to a "database" in MySQL. Note that MySQL and SQLite do not use schemas.
|
|
|
|
|
* - tablePrefix : Optional table prefix that is implicitly added on to all table names
|
|
|
|
|
* recognized in queries. This can be used in place of schemas for handle site farms.
|
|
|
|
|
* - flags : Optional bitfield of DBO_* constants that define connection, protocol,
|
|
|
|
|
* buffering, and transaction behavior. It is STRONGLY adviced to leave the DBO_DEFAULT
|
|
|
|
|
* flag in place UNLESS this this database simply acts as a key/value store.
|
2018-01-16 19:02:11 +00:00
|
|
|
* - driver: Optional name of a specific DB client driver. For MySQL, there is only the
|
|
|
|
|
* 'mysqli' driver; the old one 'mysql' has been removed.
|
2016-09-19 23:34:32 +00:00
|
|
|
* - variables: Optional map of session variables to set after connecting. This can be
|
|
|
|
|
* used to adjust lock timeouts or encoding modes and the like.
|
|
|
|
|
* - connLogger: Optional PSR-3 logger interface instance.
|
|
|
|
|
* - queryLogger: Optional PSR-3 logger interface instance.
|
|
|
|
|
* - profiler: Optional class name or object with profileIn()/profileOut() methods.
|
|
|
|
|
* These will be called in query(), using a simplified version of the SQL that also
|
|
|
|
|
* includes the agent as a SQL comment.
|
|
|
|
|
* - trxProfiler: Optional TransactionProfiler instance.
|
|
|
|
|
* - errorLogger: Optional callback that takes an Exception and logs it.
|
2018-04-05 16:13:08 +00:00
|
|
|
* - deprecationLogger: Optional callback that takes a string and logs it.
|
2016-09-19 23:34:32 +00:00
|
|
|
* - cliMode: Whether to consider the execution context that of a CLI script.
|
|
|
|
|
* - agent: Optional name used to identify the end-user in query profiling/logging.
|
|
|
|
|
* - srvCache: Optional BagOStuff instance to an APC-style cache.
|
2018-02-08 19:16:29 +00:00
|
|
|
* - nonNativeInsertSelectBatchSize: Optional batch size for non-native INSERT SELECT emulation.
|
2018-02-28 20:56:34 +00:00
|
|
|
* @param int $connect One of the class constants (NEW_CONNECTED, NEW_UNCONNECTED) [optional]
|
2016-09-19 23:34:32 +00:00
|
|
|
* @return Database|null If the database driver or extension cannot be found
|
2016-09-16 03:14:58 +00:00
|
|
|
* @throws InvalidArgumentException If the database driver or extension cannot be found
|
2016-09-19 23:34:32 +00:00
|
|
|
* @since 1.18
|
2016-09-16 03:14:58 +00:00
|
|
|
*/
|
2018-02-28 20:56:34 +00:00
|
|
|
final public static function factory( $dbType, $p = [], $connect = self::NEW_CONNECTED ) {
|
2017-10-06 22:17:58 +00:00
|
|
|
$class = self::getClass( $dbType, $p['driver'] ?? null );
|
2018-02-27 21:44:14 +00:00
|
|
|
|
|
|
|
|
if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
|
|
|
|
|
// Resolve some defaults for b/c
|
2017-10-06 22:17:58 +00:00
|
|
|
$p['host'] = $p['host'] ?? false;
|
|
|
|
|
$p['user'] = $p['user'] ?? false;
|
|
|
|
|
$p['password'] = $p['password'] ?? false;
|
|
|
|
|
$p['dbname'] = $p['dbname'] ?? false;
|
|
|
|
|
$p['flags'] = $p['flags'] ?? 0;
|
|
|
|
|
$p['variables'] = $p['variables'] ?? [];
|
|
|
|
|
$p['tablePrefix'] = $p['tablePrefix'] ?? '';
|
2018-08-14 23:44:41 +00:00
|
|
|
$p['schema'] = $p['schema'] ?? null;
|
2017-10-06 22:17:58 +00:00
|
|
|
$p['cliMode'] = $p['cliMode'] ?? ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' );
|
|
|
|
|
$p['agent'] = $p['agent'] ?? '';
|
2018-02-27 21:44:14 +00:00
|
|
|
if ( !isset( $p['connLogger'] ) ) {
|
|
|
|
|
$p['connLogger'] = new NullLogger();
|
|
|
|
|
}
|
|
|
|
|
if ( !isset( $p['queryLogger'] ) ) {
|
|
|
|
|
$p['queryLogger'] = new NullLogger();
|
|
|
|
|
}
|
2017-10-06 22:17:58 +00:00
|
|
|
$p['profiler'] = $p['profiler'] ?? null;
|
2018-02-27 21:44:14 +00:00
|
|
|
if ( !isset( $p['trxProfiler'] ) ) {
|
|
|
|
|
$p['trxProfiler'] = new TransactionProfiler();
|
|
|
|
|
}
|
|
|
|
|
if ( !isset( $p['errorLogger'] ) ) {
|
|
|
|
|
$p['errorLogger'] = function ( Exception $e ) {
|
|
|
|
|
trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
|
|
|
|
|
};
|
|
|
|
|
}
|
2018-04-05 16:13:08 +00:00
|
|
|
if ( !isset( $p['deprecationLogger'] ) ) {
|
|
|
|
|
$p['deprecationLogger'] = function ( $msg ) {
|
|
|
|
|
trigger_error( $msg, E_USER_DEPRECATED );
|
|
|
|
|
};
|
|
|
|
|
}
|
2018-02-27 21:44:14 +00:00
|
|
|
|
2018-02-28 20:56:34 +00:00
|
|
|
/** @var Database $conn */
|
2018-02-27 21:44:14 +00:00
|
|
|
$conn = new $class( $p );
|
2018-02-28 20:56:34 +00:00
|
|
|
if ( $connect == self::NEW_CONNECTED ) {
|
|
|
|
|
$conn->initConnection();
|
|
|
|
|
}
|
2018-02-27 21:44:14 +00:00
|
|
|
} else {
|
|
|
|
|
$conn = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $conn;
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-28 00:00:05 +00:00
|
|
|
/**
|
|
|
|
|
* @param string $dbType A possible DB type (sqlite, mysql, postgres,...)
|
|
|
|
|
* @param string|null $driver Optional name of a specific DB client driver
|
|
|
|
|
* @return array Map of (Database::ATTRIBUTE_* constant => value) for all such constants
|
|
|
|
|
* @throws InvalidArgumentException
|
|
|
|
|
* @since 1.31
|
|
|
|
|
*/
|
|
|
|
|
final public static function attributesFromType( $dbType, $driver = null ) {
|
|
|
|
|
static $defaults = [ self::ATTR_DB_LEVEL_LOCKING => false ];
|
|
|
|
|
|
|
|
|
|
$class = self::getClass( $dbType, $driver );
|
|
|
|
|
|
|
|
|
|
return call_user_func( [ $class, 'getAttributes' ] ) + $defaults;
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-27 21:44:14 +00:00
|
|
|
/**
|
|
|
|
|
* @param string $dbType A possible DB type (sqlite, mysql, postgres,...)
|
|
|
|
|
* @param string|null $driver Optional name of a specific DB client driver
|
|
|
|
|
* @return string Database subclass name to use
|
|
|
|
|
* @throws InvalidArgumentException
|
|
|
|
|
*/
|
|
|
|
|
private static function getClass( $dbType, $driver = null ) {
|
2018-01-24 19:33:37 +00:00
|
|
|
// For database types with built-in support, the below maps type to IDatabase
|
|
|
|
|
// implementations. For types with multipe driver implementations (PHP extensions),
|
|
|
|
|
// an array can be used, keyed by extension name. In case of an array, the
|
|
|
|
|
// optional 'driver' parameter can be used to force a specific driver. Otherwise,
|
|
|
|
|
// we auto-detect the first available driver. For types without built-in support,
|
|
|
|
|
// an class named "Database<Type>" us used, eg. DatabaseFoo for type 'foo'.
|
|
|
|
|
static $builtinTypes = [
|
|
|
|
|
'mssql' => DatabaseMssql::class,
|
|
|
|
|
'mysql' => [ 'mysqli' => DatabaseMysqli::class ],
|
|
|
|
|
'sqlite' => DatabaseSqlite::class,
|
|
|
|
|
'postgres' => DatabasePostgres::class,
|
2017-02-07 04:49:57 +00:00
|
|
|
];
|
2016-09-16 03:14:58 +00:00
|
|
|
|
|
|
|
|
$dbType = strtolower( $dbType );
|
2018-01-24 19:33:37 +00:00
|
|
|
$class = false;
|
2018-02-27 21:44:14 +00:00
|
|
|
|
2018-01-24 19:33:37 +00:00
|
|
|
if ( isset( $builtinTypes[$dbType] ) ) {
|
|
|
|
|
$possibleDrivers = $builtinTypes[$dbType];
|
|
|
|
|
if ( is_string( $possibleDrivers ) ) {
|
|
|
|
|
$class = $possibleDrivers;
|
2016-09-16 03:14:58 +00:00
|
|
|
} else {
|
2018-02-27 21:44:14 +00:00
|
|
|
if ( (string)$driver !== '' ) {
|
|
|
|
|
if ( !isset( $possibleDrivers[$driver] ) ) {
|
2018-01-24 19:33:37 +00:00
|
|
|
throw new InvalidArgumentException( __METHOD__ .
|
2018-02-27 21:44:14 +00:00
|
|
|
" type '$dbType' does not support driver '{$driver}'" );
|
2018-01-24 19:33:37 +00:00
|
|
|
} else {
|
2018-02-27 21:44:14 +00:00
|
|
|
$class = $possibleDrivers[$driver];
|
2018-01-24 19:33:37 +00:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
|
|
|
|
|
if ( extension_loaded( $posDriver ) ) {
|
|
|
|
|
$class = $possibleClass;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2018-01-24 19:33:37 +00:00
|
|
|
$class = 'Database' . ucfirst( $dbType );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
2017-02-07 04:49:57 +00:00
|
|
|
|
2018-01-24 19:33:37 +00:00
|
|
|
if ( $class === false ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
throw new InvalidArgumentException( __METHOD__ .
|
|
|
|
|
" no viable database extension found for type '$dbType'" );
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-27 21:44:14 +00:00
|
|
|
return $class;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-02-28 00:00:05 +00:00
|
|
|
/**
|
|
|
|
|
* @return array Map of (Database::ATTRIBUTE_* constant => value
|
|
|
|
|
* @since 1.31
|
|
|
|
|
*/
|
|
|
|
|
protected static function getAttributes() {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-13 01:10:47 +00:00
|
|
|
/**
|
|
|
|
|
* Set the PSR-3 logger interface to use for query logging. (The logger
|
|
|
|
|
* interfaces for connection logging and error logging can be set with the
|
|
|
|
|
* constructor.)
|
|
|
|
|
*
|
|
|
|
|
* @param LoggerInterface $logger
|
|
|
|
|
*/
|
2016-09-16 03:14:58 +00:00
|
|
|
public function setLogger( LoggerInterface $logger ) {
|
|
|
|
|
$this->queryLogger = $logger;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getServerInfo() {
|
|
|
|
|
return $this->getServerVersion();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function bufferResults( $buffer = null ) {
|
2016-09-23 19:41:22 +00:00
|
|
|
$res = !$this->getFlag( self::DBO_NOBUFFER );
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( $buffer !== null ) {
|
2016-09-23 19:41:22 +00:00
|
|
|
$buffer
|
|
|
|
|
? $this->clearFlag( self::DBO_NOBUFFER )
|
|
|
|
|
: $this->setFlag( self::DBO_NOBUFFER );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function trxLevel() {
|
2018-02-13 06:58:57 +00:00
|
|
|
return $this->trxLevel;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function trxTimestamp() {
|
2018-02-13 06:58:57 +00:00
|
|
|
return $this->trxLevel ? $this->trxTimestamp : null;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-03-23 09:57:21 +00:00
|
|
|
/**
|
|
|
|
|
* @return int One of the STATUS_TRX_* class constants
|
|
|
|
|
* @since 1.31
|
|
|
|
|
*/
|
|
|
|
|
public function trxStatus() {
|
|
|
|
|
return $this->trxStatus;
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
public function tablePrefix( $prefix = null ) {
|
2018-08-14 23:44:41 +00:00
|
|
|
$old = $this->currentDomain->getTablePrefix();
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( $prefix !== null ) {
|
2018-08-14 23:44:41 +00:00
|
|
|
$this->currentDomain = new DatabaseDomain(
|
|
|
|
|
$this->currentDomain->getDatabase(),
|
|
|
|
|
$this->currentDomain->getSchema(),
|
|
|
|
|
$prefix
|
|
|
|
|
);
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $old;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function dbSchema( $schema = null ) {
|
2018-08-14 23:44:41 +00:00
|
|
|
$old = $this->currentDomain->getSchema();
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( $schema !== null ) {
|
2018-08-14 23:44:41 +00:00
|
|
|
$this->currentDomain = new DatabaseDomain(
|
|
|
|
|
$this->currentDomain->getDatabase(),
|
|
|
|
|
// DatabaseDomain uses null for unspecified schemas
|
|
|
|
|
strlen( $schema ) ? $schema : null,
|
|
|
|
|
$this->currentDomain->getTablePrefix()
|
|
|
|
|
);
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-08-14 23:44:41 +00:00
|
|
|
return (string)$old;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return string Schema to use to qualify relations in queries
|
|
|
|
|
*/
|
|
|
|
|
protected function relationSchemaQualifier() {
|
|
|
|
|
return $this->dbSchema();
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getLBInfo( $name = null ) {
|
|
|
|
|
if ( is_null( $name ) ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
return $this->lbInfo;
|
2016-09-16 03:14:58 +00:00
|
|
|
} else {
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( array_key_exists( $name, $this->lbInfo ) ) {
|
|
|
|
|
return $this->lbInfo[$name];
|
2016-09-16 03:14:58 +00:00
|
|
|
} else {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function setLBInfo( $name, $value = null ) {
|
|
|
|
|
if ( is_null( $value ) ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->lbInfo = $name;
|
2016-09-16 03:14:58 +00:00
|
|
|
} else {
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->lbInfo[$name] = $value;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function setLazyMasterHandle( IDatabase $conn ) {
|
|
|
|
|
$this->lazyMasterHandle = $conn;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return IDatabase|null
|
|
|
|
|
* @see setLazyMasterHandle()
|
|
|
|
|
* @since 1.27
|
|
|
|
|
*/
|
2016-09-23 03:11:18 +00:00
|
|
|
protected function getLazyMasterHandle() {
|
2016-09-16 03:14:58 +00:00
|
|
|
return $this->lazyMasterHandle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function implicitGroupby() {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function implicitOrderby() {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function lastQuery() {
|
2018-02-13 06:58:57 +00:00
|
|
|
return $this->lastQuery;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function doneWrites() {
|
2018-02-13 06:58:57 +00:00
|
|
|
return (bool)$this->lastWriteTime;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function lastDoneWrites() {
|
2018-02-13 06:58:57 +00:00
|
|
|
return $this->lastWriteTime ?: false;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function writesPending() {
|
2018-02-13 06:58:57 +00:00
|
|
|
return $this->trxLevel && $this->trxDoneWrites;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function writesOrCallbacksPending() {
|
2018-02-13 06:58:57 +00:00
|
|
|
return $this->trxLevel && (
|
2018-03-08 21:38:10 +00:00
|
|
|
$this->trxDoneWrites ||
|
|
|
|
|
$this->trxIdleCallbacks ||
|
|
|
|
|
$this->trxPreCommitCallbacks ||
|
|
|
|
|
$this->trxEndCallbacks
|
2016-09-16 03:14:58 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-05 22:09:30 +00:00
|
|
|
public function preCommitCallbacksPending() {
|
|
|
|
|
return $this->trxLevel && $this->trxPreCommitCallbacks;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-19 05:26:11 +00:00
|
|
|
/**
|
|
|
|
|
* @return string|null
|
|
|
|
|
*/
|
|
|
|
|
final protected function getTransactionRoundId() {
|
|
|
|
|
// If transaction round participation is enabled, see if one is active
|
|
|
|
|
if ( $this->getFlag( self::DBO_TRX ) ) {
|
|
|
|
|
$id = $this->getLBInfo( 'trxRoundId' );
|
|
|
|
|
|
|
|
|
|
return is_string( $id ) ? $id : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( !$this->trxLevel ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
return false;
|
2018-02-13 06:58:57 +00:00
|
|
|
} elseif ( !$this->trxDoneWrites ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
return 0.0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch ( $type ) {
|
|
|
|
|
case self::ESTIMATE_DB_APPLY:
|
2018-10-25 15:34:39 +00:00
|
|
|
return $this->pingAndCalculateLastTrxApplyTime();
|
2016-09-16 03:14:58 +00:00
|
|
|
default: // everything
|
2018-02-13 06:58:57 +00:00
|
|
|
return $this->trxWriteDuration;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-25 15:34:39 +00:00
|
|
|
/**
|
|
|
|
|
* @return float Time to apply writes to replicas based on trxWrite* fields
|
|
|
|
|
*/
|
|
|
|
|
private function pingAndCalculateLastTrxApplyTime() {
|
|
|
|
|
$this->ping( $rtt );
|
|
|
|
|
|
|
|
|
|
$rttAdjTotal = $this->trxWriteAdjQueryCount * $rtt;
|
|
|
|
|
$applyTime = max( $this->trxWriteAdjDuration - $rttAdjTotal, 0 );
|
|
|
|
|
// For omitted queries, make them count as something at least
|
|
|
|
|
$omitted = $this->trxWriteQueryCount - $this->trxWriteAdjQueryCount;
|
|
|
|
|
$applyTime += self::TINY_WRITE_SEC * $omitted;
|
|
|
|
|
|
|
|
|
|
return $applyTime;
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
public function pendingWriteCallers() {
|
2018-02-13 06:58:57 +00:00
|
|
|
return $this->trxLevel ? $this->trxWriteCallers : [];
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2017-05-26 18:42:05 +00:00
|
|
|
public function pendingWriteRowsAffected() {
|
2018-02-13 06:58:57 +00:00
|
|
|
return $this->trxWriteAffectedRows;
|
2017-05-26 18:42:05 +00:00
|
|
|
}
|
|
|
|
|
|
2017-05-13 01:10:47 +00:00
|
|
|
/**
|
2018-05-01 17:57:18 +00:00
|
|
|
* List the methods that have write queries or callbacks for the current transaction
|
2017-05-13 01:10:47 +00:00
|
|
|
*
|
2018-05-01 17:57:18 +00:00
|
|
|
* This method should not be used outside of Database/LoadBalancer
|
|
|
|
|
*
|
|
|
|
|
* @return string[]
|
|
|
|
|
* @since 1.32
|
2017-05-13 01:10:47 +00:00
|
|
|
*/
|
2018-05-01 17:57:18 +00:00
|
|
|
public function pendingWriteAndCallbackCallers() {
|
2018-03-28 20:01:32 +00:00
|
|
|
$fnames = $this->pendingWriteCallers();
|
2016-09-16 03:14:58 +00:00
|
|
|
foreach ( [
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->trxIdleCallbacks,
|
|
|
|
|
$this->trxPreCommitCallbacks,
|
|
|
|
|
$this->trxEndCallbacks
|
2016-09-16 03:14:58 +00:00
|
|
|
] as $callbacks ) {
|
|
|
|
|
foreach ( $callbacks as $callback ) {
|
|
|
|
|
$fnames[] = $callback[1];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $fnames;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-23 09:57:21 +00:00
|
|
|
/**
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
private function flatAtomicSectionList() {
|
|
|
|
|
return array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) {
|
|
|
|
|
return $accum === null ? $v[0] : "$accum, " . $v[0];
|
|
|
|
|
} );
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
public function isOpen() {
|
2018-02-13 06:58:57 +00:00
|
|
|
return $this->opened;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
|
2017-11-23 10:17:14 +00:00
|
|
|
if ( ( $flag & self::DBO_IGNORE ) ) {
|
2018-03-24 12:16:29 +00:00
|
|
|
throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
|
2017-11-23 10:17:14 +00:00
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( $remember === self::REMEMBER_PRIOR ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
array_push( $this->priorFlags, $this->flags );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->flags |= $flag;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
|
2017-11-23 10:17:14 +00:00
|
|
|
if ( ( $flag & self::DBO_IGNORE ) ) {
|
2018-03-24 12:16:29 +00:00
|
|
|
throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
|
2017-11-23 10:17:14 +00:00
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( $remember === self::REMEMBER_PRIOR ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
array_push( $this->priorFlags, $this->flags );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->flags &= ~$flag;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function restoreFlags( $state = self::RESTORE_PRIOR ) {
|
|
|
|
|
if ( !$this->priorFlags ) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $state === self::RESTORE_INITIAL ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->flags = reset( $this->priorFlags );
|
2016-09-16 03:14:58 +00:00
|
|
|
$this->priorFlags = [];
|
|
|
|
|
} else {
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->flags = array_pop( $this->priorFlags );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getFlag( $flag ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
return !!( $this->flags & $flag );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2016-10-18 18:05:39 +00:00
|
|
|
/**
|
|
|
|
|
* @param string $name Class field name
|
|
|
|
|
* @return mixed
|
|
|
|
|
* @deprecated Since 1.28
|
|
|
|
|
*/
|
2016-09-16 03:14:58 +00:00
|
|
|
public function getProperty( $name ) {
|
|
|
|
|
return $this->$name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getDomainID() {
|
2016-09-17 04:39:57 +00:00
|
|
|
return $this->currentDomain->getId();
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final public function getWikiID() {
|
|
|
|
|
return $this->getDomainID();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get information about an index into an object
|
|
|
|
|
* @param string $table Table name
|
|
|
|
|
* @param string $index Index name
|
|
|
|
|
* @param string $fname Calling function name
|
|
|
|
|
* @return mixed Database-specific index description class or false if the index does not exist
|
|
|
|
|
*/
|
|
|
|
|
abstract function indexInfo( $table, $index, $fname = __METHOD__ );
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Wrapper for addslashes()
|
|
|
|
|
*
|
|
|
|
|
* @param string $s String to be slashed.
|
|
|
|
|
* @return string Slashed string.
|
|
|
|
|
*/
|
|
|
|
|
abstract function strencode( $s );
|
|
|
|
|
|
2017-05-13 01:10:47 +00:00
|
|
|
/**
|
|
|
|
|
* Set a custom error handler for logging errors during database connection
|
|
|
|
|
*/
|
2016-09-16 03:14:58 +00:00
|
|
|
protected function installErrorHandler() {
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->phpError = false;
|
2016-09-16 03:14:58 +00:00
|
|
|
$this->htmlErrors = ini_set( 'html_errors', '0' );
|
2016-09-21 19:25:08 +00:00
|
|
|
set_error_handler( [ $this, 'connectionErrorLogger' ] );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2017-05-13 01:10:47 +00:00
|
|
|
* Restore the previous error handler and return the last PHP error for this DB
|
|
|
|
|
*
|
2016-09-16 03:14:58 +00:00
|
|
|
* @return bool|string
|
|
|
|
|
*/
|
|
|
|
|
protected function restoreErrorHandler() {
|
|
|
|
|
restore_error_handler();
|
|
|
|
|
if ( $this->htmlErrors !== false ) {
|
|
|
|
|
ini_set( 'html_errors', $this->htmlErrors );
|
|
|
|
|
}
|
2016-10-18 01:21:46 +00:00
|
|
|
|
|
|
|
|
return $this->getLastPHPError();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return string|bool Last PHP error for this DB (typically connection errors)
|
|
|
|
|
*/
|
|
|
|
|
protected function getLastPHPError() {
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( $this->phpError ) {
|
|
|
|
|
$error = preg_replace( '!\[<a.*</a>\]!', '', $this->phpError );
|
2016-09-16 03:14:58 +00:00
|
|
|
$error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
|
|
|
|
|
|
|
|
|
|
return $error;
|
|
|
|
|
}
|
2016-10-18 01:21:46 +00:00
|
|
|
|
|
|
|
|
return false;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2017-05-13 01:10:47 +00:00
|
|
|
* Error handler for logging errors during database connection
|
2016-09-21 19:25:08 +00:00
|
|
|
* This method should not be used outside of Database classes
|
|
|
|
|
*
|
2016-09-16 03:14:58 +00:00
|
|
|
* @param int $errno
|
|
|
|
|
* @param string $errstr
|
|
|
|
|
*/
|
2016-09-21 19:25:08 +00:00
|
|
|
public function connectionErrorLogger( $errno, $errstr ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->phpError = $errstr;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2016-09-19 23:34:32 +00:00
|
|
|
* Create a log context to pass to PSR-3 logger functions.
|
2016-09-16 03:14:58 +00:00
|
|
|
*
|
|
|
|
|
* @param array $extras Additional data to add to context
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
protected function getLogContext( array $extras = [] ) {
|
|
|
|
|
return array_merge(
|
|
|
|
|
[
|
2018-02-13 06:58:57 +00:00
|
|
|
'db_server' => $this->server,
|
2018-08-14 23:44:41 +00:00
|
|
|
'db_name' => $this->getDBname(),
|
2018-02-13 06:58:57 +00:00
|
|
|
'db_user' => $this->user,
|
2016-09-16 03:14:58 +00:00
|
|
|
],
|
|
|
|
|
$extras
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-23 09:57:21 +00:00
|
|
|
final public function close() {
|
|
|
|
|
$exception = null; // error to throw after disconnecting
|
|
|
|
|
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( $this->conn ) {
|
2018-10-03 17:38:19 +00:00
|
|
|
// Roll back any dangling transaction first
|
2018-03-23 09:57:21 +00:00
|
|
|
if ( $this->trxLevel ) {
|
2018-04-10 23:56:29 +00:00
|
|
|
if ( $this->trxAtomicLevels ) {
|
2018-03-23 09:57:21 +00:00
|
|
|
// Cannot let incomplete atomic sections be committed
|
2018-04-10 23:56:29 +00:00
|
|
|
$levels = $this->flatAtomicSectionList();
|
|
|
|
|
$exception = new DBUnexpectedError(
|
|
|
|
|
$this,
|
|
|
|
|
__METHOD__ . ": atomic sections $levels are still open."
|
|
|
|
|
);
|
|
|
|
|
} elseif ( $this->trxAutomatic ) {
|
|
|
|
|
// Only the connection manager can commit non-empty DBO_TRX transactions
|
2018-10-03 17:38:19 +00:00
|
|
|
// (empty ones we can silently roll back)
|
2018-04-10 23:56:29 +00:00
|
|
|
if ( $this->writesOrCallbacksPending() ) {
|
2018-03-23 09:57:21 +00:00
|
|
|
$exception = new DBUnexpectedError(
|
|
|
|
|
$this,
|
2018-04-10 23:56:29 +00:00
|
|
|
__METHOD__ .
|
|
|
|
|
": mass commit/rollback of peer transaction required (DBO_TRX set)."
|
2018-03-23 09:57:21 +00:00
|
|
|
);
|
|
|
|
|
}
|
2018-10-03 17:38:19 +00:00
|
|
|
} else {
|
|
|
|
|
// Manual transactions should have been committed or rolled
|
|
|
|
|
// back, even if empty.
|
|
|
|
|
$exception = new DBUnexpectedError(
|
|
|
|
|
$this,
|
|
|
|
|
__METHOD__ . ": transaction is still open (from {$this->trxFname})."
|
2018-04-10 23:56:29 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $this->trxEndCallbacksSuppressed ) {
|
|
|
|
|
$exception = $exception ?: new DBUnexpectedError(
|
|
|
|
|
$this,
|
|
|
|
|
__METHOD__ . ': callbacks are suppressed; cannot properly commit.'
|
|
|
|
|
);
|
2018-03-08 21:38:10 +00:00
|
|
|
}
|
2018-04-10 23:56:29 +00:00
|
|
|
|
2018-10-03 17:38:19 +00:00
|
|
|
// Rollback the changes and run any callbacks as needed
|
|
|
|
|
$this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
2018-04-10 23:56:29 +00:00
|
|
|
|
2018-03-08 21:38:10 +00:00
|
|
|
// Close the actual connection in the binding handle
|
2016-09-16 03:14:58 +00:00
|
|
|
$closed = $this->closeConnection();
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->conn = false;
|
2016-09-16 03:14:58 +00:00
|
|
|
} else {
|
2018-03-08 21:38:10 +00:00
|
|
|
$closed = true; // already closed; nothing to do
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
2018-03-08 21:38:10 +00:00
|
|
|
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->opened = false;
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2018-03-23 09:57:21 +00:00
|
|
|
// Throw any unexpected errors after having disconnected
|
|
|
|
|
if ( $exception instanceof Exception ) {
|
|
|
|
|
throw $exception;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sanity check that no callbacks are dangling
|
2018-03-28 20:01:32 +00:00
|
|
|
$fnames = $this->pendingWriteAndCallbackCallers();
|
|
|
|
|
if ( $fnames ) {
|
2018-03-23 09:57:21 +00:00
|
|
|
throw new RuntimeException(
|
2018-03-28 20:01:32 +00:00
|
|
|
"Transaction callbacks are still pending:\n" . implode( ', ', $fnames )
|
2018-03-23 09:57:21 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
return $closed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Make sure isOpen() returns true as a sanity check
|
|
|
|
|
*
|
|
|
|
|
* @throws DBUnexpectedError
|
|
|
|
|
*/
|
|
|
|
|
protected function assertOpen() {
|
|
|
|
|
if ( !$this->isOpen() ) {
|
|
|
|
|
throw new DBUnexpectedError( $this, "DB connection was already closed." );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Closes underlying database connection
|
|
|
|
|
* @since 1.20
|
|
|
|
|
* @return bool Whether connection was closed successfully
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function closeConnection();
|
|
|
|
|
|
2018-03-22 15:33:59 +00:00
|
|
|
/**
|
2018-04-19 23:21:51 +00:00
|
|
|
* @deprecated since 1.32
|
|
|
|
|
* @param string $error Fallback message, if none is given by DB
|
2018-03-22 15:33:59 +00:00
|
|
|
* @throws DBConnectionError
|
|
|
|
|
*/
|
2016-09-21 19:25:08 +00:00
|
|
|
public function reportConnectionError( $error = 'Unknown error' ) {
|
2018-04-19 23:21:51 +00:00
|
|
|
call_user_func( $this->deprecationLogger, 'Use of ' . __METHOD__ . ' is deprecated.' );
|
|
|
|
|
throw new DBConnectionError( $this, $this->lastError() ?: $error );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2018-02-28 20:56:34 +00:00
|
|
|
* Run a query and return a DBMS-dependent wrapper (that has all IResultWrapper methods)
|
|
|
|
|
*
|
|
|
|
|
* This might return things, such as mysqli_result, that do not formally implement
|
|
|
|
|
* IResultWrapper, but nonetheless implement all of its methods correctly
|
2016-09-16 03:14:58 +00:00
|
|
|
*
|
|
|
|
|
* @param string $sql SQL query.
|
2018-02-28 20:56:34 +00:00
|
|
|
* @return IResultWrapper|bool Iterator to feed to fetchObject/fetchRow; false on failure
|
2016-09-16 03:14:58 +00:00
|
|
|
*/
|
|
|
|
|
abstract protected function doQuery( $sql );
|
|
|
|
|
|
|
|
|
|
/**
|
2018-09-28 23:42:43 +00:00
|
|
|
* Determine whether a query writes to the DB. When in doubt, this returns true.
|
|
|
|
|
*
|
|
|
|
|
* Main use cases:
|
|
|
|
|
*
|
|
|
|
|
* - Subsequent web requests should not need to wait for replication from
|
|
|
|
|
* the master position seen by this web request, unless this request made
|
|
|
|
|
* changes to the master. This is handled by ChronologyProtector by checking
|
|
|
|
|
* doneWrites() at the end of the request. doneWrites() returns true if any
|
|
|
|
|
* query set lastWriteTime; which query() does based on isWriteQuery().
|
|
|
|
|
*
|
|
|
|
|
* - Reject write queries to replica DBs, in query().
|
2016-09-16 03:14:58 +00:00
|
|
|
*
|
|
|
|
|
* @param string $sql
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
protected function isWriteQuery( $sql ) {
|
2018-09-28 23:42:43 +00:00
|
|
|
// BEGIN and COMMIT queries are considered read queries here.
|
|
|
|
|
// Database backends and drivers (MySQL, MariaDB, php-mysqli) generally
|
|
|
|
|
// treat these as write queries, in that their results have "affected rows"
|
|
|
|
|
// as meta data as from writes, instead of "num rows" as from reads.
|
|
|
|
|
// But, we treat them as read queries because when reading data (from
|
|
|
|
|
// either replica or master) we use transactions to enable repeatable-read
|
|
|
|
|
// snapshots, which ensures we get consistent results from the same snapshot
|
|
|
|
|
// for all queries within a request. Use cases:
|
|
|
|
|
// - Treating these as writes would trigger ChronologyProtector (see method doc).
|
|
|
|
|
// - We use this method to reject writes to replicas, but we need to allow
|
|
|
|
|
// use of transactions on replicas for read snapshots. This fine given
|
|
|
|
|
// that transactions by themselves don't make changes, only actual writes
|
|
|
|
|
// within the transaction matter, which we still detect.
|
2016-09-16 03:14:58 +00:00
|
|
|
return !preg_match(
|
2018-10-17 05:36:06 +00:00
|
|
|
'/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SAVEPOINT|RELEASE|SET|SHOW|EXPLAIN|\(SELECT)\b/i',
|
|
|
|
|
$sql
|
|
|
|
|
);
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2017-08-11 15:46:31 +00:00
|
|
|
* @param string $sql
|
2016-09-16 03:14:58 +00:00
|
|
|
* @return string|null
|
|
|
|
|
*/
|
|
|
|
|
protected function getQueryVerb( $sql ) {
|
|
|
|
|
return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determine whether a SQL statement is sensitive to isolation level.
|
2018-09-28 23:42:43 +00:00
|
|
|
*
|
2016-09-16 03:14:58 +00:00
|
|
|
* A SQL statement is considered transactable if its result could vary
|
|
|
|
|
* depending on the transaction isolation level. Operational commands
|
|
|
|
|
* such as 'SET' and 'SHOW' are not considered to be transactable.
|
|
|
|
|
*
|
2018-09-28 23:42:43 +00:00
|
|
|
* Main purpose: Used by query() to decide whether to begin a transaction
|
|
|
|
|
* before the current query (in DBO_TRX mode, on by default).
|
|
|
|
|
*
|
2016-09-16 03:14:58 +00:00
|
|
|
* @param string $sql
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
protected function isTransactableQuery( $sql ) {
|
2016-10-17 18:21:40 +00:00
|
|
|
return !in_array(
|
|
|
|
|
$this->getQueryVerb( $sql ),
|
2018-09-28 23:42:43 +00:00
|
|
|
[ 'BEGIN', 'ROLLBACK', 'COMMIT', 'SET', 'SHOW', 'CREATE', 'ALTER' ],
|
2016-10-17 18:21:40 +00:00
|
|
|
true
|
|
|
|
|
);
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2016-09-19 21:15:05 +00:00
|
|
|
/**
|
|
|
|
|
* @param string $sql A SQL query
|
2017-04-30 18:59:23 +00:00
|
|
|
* @return bool Whether $sql is SQL for TEMPORARY table operation
|
2016-09-19 21:15:05 +00:00
|
|
|
*/
|
|
|
|
|
protected function registerTempTableOperation( $sql ) {
|
|
|
|
|
if ( preg_match(
|
2016-09-21 17:44:15 +00:00
|
|
|
'/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
|
2016-09-19 21:15:05 +00:00
|
|
|
$sql,
|
|
|
|
|
$matches
|
|
|
|
|
) ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->sessionTempTables[$matches[1]] = 1;
|
2016-09-21 17:44:15 +00:00
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
} elseif ( preg_match(
|
|
|
|
|
'/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
|
|
|
|
|
$sql,
|
|
|
|
|
$matches
|
|
|
|
|
) ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
$isTemp = isset( $this->sessionTempTables[$matches[1]] );
|
|
|
|
|
unset( $this->sessionTempTables[$matches[1]] );
|
2016-09-19 21:15:05 +00:00
|
|
|
|
2017-05-04 20:09:27 +00:00
|
|
|
return $isTemp;
|
2017-04-30 18:59:23 +00:00
|
|
|
} elseif ( preg_match(
|
|
|
|
|
'/^TRUNCATE\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
|
|
|
|
|
$sql,
|
|
|
|
|
$matches
|
|
|
|
|
) ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
return isset( $this->sessionTempTables[$matches[1]] );
|
2016-09-19 21:15:05 +00:00
|
|
|
} elseif ( preg_match(
|
|
|
|
|
'/^(?:INSERT\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+[`"\']?(\w+)[`"\']?/i',
|
|
|
|
|
$sql,
|
|
|
|
|
$matches
|
|
|
|
|
) ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
return isset( $this->sessionTempTables[$matches[1]] );
|
2016-09-19 21:15:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
|
2018-03-23 09:57:21 +00:00
|
|
|
$this->assertTransactionStatus( $sql, $fname );
|
|
|
|
|
|
2018-04-05 20:49:55 +00:00
|
|
|
# Avoid fatals if close() was called
|
|
|
|
|
$this->assertOpen();
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
$priorWritesPending = $this->writesOrCallbacksPending();
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->lastQuery = $sql;
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2017-04-30 18:59:23 +00:00
|
|
|
$isWrite = $this->isWriteQuery( $sql );
|
|
|
|
|
if ( $isWrite ) {
|
|
|
|
|
$isNonTempWrite = !$this->registerTempTableOperation( $sql );
|
|
|
|
|
} else {
|
|
|
|
|
$isNonTempWrite = false;
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( $isWrite ) {
|
2017-12-20 12:31:07 +00:00
|
|
|
if ( $this->getLBInfo( 'replica' ) === true ) {
|
2017-12-19 16:11:08 +00:00
|
|
|
throw new DBError(
|
|
|
|
|
$this,
|
2017-12-20 12:31:07 +00:00
|
|
|
'Write operations are not allowed on replica database connections.'
|
2017-12-19 16:11:08 +00:00
|
|
|
);
|
|
|
|
|
}
|
2017-04-30 18:59:23 +00:00
|
|
|
# In theory, non-persistent writes are allowed in read-only mode, but due to things
|
|
|
|
|
# like https://bugs.mysql.com/bug.php?id=33669 that might not work anyway...
|
2016-09-16 03:14:58 +00:00
|
|
|
$reason = $this->getReadOnlyReason();
|
|
|
|
|
if ( $reason !== false ) {
|
|
|
|
|
throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
|
|
|
|
|
}
|
|
|
|
|
# Set a flag indicating that writes have been done
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->lastWriteTime = microtime( true );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2017-04-30 18:59:23 +00:00
|
|
|
# Add trace comment to the begin of the sql string, right after the operator.
|
|
|
|
|
# Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
|
2016-09-16 03:14:58 +00:00
|
|
|
$commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
|
|
|
|
|
|
|
|
|
|
# Start implicit transactions that wrap the request if DBO_TRX is enabled
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( !$this->trxLevel && $this->getFlag( self::DBO_TRX )
|
2016-09-16 03:14:58 +00:00
|
|
|
&& $this->isTransactableQuery( $sql )
|
|
|
|
|
) {
|
|
|
|
|
$this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->trxAutomatic = true;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Keep track of whether the transaction has write queries pending
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( $this->trxLevel && !$this->trxDoneWrites && $isWrite ) {
|
|
|
|
|
$this->trxDoneWrites = true;
|
2016-09-16 03:14:58 +00:00
|
|
|
$this->trxProfiler->transactionWritingIn(
|
2018-08-15 08:20:30 +00:00
|
|
|
$this->server, $this->getDomainID(), $this->trxShortId );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2016-09-23 19:41:22 +00:00
|
|
|
if ( $this->getFlag( self::DBO_DEBUG ) ) {
|
2018-08-15 08:20:30 +00:00
|
|
|
$this->queryLogger->debug( "{$this->getDomainID()} {$commentedSql}" );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-03-20 11:16:41 +00:00
|
|
|
# Send the query to the server and fetch any corresponding errors
|
2017-04-30 18:59:23 +00:00
|
|
|
$ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
|
2018-03-20 11:16:41 +00:00
|
|
|
$lastError = $this->lastError();
|
|
|
|
|
$lastErrno = $this->lastErrno();
|
2016-09-16 03:14:58 +00:00
|
|
|
|
|
|
|
|
# Try reconnecting if the connection was lost
|
2018-03-20 11:16:41 +00:00
|
|
|
if ( $ret === false && $this->wasConnectionLoss() ) {
|
|
|
|
|
# Check if any meaningful session state was lost
|
2016-09-16 03:14:58 +00:00
|
|
|
$recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
|
2018-03-20 11:16:41 +00:00
|
|
|
# Update session state tracking and try to restore the connection
|
|
|
|
|
$reconnected = $this->replaceLostConnection( __METHOD__ );
|
|
|
|
|
# Silently resend the query to the server if it is safe and possible
|
|
|
|
|
if ( $reconnected && $recoverable ) {
|
|
|
|
|
$ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
|
|
|
|
|
$lastError = $this->lastError();
|
|
|
|
|
$lastErrno = $this->lastErrno();
|
|
|
|
|
|
|
|
|
|
if ( $ret === false && $this->wasConnectionLoss() ) {
|
|
|
|
|
# Query probably causes disconnects; reconnect and do not re-run it
|
|
|
|
|
$this->replaceLostConnection( __METHOD__ );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-20 11:16:41 +00:00
|
|
|
if ( $ret === false ) {
|
2018-04-05 18:17:09 +00:00
|
|
|
if ( $this->trxLevel ) {
|
2018-04-22 22:38:49 +00:00
|
|
|
if ( $this->wasKnownStatementRollbackError() ) {
|
2018-04-05 18:17:09 +00:00
|
|
|
# We're ignoring an error that caused just the current query to be aborted.
|
2018-04-22 22:38:49 +00:00
|
|
|
# But log the cause so we can log a deprecation notice if a caller actually
|
|
|
|
|
# does ignore it.
|
2018-04-05 18:17:09 +00:00
|
|
|
$this->trxStatusIgnoredCause = [ $lastError, $lastErrno, $fname ];
|
2018-04-22 22:38:49 +00:00
|
|
|
} else {
|
|
|
|
|
# Either the query was aborted or all queries after BEGIN where aborted.
|
|
|
|
|
# In the first case, the only options going forward are (a) ROLLBACK, or
|
|
|
|
|
# (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
|
|
|
|
|
# option is ROLLBACK, since the snapshots would have been released.
|
|
|
|
|
$this->trxStatus = self::STATUS_TRX_ERROR;
|
|
|
|
|
$this->trxStatusCause =
|
|
|
|
|
$this->makeQueryException( $lastError, $lastErrno, $sql, $fname );
|
|
|
|
|
$tempIgnore = false; // cannot recover
|
|
|
|
|
$this->trxStatusIgnoredCause = null;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-20 11:16:41 +00:00
|
|
|
$this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $tempIgnore );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-03-20 11:16:41 +00:00
|
|
|
return $this->resultObject( $ret );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2017-05-13 01:10:47 +00:00
|
|
|
/**
|
2018-01-28 14:10:39 +00:00
|
|
|
* Wrapper for query() that also handles profiling, logging, and affected row count updates
|
2017-05-13 01:10:47 +00:00
|
|
|
*
|
|
|
|
|
* @param string $sql Original SQL query
|
|
|
|
|
* @param string $commentedSql SQL query with debugging/trace comment
|
|
|
|
|
* @param bool $isWrite Whether the query is a (non-temporary) write operation
|
|
|
|
|
* @param string $fname Name of the calling function
|
|
|
|
|
* @return bool|ResultWrapper True for a successful write query, ResultWrapper
|
|
|
|
|
* object for a successful read query, or false on failure
|
|
|
|
|
*/
|
2016-09-16 03:14:58 +00:00
|
|
|
private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) {
|
|
|
|
|
$isMaster = !is_null( $this->getLBInfo( 'master' ) );
|
2016-11-16 10:35:20 +00:00
|
|
|
# generalizeSQL() will probably cut down the query to reasonable
|
|
|
|
|
# logging size most of the time. The substr is really just a sanity check.
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( $isMaster ) {
|
|
|
|
|
$queryProf = 'query-m: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
|
|
|
|
|
} else {
|
|
|
|
|
$queryProf = 'query: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-16 10:35:20 +00:00
|
|
|
# Include query transaction state
|
2018-02-13 06:58:57 +00:00
|
|
|
$queryProf .= $this->trxShortId ? " [TRX#{$this->trxShortId}]" : "";
|
2016-09-16 03:14:58 +00:00
|
|
|
|
|
|
|
|
$startTime = microtime( true );
|
2016-09-17 22:30:17 +00:00
|
|
|
if ( $this->profiler ) {
|
2018-06-09 23:26:32 +00:00
|
|
|
$this->profiler->profileIn( $queryProf );
|
2016-09-17 22:30:17 +00:00
|
|
|
}
|
2018-01-28 14:10:39 +00:00
|
|
|
$this->affectedRowCount = null;
|
2016-09-16 03:14:58 +00:00
|
|
|
$ret = $this->doQuery( $commentedSql );
|
2018-01-28 14:10:39 +00:00
|
|
|
$this->affectedRowCount = $this->affectedRows();
|
2016-09-17 22:30:17 +00:00
|
|
|
if ( $this->profiler ) {
|
2018-06-09 23:26:32 +00:00
|
|
|
$this->profiler->profileOut( $queryProf );
|
2016-09-17 22:30:17 +00:00
|
|
|
}
|
2016-09-16 03:14:58 +00:00
|
|
|
$queryRuntime = max( microtime( true ) - $startTime, 0.0 );
|
|
|
|
|
|
|
|
|
|
unset( $queryProfSection ); // profile out (if set)
|
|
|
|
|
|
|
|
|
|
if ( $ret !== false ) {
|
|
|
|
|
$this->lastPing = $startTime;
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( $isWrite && $this->trxLevel ) {
|
2017-05-26 18:42:05 +00:00
|
|
|
$this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() );
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->trxWriteCallers[] = $fname;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $sql === self::PING_QUERY ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->rttEstimate = $queryRuntime;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->trxProfiler->recordQueryCompletion(
|
2018-07-02 11:19:23 +00:00
|
|
|
$queryProf,
|
|
|
|
|
$startTime,
|
|
|
|
|
$isWrite,
|
|
|
|
|
$isWrite ? $this->affectedRows() : $this->numRows( $ret )
|
2016-09-16 03:14:58 +00:00
|
|
|
);
|
2016-09-16 20:57:56 +00:00
|
|
|
$this->queryLogger->debug( $sql, [
|
|
|
|
|
'method' => $fname,
|
|
|
|
|
'master' => $isMaster,
|
|
|
|
|
'runtime' => $queryRuntime,
|
|
|
|
|
] );
|
2016-09-16 03:14:58 +00:00
|
|
|
|
|
|
|
|
return $ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update the estimated run-time of a query, not counting large row lock times
|
|
|
|
|
*
|
|
|
|
|
* LoadBalancer can be set to rollback transactions that will create huge replication
|
|
|
|
|
* lag. It bases this estimate off of pendingWriteQueryDuration(). Certain simple
|
|
|
|
|
* queries, like inserting a row can take a long time due to row locking. This method
|
|
|
|
|
* uses some simple heuristics to discount those cases.
|
|
|
|
|
*
|
|
|
|
|
* @param string $sql A SQL write query
|
|
|
|
|
* @param float $runtime Total runtime, including RTT
|
2017-08-20 11:20:59 +00:00
|
|
|
* @param int $affected Affected row count
|
2016-09-16 03:14:58 +00:00
|
|
|
*/
|
2017-05-26 18:42:05 +00:00
|
|
|
private function updateTrxWriteQueryTime( $sql, $runtime, $affected ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
// Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
|
|
|
|
|
$indicativeOfReplicaRuntime = true;
|
|
|
|
|
if ( $runtime > self::SLOW_WRITE_SEC ) {
|
|
|
|
|
$verb = $this->getQueryVerb( $sql );
|
|
|
|
|
// insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
|
|
|
|
|
if ( $verb === 'INSERT' ) {
|
|
|
|
|
$indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS;
|
|
|
|
|
} elseif ( $verb === 'REPLACE' ) {
|
|
|
|
|
$indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS / 2;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->trxWriteDuration += $runtime;
|
|
|
|
|
$this->trxWriteQueryCount += 1;
|
|
|
|
|
$this->trxWriteAffectedRows += $affected;
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( $indicativeOfReplicaRuntime ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->trxWriteAdjDuration += $runtime;
|
|
|
|
|
$this->trxWriteAdjQueryCount += 1;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-23 09:57:21 +00:00
|
|
|
/**
|
|
|
|
|
* @param string $sql
|
|
|
|
|
* @param string $fname
|
|
|
|
|
* @throws DBTransactionStateError
|
|
|
|
|
*/
|
|
|
|
|
private function assertTransactionStatus( $sql, $fname ) {
|
2018-04-05 18:17:09 +00:00
|
|
|
if ( $this->getQueryVerb( $sql ) === 'ROLLBACK' ) { // transaction/savepoint
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $this->trxStatus < self::STATUS_TRX_OK ) {
|
2018-03-23 09:57:21 +00:00
|
|
|
throw new DBTransactionStateError(
|
|
|
|
|
$this,
|
2018-03-29 07:23:10 +00:00
|
|
|
"Cannot execute query from $fname while transaction status is ERROR.",
|
2018-03-23 09:57:21 +00:00
|
|
|
[],
|
|
|
|
|
$this->trxStatusCause
|
|
|
|
|
);
|
2018-04-05 18:17:09 +00:00
|
|
|
} elseif ( $this->trxStatus === self::STATUS_TRX_OK && $this->trxStatusIgnoredCause ) {
|
|
|
|
|
list( $iLastError, $iLastErrno, $iFname ) = $this->trxStatusIgnoredCause;
|
|
|
|
|
call_user_func( $this->deprecationLogger,
|
|
|
|
|
"Caller from $fname ignored an error originally raised from $iFname: " .
|
|
|
|
|
"[$iLastErrno] $iLastError"
|
|
|
|
|
);
|
|
|
|
|
$this->trxStatusIgnoredCause = null;
|
2018-03-23 09:57:21 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-09 02:04:59 +00:00
|
|
|
public function assertNoOpenTransactions() {
|
|
|
|
|
if ( $this->explicitTrxActive() ) {
|
|
|
|
|
throw new DBTransactionError(
|
|
|
|
|
$this,
|
|
|
|
|
"Explicit transaction still active. A caller may have caught an error. "
|
|
|
|
|
. "Open transactions: " . $this->flatAtomicSectionList()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-13 01:10:47 +00:00
|
|
|
/**
|
|
|
|
|
* Determine whether or not it is safe to retry queries after a database
|
|
|
|
|
* connection is lost
|
|
|
|
|
*
|
|
|
|
|
* @param string $sql SQL query
|
|
|
|
|
* @param bool $priorWritesPending Whether there is a transaction open with
|
|
|
|
|
* possible write queries or transaction pre-commit/idle callbacks
|
|
|
|
|
* waiting on it to finish.
|
|
|
|
|
* @return bool True if it is safe to retry the query, false otherwise
|
|
|
|
|
*/
|
2016-09-16 03:14:58 +00:00
|
|
|
private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
|
|
|
|
|
# Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
|
|
|
|
|
# Dropped connections also mean that named locks are automatically released.
|
|
|
|
|
# Only allow error suppression in autocommit mode or when the lost transaction
|
|
|
|
|
# didn't matter anyway (aside from DBO_TRX snapshot loss).
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( $this->namedLocksHeld ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
return false; // possible critical section violation
|
2018-03-20 11:16:41 +00:00
|
|
|
} elseif ( $this->sessionTempTables ) {
|
|
|
|
|
return false; // tables might be queried latter
|
2016-09-16 03:14:58 +00:00
|
|
|
} elseif ( $sql === 'COMMIT' ) {
|
|
|
|
|
return !$priorWritesPending; // nothing written anyway? (T127428)
|
|
|
|
|
} elseif ( $sql === 'ROLLBACK' ) {
|
|
|
|
|
return true; // transaction lost...which is also what was requested :)
|
|
|
|
|
} elseif ( $this->explicitTrxActive() ) {
|
2018-03-23 09:57:21 +00:00
|
|
|
return false; // don't drop atomocity and explicit snapshots
|
2016-09-16 03:14:58 +00:00
|
|
|
} elseif ( $priorWritesPending ) {
|
|
|
|
|
return false; // prior writes lost from implicit transaction
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-13 01:10:47 +00:00
|
|
|
/**
|
2018-03-20 11:16:41 +00:00
|
|
|
* Clean things up after session (and thus transaction) loss
|
2017-05-13 01:10:47 +00:00
|
|
|
*/
|
2016-09-19 21:15:05 +00:00
|
|
|
private function handleSessionLoss() {
|
2018-03-20 11:16:41 +00:00
|
|
|
// Clean up tracking of session-level things...
|
|
|
|
|
// https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html
|
2018-03-18 00:29:31 +00:00
|
|
|
// https://www.postgresql.org/docs/9.2/static/sql-createtable.html (ignoring ON COMMIT)
|
2018-03-20 11:16:41 +00:00
|
|
|
$this->sessionTempTables = [];
|
|
|
|
|
// https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
|
|
|
|
|
// https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
|
|
|
|
|
$this->namedLocksHeld = [];
|
|
|
|
|
// Session loss implies transaction loss
|
|
|
|
|
$this->handleTransactionLoss();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clean things up after transaction loss
|
|
|
|
|
*/
|
|
|
|
|
private function handleTransactionLoss() {
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->trxLevel = 0;
|
2018-03-17 21:59:56 +00:00
|
|
|
$this->trxAtomicCounter = 0;
|
2018-03-08 20:40:07 +00:00
|
|
|
$this->trxIdleCallbacks = []; // T67263; transaction already lost
|
|
|
|
|
$this->trxPreCommitCallbacks = []; // T67263; transaction already lost
|
2016-09-16 03:14:58 +00:00
|
|
|
try {
|
2018-03-20 11:16:41 +00:00
|
|
|
// Handle callbacks in trxEndCallbacks, e.g. onTransactionResolution().
|
|
|
|
|
// If callback suppression is set then the array will remain unhandled.
|
2016-09-16 03:14:58 +00:00
|
|
|
$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
|
2018-03-08 20:40:07 +00:00
|
|
|
} catch ( Exception $ex ) {
|
|
|
|
|
// Already logged; move on...
|
|
|
|
|
}
|
|
|
|
|
try {
|
2018-03-20 11:16:41 +00:00
|
|
|
// Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener()
|
2016-09-16 03:14:58 +00:00
|
|
|
$this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
|
2018-03-08 20:40:07 +00:00
|
|
|
} catch ( Exception $ex ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
// Already logged; move on...
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-18 11:38:59 +00:00
|
|
|
/**
|
|
|
|
|
* Checks whether the cause of the error is detected to be a timeout.
|
|
|
|
|
*
|
|
|
|
|
* It returns false by default, and not all engines support detecting this yet.
|
|
|
|
|
* If this returns false, it will be treated as a generic query error.
|
|
|
|
|
*
|
|
|
|
|
* @param string $error Error text
|
|
|
|
|
* @param int $errno Error number
|
2017-09-24 16:13:53 +00:00
|
|
|
* @return bool
|
2017-09-18 11:38:59 +00:00
|
|
|
*/
|
|
|
|
|
protected function wasQueryTimeout( $error, $errno ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-22 15:33:59 +00:00
|
|
|
/**
|
|
|
|
|
* Report a query error. Log the error, and if neither the object ignore
|
|
|
|
|
* flag nor the $tempIgnore flag is set, throw a DBQueryError.
|
|
|
|
|
*
|
|
|
|
|
* @param string $error
|
|
|
|
|
* @param int $errno
|
|
|
|
|
* @param string $sql
|
|
|
|
|
* @param string $fname
|
|
|
|
|
* @param bool $tempIgnore
|
|
|
|
|
* @throws DBQueryError
|
|
|
|
|
*/
|
2016-09-16 03:14:58 +00:00
|
|
|
public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
|
2018-03-24 12:17:12 +00:00
|
|
|
if ( $tempIgnore ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
|
|
|
|
|
} else {
|
2018-03-23 09:57:21 +00:00
|
|
|
$exception = $this->makeQueryException( $error, $errno, $sql, $fname );
|
|
|
|
|
|
|
|
|
|
throw $exception;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param string $error
|
|
|
|
|
* @param string|int $errno
|
|
|
|
|
* @param string $sql
|
|
|
|
|
* @param string $fname
|
|
|
|
|
* @return DBError
|
|
|
|
|
*/
|
|
|
|
|
private function makeQueryException( $error, $errno, $sql, $fname ) {
|
|
|
|
|
$sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
|
|
|
|
|
$this->queryLogger->error(
|
|
|
|
|
"{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
|
|
|
|
|
$this->getLogContext( [
|
|
|
|
|
'method' => __METHOD__,
|
|
|
|
|
'errno' => $errno,
|
|
|
|
|
'error' => $error,
|
|
|
|
|
'sql1line' => $sql1line,
|
|
|
|
|
'fname' => $fname,
|
|
|
|
|
] )
|
|
|
|
|
);
|
|
|
|
|
$this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
|
|
|
|
|
$wasQueryTimeout = $this->wasQueryTimeout( $error, $errno );
|
|
|
|
|
if ( $wasQueryTimeout ) {
|
|
|
|
|
$e = new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
|
|
|
|
|
} else {
|
|
|
|
|
$e = new DBQueryError( $this, $error, $errno, $sql, $fname );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
2018-03-23 09:57:21 +00:00
|
|
|
|
|
|
|
|
return $e;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function freeResult( $res ) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function selectField(
|
2017-06-09 16:58:09 +00:00
|
|
|
$table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
|
2016-09-16 03:14:58 +00:00
|
|
|
) {
|
|
|
|
|
if ( $var === '*' ) { // sanity
|
|
|
|
|
throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !is_array( $options ) ) {
|
|
|
|
|
$options = [ $options ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$options['LIMIT'] = 1;
|
|
|
|
|
|
2017-06-09 16:58:09 +00:00
|
|
|
$res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( $res === false || !$this->numRows( $res ) ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$row = $this->fetchRow( $res );
|
|
|
|
|
|
|
|
|
|
if ( $row !== false ) {
|
|
|
|
|
return reset( $row );
|
|
|
|
|
} else {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function selectFieldValues(
|
|
|
|
|
$table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
|
|
|
|
|
) {
|
|
|
|
|
if ( $var === '*' ) { // sanity
|
|
|
|
|
throw new DBUnexpectedError( $this, "Cannot use a * field" );
|
|
|
|
|
} elseif ( !is_string( $var ) ) { // sanity
|
|
|
|
|
throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !is_array( $options ) ) {
|
|
|
|
|
$options = [ $options ];
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-17 15:26:51 +00:00
|
|
|
$res = $this->select( $table, [ 'value' => $var ], $cond, $fname, $options, $join_conds );
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( $res === false ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$values = [];
|
|
|
|
|
foreach ( $res as $row ) {
|
2018-10-17 15:26:51 +00:00
|
|
|
$values[] = $row->value;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $values;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns an optional USE INDEX clause to go after the table, and a
|
|
|
|
|
* string to go at the end of the query.
|
|
|
|
|
*
|
|
|
|
|
* @param array $options Associative array of options to be turned into
|
|
|
|
|
* an SQL query, valid keys are listed in the function.
|
|
|
|
|
* @return array
|
2016-09-26 22:40:07 +00:00
|
|
|
* @see Database::select()
|
2016-09-16 03:14:58 +00:00
|
|
|
*/
|
2016-09-21 19:25:08 +00:00
|
|
|
protected function makeSelectOptions( $options ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$preLimitTail = $postLimitTail = '';
|
|
|
|
|
$startOpts = '';
|
|
|
|
|
|
|
|
|
|
$noKeyOptions = [];
|
|
|
|
|
|
|
|
|
|
foreach ( $options as $key => $option ) {
|
|
|
|
|
if ( is_numeric( $key ) ) {
|
|
|
|
|
$noKeyOptions[$option] = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$preLimitTail .= $this->makeGroupByWithHaving( $options );
|
|
|
|
|
|
|
|
|
|
$preLimitTail .= $this->makeOrderBy( $options );
|
|
|
|
|
|
|
|
|
|
if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
|
|
|
|
|
$postLimitTail .= ' FOR UPDATE';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
|
|
|
|
|
$postLimitTail .= ' LOCK IN SHARE MODE';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
|
|
|
|
|
$startOpts .= 'DISTINCT';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Various MySQL extensions
|
|
|
|
|
if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
|
|
|
|
|
$startOpts .= ' /*! STRAIGHT_JOIN */';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
|
|
|
|
|
$startOpts .= ' HIGH_PRIORITY';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
|
|
|
|
|
$startOpts .= ' SQL_BIG_RESULT';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
|
|
|
|
|
$startOpts .= ' SQL_BUFFER_RESULT';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
|
|
|
|
|
$startOpts .= ' SQL_SMALL_RESULT';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
|
|
|
|
|
$startOpts .= ' SQL_CALC_FOUND_ROWS';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
|
|
|
|
|
$startOpts .= ' SQL_CACHE';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
|
|
|
|
|
$startOpts .= ' SQL_NO_CACHE';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
|
|
|
|
|
$useIndex = $this->useIndexClause( $options['USE INDEX'] );
|
|
|
|
|
} else {
|
|
|
|
|
$useIndex = '';
|
|
|
|
|
}
|
|
|
|
|
if ( isset( $options['IGNORE INDEX'] ) && is_string( $options['IGNORE INDEX'] ) ) {
|
|
|
|
|
$ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
|
|
|
|
|
} else {
|
|
|
|
|
$ignoreIndex = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns an optional GROUP BY with an optional HAVING
|
|
|
|
|
*
|
|
|
|
|
* @param array $options Associative array of options
|
|
|
|
|
* @return string
|
2016-09-26 22:40:07 +00:00
|
|
|
* @see Database::select()
|
2016-09-16 03:14:58 +00:00
|
|
|
* @since 1.21
|
|
|
|
|
*/
|
2016-09-21 19:25:08 +00:00
|
|
|
protected function makeGroupByWithHaving( $options ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$sql = '';
|
|
|
|
|
if ( isset( $options['GROUP BY'] ) ) {
|
|
|
|
|
$gb = is_array( $options['GROUP BY'] )
|
|
|
|
|
? implode( ',', $options['GROUP BY'] )
|
|
|
|
|
: $options['GROUP BY'];
|
|
|
|
|
$sql .= ' GROUP BY ' . $gb;
|
|
|
|
|
}
|
|
|
|
|
if ( isset( $options['HAVING'] ) ) {
|
|
|
|
|
$having = is_array( $options['HAVING'] )
|
2016-09-19 07:28:17 +00:00
|
|
|
? $this->makeList( $options['HAVING'], self::LIST_AND )
|
2016-09-16 03:14:58 +00:00
|
|
|
: $options['HAVING'];
|
|
|
|
|
$sql .= ' HAVING ' . $having;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $sql;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns an optional ORDER BY
|
|
|
|
|
*
|
|
|
|
|
* @param array $options Associative array of options
|
|
|
|
|
* @return string
|
2016-09-26 22:40:07 +00:00
|
|
|
* @see Database::select()
|
2016-09-16 03:14:58 +00:00
|
|
|
* @since 1.21
|
|
|
|
|
*/
|
2016-09-21 19:25:08 +00:00
|
|
|
protected function makeOrderBy( $options ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( isset( $options['ORDER BY'] ) ) {
|
|
|
|
|
$ob = is_array( $options['ORDER BY'] )
|
|
|
|
|
? implode( ',', $options['ORDER BY'] )
|
|
|
|
|
: $options['ORDER BY'];
|
|
|
|
|
|
|
|
|
|
return ' ORDER BY ' . $ob;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-11 22:56:44 +00:00
|
|
|
public function select(
|
|
|
|
|
$table, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
|
|
|
|
|
) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
|
|
|
|
|
|
|
|
|
|
return $this->query( $sql, $fname );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
|
|
|
|
|
$options = [], $join_conds = []
|
|
|
|
|
) {
|
|
|
|
|
if ( is_array( $vars ) ) {
|
2018-04-11 22:56:44 +00:00
|
|
|
$fields = implode( ',', $this->fieldNamesWithAlias( $vars ) );
|
|
|
|
|
} else {
|
|
|
|
|
$fields = $vars;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$options = (array)$options;
|
|
|
|
|
$useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
|
|
|
|
|
? $options['USE INDEX']
|
|
|
|
|
: [];
|
2016-09-22 21:02:53 +00:00
|
|
|
$ignoreIndexes = (
|
|
|
|
|
isset( $options['IGNORE INDEX'] ) &&
|
|
|
|
|
is_array( $options['IGNORE INDEX'] )
|
|
|
|
|
)
|
2016-09-16 03:14:58 +00:00
|
|
|
? $options['IGNORE INDEX']
|
|
|
|
|
: [];
|
|
|
|
|
|
2018-04-11 22:56:44 +00:00
|
|
|
if (
|
|
|
|
|
$this->selectOptionsIncludeLocking( $options ) &&
|
|
|
|
|
$this->selectFieldsOrOptionsAggregate( $vars, $options )
|
|
|
|
|
) {
|
|
|
|
|
// Some DB types (postgres/oracle) disallow FOR UPDATE with aggregate
|
|
|
|
|
// functions. Discourage use of such queries to encourage compatibility.
|
|
|
|
|
call_user_func(
|
|
|
|
|
$this->deprecationLogger,
|
|
|
|
|
__METHOD__ . ": aggregation used with a locking SELECT ($fname)."
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( is_array( $table ) ) {
|
|
|
|
|
$from = ' FROM ' .
|
2016-09-22 21:02:53 +00:00
|
|
|
$this->tableNamesWithIndexClauseOrJOIN(
|
|
|
|
|
$table, $useIndexes, $ignoreIndexes, $join_conds );
|
2016-09-16 03:14:58 +00:00
|
|
|
} elseif ( $table != '' ) {
|
2018-02-15 10:30:12 +00:00
|
|
|
$from = ' FROM ' .
|
|
|
|
|
$this->tableNamesWithIndexClauseOrJOIN(
|
|
|
|
|
[ $table ], $useIndexes, $ignoreIndexes, [] );
|
2016-09-16 03:14:58 +00:00
|
|
|
} else {
|
|
|
|
|
$from = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
|
|
|
|
|
$this->makeSelectOptions( $options );
|
|
|
|
|
|
2018-02-27 18:08:47 +00:00
|
|
|
if ( is_array( $conds ) ) {
|
|
|
|
|
$conds = $this->makeList( $conds, self::LIST_AND );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $conds === null || $conds === false ) {
|
|
|
|
|
$this->queryLogger->warning(
|
|
|
|
|
__METHOD__
|
|
|
|
|
. ' called from '
|
|
|
|
|
. $fname
|
|
|
|
|
. ' with incorrect parameters: $conds must be a string or an array'
|
|
|
|
|
);
|
|
|
|
|
$conds = '';
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-01 21:31:53 +00:00
|
|
|
if ( $conds === '' || $conds === '*' ) {
|
2018-04-11 22:56:44 +00:00
|
|
|
$sql = "SELECT $startOpts $fields $from $useIndex $ignoreIndex $preLimitTail";
|
2018-02-27 18:08:47 +00:00
|
|
|
} elseif ( is_string( $conds ) ) {
|
2018-04-11 22:56:44 +00:00
|
|
|
$sql = "SELECT $startOpts $fields $from $useIndex $ignoreIndex " .
|
2016-09-22 21:02:53 +00:00
|
|
|
"WHERE $conds $preLimitTail";
|
2016-09-16 03:14:58 +00:00
|
|
|
} else {
|
2018-02-27 18:08:47 +00:00
|
|
|
throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $options['LIMIT'] ) ) {
|
|
|
|
|
$sql = $this->limitResult( $sql, $options['LIMIT'],
|
2017-10-06 22:17:58 +00:00
|
|
|
$options['OFFSET'] ?? false );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
$sql = "$sql $postLimitTail";
|
|
|
|
|
|
|
|
|
|
if ( isset( $options['EXPLAIN'] ) ) {
|
|
|
|
|
$sql = 'EXPLAIN ' . $sql;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $sql;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
|
|
|
|
|
$options = [], $join_conds = []
|
|
|
|
|
) {
|
|
|
|
|
$options = (array)$options;
|
|
|
|
|
$options['LIMIT'] = 1;
|
|
|
|
|
$res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
|
|
|
|
|
|
|
|
|
|
if ( $res === false ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !$this->numRows( $res ) ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$obj = $this->fetchObject( $res );
|
|
|
|
|
|
|
|
|
|
return $obj;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function estimateRowCount(
|
2018-02-15 03:46:04 +00:00
|
|
|
$table, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
|
2016-09-16 03:14:58 +00:00
|
|
|
) {
|
2018-02-15 03:46:04 +00:00
|
|
|
$conds = $this->normalizeConditions( $conds, $fname );
|
|
|
|
|
$column = $this->extractSingleFieldFromList( $var );
|
|
|
|
|
if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
|
|
|
|
|
$conds[] = "$column IS NOT NULL";
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-12 16:15:14 +00:00
|
|
|
$res = $this->select(
|
|
|
|
|
$table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options, $join_conds
|
|
|
|
|
);
|
2018-03-15 21:29:29 +00:00
|
|
|
$row = $res ? $this->fetchRow( $res ) : [];
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2018-03-15 21:29:29 +00:00
|
|
|
return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function selectRowCount(
|
2018-02-15 03:46:04 +00:00
|
|
|
$tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
|
2016-09-16 03:14:58 +00:00
|
|
|
) {
|
2018-02-15 03:46:04 +00:00
|
|
|
$conds = $this->normalizeConditions( $conds, $fname );
|
|
|
|
|
$column = $this->extractSingleFieldFromList( $var );
|
|
|
|
|
if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
|
|
|
|
|
$conds[] = "$column IS NOT NULL";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$res = $this->select(
|
|
|
|
|
[
|
|
|
|
|
'tmp_count' => $this->buildSelectSubquery(
|
|
|
|
|
$tables,
|
|
|
|
|
'1',
|
|
|
|
|
$conds,
|
|
|
|
|
$fname,
|
|
|
|
|
$options,
|
|
|
|
|
$join_conds
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
[ 'rowcount' => 'COUNT(*)' ],
|
|
|
|
|
[],
|
|
|
|
|
$fname
|
|
|
|
|
);
|
|
|
|
|
$row = $res ? $this->fetchRow( $res ) : [];
|
|
|
|
|
|
|
|
|
|
return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-11 22:56:44 +00:00
|
|
|
/**
|
|
|
|
|
* @param string|array $options
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
private function selectOptionsIncludeLocking( $options ) {
|
|
|
|
|
$options = (array)$options;
|
|
|
|
|
foreach ( [ 'FOR UPDATE', 'LOCK IN SHARE MODE' ] as $lock ) {
|
|
|
|
|
if ( in_array( $lock, $options, true ) ) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param array|string $fields
|
|
|
|
|
* @param array|string $options
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
private function selectFieldsOrOptionsAggregate( $fields, $options ) {
|
|
|
|
|
foreach ( (array)$options as $key => $value ) {
|
|
|
|
|
if ( is_string( $key ) ) {
|
|
|
|
|
if ( preg_match( '/^(?:GROUP BY|HAVING)$/i', $key ) ) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
} elseif ( is_string( $value ) ) {
|
|
|
|
|
if ( preg_match( '/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$regex = '/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
|
|
|
|
|
foreach ( (array)$fields as $field ) {
|
|
|
|
|
if ( is_string( $field ) && preg_match( $regex, $field ) ) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-15 03:46:04 +00:00
|
|
|
/**
|
|
|
|
|
* @param array|string $conds
|
|
|
|
|
* @param string $fname
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
final protected function normalizeConditions( $conds, $fname ) {
|
|
|
|
|
if ( $conds === null || $conds === false ) {
|
|
|
|
|
$this->queryLogger->warning(
|
|
|
|
|
__METHOD__
|
|
|
|
|
. ' called from '
|
|
|
|
|
. $fname
|
|
|
|
|
. ' with incorrect parameters: $conds must be a string or an array'
|
|
|
|
|
);
|
|
|
|
|
$conds = '';
|
|
|
|
|
}
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2018-02-15 03:46:04 +00:00
|
|
|
if ( !is_array( $conds ) ) {
|
|
|
|
|
$conds = ( $conds === '' ) ? [] : [ $conds ];
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-02-15 03:46:04 +00:00
|
|
|
return $conds;
|
|
|
|
|
}
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2018-02-15 03:46:04 +00:00
|
|
|
/**
|
|
|
|
|
* @param array|string $var Field parameter in the style of select()
|
|
|
|
|
* @return string|null Column name or null; ignores aliases
|
|
|
|
|
* @throws DBUnexpectedError Errors out if multiple columns are given
|
|
|
|
|
*/
|
|
|
|
|
final protected function extractSingleFieldFromList( $var ) {
|
|
|
|
|
if ( is_array( $var ) ) {
|
|
|
|
|
if ( !$var ) {
|
|
|
|
|
$column = null;
|
|
|
|
|
} elseif ( count( $var ) == 1 ) {
|
2017-10-06 22:17:58 +00:00
|
|
|
$column = $var[0] ?? reset( $var );
|
2018-02-15 03:46:04 +00:00
|
|
|
} else {
|
|
|
|
|
throw new DBUnexpectedError( $this, __METHOD__ . ': got multiple columns.' );
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$column = $var;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-02-15 03:46:04 +00:00
|
|
|
return $column;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-07-02 18:02:54 +00:00
|
|
|
public function lockForUpdate(
|
|
|
|
|
$table, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
|
|
|
|
|
) {
|
|
|
|
|
if ( !$this->trxLevel && !$this->getFlag( self::DBO_TRX ) ) {
|
|
|
|
|
throw new DBUnexpectedError(
|
|
|
|
|
$this,
|
|
|
|
|
__METHOD__ . ': no transaction is active nor is DBO_TRX set'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$options = (array)$options;
|
|
|
|
|
$options[] = 'FOR UPDATE';
|
|
|
|
|
|
|
|
|
|
return $this->selectRowCount( $table, '*', $conds, $fname, $options, $join_conds );
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
|
|
|
|
* Removes most variables from an SQL query and replaces them with X or N for numbers.
|
|
|
|
|
* It's only slightly flawed. Don't use for anything important.
|
|
|
|
|
*
|
|
|
|
|
* @param string $sql A SQL Query
|
|
|
|
|
*
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
protected static function generalizeSQL( $sql ) {
|
|
|
|
|
# This does the same as the regexp below would do, but in such a way
|
|
|
|
|
# as to avoid crashing php on some large strings.
|
|
|
|
|
# $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
|
|
|
|
|
|
|
|
|
|
$sql = str_replace( "\\\\", '', $sql );
|
|
|
|
|
$sql = str_replace( "\\'", '', $sql );
|
|
|
|
|
$sql = str_replace( "\\\"", '', $sql );
|
|
|
|
|
$sql = preg_replace( "/'.*'/s", "'X'", $sql );
|
|
|
|
|
$sql = preg_replace( '/".*"/s', "'X'", $sql );
|
|
|
|
|
|
|
|
|
|
# All newlines, tabs, etc replaced by single space
|
|
|
|
|
$sql = preg_replace( '/\s+/', ' ', $sql );
|
|
|
|
|
|
|
|
|
|
# All numbers => N,
|
|
|
|
|
# except the ones surrounded by characters, e.g. l10n
|
|
|
|
|
$sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
|
|
|
|
|
$sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
|
|
|
|
|
|
|
|
|
|
return $sql;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function fieldExists( $table, $field, $fname = __METHOD__ ) {
|
|
|
|
|
$info = $this->fieldInfo( $table, $field );
|
|
|
|
|
|
|
|
|
|
return (bool)$info;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function indexExists( $table, $index, $fname = __METHOD__ ) {
|
|
|
|
|
if ( !$this->tableExists( $table ) ) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$info = $this->indexInfo( $table, $index, $fname );
|
|
|
|
|
if ( is_null( $info ) ) {
|
|
|
|
|
return null;
|
|
|
|
|
} else {
|
|
|
|
|
return $info !== false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-06 21:38:47 +00:00
|
|
|
abstract public function tableExists( $table, $fname = __METHOD__ );
|
2016-09-16 03:14:58 +00:00
|
|
|
|
|
|
|
|
public function indexUnique( $table, $index ) {
|
|
|
|
|
$indexInfo = $this->indexInfo( $table, $index );
|
|
|
|
|
|
|
|
|
|
if ( !$indexInfo ) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return !$indexInfo[0]->Non_unique;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2016-09-26 22:40:07 +00:00
|
|
|
* Helper for Database::insert().
|
2016-09-16 03:14:58 +00:00
|
|
|
*
|
|
|
|
|
* @param array $options
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
protected function makeInsertOptions( $options ) {
|
|
|
|
|
return implode( ' ', $options );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
|
|
|
|
|
# No rows to insert, easy just return now
|
|
|
|
|
if ( !count( $a ) ) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$table = $this->tableName( $table );
|
|
|
|
|
|
|
|
|
|
if ( !is_array( $options ) ) {
|
|
|
|
|
$options = [ $options ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$options = $this->makeInsertOptions( $options );
|
|
|
|
|
|
|
|
|
|
if ( isset( $a[0] ) && is_array( $a[0] ) ) {
|
|
|
|
|
$multi = true;
|
|
|
|
|
$keys = array_keys( $a[0] );
|
|
|
|
|
} else {
|
|
|
|
|
$multi = false;
|
|
|
|
|
$keys = array_keys( $a );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$sql = 'INSERT ' . $options .
|
|
|
|
|
" INTO $table (" . implode( ',', $keys ) . ') VALUES ';
|
|
|
|
|
|
|
|
|
|
if ( $multi ) {
|
|
|
|
|
$first = true;
|
|
|
|
|
foreach ( $a as $row ) {
|
|
|
|
|
if ( $first ) {
|
|
|
|
|
$first = false;
|
|
|
|
|
} else {
|
|
|
|
|
$sql .= ',';
|
|
|
|
|
}
|
|
|
|
|
$sql .= '(' . $this->makeList( $row ) . ')';
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$sql .= '(' . $this->makeList( $a ) . ')';
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-26 20:17:34 +00:00
|
|
|
$this->query( $sql, $fname );
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2018-10-26 20:17:34 +00:00
|
|
|
return true;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2016-09-26 22:40:07 +00:00
|
|
|
* Make UPDATE options array for Database::makeUpdateOptions
|
2016-09-16 03:14:58 +00:00
|
|
|
*
|
|
|
|
|
* @param array $options
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
protected function makeUpdateOptionsArray( $options ) {
|
|
|
|
|
if ( !is_array( $options ) ) {
|
|
|
|
|
$options = [ $options ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$opts = [];
|
|
|
|
|
|
|
|
|
|
if ( in_array( 'IGNORE', $options ) ) {
|
|
|
|
|
$opts[] = 'IGNORE';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $opts;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2016-09-26 22:40:07 +00:00
|
|
|
* Make UPDATE options for the Database::update function
|
2016-09-16 03:14:58 +00:00
|
|
|
*
|
2016-09-26 22:40:07 +00:00
|
|
|
* @param array $options The options passed to Database::update
|
2016-09-16 03:14:58 +00:00
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
protected function makeUpdateOptions( $options ) {
|
|
|
|
|
$opts = $this->makeUpdateOptionsArray( $options );
|
|
|
|
|
|
|
|
|
|
return implode( ' ', $opts );
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-21 19:25:08 +00:00
|
|
|
public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$table = $this->tableName( $table );
|
|
|
|
|
$opts = $this->makeUpdateOptions( $options );
|
2016-09-19 07:28:17 +00:00
|
|
|
$sql = "UPDATE $opts $table SET " . $this->makeList( $values, self::LIST_SET );
|
2016-09-16 03:14:58 +00:00
|
|
|
|
|
|
|
|
if ( $conds !== [] && $conds !== '*' ) {
|
2016-09-19 07:28:17 +00:00
|
|
|
$sql .= " WHERE " . $this->makeList( $conds, self::LIST_AND );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-10-26 20:17:34 +00:00
|
|
|
$this->query( $sql, $fname );
|
|
|
|
|
|
|
|
|
|
return true;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2016-09-19 07:28:17 +00:00
|
|
|
public function makeList( $a, $mode = self::LIST_COMMA ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( !is_array( $a ) ) {
|
|
|
|
|
throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$first = true;
|
|
|
|
|
$list = '';
|
|
|
|
|
|
|
|
|
|
foreach ( $a as $field => $value ) {
|
|
|
|
|
if ( !$first ) {
|
2016-09-19 07:28:17 +00:00
|
|
|
if ( $mode == self::LIST_AND ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$list .= ' AND ';
|
2016-09-19 07:28:17 +00:00
|
|
|
} elseif ( $mode == self::LIST_OR ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$list .= ' OR ';
|
|
|
|
|
} else {
|
|
|
|
|
$list .= ',';
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$first = false;
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-19 07:28:17 +00:00
|
|
|
if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$list .= "($value)";
|
2016-09-19 07:28:17 +00:00
|
|
|
} elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$list .= "$value";
|
2016-09-19 07:28:17 +00:00
|
|
|
} elseif (
|
|
|
|
|
( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
|
|
|
|
|
) {
|
2016-09-16 03:14:58 +00:00
|
|
|
// Remove null from array to be handled separately if found
|
|
|
|
|
$includeNull = false;
|
|
|
|
|
foreach ( array_keys( $value, null, true ) as $nullKey ) {
|
|
|
|
|
$includeNull = true;
|
|
|
|
|
unset( $value[$nullKey] );
|
|
|
|
|
}
|
|
|
|
|
if ( count( $value ) == 0 && !$includeNull ) {
|
2016-09-19 07:28:17 +00:00
|
|
|
throw new InvalidArgumentException(
|
|
|
|
|
__METHOD__ . ": empty input for field $field" );
|
2016-09-16 03:14:58 +00:00
|
|
|
} elseif ( count( $value ) == 0 ) {
|
|
|
|
|
// only check if $field is null
|
|
|
|
|
$list .= "$field IS NULL";
|
|
|
|
|
} else {
|
|
|
|
|
// IN clause contains at least one valid element
|
|
|
|
|
if ( $includeNull ) {
|
|
|
|
|
// Group subconditions to ensure correct precedence
|
|
|
|
|
$list .= '(';
|
|
|
|
|
}
|
|
|
|
|
if ( count( $value ) == 1 ) {
|
|
|
|
|
// Special-case single values, as IN isn't terribly efficient
|
|
|
|
|
// Don't necessarily assume the single key is 0; we don't
|
|
|
|
|
// enforce linear numeric ordering on other arrays here.
|
|
|
|
|
$value = array_values( $value )[0];
|
|
|
|
|
$list .= $field . " = " . $this->addQuotes( $value );
|
|
|
|
|
} else {
|
|
|
|
|
$list .= $field . " IN (" . $this->makeList( $value ) . ") ";
|
|
|
|
|
}
|
|
|
|
|
// if null present in array, append IS NULL
|
|
|
|
|
if ( $includeNull ) {
|
|
|
|
|
$list .= " OR $field IS NULL)";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} elseif ( $value === null ) {
|
2016-09-19 07:28:17 +00:00
|
|
|
if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$list .= "$field IS ";
|
2016-09-19 07:28:17 +00:00
|
|
|
} elseif ( $mode == self::LIST_SET ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$list .= "$field = ";
|
|
|
|
|
}
|
|
|
|
|
$list .= 'NULL';
|
|
|
|
|
} else {
|
2016-09-19 07:28:17 +00:00
|
|
|
if (
|
|
|
|
|
$mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
|
|
|
|
|
) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$list .= "$field = ";
|
|
|
|
|
}
|
2016-09-19 07:28:17 +00:00
|
|
|
$list .= $mode == self::LIST_NAMES ? $value : $this->addQuotes( $value );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $list;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
|
|
|
|
|
$conds = [];
|
|
|
|
|
|
|
|
|
|
foreach ( $data as $base => $sub ) {
|
|
|
|
|
if ( count( $sub ) ) {
|
|
|
|
|
$conds[] = $this->makeList(
|
|
|
|
|
[ $baseKey => $base, $subKey => array_keys( $sub ) ],
|
2016-09-19 07:28:17 +00:00
|
|
|
self::LIST_AND );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $conds ) {
|
2016-09-19 07:28:17 +00:00
|
|
|
return $this->makeList( $conds, self::LIST_OR );
|
2016-09-16 03:14:58 +00:00
|
|
|
} else {
|
|
|
|
|
// Nothing to search for...
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function aggregateValue( $valuedata, $valuename = 'value' ) {
|
|
|
|
|
return $valuename;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function bitNot( $field ) {
|
|
|
|
|
return "(~$field)";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function bitAnd( $fieldLeft, $fieldRight ) {
|
|
|
|
|
return "($fieldLeft & $fieldRight)";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function bitOr( $fieldLeft, $fieldRight ) {
|
|
|
|
|
return "($fieldLeft | $fieldRight)";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function buildConcat( $stringList ) {
|
|
|
|
|
return 'CONCAT(' . implode( ',', $stringList ) . ')';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function buildGroupConcatField(
|
|
|
|
|
$delim, $table, $field, $conds = '', $join_conds = []
|
|
|
|
|
) {
|
|
|
|
|
$fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
|
|
|
|
|
|
|
|
|
|
return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-04 13:23:39 +00:00
|
|
|
public function buildSubstring( $input, $startPosition, $length = null ) {
|
|
|
|
|
$this->assertBuildSubstringParams( $startPosition, $length );
|
|
|
|
|
$functionBody = "$input FROM $startPosition";
|
|
|
|
|
if ( $length !== null ) {
|
|
|
|
|
$functionBody .= " FOR $length";
|
|
|
|
|
}
|
|
|
|
|
return 'SUBSTRING(' . $functionBody . ')';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check type and bounds for parameters to self::buildSubstring()
|
|
|
|
|
*
|
|
|
|
|
* All supported databases have substring functions that behave the same for
|
|
|
|
|
* positive $startPosition and non-negative $length, but behaviors differ when
|
|
|
|
|
* given 0 or negative $startPosition or negative $length. The simplest
|
|
|
|
|
* solution to that is to just forbid those values.
|
|
|
|
|
*
|
|
|
|
|
* @param int $startPosition
|
|
|
|
|
* @param int|null $length
|
|
|
|
|
* @since 1.31
|
|
|
|
|
*/
|
|
|
|
|
protected function assertBuildSubstringParams( $startPosition, $length ) {
|
|
|
|
|
if ( !is_int( $startPosition ) || $startPosition <= 0 ) {
|
|
|
|
|
throw new InvalidArgumentException(
|
|
|
|
|
'$startPosition must be a positive integer'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if ( !( is_int( $length ) && $length >= 0 || $length === null ) ) {
|
|
|
|
|
throw new InvalidArgumentException(
|
|
|
|
|
'$length must be null or an integer greater than or equal to 0'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
public function buildStringCast( $field ) {
|
|
|
|
|
return $field;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-04 13:50:28 +00:00
|
|
|
public function buildIntegerCast( $field ) {
|
|
|
|
|
return 'CAST( ' . $field . ' AS INTEGER )';
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-15 03:46:04 +00:00
|
|
|
public function buildSelectSubquery(
|
|
|
|
|
$table, $vars, $conds = '', $fname = __METHOD__,
|
|
|
|
|
$options = [], $join_conds = []
|
|
|
|
|
) {
|
|
|
|
|
return new Subquery(
|
|
|
|
|
$this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-10 01:27:28 +00:00
|
|
|
public function databasesAreIndependent() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-14 23:44:41 +00:00
|
|
|
final public function selectDB( $db ) {
|
|
|
|
|
$this->selectDomain( new DatabaseDomain(
|
|
|
|
|
$db,
|
|
|
|
|
$this->currentDomain->getSchema(),
|
|
|
|
|
$this->currentDomain->getTablePrefix()
|
|
|
|
|
) );
|
2016-09-16 03:14:58 +00:00
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-14 23:44:41 +00:00
|
|
|
final public function selectDomain( $domain ) {
|
|
|
|
|
$this->doSelectDomain( DatabaseDomain::newFromId( $domain ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function doSelectDomain( DatabaseDomain $domain ) {
|
|
|
|
|
$this->currentDomain = $domain;
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
public function getDBname() {
|
2018-08-14 23:44:41 +00:00
|
|
|
return $this->currentDomain->getDatabase();
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getServer() {
|
2018-02-13 06:58:57 +00:00
|
|
|
return $this->server;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function tableName( $name, $format = 'quoted' ) {
|
2018-02-15 03:46:04 +00:00
|
|
|
if ( $name instanceof Subquery ) {
|
|
|
|
|
throw new DBUnexpectedError(
|
|
|
|
|
$this,
|
|
|
|
|
__METHOD__ . ': got Subquery instance when expecting a string.'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
# Skip the entire process when we have a string quoted on both ends.
|
|
|
|
|
# Note that we check the end so that we will still quote any use of
|
|
|
|
|
# use of `database`.table. But won't break things if someone wants
|
|
|
|
|
# to query a database table with a dot in the name.
|
|
|
|
|
if ( $this->isQuotedIdentifier( $name ) ) {
|
|
|
|
|
return $name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Lets test for any bits of text that should never show up in a table
|
|
|
|
|
# name. Basically anything like JOIN or ON which are actually part of
|
|
|
|
|
# SQL queries, but may end up inside of the table value to combine
|
|
|
|
|
# sql. Such as how the API is doing.
|
|
|
|
|
# Note that we use a whitespace test rather than a \b test to avoid
|
|
|
|
|
# any remote case where a word like on may be inside of a table name
|
|
|
|
|
# surrounded by symbols which may be considered word breaks.
|
|
|
|
|
if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
|
2018-02-15 03:46:04 +00:00
|
|
|
$this->queryLogger->warning(
|
|
|
|
|
__METHOD__ . ": use of subqueries is not supported this way.",
|
|
|
|
|
[ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
|
|
|
|
|
);
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
return $name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Split database and table into proper variables.
|
2017-06-20 23:29:56 +00:00
|
|
|
list( $database, $schema, $prefix, $table ) = $this->qualifiedTableComponents( $name );
|
|
|
|
|
|
|
|
|
|
# Quote $table and apply the prefix if not quoted.
|
|
|
|
|
# $tableName might be empty if this is called from Database::replaceVars()
|
|
|
|
|
$tableName = "{$prefix}{$table}";
|
|
|
|
|
if ( $format === 'quoted'
|
|
|
|
|
&& !$this->isQuotedIdentifier( $tableName )
|
|
|
|
|
&& $tableName !== ''
|
|
|
|
|
) {
|
|
|
|
|
$tableName = $this->addIdentifierQuotes( $tableName );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Quote $schema and $database and merge them with the table name if needed
|
|
|
|
|
$tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
|
|
|
|
|
$tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
|
|
|
|
|
|
|
|
|
|
return $tableName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2017-06-27 18:31:35 +00:00
|
|
|
* Get the table components needed for a query given the currently selected database
|
|
|
|
|
*
|
|
|
|
|
* @param string $name Table name in the form of db.schema.table, db.table, or table
|
|
|
|
|
* @return array (DB name or "" for default, schema name, table prefix, table name)
|
2017-06-20 23:29:56 +00:00
|
|
|
*/
|
|
|
|
|
protected function qualifiedTableComponents( $name ) {
|
|
|
|
|
# We reverse the explode so that database.table and table both output the correct table.
|
2016-09-16 03:14:58 +00:00
|
|
|
$dbDetails = explode( '.', $name, 3 );
|
|
|
|
|
if ( count( $dbDetails ) == 3 ) {
|
|
|
|
|
list( $database, $schema, $table ) = $dbDetails;
|
|
|
|
|
# We don't want any prefix added in this case
|
|
|
|
|
$prefix = '';
|
|
|
|
|
} elseif ( count( $dbDetails ) == 2 ) {
|
|
|
|
|
list( $database, $table ) = $dbDetails;
|
|
|
|
|
# We don't want any prefix added in this case
|
2016-10-25 18:56:41 +00:00
|
|
|
$prefix = '';
|
2016-09-16 03:14:58 +00:00
|
|
|
# In dbs that support it, $database may actually be the schema
|
|
|
|
|
# but that doesn't affect any of the functionality here
|
|
|
|
|
$schema = '';
|
|
|
|
|
} else {
|
|
|
|
|
list( $table ) = $dbDetails;
|
|
|
|
|
if ( isset( $this->tableAliases[$table] ) ) {
|
|
|
|
|
$database = $this->tableAliases[$table]['dbname'];
|
|
|
|
|
$schema = is_string( $this->tableAliases[$table]['schema'] )
|
|
|
|
|
? $this->tableAliases[$table]['schema']
|
2018-08-14 23:44:41 +00:00
|
|
|
: $this->relationSchemaQualifier();
|
2016-09-16 03:14:58 +00:00
|
|
|
$prefix = is_string( $this->tableAliases[$table]['prefix'] )
|
|
|
|
|
? $this->tableAliases[$table]['prefix']
|
2018-08-14 23:44:41 +00:00
|
|
|
: $this->tablePrefix();
|
2016-09-16 03:14:58 +00:00
|
|
|
} else {
|
|
|
|
|
$database = '';
|
2018-08-14 23:44:41 +00:00
|
|
|
$schema = $this->relationSchemaQualifier(); # Default schema
|
|
|
|
|
$prefix = $this->tablePrefix(); # Default prefix
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-20 23:29:56 +00:00
|
|
|
return [ $database, $schema, $prefix, $table ];
|
2016-10-25 18:56:41 +00:00
|
|
|
}
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2016-10-25 18:56:41 +00:00
|
|
|
/**
|
|
|
|
|
* @param string|null $namespace Database or schema
|
|
|
|
|
* @param string $relation Name of table, view, sequence, etc...
|
|
|
|
|
* @param string $format One of (raw, quoted)
|
|
|
|
|
* @return string Relation name with quoted and merged $namespace as needed
|
|
|
|
|
*/
|
|
|
|
|
private function prependDatabaseOrSchema( $namespace, $relation, $format ) {
|
|
|
|
|
if ( strlen( $namespace ) ) {
|
|
|
|
|
if ( $format === 'quoted' && !$this->isQuotedIdentifier( $namespace ) ) {
|
|
|
|
|
$namespace = $this->addIdentifierQuotes( $namespace );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
2016-10-25 18:56:41 +00:00
|
|
|
$relation = $namespace . '.' . $relation;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2016-10-25 18:56:41 +00:00
|
|
|
return $relation;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function tableNames() {
|
|
|
|
|
$inArray = func_get_args();
|
|
|
|
|
$retVal = [];
|
|
|
|
|
|
|
|
|
|
foreach ( $inArray as $name ) {
|
|
|
|
|
$retVal[$name] = $this->tableName( $name );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $retVal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function tableNamesN() {
|
|
|
|
|
$inArray = func_get_args();
|
|
|
|
|
$retVal = [];
|
|
|
|
|
|
|
|
|
|
foreach ( $inArray as $name ) {
|
|
|
|
|
$retVal[] = $this->tableName( $name );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $retVal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get an aliased table name
|
|
|
|
|
*
|
2018-02-15 03:46:04 +00:00
|
|
|
* This returns strings like "tableName AS newTableName" for aliased tables
|
|
|
|
|
* and "(SELECT * from tableA) newTablename" for subqueries (e.g. derived tables)
|
|
|
|
|
*
|
|
|
|
|
* @see Database::tableName()
|
|
|
|
|
* @param string|Subquery $table Table name or object with a 'sql' field
|
|
|
|
|
* @param string|bool $alias Table alias (optional)
|
2016-09-16 03:14:58 +00:00
|
|
|
* @return string SQL name for aliased table. Will not alias a table to its own name
|
|
|
|
|
*/
|
2018-02-15 03:46:04 +00:00
|
|
|
protected function tableNameWithAlias( $table, $alias = false ) {
|
|
|
|
|
if ( is_string( $table ) ) {
|
|
|
|
|
$quotedTable = $this->tableName( $table );
|
|
|
|
|
} elseif ( $table instanceof Subquery ) {
|
|
|
|
|
$quotedTable = (string)$table;
|
|
|
|
|
} else {
|
|
|
|
|
throw new InvalidArgumentException( "Table must be a string or Subquery." );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !strlen( $alias ) || $alias === $table ) {
|
|
|
|
|
if ( $table instanceof Subquery ) {
|
|
|
|
|
throw new InvalidArgumentException( "Subquery table missing alias." );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $quotedTable;
|
2016-09-16 03:14:58 +00:00
|
|
|
} else {
|
2018-02-15 03:46:04 +00:00
|
|
|
return $quotedTable . ' ' . $this->addIdentifierQuotes( $alias );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Gets an array of aliased table names
|
|
|
|
|
*
|
|
|
|
|
* @param array $tables [ [alias] => table ]
|
|
|
|
|
* @return string[] See tableNameWithAlias()
|
|
|
|
|
*/
|
2016-09-21 19:25:08 +00:00
|
|
|
protected function tableNamesWithAlias( $tables ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$retval = [];
|
|
|
|
|
foreach ( $tables as $alias => $table ) {
|
|
|
|
|
if ( is_numeric( $alias ) ) {
|
|
|
|
|
$alias = $table;
|
|
|
|
|
}
|
|
|
|
|
$retval[] = $this->tableNameWithAlias( $table, $alias );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $retval;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get an aliased field name
|
|
|
|
|
* e.g. fieldName AS newFieldName
|
|
|
|
|
*
|
|
|
|
|
* @param string $name Field name
|
|
|
|
|
* @param string|bool $alias Alias (optional)
|
|
|
|
|
* @return string SQL name for aliased field. Will not alias a field to its own name
|
|
|
|
|
*/
|
2016-09-21 19:25:08 +00:00
|
|
|
protected function fieldNameWithAlias( $name, $alias = false ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( !$alias || (string)$alias === (string)$name ) {
|
|
|
|
|
return $name;
|
|
|
|
|
} else {
|
|
|
|
|
return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Gets an array of aliased field names
|
|
|
|
|
*
|
|
|
|
|
* @param array $fields [ [alias] => field ]
|
|
|
|
|
* @return string[] See fieldNameWithAlias()
|
|
|
|
|
*/
|
2016-09-21 19:25:08 +00:00
|
|
|
protected function fieldNamesWithAlias( $fields ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$retval = [];
|
|
|
|
|
foreach ( $fields as $alias => $field ) {
|
|
|
|
|
if ( is_numeric( $alias ) ) {
|
|
|
|
|
$alias = $field;
|
|
|
|
|
}
|
|
|
|
|
$retval[] = $this->fieldNameWithAlias( $field, $alias );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $retval;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the aliased table name clause for a FROM clause
|
|
|
|
|
* which might have a JOIN and/or USE INDEX or IGNORE INDEX clause
|
|
|
|
|
*
|
|
|
|
|
* @param array $tables ( [alias] => table )
|
|
|
|
|
* @param array $use_index Same as for select()
|
|
|
|
|
* @param array $ignore_index Same as for select()
|
|
|
|
|
* @param array $join_conds Same as for select()
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
protected function tableNamesWithIndexClauseOrJOIN(
|
|
|
|
|
$tables, $use_index = [], $ignore_index = [], $join_conds = []
|
|
|
|
|
) {
|
|
|
|
|
$ret = [];
|
|
|
|
|
$retJOIN = [];
|
|
|
|
|
$use_index = (array)$use_index;
|
|
|
|
|
$ignore_index = (array)$ignore_index;
|
|
|
|
|
$join_conds = (array)$join_conds;
|
|
|
|
|
|
|
|
|
|
foreach ( $tables as $alias => $table ) {
|
|
|
|
|
if ( !is_string( $alias ) ) {
|
|
|
|
|
// No alias? Set it equal to the table name
|
|
|
|
|
$alias = $table;
|
|
|
|
|
}
|
Database: Support parenthesized JOINs
SQL supports parentheses for grouping in the FROM clause.[1] This is
useful when you want to left-join against a join of other tables.
For example, say you have tables 'a', 'b', and 'c'. You want all rows
from 'a', along with rows from 'b' + 'c' only where both of those
exist.
SELECT * FROM a LEFT JOIN b ON (a_b = b_id) JOIN c ON (b_c = c_id)
doesn't work, it'll only give you the rows where 'c' exists.
SELECT * FROM a LEFT JOIN b ON (a_b = b_id) LEFT JOIN c ON (b_c = c_id)
doesn't work either, it'll give you rows from 'b' without a
corresponding row in 'c'. What you need to do is
SELECT * FROM a LEFT JOIN (b JOIN c ON (b_c = c_id)) ON (a_b = b_id)
This patch implements this by extending the syntax for the $table
parameter to IDatabase::select(). When passing an array of tables, if a
value in the array is itself an array that is interpreted as a request
for a parenthesized join. To produce the example above, you'd do
something like
$db->select(
[ 'a', 'nest' => [ 'b', 'c' ] ],
'*',
[],
__METHOD__,
[],
[
'c' => [ 'JOIN', 'b_c = c_id ],
'nest' => [ 'LEFT JOIN', 'a_b = b_id' ],
]
);
[1]: In standards as far back as SQL-1992 (I couldn't find an earlier
version), and it seems to be supported by at least MySQL 5.6, MariaDB
10.1.28, PostgreSQL 9.3, PostgreSQL 10.0, Oracle 11g R2, SQLite 3.20.1,
and MSSQL 2014 (from local testing and sqlfiddle.com).
Change-Id: I1e0a77381e06d885650a94f53847fb82f01c2694
2017-10-13 17:51:43 +00:00
|
|
|
|
|
|
|
|
if ( is_array( $table ) ) {
|
|
|
|
|
// A parenthesized group
|
2017-11-29 20:42:27 +00:00
|
|
|
if ( count( $table ) > 1 ) {
|
2018-03-15 21:29:29 +00:00
|
|
|
$joinedTable = '(' .
|
|
|
|
|
$this->tableNamesWithIndexClauseOrJOIN(
|
|
|
|
|
$table, $use_index, $ignore_index, $join_conds ) . ')';
|
2017-11-29 20:42:27 +00:00
|
|
|
} else {
|
|
|
|
|
// Degenerate case
|
|
|
|
|
$innerTable = reset( $table );
|
|
|
|
|
$innerAlias = key( $table );
|
|
|
|
|
$joinedTable = $this->tableNameWithAlias(
|
|
|
|
|
$innerTable,
|
|
|
|
|
is_string( $innerAlias ) ? $innerAlias : $innerTable
|
|
|
|
|
);
|
|
|
|
|
}
|
Database: Support parenthesized JOINs
SQL supports parentheses for grouping in the FROM clause.[1] This is
useful when you want to left-join against a join of other tables.
For example, say you have tables 'a', 'b', and 'c'. You want all rows
from 'a', along with rows from 'b' + 'c' only where both of those
exist.
SELECT * FROM a LEFT JOIN b ON (a_b = b_id) JOIN c ON (b_c = c_id)
doesn't work, it'll only give you the rows where 'c' exists.
SELECT * FROM a LEFT JOIN b ON (a_b = b_id) LEFT JOIN c ON (b_c = c_id)
doesn't work either, it'll give you rows from 'b' without a
corresponding row in 'c'. What you need to do is
SELECT * FROM a LEFT JOIN (b JOIN c ON (b_c = c_id)) ON (a_b = b_id)
This patch implements this by extending the syntax for the $table
parameter to IDatabase::select(). When passing an array of tables, if a
value in the array is itself an array that is interpreted as a request
for a parenthesized join. To produce the example above, you'd do
something like
$db->select(
[ 'a', 'nest' => [ 'b', 'c' ] ],
'*',
[],
__METHOD__,
[],
[
'c' => [ 'JOIN', 'b_c = c_id ],
'nest' => [ 'LEFT JOIN', 'a_b = b_id' ],
]
);
[1]: In standards as far back as SQL-1992 (I couldn't find an earlier
version), and it seems to be supported by at least MySQL 5.6, MariaDB
10.1.28, PostgreSQL 9.3, PostgreSQL 10.0, Oracle 11g R2, SQLite 3.20.1,
and MSSQL 2014 (from local testing and sqlfiddle.com).
Change-Id: I1e0a77381e06d885650a94f53847fb82f01c2694
2017-10-13 17:51:43 +00:00
|
|
|
} else {
|
|
|
|
|
$joinedTable = $this->tableNameWithAlias( $table, $alias );
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
// Is there a JOIN clause for this table?
|
|
|
|
|
if ( isset( $join_conds[$alias] ) ) {
|
|
|
|
|
list( $joinType, $conds ) = $join_conds[$alias];
|
|
|
|
|
$tableClause = $joinType;
|
Database: Support parenthesized JOINs
SQL supports parentheses for grouping in the FROM clause.[1] This is
useful when you want to left-join against a join of other tables.
For example, say you have tables 'a', 'b', and 'c'. You want all rows
from 'a', along with rows from 'b' + 'c' only where both of those
exist.
SELECT * FROM a LEFT JOIN b ON (a_b = b_id) JOIN c ON (b_c = c_id)
doesn't work, it'll only give you the rows where 'c' exists.
SELECT * FROM a LEFT JOIN b ON (a_b = b_id) LEFT JOIN c ON (b_c = c_id)
doesn't work either, it'll give you rows from 'b' without a
corresponding row in 'c'. What you need to do is
SELECT * FROM a LEFT JOIN (b JOIN c ON (b_c = c_id)) ON (a_b = b_id)
This patch implements this by extending the syntax for the $table
parameter to IDatabase::select(). When passing an array of tables, if a
value in the array is itself an array that is interpreted as a request
for a parenthesized join. To produce the example above, you'd do
something like
$db->select(
[ 'a', 'nest' => [ 'b', 'c' ] ],
'*',
[],
__METHOD__,
[],
[
'c' => [ 'JOIN', 'b_c = c_id ],
'nest' => [ 'LEFT JOIN', 'a_b = b_id' ],
]
);
[1]: In standards as far back as SQL-1992 (I couldn't find an earlier
version), and it seems to be supported by at least MySQL 5.6, MariaDB
10.1.28, PostgreSQL 9.3, PostgreSQL 10.0, Oracle 11g R2, SQLite 3.20.1,
and MSSQL 2014 (from local testing and sqlfiddle.com).
Change-Id: I1e0a77381e06d885650a94f53847fb82f01c2694
2017-10-13 17:51:43 +00:00
|
|
|
$tableClause .= ' ' . $joinedTable;
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
|
|
|
|
|
$use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
|
|
|
|
|
if ( $use != '' ) {
|
|
|
|
|
$tableClause .= ' ' . $use;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
|
2016-09-22 21:02:53 +00:00
|
|
|
$ignore = $this->ignoreIndexClause(
|
|
|
|
|
implode( ',', (array)$ignore_index[$alias] ) );
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( $ignore != '' ) {
|
|
|
|
|
$tableClause .= ' ' . $ignore;
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-09-19 07:28:17 +00:00
|
|
|
$on = $this->makeList( (array)$conds, self::LIST_AND );
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( $on != '' ) {
|
|
|
|
|
$tableClause .= ' ON (' . $on . ')';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$retJOIN[] = $tableClause;
|
|
|
|
|
} elseif ( isset( $use_index[$alias] ) ) {
|
|
|
|
|
// Is there an INDEX clause for this table?
|
Database: Support parenthesized JOINs
SQL supports parentheses for grouping in the FROM clause.[1] This is
useful when you want to left-join against a join of other tables.
For example, say you have tables 'a', 'b', and 'c'. You want all rows
from 'a', along with rows from 'b' + 'c' only where both of those
exist.
SELECT * FROM a LEFT JOIN b ON (a_b = b_id) JOIN c ON (b_c = c_id)
doesn't work, it'll only give you the rows where 'c' exists.
SELECT * FROM a LEFT JOIN b ON (a_b = b_id) LEFT JOIN c ON (b_c = c_id)
doesn't work either, it'll give you rows from 'b' without a
corresponding row in 'c'. What you need to do is
SELECT * FROM a LEFT JOIN (b JOIN c ON (b_c = c_id)) ON (a_b = b_id)
This patch implements this by extending the syntax for the $table
parameter to IDatabase::select(). When passing an array of tables, if a
value in the array is itself an array that is interpreted as a request
for a parenthesized join. To produce the example above, you'd do
something like
$db->select(
[ 'a', 'nest' => [ 'b', 'c' ] ],
'*',
[],
__METHOD__,
[],
[
'c' => [ 'JOIN', 'b_c = c_id ],
'nest' => [ 'LEFT JOIN', 'a_b = b_id' ],
]
);
[1]: In standards as far back as SQL-1992 (I couldn't find an earlier
version), and it seems to be supported by at least MySQL 5.6, MariaDB
10.1.28, PostgreSQL 9.3, PostgreSQL 10.0, Oracle 11g R2, SQLite 3.20.1,
and MSSQL 2014 (from local testing and sqlfiddle.com).
Change-Id: I1e0a77381e06d885650a94f53847fb82f01c2694
2017-10-13 17:51:43 +00:00
|
|
|
$tableClause = $joinedTable;
|
2016-09-16 03:14:58 +00:00
|
|
|
$tableClause .= ' ' . $this->useIndexClause(
|
|
|
|
|
implode( ',', (array)$use_index[$alias] )
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$ret[] = $tableClause;
|
|
|
|
|
} elseif ( isset( $ignore_index[$alias] ) ) {
|
|
|
|
|
// Is there an INDEX clause for this table?
|
Database: Support parenthesized JOINs
SQL supports parentheses for grouping in the FROM clause.[1] This is
useful when you want to left-join against a join of other tables.
For example, say you have tables 'a', 'b', and 'c'. You want all rows
from 'a', along with rows from 'b' + 'c' only where both of those
exist.
SELECT * FROM a LEFT JOIN b ON (a_b = b_id) JOIN c ON (b_c = c_id)
doesn't work, it'll only give you the rows where 'c' exists.
SELECT * FROM a LEFT JOIN b ON (a_b = b_id) LEFT JOIN c ON (b_c = c_id)
doesn't work either, it'll give you rows from 'b' without a
corresponding row in 'c'. What you need to do is
SELECT * FROM a LEFT JOIN (b JOIN c ON (b_c = c_id)) ON (a_b = b_id)
This patch implements this by extending the syntax for the $table
parameter to IDatabase::select(). When passing an array of tables, if a
value in the array is itself an array that is interpreted as a request
for a parenthesized join. To produce the example above, you'd do
something like
$db->select(
[ 'a', 'nest' => [ 'b', 'c' ] ],
'*',
[],
__METHOD__,
[],
[
'c' => [ 'JOIN', 'b_c = c_id ],
'nest' => [ 'LEFT JOIN', 'a_b = b_id' ],
]
);
[1]: In standards as far back as SQL-1992 (I couldn't find an earlier
version), and it seems to be supported by at least MySQL 5.6, MariaDB
10.1.28, PostgreSQL 9.3, PostgreSQL 10.0, Oracle 11g R2, SQLite 3.20.1,
and MSSQL 2014 (from local testing and sqlfiddle.com).
Change-Id: I1e0a77381e06d885650a94f53847fb82f01c2694
2017-10-13 17:51:43 +00:00
|
|
|
$tableClause = $joinedTable;
|
2016-09-16 03:14:58 +00:00
|
|
|
$tableClause .= ' ' . $this->ignoreIndexClause(
|
|
|
|
|
implode( ',', (array)$ignore_index[$alias] )
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$ret[] = $tableClause;
|
|
|
|
|
} else {
|
Database: Support parenthesized JOINs
SQL supports parentheses for grouping in the FROM clause.[1] This is
useful when you want to left-join against a join of other tables.
For example, say you have tables 'a', 'b', and 'c'. You want all rows
from 'a', along with rows from 'b' + 'c' only where both of those
exist.
SELECT * FROM a LEFT JOIN b ON (a_b = b_id) JOIN c ON (b_c = c_id)
doesn't work, it'll only give you the rows where 'c' exists.
SELECT * FROM a LEFT JOIN b ON (a_b = b_id) LEFT JOIN c ON (b_c = c_id)
doesn't work either, it'll give you rows from 'b' without a
corresponding row in 'c'. What you need to do is
SELECT * FROM a LEFT JOIN (b JOIN c ON (b_c = c_id)) ON (a_b = b_id)
This patch implements this by extending the syntax for the $table
parameter to IDatabase::select(). When passing an array of tables, if a
value in the array is itself an array that is interpreted as a request
for a parenthesized join. To produce the example above, you'd do
something like
$db->select(
[ 'a', 'nest' => [ 'b', 'c' ] ],
'*',
[],
__METHOD__,
[],
[
'c' => [ 'JOIN', 'b_c = c_id ],
'nest' => [ 'LEFT JOIN', 'a_b = b_id' ],
]
);
[1]: In standards as far back as SQL-1992 (I couldn't find an earlier
version), and it seems to be supported by at least MySQL 5.6, MariaDB
10.1.28, PostgreSQL 9.3, PostgreSQL 10.0, Oracle 11g R2, SQLite 3.20.1,
and MSSQL 2014 (from local testing and sqlfiddle.com).
Change-Id: I1e0a77381e06d885650a94f53847fb82f01c2694
2017-10-13 17:51:43 +00:00
|
|
|
$tableClause = $joinedTable;
|
2016-09-16 03:14:58 +00:00
|
|
|
|
|
|
|
|
$ret[] = $tableClause;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We can't separate explicit JOIN clauses with ',', use ' ' for those
|
2018-03-02 04:30:07 +00:00
|
|
|
$implicitJoins = $ret ? implode( ',', $ret ) : "";
|
|
|
|
|
$explicitJoins = $retJOIN ? implode( ' ', $retJOIN ) : "";
|
2016-09-16 03:14:58 +00:00
|
|
|
|
|
|
|
|
// Compile our final table clause
|
|
|
|
|
return implode( ' ', [ $implicitJoins, $explicitJoins ] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2017-03-30 11:29:35 +00:00
|
|
|
* Allows for index remapping in queries where this is not consistent across DBMS
|
2016-09-16 03:14:58 +00:00
|
|
|
*
|
|
|
|
|
* @param string $index
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
protected function indexName( $index ) {
|
2017-10-06 22:17:58 +00:00
|
|
|
return $this->indexAliases[$index] ?? $index;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function addQuotes( $s ) {
|
|
|
|
|
if ( $s instanceof Blob ) {
|
|
|
|
|
$s = $s->fetch();
|
|
|
|
|
}
|
|
|
|
|
if ( $s === null ) {
|
|
|
|
|
return 'NULL';
|
2016-09-21 18:03:29 +00:00
|
|
|
} elseif ( is_bool( $s ) ) {
|
|
|
|
|
return (int)$s;
|
2016-09-16 03:14:58 +00:00
|
|
|
} else {
|
|
|
|
|
# This will also quote numeric values. This should be harmless,
|
|
|
|
|
# and protects against weird problems that occur when they really
|
|
|
|
|
# _are_ strings such as article titles and string->number->string
|
|
|
|
|
# conversion is not 1:1.
|
|
|
|
|
return "'" . $this->strencode( $s ) . "'";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Quotes an identifier using `backticks` or "double quotes" depending on the database type.
|
|
|
|
|
* MySQL uses `backticks` while basically everything else uses double quotes.
|
|
|
|
|
* Since MySQL is the odd one out here the double quotes are our generic
|
2016-12-30 15:13:02 +00:00
|
|
|
* and we implement backticks in DatabaseMysqlBase.
|
2016-09-16 03:14:58 +00:00
|
|
|
*
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
public function addIdentifierQuotes( $s ) {
|
|
|
|
|
return '"' . str_replace( '"', '""', $s ) . '"';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns if the given identifier looks quoted or not according to
|
|
|
|
|
* the database convention for quoting identifiers .
|
|
|
|
|
*
|
|
|
|
|
* @note Do not use this to determine if untrusted input is safe.
|
|
|
|
|
* A malicious user can trick this function.
|
|
|
|
|
* @param string $name
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public function isQuotedIdentifier( $name ) {
|
|
|
|
|
return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param string $s
|
2017-09-09 20:47:04 +00:00
|
|
|
* @param string $escapeChar
|
2016-09-16 03:14:58 +00:00
|
|
|
* @return string
|
|
|
|
|
*/
|
New maintenance script to clean up rows with invalid DB keys
The TitleValue constructor, used by the link cache among other things,
throws an exception for DB keys which do not satisfy a simple sanity test
(starting or ending with _, or containing a space, tab, CR or LF
character). This has broken certain special pages on a number of WMF sites;
see T99736, T146778 and T155091.
The new cleanupInvalidDbKeys.php script allows these bogus entries to be
removed from the DB, making sure these exceptions won't be thrown in the
future. It cleans the title columns of the page, archive, redirect,
logging, category, protected_titles, recentchanges, watchlist, pagelinks,
templatelinks, and categorylinks tables.
The script doesn't support batching; most wikis should have fewer than 500
broken entries in each table. If need be, the script can be run several
times.
To make the LIKE queries work properly I had to fix the broken escaping
behaviour of Database::buildLike() -- previously it had a habit of double-
escaping things. Now an ESCAPE clause is added to change the escape
character from the problematic default backslash, and tests are added to
cover the changes.
Bug: T155091
Change-Id: I908e795e884e35be91852c0eaf056d6acfda31d8
2017-03-10 13:27:27 +00:00
|
|
|
protected function escapeLikeInternal( $s, $escapeChar = '`' ) {
|
|
|
|
|
return str_replace( [ $escapeChar, '%', '_' ],
|
|
|
|
|
[ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ],
|
|
|
|
|
$s );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function buildLike() {
|
|
|
|
|
$params = func_get_args();
|
|
|
|
|
|
|
|
|
|
if ( count( $params ) > 0 && is_array( $params[0] ) ) {
|
|
|
|
|
$params = $params[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$s = '';
|
|
|
|
|
|
New maintenance script to clean up rows with invalid DB keys
The TitleValue constructor, used by the link cache among other things,
throws an exception for DB keys which do not satisfy a simple sanity test
(starting or ending with _, or containing a space, tab, CR or LF
character). This has broken certain special pages on a number of WMF sites;
see T99736, T146778 and T155091.
The new cleanupInvalidDbKeys.php script allows these bogus entries to be
removed from the DB, making sure these exceptions won't be thrown in the
future. It cleans the title columns of the page, archive, redirect,
logging, category, protected_titles, recentchanges, watchlist, pagelinks,
templatelinks, and categorylinks tables.
The script doesn't support batching; most wikis should have fewer than 500
broken entries in each table. If need be, the script can be run several
times.
To make the LIKE queries work properly I had to fix the broken escaping
behaviour of Database::buildLike() -- previously it had a habit of double-
escaping things. Now an ESCAPE clause is added to change the escape
character from the problematic default backslash, and tests are added to
cover the changes.
Bug: T155091
Change-Id: I908e795e884e35be91852c0eaf056d6acfda31d8
2017-03-10 13:27:27 +00:00
|
|
|
// We use ` instead of \ as the default LIKE escape character, since addQuotes()
|
|
|
|
|
// may escape backslashes, creating problems of double escaping. The `
|
|
|
|
|
// character has good cross-DBMS compatibility, avoiding special operators
|
|
|
|
|
// in MS SQL like ^ and %
|
|
|
|
|
$escapeChar = '`';
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
foreach ( $params as $value ) {
|
|
|
|
|
if ( $value instanceof LikeMatch ) {
|
|
|
|
|
$s .= $value->toString();
|
|
|
|
|
} else {
|
New maintenance script to clean up rows with invalid DB keys
The TitleValue constructor, used by the link cache among other things,
throws an exception for DB keys which do not satisfy a simple sanity test
(starting or ending with _, or containing a space, tab, CR or LF
character). This has broken certain special pages on a number of WMF sites;
see T99736, T146778 and T155091.
The new cleanupInvalidDbKeys.php script allows these bogus entries to be
removed from the DB, making sure these exceptions won't be thrown in the
future. It cleans the title columns of the page, archive, redirect,
logging, category, protected_titles, recentchanges, watchlist, pagelinks,
templatelinks, and categorylinks tables.
The script doesn't support batching; most wikis should have fewer than 500
broken entries in each table. If need be, the script can be run several
times.
To make the LIKE queries work properly I had to fix the broken escaping
behaviour of Database::buildLike() -- previously it had a habit of double-
escaping things. Now an ESCAPE clause is added to change the escape
character from the problematic default backslash, and tests are added to
cover the changes.
Bug: T155091
Change-Id: I908e795e884e35be91852c0eaf056d6acfda31d8
2017-03-10 13:27:27 +00:00
|
|
|
$s .= $this->escapeLikeInternal( $value, $escapeChar );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-15 21:29:29 +00:00
|
|
|
return ' LIKE ' .
|
|
|
|
|
$this->addQuotes( $s ) . ' ESCAPE ' . $this->addQuotes( $escapeChar ) . ' ';
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function anyChar() {
|
|
|
|
|
return new LikeMatch( '_' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function anyString() {
|
|
|
|
|
return new LikeMatch( '%' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function nextSequenceValue( $seqName ) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* USE INDEX clause. Unlikely to be useful for anything but MySQL. This
|
|
|
|
|
* is only needed because a) MySQL must be as efficient as possible due to
|
|
|
|
|
* its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
|
|
|
|
|
* which index to pick. Anyway, other databases might have different
|
|
|
|
|
* indexes on a given table. So don't bother overriding this unless you're
|
|
|
|
|
* MySQL.
|
|
|
|
|
* @param string $index
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
public function useIndexClause( $index ) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* IGNORE INDEX clause. Unlikely to be useful for anything but MySQL. This
|
|
|
|
|
* is only needed because a) MySQL must be as efficient as possible due to
|
|
|
|
|
* its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
|
|
|
|
|
* which index to pick. Anyway, other databases might have different
|
|
|
|
|
* indexes on a given table. So don't bother overriding this unless you're
|
|
|
|
|
* MySQL.
|
|
|
|
|
* @param string $index
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
public function ignoreIndexClause( $index ) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
|
|
|
|
|
if ( count( $rows ) == 0 ) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-14 03:57:51 +00:00
|
|
|
// Single row case
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( !is_array( reset( $rows ) ) ) {
|
|
|
|
|
$rows = [ $rows ];
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-21 03:16:51 +00:00
|
|
|
try {
|
2018-03-20 15:57:04 +00:00
|
|
|
$this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
|
2018-02-21 03:16:51 +00:00
|
|
|
$affectedRowCount = 0;
|
|
|
|
|
foreach ( $rows as $row ) {
|
|
|
|
|
// Delete rows which collide with this one
|
|
|
|
|
$indexWhereClauses = [];
|
|
|
|
|
foreach ( $uniqueIndexes as $index ) {
|
|
|
|
|
$indexColumns = (array)$index;
|
|
|
|
|
$indexRowValues = array_intersect_key( $row, array_flip( $indexColumns ) );
|
|
|
|
|
if ( count( $indexRowValues ) != count( $indexColumns ) ) {
|
|
|
|
|
throw new DBUnexpectedError(
|
|
|
|
|
$this,
|
|
|
|
|
'New record does not provide all values for unique key (' .
|
2018-01-14 03:57:51 +00:00
|
|
|
implode( ', ', $indexColumns ) . ')'
|
2018-02-21 03:16:51 +00:00
|
|
|
);
|
|
|
|
|
} elseif ( in_array( null, $indexRowValues, true ) ) {
|
|
|
|
|
throw new DBUnexpectedError(
|
|
|
|
|
$this,
|
|
|
|
|
'New record has a null value for unique key (' .
|
2018-01-14 03:57:51 +00:00
|
|
|
implode( ', ', $indexColumns ) . ')'
|
2018-02-21 03:16:51 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
$indexWhereClauses[] = $this->makeList( $indexRowValues, LIST_AND );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
2018-01-14 03:57:51 +00:00
|
|
|
|
2018-02-21 03:16:51 +00:00
|
|
|
if ( $indexWhereClauses ) {
|
|
|
|
|
$this->delete( $table, $this->makeList( $indexWhereClauses, LIST_OR ), $fname );
|
|
|
|
|
$affectedRowCount += $this->affectedRows();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now insert the row
|
|
|
|
|
$this->insert( $table, $row, $fname );
|
2018-01-28 14:10:39 +00:00
|
|
|
$affectedRowCount += $this->affectedRows();
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
2018-02-28 22:41:18 +00:00
|
|
|
$this->endAtomic( $fname );
|
|
|
|
|
$this->affectedRowCount = $affectedRowCount;
|
2018-02-21 03:16:51 +00:00
|
|
|
} catch ( Exception $e ) {
|
2018-03-17 21:59:56 +00:00
|
|
|
$this->cancelAtomic( $fname );
|
2018-02-21 03:16:51 +00:00
|
|
|
throw $e;
|
|
|
|
|
}
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE
|
|
|
|
|
* statement.
|
|
|
|
|
*
|
|
|
|
|
* @param string $table Table name
|
|
|
|
|
* @param array|string $rows Row(s) to insert
|
|
|
|
|
* @param string $fname Caller function name
|
|
|
|
|
*/
|
|
|
|
|
protected function nativeReplace( $table, $rows, $fname ) {
|
|
|
|
|
$table = $this->tableName( $table );
|
|
|
|
|
|
|
|
|
|
# Single row case
|
|
|
|
|
if ( !is_array( reset( $rows ) ) ) {
|
|
|
|
|
$rows = [ $rows ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
|
|
|
|
|
$first = true;
|
|
|
|
|
|
|
|
|
|
foreach ( $rows as $row ) {
|
|
|
|
|
if ( $first ) {
|
|
|
|
|
$first = false;
|
|
|
|
|
} else {
|
|
|
|
|
$sql .= ',';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$sql .= '(' . $this->makeList( $row ) . ')';
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-26 20:17:34 +00:00
|
|
|
$this->query( $sql, $fname );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
|
|
|
|
|
$fname = __METHOD__
|
|
|
|
|
) {
|
|
|
|
|
if ( !count( $rows ) ) {
|
|
|
|
|
return true; // nothing to do
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !is_array( reset( $rows ) ) ) {
|
|
|
|
|
$rows = [ $rows ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( count( $uniqueIndexes ) ) {
|
|
|
|
|
$clauses = []; // list WHERE clauses that each identify a single row
|
|
|
|
|
foreach ( $rows as $row ) {
|
|
|
|
|
foreach ( $uniqueIndexes as $index ) {
|
|
|
|
|
$index = is_array( $index ) ? $index : [ $index ]; // columns
|
|
|
|
|
$rowKey = []; // unique key to this row
|
|
|
|
|
foreach ( $index as $column ) {
|
|
|
|
|
$rowKey[$column] = $row[$column];
|
|
|
|
|
}
|
2016-09-19 07:28:17 +00:00
|
|
|
$clauses[] = $this->makeList( $rowKey, self::LIST_AND );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
2016-09-19 07:28:17 +00:00
|
|
|
$where = [ $this->makeList( $clauses, self::LIST_OR ) ];
|
2016-09-16 03:14:58 +00:00
|
|
|
} else {
|
|
|
|
|
$where = false;
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-28 14:10:39 +00:00
|
|
|
$affectedRowCount = 0;
|
2016-09-16 03:14:58 +00:00
|
|
|
try {
|
2018-03-20 15:57:04 +00:00
|
|
|
$this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
|
2016-09-16 03:14:58 +00:00
|
|
|
# Update any existing conflicting row(s)
|
|
|
|
|
if ( $where !== false ) {
|
2018-10-26 20:17:34 +00:00
|
|
|
$this->update( $table, $set, $where, $fname );
|
2018-01-28 14:10:39 +00:00
|
|
|
$affectedRowCount += $this->affectedRows();
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
# Now insert any non-conflicting row(s)
|
2018-10-26 20:17:34 +00:00
|
|
|
$this->insert( $table, $rows, $fname, [ 'IGNORE' ] );
|
2018-01-28 14:10:39 +00:00
|
|
|
$affectedRowCount += $this->affectedRows();
|
2018-02-28 22:41:18 +00:00
|
|
|
$this->endAtomic( $fname );
|
|
|
|
|
$this->affectedRowCount = $affectedRowCount;
|
2016-09-16 03:14:58 +00:00
|
|
|
} catch ( Exception $e ) {
|
2018-03-17 21:59:56 +00:00
|
|
|
$this->cancelAtomic( $fname );
|
2016-09-16 03:14:58 +00:00
|
|
|
throw $e;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-26 20:17:34 +00:00
|
|
|
return true;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
|
|
|
|
|
$fname = __METHOD__
|
|
|
|
|
) {
|
|
|
|
|
if ( !$conds ) {
|
|
|
|
|
throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$delTable = $this->tableName( $delTable );
|
|
|
|
|
$joinTable = $this->tableName( $joinTable );
|
|
|
|
|
$sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
|
|
|
|
|
if ( $conds != '*' ) {
|
2016-09-19 07:28:17 +00:00
|
|
|
$sql .= 'WHERE ' . $this->makeList( $conds, self::LIST_AND );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
$sql .= ')';
|
|
|
|
|
|
|
|
|
|
$this->query( $sql, $fname );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function textFieldSize( $table, $field ) {
|
|
|
|
|
$table = $this->tableName( $table );
|
|
|
|
|
$sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
|
|
|
|
|
$res = $this->query( $sql, __METHOD__ );
|
|
|
|
|
$row = $this->fetchObject( $res );
|
|
|
|
|
|
|
|
|
|
$m = [];
|
|
|
|
|
|
|
|
|
|
if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
|
|
|
|
|
$size = $m[1];
|
|
|
|
|
} else {
|
|
|
|
|
$size = -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function delete( $table, $conds, $fname = __METHOD__ ) {
|
|
|
|
|
if ( !$conds ) {
|
|
|
|
|
throw new DBUnexpectedError( $this, __METHOD__ . ' called with no conditions' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$table = $this->tableName( $table );
|
|
|
|
|
$sql = "DELETE FROM $table";
|
|
|
|
|
|
|
|
|
|
if ( $conds != '*' ) {
|
|
|
|
|
if ( is_array( $conds ) ) {
|
2016-09-19 07:28:17 +00:00
|
|
|
$conds = $this->makeList( $conds, self::LIST_AND );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
$sql .= ' WHERE ' . $conds;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-26 20:17:34 +00:00
|
|
|
$this->query( $sql, $fname );
|
|
|
|
|
|
|
|
|
|
return true;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-02-28 23:33:03 +00:00
|
|
|
final public function insertSelect(
|
2016-09-16 03:14:58 +00:00
|
|
|
$destTable, $srcTable, $varMap, $conds,
|
2017-06-09 16:58:09 +00:00
|
|
|
$fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = []
|
2016-09-16 03:14:58 +00:00
|
|
|
) {
|
2018-02-28 23:33:03 +00:00
|
|
|
static $hints = [ 'NO_AUTO_COLUMNS' ];
|
|
|
|
|
|
|
|
|
|
$insertOptions = (array)$insertOptions;
|
|
|
|
|
$selectOptions = (array)$selectOptions;
|
|
|
|
|
|
|
|
|
|
if ( $this->cliMode && $this->isInsertSelectSafe( $insertOptions, $selectOptions ) ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
// For massive migrations with downtime, we don't want to select everything
|
|
|
|
|
// into memory and OOM, so do all this native on the server side if possible.
|
2018-10-26 20:17:34 +00:00
|
|
|
$this->nativeInsertSelect(
|
|
|
|
|
$destTable,
|
|
|
|
|
$srcTable,
|
|
|
|
|
$varMap,
|
|
|
|
|
$conds,
|
|
|
|
|
$fname,
|
|
|
|
|
array_diff( $insertOptions, $hints ),
|
|
|
|
|
$selectOptions,
|
|
|
|
|
$selectJoinConds
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
$this->nonNativeInsertSelect(
|
2016-09-16 03:14:58 +00:00
|
|
|
$destTable,
|
|
|
|
|
$srcTable,
|
|
|
|
|
$varMap,
|
|
|
|
|
$conds,
|
|
|
|
|
$fname,
|
2018-02-28 23:33:03 +00:00
|
|
|
array_diff( $insertOptions, $hints ),
|
2017-06-09 16:58:09 +00:00
|
|
|
$selectOptions,
|
|
|
|
|
$selectJoinConds
|
2016-09-16 03:14:58 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-26 20:17:34 +00:00
|
|
|
return true;
|
2017-09-20 16:55:55 +00:00
|
|
|
}
|
|
|
|
|
|
2018-02-28 23:33:03 +00:00
|
|
|
/**
|
|
|
|
|
* @param array $insertOptions INSERT options
|
|
|
|
|
* @param array $selectOptions SELECT options
|
|
|
|
|
* @return bool Whether an INSERT SELECT with these options will be replication safe
|
|
|
|
|
* @since 1.31
|
|
|
|
|
*/
|
|
|
|
|
protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-20 16:55:55 +00:00
|
|
|
/**
|
|
|
|
|
* Implementation of insertSelect() based on select() and insert()
|
|
|
|
|
*
|
|
|
|
|
* @see IDatabase::insertSelect()
|
|
|
|
|
* @since 1.30
|
|
|
|
|
* @param string $destTable
|
|
|
|
|
* @param string|array $srcTable
|
|
|
|
|
* @param array $varMap
|
|
|
|
|
* @param array $conds
|
|
|
|
|
* @param string $fname
|
|
|
|
|
* @param array $insertOptions
|
|
|
|
|
* @param array $selectOptions
|
|
|
|
|
* @param array $selectJoinConds
|
|
|
|
|
*/
|
|
|
|
|
protected function nonNativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
|
|
|
|
|
$fname = __METHOD__,
|
|
|
|
|
$insertOptions = [], $selectOptions = [], $selectJoinConds = []
|
|
|
|
|
) {
|
2016-09-16 03:14:58 +00:00
|
|
|
// For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
|
|
|
|
|
// on only the master (without needing row-based-replication). It also makes it easy to
|
|
|
|
|
// know how big the INSERT is going to be.
|
|
|
|
|
$fields = [];
|
|
|
|
|
foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
|
|
|
|
|
$fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
|
|
|
|
|
}
|
|
|
|
|
$selectOptions[] = 'FOR UPDATE';
|
2017-06-09 16:58:09 +00:00
|
|
|
$res = $this->select(
|
|
|
|
|
$srcTable, implode( ',', $fields ), $conds, $fname, $selectOptions, $selectJoinConds
|
|
|
|
|
);
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( !$res ) {
|
2018-10-26 20:17:34 +00:00
|
|
|
return;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-02-08 19:16:29 +00:00
|
|
|
try {
|
|
|
|
|
$affectedRowCount = 0;
|
2018-03-20 15:57:04 +00:00
|
|
|
$this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
|
2018-02-08 19:16:29 +00:00
|
|
|
$rows = [];
|
|
|
|
|
$ok = true;
|
|
|
|
|
foreach ( $res as $row ) {
|
|
|
|
|
$rows[] = (array)$row;
|
|
|
|
|
|
|
|
|
|
// Avoid inserts that are too huge
|
|
|
|
|
if ( count( $rows ) >= $this->nonNativeInsertSelectBatchSize ) {
|
|
|
|
|
$ok = $this->insert( $destTable, $rows, $fname, $insertOptions );
|
|
|
|
|
if ( !$ok ) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
$affectedRowCount += $this->affectedRows();
|
|
|
|
|
$rows = [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if ( $rows && $ok ) {
|
|
|
|
|
$ok = $this->insert( $destTable, $rows, $fname, $insertOptions );
|
|
|
|
|
if ( $ok ) {
|
|
|
|
|
$affectedRowCount += $this->affectedRows();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if ( $ok ) {
|
|
|
|
|
$this->endAtomic( $fname );
|
|
|
|
|
$this->affectedRowCount = $affectedRowCount;
|
|
|
|
|
} else {
|
2018-03-17 21:59:56 +00:00
|
|
|
$this->cancelAtomic( $fname );
|
2018-02-08 19:16:29 +00:00
|
|
|
}
|
|
|
|
|
} catch ( Exception $e ) {
|
2018-03-17 21:59:56 +00:00
|
|
|
$this->cancelAtomic( $fname );
|
2018-02-08 19:16:29 +00:00
|
|
|
throw $e;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-13 01:10:47 +00:00
|
|
|
/**
|
|
|
|
|
* Native server-side implementation of insertSelect() for situations where
|
|
|
|
|
* we don't want to select everything into memory
|
|
|
|
|
*
|
|
|
|
|
* @see IDatabase::insertSelect()
|
2017-09-09 20:47:04 +00:00
|
|
|
* @param string $destTable
|
|
|
|
|
* @param string|array $srcTable
|
|
|
|
|
* @param array $varMap
|
|
|
|
|
* @param array $conds
|
|
|
|
|
* @param string $fname
|
|
|
|
|
* @param array $insertOptions
|
|
|
|
|
* @param array $selectOptions
|
|
|
|
|
* @param array $selectJoinConds
|
2017-05-13 01:10:47 +00:00
|
|
|
*/
|
2016-09-21 19:25:08 +00:00
|
|
|
protected function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
|
2016-09-16 03:14:58 +00:00
|
|
|
$fname = __METHOD__,
|
2017-06-09 16:58:09 +00:00
|
|
|
$insertOptions = [], $selectOptions = [], $selectJoinConds = []
|
2016-09-16 03:14:58 +00:00
|
|
|
) {
|
|
|
|
|
$destTable = $this->tableName( $destTable );
|
|
|
|
|
|
|
|
|
|
if ( !is_array( $insertOptions ) ) {
|
|
|
|
|
$insertOptions = [ $insertOptions ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$insertOptions = $this->makeInsertOptions( $insertOptions );
|
|
|
|
|
|
2017-06-09 16:58:09 +00:00
|
|
|
$selectSql = $this->selectSQLText(
|
|
|
|
|
$srcTable,
|
|
|
|
|
array_values( $varMap ),
|
|
|
|
|
$conds,
|
|
|
|
|
$fname,
|
|
|
|
|
$selectOptions,
|
|
|
|
|
$selectJoinConds
|
|
|
|
|
);
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2016-09-22 21:02:53 +00:00
|
|
|
$sql = "INSERT $insertOptions" .
|
2017-06-09 16:58:09 +00:00
|
|
|
" INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ') ' .
|
|
|
|
|
$selectSql;
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2018-10-26 20:17:34 +00:00
|
|
|
$this->query( $sql, $fname );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Construct a LIMIT query with optional offset. This is used for query
|
|
|
|
|
* pages. The SQL should be adjusted so that only the first $limit rows
|
|
|
|
|
* are returned. If $offset is provided as well, then the first $offset
|
|
|
|
|
* rows should be discarded, and the next $limit rows should be returned.
|
|
|
|
|
* If the result of the query is not ordered, then the rows to be returned
|
|
|
|
|
* are theoretically arbitrary.
|
|
|
|
|
*
|
|
|
|
|
* $sql is expected to be a SELECT, if that makes a difference.
|
|
|
|
|
*
|
|
|
|
|
* The version provided by default works in MySQL and SQLite. It will very
|
|
|
|
|
* likely need to be overridden for most other DBMSes.
|
|
|
|
|
*
|
|
|
|
|
* @param string $sql SQL query we will append the limit too
|
|
|
|
|
* @param int $limit The SQL limit
|
|
|
|
|
* @param int|bool $offset The SQL offset (default false)
|
|
|
|
|
* @throws DBUnexpectedError
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
public function limitResult( $sql, $limit, $offset = false ) {
|
|
|
|
|
if ( !is_numeric( $limit ) ) {
|
2016-09-21 19:25:08 +00:00
|
|
|
throw new DBUnexpectedError( $this,
|
|
|
|
|
"Invalid non-numeric limit passed to limitResult()\n" );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "$sql LIMIT "
|
|
|
|
|
. ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
|
|
|
|
|
. "{$limit} ";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function unionSupportsOrderAndLimit() {
|
|
|
|
|
return true; // True for almost every DB supported
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function unionQueries( $sqls, $all ) {
|
|
|
|
|
$glue = $all ? ') UNION ALL (' : ') UNION (';
|
|
|
|
|
|
|
|
|
|
return '(' . implode( $glue, $sqls ) . ')';
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-16 17:32:03 +00:00
|
|
|
public function unionConditionPermutations(
|
|
|
|
|
$table, $vars, array $permute_conds, $extra_conds = '', $fname = __METHOD__,
|
|
|
|
|
$options = [], $join_conds = []
|
|
|
|
|
) {
|
|
|
|
|
// First, build the Cartesian product of $permute_conds
|
|
|
|
|
$conds = [ [] ];
|
|
|
|
|
foreach ( $permute_conds as $field => $values ) {
|
|
|
|
|
if ( !$values ) {
|
|
|
|
|
// Skip empty $values
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$values = array_unique( $values ); // For sanity
|
|
|
|
|
$newConds = [];
|
|
|
|
|
foreach ( $conds as $cond ) {
|
|
|
|
|
foreach ( $values as $value ) {
|
|
|
|
|
$cond[$field] = $value;
|
|
|
|
|
$newConds[] = $cond; // Arrays are by-value, not by-reference, so this works
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$conds = $newConds;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$extra_conds = $extra_conds === '' ? [] : (array)$extra_conds;
|
|
|
|
|
|
|
|
|
|
// If there's just one condition and no subordering, hand off to
|
|
|
|
|
// selectSQLText directly.
|
|
|
|
|
if ( count( $conds ) === 1 &&
|
|
|
|
|
( !isset( $options['INNER ORDER BY'] ) || !$this->unionSupportsOrderAndLimit() )
|
|
|
|
|
) {
|
|
|
|
|
return $this->selectSQLText(
|
|
|
|
|
$table, $vars, $conds[0] + $extra_conds, $fname, $options, $join_conds
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Otherwise, we need to pull out the order and limit to apply after
|
|
|
|
|
// the union. Then build the SQL queries for each set of conditions in
|
|
|
|
|
// $conds. Then union them together (using UNION ALL, because the
|
|
|
|
|
// product *should* already be distinct).
|
|
|
|
|
$orderBy = $this->makeOrderBy( $options );
|
2017-10-06 22:17:58 +00:00
|
|
|
$limit = $options['LIMIT'] ?? null;
|
|
|
|
|
$offset = $options['OFFSET'] ?? false;
|
2017-06-16 17:32:03 +00:00
|
|
|
$all = empty( $options['NOTALL'] ) && !in_array( 'NOTALL', $options );
|
|
|
|
|
if ( !$this->unionSupportsOrderAndLimit() ) {
|
|
|
|
|
unset( $options['ORDER BY'], $options['LIMIT'], $options['OFFSET'] );
|
|
|
|
|
} else {
|
|
|
|
|
if ( array_key_exists( 'INNER ORDER BY', $options ) ) {
|
|
|
|
|
$options['ORDER BY'] = $options['INNER ORDER BY'];
|
|
|
|
|
}
|
|
|
|
|
if ( $limit !== null && is_numeric( $offset ) && $offset != 0 ) {
|
|
|
|
|
// We need to increase the limit by the offset rather than
|
|
|
|
|
// using the offset directly, otherwise it'll skip incorrectly
|
|
|
|
|
// in the subqueries.
|
|
|
|
|
$options['LIMIT'] = $limit + $offset;
|
|
|
|
|
unset( $options['OFFSET'] );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$sqls = [];
|
|
|
|
|
foreach ( $conds as $cond ) {
|
|
|
|
|
$sqls[] = $this->selectSQLText(
|
|
|
|
|
$table, $vars, $cond + $extra_conds, $fname, $options, $join_conds
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
$sql = $this->unionQueries( $sqls, $all ) . $orderBy;
|
|
|
|
|
if ( $limit !== null ) {
|
|
|
|
|
$sql = $this->limitResult( $sql, $limit, $offset );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $sql;
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
public function conditional( $cond, $trueVal, $falseVal ) {
|
|
|
|
|
if ( is_array( $cond ) ) {
|
2016-09-19 07:28:17 +00:00
|
|
|
$cond = $this->makeList( $cond, self::LIST_AND );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function strreplace( $orig, $old, $new ) {
|
|
|
|
|
return "REPLACE({$orig}, {$old}, {$new})";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getServerUptime() {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function wasDeadlock() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function wasLockTimeout() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-20 00:26:49 +00:00
|
|
|
public function wasConnectionLoss() {
|
|
|
|
|
return $this->wasConnectionError( $this->lastErrno() );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function wasReadOnlyError() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-20 00:26:49 +00:00
|
|
|
public function wasErrorReissuable() {
|
|
|
|
|
return (
|
|
|
|
|
$this->wasDeadlock() ||
|
|
|
|
|
$this->wasLockTimeout() ||
|
|
|
|
|
$this->wasConnectionLoss()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
2016-09-22 22:09:55 +00:00
|
|
|
* Do not use this method outside of Database/DBError classes
|
2016-09-16 03:14:58 +00:00
|
|
|
*
|
2017-08-20 11:20:59 +00:00
|
|
|
* @param int|string $errno
|
2016-09-21 19:25:08 +00:00
|
|
|
* @return bool Whether the given query error was a connection drop
|
2016-09-16 03:14:58 +00:00
|
|
|
*/
|
|
|
|
|
public function wasConnectionError( $errno ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-23 09:57:21 +00:00
|
|
|
/**
|
|
|
|
|
* @return bool Whether it is safe to assume the given error only caused statement rollback
|
|
|
|
|
* @note This is for backwards compatibility for callers catching DBError exceptions in
|
|
|
|
|
* order to ignore problems like duplicate key errors or foriegn key violations
|
|
|
|
|
* @since 1.31
|
|
|
|
|
*/
|
|
|
|
|
protected function wasKnownStatementRollbackError() {
|
|
|
|
|
return false; // don't know; it could have caused a transaction rollback
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
public function deadlockLoop() {
|
|
|
|
|
$args = func_get_args();
|
|
|
|
|
$function = array_shift( $args );
|
|
|
|
|
$tries = self::DEADLOCK_TRIES;
|
|
|
|
|
|
|
|
|
|
$this->begin( __METHOD__ );
|
|
|
|
|
|
|
|
|
|
$retVal = null;
|
|
|
|
|
/** @var Exception $e */
|
|
|
|
|
$e = null;
|
|
|
|
|
do {
|
|
|
|
|
try {
|
2018-06-09 23:26:32 +00:00
|
|
|
$retVal = $function( ...$args );
|
2016-09-16 03:14:58 +00:00
|
|
|
break;
|
|
|
|
|
} catch ( DBQueryError $e ) {
|
|
|
|
|
if ( $this->wasDeadlock() ) {
|
|
|
|
|
// Retry after a randomized delay
|
|
|
|
|
usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
|
|
|
|
|
} else {
|
|
|
|
|
// Throw the error back up
|
|
|
|
|
throw $e;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} while ( --$tries > 0 );
|
|
|
|
|
|
|
|
|
|
if ( $tries <= 0 ) {
|
|
|
|
|
// Too many deadlocks; give up
|
|
|
|
|
$this->rollback( __METHOD__ );
|
|
|
|
|
throw $e;
|
|
|
|
|
} else {
|
|
|
|
|
$this->commit( __METHOD__ );
|
|
|
|
|
|
|
|
|
|
return $retVal;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function masterPosWait( DBMasterPos $pos, $timeout ) {
|
|
|
|
|
# Real waits are implemented in the subclass.
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-24 04:17:32 +00:00
|
|
|
public function getReplicaPos() {
|
2016-09-16 03:14:58 +00:00
|
|
|
# Stub
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getMasterPos() {
|
|
|
|
|
# Stub
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function serverIsReadOnly() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( !$this->trxLevel ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
throw new DBUnexpectedError( $this, "No transaction is active." );
|
|
|
|
|
}
|
2018-03-24 07:02:24 +00:00
|
|
|
$this->trxEndCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-05-09 02:28:39 +00:00
|
|
|
final public function onTransactionCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
|
2018-03-19 05:26:11 +00:00
|
|
|
if ( !$this->trxLevel && $this->getTransactionRoundId() ) {
|
|
|
|
|
// Start an implicit transaction similar to how query() does
|
|
|
|
|
$this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
|
|
|
|
|
$this->trxAutomatic = true;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-24 07:02:24 +00:00
|
|
|
$this->trxIdleCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( !$this->trxLevel ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-09 02:28:39 +00:00
|
|
|
final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
|
|
|
|
|
$this->onTransactionCommitOrIdle( $callback, $fname );
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
|
2018-03-19 05:26:11 +00:00
|
|
|
if ( !$this->trxLevel && $this->getTransactionRoundId() ) {
|
|
|
|
|
// Start an implicit transaction similar to how query() does
|
|
|
|
|
$this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
|
|
|
|
|
$this->trxAutomatic = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $this->trxLevel ) {
|
2018-03-24 07:02:24 +00:00
|
|
|
$this->trxPreCommitCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
|
2016-09-16 03:14:58 +00:00
|
|
|
} else {
|
2017-07-26 09:04:34 +00:00
|
|
|
// No transaction is active nor will start implicitly, so make one for this callback
|
2018-03-20 15:57:04 +00:00
|
|
|
$this->startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
|
2016-09-16 03:14:58 +00:00
|
|
|
try {
|
2018-06-09 23:26:32 +00:00
|
|
|
$callback( $this );
|
2016-09-16 03:14:58 +00:00
|
|
|
$this->endAtomic( __METHOD__ );
|
|
|
|
|
} catch ( Exception $e ) {
|
2018-03-17 21:59:56 +00:00
|
|
|
$this->cancelAtomic( __METHOD__ );
|
2016-09-16 03:14:58 +00:00
|
|
|
throw $e;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-24 07:02:24 +00:00
|
|
|
/**
|
|
|
|
|
* @return AtomicSectionIdentifier|null ID of the topmost atomic section level
|
|
|
|
|
*/
|
|
|
|
|
private function currentAtomicSectionId() {
|
|
|
|
|
if ( $this->trxLevel && $this->trxAtomicLevels ) {
|
|
|
|
|
$levelInfo = end( $this->trxAtomicLevels );
|
|
|
|
|
|
|
|
|
|
return $levelInfo[1];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param AtomicSectionIdentifier $old
|
|
|
|
|
* @param AtomicSectionIdentifier $new
|
|
|
|
|
*/
|
|
|
|
|
private function reassignCallbacksForSection(
|
|
|
|
|
AtomicSectionIdentifier $old, AtomicSectionIdentifier $new
|
|
|
|
|
) {
|
|
|
|
|
foreach ( $this->trxPreCommitCallbacks as $key => $info ) {
|
|
|
|
|
if ( $info[2] === $old ) {
|
|
|
|
|
$this->trxPreCommitCallbacks[$key][2] = $new;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
foreach ( $this->trxIdleCallbacks as $key => $info ) {
|
|
|
|
|
if ( $info[2] === $old ) {
|
|
|
|
|
$this->trxIdleCallbacks[$key][2] = $new;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
foreach ( $this->trxEndCallbacks as $key => $info ) {
|
|
|
|
|
if ( $info[2] === $old ) {
|
|
|
|
|
$this->trxEndCallbacks[$key][2] = $new;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param AtomicSectionIdentifier[] $sectionIds ID of an actual savepoint
|
|
|
|
|
* @throws UnexpectedValueException
|
|
|
|
|
*/
|
|
|
|
|
private function modifyCallbacksForCancel( array $sectionIds ) {
|
|
|
|
|
// Cancel the "on commit" callbacks owned by this savepoint
|
|
|
|
|
$this->trxIdleCallbacks = array_filter(
|
|
|
|
|
$this->trxIdleCallbacks,
|
|
|
|
|
function ( $entry ) use ( $sectionIds ) {
|
|
|
|
|
return !in_array( $entry[2], $sectionIds, true );
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
$this->trxPreCommitCallbacks = array_filter(
|
|
|
|
|
$this->trxPreCommitCallbacks,
|
|
|
|
|
function ( $entry ) use ( $sectionIds ) {
|
|
|
|
|
return !in_array( $entry[2], $sectionIds, true );
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
// Make "on resolution" callbacks owned by this savepoint to perceive a rollback
|
|
|
|
|
foreach ( $this->trxEndCallbacks as $key => $entry ) {
|
|
|
|
|
if ( in_array( $entry[2], $sectionIds, true ) ) {
|
|
|
|
|
$callback = $entry[0];
|
|
|
|
|
$this->trxEndCallbacks[$key][0] = function () use ( $callback ) {
|
2018-04-17 04:39:02 +00:00
|
|
|
return $callback( self::TRIGGER_ROLLBACK, $this );
|
2018-03-24 07:02:24 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
final public function setTransactionListener( $name, callable $callback = null ) {
|
|
|
|
|
if ( $callback ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->trxRecurringCallbacks[$name] = $callback;
|
2016-09-16 03:14:58 +00:00
|
|
|
} else {
|
2018-02-13 06:58:57 +00:00
|
|
|
unset( $this->trxRecurringCallbacks[$name] );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Whether to disable running of post-COMMIT/ROLLBACK callbacks
|
|
|
|
|
*
|
|
|
|
|
* This method should not be used outside of Database/LoadBalancer
|
|
|
|
|
*
|
|
|
|
|
* @param bool $suppress
|
|
|
|
|
* @since 1.28
|
|
|
|
|
*/
|
|
|
|
|
final public function setTrxEndCallbackSuppression( $suppress ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->trxEndCallbacksSuppressed = $suppress;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2018-03-28 20:01:32 +00:00
|
|
|
* Actually consume and run any "on transaction idle/resolution" callbacks.
|
2016-09-16 03:14:58 +00:00
|
|
|
*
|
|
|
|
|
* This method should not be used outside of Database/LoadBalancer
|
|
|
|
|
*
|
2017-08-20 11:20:59 +00:00
|
|
|
* @param int $trigger IDatabase::TRIGGER_* constant
|
2018-03-28 20:01:32 +00:00
|
|
|
* @return int Number of callbacks attempted
|
2016-09-16 03:14:58 +00:00
|
|
|
* @since 1.20
|
|
|
|
|
* @throws Exception
|
|
|
|
|
*/
|
|
|
|
|
public function runOnTransactionIdleCallbacks( $trigger ) {
|
2018-03-28 20:01:32 +00:00
|
|
|
if ( $this->trxLevel ) { // sanity
|
|
|
|
|
throw new DBUnexpectedError( $this, __METHOD__ . ': a transaction is still open.' );
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( $this->trxEndCallbacksSuppressed ) {
|
2018-03-28 20:01:32 +00:00
|
|
|
return 0;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-03-28 20:01:32 +00:00
|
|
|
$count = 0;
|
2016-09-23 19:41:22 +00:00
|
|
|
$autoTrx = $this->getFlag( self::DBO_TRX ); // automatic begin() enabled?
|
2016-09-16 03:14:58 +00:00
|
|
|
/** @var Exception $e */
|
|
|
|
|
$e = null; // first exception
|
|
|
|
|
do { // callbacks may add callbacks :)
|
|
|
|
|
$callbacks = array_merge(
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->trxIdleCallbacks,
|
|
|
|
|
$this->trxEndCallbacks // include "transaction resolution" callbacks
|
2016-09-16 03:14:58 +00:00
|
|
|
);
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->trxIdleCallbacks = []; // consumed (and recursion guard)
|
|
|
|
|
$this->trxEndCallbacks = []; // consumed (recursion guard)
|
2016-09-16 03:14:58 +00:00
|
|
|
foreach ( $callbacks as $callback ) {
|
2018-05-25 23:02:27 +00:00
|
|
|
++$count;
|
|
|
|
|
list( $phpCallback ) = $callback;
|
|
|
|
|
$this->clearFlag( self::DBO_TRX ); // make each query its own transaction
|
2016-09-16 03:14:58 +00:00
|
|
|
try {
|
2018-04-17 04:39:02 +00:00
|
|
|
call_user_func( $phpCallback, $trigger, $this );
|
2016-09-16 03:14:58 +00:00
|
|
|
} catch ( Exception $ex ) {
|
|
|
|
|
call_user_func( $this->errorLogger, $ex );
|
|
|
|
|
$e = $e ?: $ex;
|
|
|
|
|
// Some callbacks may use startAtomic/endAtomic, so make sure
|
|
|
|
|
// their transactions are ended so other callbacks don't fail
|
|
|
|
|
if ( $this->trxLevel() ) {
|
|
|
|
|
$this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
|
|
|
|
|
}
|
2018-05-25 23:02:27 +00:00
|
|
|
} finally {
|
|
|
|
|
if ( $autoTrx ) {
|
|
|
|
|
$this->setFlag( self::DBO_TRX ); // restore automatic begin()
|
|
|
|
|
} else {
|
|
|
|
|
$this->clearFlag( self::DBO_TRX ); // restore auto-commit
|
|
|
|
|
}
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
2018-02-13 06:58:57 +00:00
|
|
|
} while ( count( $this->trxIdleCallbacks ) );
|
2016-09-16 03:14:58 +00:00
|
|
|
|
|
|
|
|
if ( $e instanceof Exception ) {
|
|
|
|
|
throw $e; // re-throw any first exception
|
|
|
|
|
}
|
2018-03-28 20:01:32 +00:00
|
|
|
|
|
|
|
|
return $count;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2018-03-28 20:01:32 +00:00
|
|
|
* Actually consume and run any "on transaction pre-commit" callbacks.
|
2016-09-16 03:14:58 +00:00
|
|
|
*
|
|
|
|
|
* This method should not be used outside of Database/LoadBalancer
|
|
|
|
|
*
|
|
|
|
|
* @since 1.22
|
2018-03-28 20:01:32 +00:00
|
|
|
* @return int Number of callbacks attempted
|
2016-09-16 03:14:58 +00:00
|
|
|
* @throws Exception
|
|
|
|
|
*/
|
|
|
|
|
public function runOnTransactionPreCommitCallbacks() {
|
2018-03-28 20:01:32 +00:00
|
|
|
$count = 0;
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
$e = null; // first exception
|
|
|
|
|
do { // callbacks may add callbacks :)
|
2018-02-13 06:58:57 +00:00
|
|
|
$callbacks = $this->trxPreCommitCallbacks;
|
|
|
|
|
$this->trxPreCommitCallbacks = []; // consumed (and recursion guard)
|
2016-09-16 03:14:58 +00:00
|
|
|
foreach ( $callbacks as $callback ) {
|
|
|
|
|
try {
|
2018-03-28 20:01:32 +00:00
|
|
|
++$count;
|
2016-09-16 03:14:58 +00:00
|
|
|
list( $phpCallback ) = $callback;
|
2018-06-09 23:26:32 +00:00
|
|
|
$phpCallback( $this );
|
2016-09-16 03:14:58 +00:00
|
|
|
} catch ( Exception $ex ) {
|
2018-08-22 04:25:48 +00:00
|
|
|
( $this->errorLogger )( $ex );
|
2016-09-16 03:14:58 +00:00
|
|
|
$e = $e ?: $ex;
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-02-13 06:58:57 +00:00
|
|
|
} while ( count( $this->trxPreCommitCallbacks ) );
|
2016-09-16 03:14:58 +00:00
|
|
|
|
|
|
|
|
if ( $e instanceof Exception ) {
|
|
|
|
|
throw $e; // re-throw any first exception
|
|
|
|
|
}
|
2018-03-28 20:01:32 +00:00
|
|
|
|
|
|
|
|
return $count;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Actually run any "transaction listener" callbacks.
|
|
|
|
|
*
|
|
|
|
|
* This method should not be used outside of Database/LoadBalancer
|
|
|
|
|
*
|
2017-08-20 11:20:59 +00:00
|
|
|
* @param int $trigger IDatabase::TRIGGER_* constant
|
2016-09-16 03:14:58 +00:00
|
|
|
* @throws Exception
|
|
|
|
|
* @since 1.20
|
|
|
|
|
*/
|
|
|
|
|
public function runTransactionListenerCallbacks( $trigger ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( $this->trxEndCallbacksSuppressed ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @var Exception $e */
|
|
|
|
|
$e = null; // first exception
|
|
|
|
|
|
2018-02-13 06:58:57 +00:00
|
|
|
foreach ( $this->trxRecurringCallbacks as $phpCallback ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
try {
|
|
|
|
|
$phpCallback( $trigger, $this );
|
|
|
|
|
} catch ( Exception $ex ) {
|
2018-06-09 23:26:32 +00:00
|
|
|
( $this->errorLogger )( $ex );
|
2016-09-16 03:14:58 +00:00
|
|
|
$e = $e ?: $ex;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $e instanceof Exception ) {
|
|
|
|
|
throw $e; // re-throw any first exception
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-17 21:59:56 +00:00
|
|
|
/**
|
|
|
|
|
* Create a savepoint
|
|
|
|
|
*
|
|
|
|
|
* This is used internally to implement atomic sections. It should not be
|
|
|
|
|
* used otherwise.
|
|
|
|
|
*
|
|
|
|
|
* @since 1.31
|
|
|
|
|
* @param string $identifier Identifier for the savepoint
|
|
|
|
|
* @param string $fname Calling function name
|
|
|
|
|
*/
|
|
|
|
|
protected function doSavepoint( $identifier, $fname ) {
|
|
|
|
|
$this->query( 'SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Release a savepoint
|
|
|
|
|
*
|
|
|
|
|
* This is used internally to implement atomic sections. It should not be
|
|
|
|
|
* used otherwise.
|
|
|
|
|
*
|
|
|
|
|
* @since 1.31
|
|
|
|
|
* @param string $identifier Identifier for the savepoint
|
|
|
|
|
* @param string $fname Calling function name
|
|
|
|
|
*/
|
|
|
|
|
protected function doReleaseSavepoint( $identifier, $fname ) {
|
|
|
|
|
$this->query( 'RELEASE SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Rollback to a savepoint
|
|
|
|
|
*
|
|
|
|
|
* This is used internally to implement atomic sections. It should not be
|
|
|
|
|
* used otherwise.
|
|
|
|
|
*
|
|
|
|
|
* @since 1.31
|
|
|
|
|
* @param string $identifier Identifier for the savepoint
|
|
|
|
|
* @param string $fname Calling function name
|
|
|
|
|
*/
|
|
|
|
|
protected function doRollbackToSavepoint( $identifier, $fname ) {
|
|
|
|
|
$this->query( 'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-24 07:02:24 +00:00
|
|
|
/**
|
|
|
|
|
* @param string $fname
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
private function nextSavepointId( $fname ) {
|
|
|
|
|
$savepointId = self::$SAVEPOINT_PREFIX . ++$this->trxAtomicCounter;
|
|
|
|
|
if ( strlen( $savepointId ) > 30 ) {
|
|
|
|
|
// 30 == Oracle's identifier length limit (pre 12c)
|
|
|
|
|
// With a 22 character prefix, that puts the highest number at 99999999.
|
|
|
|
|
throw new DBUnexpectedError(
|
|
|
|
|
$this,
|
|
|
|
|
'There have been an excessively large number of atomic sections in a transaction'
|
|
|
|
|
. " started by $this->trxFname (at $fname)"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $savepointId;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-20 15:57:04 +00:00
|
|
|
final public function startAtomic(
|
|
|
|
|
$fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE
|
|
|
|
|
) {
|
2018-03-24 07:02:24 +00:00
|
|
|
$savepointId = $cancelable === self::ATOMIC_CANCELABLE ? self::$NOT_APPLICABLE : null;
|
|
|
|
|
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( !$this->trxLevel ) {
|
2018-03-28 20:01:32 +00:00
|
|
|
$this->begin( $fname, self::TRANSACTION_INTERNAL ); // sets trxAutomatic
|
2016-09-16 03:14:58 +00:00
|
|
|
// If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
|
|
|
|
|
// in all changes being in one transaction to keep requests transactional.
|
2018-03-24 07:02:24 +00:00
|
|
|
if ( $this->getFlag( self::DBO_TRX ) ) {
|
|
|
|
|
// Since writes could happen in between the topmost atomic sections as part
|
|
|
|
|
// of the transaction, those sections will need savepoints.
|
|
|
|
|
$savepointId = $this->nextSavepointId( $fname );
|
|
|
|
|
$this->doSavepoint( $savepointId, $fname );
|
|
|
|
|
} else {
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->trxAutomaticAtomic = true;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
2018-03-20 15:57:04 +00:00
|
|
|
} elseif ( $cancelable === self::ATOMIC_CANCELABLE ) {
|
2018-03-24 07:02:24 +00:00
|
|
|
$savepointId = $this->nextSavepointId( $fname );
|
2018-03-17 21:59:56 +00:00
|
|
|
$this->doSavepoint( $savepointId, $fname );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-03-29 07:23:10 +00:00
|
|
|
$sectionId = new AtomicSectionIdentifier;
|
|
|
|
|
$this->trxAtomicLevels[] = [ $fname, $sectionId, $savepointId ];
|
2018-10-10 04:03:21 +00:00
|
|
|
$this->queryLogger->debug( 'startAtomic: entering level ' .
|
|
|
|
|
( count( $this->trxAtomicLevels ) - 1 ) . " ($fname)" );
|
2018-03-29 07:23:10 +00:00
|
|
|
|
|
|
|
|
return $sectionId;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final public function endAtomic( $fname = __METHOD__ ) {
|
2018-03-29 07:23:10 +00:00
|
|
|
if ( !$this->trxLevel || !$this->trxAtomicLevels ) {
|
|
|
|
|
throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
2018-03-17 21:59:56 +00:00
|
|
|
|
2018-03-29 07:23:10 +00:00
|
|
|
// Check if the current section matches $fname
|
|
|
|
|
$pos = count( $this->trxAtomicLevels ) - 1;
|
2018-03-24 07:02:24 +00:00
|
|
|
list( $savedFname, $sectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
|
2018-10-10 04:03:21 +00:00
|
|
|
$this->queryLogger->debug( "endAtomic: leaving level $pos ($fname)" );
|
2018-03-29 07:23:10 +00:00
|
|
|
|
2018-03-17 21:59:56 +00:00
|
|
|
if ( $savedFname !== $fname ) {
|
2018-03-29 07:23:10 +00:00
|
|
|
throw new DBUnexpectedError(
|
|
|
|
|
$this,
|
|
|
|
|
"Invalid atomic section ended (got $fname but expected $savedFname)."
|
|
|
|
|
);
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-03-24 07:02:24 +00:00
|
|
|
// Remove the last section (no need to re-index the array)
|
|
|
|
|
array_pop( $this->trxAtomicLevels );
|
2018-03-29 07:23:10 +00:00
|
|
|
|
2018-03-20 15:57:04 +00:00
|
|
|
if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$this->commit( $fname, self::FLUSHING_INTERNAL );
|
2018-03-24 07:02:24 +00:00
|
|
|
} elseif ( $savepointId !== null && $savepointId !== self::$NOT_APPLICABLE ) {
|
2018-03-17 21:59:56 +00:00
|
|
|
$this->doReleaseSavepoint( $savepointId, $fname );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
2018-03-24 07:02:24 +00:00
|
|
|
|
|
|
|
|
// Hoist callback ownership for callbacks in the section that just ended;
|
|
|
|
|
// all callbacks should have an owner that is present in trxAtomicLevels.
|
|
|
|
|
$currentSectionId = $this->currentAtomicSectionId();
|
|
|
|
|
if ( $currentSectionId ) {
|
|
|
|
|
$this->reassignCallbacksForSection( $sectionId, $currentSectionId );
|
|
|
|
|
}
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-03-29 07:23:10 +00:00
|
|
|
final public function cancelAtomic(
|
|
|
|
|
$fname = __METHOD__, AtomicSectionIdentifier $sectionId = null
|
|
|
|
|
) {
|
|
|
|
|
if ( !$this->trxLevel || !$this->trxAtomicLevels ) {
|
|
|
|
|
throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
|
2018-03-17 21:59:56 +00:00
|
|
|
}
|
|
|
|
|
|
2018-10-10 04:03:21 +00:00
|
|
|
$excisedFnames = [];
|
2018-03-29 07:23:10 +00:00
|
|
|
if ( $sectionId !== null ) {
|
|
|
|
|
// Find the (last) section with the given $sectionId
|
|
|
|
|
$pos = -1;
|
|
|
|
|
foreach ( $this->trxAtomicLevels as $i => list( $asFname, $asId, $spId ) ) {
|
|
|
|
|
if ( $asId === $sectionId ) {
|
|
|
|
|
$pos = $i;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if ( $pos < 0 ) {
|
2018-12-01 16:40:47 +00:00
|
|
|
throw new DBUnexpectedError( $this, "Atomic section not found (for $fname)" );
|
2018-03-29 07:23:10 +00:00
|
|
|
}
|
|
|
|
|
// Remove all descendant sections and re-index the array
|
2018-03-24 07:02:24 +00:00
|
|
|
$excisedIds = [];
|
|
|
|
|
$len = count( $this->trxAtomicLevels );
|
|
|
|
|
for ( $i = $pos + 1; $i < $len; ++$i ) {
|
2018-10-10 04:03:21 +00:00
|
|
|
$excisedFnames[] = $this->trxAtomicLevels[$i][0];
|
2018-03-24 07:02:24 +00:00
|
|
|
$excisedIds[] = $this->trxAtomicLevels[$i][1];
|
|
|
|
|
}
|
2018-03-29 07:23:10 +00:00
|
|
|
$this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 );
|
2018-03-24 07:02:24 +00:00
|
|
|
$this->modifyCallbacksForCancel( $excisedIds );
|
2018-03-17 21:59:56 +00:00
|
|
|
}
|
2018-03-29 07:23:10 +00:00
|
|
|
|
|
|
|
|
// Check if the current section matches $fname
|
|
|
|
|
$pos = count( $this->trxAtomicLevels ) - 1;
|
2018-03-24 07:02:24 +00:00
|
|
|
list( $savedFname, $savedSectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
|
2018-03-29 07:23:10 +00:00
|
|
|
|
2018-10-10 04:03:21 +00:00
|
|
|
if ( $excisedFnames ) {
|
|
|
|
|
$this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname) " .
|
|
|
|
|
"and descendants " . implode( ', ', $excisedFnames ) );
|
|
|
|
|
} else {
|
|
|
|
|
$this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname)" );
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-29 07:23:10 +00:00
|
|
|
if ( $savedFname !== $fname ) {
|
|
|
|
|
throw new DBUnexpectedError(
|
|
|
|
|
$this,
|
|
|
|
|
"Invalid atomic section ended (got $fname but expected $savedFname)."
|
|
|
|
|
);
|
2018-03-20 15:57:04 +00:00
|
|
|
}
|
|
|
|
|
|
2018-03-24 07:02:24 +00:00
|
|
|
// Remove the last section (no need to re-index the array)
|
|
|
|
|
array_pop( $this->trxAtomicLevels );
|
|
|
|
|
$this->modifyCallbacksForCancel( [ $savedSectionId ] );
|
2018-03-29 07:23:10 +00:00
|
|
|
|
|
|
|
|
if ( $savepointId !== null ) {
|
|
|
|
|
// Rollback the transaction to the state just before this atomic section
|
2018-03-24 07:02:24 +00:00
|
|
|
if ( $savepointId === self::$NOT_APPLICABLE ) {
|
2018-03-29 07:23:10 +00:00
|
|
|
$this->rollback( $fname, self::FLUSHING_INTERNAL );
|
|
|
|
|
} else {
|
|
|
|
|
$this->doRollbackToSavepoint( $savepointId, $fname );
|
|
|
|
|
$this->trxStatus = self::STATUS_TRX_OK; // no exception; recovered
|
|
|
|
|
$this->trxStatusIgnoredCause = null;
|
|
|
|
|
}
|
|
|
|
|
} elseif ( $this->trxStatus > self::STATUS_TRX_ERROR ) {
|
|
|
|
|
// Put the transaction into an error state if it's not already in one
|
|
|
|
|
$this->trxStatus = self::STATUS_TRX_ERROR;
|
|
|
|
|
$this->trxStatusCause = new DBUnexpectedError(
|
|
|
|
|
$this,
|
|
|
|
|
"Uncancelable atomic section canceled (got $fname)."
|
|
|
|
|
);
|
2018-03-17 21:59:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->affectedRowCount = 0; // for the sake of consistency
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-29 07:23:10 +00:00
|
|
|
final public function doAtomicSection(
|
|
|
|
|
$fname, callable $callback, $cancelable = self::ATOMIC_NOT_CANCELABLE
|
|
|
|
|
) {
|
|
|
|
|
$sectionId = $this->startAtomic( $fname, $cancelable );
|
2016-09-16 03:14:58 +00:00
|
|
|
try {
|
2018-06-09 23:26:32 +00:00
|
|
|
$res = $callback( $this, $fname );
|
2016-09-16 03:14:58 +00:00
|
|
|
} catch ( Exception $e ) {
|
2018-03-29 07:23:10 +00:00
|
|
|
$this->cancelAtomic( $fname, $sectionId );
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
throw $e;
|
|
|
|
|
}
|
|
|
|
|
$this->endAtomic( $fname );
|
|
|
|
|
|
|
|
|
|
return $res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
|
2018-04-10 23:56:29 +00:00
|
|
|
static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
|
|
|
|
|
if ( !in_array( $mode, $modes, true ) ) {
|
|
|
|
|
throw new DBUnexpectedError( $this, "$fname: invalid mode parameter '$mode'." );
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
// Protect against mismatched atomic section, transaction nesting, and snapshot loss
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( $this->trxLevel ) {
|
|
|
|
|
if ( $this->trxAtomicLevels ) {
|
2018-03-23 09:57:21 +00:00
|
|
|
$levels = $this->flatAtomicSectionList();
|
2016-09-16 03:14:58 +00:00
|
|
|
$msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
|
|
|
|
|
throw new DBUnexpectedError( $this, $msg );
|
2018-02-13 06:58:57 +00:00
|
|
|
} elseif ( !$this->trxAutomatic ) {
|
|
|
|
|
$msg = "$fname: Explicit transaction already active (from {$this->trxFname}).";
|
2016-09-16 03:14:58 +00:00
|
|
|
throw new DBUnexpectedError( $this, $msg );
|
|
|
|
|
} else {
|
2018-02-13 06:58:57 +00:00
|
|
|
$msg = "$fname: Implicit transaction already active (from {$this->trxFname}).";
|
2018-03-09 00:58:48 +00:00
|
|
|
throw new DBUnexpectedError( $this, $msg );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
2016-09-23 19:41:22 +00:00
|
|
|
} elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$msg = "$fname: Implicit transaction expected (DBO_TRX set).";
|
2018-03-09 00:58:48 +00:00
|
|
|
throw new DBUnexpectedError( $this, $msg );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Avoid fatals if close() was called
|
|
|
|
|
$this->assertOpen();
|
|
|
|
|
|
|
|
|
|
$this->doBegin( $fname );
|
2018-03-23 09:57:21 +00:00
|
|
|
$this->trxStatus = self::STATUS_TRX_OK;
|
2018-04-05 18:17:09 +00:00
|
|
|
$this->trxStatusIgnoredCause = null;
|
2018-03-17 21:59:56 +00:00
|
|
|
$this->trxAtomicCounter = 0;
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->trxTimestamp = microtime( true );
|
|
|
|
|
$this->trxFname = $fname;
|
|
|
|
|
$this->trxDoneWrites = false;
|
|
|
|
|
$this->trxAutomaticAtomic = false;
|
|
|
|
|
$this->trxAtomicLevels = [];
|
|
|
|
|
$this->trxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
|
|
|
|
|
$this->trxWriteDuration = 0.0;
|
|
|
|
|
$this->trxWriteQueryCount = 0;
|
|
|
|
|
$this->trxWriteAffectedRows = 0;
|
|
|
|
|
$this->trxWriteAdjDuration = 0.0;
|
|
|
|
|
$this->trxWriteAdjQueryCount = 0;
|
|
|
|
|
$this->trxWriteCallers = [];
|
2016-09-16 03:14:58 +00:00
|
|
|
// First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
|
2018-03-29 21:51:47 +00:00
|
|
|
// Get an estimate of the replication lag before any such queries.
|
2018-04-02 21:39:33 +00:00
|
|
|
$this->trxReplicaLag = null; // clear cached value first
|
2018-03-29 21:51:47 +00:00
|
|
|
$this->trxReplicaLag = $this->getApproximateLagStatus()['lag'];
|
2016-10-09 17:47:49 +00:00
|
|
|
// T147697: make explicitTrxActive() return true until begin() finishes. This way, no
|
|
|
|
|
// caller will think its OK to muck around with the transaction just because startAtomic()
|
2018-02-13 06:58:57 +00:00
|
|
|
// has not yet completed (e.g. setting trxAtomicLevels).
|
|
|
|
|
$this->trxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Issues the BEGIN command to the database server.
|
|
|
|
|
*
|
2016-09-26 22:40:07 +00:00
|
|
|
* @see Database::begin()
|
2016-09-16 03:14:58 +00:00
|
|
|
* @param string $fname
|
|
|
|
|
*/
|
|
|
|
|
protected function doBegin( $fname ) {
|
|
|
|
|
$this->query( 'BEGIN', $fname );
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->trxLevel = 1;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-04-10 23:56:29 +00:00
|
|
|
final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
|
|
|
|
|
static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
|
|
|
|
|
if ( !in_array( $flush, $modes, true ) ) {
|
|
|
|
|
throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'." );
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( $this->trxLevel && $this->trxAtomicLevels ) {
|
2018-04-10 23:56:29 +00:00
|
|
|
// There are still atomic sections open; this cannot be ignored
|
2018-03-23 09:57:21 +00:00
|
|
|
$levels = $this->flatAtomicSectionList();
|
2016-09-16 03:14:58 +00:00
|
|
|
throw new DBUnexpectedError(
|
|
|
|
|
$this,
|
|
|
|
|
"$fname: Got COMMIT while atomic sections $levels are still open."
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( !$this->trxLevel ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
return; // nothing to do
|
2018-02-13 06:58:57 +00:00
|
|
|
} elseif ( !$this->trxAutomatic ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
throw new DBUnexpectedError(
|
|
|
|
|
$this,
|
|
|
|
|
"$fname: Flushing an explicit transaction, getting out of sync."
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( !$this->trxLevel ) {
|
2016-09-22 21:02:53 +00:00
|
|
|
$this->queryLogger->error(
|
|
|
|
|
"$fname: No transaction to commit, something got out of sync." );
|
2016-09-16 03:14:58 +00:00
|
|
|
return; // nothing to do
|
2018-02-13 06:58:57 +00:00
|
|
|
} elseif ( $this->trxAutomatic ) {
|
2018-03-09 00:58:48 +00:00
|
|
|
throw new DBUnexpectedError(
|
|
|
|
|
$this,
|
|
|
|
|
"$fname: Expected mass commit of all peer transactions (DBO_TRX set)."
|
|
|
|
|
);
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Avoid fatals if close() was called
|
|
|
|
|
$this->assertOpen();
|
|
|
|
|
|
|
|
|
|
$this->runOnTransactionPreCommitCallbacks();
|
2018-07-02 15:49:28 +00:00
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
$writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
|
|
|
|
|
$this->doCommit( $fname );
|
2018-03-23 09:57:21 +00:00
|
|
|
$this->trxStatus = self::STATUS_TRX_NONE;
|
2018-07-02 15:49:28 +00:00
|
|
|
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( $this->trxDoneWrites ) {
|
|
|
|
|
$this->lastWriteTime = microtime( true );
|
2016-09-16 03:14:58 +00:00
|
|
|
$this->trxProfiler->transactionWritingOut(
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->server,
|
2018-08-15 08:20:30 +00:00
|
|
|
$this->getDomainID(),
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->trxShortId,
|
2017-05-26 18:42:05 +00:00
|
|
|
$writeTime,
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->trxWriteAffectedRows
|
2017-05-26 18:42:05 +00:00
|
|
|
);
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-03-28 20:01:32 +00:00
|
|
|
// With FLUSHING_ALL_PEERS, callbacks will be explicitly run later
|
|
|
|
|
if ( $flush !== self::FLUSHING_ALL_PEERS ) {
|
|
|
|
|
$this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
|
|
|
|
|
$this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
|
|
|
|
|
}
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Issues the COMMIT command to the database server.
|
|
|
|
|
*
|
2016-09-26 22:40:07 +00:00
|
|
|
* @see Database::commit()
|
2016-09-16 03:14:58 +00:00
|
|
|
* @param string $fname
|
|
|
|
|
*/
|
|
|
|
|
protected function doCommit( $fname ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( $this->trxLevel ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$this->query( 'COMMIT', $fname );
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->trxLevel = 0;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final public function rollback( $fname = __METHOD__, $flush = '' ) {
|
2018-03-19 23:20:15 +00:00
|
|
|
$trxActive = $this->trxLevel;
|
|
|
|
|
|
|
|
|
|
if ( $flush !== self::FLUSHING_INTERNAL && $flush !== self::FLUSHING_ALL_PEERS ) {
|
|
|
|
|
if ( $this->getFlag( self::DBO_TRX ) ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
throw new DBUnexpectedError(
|
|
|
|
|
$this,
|
2018-03-09 00:58:48 +00:00
|
|
|
"$fname: Expected mass rollback of all peer transactions (DBO_TRX set)."
|
2016-09-16 03:14:58 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-19 23:20:15 +00:00
|
|
|
if ( $trxActive ) {
|
|
|
|
|
// Avoid fatals if close() was called
|
|
|
|
|
$this->assertOpen();
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2018-03-19 23:20:15 +00:00
|
|
|
$this->doRollback( $fname );
|
2018-03-23 09:57:21 +00:00
|
|
|
$this->trxStatus = self::STATUS_TRX_NONE;
|
2018-03-19 23:20:15 +00:00
|
|
|
$this->trxAtomicLevels = [];
|
2018-10-25 15:34:39 +00:00
|
|
|
// Estimate the RTT via a query now that trxStatus is OK
|
|
|
|
|
$writeTime = $this->pingAndCalculateLastTrxApplyTime();
|
2018-07-02 15:49:28 +00:00
|
|
|
|
2018-03-19 23:20:15 +00:00
|
|
|
if ( $this->trxDoneWrites ) {
|
|
|
|
|
$this->trxProfiler->transactionWritingOut(
|
|
|
|
|
$this->server,
|
2018-08-15 08:20:30 +00:00
|
|
|
$this->getDomainID(),
|
2018-07-02 15:49:28 +00:00
|
|
|
$this->trxShortId,
|
|
|
|
|
$writeTime,
|
|
|
|
|
$this->trxWriteAffectedRows
|
2018-03-19 23:20:15 +00:00
|
|
|
);
|
|
|
|
|
}
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-03-19 23:20:15 +00:00
|
|
|
// Clear any commit-dependant callbacks. They might even be present
|
|
|
|
|
// only due to transaction rounds, with no SQL transaction being active
|
|
|
|
|
$this->trxIdleCallbacks = [];
|
|
|
|
|
$this->trxPreCommitCallbacks = [];
|
2018-02-28 22:41:18 +00:00
|
|
|
|
2018-03-28 20:01:32 +00:00
|
|
|
// With FLUSHING_ALL_PEERS, callbacks will be explicitly run later
|
|
|
|
|
if ( $trxActive && $flush !== self::FLUSHING_ALL_PEERS ) {
|
2018-03-19 23:20:15 +00:00
|
|
|
try {
|
|
|
|
|
$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
|
|
|
|
|
} catch ( Exception $e ) {
|
|
|
|
|
// already logged; finish and let LoadBalancer move on during mass-rollback
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
$this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
|
|
|
|
|
} catch ( Exception $e ) {
|
|
|
|
|
// already logged; let LoadBalancer move on during mass-rollback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->affectedRowCount = 0; // for the sake of consistency
|
|
|
|
|
}
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Issues the ROLLBACK command to the database server.
|
|
|
|
|
*
|
2016-09-26 22:40:07 +00:00
|
|
|
* @see Database::rollback()
|
2016-09-16 03:14:58 +00:00
|
|
|
* @param string $fname
|
|
|
|
|
*/
|
|
|
|
|
protected function doRollback( $fname ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( $this->trxLevel ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
# Disconnects cause rollback anyway, so ignore those errors
|
|
|
|
|
$ignoreErrors = true;
|
|
|
|
|
$this->query( 'ROLLBACK', $fname, $ignoreErrors );
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->trxLevel = 0;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function flushSnapshot( $fname = __METHOD__ ) {
|
|
|
|
|
if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
|
|
|
|
|
// This only flushes transactions to clear snapshots, not to write data
|
|
|
|
|
$fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
|
|
|
|
|
throw new DBUnexpectedError(
|
|
|
|
|
$this,
|
2016-10-17 18:21:40 +00:00
|
|
|
"$fname: Cannot flush snapshot because writes are pending ($fnames)."
|
2016-09-16 03:14:58 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->commit( $fname, self::FLUSHING_INTERNAL );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function explicitTrxActive() {
|
2018-02-13 06:58:57 +00:00
|
|
|
return $this->trxLevel && ( $this->trxAtomicLevels || !$this->trxAutomatic );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2016-11-28 18:26:14 +00:00
|
|
|
public function duplicateTableStructure(
|
|
|
|
|
$oldName, $newName, $temporary = false, $fname = __METHOD__
|
2016-09-16 03:14:58 +00:00
|
|
|
) {
|
|
|
|
|
throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-21 19:25:08 +00:00
|
|
|
public function listTables( $prefix = null, $fname = __METHOD__ ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function listViews( $prefix = null, $fname = __METHOD__ ) {
|
|
|
|
|
throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function timestamp( $ts = 0 ) {
|
2016-09-22 04:43:32 +00:00
|
|
|
$t = new ConvertibleTimestamp( $ts );
|
2016-09-16 03:14:58 +00:00
|
|
|
// Let errors bubble up to avoid putting garbage in the DB
|
|
|
|
|
return $t->getTimestamp( TS_MW );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function timestampOrNull( $ts = null ) {
|
|
|
|
|
if ( is_null( $ts ) ) {
|
|
|
|
|
return null;
|
|
|
|
|
} else {
|
|
|
|
|
return $this->timestamp( $ts );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-28 14:10:39 +00:00
|
|
|
public function affectedRows() {
|
|
|
|
|
return ( $this->affectedRowCount === null )
|
|
|
|
|
? $this->fetchAffectedRowCount() // default to driver value
|
|
|
|
|
: $this->affectedRowCount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return int Number of retrieved rows according to the driver
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function fetchAffectedRowCount();
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
|
|
|
|
* Take the result from a query, and wrap it in a ResultWrapper if
|
|
|
|
|
* necessary. Boolean values are passed through as is, to indicate success
|
|
|
|
|
* of write queries or failure.
|
|
|
|
|
*
|
2016-09-26 22:40:07 +00:00
|
|
|
* Once upon a time, Database::query() returned a bare MySQL result
|
2016-09-16 03:14:58 +00:00
|
|
|
* resource, and it was necessary to call this function to convert it to
|
|
|
|
|
* a wrapper. Nowadays, raw database objects are never exposed to external
|
|
|
|
|
* callers, so this is unnecessary in external code.
|
|
|
|
|
*
|
2018-08-22 04:25:48 +00:00
|
|
|
* @param bool|ResultWrapper|resource $result
|
2016-09-16 03:14:58 +00:00
|
|
|
* @return bool|ResultWrapper
|
|
|
|
|
*/
|
|
|
|
|
protected function resultObject( $result ) {
|
|
|
|
|
if ( !$result ) {
|
|
|
|
|
return false;
|
|
|
|
|
} elseif ( $result instanceof ResultWrapper ) {
|
|
|
|
|
return $result;
|
|
|
|
|
} elseif ( $result === true ) {
|
|
|
|
|
// Successful write query
|
|
|
|
|
return $result;
|
|
|
|
|
} else {
|
|
|
|
|
return new ResultWrapper( $this, $result );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function ping( &$rtt = null ) {
|
|
|
|
|
// Avoid hitting the server if it was hit recently
|
|
|
|
|
if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( !func_num_args() || $this->rttEstimate > 0 ) {
|
|
|
|
|
$rtt = $this->rttEstimate;
|
2016-09-16 03:14:58 +00:00
|
|
|
return true; // don't care about $rtt
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// This will reconnect if possible or return false if not
|
2016-09-23 19:41:22 +00:00
|
|
|
$this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
|
2016-09-16 03:14:58 +00:00
|
|
|
$ok = ( $this->query( self::PING_QUERY, __METHOD__, true ) !== false );
|
|
|
|
|
$this->restoreFlags( self::RESTORE_PRIOR );
|
|
|
|
|
|
|
|
|
|
if ( $ok ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
$rtt = $this->rttEstimate;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $ok;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2018-03-20 11:16:41 +00:00
|
|
|
* Close any existing (dead) database connection and open a new connection
|
2017-05-13 01:10:47 +00:00
|
|
|
*
|
2018-03-20 11:16:41 +00:00
|
|
|
* @param string $fname
|
2017-05-13 01:10:47 +00:00
|
|
|
* @return bool True if new connection is opened successfully, false if error
|
2016-09-16 03:14:58 +00:00
|
|
|
*/
|
2018-03-20 11:16:41 +00:00
|
|
|
protected function replaceLostConnection( $fname ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$this->closeConnection();
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->opened = false;
|
|
|
|
|
$this->conn = false;
|
2016-09-16 03:14:58 +00:00
|
|
|
try {
|
2018-08-14 23:44:41 +00:00
|
|
|
$this->open(
|
|
|
|
|
$this->server,
|
|
|
|
|
$this->user,
|
|
|
|
|
$this->password,
|
|
|
|
|
$this->getDBname(),
|
|
|
|
|
$this->dbSchema(),
|
|
|
|
|
$this->tablePrefix()
|
|
|
|
|
);
|
2016-09-16 03:14:58 +00:00
|
|
|
$this->lastPing = microtime( true );
|
|
|
|
|
$ok = true;
|
2018-03-20 11:16:41 +00:00
|
|
|
|
|
|
|
|
$this->connLogger->warning(
|
|
|
|
|
$fname . ': lost connection to {dbserver}; reconnected',
|
|
|
|
|
[
|
|
|
|
|
'dbserver' => $this->getServer(),
|
|
|
|
|
'trace' => ( new RuntimeException() )->getTraceAsString()
|
|
|
|
|
]
|
|
|
|
|
);
|
2016-09-16 03:14:58 +00:00
|
|
|
} catch ( DBConnectionError $e ) {
|
|
|
|
|
$ok = false;
|
2018-03-20 11:16:41 +00:00
|
|
|
|
|
|
|
|
$this->connLogger->error(
|
|
|
|
|
$fname . ': lost connection to {dbserver} permanently',
|
|
|
|
|
[ 'dbserver' => $this->getServer() ]
|
|
|
|
|
);
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-03-20 11:16:41 +00:00
|
|
|
$this->handleSessionLoss();
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
return $ok;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getSessionLagStatus() {
|
2018-04-02 21:39:33 +00:00
|
|
|
return $this->getRecordedTransactionLagStatus() ?: $this->getApproximateLagStatus();
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the replica DB lag when the current transaction started
|
|
|
|
|
*
|
|
|
|
|
* This is useful when transactions might use snapshot isolation
|
|
|
|
|
* (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
|
|
|
|
|
* is this lag plus transaction duration. If they don't, it is still
|
|
|
|
|
* safe to be pessimistic. This returns null if there is no transaction.
|
|
|
|
|
*
|
2018-04-02 21:39:33 +00:00
|
|
|
* This returns null if the lag status for this transaction was not yet recorded.
|
|
|
|
|
*
|
2016-09-16 03:14:58 +00:00
|
|
|
* @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
|
|
|
|
|
* @since 1.27
|
|
|
|
|
*/
|
2018-04-02 21:39:33 +00:00
|
|
|
final protected function getRecordedTransactionLagStatus() {
|
|
|
|
|
return ( $this->trxLevel && $this->trxReplicaLag !== null )
|
2018-02-13 06:58:57 +00:00
|
|
|
? [ 'lag' => $this->trxReplicaLag, 'since' => $this->trxTimestamp() ]
|
2016-09-16 03:14:58 +00:00
|
|
|
: null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a replica DB lag estimate for this server
|
|
|
|
|
*
|
|
|
|
|
* @return array ('lag': seconds or false on error, 'since': UNIX timestamp of estimate)
|
|
|
|
|
* @since 1.27
|
|
|
|
|
*/
|
2016-09-21 19:25:08 +00:00
|
|
|
protected function getApproximateLagStatus() {
|
2016-09-16 03:14:58 +00:00
|
|
|
return [
|
|
|
|
|
'lag' => $this->getLBInfo( 'replica' ) ? $this->getLag() : 0,
|
|
|
|
|
'since' => microtime( true )
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Merge the result of getSessionLagStatus() for several DBs
|
|
|
|
|
* using the most pessimistic values to estimate the lag of
|
|
|
|
|
* any data derived from them in combination
|
|
|
|
|
*
|
|
|
|
|
* This is information is useful for caching modules
|
|
|
|
|
*
|
|
|
|
|
* @see WANObjectCache::set()
|
|
|
|
|
* @see WANObjectCache::getWithSetCallback()
|
|
|
|
|
*
|
|
|
|
|
* @param IDatabase $db1
|
2018-06-26 21:14:43 +00:00
|
|
|
* @param IDatabase|null $db2 [optional]
|
2016-09-16 03:14:58 +00:00
|
|
|
* @return array Map of values:
|
|
|
|
|
* - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
|
|
|
|
|
* - since: oldest UNIX timestamp of any of the DB lag estimates
|
|
|
|
|
* - pending: whether any of the DBs have uncommitted changes
|
2017-11-30 22:03:38 +00:00
|
|
|
* @throws DBError
|
2016-09-16 03:14:58 +00:00
|
|
|
* @since 1.27
|
|
|
|
|
*/
|
2017-11-30 22:03:38 +00:00
|
|
|
public static function getCacheSetOptions( IDatabase $db1, IDatabase $db2 = null ) {
|
2016-11-16 10:35:20 +00:00
|
|
|
$res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
|
2016-09-16 03:14:58 +00:00
|
|
|
foreach ( func_get_args() as $db ) {
|
|
|
|
|
/** @var IDatabase $db */
|
2016-11-16 10:35:20 +00:00
|
|
|
$status = $db->getSessionLagStatus();
|
|
|
|
|
if ( $status['lag'] === false ) {
|
|
|
|
|
$res['lag'] = false;
|
|
|
|
|
} elseif ( $res['lag'] !== false ) {
|
|
|
|
|
$res['lag'] = max( $res['lag'], $status['lag'] );
|
|
|
|
|
}
|
|
|
|
|
$res['since'] = min( $res['since'], $status['since'] );
|
|
|
|
|
$res['pending'] = $res['pending'] ?: $db->writesPending();
|
2016-10-22 04:12:12 +00:00
|
|
|
}
|
|
|
|
|
|
2016-11-16 10:35:20 +00:00
|
|
|
return $res;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getLag() {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-21 19:25:08 +00:00
|
|
|
public function maxListLen() {
|
2016-09-16 03:14:58 +00:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function encodeBlob( $b ) {
|
|
|
|
|
return $b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function decodeBlob( $b ) {
|
|
|
|
|
if ( $b instanceof Blob ) {
|
|
|
|
|
$b = $b->fetch();
|
|
|
|
|
}
|
|
|
|
|
return $b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function setSessionOptions( array $options ) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function sourceFile(
|
2016-09-22 21:02:53 +00:00
|
|
|
$filename,
|
2016-10-03 17:48:58 +00:00
|
|
|
callable $lineCallback = null,
|
|
|
|
|
callable $resultCallback = null,
|
2016-09-22 21:02:53 +00:00
|
|
|
$fname = false,
|
2016-10-03 17:48:58 +00:00
|
|
|
callable $inputCallback = null
|
2016-09-16 03:14:58 +00:00
|
|
|
) {
|
2018-02-10 07:52:26 +00:00
|
|
|
Wikimedia\suppressWarnings();
|
2016-09-16 03:14:58 +00:00
|
|
|
$fp = fopen( $filename, 'r' );
|
2018-02-10 07:52:26 +00:00
|
|
|
Wikimedia\restoreWarnings();
|
2016-09-16 03:14:58 +00:00
|
|
|
|
2018-06-30 09:43:00 +00:00
|
|
|
if ( $fp === false ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
throw new RuntimeException( "Could not open \"{$filename}\".\n" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !$fname ) {
|
|
|
|
|
$fname = __METHOD__ . "( $filename )";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2016-09-22 21:02:53 +00:00
|
|
|
$error = $this->sourceStream(
|
|
|
|
|
$fp, $lineCallback, $resultCallback, $fname, $inputCallback );
|
2016-09-16 03:14:58 +00:00
|
|
|
} catch ( Exception $e ) {
|
|
|
|
|
fclose( $fp );
|
|
|
|
|
throw $e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fclose( $fp );
|
|
|
|
|
|
|
|
|
|
return $error;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function setSchemaVars( $vars ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->schemaVars = $vars;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2016-09-22 21:02:53 +00:00
|
|
|
public function sourceStream(
|
|
|
|
|
$fp,
|
2016-10-03 17:48:58 +00:00
|
|
|
callable $lineCallback = null,
|
|
|
|
|
callable $resultCallback = null,
|
2016-09-22 21:02:53 +00:00
|
|
|
$fname = __METHOD__,
|
2016-10-03 17:48:58 +00:00
|
|
|
callable $inputCallback = null
|
2016-09-16 03:14:58 +00:00
|
|
|
) {
|
2018-01-05 15:37:30 +00:00
|
|
|
$delimiterReset = new ScopedCallback(
|
|
|
|
|
function ( $delimiter ) {
|
|
|
|
|
$this->delimiter = $delimiter;
|
|
|
|
|
},
|
|
|
|
|
[ $this->delimiter ]
|
|
|
|
|
);
|
2016-09-16 03:14:58 +00:00
|
|
|
$cmd = '';
|
|
|
|
|
|
|
|
|
|
while ( !feof( $fp ) ) {
|
|
|
|
|
if ( $lineCallback ) {
|
|
|
|
|
call_user_func( $lineCallback );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$line = trim( fgets( $fp ) );
|
|
|
|
|
|
|
|
|
|
if ( $line == '' ) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-30 09:43:00 +00:00
|
|
|
if ( $line[0] == '-' && $line[1] == '-' ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $cmd != '' ) {
|
|
|
|
|
$cmd .= ' ';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$done = $this->streamStatementEnd( $cmd, $line );
|
|
|
|
|
|
|
|
|
|
$cmd .= "$line\n";
|
|
|
|
|
|
|
|
|
|
if ( $done || feof( $fp ) ) {
|
|
|
|
|
$cmd = $this->replaceVars( $cmd );
|
|
|
|
|
|
2017-11-16 16:48:25 +00:00
|
|
|
if ( $inputCallback ) {
|
2018-06-09 23:26:32 +00:00
|
|
|
$callbackResult = $inputCallback( $cmd );
|
2017-11-16 16:48:25 +00:00
|
|
|
|
|
|
|
|
if ( is_string( $callbackResult ) || !$callbackResult ) {
|
|
|
|
|
$cmd = $callbackResult;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $cmd ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$res = $this->query( $cmd, $fname );
|
|
|
|
|
|
|
|
|
|
if ( $resultCallback ) {
|
2018-06-09 23:26:32 +00:00
|
|
|
$resultCallback( $res, $this );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-06-30 09:43:00 +00:00
|
|
|
if ( $res === false ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
$err = $this->lastError();
|
|
|
|
|
|
|
|
|
|
return "Query \"{$cmd}\" failed with error code \"$err\".\n";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$cmd = '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-05 15:37:30 +00:00
|
|
|
ScopedCallback::consume( $delimiterReset );
|
2016-09-16 03:14:58 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Called by sourceStream() to check if we've reached a statement end
|
|
|
|
|
*
|
2016-09-22 22:09:55 +00:00
|
|
|
* @param string &$sql SQL assembled so far
|
|
|
|
|
* @param string &$newLine New line about to be added to $sql
|
2016-09-16 03:14:58 +00:00
|
|
|
* @return bool Whether $newLine contains end of the statement
|
|
|
|
|
*/
|
|
|
|
|
public function streamStatementEnd( &$sql, &$newLine ) {
|
|
|
|
|
if ( $this->delimiter ) {
|
|
|
|
|
$prev = $newLine;
|
2016-09-22 21:02:53 +00:00
|
|
|
$newLine = preg_replace(
|
|
|
|
|
'/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
|
2016-09-16 03:14:58 +00:00
|
|
|
if ( $newLine != $prev ) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Database independent variable replacement. Replaces a set of variables
|
|
|
|
|
* in an SQL statement with their contents as given by $this->getSchemaVars().
|
|
|
|
|
*
|
|
|
|
|
* Supports '{$var}' `{$var}` and / *$var* / (without the spaces) style variables.
|
|
|
|
|
*
|
|
|
|
|
* - '{$var}' should be used for text and is passed through the database's
|
|
|
|
|
* addQuotes method.
|
|
|
|
|
* - `{$var}` should be used for identifiers (e.g. table and database names).
|
|
|
|
|
* It is passed through the database's addIdentifierQuotes method which
|
|
|
|
|
* can be overridden if the database uses something other than backticks.
|
|
|
|
|
* - / *_* / or / *$wgDBprefix* / passes the name that follows through the
|
|
|
|
|
* database's tableName method.
|
|
|
|
|
* - / *i* / passes the name that follows through the database's indexName method.
|
|
|
|
|
* - In all other cases, / *$var* / is left unencoded. Except for table options,
|
|
|
|
|
* its use should be avoided. In 1.24 and older, string encoding was applied.
|
|
|
|
|
*
|
|
|
|
|
* @param string $ins SQL statement to replace variables in
|
|
|
|
|
* @return string The new SQL statement with variables replaced
|
|
|
|
|
*/
|
|
|
|
|
protected function replaceVars( $ins ) {
|
|
|
|
|
$vars = $this->getSchemaVars();
|
|
|
|
|
return preg_replace_callback(
|
|
|
|
|
'!
|
|
|
|
|
/\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
|
|
|
|
|
\'\{\$ (\w+) }\' | # 3. addQuotes
|
|
|
|
|
`\{\$ (\w+) }` | # 4. addIdentifierQuotes
|
|
|
|
|
/\*\$ (\w+) \*/ # 5. leave unencoded
|
|
|
|
|
!x',
|
|
|
|
|
function ( $m ) use ( $vars ) {
|
|
|
|
|
// Note: Because of <https://bugs.php.net/bug.php?id=51881>,
|
|
|
|
|
// check for both nonexistent keys *and* the empty string.
|
|
|
|
|
if ( isset( $m[1] ) && $m[1] !== '' ) {
|
|
|
|
|
if ( $m[1] === 'i' ) {
|
|
|
|
|
return $this->indexName( $m[2] );
|
|
|
|
|
} else {
|
|
|
|
|
return $this->tableName( $m[2] );
|
|
|
|
|
}
|
|
|
|
|
} elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
|
|
|
|
|
return $this->addQuotes( $vars[$m[3]] );
|
|
|
|
|
} elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
|
|
|
|
|
return $this->addIdentifierQuotes( $vars[$m[4]] );
|
|
|
|
|
} elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
|
|
|
|
|
return $vars[$m[5]];
|
|
|
|
|
} else {
|
|
|
|
|
return $m[0];
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
$ins
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get schema variables. If none have been set via setSchemaVars(), then
|
|
|
|
|
* use some defaults from the current object.
|
|
|
|
|
*
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
protected function getSchemaVars() {
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( $this->schemaVars ) {
|
|
|
|
|
return $this->schemaVars;
|
2016-09-16 03:14:58 +00:00
|
|
|
} else {
|
|
|
|
|
return $this->getDefaultSchemaVars();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get schema variables to use if none have been set via setSchemaVars().
|
|
|
|
|
*
|
|
|
|
|
* Override this in derived classes to provide variables for tables.sql
|
|
|
|
|
* and SQL patch files.
|
|
|
|
|
*
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
protected function getDefaultSchemaVars() {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function lockIsFree( $lockName, $method ) {
|
2018-02-14 08:27:14 +00:00
|
|
|
// RDBMs methods for checking named locks may or may not count this thread itself.
|
|
|
|
|
// In MySQL, IS_FREE_LOCK() returns 0 if the thread already has the lock. This is
|
|
|
|
|
// the behavior choosen by the interface for this method.
|
|
|
|
|
return !isset( $this->namedLocksHeld[$lockName] );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function lock( $lockName, $method, $timeout = 5 ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->namedLocksHeld[$lockName] = 1;
|
2016-09-16 03:14:58 +00:00
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function unlock( $lockName, $method ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
unset( $this->namedLocksHeld[$lockName] );
|
2016-09-16 03:14:58 +00:00
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
|
|
|
|
|
if ( $this->writesOrCallbacksPending() ) {
|
|
|
|
|
// This only flushes transactions to clear snapshots, not to write data
|
|
|
|
|
$fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
|
|
|
|
|
throw new DBUnexpectedError(
|
|
|
|
|
$this,
|
2016-10-17 18:21:40 +00:00
|
|
|
"$fname: Cannot flush pre-lock snapshot because writes are pending ($fnames)."
|
2016-09-16 03:14:58 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
|
|
|
|
|
if ( $this->trxLevel() ) {
|
|
|
|
|
// There is a good chance an exception was thrown, causing any early return
|
|
|
|
|
// from the caller. Let any error handler get a chance to issue rollback().
|
|
|
|
|
// If there isn't one, let the error bubble up and trigger server-side rollback.
|
|
|
|
|
$this->onTransactionResolution(
|
|
|
|
|
function () use ( $lockKey, $fname ) {
|
|
|
|
|
$this->unlock( $lockKey, $fname );
|
|
|
|
|
},
|
|
|
|
|
$fname
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
$this->unlock( $lockKey, $fname );
|
|
|
|
|
}
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
$this->commit( $fname, self::FLUSHING_INTERNAL );
|
|
|
|
|
|
|
|
|
|
return $unlocker;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function namedLocksEnqueue() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-30 21:56:22 +00:00
|
|
|
public function tableLocksHaveTransactionScope() {
|
2016-09-16 03:14:58 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-30 21:56:22 +00:00
|
|
|
final public function lockTables( array $read, array $write, $method ) {
|
|
|
|
|
if ( $this->writesOrCallbacksPending() ) {
|
|
|
|
|
throw new DBUnexpectedError( $this, "Transaction writes or callbacks still pending." );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $this->tableLocksHaveTransactionScope() ) {
|
|
|
|
|
$this->startAtomic( $method );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->doLockTables( $read, $write, $method );
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-13 01:10:47 +00:00
|
|
|
/**
|
|
|
|
|
* Helper function for lockTables() that handles the actual table locking
|
|
|
|
|
*
|
|
|
|
|
* @param array $read Array of tables to lock for read access
|
|
|
|
|
* @param array $write Array of tables to lock for write access
|
|
|
|
|
* @param string $method Name of caller
|
|
|
|
|
* @return true
|
|
|
|
|
*/
|
2017-03-30 21:56:22 +00:00
|
|
|
protected function doLockTables( array $read, array $write, $method ) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final public function unlockTables( $method ) {
|
|
|
|
|
if ( $this->tableLocksHaveTransactionScope() ) {
|
|
|
|
|
$this->endAtomic( $method );
|
|
|
|
|
|
|
|
|
|
return true; // locks released on COMMIT/ROLLBACK
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->doUnlockTables( $method );
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-13 01:10:47 +00:00
|
|
|
/**
|
|
|
|
|
* Helper function for unlockTables() that handles the actual table unlocking
|
|
|
|
|
*
|
|
|
|
|
* @param string $method Name of caller
|
|
|
|
|
* @return true
|
|
|
|
|
*/
|
2017-03-30 21:56:22 +00:00
|
|
|
protected function doUnlockTables( $method ) {
|
2016-09-16 03:14:58 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete a table
|
|
|
|
|
* @param string $tableName
|
|
|
|
|
* @param string $fName
|
|
|
|
|
* @return bool|ResultWrapper
|
|
|
|
|
* @since 1.18
|
|
|
|
|
*/
|
|
|
|
|
public function dropTable( $tableName, $fName = __METHOD__ ) {
|
|
|
|
|
if ( !$this->tableExists( $tableName, $fName ) ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
$sql = "DROP TABLE " . $this->tableName( $tableName ) . " CASCADE";
|
|
|
|
|
|
|
|
|
|
return $this->query( $sql, $fName );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getInfinity() {
|
|
|
|
|
return 'infinity';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function encodeExpiry( $expiry ) {
|
|
|
|
|
return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
|
|
|
|
|
? $this->getInfinity()
|
|
|
|
|
: $this->timestamp( $expiry );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function decodeExpiry( $expiry, $format = TS_MW ) {
|
|
|
|
|
if ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) {
|
|
|
|
|
return 'infinity';
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-22 04:46:24 +00:00
|
|
|
return ConvertibleTimestamp::convert( $format, $expiry );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function setBigSelects( $value = true ) {
|
|
|
|
|
// no-op
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function isReadOnly() {
|
|
|
|
|
return ( $this->getReadOnlyReason() !== false );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return string|bool Reason this DB is read-only or false if it is not
|
|
|
|
|
*/
|
|
|
|
|
protected function getReadOnlyReason() {
|
|
|
|
|
$reason = $this->getLBInfo( 'readOnlyReason' );
|
|
|
|
|
|
|
|
|
|
return is_string( $reason ) ? $reason : false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function setTableAliases( array $aliases ) {
|
|
|
|
|
$this->tableAliases = $aliases;
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-17 22:09:02 +00:00
|
|
|
public function setIndexAliases( array $aliases ) {
|
|
|
|
|
$this->indexAliases = $aliases;
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-17 18:21:40 +00:00
|
|
|
/**
|
2018-02-13 06:58:57 +00:00
|
|
|
* Get the underlying binding connection handle
|
2016-10-17 18:21:40 +00:00
|
|
|
*
|
2018-02-13 06:58:57 +00:00
|
|
|
* Makes sure the connection resource is set (disconnects and ping() failure can unset it).
|
2016-10-17 18:21:40 +00:00
|
|
|
* This catches broken callers than catch and ignore disconnection exceptions.
|
|
|
|
|
* Unlike checking isOpen(), this is safe to call inside of open().
|
|
|
|
|
*
|
2018-02-28 20:56:34 +00:00
|
|
|
* @return mixed
|
2016-10-17 18:21:40 +00:00
|
|
|
* @throws DBUnexpectedError
|
|
|
|
|
* @since 1.26
|
|
|
|
|
*/
|
|
|
|
|
protected function getBindingHandle() {
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( !$this->conn ) {
|
2016-10-17 18:21:40 +00:00
|
|
|
throw new DBUnexpectedError(
|
|
|
|
|
$this,
|
|
|
|
|
'DB connection was already closed or the connection dropped.'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-13 06:58:57 +00:00
|
|
|
return $this->conn;
|
2016-10-17 18:21:40 +00:00
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
|
|
|
|
* @since 1.19
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
public function __toString() {
|
2018-02-13 06:58:57 +00:00
|
|
|
return (string)$this->conn;
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
2016-09-20 16:17:14 +00:00
|
|
|
/**
|
|
|
|
|
* Make sure that copies do not share the same client binding handle
|
|
|
|
|
* @throws DBConnectionError
|
|
|
|
|
*/
|
|
|
|
|
public function __clone() {
|
2016-09-20 23:24:16 +00:00
|
|
|
$this->connLogger->warning(
|
2018-08-13 17:07:14 +00:00
|
|
|
"Cloning " . static::class . " is not recommended; forking connection:\n" .
|
2016-09-20 16:17:14 +00:00
|
|
|
( new RuntimeException() )->getTraceAsString()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if ( $this->isOpen() ) {
|
|
|
|
|
// Open a new connection resource without messing with the old one
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->opened = false;
|
|
|
|
|
$this->conn = false;
|
|
|
|
|
$this->trxEndCallbacks = []; // don't copy
|
2016-09-21 21:25:00 +00:00
|
|
|
$this->handleSessionLoss(); // no trx or locks anymore
|
2018-08-14 23:44:41 +00:00
|
|
|
$this->open(
|
|
|
|
|
$this->server,
|
|
|
|
|
$this->user,
|
|
|
|
|
$this->password,
|
|
|
|
|
$this->getDBname(),
|
|
|
|
|
$this->dbSchema(),
|
|
|
|
|
$this->tablePrefix()
|
|
|
|
|
);
|
2016-09-20 16:17:14 +00:00
|
|
|
$this->lastPing = microtime( true );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-16 03:14:58 +00:00
|
|
|
/**
|
|
|
|
|
* Called by serialize. Throw an exception when DB connection is serialized.
|
|
|
|
|
* This causes problems on some database engines because the connection is
|
|
|
|
|
* not restored on unserialize.
|
|
|
|
|
*/
|
|
|
|
|
public function __sleep() {
|
|
|
|
|
throw new RuntimeException( 'Database serialization may cause problems, since ' .
|
|
|
|
|
'the connection is not restored on wakeup.' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2016-09-20 16:17:14 +00:00
|
|
|
* Run a few simple sanity checks and close dangling connections
|
2016-09-16 03:14:58 +00:00
|
|
|
*/
|
|
|
|
|
public function __destruct() {
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( $this->trxLevel && $this->trxDoneWrites ) {
|
|
|
|
|
trigger_error( "Uncommitted DB writes (transaction from {$this->trxFname})." );
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$danglingWriters = $this->pendingWriteAndCallbackCallers();
|
|
|
|
|
if ( $danglingWriters ) {
|
|
|
|
|
$fnames = implode( ', ', $danglingWriters );
|
|
|
|
|
trigger_error( "DB transaction writes or callbacks still pending ($fnames)." );
|
|
|
|
|
}
|
2016-09-20 16:17:14 +00:00
|
|
|
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( $this->conn ) {
|
2016-10-17 18:21:40 +00:00
|
|
|
// Avoid connection leaks for sanity. Normally, resources close at script completion.
|
|
|
|
|
// The connection might already be closed in zend/hhvm by now, so suppress warnings.
|
2018-02-10 07:52:26 +00:00
|
|
|
Wikimedia\suppressWarnings();
|
2016-09-20 16:17:14 +00:00
|
|
|
$this->closeConnection();
|
2018-02-10 07:52:26 +00:00
|
|
|
Wikimedia\restoreWarnings();
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->conn = false;
|
|
|
|
|
$this->opened = false;
|
2016-09-20 16:17:14 +00:00
|
|
|
}
|
2016-09-16 03:14:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
2016-09-28 23:08:15 +00:00
|
|
|
|
2018-05-29 21:38:49 +00:00
|
|
|
/**
|
|
|
|
|
* @deprecated since 1.28
|
|
|
|
|
*/
|
|
|
|
|
class_alias( Database::class, 'DatabaseBase' );
|
|
|
|
|
|
2018-05-29 16:21:31 +00:00
|
|
|
/**
|
|
|
|
|
* @deprecated since 1.29
|
|
|
|
|
*/
|
2018-05-29 21:38:49 +00:00
|
|
|
class_alias( Database::class, 'Database' );
|