'DatabaseMysqlBase' => __DIR__ . '/includes/libs/rdbms/database/DatabaseMysqlBase.php',
'DatabaseMysqli' => __DIR__ . '/includes/libs/rdbms/database/DatabaseMysqli.php',
'DatabaseOracle' => __DIR__ . '/includes/db/DatabaseOracle.php',
- 'DatabasePostgres' => __DIR__ . '/includes/db/DatabasePostgres.php',
+ 'DatabasePostgres' => __DIR__ . '/includes/libs/rdbms/database/DatabasePostgres.php',
'DatabaseSqlite' => __DIR__ . '/includes/libs/rdbms/database/DatabaseSqlite.php',
'DatabaseUpdater' => __DIR__ . '/includes/installer/DatabaseUpdater.php',
'DateFormats' => __DIR__ . '/maintenance/language/date-formats.php',
'PopulateRevisionSha1' => __DIR__ . '/maintenance/populateRevisionSha1.php',
'PostgreSqlLockManager' => __DIR__ . '/includes/filebackend/lockmanager/PostgreSqlLockManager.php',
'PostgresBlob' => __DIR__ . '/includes/libs/rdbms/encasing/PostgresBlob.php',
- 'PostgresField' => __DIR__ . '/includes/db/DatabasePostgres.php',
+ 'PostgresField' => __DIR__ . '/includes/libs/rdbms/field/PostgresField.php',
'PostgresInstaller' => __DIR__ . '/includes/installer/PostgresInstaller.php',
'PostgresUpdater' => __DIR__ . '/includes/installer/PostgresUpdater.php',
'Preferences' => __DIR__ . '/includes/Preferences.php',
'SVGReader' => __DIR__ . '/includes/media/SVGMetadataExtractor.php',
'SamplingStatsdClient' => __DIR__ . '/includes/libs/SamplingStatsdClient.php',
'Sanitizer' => __DIR__ . '/includes/Sanitizer.php',
- 'SavepointPostgres' => __DIR__ . '/includes/db/DatabasePostgres.php',
+ 'SavepointPostgres' => __DIR__ . '/includes/libs/rdbms/database/utils/SavepointPostgres.php',
'ScopedCallback' => __DIR__ . '/includes/libs/ScopedCallback.php',
'ScopedLock' => __DIR__ . '/includes/filebackend/lockmanager/ScopedLock.php',
'SearchApi' => __DIR__ . '/includes/api/SearchApi.php',
* Create a cluster named 'cluster1' containing three servers:
* @code
* $wgExternalServers = [
- * 'cluster1' => [ 'srv28', 'srv29', 'srv30' ]
+ * 'cluster1' => <array in the same format as $wgDBservers>
* ];
* @endcode
*
}
} catch ( Exception $e ) {
$context = $this->context;
+ $action = $context->getRequest()->getVal( 'action', 'view' );
if (
$e instanceof DBConnectionError &&
$context->hasTitle() &&
$context->getTitle()->canExist() &&
- $context->getRequest()->getVal( 'action', 'view' ) === 'view' &&
+ in_array( $action, [ 'view', 'history' ], true ) &&
HTMLFileCache::useFileCache( $this->context, HTMLFileCache::MODE_OUTAGE )
) {
// Try to use any (even stale) file during outages...
$config = $this->context->getConfig();
# Fill in the file cache if not set already
- $useFileCache = $config->get( 'UseFileCache' );
- if ( $useFileCache && HTMLFileCache::useFileCache( $this->getContext() ) ) {
+ if ( HTMLFileCache::useFileCache( $this->getContext() ) ) {
$cache = new HTMLFileCache( $this->getTitle(), 'history' );
if ( !$cache->isCacheGood( /* Assume up to date */ ) ) {
ob_start( [ &$cache, 'saveToFileCache' ] );
* @return string Compressed text
*/
public function saveText( $text ) {
- global $wgUseFileCache;
-
- if ( !$wgUseFileCache ) {
- return false;
- }
-
if ( $this->useGzip() ) {
$text = gzencode( $text );
}
* @ingroup Cache
*/
+use MediaWiki\MediaWikiServices;
+
/**
* Page view caching in the file system.
* The only cacheable actions are "view" and "history". Also special pages
class HTMLFileCache extends FileCacheBase {
const MODE_NORMAL = 0; // normal cache mode
const MODE_OUTAGE = 1; // fallback cache for DB outages
+ const MODE_REBUILD = 2; // background cache rebuild mode
/**
* Construct an HTMLFileCache object from a Title and an action
*/
public function __construct( $title, $action ) {
parent::__construct();
+
$allowedTypes = self::cacheablePageActions();
if ( !in_array( $action, $allowedTypes ) ) {
throw new MWException( 'Invalid file cache type given.' );
/**
* Check if pages can be cached for this request/user
* @param IContextSource $context
- * @param integer $mode One of the HTMLFileCache::MODE_* constants
+ * @param integer $mode One of the HTMLFileCache::MODE_* constants (since 1.28)
* @return bool
*/
public static function useFileCache( IContextSource $context, $mode = self::MODE_NORMAL ) {
- global $wgUseFileCache, $wgDebugToolbar, $wgContLang;
+ $config = MediaWikiServices::getInstance()->getMainConfig();
- if ( !$wgUseFileCache ) {
+ if ( !$config->get( 'UseFileCache' ) && $mode !== self::MODE_REBUILD ) {
return false;
- }
- if ( $wgDebugToolbar ) {
+ } elseif ( $config->get( 'DebugToolbar' ) ) {
wfDebug( "HTML file cache skipped. \$wgDebugToolbar on\n" );
return false;
$ulang = $context->getLanguage();
// Check that there are no other sources of variation
- if ( $user->getId() || !$ulang->equals( $wgContLang ) ) {
+ if ( $user->getId() || $ulang->getCode() !== $config->get( 'LanguageCode' ) ) {
return false;
}
* @return void
*/
public function loadFromFileCache( IContextSource $context, $mode = self::MODE_NORMAL ) {
- global $wgMimeType, $wgContLang;
+ $config = MediaWikiServices::getInstance()->getMainConfig();
wfDebug( __METHOD__ . "()\n" );
$filename = $this->cachePath();
}
$context->getOutput()->sendCacheControl();
- header( "Content-Type: $wgMimeType; charset=UTF-8" );
- header( 'Content-Language: ' . $wgContLang->getHtmlCode() );
+ header( "Content-Type: {$config->get( 'MimeType' )}; charset=UTF-8" );
+ header( "Content-Language: {$config->get( 'LanguageCode' )}" );
if ( $this->useGzip() ) {
if ( wfClientAcceptsGzip() ) {
header( 'Content-Encoding: gzip' );
} else {
readfile( $filename );
}
+
$context->getOutput()->disable(); // tell $wgOut that output is taken care of
}
/**
* Save this cache object with the given text.
* Use this as an ob_start() handler.
+ *
+ * Normally this is only registed as a handler if $wgUseFileCache is on.
+ * If can be explicitly called by rebuildFileCache.php when it takes over
+ * handling file caching itself, disabling any automatic handling the the
+ * process.
+ *
* @param string $text
- * @return bool Whether $wgUseFileCache is enabled
+ * @return string|bool The annotated $text or false on error
*/
public function saveToFileCache( $text ) {
- global $wgUseFileCache;
-
- if ( !$wgUseFileCache || strlen( $text ) < 512 ) {
+ if ( strlen( $text ) < 512 ) {
// Disabled or empty/broken output (OOM and PHP errors)
return $text;
}
* @return bool Whether $wgUseFileCache is enabled
*/
public static function clearFileCache( Title $title ) {
- global $wgUseFileCache;
+ $config = MediaWikiServices::getInstance()->getMainConfig();
- if ( !$wgUseFileCache ) {
+ if ( !$config->get( 'UseFileCache' ) ) {
return false;
}
+++ /dev/null
-<?php
-/**
- * This is the Postgres database abstraction layer.
- *
- * 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
- */
-
-class PostgresField implements Field {
- private $name, $tablename, $type, $nullable, $max_length, $deferred, $deferrable, $conname,
- $has_default, $default;
-
- /**
- * @param IDatabase $db
- * @param string $table
- * @param string $field
- * @return null|PostgresField
- */
- static function fromText( $db, $table, $field ) {
- $q = <<<SQL
-SELECT
- attnotnull, attlen, conname AS conname,
- atthasdef,
- adsrc,
- COALESCE(condeferred, 'f') AS deferred,
- COALESCE(condeferrable, 'f') AS deferrable,
- CASE WHEN typname = 'int2' THEN 'smallint'
- WHEN typname = 'int4' THEN 'integer'
- WHEN typname = 'int8' THEN 'bigint'
- WHEN typname = 'bpchar' THEN 'char'
- ELSE typname END AS typname
-FROM pg_class c
-JOIN pg_namespace n ON (n.oid = c.relnamespace)
-JOIN pg_attribute a ON (a.attrelid = c.oid)
-JOIN pg_type t ON (t.oid = a.atttypid)
-LEFT JOIN pg_constraint o ON (o.conrelid = c.oid AND a.attnum = ANY(o.conkey) AND o.contype = 'f')
-LEFT JOIN pg_attrdef d on c.oid=d.adrelid and a.attnum=d.adnum
-WHERE relkind = 'r'
-AND nspname=%s
-AND relname=%s
-AND attname=%s;
-SQL;
-
- $table = $db->tableName( $table, 'raw' );
- $res = $db->query(
- sprintf( $q,
- $db->addQuotes( $db->getCoreSchema() ),
- $db->addQuotes( $table ),
- $db->addQuotes( $field )
- )
- );
- $row = $db->fetchObject( $res );
- if ( !$row ) {
- return null;
- }
- $n = new PostgresField;
- $n->type = $row->typname;
- $n->nullable = ( $row->attnotnull == 'f' );
- $n->name = $field;
- $n->tablename = $table;
- $n->max_length = $row->attlen;
- $n->deferrable = ( $row->deferrable == 't' );
- $n->deferred = ( $row->deferred == 't' );
- $n->conname = $row->conname;
- $n->has_default = ( $row->atthasdef === 't' );
- $n->default = $row->adsrc;
-
- return $n;
- }
-
- function name() {
- return $this->name;
- }
-
- function tableName() {
- return $this->tablename;
- }
-
- function type() {
- return $this->type;
- }
-
- function isNullable() {
- return $this->nullable;
- }
-
- function maxLength() {
- return $this->max_length;
- }
-
- function is_deferrable() {
- return $this->deferrable;
- }
-
- function is_deferred() {
- return $this->deferred;
- }
-
- function conname() {
- return $this->conname;
- }
-
- /**
- * @since 1.19
- * @return bool|mixed
- */
- function defaultValue() {
- if ( $this->has_default ) {
- return $this->default;
- } else {
- return false;
- }
- }
-}
-
-/**
- * Manage savepoints within a transaction
- * @ingroup Database
- * @since 1.19
- */
-class SavepointPostgres {
- /** @var DatabasePostgres Establish a savepoint within a transaction */
- protected $dbw;
- protected $id;
- protected $didbegin;
-
- /**
- * @param IDatabase $dbw
- * @param int $id
- */
- public function __construct( $dbw, $id ) {
- $this->dbw = $dbw;
- $this->id = $id;
- $this->didbegin = false;
- /* If we are not in a transaction, we need to be for savepoint trickery */
- if ( !$dbw->trxLevel() ) {
- $dbw->begin( "FOR SAVEPOINT", DatabasePostgres::TRANSACTION_INTERNAL );
- $this->didbegin = true;
- }
- }
-
- public function __destruct() {
- if ( $this->didbegin ) {
- $this->dbw->rollback();
- $this->didbegin = false;
- }
- }
-
- public function commit() {
- if ( $this->didbegin ) {
- $this->dbw->commit();
- $this->didbegin = false;
- }
- }
-
- protected function query( $keyword, $msg_ok, $msg_failed ) {
- if ( $this->dbw->doQuery( $keyword . " " . $this->id ) !== false ) {
- } else {
- wfDebug( sprintf( $msg_failed, $this->id ) );
- }
- }
-
- public function savepoint() {
- $this->query( "SAVEPOINT",
- "Transaction state: savepoint \"%s\" established.\n",
- "Transaction state: establishment of savepoint \"%s\" FAILED.\n"
- );
- }
-
- public function release() {
- $this->query( "RELEASE",
- "Transaction state: savepoint \"%s\" released.\n",
- "Transaction state: release of savepoint \"%s\" FAILED.\n"
- );
- }
-
- public function rollback() {
- $this->query( "ROLLBACK TO",
- "Transaction state: savepoint \"%s\" rolled back.\n",
- "Transaction state: rollback of savepoint \"%s\" FAILED.\n"
- );
- }
-
- public function __toString() {
- return (string)$this->id;
- }
-}
-
-/**
- * @ingroup Database
- */
-class DatabasePostgres extends DatabaseBase {
- /** @var resource */
- protected $mLastResult = null;
-
- /** @var int The number of rows affected as an integer */
- protected $mAffectedRows = null;
-
- /** @var int */
- private $mInsertId = null;
-
- /** @var float|string */
- private $numericVersion = null;
-
- /** @var string Connect string to open a PostgreSQL connection */
- private $connectString;
-
- /** @var string */
- private $mCoreSchema;
-
- function getType() {
- return 'postgres';
- }
-
- function implicitGroupby() {
- return false;
- }
-
- function implicitOrderby() {
- return false;
- }
-
- function hasConstraint( $name ) {
- $sql = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n " .
- "WHERE c.connamespace = n.oid AND conname = '" .
- pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" .
- pg_escape_string( $this->mConn, $this->getCoreSchema() ) . "'";
- $res = $this->doQuery( $sql );
-
- return $this->numRows( $res );
- }
-
- /**
- * Usually aborts on failure
- * @param string $server
- * @param string $user
- * @param string $password
- * @param string $dbName
- * @throws DBConnectionError|Exception
- * @return resource|bool|null
- */
- function open( $server, $user, $password, $dbName ) {
- # Test for Postgres support, to avoid suppressed fatal error
- if ( !function_exists( 'pg_connect' ) ) {
- throw new DBConnectionError(
- $this,
- "Postgres functions missing, have you compiled PHP with the --with-pgsql\n" .
- "option? (Note: if you recently installed PHP, you may need to restart your\n" .
- "webserver and database)\n"
- );
- }
-
- global $wgDBport;
-
- if ( !strlen( $user ) ) { # e.g. the class is being loaded
- return null;
- }
-
- $this->mServer = $server;
- $port = $wgDBport;
- $this->mUser = $user;
- $this->mPassword = $password;
- $this->mDBname = $dbName;
-
- $connectVars = [
- 'dbname' => $dbName,
- 'user' => $user,
- 'password' => $password
- ];
- if ( $server != false && $server != '' ) {
- $connectVars['host'] = $server;
- }
- if ( $port != false && $port != '' ) {
- $connectVars['port'] = $port;
- }
- if ( $this->mFlags & DBO_SSL ) {
- $connectVars['sslmode'] = 1;
- }
-
- $this->connectString = $this->makeConnectionString( $connectVars, PGSQL_CONNECT_FORCE_NEW );
- $this->close();
- $this->installErrorHandler();
-
- try {
- $this->mConn = pg_connect( $this->connectString );
- } catch ( Exception $ex ) {
- $this->restoreErrorHandler();
- throw $ex;
- }
-
- $phpError = $this->restoreErrorHandler();
-
- if ( !$this->mConn ) {
- wfDebug( "DB connection error\n" );
- wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " .
- substr( $password, 0, 3 ) . "...\n" );
- wfDebug( $this->lastError() . "\n" );
- throw new DBConnectionError( $this, str_replace( "\n", ' ', $phpError ) );
- }
-
- $this->mOpened = true;
-
- global $wgCommandLineMode;
- # If called from the command-line (e.g. importDump), only show errors
- if ( $wgCommandLineMode ) {
- $this->doQuery( "SET client_min_messages = 'ERROR'" );
- }
-
- $this->query( "SET client_encoding='UTF8'", __METHOD__ );
- $this->query( "SET datestyle = 'ISO, YMD'", __METHOD__ );
- $this->query( "SET timezone = 'GMT'", __METHOD__ );
- $this->query( "SET standard_conforming_strings = on", __METHOD__ );
- if ( $this->getServerVersion() >= 9.0 ) {
- $this->query( "SET bytea_output = 'escape'", __METHOD__ ); // PHP bug 53127
- }
-
- global $wgDBmwschema;
- $this->determineCoreSchema( $wgDBmwschema );
-
- return $this->mConn;
- }
-
- /**
- * Postgres doesn't support selectDB in the same way MySQL does. So if the
- * DB name doesn't match the open connection, open a new one
- * @param string $db
- * @return bool
- */
- function selectDB( $db ) {
- if ( $this->mDBname !== $db ) {
- return (bool)$this->open( $this->mServer, $this->mUser, $this->mPassword, $db );
- } else {
- return true;
- }
- }
-
- function makeConnectionString( $vars ) {
- $s = '';
- foreach ( $vars as $name => $value ) {
- $s .= "$name='" . str_replace( "'", "\\'", $value ) . "' ";
- }
-
- return $s;
- }
-
- /**
- * Closes a database connection, if it is open
- * Returns success, true if already closed
- * @return bool
- */
- protected function closeConnection() {
- return pg_close( $this->mConn );
- }
-
- public function doQuery( $sql ) {
- $sql = mb_convert_encoding( $sql, 'UTF-8' );
- // Clear previously left over PQresult
- while ( $res = pg_get_result( $this->mConn ) ) {
- pg_free_result( $res );
- }
- if ( pg_send_query( $this->mConn, $sql ) === false ) {
- throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" );
- }
- $this->mLastResult = pg_get_result( $this->mConn );
- $this->mAffectedRows = null;
- if ( pg_result_error( $this->mLastResult ) ) {
- return false;
- }
-
- return $this->mLastResult;
- }
-
- protected function dumpError() {
- $diags = [
- PGSQL_DIAG_SEVERITY,
- PGSQL_DIAG_SQLSTATE,
- PGSQL_DIAG_MESSAGE_PRIMARY,
- PGSQL_DIAG_MESSAGE_DETAIL,
- PGSQL_DIAG_MESSAGE_HINT,
- PGSQL_DIAG_STATEMENT_POSITION,
- PGSQL_DIAG_INTERNAL_POSITION,
- PGSQL_DIAG_INTERNAL_QUERY,
- PGSQL_DIAG_CONTEXT,
- PGSQL_DIAG_SOURCE_FILE,
- PGSQL_DIAG_SOURCE_LINE,
- PGSQL_DIAG_SOURCE_FUNCTION
- ];
- foreach ( $diags as $d ) {
- wfDebug( sprintf( "PgSQL ERROR(%d): %s\n",
- $d, pg_result_error_field( $this->mLastResult, $d ) ) );
- }
- }
-
- function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
- if ( $tempIgnore ) {
- /* Check for constraint violation */
- if ( $errno === '23505' ) {
- parent::reportQueryError( $error, $errno, $sql, $fname, $tempIgnore );
-
- return;
- }
- }
- /* Transaction stays in the ERROR state until rolled back */
- if ( $this->mTrxLevel ) {
- $ignore = $this->ignoreErrors( true );
- $this->rollback( __METHOD__ );
- $this->ignoreErrors( $ignore );
- }
- parent::reportQueryError( $error, $errno, $sql, $fname, false );
- }
-
- function queryIgnore( $sql, $fname = __METHOD__ ) {
- return $this->query( $sql, $fname, true );
- }
-
- /**
- * @param stdClass|ResultWrapper $res
- * @throws DBUnexpectedError
- */
- function freeResult( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- MediaWiki\suppressWarnings();
- $ok = pg_free_result( $res );
- MediaWiki\restoreWarnings();
- if ( !$ok ) {
- throw new DBUnexpectedError( $this, "Unable to free Postgres result\n" );
- }
- }
-
- /**
- * @param ResultWrapper|stdClass $res
- * @return stdClass
- * @throws DBUnexpectedError
- */
- function fetchObject( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- MediaWiki\suppressWarnings();
- $row = pg_fetch_object( $res );
- MediaWiki\restoreWarnings();
- # @todo FIXME: HACK HACK HACK HACK debug
-
- # @todo hashar: not sure if the following test really trigger if the object
- # fetching failed.
- if ( pg_last_error( $this->mConn ) ) {
- throw new DBUnexpectedError(
- $this,
- 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
- );
- }
-
- return $row;
- }
-
- function fetchRow( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- MediaWiki\suppressWarnings();
- $row = pg_fetch_array( $res );
- MediaWiki\restoreWarnings();
- if ( pg_last_error( $this->mConn ) ) {
- throw new DBUnexpectedError(
- $this,
- 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
- );
- }
-
- return $row;
- }
-
- function numRows( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- MediaWiki\suppressWarnings();
- $n = pg_num_rows( $res );
- MediaWiki\restoreWarnings();
- if ( pg_last_error( $this->mConn ) ) {
- throw new DBUnexpectedError(
- $this,
- 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
- );
- }
-
- return $n;
- }
-
- function numFields( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
-
- return pg_num_fields( $res );
- }
-
- function fieldName( $res, $n ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
-
- return pg_field_name( $res, $n );
- }
-
- /**
- * Return the result of the last call to nextSequenceValue();
- * This must be called after nextSequenceValue().
- *
- * @return int|null
- */
- function insertId() {
- return $this->mInsertId;
- }
-
- /**
- * @param mixed $res
- * @param int $row
- * @return bool
- */
- function dataSeek( $res, $row ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
-
- return pg_result_seek( $res, $row );
- }
-
- function lastError() {
- if ( $this->mConn ) {
- if ( $this->mLastResult ) {
- return pg_result_error( $this->mLastResult );
- } else {
- return pg_last_error();
- }
- } else {
- return 'No database connection';
- }
- }
-
- function lastErrno() {
- if ( $this->mLastResult ) {
- return pg_result_error_field( $this->mLastResult, PGSQL_DIAG_SQLSTATE );
- } else {
- return false;
- }
- }
-
- function affectedRows() {
- if ( !is_null( $this->mAffectedRows ) ) {
- // Forced result for simulated queries
- return $this->mAffectedRows;
- }
- if ( empty( $this->mLastResult ) ) {
- return 0;
- }
-
- return pg_affected_rows( $this->mLastResult );
- }
-
- /**
- * Estimate rows in dataset
- * Returns estimated count, based on EXPLAIN output
- * This is not necessarily an accurate estimate, so use sparingly
- * Returns -1 if count cannot be found
- * Takes same arguments as Database::select()
- *
- * @param string $table
- * @param string $vars
- * @param string $conds
- * @param string $fname
- * @param array $options
- * @return int
- */
- function estimateRowCount( $table, $vars = '*', $conds = '',
- $fname = __METHOD__, $options = []
- ) {
- $options['EXPLAIN'] = true;
- $res = $this->select( $table, $vars, $conds, $fname, $options );
- $rows = -1;
- if ( $res ) {
- $row = $this->fetchRow( $res );
- $count = [];
- if ( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
- $rows = (int)$count[1];
- }
- }
-
- return $rows;
- }
-
- /**
- * Returns information about an index
- * If errors are explicitly ignored, returns NULL on failure
- *
- * @param string $table
- * @param string $index
- * @param string $fname
- * @return bool|null
- */
- function indexInfo( $table, $index, $fname = __METHOD__ ) {
- $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
- $res = $this->query( $sql, $fname );
- if ( !$res ) {
- return null;
- }
- foreach ( $res as $row ) {
- if ( $row->indexname == $this->indexName( $index ) ) {
- return $row;
- }
- }
-
- return false;
- }
-
- /**
- * Returns is of attributes used in index
- *
- * @since 1.19
- * @param string $index
- * @param bool|string $schema
- * @return array
- */
- function indexAttributes( $index, $schema = false ) {
- if ( $schema === false ) {
- $schema = $this->getCoreSchema();
- }
- /*
- * A subquery would be not needed if we didn't care about the order
- * of attributes, but we do
- */
- $sql = <<<__INDEXATTR__
-
- SELECT opcname,
- attname,
- i.indoption[s.g] as option,
- pg_am.amname
- FROM
- (SELECT generate_series(array_lower(isub.indkey,1), array_upper(isub.indkey,1)) AS g
- FROM
- pg_index isub
- JOIN pg_class cis
- ON cis.oid=isub.indexrelid
- JOIN pg_namespace ns
- ON cis.relnamespace = ns.oid
- WHERE cis.relname='$index' AND ns.nspname='$schema') AS s,
- pg_attribute,
- pg_opclass opcls,
- pg_am,
- pg_class ci
- JOIN pg_index i
- ON ci.oid=i.indexrelid
- JOIN pg_class ct
- ON ct.oid = i.indrelid
- JOIN pg_namespace n
- ON ci.relnamespace = n.oid
- WHERE
- ci.relname='$index' AND n.nspname='$schema'
- AND attrelid = ct.oid
- AND i.indkey[s.g] = attnum
- AND i.indclass[s.g] = opcls.oid
- AND pg_am.oid = opcls.opcmethod
-__INDEXATTR__;
- $res = $this->query( $sql, __METHOD__ );
- $a = [];
- if ( $res ) {
- foreach ( $res as $row ) {
- $a[] = [
- $row->attname,
- $row->opcname,
- $row->amname,
- $row->option ];
- }
- } else {
- return null;
- }
-
- return $a;
- }
-
- function indexUnique( $table, $index, $fname = __METHOD__ ) {
- $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'" .
- " AND indexdef LIKE 'CREATE UNIQUE%(" .
- $this->strencode( $this->indexName( $index ) ) .
- ")'";
- $res = $this->query( $sql, $fname );
- if ( !$res ) {
- return null;
- }
-
- return $res->numRows() > 0;
- }
-
- /**
- * Change the FOR UPDATE option as necessary based on the join conditions. Then pass
- * to the parent function to get the actual SQL text.
- *
- * In Postgres when using FOR UPDATE, only the main table and tables that are inner joined
- * can be locked. That means tables in an outer join cannot be FOR UPDATE locked. Trying to do
- * so causes a DB error. This wrapper checks which tables can be locked and adjusts it accordingly.
- *
- * MySQL uses "ORDER BY NULL" as an optimization hint, but that syntax is illegal in PostgreSQL.
- * @see DatabaseBase::selectSQLText
- */
- function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
- $options = [], $join_conds = []
- ) {
- if ( is_array( $options ) ) {
- $forUpdateKey = array_search( 'FOR UPDATE', $options, true );
- if ( $forUpdateKey !== false && $join_conds ) {
- unset( $options[$forUpdateKey] );
-
- foreach ( $join_conds as $table_cond => $join_cond ) {
- if ( 0 === preg_match( '/^(?:LEFT|RIGHT|FULL)(?: OUTER)? JOIN$/i', $join_cond[0] ) ) {
- $options['FOR UPDATE'][] = $table_cond;
- }
- }
- }
-
- if ( isset( $options['ORDER BY'] ) && $options['ORDER BY'] == 'NULL' ) {
- unset( $options['ORDER BY'] );
- }
- }
-
- return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
- }
-
- /**
- * INSERT wrapper, inserts an array into a table
- *
- * $args may be a single associative array, or an array of these with numeric keys,
- * for multi-row insert (Postgres version 8.2 and above only).
- *
- * @param string $table Name of the table to insert to.
- * @param array $args Items to insert into the table.
- * @param string $fname Name of the function, for profiling
- * @param array|string $options String or array. Valid options: IGNORE
- * @return bool Success of insert operation. IGNORE always returns true.
- */
- function insert( $table, $args, $fname = __METHOD__, $options = [] ) {
- if ( !count( $args ) ) {
- return true;
- }
-
- $table = $this->tableName( $table );
- if ( !isset( $this->numericVersion ) ) {
- $this->getServerVersion();
- }
-
- if ( !is_array( $options ) ) {
- $options = [ $options ];
- }
-
- if ( isset( $args[0] ) && is_array( $args[0] ) ) {
- $multi = true;
- $keys = array_keys( $args[0] );
- } else {
- $multi = false;
- $keys = array_keys( $args );
- }
-
- // If IGNORE is set, we use savepoints to emulate mysql's behavior
- $savepoint = null;
- if ( in_array( 'IGNORE', $options ) ) {
- $savepoint = new SavepointPostgres( $this, 'mw' );
- $olde = error_reporting( 0 );
- // For future use, we may want to track the number of actual inserts
- // Right now, insert (all writes) simply return true/false
- $numrowsinserted = 0;
- }
-
- $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
-
- if ( $multi ) {
- if ( $this->numericVersion >= 8.2 && !$savepoint ) {
- $first = true;
- foreach ( $args as $row ) {
- if ( $first ) {
- $first = false;
- } else {
- $sql .= ',';
- }
- $sql .= '(' . $this->makeList( $row ) . ')';
- }
- $res = (bool)$this->query( $sql, $fname, $savepoint );
- } else {
- $res = true;
- $origsql = $sql;
- foreach ( $args as $row ) {
- $tempsql = $origsql;
- $tempsql .= '(' . $this->makeList( $row ) . ')';
-
- if ( $savepoint ) {
- $savepoint->savepoint();
- }
-
- $tempres = (bool)$this->query( $tempsql, $fname, $savepoint );
-
- if ( $savepoint ) {
- $bar = pg_result_error( $this->mLastResult );
- if ( $bar != false ) {
- $savepoint->rollback();
- } else {
- $savepoint->release();
- $numrowsinserted++;
- }
- }
-
- // If any of them fail, we fail overall for this function call
- // Note that this will be ignored if IGNORE is set
- if ( !$tempres ) {
- $res = false;
- }
- }
- }
- } else {
- // Not multi, just a lone insert
- if ( $savepoint ) {
- $savepoint->savepoint();
- }
-
- $sql .= '(' . $this->makeList( $args ) . ')';
- $res = (bool)$this->query( $sql, $fname, $savepoint );
- if ( $savepoint ) {
- $bar = pg_result_error( $this->mLastResult );
- if ( $bar != false ) {
- $savepoint->rollback();
- } else {
- $savepoint->release();
- $numrowsinserted++;
- }
- }
- }
- if ( $savepoint ) {
- error_reporting( $olde );
- $savepoint->commit();
-
- // Set the affected row count for the whole operation
- $this->mAffectedRows = $numrowsinserted;
-
- // IGNORE always returns true
- return true;
- }
-
- return $res;
- }
-
- /**
- * INSERT SELECT wrapper
- * $varMap must be an associative array of the form [ 'dest1' => 'source1', ... ]
- * Source items may be literals rather then field names, but strings should
- * be quoted with Database::addQuotes()
- * $conds may be "*" to copy the whole table
- * srcTable may be an array of tables.
- * @todo FIXME: Implement this a little better (seperate select/insert)?
- *
- * @param string $destTable
- * @param array|string $srcTable
- * @param array $varMap
- * @param array $conds
- * @param string $fname
- * @param array $insertOptions
- * @param array $selectOptions
- * @return bool
- */
- function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
- $insertOptions = [], $selectOptions = [] ) {
- $destTable = $this->tableName( $destTable );
-
- if ( !is_array( $insertOptions ) ) {
- $insertOptions = [ $insertOptions ];
- }
-
- /*
- * If IGNORE is set, we use savepoints to emulate mysql's behavior
- * Ignore LOW PRIORITY option, since it is MySQL-specific
- */
- $savepoint = null;
- if ( in_array( 'IGNORE', $insertOptions ) ) {
- $savepoint = new SavepointPostgres( $this, 'mw' );
- $olde = error_reporting( 0 );
- $numrowsinserted = 0;
- $savepoint->savepoint();
- }
-
- if ( !is_array( $selectOptions ) ) {
- $selectOptions = [ $selectOptions ];
- }
- list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) =
- $this->makeSelectOptions( $selectOptions );
- if ( is_array( $srcTable ) ) {
- $srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
- } else {
- $srcTable = $this->tableName( $srcTable );
- }
-
- $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
- " SELECT $startOpts " . implode( ',', $varMap ) .
- " FROM $srcTable $useIndex $ignoreIndex ";
-
- if ( $conds != '*' ) {
- $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
- }
-
- $sql .= " $tailOpts";
-
- $res = (bool)$this->query( $sql, $fname, $savepoint );
- if ( $savepoint ) {
- $bar = pg_result_error( $this->mLastResult );
- if ( $bar != false ) {
- $savepoint->rollback();
- } else {
- $savepoint->release();
- $numrowsinserted++;
- }
- error_reporting( $olde );
- $savepoint->commit();
-
- // Set the affected row count for the whole operation
- $this->mAffectedRows = $numrowsinserted;
-
- // IGNORE always returns true
- return true;
- }
-
- return $res;
- }
-
- function tableName( $name, $format = 'quoted' ) {
- # Replace reserved words with better ones
- switch ( $name ) {
- case 'user':
- return $this->realTableName( 'mwuser', $format );
- case 'text':
- return $this->realTableName( 'pagecontent', $format );
- default:
- return $this->realTableName( $name, $format );
- }
- }
-
- /* Don't cheat on installer */
- function realTableName( $name, $format = 'quoted' ) {
- return parent::tableName( $name, $format );
- }
-
- /**
- * Return the next in a sequence, save the value for retrieval via insertId()
- *
- * @param string $seqName
- * @return int|null
- */
- function nextSequenceValue( $seqName ) {
- $safeseq = str_replace( "'", "''", $seqName );
- $res = $this->query( "SELECT nextval('$safeseq')" );
- $row = $this->fetchRow( $res );
- $this->mInsertId = $row[0];
-
- return $this->mInsertId;
- }
-
- /**
- * Return the current value of a sequence. Assumes it has been nextval'ed in this session.
- *
- * @param string $seqName
- * @return int
- */
- function currentSequenceValue( $seqName ) {
- $safeseq = str_replace( "'", "''", $seqName );
- $res = $this->query( "SELECT currval('$safeseq')" );
- $row = $this->fetchRow( $res );
- $currval = $row[0];
-
- return $currval;
- }
-
- # Returns the size of a text field, or -1 for "unlimited"
- function textFieldSize( $table, $field ) {
- $table = $this->tableName( $table );
- $sql = "SELECT t.typname as ftype,a.atttypmod as size
- FROM pg_class c, pg_attribute a, pg_type t
- WHERE relname='$table' AND a.attrelid=c.oid AND
- a.atttypid=t.oid and a.attname='$field'";
- $res = $this->query( $sql );
- $row = $this->fetchObject( $res );
- if ( $row->ftype == 'varchar' ) {
- $size = $row->size - 4;
- } else {
- $size = $row->size;
- }
-
- return $size;
- }
-
- function limitResult( $sql, $limit, $offset = false ) {
- return "$sql LIMIT $limit " . ( is_numeric( $offset ) ? " OFFSET {$offset} " : '' );
- }
-
- function wasDeadlock() {
- return $this->lastErrno() == '40P01';
- }
-
- function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
- $newName = $this->addIdentifierQuotes( $newName );
- $oldName = $this->addIdentifierQuotes( $oldName );
-
- return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName " .
- "(LIKE $oldName INCLUDING DEFAULTS)", $fname );
- }
-
- function listTables( $prefix = null, $fname = __METHOD__ ) {
- $eschema = $this->addQuotes( $this->getCoreSchema() );
- $result = $this->query( "SELECT tablename FROM pg_tables WHERE schemaname = $eschema", $fname );
- $endArray = [];
-
- foreach ( $result as $table ) {
- $vars = get_object_vars( $table );
- $table = array_pop( $vars );
- if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
- $endArray[] = $table;
- }
- }
-
- return $endArray;
- }
-
- function timestamp( $ts = 0 ) {
- return wfTimestamp( TS_POSTGRES, $ts );
- }
-
- /**
- * Posted by cc[plus]php[at]c2se[dot]com on 25-Mar-2009 09:12
- * to http://www.php.net/manual/en/ref.pgsql.php
- *
- * Parsing a postgres array can be a tricky problem, he's my
- * take on this, it handles multi-dimensional arrays plus
- * escaping using a nasty regexp to determine the limits of each
- * data-item.
- *
- * This should really be handled by PHP PostgreSQL module
- *
- * @since 1.19
- * @param string $text Postgreql array returned in a text form like {a,b}
- * @param string $output
- * @param int $limit
- * @param int $offset
- * @return string
- */
- function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) {
- if ( false === $limit ) {
- $limit = strlen( $text ) - 1;
- $output = [];
- }
- if ( '{}' == $text ) {
- return $output;
- }
- do {
- if ( '{' != $text[$offset] ) {
- preg_match( "/(\\{?\"([^\"\\\\]|\\\\.)*\"|[^,{}]+)+([,}]+)/",
- $text, $match, 0, $offset );
- $offset += strlen( $match[0] );
- $output[] = ( '"' != $match[1][0]
- ? $match[1]
- : stripcslashes( substr( $match[1], 1, -1 ) ) );
- if ( '},' == $match[3] ) {
- return $output;
- }
- } else {
- $offset = $this->pg_array_parse( $text, $output, $limit, $offset + 1 );
- }
- } while ( $limit > $offset );
-
- return $output;
- }
-
- /**
- * Return aggregated value function call
- * @param array $valuedata
- * @param string $valuename
- * @return array
- */
- public function aggregateValue( $valuedata, $valuename = 'value' ) {
- return $valuedata;
- }
-
- /**
- * @return string Wikitext of a link to the server software's web site
- */
- public function getSoftwareLink() {
- return '[{{int:version-db-postgres-url}} PostgreSQL]';
- }
-
- /**
- * Return current schema (executes SELECT current_schema())
- * Needs transaction
- *
- * @since 1.19
- * @return string Default schema for the current session
- */
- function getCurrentSchema() {
- $res = $this->query( "SELECT current_schema()", __METHOD__ );
- $row = $this->fetchRow( $res );
-
- return $row[0];
- }
-
- /**
- * Return list of schemas which are accessible without schema name
- * This is list does not contain magic keywords like "$user"
- * Needs transaction
- *
- * @see getSearchPath()
- * @see setSearchPath()
- * @since 1.19
- * @return array List of actual schemas for the current sesson
- */
- function getSchemas() {
- $res = $this->query( "SELECT current_schemas(false)", __METHOD__ );
- $row = $this->fetchRow( $res );
- $schemas = [];
-
- /* PHP pgsql support does not support array type, "{a,b}" string is returned */
-
- return $this->pg_array_parse( $row[0], $schemas );
- }
-
- /**
- * Return search patch for schemas
- * This is different from getSchemas() since it contain magic keywords
- * (like "$user").
- * Needs transaction
- *
- * @since 1.19
- * @return array How to search for table names schemas for the current user
- */
- function getSearchPath() {
- $res = $this->query( "SHOW search_path", __METHOD__ );
- $row = $this->fetchRow( $res );
-
- /* PostgreSQL returns SHOW values as strings */
-
- return explode( ",", $row[0] );
- }
-
- /**
- * Update search_path, values should already be sanitized
- * Values may contain magic keywords like "$user"
- * @since 1.19
- *
- * @param array $search_path List of schemas to be searched by default
- */
- function setSearchPath( $search_path ) {
- $this->query( "SET search_path = " . implode( ", ", $search_path ) );
- }
-
- /**
- * Determine default schema for MediaWiki core
- * Adjust this session schema search path if desired schema exists
- * and is not alread there.
- *
- * We need to have name of the core schema stored to be able
- * to query database metadata.
- *
- * This will be also called by the installer after the schema is created
- *
- * @since 1.19
- *
- * @param string $desiredSchema
- */
- function determineCoreSchema( $desiredSchema ) {
- $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
- if ( $this->schemaExists( $desiredSchema ) ) {
- if ( in_array( $desiredSchema, $this->getSchemas() ) ) {
- $this->mCoreSchema = $desiredSchema;
- wfDebug( "Schema \"" . $desiredSchema . "\" already in the search path\n" );
- } else {
- /**
- * Prepend our schema (e.g. 'mediawiki') in front
- * of the search path
- * Fixes bug 15816
- */
- $search_path = $this->getSearchPath();
- array_unshift( $search_path,
- $this->addIdentifierQuotes( $desiredSchema ) );
- $this->setSearchPath( $search_path );
- $this->mCoreSchema = $desiredSchema;
- wfDebug( "Schema \"" . $desiredSchema . "\" added to the search path\n" );
- }
- } else {
- $this->mCoreSchema = $this->getCurrentSchema();
- wfDebug( "Schema \"" . $desiredSchema . "\" not found, using current \"" .
- $this->mCoreSchema . "\"\n" );
- }
- /* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */
- $this->commit( __METHOD__ );
- }
-
- /**
- * Return schema name fore core MediaWiki tables
- *
- * @since 1.19
- * @return string Core schema name
- */
- function getCoreSchema() {
- return $this->mCoreSchema;
- }
-
- /**
- * @return string Version information from the database
- */
- function getServerVersion() {
- if ( !isset( $this->numericVersion ) ) {
- $versionInfo = pg_version( $this->mConn );
- if ( version_compare( $versionInfo['client'], '7.4.0', 'lt' ) ) {
- // Old client, abort install
- $this->numericVersion = '7.3 or earlier';
- } elseif ( isset( $versionInfo['server'] ) ) {
- // Normal client
- $this->numericVersion = $versionInfo['server'];
- } else {
- // Bug 16937: broken pgsql extension from PHP<5.3
- $this->numericVersion = pg_parameter_status( $this->mConn, 'server_version' );
- }
- }
-
- return $this->numericVersion;
- }
-
- /**
- * Query whether a given relation exists (in the given schema, or the
- * default mw one if not given)
- * @param string $table
- * @param array|string $types
- * @param bool|string $schema
- * @return bool
- */
- function relationExists( $table, $types, $schema = false ) {
- if ( !is_array( $types ) ) {
- $types = [ $types ];
- }
- if ( !$schema ) {
- $schema = $this->getCoreSchema();
- }
- $table = $this->realTableName( $table, 'raw' );
- $etable = $this->addQuotes( $table );
- $eschema = $this->addQuotes( $schema );
- $sql = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
- . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
- . "AND c.relkind IN ('" . implode( "','", $types ) . "')";
- $res = $this->query( $sql );
- $count = $res ? $res->numRows() : 0;
-
- return (bool)$count;
- }
-
- /**
- * For backward compatibility, this function checks both tables and
- * views.
- * @param string $table
- * @param string $fname
- * @param bool|string $schema
- * @return bool
- */
- function tableExists( $table, $fname = __METHOD__, $schema = false ) {
- return $this->relationExists( $table, [ 'r', 'v' ], $schema );
- }
-
- function sequenceExists( $sequence, $schema = false ) {
- return $this->relationExists( $sequence, 'S', $schema );
- }
-
- function triggerExists( $table, $trigger ) {
- $q = <<<SQL
- SELECT 1 FROM pg_class, pg_namespace, pg_trigger
- WHERE relnamespace=pg_namespace.oid AND relkind='r'
- AND tgrelid=pg_class.oid
- AND nspname=%s AND relname=%s AND tgname=%s
-SQL;
- $res = $this->query(
- sprintf(
- $q,
- $this->addQuotes( $this->getCoreSchema() ),
- $this->addQuotes( $table ),
- $this->addQuotes( $trigger )
- )
- );
- if ( !$res ) {
- return null;
- }
- $rows = $res->numRows();
-
- return $rows;
- }
-
- function ruleExists( $table, $rule ) {
- $exists = $this->selectField( 'pg_rules', 'rulename',
- [
- 'rulename' => $rule,
- 'tablename' => $table,
- 'schemaname' => $this->getCoreSchema()
- ]
- );
-
- return $exists === $rule;
- }
-
- function constraintExists( $table, $constraint ) {
- $sql = sprintf( "SELECT 1 FROM information_schema.table_constraints " .
- "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
- $this->addQuotes( $this->getCoreSchema() ),
- $this->addQuotes( $table ),
- $this->addQuotes( $constraint )
- );
- $res = $this->query( $sql );
- if ( !$res ) {
- return null;
- }
- $rows = $res->numRows();
-
- return $rows;
- }
-
- /**
- * Query whether a given schema exists. Returns true if it does, false if it doesn't.
- * @param string $schema
- * @return bool
- */
- function schemaExists( $schema ) {
- $exists = $this->selectField( '"pg_catalog"."pg_namespace"', 1,
- [ 'nspname' => $schema ], __METHOD__ );
-
- return (bool)$exists;
- }
-
- /**
- * Returns true if a given role (i.e. user) exists, false otherwise.
- * @param string $roleName
- * @return bool
- */
- function roleExists( $roleName ) {
- $exists = $this->selectField( '"pg_catalog"."pg_roles"', 1,
- [ 'rolname' => $roleName ], __METHOD__ );
-
- return (bool)$exists;
- }
-
- /**
- * @var string $table
- * @var string $field
- * @return PostgresField|null
- */
- function fieldInfo( $table, $field ) {
- return PostgresField::fromText( $this, $table, $field );
- }
-
- /**
- * pg_field_type() wrapper
- * @param ResultWrapper|resource $res ResultWrapper or PostgreSQL query result resource
- * @param int $index Field number, starting from 0
- * @return string
- */
- function fieldType( $res, $index ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
-
- return pg_field_type( $res, $index );
- }
-
- /**
- * @param string $b
- * @return Blob
- */
- function encodeBlob( $b ) {
- return new PostgresBlob( pg_escape_bytea( $b ) );
- }
-
- function decodeBlob( $b ) {
- if ( $b instanceof PostgresBlob ) {
- $b = $b->fetch();
- } elseif ( $b instanceof Blob ) {
- return $b->fetch();
- }
-
- return pg_unescape_bytea( $b );
- }
-
- function strencode( $s ) {
- // Should not be called by us
-
- return pg_escape_string( $this->mConn, $s );
- }
-
- /**
- * @param null|bool|Blob $s
- * @return int|string
- */
- function addQuotes( $s ) {
- if ( is_null( $s ) ) {
- return 'NULL';
- } elseif ( is_bool( $s ) ) {
- return intval( $s );
- } elseif ( $s instanceof Blob ) {
- if ( $s instanceof PostgresBlob ) {
- $s = $s->fetch();
- } else {
- $s = pg_escape_bytea( $this->mConn, $s->fetch() );
- }
- return "'$s'";
- }
-
- return "'" . pg_escape_string( $this->mConn, $s ) . "'";
- }
-
- /**
- * Postgres specific version of replaceVars.
- * Calls the parent version in Database.php
- *
- * @param string $ins SQL string, read from a stream (usually tables.sql)
- * @return string SQL string
- */
- protected function replaceVars( $ins ) {
- $ins = parent::replaceVars( $ins );
-
- if ( $this->numericVersion >= 8.3 ) {
- // Thanks for not providing backwards-compatibility, 8.3
- $ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins );
- }
-
- if ( $this->numericVersion <= 8.1 ) { // Our minimum version
- $ins = str_replace( 'USING gin', 'USING gist', $ins );
- }
-
- return $ins;
- }
-
- /**
- * Various select options
- *
- * @param array $options An associative array of options to be turned into
- * an SQL query, valid keys are listed in the function.
- * @return array
- */
- function makeSelectOptions( $options ) {
- $preLimitTail = $postLimitTail = '';
- $startOpts = $useIndex = $ignoreIndex = '';
-
- $noKeyOptions = [];
- foreach ( $options as $key => $option ) {
- if ( is_numeric( $key ) ) {
- $noKeyOptions[$option] = true;
- }
- }
-
- $preLimitTail .= $this->makeGroupByWithHaving( $options );
-
- $preLimitTail .= $this->makeOrderBy( $options );
-
- // if ( isset( $options['LIMIT'] ) ) {
- // $tailOpts .= $this->limitResult( '', $options['LIMIT'],
- // isset( $options['OFFSET'] ) ? $options['OFFSET']
- // : false );
- // }
-
- if ( isset( $options['FOR UPDATE'] ) ) {
- $postLimitTail .= ' FOR UPDATE OF ' .
- implode( ', ', array_map( [ &$this, 'tableName' ], $options['FOR UPDATE'] ) );
- } elseif ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
- $postLimitTail .= ' FOR UPDATE';
- }
-
- if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
- $startOpts .= 'DISTINCT';
- }
-
- return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
- }
-
- function getDBname() {
- return $this->mDBname;
- }
-
- function getServer() {
- return $this->mServer;
- }
-
- function buildConcat( $stringList ) {
- return implode( ' || ', $stringList );
- }
-
- public function buildGroupConcatField(
- $delimiter, $table, $field, $conds = '', $options = [], $join_conds = []
- ) {
- $fld = "array_to_string(array_agg($field)," . $this->addQuotes( $delimiter ) . ')';
-
- return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
- }
-
- /**
- * @param string $field Field or column to cast
- * @return string
- * @since 1.28
- */
- public function buildStringCast( $field ) {
- return $field . '::text';
- }
-
- public function streamStatementEnd( &$sql, &$newLine ) {
- # Allow dollar quoting for function declarations
- if ( substr( $newLine, 0, 4 ) == '$mw$' ) {
- if ( $this->delimiter ) {
- $this->delimiter = false;
- } else {
- $this->delimiter = ';';
- }
- }
-
- return parent::streamStatementEnd( $sql, $newLine );
- }
-
- /**
- * Check to see if a named lock is available. This is non-blocking.
- * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
- *
- * @param string $lockName Name of lock to poll
- * @param string $method Name of method calling us
- * @return bool
- * @since 1.20
- */
- public function lockIsFree( $lockName, $method ) {
- $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
- $result = $this->query( "SELECT (CASE(pg_try_advisory_lock($key))
- WHEN 'f' THEN 'f' ELSE pg_advisory_unlock($key) END) AS lockstatus", $method );
- $row = $this->fetchObject( $result );
-
- return ( $row->lockstatus === 't' );
- }
-
- /**
- * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
- * @param string $lockName
- * @param string $method
- * @param int $timeout
- * @return bool
- */
- public function lock( $lockName, $method, $timeout = 5 ) {
- $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
- $loop = new WaitConditionLoop(
- function () use ( $lockName, $key, $timeout, $method ) {
- $res = $this->query( "SELECT pg_try_advisory_lock($key) AS lockstatus", $method );
- $row = $this->fetchObject( $res );
- if ( $row->lockstatus === 't' ) {
- parent::lock( $lockName, $method, $timeout ); // record
- return true;
- }
-
- return WaitConditionLoop::CONDITION_CONTINUE;
- },
- $timeout
- );
-
- return ( $loop->invoke() === $loop::CONDITION_REACHED );
- }
-
- /**
- * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKSFROM
- * PG DOCS: http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
- * @param string $lockName
- * @param string $method
- * @return bool
- */
- public function unlock( $lockName, $method ) {
- $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
- $result = $this->query( "SELECT pg_advisory_unlock($key) as lockstatus", $method );
- $row = $this->fetchObject( $result );
-
- if ( $row->lockstatus === 't' ) {
- parent::unlock( $lockName, $method ); // record
- return true;
- }
-
- wfDebug( __METHOD__ . " failed to release lock\n" );
-
- return false;
- }
-
- /**
- * @param string $lockName
- * @return string Integer
- */
- private function bigintFromLockName( $lockName ) {
- return Wikimedia\base_convert( substr( sha1( $lockName ), 0, 15 ), 16, 10 );
- }
-} // end DatabasePostgres class
foreach ( $mainConfig->get( 'DBservers' ) as $i => $server ) {
if ( $server['type'] === 'sqlite' ) {
$server += [ 'dbDirectory' => $mainConfig->get( 'SQLiteDataDir' ) ];
+ } elseif ( $server['type'] === 'postgres' ) {
+ $server += [ 'port' => $mainConfig->get( 'DBport' ) ];
}
$lbConf['servers'][$i] = $server + [
'schema' => $mainConfig->get( 'DBmwschema' ),
];
if ( $server['type'] === 'sqlite' ) {
$server[ 'dbDirectory'] = $mainConfig->get( 'SQLiteDataDir' );
+ } elseif ( $server['type'] === 'postgres' ) {
+ $server['port'] = $mainConfig->get( 'DBport' );
}
$lbConf['servers'] = [ $server ];
}
- if ( !isset( $lbConf['externalServers'] ) ) {
- $lbConf['externalServers'] = $mainConfig->get( 'ExternalServers' );
+ if ( !isset( $lbConf['externalClusters'] ) ) {
+ $lbConf['externalClusters'] = $mainConfig->get( 'ExternalServers' );
}
} elseif ( $lbConf['class'] === 'LBFactoryMulti' ) {
if ( isset( $lbConf['serverTemplate'] ) ) {
protected $trxProfiler;
/**
- * Constructor.
- *
- * FIXME: It is possible to construct a Database object with no associated
- * connection object, by specifying no parameters to __construct(). This
- * feature is deprecated and should be removed.
+ * Constructor and database handle and attempt to connect to the DB server
*
* IDatabase classes should not be constructed directly in external
- * code. DatabaseBase::factory() should be used instead.
+ * code. Database::factory() should be used instead.
*
- * @param array $params Parameters passed from DatabaseBase::factory()
+ * @param array $params Parameters passed from Database::factory()
*/
function __construct( array $params ) {
$server = $params['host'];
if ( $user ) {
$this->open( $server, $user, $password, $dbName );
+ } elseif ( $this->requiresDatabaseUser() ) {
+ throw new InvalidArgumentException( "No database user provided." );
}
+ // Set the domain object after open() sets the relevant fields
$this->currentDomain = ( $this->mDBname != '' )
? new DatabaseDomain( $this->mDBname, null, $this->mTablePrefix )
: DatabaseDomain::newUnspecified();
}
/**
- * Given a DB type, construct the name of the appropriate child class of
- * IDatabase. This is designed to replace all of the manual stuff like:
- * $class = 'Database' . ucfirst( strtolower( $dbType ) );
- * as well as validate against the canonical list of DB types we have
- *
- * This factory function is mostly useful for when you need to connect to a
- * database other than the MediaWiki default (such as for external auth,
- * an extension, et cetera). Do not use this to connect to the MediaWiki
- * database. Example uses in core:
- * @see LoadBalancer::reallyOpenConnection()
- * @see ForeignDBRepo::getMasterDB()
- * @see WebInstallerDBConnect::execute()
- *
- * @since 1.18
- *
- * @param string $dbType A possible DB type
- * @param array $p An array of options to pass to the constructor.
- * Valid options are: host, user, password, dbname, flags, tablePrefix, schema, driver
- * @return IDatabase|null If the database driver or extension cannot be found
+ * Construct a Database subclass instance given a database type and parameters
+ *
+ * This also connects to the database immediately upon object construction
+ *
+ * @param string $dbType A possible DB type (sqlite, mysql, postgres)
+ * @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.
+ * - driver: Optional name of a specific DB client driver. For MySQL, there is the old
+ * 'mysql' driver and the newer 'mysqli' driver.
+ * - 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.
+ * - 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.
+ * @return Database|null If the database driver or extension cannot be found
* @throws InvalidArgumentException If the database driver or extension cannot be found
+ * @since 1.18
*/
final public static function factory( $dbType, $p = [] ) {
- $canonicalDBTypes = [
+ static $canonicalDBTypes = [
'mysql' => [ 'mysqli', 'mysql' ],
'postgres' => [],
'sqlite' => [],
}
/**
- * Create a log context to pass to PSR logging functions.
+ * Create a log context to pass to PSR-3 logger functions.
*
* @param array $extras Additional data to add to context
* @return array
$this->tableAliases = $aliases;
}
+ /**
+ * @return bool Whether a DB user is required to access the DB
+ * @since 1.28
+ */
+ protected function requiresDatabaseUser() {
+ return true;
+ }
+
/**
* @since 1.19
* @return string
* @ingroup Database
*/
abstract class DatabaseBase extends Database {
- /**
- * Boolean, controls output of large amounts of debug information.
- * @param bool|null $debug
- * - true to enable debugging
- * - false to disable debugging
- * - omitted or null to do nothing
- *
- * @return bool Previous value of the flag
- * @deprecated since 1.28; use setFlag()
- */
- public function debug( $debug = null ) {
- $res = $this->getFlag( DBO_DEBUG );
- if ( $debug !== null ) {
- $debug ? $this->setFlag( DBO_DEBUG ) : $this->clearFlag( DBO_DEBUG );
- }
-
- return $res;
- }
-
/**
* Get search engine class. All subclasses of this need to implement this
* if they wish to use searching.
--- /dev/null
+<?php
+/**
+ * This is the Postgres database abstraction layer.
+ *
+ * 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
+ */
+
+/**
+ * @ingroup Database
+ */
+class DatabasePostgres extends DatabaseBase {
+ /** @var int|bool */
+ protected $port;
+
+ /** @var resource */
+ protected $mLastResult = null;
+ /** @var int The number of rows affected as an integer */
+ protected $mAffectedRows = null;
+
+ /** @var int */
+ private $mInsertId = null;
+ /** @var float|string */
+ private $numericVersion = null;
+ /** @var string Connect string to open a PostgreSQL connection */
+ private $connectString;
+ /** @var string */
+ private $mCoreSchema;
+
+ public function __construct( array $params ) {
+ $this->port = isset( $params['port'] ) ? $params['port'] : false;
+ parent::__construct( $params );
+ }
+
+ function getType() {
+ return 'postgres';
+ }
+
+ function implicitGroupby() {
+ return false;
+ }
+
+ function implicitOrderby() {
+ return false;
+ }
+
+ function hasConstraint( $name ) {
+ $sql = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n " .
+ "WHERE c.connamespace = n.oid AND conname = '" .
+ pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" .
+ pg_escape_string( $this->mConn, $this->getCoreSchema() ) . "'";
+ $res = $this->doQuery( $sql );
+
+ return $this->numRows( $res );
+ }
+
+ /**
+ * Usually aborts on failure
+ * @param string $server
+ * @param string $user
+ * @param string $password
+ * @param string $dbName
+ * @throws DBConnectionError|Exception
+ * @return resource|bool|null
+ */
+ function open( $server, $user, $password, $dbName ) {
+ # Test for Postgres support, to avoid suppressed fatal error
+ if ( !function_exists( 'pg_connect' ) ) {
+ throw new DBConnectionError(
+ $this,
+ "Postgres functions missing, have you compiled PHP with the --with-pgsql\n" .
+ "option? (Note: if you recently installed PHP, you may need to restart your\n" .
+ "webserver and database)\n"
+ );
+ }
+
+ if ( !strlen( $user ) ) { # e.g. the class is being loaded
+ return null;
+ }
+
+ $this->mServer = $server;
+ $this->mUser = $user;
+ $this->mPassword = $password;
+ $this->mDBname = $dbName;
+
+ $connectVars = [
+ 'dbname' => $dbName,
+ 'user' => $user,
+ 'password' => $password
+ ];
+ if ( $server != false && $server != '' ) {
+ $connectVars['host'] = $server;
+ }
+ if ( (int)$this->port > 0 ) {
+ $connectVars['port'] = (int)$this->port;
+ }
+ if ( $this->mFlags & DBO_SSL ) {
+ $connectVars['sslmode'] = 1;
+ }
+
+ $this->connectString = $this->makeConnectionString( $connectVars );
+ $this->close();
+ $this->installErrorHandler();
+
+ try {
+ $this->mConn = pg_connect( $this->connectString );
+ } catch ( Exception $ex ) {
+ $this->restoreErrorHandler();
+ throw $ex;
+ }
+
+ $phpError = $this->restoreErrorHandler();
+
+ if ( !$this->mConn ) {
+ $this->queryLogger->debug( "DB connection error\n" );
+ $this->queryLogger->debug(
+ "Server: $server, Database: $dbName, User: $user, Password: " .
+ substr( $password, 0, 3 ) . "...\n" );
+ $this->queryLogger->debug( $this->lastError() . "\n" );
+ throw new DBConnectionError( $this, str_replace( "\n", ' ', $phpError ) );
+ }
+
+ $this->mOpened = true;
+
+ # If called from the command-line (e.g. importDump), only show errors
+ if ( $this->cliMode ) {
+ $this->doQuery( "SET client_min_messages = 'ERROR'" );
+ }
+
+ $this->query( "SET client_encoding='UTF8'", __METHOD__ );
+ $this->query( "SET datestyle = 'ISO, YMD'", __METHOD__ );
+ $this->query( "SET timezone = 'GMT'", __METHOD__ );
+ $this->query( "SET standard_conforming_strings = on", __METHOD__ );
+ if ( $this->getServerVersion() >= 9.0 ) {
+ $this->query( "SET bytea_output = 'escape'", __METHOD__ ); // PHP bug 53127
+ }
+
+ $this->determineCoreSchema( $this->mSchema );
+
+ return $this->mConn;
+ }
+
+ /**
+ * Postgres doesn't support selectDB in the same way MySQL does. So if the
+ * DB name doesn't match the open connection, open a new one
+ * @param string $db
+ * @return bool
+ */
+ function selectDB( $db ) {
+ if ( $this->mDBname !== $db ) {
+ return (bool)$this->open( $this->mServer, $this->mUser, $this->mPassword, $db );
+ } else {
+ return true;
+ }
+ }
+
+ function makeConnectionString( $vars ) {
+ $s = '';
+ foreach ( $vars as $name => $value ) {
+ $s .= "$name='" . str_replace( "'", "\\'", $value ) . "' ";
+ }
+
+ return $s;
+ }
+
+ /**
+ * Closes a database connection, if it is open
+ * Returns success, true if already closed
+ * @return bool
+ */
+ protected function closeConnection() {
+ return pg_close( $this->mConn );
+ }
+
+ public function doQuery( $sql ) {
+ $sql = mb_convert_encoding( $sql, 'UTF-8' );
+ // Clear previously left over PQresult
+ while ( $res = pg_get_result( $this->mConn ) ) {
+ pg_free_result( $res );
+ }
+ if ( pg_send_query( $this->mConn, $sql ) === false ) {
+ throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" );
+ }
+ $this->mLastResult = pg_get_result( $this->mConn );
+ $this->mAffectedRows = null;
+ if ( pg_result_error( $this->mLastResult ) ) {
+ return false;
+ }
+
+ return $this->mLastResult;
+ }
+
+ protected function dumpError() {
+ $diags = [
+ PGSQL_DIAG_SEVERITY,
+ PGSQL_DIAG_SQLSTATE,
+ PGSQL_DIAG_MESSAGE_PRIMARY,
+ PGSQL_DIAG_MESSAGE_DETAIL,
+ PGSQL_DIAG_MESSAGE_HINT,
+ PGSQL_DIAG_STATEMENT_POSITION,
+ PGSQL_DIAG_INTERNAL_POSITION,
+ PGSQL_DIAG_INTERNAL_QUERY,
+ PGSQL_DIAG_CONTEXT,
+ PGSQL_DIAG_SOURCE_FILE,
+ PGSQL_DIAG_SOURCE_LINE,
+ PGSQL_DIAG_SOURCE_FUNCTION
+ ];
+ foreach ( $diags as $d ) {
+ $this->queryLogger->debug( sprintf( "PgSQL ERROR(%d): %s\n",
+ $d, pg_result_error_field( $this->mLastResult, $d ) ) );
+ }
+ }
+
+ function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
+ if ( $tempIgnore ) {
+ /* Check for constraint violation */
+ if ( $errno === '23505' ) {
+ parent::reportQueryError( $error, $errno, $sql, $fname, $tempIgnore );
+
+ return;
+ }
+ }
+ /* Transaction stays in the ERROR state until rolled back */
+ if ( $this->mTrxLevel ) {
+ $ignore = $this->ignoreErrors( true );
+ $this->rollback( __METHOD__ );
+ $this->ignoreErrors( $ignore );
+ }
+ parent::reportQueryError( $error, $errno, $sql, $fname, false );
+ }
+
+ function queryIgnore( $sql, $fname = __METHOD__ ) {
+ return $this->query( $sql, $fname, true );
+ }
+
+ /**
+ * @param stdClass|ResultWrapper $res
+ * @throws DBUnexpectedError
+ */
+ function freeResult( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ MediaWiki\suppressWarnings();
+ $ok = pg_free_result( $res );
+ MediaWiki\restoreWarnings();
+ if ( !$ok ) {
+ throw new DBUnexpectedError( $this, "Unable to free Postgres result\n" );
+ }
+ }
+
+ /**
+ * @param ResultWrapper|stdClass $res
+ * @return stdClass
+ * @throws DBUnexpectedError
+ */
+ function fetchObject( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ MediaWiki\suppressWarnings();
+ $row = pg_fetch_object( $res );
+ MediaWiki\restoreWarnings();
+ # @todo FIXME: HACK HACK HACK HACK debug
+
+ # @todo hashar: not sure if the following test really trigger if the object
+ # fetching failed.
+ if ( pg_last_error( $this->mConn ) ) {
+ throw new DBUnexpectedError(
+ $this,
+ 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
+ );
+ }
+
+ return $row;
+ }
+
+ function fetchRow( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ MediaWiki\suppressWarnings();
+ $row = pg_fetch_array( $res );
+ MediaWiki\restoreWarnings();
+ if ( pg_last_error( $this->mConn ) ) {
+ throw new DBUnexpectedError(
+ $this,
+ 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
+ );
+ }
+
+ return $row;
+ }
+
+ function numRows( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ MediaWiki\suppressWarnings();
+ $n = pg_num_rows( $res );
+ MediaWiki\restoreWarnings();
+ if ( pg_last_error( $this->mConn ) ) {
+ throw new DBUnexpectedError(
+ $this,
+ 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
+ );
+ }
+
+ return $n;
+ }
+
+ function numFields( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+
+ return pg_num_fields( $res );
+ }
+
+ function fieldName( $res, $n ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+
+ return pg_field_name( $res, $n );
+ }
+
+ /**
+ * Return the result of the last call to nextSequenceValue();
+ * This must be called after nextSequenceValue().
+ *
+ * @return int|null
+ */
+ function insertId() {
+ return $this->mInsertId;
+ }
+
+ /**
+ * @param mixed $res
+ * @param int $row
+ * @return bool
+ */
+ function dataSeek( $res, $row ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+
+ return pg_result_seek( $res, $row );
+ }
+
+ function lastError() {
+ if ( $this->mConn ) {
+ if ( $this->mLastResult ) {
+ return pg_result_error( $this->mLastResult );
+ } else {
+ return pg_last_error();
+ }
+ } else {
+ return 'No database connection';
+ }
+ }
+
+ function lastErrno() {
+ if ( $this->mLastResult ) {
+ return pg_result_error_field( $this->mLastResult, PGSQL_DIAG_SQLSTATE );
+ } else {
+ return false;
+ }
+ }
+
+ function affectedRows() {
+ if ( !is_null( $this->mAffectedRows ) ) {
+ // Forced result for simulated queries
+ return $this->mAffectedRows;
+ }
+ if ( empty( $this->mLastResult ) ) {
+ return 0;
+ }
+
+ return pg_affected_rows( $this->mLastResult );
+ }
+
+ /**
+ * Estimate rows in dataset
+ * Returns estimated count, based on EXPLAIN output
+ * This is not necessarily an accurate estimate, so use sparingly
+ * Returns -1 if count cannot be found
+ * Takes same arguments as Database::select()
+ *
+ * @param string $table
+ * @param string $vars
+ * @param string $conds
+ * @param string $fname
+ * @param array $options
+ * @return int
+ */
+ function estimateRowCount( $table, $vars = '*', $conds = '',
+ $fname = __METHOD__, $options = []
+ ) {
+ $options['EXPLAIN'] = true;
+ $res = $this->select( $table, $vars, $conds, $fname, $options );
+ $rows = -1;
+ if ( $res ) {
+ $row = $this->fetchRow( $res );
+ $count = [];
+ if ( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
+ $rows = (int)$count[1];
+ }
+ }
+
+ return $rows;
+ }
+
+ /**
+ * Returns information about an index
+ * If errors are explicitly ignored, returns NULL on failure
+ *
+ * @param string $table
+ * @param string $index
+ * @param string $fname
+ * @return bool|null
+ */
+ function indexInfo( $table, $index, $fname = __METHOD__ ) {
+ $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
+ $res = $this->query( $sql, $fname );
+ if ( !$res ) {
+ return null;
+ }
+ foreach ( $res as $row ) {
+ if ( $row->indexname == $this->indexName( $index ) ) {
+ return $row;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns is of attributes used in index
+ *
+ * @since 1.19
+ * @param string $index
+ * @param bool|string $schema
+ * @return array
+ */
+ function indexAttributes( $index, $schema = false ) {
+ if ( $schema === false ) {
+ $schema = $this->getCoreSchema();
+ }
+ /*
+ * A subquery would be not needed if we didn't care about the order
+ * of attributes, but we do
+ */
+ $sql = <<<__INDEXATTR__
+
+ SELECT opcname,
+ attname,
+ i.indoption[s.g] as option,
+ pg_am.amname
+ FROM
+ (SELECT generate_series(array_lower(isub.indkey,1), array_upper(isub.indkey,1)) AS g
+ FROM
+ pg_index isub
+ JOIN pg_class cis
+ ON cis.oid=isub.indexrelid
+ JOIN pg_namespace ns
+ ON cis.relnamespace = ns.oid
+ WHERE cis.relname='$index' AND ns.nspname='$schema') AS s,
+ pg_attribute,
+ pg_opclass opcls,
+ pg_am,
+ pg_class ci
+ JOIN pg_index i
+ ON ci.oid=i.indexrelid
+ JOIN pg_class ct
+ ON ct.oid = i.indrelid
+ JOIN pg_namespace n
+ ON ci.relnamespace = n.oid
+ WHERE
+ ci.relname='$index' AND n.nspname='$schema'
+ AND attrelid = ct.oid
+ AND i.indkey[s.g] = attnum
+ AND i.indclass[s.g] = opcls.oid
+ AND pg_am.oid = opcls.opcmethod
+__INDEXATTR__;
+ $res = $this->query( $sql, __METHOD__ );
+ $a = [];
+ if ( $res ) {
+ foreach ( $res as $row ) {
+ $a[] = [
+ $row->attname,
+ $row->opcname,
+ $row->amname,
+ $row->option ];
+ }
+ } else {
+ return null;
+ }
+
+ return $a;
+ }
+
+ function indexUnique( $table, $index, $fname = __METHOD__ ) {
+ $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'" .
+ " AND indexdef LIKE 'CREATE UNIQUE%(" .
+ $this->strencode( $this->indexName( $index ) ) .
+ ")'";
+ $res = $this->query( $sql, $fname );
+ if ( !$res ) {
+ return null;
+ }
+
+ return $res->numRows() > 0;
+ }
+
+ function selectSQLText(
+ $table, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
+ ) {
+ // Change the FOR UPDATE option as necessary based on the join conditions. Then pass
+ // to the parent function to get the actual SQL text.
+ // In Postgres when using FOR UPDATE, only the main table and tables that are inner joined
+ // can be locked. That means tables in an outer join cannot be FOR UPDATE locked. Trying to
+ // do so causes a DB error. This wrapper checks which tables can be locked and adjusts it
+ // accordingly.
+ // MySQL uses "ORDER BY NULL" as an optimization hint, but that is illegal in PostgreSQL.
+ if ( is_array( $options ) ) {
+ $forUpdateKey = array_search( 'FOR UPDATE', $options, true );
+ if ( $forUpdateKey !== false && $join_conds ) {
+ unset( $options[$forUpdateKey] );
+
+ foreach ( $join_conds as $table_cond => $join_cond ) {
+ if ( 0 === preg_match( '/^(?:LEFT|RIGHT|FULL)(?: OUTER)? JOIN$/i', $join_cond[0] ) ) {
+ $options['FOR UPDATE'][] = $table_cond;
+ }
+ }
+ }
+
+ if ( isset( $options['ORDER BY'] ) && $options['ORDER BY'] == 'NULL' ) {
+ unset( $options['ORDER BY'] );
+ }
+ }
+
+ return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
+ }
+
+ /**
+ * INSERT wrapper, inserts an array into a table
+ *
+ * $args may be a single associative array, or an array of these with numeric keys,
+ * for multi-row insert (Postgres version 8.2 and above only).
+ *
+ * @param string $table Name of the table to insert to.
+ * @param array $args Items to insert into the table.
+ * @param string $fname Name of the function, for profiling
+ * @param array|string $options String or array. Valid options: IGNORE
+ * @return bool Success of insert operation. IGNORE always returns true.
+ */
+ function insert( $table, $args, $fname = __METHOD__, $options = [] ) {
+ if ( !count( $args ) ) {
+ return true;
+ }
+
+ $table = $this->tableName( $table );
+ if ( !isset( $this->numericVersion ) ) {
+ $this->getServerVersion();
+ }
+
+ if ( !is_array( $options ) ) {
+ $options = [ $options ];
+ }
+
+ if ( isset( $args[0] ) && is_array( $args[0] ) ) {
+ $multi = true;
+ $keys = array_keys( $args[0] );
+ } else {
+ $multi = false;
+ $keys = array_keys( $args );
+ }
+
+ // If IGNORE is set, we use savepoints to emulate mysql's behavior
+ $savepoint = $olde = null;
+ $numrowsinserted = 0;
+ if ( in_array( 'IGNORE', $options ) ) {
+ $savepoint = new SavepointPostgres( $this, 'mw', $this->queryLogger );
+ $olde = error_reporting( 0 );
+ // For future use, we may want to track the number of actual inserts
+ // Right now, insert (all writes) simply return true/false
+ }
+
+ $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
+
+ if ( $multi ) {
+ if ( $this->numericVersion >= 8.2 && !$savepoint ) {
+ $first = true;
+ foreach ( $args as $row ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $sql .= ',';
+ }
+ $sql .= '(' . $this->makeList( $row ) . ')';
+ }
+ $res = (bool)$this->query( $sql, $fname, $savepoint );
+ } else {
+ $res = true;
+ $origsql = $sql;
+ foreach ( $args as $row ) {
+ $tempsql = $origsql;
+ $tempsql .= '(' . $this->makeList( $row ) . ')';
+
+ if ( $savepoint ) {
+ $savepoint->savepoint();
+ }
+
+ $tempres = (bool)$this->query( $tempsql, $fname, $savepoint );
+
+ if ( $savepoint ) {
+ $bar = pg_result_error( $this->mLastResult );
+ if ( $bar != false ) {
+ $savepoint->rollback();
+ } else {
+ $savepoint->release();
+ $numrowsinserted++;
+ }
+ }
+
+ // If any of them fail, we fail overall for this function call
+ // Note that this will be ignored if IGNORE is set
+ if ( !$tempres ) {
+ $res = false;
+ }
+ }
+ }
+ } else {
+ // Not multi, just a lone insert
+ if ( $savepoint ) {
+ $savepoint->savepoint();
+ }
+
+ $sql .= '(' . $this->makeList( $args ) . ')';
+ $res = (bool)$this->query( $sql, $fname, $savepoint );
+ if ( $savepoint ) {
+ $bar = pg_result_error( $this->mLastResult );
+ if ( $bar != false ) {
+ $savepoint->rollback();
+ } else {
+ $savepoint->release();
+ $numrowsinserted++;
+ }
+ }
+ }
+ if ( $savepoint ) {
+ error_reporting( $olde );
+ $savepoint->commit();
+
+ // Set the affected row count for the whole operation
+ $this->mAffectedRows = $numrowsinserted;
+
+ // IGNORE always returns true
+ return true;
+ }
+
+ return $res;
+ }
+
+ /**
+ * INSERT SELECT wrapper
+ * $varMap must be an associative array of the form [ 'dest1' => 'source1', ... ]
+ * Source items may be literals rather then field names, but strings should
+ * be quoted with Database::addQuotes()
+ * $conds may be "*" to copy the whole table
+ * srcTable may be an array of tables.
+ * @todo FIXME: Implement this a little better (seperate select/insert)?
+ *
+ * @param string $destTable
+ * @param array|string $srcTable
+ * @param array $varMap
+ * @param array $conds
+ * @param string $fname
+ * @param array $insertOptions
+ * @param array $selectOptions
+ * @return bool
+ */
+ function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
+ $insertOptions = [], $selectOptions = [] ) {
+ $destTable = $this->tableName( $destTable );
+
+ if ( !is_array( $insertOptions ) ) {
+ $insertOptions = [ $insertOptions ];
+ }
+
+ /*
+ * If IGNORE is set, we use savepoints to emulate mysql's behavior
+ * Ignore LOW PRIORITY option, since it is MySQL-specific
+ */
+ $savepoint = $olde = null;
+ $numrowsinserted = 0;
+ if ( in_array( 'IGNORE', $insertOptions ) ) {
+ $savepoint = new SavepointPostgres( $this, 'mw', $this->queryLogger );
+ $olde = error_reporting( 0 );
+ $savepoint->savepoint();
+ }
+
+ if ( !is_array( $selectOptions ) ) {
+ $selectOptions = [ $selectOptions ];
+ }
+ list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) =
+ $this->makeSelectOptions( $selectOptions );
+ if ( is_array( $srcTable ) ) {
+ $srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
+ } else {
+ $srcTable = $this->tableName( $srcTable );
+ }
+
+ $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
+ " SELECT $startOpts " . implode( ',', $varMap ) .
+ " FROM $srcTable $useIndex $ignoreIndex ";
+
+ if ( $conds != '*' ) {
+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+ }
+
+ $sql .= " $tailOpts";
+
+ $res = (bool)$this->query( $sql, $fname, $savepoint );
+ if ( $savepoint ) {
+ $bar = pg_result_error( $this->mLastResult );
+ if ( $bar != false ) {
+ $savepoint->rollback();
+ } else {
+ $savepoint->release();
+ $numrowsinserted++;
+ }
+ error_reporting( $olde );
+ $savepoint->commit();
+
+ // Set the affected row count for the whole operation
+ $this->mAffectedRows = $numrowsinserted;
+
+ // IGNORE always returns true
+ return true;
+ }
+
+ return $res;
+ }
+
+ function tableName( $name, $format = 'quoted' ) {
+ # Replace reserved words with better ones
+ switch ( $name ) {
+ case 'user':
+ return $this->realTableName( 'mwuser', $format );
+ case 'text':
+ return $this->realTableName( 'pagecontent', $format );
+ default:
+ return $this->realTableName( $name, $format );
+ }
+ }
+
+ /* Don't cheat on installer */
+ function realTableName( $name, $format = 'quoted' ) {
+ return parent::tableName( $name, $format );
+ }
+
+ /**
+ * Return the next in a sequence, save the value for retrieval via insertId()
+ *
+ * @param string $seqName
+ * @return int|null
+ */
+ function nextSequenceValue( $seqName ) {
+ $safeseq = str_replace( "'", "''", $seqName );
+ $res = $this->query( "SELECT nextval('$safeseq')" );
+ $row = $this->fetchRow( $res );
+ $this->mInsertId = $row[0];
+
+ return $this->mInsertId;
+ }
+
+ /**
+ * Return the current value of a sequence. Assumes it has been nextval'ed in this session.
+ *
+ * @param string $seqName
+ * @return int
+ */
+ function currentSequenceValue( $seqName ) {
+ $safeseq = str_replace( "'", "''", $seqName );
+ $res = $this->query( "SELECT currval('$safeseq')" );
+ $row = $this->fetchRow( $res );
+ $currval = $row[0];
+
+ return $currval;
+ }
+
+ # Returns the size of a text field, or -1 for "unlimited"
+ function textFieldSize( $table, $field ) {
+ $table = $this->tableName( $table );
+ $sql = "SELECT t.typname as ftype,a.atttypmod as size
+ FROM pg_class c, pg_attribute a, pg_type t
+ WHERE relname='$table' AND a.attrelid=c.oid AND
+ a.atttypid=t.oid and a.attname='$field'";
+ $res = $this->query( $sql );
+ $row = $this->fetchObject( $res );
+ if ( $row->ftype == 'varchar' ) {
+ $size = $row->size - 4;
+ } else {
+ $size = $row->size;
+ }
+
+ return $size;
+ }
+
+ function limitResult( $sql, $limit, $offset = false ) {
+ return "$sql LIMIT $limit " . ( is_numeric( $offset ) ? " OFFSET {$offset} " : '' );
+ }
+
+ function wasDeadlock() {
+ return $this->lastErrno() == '40P01';
+ }
+
+ function duplicateTableStructure(
+ $oldName, $newName, $temporary = false, $fname = __METHOD__
+ ) {
+ $newName = $this->addIdentifierQuotes( $newName );
+ $oldName = $this->addIdentifierQuotes( $oldName );
+
+ return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName " .
+ "(LIKE $oldName INCLUDING DEFAULTS)", $fname );
+ }
+
+ function listTables( $prefix = null, $fname = __METHOD__ ) {
+ $eschema = $this->addQuotes( $this->getCoreSchema() );
+ $result = $this->query(
+ "SELECT tablename FROM pg_tables WHERE schemaname = $eschema", $fname );
+ $endArray = [];
+
+ foreach ( $result as $table ) {
+ $vars = get_object_vars( $table );
+ $table = array_pop( $vars );
+ if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
+ $endArray[] = $table;
+ }
+ }
+
+ return $endArray;
+ }
+
+ function timestamp( $ts = 0 ) {
+ $ct = new ConvertableTimestamp( $ts );
+
+ return $ct->getTimestamp( TS_POSTGRES );
+ }
+
+ /**
+ * Posted by cc[plus]php[at]c2se[dot]com on 25-Mar-2009 09:12
+ * to http://www.php.net/manual/en/ref.pgsql.php
+ *
+ * Parsing a postgres array can be a tricky problem, he's my
+ * take on this, it handles multi-dimensional arrays plus
+ * escaping using a nasty regexp to determine the limits of each
+ * data-item.
+ *
+ * This should really be handled by PHP PostgreSQL module
+ *
+ * @since 1.19
+ * @param string $text Postgreql array returned in a text form like {a,b}
+ * @param string $output
+ * @param int|bool $limit
+ * @param int $offset
+ * @return string
+ */
+ function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) {
+ if ( false === $limit ) {
+ $limit = strlen( $text ) - 1;
+ $output = [];
+ }
+ if ( '{}' == $text ) {
+ return $output;
+ }
+ do {
+ if ( '{' != $text[$offset] ) {
+ preg_match( "/(\\{?\"([^\"\\\\]|\\\\.)*\"|[^,{}]+)+([,}]+)/",
+ $text, $match, 0, $offset );
+ $offset += strlen( $match[0] );
+ $output[] = ( '"' != $match[1][0]
+ ? $match[1]
+ : stripcslashes( substr( $match[1], 1, -1 ) ) );
+ if ( '},' == $match[3] ) {
+ return $output;
+ }
+ } else {
+ $offset = $this->pg_array_parse( $text, $output, $limit, $offset + 1 );
+ }
+ } while ( $limit > $offset );
+
+ return $output;
+ }
+
+ /**
+ * Return aggregated value function call
+ * @param array $valuedata
+ * @param string $valuename
+ * @return array
+ */
+ public function aggregateValue( $valuedata, $valuename = 'value' ) {
+ return $valuedata;
+ }
+
+ /**
+ * @return string Wikitext of a link to the server software's web site
+ */
+ public function getSoftwareLink() {
+ return '[{{int:version-db-postgres-url}} PostgreSQL]';
+ }
+
+ /**
+ * Return current schema (executes SELECT current_schema())
+ * Needs transaction
+ *
+ * @since 1.19
+ * @return string Default schema for the current session
+ */
+ function getCurrentSchema() {
+ $res = $this->query( "SELECT current_schema()", __METHOD__ );
+ $row = $this->fetchRow( $res );
+
+ return $row[0];
+ }
+
+ /**
+ * Return list of schemas which are accessible without schema name
+ * This is list does not contain magic keywords like "$user"
+ * Needs transaction
+ *
+ * @see getSearchPath()
+ * @see setSearchPath()
+ * @since 1.19
+ * @return array List of actual schemas for the current sesson
+ */
+ function getSchemas() {
+ $res = $this->query( "SELECT current_schemas(false)", __METHOD__ );
+ $row = $this->fetchRow( $res );
+ $schemas = [];
+
+ /* PHP pgsql support does not support array type, "{a,b}" string is returned */
+
+ return $this->pg_array_parse( $row[0], $schemas );
+ }
+
+ /**
+ * Return search patch for schemas
+ * This is different from getSchemas() since it contain magic keywords
+ * (like "$user").
+ * Needs transaction
+ *
+ * @since 1.19
+ * @return array How to search for table names schemas for the current user
+ */
+ function getSearchPath() {
+ $res = $this->query( "SHOW search_path", __METHOD__ );
+ $row = $this->fetchRow( $res );
+
+ /* PostgreSQL returns SHOW values as strings */
+
+ return explode( ",", $row[0] );
+ }
+
+ /**
+ * Update search_path, values should already be sanitized
+ * Values may contain magic keywords like "$user"
+ * @since 1.19
+ *
+ * @param array $search_path List of schemas to be searched by default
+ */
+ function setSearchPath( $search_path ) {
+ $this->query( "SET search_path = " . implode( ", ", $search_path ) );
+ }
+
+ /**
+ * Determine default schema for MediaWiki core
+ * Adjust this session schema search path if desired schema exists
+ * and is not alread there.
+ *
+ * We need to have name of the core schema stored to be able
+ * to query database metadata.
+ *
+ * This will be also called by the installer after the schema is created
+ *
+ * @since 1.19
+ *
+ * @param string $desiredSchema
+ */
+ function determineCoreSchema( $desiredSchema ) {
+ $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
+ if ( $this->schemaExists( $desiredSchema ) ) {
+ if ( in_array( $desiredSchema, $this->getSchemas() ) ) {
+ $this->mCoreSchema = $desiredSchema;
+ $this->queryLogger->debug(
+ "Schema \"" . $desiredSchema . "\" already in the search path\n" );
+ } else {
+ /**
+ * Prepend our schema (e.g. 'mediawiki') in front
+ * of the search path
+ * Fixes bug 15816
+ */
+ $search_path = $this->getSearchPath();
+ array_unshift( $search_path,
+ $this->addIdentifierQuotes( $desiredSchema ) );
+ $this->setSearchPath( $search_path );
+ $this->mCoreSchema = $desiredSchema;
+ $this->queryLogger->debug(
+ "Schema \"" . $desiredSchema . "\" added to the search path\n" );
+ }
+ } else {
+ $this->mCoreSchema = $this->getCurrentSchema();
+ $this->queryLogger->debug(
+ "Schema \"" . $desiredSchema . "\" not found, using current \"" .
+ $this->mCoreSchema . "\"\n" );
+ }
+ /* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */
+ $this->commit( __METHOD__ );
+ }
+
+ /**
+ * Return schema name fore core MediaWiki tables
+ *
+ * @since 1.19
+ * @return string Core schema name
+ */
+ function getCoreSchema() {
+ return $this->mCoreSchema;
+ }
+
+ /**
+ * @return string Version information from the database
+ */
+ function getServerVersion() {
+ if ( !isset( $this->numericVersion ) ) {
+ $versionInfo = pg_version( $this->mConn );
+ if ( version_compare( $versionInfo['client'], '7.4.0', 'lt' ) ) {
+ // Old client, abort install
+ $this->numericVersion = '7.3 or earlier';
+ } elseif ( isset( $versionInfo['server'] ) ) {
+ // Normal client
+ $this->numericVersion = $versionInfo['server'];
+ } else {
+ // Bug 16937: broken pgsql extension from PHP<5.3
+ $this->numericVersion = pg_parameter_status( $this->mConn, 'server_version' );
+ }
+ }
+
+ return $this->numericVersion;
+ }
+
+ /**
+ * Query whether a given relation exists (in the given schema, or the
+ * default mw one if not given)
+ * @param string $table
+ * @param array|string $types
+ * @param bool|string $schema
+ * @return bool
+ */
+ function relationExists( $table, $types, $schema = false ) {
+ if ( !is_array( $types ) ) {
+ $types = [ $types ];
+ }
+ if ( !$schema ) {
+ $schema = $this->getCoreSchema();
+ }
+ $table = $this->realTableName( $table, 'raw' );
+ $etable = $this->addQuotes( $table );
+ $eschema = $this->addQuotes( $schema );
+ $sql = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
+ . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
+ . "AND c.relkind IN ('" . implode( "','", $types ) . "')";
+ $res = $this->query( $sql );
+ $count = $res ? $res->numRows() : 0;
+
+ return (bool)$count;
+ }
+
+ /**
+ * For backward compatibility, this function checks both tables and
+ * views.
+ * @param string $table
+ * @param string $fname
+ * @param bool|string $schema
+ * @return bool
+ */
+ function tableExists( $table, $fname = __METHOD__, $schema = false ) {
+ return $this->relationExists( $table, [ 'r', 'v' ], $schema );
+ }
+
+ function sequenceExists( $sequence, $schema = false ) {
+ return $this->relationExists( $sequence, 'S', $schema );
+ }
+
+ function triggerExists( $table, $trigger ) {
+ $q = <<<SQL
+ SELECT 1 FROM pg_class, pg_namespace, pg_trigger
+ WHERE relnamespace=pg_namespace.oid AND relkind='r'
+ AND tgrelid=pg_class.oid
+ AND nspname=%s AND relname=%s AND tgname=%s
+SQL;
+ $res = $this->query(
+ sprintf(
+ $q,
+ $this->addQuotes( $this->getCoreSchema() ),
+ $this->addQuotes( $table ),
+ $this->addQuotes( $trigger )
+ )
+ );
+ if ( !$res ) {
+ return null;
+ }
+ $rows = $res->numRows();
+
+ return $rows;
+ }
+
+ function ruleExists( $table, $rule ) {
+ $exists = $this->selectField( 'pg_rules', 'rulename',
+ [
+ 'rulename' => $rule,
+ 'tablename' => $table,
+ 'schemaname' => $this->getCoreSchema()
+ ]
+ );
+
+ return $exists === $rule;
+ }
+
+ function constraintExists( $table, $constraint ) {
+ $sql = sprintf( "SELECT 1 FROM information_schema.table_constraints " .
+ "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
+ $this->addQuotes( $this->getCoreSchema() ),
+ $this->addQuotes( $table ),
+ $this->addQuotes( $constraint )
+ );
+ $res = $this->query( $sql );
+ if ( !$res ) {
+ return null;
+ }
+ $rows = $res->numRows();
+
+ return $rows;
+ }
+
+ /**
+ * Query whether a given schema exists. Returns true if it does, false if it doesn't.
+ * @param string $schema
+ * @return bool
+ */
+ function schemaExists( $schema ) {
+ $exists = $this->selectField( '"pg_catalog"."pg_namespace"', 1,
+ [ 'nspname' => $schema ], __METHOD__ );
+
+ return (bool)$exists;
+ }
+
+ /**
+ * Returns true if a given role (i.e. user) exists, false otherwise.
+ * @param string $roleName
+ * @return bool
+ */
+ function roleExists( $roleName ) {
+ $exists = $this->selectField( '"pg_catalog"."pg_roles"', 1,
+ [ 'rolname' => $roleName ], __METHOD__ );
+
+ return (bool)$exists;
+ }
+
+ /**
+ * @var string $table
+ * @var string $field
+ * @return PostgresField|null
+ */
+ function fieldInfo( $table, $field ) {
+ return PostgresField::fromText( $this, $table, $field );
+ }
+
+ /**
+ * pg_field_type() wrapper
+ * @param ResultWrapper|resource $res ResultWrapper or PostgreSQL query result resource
+ * @param int $index Field number, starting from 0
+ * @return string
+ */
+ function fieldType( $res, $index ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+
+ return pg_field_type( $res, $index );
+ }
+
+ /**
+ * @param string $b
+ * @return Blob
+ */
+ function encodeBlob( $b ) {
+ return new PostgresBlob( pg_escape_bytea( $b ) );
+ }
+
+ function decodeBlob( $b ) {
+ if ( $b instanceof PostgresBlob ) {
+ $b = $b->fetch();
+ } elseif ( $b instanceof Blob ) {
+ return $b->fetch();
+ }
+
+ return pg_unescape_bytea( $b );
+ }
+
+ function strencode( $s ) {
+ // Should not be called by us
+
+ return pg_escape_string( $this->mConn, $s );
+ }
+
+ /**
+ * @param null|bool|Blob $s
+ * @return int|string
+ */
+ function addQuotes( $s ) {
+ if ( is_null( $s ) ) {
+ return 'NULL';
+ } elseif ( is_bool( $s ) ) {
+ return intval( $s );
+ } elseif ( $s instanceof Blob ) {
+ if ( $s instanceof PostgresBlob ) {
+ $s = $s->fetch();
+ } else {
+ $s = pg_escape_bytea( $this->mConn, $s->fetch() );
+ }
+ return "'$s'";
+ }
+
+ return "'" . pg_escape_string( $this->mConn, $s ) . "'";
+ }
+
+ /**
+ * Postgres specific version of replaceVars.
+ * Calls the parent version in Database.php
+ *
+ * @param string $ins SQL string, read from a stream (usually tables.sql)
+ * @return string SQL string
+ */
+ protected function replaceVars( $ins ) {
+ $ins = parent::replaceVars( $ins );
+
+ if ( $this->numericVersion >= 8.3 ) {
+ // Thanks for not providing backwards-compatibility, 8.3
+ $ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins );
+ }
+
+ if ( $this->numericVersion <= 8.1 ) { // Our minimum version
+ $ins = str_replace( 'USING gin', 'USING gist', $ins );
+ }
+
+ return $ins;
+ }
+
+ /**
+ * Various select options
+ *
+ * @param array $options An associative array of options to be turned into
+ * an SQL query, valid keys are listed in the function.
+ * @return array
+ */
+ function makeSelectOptions( $options ) {
+ $preLimitTail = $postLimitTail = '';
+ $startOpts = $useIndex = $ignoreIndex = '';
+
+ $noKeyOptions = [];
+ foreach ( $options as $key => $option ) {
+ if ( is_numeric( $key ) ) {
+ $noKeyOptions[$option] = true;
+ }
+ }
+
+ $preLimitTail .= $this->makeGroupByWithHaving( $options );
+
+ $preLimitTail .= $this->makeOrderBy( $options );
+
+ // if ( isset( $options['LIMIT'] ) ) {
+ // $tailOpts .= $this->limitResult( '', $options['LIMIT'],
+ // isset( $options['OFFSET'] ) ? $options['OFFSET']
+ // : false );
+ // }
+
+ if ( isset( $options['FOR UPDATE'] ) ) {
+ $postLimitTail .= ' FOR UPDATE OF ' .
+ implode( ', ', array_map( [ &$this, 'tableName' ], $options['FOR UPDATE'] ) );
+ } elseif ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
+ $postLimitTail .= ' FOR UPDATE';
+ }
+
+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
+ $startOpts .= 'DISTINCT';
+ }
+
+ return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
+ }
+
+ function getDBname() {
+ return $this->mDBname;
+ }
+
+ function getServer() {
+ return $this->mServer;
+ }
+
+ function buildConcat( $stringList ) {
+ return implode( ' || ', $stringList );
+ }
+
+ public function buildGroupConcatField(
+ $delimiter, $table, $field, $conds = '', $options = [], $join_conds = []
+ ) {
+ $fld = "array_to_string(array_agg($field)," . $this->addQuotes( $delimiter ) . ')';
+
+ return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
+ }
+
+ /**
+ * @param string $field Field or column to cast
+ * @return string
+ * @since 1.28
+ */
+ public function buildStringCast( $field ) {
+ return $field . '::text';
+ }
+
+ public function streamStatementEnd( &$sql, &$newLine ) {
+ # Allow dollar quoting for function declarations
+ if ( substr( $newLine, 0, 4 ) == '$mw$' ) {
+ if ( $this->delimiter ) {
+ $this->delimiter = false;
+ } else {
+ $this->delimiter = ';';
+ }
+ }
+
+ return parent::streamStatementEnd( $sql, $newLine );
+ }
+
+ /**
+ * Check to see if a named lock is available. This is non-blocking.
+ * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
+ *
+ * @param string $lockName Name of lock to poll
+ * @param string $method Name of method calling us
+ * @return bool
+ * @since 1.20
+ */
+ public function lockIsFree( $lockName, $method ) {
+ $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
+ $result = $this->query( "SELECT (CASE(pg_try_advisory_lock($key))
+ WHEN 'f' THEN 'f' ELSE pg_advisory_unlock($key) END) AS lockstatus", $method );
+ $row = $this->fetchObject( $result );
+
+ return ( $row->lockstatus === 't' );
+ }
+
+ /**
+ * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
+ * @param string $lockName
+ * @param string $method
+ * @param int $timeout
+ * @return bool
+ */
+ public function lock( $lockName, $method, $timeout = 5 ) {
+ $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
+ $loop = new WaitConditionLoop(
+ function () use ( $lockName, $key, $timeout, $method ) {
+ $res = $this->query( "SELECT pg_try_advisory_lock($key) AS lockstatus", $method );
+ $row = $this->fetchObject( $res );
+ if ( $row->lockstatus === 't' ) {
+ parent::lock( $lockName, $method, $timeout ); // record
+ return true;
+ }
+
+ return WaitConditionLoop::CONDITION_CONTINUE;
+ },
+ $timeout
+ );
+
+ return ( $loop->invoke() === $loop::CONDITION_REACHED );
+ }
+
+ /**
+ * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKSFROM
+ * PG DOCS: http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
+ * @param string $lockName
+ * @param string $method
+ * @return bool
+ */
+ public function unlock( $lockName, $method ) {
+ $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
+ $result = $this->query( "SELECT pg_advisory_unlock($key) as lockstatus", $method );
+ $row = $this->fetchObject( $result );
+
+ if ( $row->lockstatus === 't' ) {
+ parent::unlock( $lockName, $method ); // record
+ return true;
+ }
+
+ $this->queryLogger->debug( __METHOD__ . " failed to release lock\n" );
+
+ return false;
+ }
+
+ /**
+ * @param string $lockName
+ * @return string Integer
+ */
+ private function bigintFromLockName( $lockName ) {
+ return Wikimedia\base_convert( substr( sha1( $lockName ), 0, 15 ), 16, 10 );
+ }
+}
return $this->query( $sql, $fName );
}
+ protected function requiresDatabaseUser() {
+ return false; // just a file
+ }
+
/**
* @return string
*/
public function __toString() {
return 'SQLite ' . (string)$this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
}
-
}
--- /dev/null
+<?php
+/**
+ * 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
+ */
+use Psr\Log\LoggerInterface;
+
+/**
+ * Manage savepoints within a transaction
+ * @ingroup Database
+ * @since 1.19
+ */
+class SavepointPostgres {
+ /** @var DatabasePostgres Establish a savepoint within a transaction */
+ protected $dbw;
+ /** @var LoggerInterface */
+ protected $logger;
+ /** @var int */
+ protected $id;
+ /** @var bool */
+ protected $didbegin;
+
+ /**
+ * @param DatabasePostgres $dbw
+ * @param int $id
+ * @param LoggerInterface $logger
+ */
+ public function __construct( DatabasePostgres $dbw, $id, LoggerInterface $logger ) {
+ $this->dbw = $dbw;
+ $this->logger = $logger;
+ $this->id = $id;
+ $this->didbegin = false;
+ /* If we are not in a transaction, we need to be for savepoint trickery */
+ if ( !$dbw->trxLevel() ) {
+ $dbw->begin( "FOR SAVEPOINT", DatabasePostgres::TRANSACTION_INTERNAL );
+ $this->didbegin = true;
+ }
+ }
+
+ public function __destruct() {
+ if ( $this->didbegin ) {
+ $this->dbw->rollback();
+ $this->didbegin = false;
+ }
+ }
+
+ public function commit() {
+ if ( $this->didbegin ) {
+ $this->dbw->commit();
+ $this->didbegin = false;
+ }
+ }
+
+ protected function query( $keyword, $msg_ok, $msg_failed ) {
+ if ( $this->dbw->doQuery( $keyword . " " . $this->id ) !== false ) {
+ $this->logger->debug( sprintf( $msg_ok, $this->id ) );
+ } else {
+ $this->logger->debug( sprintf( $msg_failed, $this->id ) );
+ }
+ }
+
+ public function savepoint() {
+ $this->query( "SAVEPOINT",
+ "Transaction state: savepoint \"%s\" established.\n",
+ "Transaction state: establishment of savepoint \"%s\" FAILED.\n"
+ );
+ }
+
+ public function release() {
+ $this->query( "RELEASE",
+ "Transaction state: savepoint \"%s\" released.\n",
+ "Transaction state: release of savepoint \"%s\" FAILED.\n"
+ );
+ }
+
+ public function rollback() {
+ $this->query( "ROLLBACK TO",
+ "Transaction state: savepoint \"%s\" rolled back.\n",
+ "Transaction state: rollback of savepoint \"%s\" FAILED.\n"
+ );
+ }
+
+ public function __toString() {
+ return (string)$this->id;
+ }
+}
--- /dev/null
+<?php
+class PostgresField implements Field {
+ private $name, $tablename, $type, $nullable, $max_length, $deferred, $deferrable, $conname,
+ $has_default, $default;
+
+ /**
+ * @param DatabasePostgres $db
+ * @param string $table
+ * @param string $field
+ * @return null|PostgresField
+ */
+ static function fromText( $db, $table, $field ) {
+ $q = <<<SQL
+SELECT
+ attnotnull, attlen, conname AS conname,
+ atthasdef,
+ adsrc,
+ COALESCE(condeferred, 'f') AS deferred,
+ COALESCE(condeferrable, 'f') AS deferrable,
+ CASE WHEN typname = 'int2' THEN 'smallint'
+ WHEN typname = 'int4' THEN 'integer'
+ WHEN typname = 'int8' THEN 'bigint'
+ WHEN typname = 'bpchar' THEN 'char'
+ ELSE typname END AS typname
+FROM pg_class c
+JOIN pg_namespace n ON (n.oid = c.relnamespace)
+JOIN pg_attribute a ON (a.attrelid = c.oid)
+JOIN pg_type t ON (t.oid = a.atttypid)
+LEFT JOIN pg_constraint o ON (o.conrelid = c.oid AND a.attnum = ANY(o.conkey) AND o.contype = 'f')
+LEFT JOIN pg_attrdef d on c.oid=d.adrelid and a.attnum=d.adnum
+WHERE relkind = 'r'
+AND nspname=%s
+AND relname=%s
+AND attname=%s;
+SQL;
+
+ $table = $db->tableName( $table, 'raw' );
+ $res = $db->query(
+ sprintf( $q,
+ $db->addQuotes( $db->getCoreSchema() ),
+ $db->addQuotes( $table ),
+ $db->addQuotes( $field )
+ )
+ );
+ $row = $db->fetchObject( $res );
+ if ( !$row ) {
+ return null;
+ }
+ $n = new PostgresField;
+ $n->type = $row->typname;
+ $n->nullable = ( $row->attnotnull == 'f' );
+ $n->name = $field;
+ $n->tablename = $table;
+ $n->max_length = $row->attlen;
+ $n->deferrable = ( $row->deferrable == 't' );
+ $n->deferred = ( $row->deferred == 't' );
+ $n->conname = $row->conname;
+ $n->has_default = ( $row->atthasdef === 't' );
+ $n->default = $row->adsrc;
+
+ return $n;
+ }
+
+ function name() {
+ return $this->name;
+ }
+
+ function tableName() {
+ return $this->tablename;
+ }
+
+ function type() {
+ return $this->type;
+ }
+
+ function isNullable() {
+ return $this->nullable;
+ }
+
+ function maxLength() {
+ return $this->max_length;
+ }
+
+ function is_deferrable() {
+ return $this->deferrable;
+ }
+
+ function is_deferred() {
+ return $this->deferred;
+ }
+
+ function conname() {
+ return $this->conname;
+ }
+
+ /**
+ * @since 1.19
+ * @return bool|mixed
+ */
+ function defaultValue() {
+ if ( $this->has_default ) {
+ return $this->default;
+ } else {
+ return false;
+ }
+ }
+}
[ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ];
/**
- * @TODO: document base params here
- * @param array $conf
+ * Construct a manager of ILoadBalancer objects
+ *
+ * Sub-classes will extend the required keys in $conf with additional parameters
+ *
+ * @param $conf $params Array with keys:
+ * - localDomain: A DatabaseDomain or domain ID string.
+ * - readOnlyReason : Reason the master DB is read-only if so [optional]
+ * - srvCache : BagOStuff object for server cache [optional]
+ * - memCache : BagOStuff object for cluster memory cache [optional]
+ * - wanCache : WANObjectCache object [optional]
+ * - hostname : The name of the current server [optional]
+ * - cliMode: Whether the execution context is a CLI script. [optional]
+ * - profiler : Class name or instance with profileIn()/profileOut() methods. [optional]
+ * - trxProfiler: TransactionProfiler instance. [optional]
+ * - replLogger: PSR-3 logger instance. [optional]
+ * - connLogger: PSR-3 logger instance. [optional]
+ * - queryLogger: PSR-3 logger instance. [optional]
+ * - perfLogger: PSR-3 logger instance. [optional]
+ * - errorLogger : Callback that takes an Exception and logs it. [optional]
+ * @throws InvalidArgumentException
*/
public function __construct( array $conf ) {
$this->localDomain = isset( $conf['localDomain'] )
trigger_error( E_WARNING, get_class( $e ) . ': ' . $e->getMessage() );
};
- $this->chronProt = isset( $conf['chronProt'] ) ? $conf['chronProt'] : null;
-
$this->profiler = isset( $params['profiler'] ) ? $params['profiler'] : null;
$this->trxProfiler = isset( $conf['trxProfiler'] )
? $conf['trxProfiler']
/**
* Disables all load balancers. All connections are closed, and any attempt to
* open a new connection will result in a DBAccessError.
- * @see LoadBalancer::disable()
+ * @see ILoadBalancer::disable()
*/
public function destroy() {
$this->shutdown( self::SHUTDOWN_NO_CHRONPROT );
$prefix
);
- $this->forEachLB( function( LoadBalancer $lb ) use ( $prefix ) {
+ $this->forEachLB( function( ILoadBalancer $lb ) use ( $prefix ) {
$lb->setDomainPrefix( $prefix );
} );
}
* A multi-database, multi-master factory for Wikimedia and similar installations.
* Ignores the old configuration globals.
*
- * Template override precedence (highest => lowest):
- * - templateOverridesByServer
- * - masterTemplateOverrides
- * - templateOverridesBySection/templateOverridesByCluster
- * - externalTemplateOverrides
- * - serverTemplate
- * Overrides only work on top level keys (so nested values will not be merged).
- *
- * Configuration:
- * sectionsByDB A map of database names to section names.
- *
- * sectionLoads A 2-d map. For each section, gives a map of server names to
- * load ratios. For example:
- * [
- * 'section1' => [
- * 'db1' => 100,
- * 'db2' => 100
- * ]
- * ]
- *
- * serverTemplate A server info associative array as documented for $wgDBservers.
- * The host, hostName and load entries will be overridden.
- *
- * groupLoadsBySection A 3-d map giving server load ratios for each section and group.
- * For example:
- * [
- * 'section1' => [
- * 'group1' => [
- * 'db1' => 100,
- * 'db2' => 100
- * ]
- * ]
- * ]
- *
- * groupLoadsByDB A 3-d map giving server load ratios by DB name.
- *
- * hostsByName A map of hostname to IP address.
- *
- * externalLoads A map of external storage cluster name to server load map.
- *
- * externalTemplateOverrides A set of server info keys overriding serverTemplate for external
- * storage.
- *
- * templateOverridesByServer A 2-d map overriding serverTemplate and
- * externalTemplateOverrides on a server-by-server basis. Applies
- * to both core and external storage.
- * templateOverridesBySection A 2-d map overriding the server info by section.
- * templateOverridesByCluster A 2-d map overriding the server info by external storage cluster.
- *
- * masterTemplateOverrides An override array for all master servers.
- *
- * loadMonitorClass Name of the LoadMonitor class to always use.
- *
- * readOnlyBySection A map of section name to read-only message.
- * Missing or false for read/write.
- *
* @ingroup Database
*/
class LBFactoryMulti extends LBFactory {
*/
private $readOnlyBySection = [];
- // Other stuff
-
/** @var array Load balancer factory configuration */
private $conf;
private $lastSection;
/**
- * @param array $conf
- * @throws InvalidArgumentException
+ * @see LBFactory::__construct()
+ *
+ * Template override precedence (highest => lowest):
+ * - templateOverridesByServer
+ * - masterTemplateOverrides
+ * - templateOverridesBySection/templateOverridesByCluster
+ * - externalTemplateOverrides
+ * - serverTemplate
+ * Overrides only work on top level keys (so nested values will not be merged).
+ *
+ * Server configuration maps should be of the format Database::factory() requires.
+ * Additionally, a 'max lag' key should also be set on server maps, indicating how stale the
+ * data can be before the load balancer tries to avoid using it. The map can have 'is static'
+ * set to disable blocking replication sync checks (intended for archive servers with
+ * unchanging data).
+ *
+ * @param array $conf Parameters of LBFactory::__construct() as well as:
+ * - sectionsByDB Map of database names to section names.
+ * - sectionLoads 2-d map. For each section, gives a map of server names to
+ * load ratios. For example:
+ * [
+ * 'section1' => [
+ * 'db1' => 100,
+ * 'db2' => 100
+ * ]
+ * ]
+ * - serverTemplate Server configuration map intended for Database::factory().
+ * Note that "host", "hostName" and "load" entries will be
+ * overridden by "sectionLoads" and "hostsByName".
+ * - groupLoadsBySection 3-d map giving server load ratios for each section/group.
+ * For example:
+ * [
+ * 'section1' => [
+ * 'group1' => [
+ * 'db1' => 100,
+ * 'db2' => 100
+ * ]
+ * ]
+ * ]
+ * - groupLoadsByDB 3-d map giving server load ratios by DB name.
+ * - hostsByName Map of hostname to IP address.
+ * - externalLoads Map of external storage cluster name to server load map.
+ * - externalTemplateOverrides Set of server configuration maps overriding
+ * "serverTemplate" for external storage.
+ * - templateOverridesByServer 2-d map overriding "serverTemplate" and
+ * "externalTemplateOverrides" on a server-by-server basis.
+ * Applies to both core and external storage.
+ * - templateOverridesBySection 2-d map overriding the server configuration maps by section.
+ * - templateOverridesByCluster 2-d map overriding the server configuration maps by external
+ * storage cluster.
+ * - masterTemplateOverrides Server configuration map overrides for all master servers.
+ * - loadMonitorClass Name of the LoadMonitor class to always use.
+ * - readOnlyBySection A map of section name to read-only message.
+ * Missing or false for read/write.
*/
public function __construct( array $conf ) {
parent::__construct( $conf );
/** @var string */
private $loadMonitorClass;
+ /**
+ * @see LBFactory::__construct()
+ * @param array $conf Parameters of LBFactory::__construct() as well as:
+ * - servers : list of server configuration maps to Database::factory().
+ * Additionally, the server maps should have a 'load' key, which is used to decide
+ * how often clients connect to one server verses the others. A 'max lag' key should
+ * also be set on server maps, indicating how stale the data can be before the load
+ * balancer tries to avoid using it. The map can have 'is static' set to disable blocking
+ * replication sync checks (intended for archive servers with unchanging data).
+ * - externalClusters : map of cluster names to server arrays. The servers arrays have the
+ * same format as "servers" above.
+ */
public function __construct( array $conf ) {
parent::__construct( $conf );
*/
protected function newExternalLB( $cluster, $domain = false ) {
if ( !isset( $this->externalClusters[$cluster] ) ) {
- throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
+ throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"." );
}
return $this->newLoadBalancer( $this->externalClusters[$cluster] );
*/
interface ILoadBalancer {
/**
- * @param array $params Array with keys:
+ * Construct a manager of IDatabase connection objects
+ *
+ * @param array $params Parameter map with keys:
* - servers : Required. Array of server info structures.
+ * - localDomain: A DatabaseDomain or domain ID string.
* - loadMonitor : Name of a class used to fetch server lag and load.
* - readOnlyReason : Reason the master DB is read-only if so [optional]
* - waitTimeout : Maximum time to wait for replicas for consistency [optional]
* - srvCache : BagOStuff object for server cache [optional]
* - memCache : BagOStuff object for cluster memory cache [optional]
* - wanCache : WANObjectCache object [optional]
- * - hostname : the name of the current server [optional]
+ * - hostname : The name of the current server [optional]
+ * - cliMode: Whether the execution context is a CLI script. [optional]
+ * - profiler : Class name or instance with profileIn()/profileOut() methods. [optional]
+ * - trxProfiler: TransactionProfiler instance. [optional]
+ * - replLogger: PSR-3 logger instance. [optional]
+ * - connLogger: PSR-3 logger instance. [optional]
+ * - queryLogger: PSR-3 logger instance. [optional]
+ * - perfLogger: PSR-3 logger instance. [optional]
+ * - errorLogger : Callback that takes an Exception and logs it. [optional]
* @throws InvalidArgumentException
*/
public function __construct( array $params );
*
* Side effect: opens connections to databases
* @param string|bool $group Query group, or false for the generic reader
- * @param string|bool $wiki Wiki ID, or false for the current wiki
+ * @param string|bool $domain Domain ID, or false for the current domain
* @throws DBError
* @return bool|int|string
*/
- public function getReaderIndex( $group = false, $wiki = false );
+ public function getReaderIndex( $group = false, $domain = false );
/**
* Set the master wait position
*
* @param int $i Server index
* @param array|string|bool $groups Query group(s), or false for the generic reader
- * @param string|bool $wiki Wiki ID, or false for the current wiki
+ * @param string|bool $domain Domain ID, or false for the current domain
*
* @throws DBError
* @return IDatabase
*/
- public function getConnection( $i, $groups = [], $wiki = false );
+ public function getConnection( $i, $groups = [], $domain = false );
/**
* Mark a foreign connection as being available for reuse under a different
*
* @param int $db
* @param array|string|bool $groups Query group(s), or false for the generic reader
- * @param string|bool $wiki Wiki ID, or false for the current wiki
+ * @param string|bool $domain Domain ID, or false for the current domain
* @return DBConnRef
*/
- public function getConnectionRef( $db, $groups = [], $wiki = false );
+ public function getConnectionRef( $db, $groups = [], $domain = false );
/**
* Get a database connection handle reference without connecting yet
*
* @param int $db
* @param array|string|bool $groups Query group(s), or false for the generic reader
- * @param string|bool $wiki Wiki ID, or false for the current wiki
+ * @param string|bool $domain Domain ID, or false for the current domain
* @return DBConnRef
*/
- public function getLazyConnectionRef( $db, $groups = [], $wiki = false );
+ public function getLazyConnectionRef( $db, $groups = [], $domain = false );
/**
* Open a connection to the server given by the specified index
* @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError.
*
* @param int $i Server index
- * @param string|bool $wiki Wiki ID, or false for the current wiki
+ * @param string|bool $domain Domain ID, or false for the current domain
* @return IDatabase|bool Returns false on errors
*/
- public function openConnection( $i, $wiki = false );
+ public function openConnection( $i, $domain = false );
/**
* @return int
/**
* @note This method will trigger a DB connection if not yet done
- * @param string|bool $wiki Wiki ID, or false for the current wiki
+ * @param string|bool $domain Domain ID, or false for the current domain
* @return bool Whether the generic connection for reads is highly "lagged"
*/
- public function getLaggedReplicaMode( $wiki = false );
+ public function getLaggedReplicaMode( $domain = false );
/**
* @note This method will never cause a new DB connection
/**
* @note This method may trigger a DB connection if not yet done
- * @param string|bool $wiki Wiki ID, or false for the current wiki
+ * @param string|bool $domain Domain ID, or false for the current domain
* @param IDatabase|null DB master connection; used to avoid loops [optional]
* @return string|bool Reason the master is read-only or false if it is not
*/
- public function getReadOnlyReason( $wiki = false, IDatabase $conn = null );
+ public function getReadOnlyReason( $domain = false, IDatabase $conn = null );
/**
* Disables/enables lag checks
* May attempt to open connections to replica DBs on the default DB. If there is
* no lag, the maximum lag will be reported as -1.
*
- * @param bool|string $wiki Wiki ID, or false for the default database
+ * @param bool|string $domain Domain ID, or false for the default database
* @return array ( host, max lag, index of max lagged host )
*/
- public function getMaxLag( $wiki = false );
+ public function getMaxLag( $domain = false );
/**
* Get an estimate of replication lag (in seconds) for each server
*
* Values may be "false" if replication is too broken to estimate
*
- * @param string|bool $wiki
+ * @param string|bool $domain
* @return int[] Map of (server index => float|int|bool)
*/
- public function getLagTimes( $wiki = false );
+ public function getLagTimes( $domain = false );
/**
* Get the lag in seconds for a given connection, or zero if this load
* @param callable|null $callback
*/
public function setTransactionListener( $name, callable $callback = null );
+
+ /**
+ * Set a new table prefix for the existing local domain ID for testing
+ *
+ * @param string $prefix
+ */
+ public function setDomainPrefix( $prefix );
+
+ /**
+ * Make certain table names use their own database, schema, and table prefix
+ * when passed into SQL queries pre-escaped and without a qualified database name
+ *
+ * For example, "user" can be converted to "myschema.mydbname.user" for convenience.
+ * Appearances like `user`, somedb.user, somedb.someschema.user will used literally.
+ *
+ * Calling this twice will completely clear any old table aliases. Also, note that
+ * callers are responsible for making sure the schemas and databases actually exist.
+ *
+ * @param array[] $aliases Map of (table => (dbname, schema, prefix) map)
+ */
+ public function setTableAliases( array $aliases );
}
}
}
- /**
- * Set the master wait position and wait for a "generic" replica DB to catch up to it
- *
- * This can be used a faster proxy for waitForAll()
- *
- * @param DBMasterPos $pos
- * @param int $timeout Max seconds to wait; default is mWaitTimeout
- * @return bool Success (able to connect and no timeouts reached)
- * @since 1.26
- */
public function waitForOne( $pos, $timeout = null ) {
$this->mWaitForPos = $pos;
}
}
- /**
- * Get a database connection handle reference
- *
- * The handle's methods wrap simply wrap those of a IDatabase handle
- *
- * @see LoadBalancer::getConnection() for parameter information
- *
- * @param int $db
- * @param array|string|bool $groups Query group(s), or false for the generic reader
- * @param string|bool $domain Domain ID, or false for the current domain
- * @return DBConnRef
- * @since 1.22
- */
public function getConnectionRef( $db, $groups = [], $domain = false ) {
$domain = ( $domain !== false ) ? $domain : $this->localDomain;
return new DBConnRef( $this, $this->getConnection( $db, $groups, $domain ) );
}
- /**
- * Get a database connection handle reference without connecting yet
- *
- * The handle's methods wrap simply wrap those of a IDatabase handle
- *
- * @see LoadBalancer::getConnection() for parameter information
- *
- * @param int $db
- * @param array|string|bool $groups Query group(s), or false for the generic reader
- * @param string|bool $domain Domain ID, or false for the current domain
- * @return DBConnRef
- * @since 1.22
- */
public function getLazyConnectionRef( $db, $groups = [], $domain = false ) {
$domain = ( $domain !== false ) ? $domain : $this->localDomain;
// Create a live connection object
try {
- $db = DatabaseBase::factory( $server['type'], $server );
+ $db = Database::factory( $server['type'], $server );
} catch ( DBConnectionError $e ) {
// FIXME: This is probably the ugliest thing I have ever done to
// PHP. I'm half-expecting it to segfault, just out of disgust. -- TS
return false;
}
- /**
- * Disable this load balancer. All connections are closed, and any attempt to
- * open a new connection will result in a DBAccessError.
- *
- * @since 1.27
- */
public function disable() {
$this->closeAll();
$this->disabled = true;
}
}
- /**
- * Perform all pre-commit callbacks that remain part of the atomic transactions
- * and disable any post-commit callbacks until runMasterPostTrxCallbacks()
- * @since 1.28
- */
public function finalizeMasterChanges() {
$this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) {
// Any error should cause all DB transactions to be rolled back together
} );
}
- /**
- * Perform all pre-commit checks for things like replication safety
- * @param array $options Includes:
- * - maxWriteDuration : max write query duration time in seconds
- * @throws DBTransactionError
- * @since 1.28
- */
public function approveMasterChanges( array $options ) {
$limit = isset( $options['maxWriteDuration'] ) ? $options['maxWriteDuration'] : 0;
$this->forEachOpenMasterConnection( function ( IDatabase $conn ) use ( $limit ) {
} );
}
- /**
- * Flush any master transaction snapshots and set DBO_TRX (if DBO_DEFAULT is set)
- *
- * The DBO_TRX setting will be reverted to the default in each of these methods:
- * - commitMasterChanges()
- * - rollbackMasterChanges()
- * - commitAll()
- * This allows for custom transaction rounds from any outer transaction scope.
- *
- * @param string $fname
- * @throws DBExpectedError
- * @since 1.28
- */
public function beginMasterChanges( $fname = __METHOD__ ) {
if ( $this->trxRoundId !== false ) {
throw new DBTransactionError(
}
}
- /**
- * Issue all pending post-COMMIT/ROLLBACK callbacks
- * @param integer $type IDatabase::TRIGGER_* constant
- * @return Exception|null The first exception or null if there were none
- * @since 1.28
- */
public function runMasterPostTrxCallbacks( $type ) {
$e = null; // first exception
$this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) use ( $type, &$e ) {
return $e;
}
- /**
- * Issue ROLLBACK only on master, only if queries were done on connection
- * @param string $fname Caller name
- * @throws DBExpectedError
- * @since 1.23
- */
public function rollbackMasterChanges( $fname = __METHOD__ ) {
$restore = ( $this->trxRoundId !== false );
$this->trxRoundId = false;
);
}
- /**
- * Suppress all pending post-COMMIT/ROLLBACK callbacks
- * @return Exception|null The first exception or null if there were none
- * @since 1.28
- */
public function suppressTransactionEndCallbacks() {
$this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) {
$conn->setTrxEndCallbackSuppression( true );
}
}
- /**
- * Commit all replica DB transactions so as to flush any REPEATABLE-READ or SSI snapshot
- *
- * @param string $fname Caller name
- * @since 1.28
- */
public function flushReplicaSnapshots( $fname = __METHOD__ ) {
$this->forEachOpenReplicaConnection( function ( IDatabase $conn ) {
$conn->flushSnapshot( __METHOD__ );
} );
}
- /**
- * @return bool Whether a master connection is already open
- * @since 1.24
- */
public function hasMasterConnection() {
return $this->isOpen( $this->getWriterIndex() );
}
- /**
- * Determine if there are pending changes in a transaction by this thread
- * @since 1.23
- * @return bool
- */
public function hasMasterChanges() {
$pending = 0;
$this->forEachOpenMasterConnection( function ( IDatabase $conn ) use ( &$pending ) {
return (bool)$pending;
}
- /**
- * Get the timestamp of the latest write query done by this thread
- * @since 1.25
- * @return float|bool UNIX timestamp or false
- */
public function lastMasterChangeTimestamp() {
$lastTime = false;
$this->forEachOpenMasterConnection( function ( IDatabase $conn ) use ( &$lastTime ) {
return $lastTime;
}
- /**
- * Check if this load balancer object had any recent or still
- * pending writes issued against it by this PHP thread
- *
- * @param float $age How many seconds ago is "recent" [defaults to mWaitTimeout]
- * @return bool
- * @since 1.25
- */
public function hasOrMadeRecentMasterChanges( $age = null ) {
$age = ( $age === null ) ? $this->mWaitTimeout : $age;
|| $this->lastMasterChangeTimestamp() > microtime( true ) - $age );
}
- /**
- * Get the list of callers that have pending master changes
- *
- * @return string[] List of method names
- * @since 1.27
- */
public function pendingMasterChangeCallers() {
$fnames = [];
$this->forEachOpenMasterConnection( function ( IDatabase $conn ) use ( &$fnames ) {
return $this->getLaggedReplicaMode( $domain );
}
- /**
- * @note This method will never cause a new DB connection
- * @return bool Whether any generic connection used for reads was highly "lagged"
- * @since 1.28
- */
public function laggedReplicaUsed() {
return $this->laggedReplicaMode;
}
return $this->laggedReplicaUsed();
}
- /**
- * @note This method may trigger a DB connection if not yet done
- * @param string|bool $domain Domain ID, or false for the current domain
- * @param IDatabase|null DB master connection; used to avoid loops [optional]
- * @return string|bool Reason the master is read-only or false if it is not
- * @since 1.27
- */
public function getReadOnlyReason( $domain = false, IDatabase $conn = null ) {
if ( $this->readOnlyReason !== false ) {
return $this->readOnlyReason;
}
}
- /**
- * Call a function with each open connection object to a master
- * @param callable $callback
- * @param array $params
- * @since 1.28
- */
public function forEachOpenMasterConnection( $callback, array $params = [] ) {
$masterIndex = $this->getWriterIndex();
foreach ( $this->mConns as $connsByServer ) {
}
}
- /**
- * Call a function with each open replica DB connection object
- * @param callable $callback
- * @param array $params
- * @since 1.28
- */
public function forEachOpenReplicaConnection( $callback, array $params = [] ) {
foreach ( $this->mConns as $connsByServer ) {
foreach ( $connsByServer as $i => $serverConns ) {
}
}
- /**
- * Wait for a replica DB to reach a specified master position
- *
- * This will connect to the master to get an accurate position if $pos is not given
- *
- * @param IDatabase $conn Replica DB
- * @param DBMasterPos|bool $pos Master position; default: current position
- * @param integer $timeout Timeout in seconds
- * @return bool Success
- * @since 1.27
- */
public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = 10 ) {
if ( $this->getServerCount() == 1 || !$conn->getLBInfo( 'replica' ) ) {
return true; // server is not a replica DB
return $ok;
}
- /**
- * Clear the cache for slag lag delay times
- *
- * This is only used for testing
- * @since 1.26
- */
public function clearLagTimeCache() {
$this->getLoadMonitor()->clearCaches();
}
- /**
- * Set a callback via IDatabase::setTransactionListener() on
- * all current and future master connections of this load balancer
- *
- * @param string $name Callback name
- * @param callable|null $callback
- * @since 1.28
- */
public function setTransactionListener( $name, callable $callback = null ) {
if ( $callback ) {
$this->trxRecurringCallbacks[$name] = $callback;
);
}
- /**
- * Make certain table names use their own database, schema, and table prefix
- * when passed into SQL queries pre-escaped and without a qualified database name
- *
- * For example, "user" can be converted to "myschema.mydbname.user" for convenience.
- * Appearances like `user`, somedb.user, somedb.someschema.user will used literally.
- *
- * Calling this twice will completely clear any old table aliases. Also, note that
- * callers are responsible for making sure the schemas and databases actually exist.
- *
- * @param array[] $aliases Map of (table => (dbname, schema, prefix) map)
- * @since 1.28
- */
public function setTableAliases( array $aliases ) {
$this->tableAliases = $aliases;
}
- /**
- * Set a new table prefix for the existing local domain ID for testing
- *
- * @param string $prefix
- * @since 1.28
- */
public function setDomainPrefix( $prefix ) {
$this->localDomain = new DatabaseDomain(
$this->localDomain->getDatabase(),
/**
* Check if the page can be cached
+ * @param integer $mode One of the HTMLFileCache::MODE_* constants (since 1.28)
* @return bool
*/
- public function isFileCacheable() {
+ public function isFileCacheable( $mode = HTMLFileCache::MODE_NORMAL ) {
$cacheable = false;
- if ( HTMLFileCache::useFileCache( $this->getContext() ) ) {
+ if ( HTMLFileCache::useFileCache( $this->getContext(), $mode ) ) {
$cacheable = $this->mPage->getId()
&& !$this->mRedirectedFrom && !$this->getTitle()->isRedirect();
// Extension may have reason to disable file caching on some pages.
* @ingroup Maintenance
*/
class RebuildFileCache extends Maintenance {
+ private $enabled = true;
+
public function __construct() {
parent::__construct();
$this->addDescription( 'Build file cache for content pages' );
}
public function finalSetup() {
- global $wgDebugToolbar;
+ global $wgDebugToolbar, $wgUseFileCache, $wgReadOnly;
+ $this->enabled = $wgUseFileCache;
+ // Script will handle capturing output and saving it itself
+ $wgUseFileCache = false;
// Debug toolbar makes content uncacheable so we disable it.
// Has to be done before Setup.php initialize MWDebug
$wgDebugToolbar = false;
+ // Avoid DB writes (like enotif/counters)
+ $wgReadOnly = 'Building cache'; // avoid DB writes (like enotif/counters)
+
parent::finalSetup();
}
public function execute() {
- global $wgUseFileCache, $wgReadOnly, $wgRequestTime;
- global $wgOut;
- if ( !$wgUseFileCache ) {
+ global $wgRequestTime;
+
+ if ( !$this->enabled ) {
$this->error( "Nothing to do -- \$wgUseFileCache is disabled.", true );
}
- $wgReadOnly = 'Building cache'; // avoid DB writes (like enotif/counters)
-
$start = $this->getOption( 'start', "0" );
if ( !ctype_digit( $start ) ) {
$this->error( "Invalid value for start parameter.", true );
$blockEnd = $start + $this->mBatchSize - 1;
$dbw = $this->getDB( DB_MASTER );
+ $mainContext = RequestContext::getMain();
// Go through each page and save the output
while ( $blockEnd <= $end ) {
// Get the pages
$this->beginTransaction( $dbw, __METHOD__ ); // for any changes
foreach ( $res as $row ) {
$rebuilt = false;
- $wgRequestTime = microtime( true ); # bug 22852
$title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
if ( null == $title ) {
continue; // broken title?
}
- $context = new RequestContext;
+ $context = new RequestContext();
$context->setTitle( $title );
$article = Article::newFromTitle( $title, $context );
$context->setWikiPage( $article->getPage() );
- $wgOut = $context->getOutput(); // set display title
-
// If the article is cacheable, then load it
- if ( $article->isFileCacheable() ) {
- $cache = new HTMLFileCache( $title, 'view' );
- if ( $cache->isCacheGood() ) {
+ if ( $article->isFileCacheable( HTMLFileCache::MODE_REBUILD ) ) {
+ $viewCache = new HTMLFileCache( $title, 'view' );
+ $historyCache = new HTMLFileCache( $title, 'history' );
+ if ( $viewCache->isCacheGood() && $historyCache->isCacheGood() ) {
if ( $overwrite ) {
$rebuilt = true;
} else {
- $this->output( "Page {$row->page_id} already cached\n" );
+ $this->output( "Page '$title' (id {$row->page_id}) already cached\n" );
continue; // done already!
}
}
- ob_start( [ &$cache, 'saveToFileCache' ] ); // save on ob_end_clean()
- $wgUseFileCache = false; // hack, we don't want $article fiddling with filecache
- $article->view();
+
MediaWiki\suppressWarnings(); // header notices
- $wgOut->output();
+ // Cache ?action=view
+ $wgRequestTime = microtime( true ); # bug 22852
+ ob_start();
+ $article->view();
+ $context->getOutput()->output();
+ $context->getOutput()->clearHTML();
+ $viewHtml = ob_get_clean();
+ $viewCache->saveToFileCache( $viewHtml );
+ // Cache ?action=history
+ $wgRequestTime = microtime( true ); # bug 22852
+ ob_start();
+ Action::factory( 'history', $article, $context )->show();
+ $context->getOutput()->output();
+ $context->getOutput()->clearHTML();
+ $historyHtml = ob_get_clean();
+ $historyCache->saveToFileCache( $historyHtml );
MediaWiki\restoreWarnings();
- $wgUseFileCache = true;
- ob_end_clean(); // clear buffer
+
if ( $rebuilt ) {
- $this->output( "Re-cached page {$row->page_id}\n" );
+ $this->output( "Re-cached page '$title' (id {$row->page_id})..." );
} else {
- $this->output( "Cached page {$row->page_id}\n" );
+ $this->output( "Cached page '$title' (id {$row->page_id})..." );
}
+ $this->output( "[view: " . strlen( $viewHtml ) . " bytes; " .
+ "history: " . strlen( $historyHtml ) . " bytes]\n" );
} else {
- $this->output( "Page {$row->page_id} not cacheable\n" );
+ $this->output( "Page '$title' (id {$row->page_id}) not cacheable\n" );
}
}
$this->commitTransaction( $dbw, __METHOD__ ); // commit any changes (just for sanity)
}
public function execute() {
+ global $IP;
+
// We wan't to allow "" for the wikidb, meaning don't call select_db()
$wiki = $this->hasOption( 'wikidb' ) ? $this->getOption( 'wikidb' ) : false;
// Get the appropriate load balancer (for this wiki)
}
}
if ( $index === null ) {
- $this->error( "No replica DB server configured with the name '$server'.", 1 );
+ $this->error( "No replica DB server configured with the name '$replicaDB'.", 1 );
}
} else {
$index = DB_MASTER;
}
- // Get a DB handle (with this wiki's DB selected) from the appropriate load balancer
+
+ /** @var Database $db DB handle for the appropriate cluster/wiki */
$db = $lb->getConnection( $index, [], $wiki );
if ( $replicaDB != '' && $db->getLBInfo( 'master' ) !== null ) {
$this->error( "The server selected ({$db->getServer()}) is not a replica DB.", 1 );
return;
}
- $useReadline = function_exists( 'readline_add_history' )
- && Maintenance::posix_isatty( 0 /*STDIN*/ );
-
- if ( $useReadline ) {
- global $IP;
+ if (
+ function_exists( 'readline_add_history' ) &&
+ Maintenance::posix_isatty( 0 /*STDIN*/ )
+ ) {
$historyFile = isset( $_ENV['HOME'] ) ?
"{$_ENV['HOME']}/.mwsql_history" : "$IP/maintenance/.mwsql_history";
readline_read_history( $historyFile );
+ } else {
+ $historyFile = null;
}
$wholeLine = '';
$prompt = ' -> ';
continue;
}
- if ( $useReadline ) {
+ if ( $historyFile ) {
# Delimiter is eated by streamStatementEnd, we add it
# up in the history (bug 37020)
- readline_add_history( $wholeLine . $db->getDelimiter() );
+ readline_add_history( $wholeLine . ';' );
readline_write_history( $historyFile );
}
$this->sqlDoQuery( $db, $wholeLine, $doDie );
wfWaitForSlaves();
}
- protected function sqlDoQuery( $db, $line, $dieOnError ) {
+ protected function sqlDoQuery( IDatabase $db, $line, $dieOnError ) {
try {
$res = $db->query( $line );
$this->sqlPrintResult( $res, $db );
/**
* Print the results, callback for $db->sourceStream()
* @param ResultWrapper $res The results object
- * @param DatabaseBase $db
+ * @param IDatabase $db
*/
public function sqlPrintResult( $res, $db ) {
if ( !$res ) {
height: auto;
margin: 0 0.1em 0 0;
padding: 0;
- border: 1px solid @colorFieldBorder;
+ border: 1px solid @colorGray7;
cursor: pointer;
}
}
.button-colors( @bgColor, @highlightColor, @activeColor ) when ( lightness( @bgColor ) >= 70% ) {
color: @colorButtonText;
- border: 1px solid @colorGray12;
+ border: 1px solid @colorFieldBorder;
&:hover,
&:active,
.button-colors-quiet( @textColor, @highlightColor, @activeColor ) {
// Quiet buttons all start gray, and reveal
// constructive/progressive/destructive color on hover and active.
- color: @colorButtonText;
+ color: @textColor;
- &:hover,
- &:focus {
+ &:hover {
background-color: transparent;
- color: @textColor;
+ color: @highlightColor;
}
&:active,
color: @activeColor;
}
+ &:focus {
+ background-color: transparent;
+ color: @textColor;
+ }
+
&:disabled {
color: @colorDisabledText;
}
// Although this defines many shades, be parsimonious in your own use of grays. Prefer
// colors already in use in MediaWiki. Prefer semantic color names such as "@colorText".
-@colorGray1: #111; // darkest
+@colorGray1: #000; // darkest
@colorGray2: #222;
@colorGray3: #333;
@colorGray4: #444;
@colorGray5: #555;
@colorGray6: #666;
-@colorGray7: #777;
+@colorGray7: #72777d;
@colorGray8: #888;
@colorGray9: #999;
@colorGray10: #aaa;
@colorGray12: #ccc;
@colorGray13: #ddd;
@colorGray14: #eee;
-@colorGray15: #f9f9f9; // lightest
+@colorGray15: #f8f9fa; // lightest
// Semantic background colors
// Blue; for contextual use of a continuing action
-@colorProgressive: #347bff;
-@colorProgressiveHighlight: #2962cc;
-@colorProgressiveActive: #2962cc;
-// Green; for contextual use of a positive finalizing action
-@colorConstructive: #00af89;
-@colorConstructiveHighlight: #008c6d;
-@colorConstructiveActive: #008c6d;
+@colorProgressive: #36c;
+@colorProgressiveHighlight: #447ff5;
+@colorProgressiveActive: #2a4b8d;
// Orange; for contextual use of returning to a past action
@colorRegressive: #ff5d00;
// Red; for contextual use of a negative action of high severity
-@colorDestructive: #d11d13;
-@colorDestructiveHighlight: #a7170f;
-@colorDestructiveActive: #a7170f;
+@colorDestructive: #c33;
+@colorDestructiveHighlight: #e53939;
+@colorDestructiveActive: #873636;
// Orange; for contextual use of a potentially negative action of medium severity
@colorMediumSevere: #ff5d00;
// Yellow; for contextual use of a potentially negative action of low severity
-@colorLowSevere: #ffb50d;
+@colorLowSevere: #fc3;
// Used in mixins to darken contextual colors by the same amount (eg. focus)
@colorDarkenPercentage: 13.5%;
// Text colors
@colorText: @colorGray2;
@colorTextLight: @colorGray6;
-@colorButtonText: @colorGray5;
-@colorButtonTextHighlight: @colorGray7;
-@colorButtonTextActive: @colorGray7;
+@colorButtonText: @colorGray2;
+@colorButtonTextHighlight: @colorGray4;
+@colorButtonTextActive: @colorGray1;
@colorDisabledText: @colorGray12;
@colorErrorText: #c00;
@colorWarningText: #705000;
// UI colors
-@colorFieldBorder: @colorGray12;
+@colorFieldBorder: #9aa0a7;
@colorShadow: @colorGray14;
@colorPlaceholder: @colorGray10;
@colorNeutral: @colorGray7;
-// The following rules are deprecated
-@colorWhite: #fff;
-@colorOffWhite: #fafafa;
-@colorGrayDark: #898989;
-@colorGrayLight: #ccc;
-@colorGrayLighter: #ddd;
-@colorGrayLightest: #eee;
-
// Global border radius to be used to buttons and inputs
@borderRadius: 2px;
// Form input sizes
@checkboxSize: 2em;
@radioSize: 2em;
+
+// The following rules are deprecated
+@colorWhite: #fff;
+@colorOffWhite: #fafafa;
+@colorGrayDark: #898989;
+@colorGrayLight: #ccc;
+@colorGrayLighter: #ddd;
+@colorGrayLightest: #eee;
+// Green; for contextual use of a positive finalizing action
+@colorConstructive: #00af89;
+@colorConstructiveHighlight: #1c6665;
+@colorConstructiveActive: #134645;
+
/* Login Button, following `ButtonWidget (progressive)` from OOjs UI */
#mw-createaccount-join {
- color: #347bff;
+ background-color: #f8f9fa;
+ color: #36c;
}
#mw-createaccount-join:hover {
- background-color: #ebf2ff; /* rgba( 52, 123, 255, 0.1 ); */
+ background-color: #fff;
border-color: #859ecc;
box-shadow: none;
}
#mw-createaccount-join:active {
- background-color: #ebf2ff;
- color: #1f4999;
- border-color: #1f4999;
+ background-color: #eff3fa;
+ color: #2a4b8d;
+ border-color: #2a4b8d;
}
#mw-createaccount-join:focus {
- background-color: #fff;
- color: #1f4999;
- border-color: #1f4999;
- box-shadow: inset 0 0 0 1px #1f4999;
-}
-#mw-createaccount-join:active:focus {
- background-color: #ebf2ff;
+ border-color: #36c;
+ box-shadow: inset 0 0 0 1px #36c;
}
@indicator-size: unit( 12 / 16 / 0.8, em );
@icon-size: unit( 24 / 16 / 0.8, em );
@quick-ease: 100ms ease;
-@progressive: #347bff;
+@progressive: #36c;
.mw-widget-calendarWidget:focus {
outline: none;
- box-shadow: inset 0 0 0 2px #347bff;
+ box-shadow: inset 0 0 0 2px #36c;
}
.mw-widget-calendarWidget-day {
&.oo-ui-widget-enabled {
.mw-widget-dateInputWidget-handle:hover {
- border-color: #347bff;
+ border-color: #36c;
}
}
}
.mw-upload-bookletLayout-filePreview .oo-ui-progressBarWidget-bar {
- background-color: #347bff;
+ background-color: #36c;
height: 0.5em;
}
\ No newline at end of file