can use MediaWikiTitleCodec::getTitleInvalidRegex() instead.
* HTMLForm & VFormHTMLForm::isVForm(), deprecated in 1.25, have been removed.
* The ProfileSection class, deprecated in 1.25 and unused, has been removed.
+* Wikimedia\Rdbms\SavepointPostgres is deprecated.
== Compatibility ==
MediaWiki 1.31 requires PHP 5.5.9 or later. Although HHVM 3.18.5 or later is supported,
Currently only data attributes reserved to MediaWiki are allowed
(see Sanitizer::isReservedDataAttribute).
+'DeleteUnknownPreferences': Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which
+to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences
+that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed
+with 'gadget-', and so anything with that prefix is excluded from the deletion.
+&where: An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted
+ from the user_properties table.
+$db: The IDatabase object, useful for accessing $db->buildLike() etc.
+
'DifferenceEngineAfterLoadNewText': called in DifferenceEngine::loadNewText()
after the new revision's content has been loaded into the class member variable
$differenceEngine->mNewContent but before returning true from this function.
"apihelp-query+exturlusage-param-namespace": "Espazo de nomes a enumerar.",
"apihelp-query+exturlusage-param-limit": "Cantas páxinas devolver.",
"apihelp-query+exturlusage-param-expandurl": "Expandir as URLs relativas a un protocolo co protocolo canónico.",
- "apihelp-query+exturlusage-example-simple": "Mostrar páxinas ligando a <kbd>https://www.mediawiki.org</kbd>.",
+ "apihelp-query+exturlusage-example-simple": "Amosar páxinas que ligan con <kbd>https://www.mediawiki.org</kbd>.",
"apihelp-query+filearchive-summary": "Enumerar secuencialmente todos os ficheiros borrados.",
"apihelp-query+filearchive-param-from": "Título da imaxe coa que comezar a enumeración.",
"apihelp-query+filearchive-param-to": "Título da imaxe coa que rematar a enumeración.",
* @ingroup Database
*/
abstract class MWLBFactory {
+
+ /** @var array Cache of already-logged deprecation messages */
+ private static $loggedDeprecations = [];
+
/**
* @param array $lbConf Config for LBFactory::__construct()
* @param Config $mainConfig Main config object from MediaWikiServices
'connLogger' => LoggerFactory::getInstance( 'DBConnection' ),
'perfLogger' => LoggerFactory::getInstance( 'DBPerformance' ),
'errorLogger' => [ MWExceptionHandler::class, 'logException' ],
+ 'deprecationLogger' => [ static::class, 'logDeprecation' ],
'cliMode' => $wgCommandLineMode,
'hostname' => wfHostname(),
'readOnlyReason' => $readOnlyMode->getReason(),
] );
}
}
+
+ /**
+ * Log a database deprecation warning
+ * @param string $msg Deprecation message
+ */
+ public static function logDeprecation( $msg ) {
+ global $wgDevelopmentWarnings;
+
+ if ( isset( self::$loggedDeprecations[$msg] ) ) {
+ return;
+ }
+ self::$loggedDeprecations[$msg] = true;
+
+ if ( $wgDevelopmentWarnings ) {
+ trigger_error( $msg, E_USER_DEPRECATED );
+ }
+ wfDebugLog( 'deprecated', $msg, 'private' );
+ }
}
protected $queryLogger;
/** @var callback Error logging callback */
protected $errorLogger;
+ /** @var callback Deprecation logging callback */
+ protected $deprecationLogger;
/** @var resource|null Database connection */
protected $conn = null;
$this->connLogger = $params['connLogger'];
$this->queryLogger = $params['queryLogger'];
$this->errorLogger = $params['errorLogger'];
+ $this->deprecationLogger = $params['deprecationLogger'];
if ( isset( $params['nonNativeInsertSelectBatchSize'] ) ) {
$this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'];
* includes the agent as a SQL comment.
* - trxProfiler: Optional TransactionProfiler instance.
* - errorLogger: Optional callback that takes an Exception and logs it.
+ * - deprecationLogger: Optional callback that takes a string 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.
trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
};
}
+ if ( !isset( $p['deprecationLogger'] ) ) {
+ $p['deprecationLogger'] = function ( $msg ) {
+ trigger_error( $msg, E_USER_DEPRECATED );
+ };
+ }
/** @var Database $conn */
$conn = new $class( $p );
public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
$this->assertTransactionStatus( $sql, $fname );
+ # Avoid fatals if close() was called
+ $this->assertOpen();
+
$priorWritesPending = $this->writesOrCallbacksPending();
$this->lastQuery = $sql;
$this->queryLogger->debug( "{$this->dbName} {$commentedSql}" );
}
- # Avoid fatals if close() was called
- $this->assertOpen();
-
# Send the query to the server and fetch any corresponding errors
$ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
$lastError = $this->lastError();
# In the first case, the only options going forward are (a) ROLLBACK, or
# (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
# option is ROLLBACK, since the snapshots would have been released.
- if ( is_object( $tempIgnore ) ) {
- // Ugly hack to know that savepoints are in use for postgres
- // FIXME: remove this and make DatabasePostgres use ATOMIC_CANCELABLE
- } else {
- $this->trxStatus = self::STATUS_TRX_ERROR;
- $this->trxStatusCause =
- $this->makeQueryException( $lastError, $lastErrno, $sql, $fname );
- $tempIgnore = false; // cannot recover
- }
+ $this->trxStatus = self::STATUS_TRX_ERROR;
+ $this->trxStatusCause =
+ $this->makeQueryException( $lastError, $lastErrno, $sql, $fname );
+ $tempIgnore = false; // cannot recover
} else {
# Nothing prior was there to lose from the transaction,
# so just roll it back.
private function handleSessionLoss() {
// Clean up tracking of session-level things...
// https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html
- // https://www.postgresql.org/docs/9.1/static/sql-createtable.html (ignoring ON COMMIT)
+ // https://www.postgresql.org/docs/9.2/static/sql-createtable.html (ignoring ON COMMIT)
$this->sessionTempTables = [];
// https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
// https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
/** @var resource */
protected $lastResultHandle = null;
- /** @var int The number of rows affected as an integer */
- protected $lastAffectedRowCount = null;
/** @var float|string */
private $numericVersion = null;
private $connectString;
/** @var string */
private $coreSchema;
+ /** @var string */
+ private $tempSchema;
/** @var string[] Map of (reserved table name => alternate table name) */
private $keywordTableMap = [];
}
public function hasConstraint( $name ) {
- $conn = $this->getBindingHandle();
-
- $sql = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n " .
- "WHERE c.connamespace = n.oid AND conname = '" .
- pg_escape_string( $conn, $name ) . "' AND n.nspname = '" .
- pg_escape_string( $conn, $this->getCoreSchema() ) . "'";
- $res = $this->doQuery( $sql );
-
- return $this->numRows( $res );
+ foreach ( $this->getCoreSchemas() as $schema ) {
+ $sql = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n " .
+ "WHERE c.connamespace = n.oid AND conname = " .
+ $this->addQuotes( $name ) . " AND n.nspname = " .
+ $this->addQuotes( $schema );
+ $res = $this->doQuery( $sql );
+ if ( $res && $this->numRows( $res ) ) {
+ return true;
+ }
+ }
+ return false;
}
public function open( $server, $user, $password, $dbName ) {
$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->query( "SET bytea_output = 'escape'", __METHOD__ ); // PHP bug 53127
$this->determineCoreSchema( $this->schema );
// The schema to be used is now in the search path; no need for explicit qualification
throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" );
}
$this->lastResultHandle = pg_get_result( $conn );
- $this->lastAffectedRowCount = null;
if ( pg_result_error( $this->lastResultHandle ) ) {
return false;
}
}
protected function fetchAffectedRowCount() {
- if ( !is_null( $this->lastAffectedRowCount ) ) {
- // Forced result for simulated queries
- return $this->lastAffectedRowCount;
- }
if ( !$this->lastResultHandle ) {
return 0;
}
public 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
+ $schemas = $this->getCoreSchemas();
+ } else {
+ $schemas = [ $schema ];
+ }
+
+ $eindex = $this->addQuotes( $index );
+
+ foreach ( $schemas as $schema ) {
+ $eschema = $this->addQuotes( $schema );
+ /*
+ * 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=$eindex AND ns.nspname=$eschema) 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=$eindex AND n.nspname=$eschema
+ 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 ];
+ $res = $this->query( $sql, __METHOD__ );
+ $a = [];
+ if ( $res ) {
+ foreach ( $res as $row ) {
+ $a[] = [
+ $row->attname,
+ $row->opcname,
+ $row->amname,
+ $row->option ];
+ }
+ return $a;
}
- } else {
- return null;
}
-
- return $a;
+ return null;
}
public function indexUnique( $table, $index, $fname = __METHOD__ ) {
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.
- */
+ /** @inheritDoc */
public function insert( $table, $args, $fname = __METHOD__, $options = [] ) {
if ( !count( $args ) ) {
return true;
}
if ( isset( $args[0] ) && is_array( $args[0] ) ) {
- $multi = true;
+ $rows = $args;
$keys = array_keys( $args[0] );
} else {
- $multi = false;
+ $rows = [ $args ];
$keys = array_keys( $args );
}
- // If IGNORE is set, we use savepoints to emulate mysql's behavior
- // @todo If PostgreSQL 9.5+, we could use ON CONFLICT DO NOTHING instead
- $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
- }
+ $ignore = in_array( 'IGNORE', $options );
$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 ) . ')';
+ if ( $this->numericVersion >= 9.5 || !$ignore ) {
+ // No IGNORE or our PG has "ON CONFLICT DO NOTHING"
+ $first = true;
+ foreach ( $rows as $row ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $sql .= ',';
}
- $res = (bool)$this->query( $sql, $fname, $savepoint );
- } else {
- $res = true;
- $origsql = $sql;
- foreach ( $args as $row ) {
- $tempsql = $origsql;
+ $sql .= '(' . $this->makeList( $row ) . ')';
+ }
+ if ( $ignore ) {
+ $sql .= ' ON CONFLICT DO NOTHING';
+ }
+ $this->query( $sql, $fname );
+ } else {
+ // Emulate IGNORE by doing each row individually, with savepoints
+ // to roll back as necessary.
+ $numrowsinserted = 0;
+
+ $tok = $this->startAtomic( "$fname (outer)", self::ATOMIC_CANCELABLE );
+ try {
+ foreach ( $rows as $row ) {
+ $tempsql = $sql;
$tempsql .= '(' . $this->makeList( $row ) . ')';
- if ( $savepoint ) {
- $savepoint->savepoint();
- }
-
- $tempres = (bool)$this->query( $tempsql, $fname, $savepoint );
-
- if ( $savepoint ) {
- $bar = pg_result_error( $this->lastResultHandle );
- if ( $bar != false ) {
- $savepoint->rollback();
- } else {
- $savepoint->release();
- $numrowsinserted++;
+ $this->startAtomic( "$fname (inner)", self::ATOMIC_CANCELABLE );
+ try {
+ $this->query( $tempsql, $fname );
+ $this->endAtomic( "$fname (inner)" );
+ $numrowsinserted++;
+ } catch ( DBQueryError $e ) {
+ $this->cancelAtomic( "$fname (inner)" );
+ // Our IGNORE is supposed to ignore duplicate key errors, but not others.
+ // (even though MySQL's version apparently ignores all errors)
+ if ( $e->errno !== '23505' ) {
+ throw $e;
}
}
-
- // 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->lastResultHandle );
- if ( $bar != false ) {
- $savepoint->rollback();
- } else {
- $savepoint->release();
- $numrowsinserted++;
}
+ } catch ( Exception $e ) {
+ $this->cancelAtomic( "$fname (outer)", $tok );
+ throw $e;
}
- }
- if ( $savepoint ) {
- error_reporting( $olde );
- $savepoint->commit();
+ $this->endAtomic( "$fname (outer)" );
// Set the affected row count for the whole operation
- $this->lastAffectedRowCount = $numrowsinserted;
-
- // IGNORE always returns true
- return true;
+ $this->affectedRowCount = $numrowsinserted;
}
- return $res;
+ return true;
}
/**
$insertOptions = [ $insertOptions ];
}
- /*
- * If IGNORE is set, use the non-native version.
- * @todo If PostgreSQL 9.5+, we could use ON CONFLICT DO NOTHING
- */
if ( in_array( 'IGNORE', $insertOptions ) ) {
- return $this->nonNativeInsertSelect(
- $destTable, $srcTable, $varMap, $conds, $fname, $insertOptions, $selectOptions, $selectJoinConds
- );
+ if ( $this->getServerVersion() >= 9.5 ) {
+ // Use ON CONFLICT DO NOTHING if we have it for IGNORE
+ $destTable = $this->tableName( $destTable );
+
+ $selectSql = $this->selectSQLText(
+ $srcTable,
+ array_values( $varMap ),
+ $conds,
+ $fname,
+ $selectOptions,
+ $selectJoinConds
+ );
+
+ $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ') ' .
+ $selectSql . ' ON CONFLICT DO NOTHING';
+
+ return $this->query( $sql, $fname );
+ } else {
+ // IGNORE and we don't have ON CONFLICT DO NOTHING, so just use the non-native version
+ return $this->nonNativeInsertSelect(
+ $destTable, $srcTable, $varMap, $conds, $fname,
+ $insertOptions, $selectOptions, $selectJoinConds
+ );
+ }
}
return parent::nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname,
}
public function wasDeadlock() {
- // https://www.postgresql.org/docs/8.2/static/errcodes-appendix.html
+ // https://www.postgresql.org/docs/9.2/static/errcodes-appendix.html
return $this->lastErrno() === '40P01';
}
public function wasLockTimeout() {
- // https://www.postgresql.org/docs/8.2/static/errcodes-appendix.html
+ // https://www.postgresql.org/docs/9.2/static/errcodes-appendix.html
return $this->lastErrno() === '55P03';
}
public function wasConnectionError( $errno ) {
- // https://www.postgresql.org/docs/8.2/static/errcodes-appendix.html
+ // https://www.postgresql.org/docs/9.2/static/errcodes-appendix.html
static $codes = [ '08000', '08003', '08006', '08001', '08004', '57P01', '57P03', '53300' ];
return in_array( $errno, $codes, true );
public function duplicateTableStructure(
$oldName, $newName, $temporary = false, $fname = __METHOD__
) {
- $newName = $this->addIdentifierQuotes( $newName );
- $oldName = $this->addIdentifierQuotes( $oldName );
+ $newNameE = $this->addIdentifierQuotes( $newName );
+ $oldNameE = $this->addIdentifierQuotes( $oldName );
+
+ $ret = $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newNameE " .
+ "(LIKE $oldNameE INCLUDING DEFAULTS INCLUDING INDEXES)", $fname );
+ if ( !$ret ) {
+ return $ret;
+ }
+
+ $res = $this->query( 'SELECT attname FROM pg_class c'
+ . ' JOIN pg_namespace n ON (n.oid = c.relnamespace)'
+ . ' JOIN pg_attribute a ON (a.attrelid = c.oid)'
+ . ' JOIN pg_attrdef d ON (c.oid=d.adrelid and a.attnum=d.adnum)'
+ . ' WHERE relkind = \'r\''
+ . ' AND nspname = ' . $this->addQuotes( $this->getCoreSchema() )
+ . ' AND relname = ' . $this->addQuotes( $oldName )
+ . ' AND adsrc LIKE \'nextval(%\'',
+ $fname
+ );
+ $row = $this->fetchObject( $res );
+ if ( $row ) {
+ $field = $row->attname;
+ $newSeq = "{$newName}_{$field}_seq";
+ $fieldE = $this->addIdentifierQuotes( $field );
+ $newSeqE = $this->addIdentifierQuotes( $newSeq );
+ $newSeqQ = $this->addQuotes( $newSeq );
+ $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " SEQUENCE $newSeqE", $fname );
+ $this->query(
+ "ALTER TABLE $newNameE ALTER COLUMN $fieldE SET DEFAULT nextval({$newSeqQ}::regclass)",
+ $fname
+ );
+ }
- return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName " .
- "(LIKE $oldName INCLUDING DEFAULTS INCLUDING INDEXES)", $fname );
+ return $ret;
+ }
+
+ public function resetSequenceForTable( $table, $fname = __METHOD__ ) {
+ $table = $this->tableName( $table, 'raw' );
+ foreach ( $this->getCoreSchemas() as $schema ) {
+ $res = $this->query(
+ 'SELECT c.oid FROM pg_class c JOIN pg_namespace n ON (n.oid = c.relnamespace)'
+ . ' WHERE relkind = \'r\''
+ . ' AND nspname = ' . $this->addQuotes( $schema )
+ . ' AND relname = ' . $this->addQuotes( $table ),
+ $fname
+ );
+ if ( !$res || !$this->numRows( $res ) ) {
+ continue;
+ }
+
+ $oid = $this->fetchObject( $res )->oid;
+ $res = $this->query( 'SELECT adsrc FROM pg_attribute a'
+ . ' JOIN pg_attrdef d ON (a.attrelid=d.adrelid and a.attnum=d.adnum)'
+ . " WHERE a.attrelid = $oid"
+ . ' AND adsrc LIKE \'nextval(%\'',
+ $fname
+ );
+ $row = $this->fetchObject( $res );
+ if ( $row ) {
+ $this->query(
+ 'SELECT ' . preg_replace( '/^nextval\((.+)\)$/', 'setval($1,1,false)', $row->adsrc ),
+ $fname
+ );
+ return true;
+ }
+ return false;
+ }
+
+ return false;
}
public function listTables( $prefix = null, $fname = __METHOD__ ) {
- $eschema = $this->addQuotes( $this->getCoreSchema() );
+ $eschemas = implode( ',', array_map( [ $this, 'addQuotes' ], $this->getCoreSchemas() ) );
$result = $this->query(
- "SELECT tablename FROM pg_tables WHERE schemaname = $eschema", $fname );
+ "SELECT DISTINCT tablename FROM pg_tables WHERE schemaname IN ($eschemas)", $fname );
$endArray = [];
foreach ( $result as $table ) {
return $this->coreSchema;
}
+ /**
+ * Return schema names for temporary tables and core application tables
+ *
+ * @since 1.31
+ * @return string[] schema names
+ */
+ public function getCoreSchemas() {
+ if ( $this->tempSchema ) {
+ return [ $this->tempSchema, $this->getCoreSchema() ];
+ }
+
+ $res = $this->query(
+ "SELECT nspname FROM pg_catalog.pg_namespace n WHERE n.oid = pg_my_temp_schema()", __METHOD__
+ );
+ $row = $this->fetchObject( $res );
+ if ( $row ) {
+ $this->tempSchema = $row->nspname;
+ return [ $this->tempSchema, $this->getCoreSchema() ];
+ }
+
+ return [ $this->getCoreSchema() ];
+ }
+
public function getServerVersion() {
if ( !isset( $this->numericVersion ) ) {
$conn = $this->getBindingHandle();
$types = [ $types ];
}
if ( $schema === false ) {
- $schema = $this->getCoreSchema();
+ $schemas = $this->getCoreSchemas();
+ } else {
+ $schemas = [ $schema ];
}
$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;
+ foreach ( $schemas as $schema ) {
+ $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 );
+ if ( $res && $res->numRows() ) {
+ return true;
+ }
+ }
- return (bool)$count;
+ return false;
}
/**
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;
+ foreach ( $this->getCoreSchemas() as $schema ) {
+ $res = $this->query(
+ sprintf(
+ $q,
+ $this->addQuotes( $schema ),
+ $this->addQuotes( $table ),
+ $this->addQuotes( $trigger )
+ )
+ );
+ if ( $res && $res->numRows() ) {
+ return true;
+ }
}
- $rows = $res->numRows();
- return $rows;
+ return false;
}
public function ruleExists( $table, $rule ) {
[
'rulename' => $rule,
'tablename' => $table,
- 'schemaname' => $this->getCoreSchema()
+ 'schemaname' => $this->getCoreSchemas()
]
);
}
public 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;
+ foreach ( $this->getCoreSchemas() as $schema ) {
+ $sql = sprintf( "SELECT 1 FROM information_schema.table_constraints " .
+ "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
+ $this->addQuotes( $schema ),
+ $this->addQuotes( $table ),
+ $this->addQuotes( $constraint )
+ );
+ $res = $this->query( $sql );
+ if ( $res && $res->numRows() ) {
+ return true;
+ }
}
- $rows = $res->numRows();
-
- return $rows;
+ return false;
}
/**
return "'" . pg_escape_string( $conn, (string)$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;
- }
-
public function makeSelectOptions( $options ) {
$preLimitTail = $postLimitTail = '';
$startOpts = $useIndex = $ignoreIndex = '';
if ( !parent::lockIsFree( $lockName, $method ) ) {
return false; // already held
}
- // http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
+ // http://www.postgresql.org/docs/9.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
$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 );
}
public function lock( $lockName, $method, $timeout = 5 ) {
- // http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
+ // http://www.postgresql.org/docs/9.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
$key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
$loop = new WaitConditionLoop(
function () use ( $lockName, $key, $timeout, $method ) {
}
public function unlock( $lockName, $method ) {
- // http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
+ // http://www.postgresql.org/docs/9.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
$key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
$result = $this->query( "SELECT pg_advisory_unlock($key) as lockstatus", $method );
$row = $this->fetchObject( $result );
* Manage savepoints within a transaction
* @ingroup Database
* @since 1.19
+ * @deprecated since 1.31, use IDatabase::startAtomic() and such instead.
*/
class SavepointPostgres {
/** @var DatabasePostgres Establish a savepoint within a transaction */
SQL;
$table = $db->remappedTableName( $table );
- $res = $db->query(
- sprintf( $q,
- $db->addQuotes( $db->getCoreSchema() ),
- $db->addQuotes( $table ),
- $db->addQuotes( $field )
- )
- );
- $row = $db->fetchObject( $res );
- if ( !$row ) {
- return null;
+ foreach ( $db->getCoreSchemas() as $schema ) {
+ $res = $db->query(
+ sprintf( $q,
+ $db->addQuotes( $schema ),
+ $db->addQuotes( $table ),
+ $db->addQuotes( $field )
+ )
+ );
+ $row = $db->fetchObject( $res );
+ if ( !$row ) {
+ continue;
+ }
+ $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;
}
- $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;
+ return null;
}
function name() {
* - queryLogger: PSR-3 logger instance. [optional]
* - perfLogger: PSR-3 logger instance. [optional]
* - errorLogger: Callback that takes an Exception and logs it. [optional]
+ * - deprecationLogger: Callback to log a deprecation warning. [optional]
* @throws InvalidArgumentException
*/
public function __construct( array $conf );
protected $perfLogger;
/** @var callable Error logger */
protected $errorLogger;
+ /** @var callable Deprecation logger */
+ protected $deprecationLogger;
/** @var BagOStuff */
protected $srvCache;
/** @var BagOStuff */
$this->errorLogger = isset( $conf['errorLogger'] )
? $conf['errorLogger']
: function ( Exception $e ) {
- trigger_error( E_USER_WARNING, get_class( $e ) . ': ' . $e->getMessage() );
+ trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
+ };
+ $this->deprecationLogger = isset( $conf['deprecationLogger'] )
+ ? $conf['deprecationLogger']
+ : function ( $msg ) {
+ trigger_error( $msg, E_USER_DEPRECATED );
};
$this->profiler = isset( $conf['profiler'] ) ? $conf['profiler'] : null;
'connLogger' => $this->connLogger,
'replLogger' => $this->replLogger,
'errorLogger' => $this->errorLogger,
+ 'deprecationLogger' => $this->deprecationLogger,
'hostname' => $this->hostname,
'cliMode' => $this->cliMode,
'agent' => $this->agent,
* - queryLogger: PSR-3 logger instance. [optional]
* - perfLogger: PSR-3 logger instance. [optional]
* - errorLogger : Callback that takes an Exception and logs it. [optional]
+ * - deprecationLogger: Callback to log a deprecation warning. [optional]
* @throws InvalidArgumentException
*/
public function __construct( array $params );
/** @var callable Exception logger */
private $errorLogger;
+ /** @var callable Deprecation logger */
+ private $deprecationLogger;
/** @var bool */
private $disabled = false;
: function ( Exception $e ) {
trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
};
+ $this->deprecationLogger = isset( $params['deprecationLogger'] )
+ ? $params['deprecationLogger']
+ : function ( $msg ) {
+ trigger_error( $msg, E_USER_DEPRECATED );
+ };
foreach ( [ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ] as $key ) {
$this->$key = isset( $params[$key] ) ? $params[$key] : new NullLogger();
$server['connLogger'] = $this->connLogger;
$server['queryLogger'] = $this->queryLogger;
$server['errorLogger'] = $this->errorLogger;
+ $server['deprecationLogger'] = $this->deprecationLogger;
$server['profiler'] = $this->profiler;
$server['trxProfiler'] = $this->trxProfiler;
// Use the same agent and PHP mode for all DB handles
/**
* @throws MWException
* @param array $m
- * @return HTML|string
+ * @return string HTML
*/
public function magicLinkCallback( $m ) {
if ( isset( $m[1] ) && $m[1] !== '' ) {
/* these are used extensively in SkinTemplate, but also some other places */
/**
- * @param string $urlaction
+ * @param string|string[] $urlaction
* @return string
*/
static function makeMainPageUrl( $urlaction = '' ) {
* URL with the protocol specified.
*
* @param string $name Name of the Special page
- * @param string $urlaction Query to append
+ * @param string|string[] $urlaction Query to append
* @param string|null $proto Protocol to use or null for a local URL
* @return string
*/
/**
* @param string $name
* @param string $subpage
- * @param string $urlaction
+ * @param string|string[] $urlaction
* @return string
*/
static function makeSpecialUrlSubpage( $name, $subpage, $urlaction = '' ) {
/**
* @param string $name
- * @param string $urlaction
+ * @param string|string[] $urlaction
* @return string
*/
static function makeI18nUrl( $name, $urlaction = '' ) {
/**
* @param string $name
- * @param string $urlaction
+ * @param string|string[] $urlaction
* @return string
*/
static function makeUrl( $name, $urlaction = '' ) {
/**
* this can be passed the NS number as defined in Language.php
* @param string $name
- * @param string $urlaction
+ * @param string|string[] $urlaction
* @param int $namespace
* @return string
*/
/**
* these return an array with the 'href' and boolean 'exists'
* @param string $name
- * @param string $urlaction
+ * @param string|string[] $urlaction
* @return array
*/
static function makeUrlDetails( $name, $urlaction = '' ) {
/**
* Make URL details where the article exists (or at least it's convenient to think so)
* @param string $name Article name
- * @param string $urlaction
+ * @param string|string[] $urlaction
* @return array
*/
static function makeKnownUrlDetails( $name, $urlaction = '' ) {
* @param string $details HTML safe text between brackets
* @param bool $oppositedm Add the direction mark opposite to your
* language, to display text properly
- * @return HTML escaped string
+ * @return string HTML escaped
*/
function specialList( $page, $details, $oppositedm = true ) {
if ( !$details ) {
"savearticle-start": "Захаваць старонку…",
"savechanges-start": "Захаваць зьмены…",
"publishpage-start": "Апублікаваць старонку…",
+ "publishchanges-start": "Апублікаваць зьмены…",
"preview": "Папярэдні прагляд",
"showpreview": "Праглядзець",
"showdiff": "Паказаць зьмены",
"grant-editmycssjs": "Рэдагаваньне вашага CSS/JSON/JavaScript",
"grant-editmyoptions": "Рэдагаваньне вашых наладаў удзельніка",
"grant-editmywatchlist": "Рэдагаваньне вашага сьпісу назіраньня",
- "grant-editpage": "Рэдагаваць існыя старонкі",
+ "grant-editpage": "Рэдагаваньне існых старонак",
"grant-editprotected": "Рэдагаваць абароненыя старонкі",
"grant-highvolume": "Рэдагаваньне з высокай інтэнсіўнасьцю",
"grant-oversight": "Хаваньне ўдзельнікаў і вэрсіяў старонак",
"recentchanges-legend": "Настройки на списъка с последни промени",
"recentchanges-summary": "Проследяване на последните промени в {{SITENAME}}.\n\nЛегенда: '''{{int:diff}}''' = разлика на текущата версия,\n'''{{int:hist}}''' = история на версиите",
"recentchanges-noresult": "За дадения период не бяха намерени промени, които да отговарят на критериите.",
+ "recentchanges-timeout": "Времето за търсене изтече. Моля, опитайте да търсите с различни параметри.",
+ "recentchanges-network": "Поради техническа грешка, резултатите не могат да бъдат заредени. Моля, опитайте да презаредите страницата.",
+ "recentchanges-notargetpage": "Въведете име на страница отгоре, за да видите промени, свързани с нея.",
"recentchanges-feed-description": "Проследяване на последните промени в {{SITENAME}}.",
"recentchanges-label-newpage": "Нова страница",
"recentchanges-label-minor": "Това е малка промяна",
"rcfilters-savedqueries-apply-and-setdefault-label": "Създаване на филтър по подразбиране",
"rcfilters-savedqueries-cancel-label": "Отказ",
"rcfilters-savedqueries-add-new-title": "Съхраняване на текущите настройки на филтрите",
+ "rcfilters-savedqueries-already-saved": "Тези филтри вече са съхранени. Променете настройките си, за да създадете нов Запазен филтър.",
"rcfilters-restore-default-filters": "Възстановяване на филтрите по подразбиране",
"rcfilters-clear-all-filters": "Изчистване на всички филтри",
"rcfilters-show-new-changes": "Преглед на най-новите промени",
"rcfilters-highlightmenu-title": "Изберете цвят",
"rcfilters-highlightmenu-help": "Изберете цвят за отбелязване на свойството",
"rcfilters-filterlist-noresults": "Не са намерени филтри",
+ "rcfilters-noresults-conflict": "Няма намерени резултати, тъй като критериите за търсене са в противоречие",
+ "rcfilters-state-message-fullcoverage": "Избирането на всички филтри в тази група е същото като избирането на николко, така че този филтър няма да има ефект. Групата включва: $1",
"rcfilters-filtergroup-authorship": "Авторство на редакциите",
"rcfilters-filter-editsbyself-label": "Ваши редакции",
"rcfilters-filter-editsbyself-description": "Ваши редакции.",
"rcfilters-filter-watchlist-notwatched-description": "Всички, освен промените в страници от списъка за наблюдение.",
"rcfilters-filtergroup-watchlistactivity": "Активност по списъка за наблюдение",
"rcfilters-filter-watchlistactivity-unseen-label": "Невидяни промени",
+ "rcfilters-filter-watchlistactivity-unseen-description": "Промени по страници, които не сте посетили откакто са настъпили промените.",
"rcfilters-filter-watchlistactivity-seen-label": "Видени промени",
+ "rcfilters-filter-watchlistactivity-seen-description": "Промени по страници, които сте посетили откакто са настъпили промените.",
"rcfilters-filtergroup-changetype": "Вид на промяната",
"rcfilters-filter-pageedits-label": "Редакции на страници",
"rcfilters-filter-pageedits-description": "Редакции на съдържанието, беседи, описания на категории...",
"rcfilters-filter-categorization-description": "Записи от добавяне или премахване на страници от категории.",
"rcfilters-filter-logactions-label": "Записани в дневника действия",
"rcfilters-filter-logactions-description": "Административни действия, създавания на сметки, изтривания на страници, качвания...",
+ "rcfilters-hideminor-conflicts-typeofchange": "Определени видове промени не могат да бъдат отбелязвани като „малки“, така че този филтър противоречи със следните филтри за Вид на промяната: $1",
"rcfilters-filtergroup-lastRevision": "Текущи версии",
"rcfilters-filter-lastrevision-label": "Текуща версия",
"rcfilters-filter-lastrevision-description": "Само последната промяна на страница.",
"rcfilters-view-return-to-default-tooltip": "Назад към главното меню на филтрите",
"rcfilters-view-tags-help-icon-tooltip": "Научете повече за Етикетираните редакции",
"rcfilters-liveupdates-button": "Обновяване на живо",
+ "rcfilters-liveupdates-button-title-on": "Изключване на обновяването в реално време",
"rcfilters-liveupdates-button-title-off": "Показване на новите промени в реално време",
"rcfilters-watchlist-markseen-button": "Отбелязване на всички промени като видени",
"rcfilters-watchlist-edit-watchlist-button": "Редактиране на списъка за наблюдение",
+ "rcfilters-watchlist-showupdated": "Промени по страници, които не сте посетили откакто са внесени промените, са в <strong>получер</strong>, с удебелени маркери.",
"rcfilters-preference-label": "Скриване на подобрената версия на Последни промени",
"rcfilters-preference-help": "Премахва новия дизайн на интерфейса от 2017 г. и всички инструменти, добавени тогава и след това.",
+ "rcfilters-filter-showlinkedfrom-label": "Показване на промени на страници, към които има връзка от",
+ "rcfilters-filter-showlinkedfrom-option-label": "<strong>Страници, към които има връзка от</strong> избраната страница",
+ "rcfilters-filter-showlinkedto-label": "Показване на промени на страници, сочещи към",
+ "rcfilters-filter-showlinkedto-option-label": "<strong>Страници, сочещи към</strong> избраната страница",
+ "rcfilters-target-page-placeholder": "Въведете име на страница (или категория)",
"rcnotefrom": "{{PLURAL:$5|Дадена е промяната|Дадени са промените}} от <strong>$3, $4</strong> (до <strong>$1</strong> показани).",
"rclistfromreset": "Нулиране на избора на дата",
"rclistfrom": "Показване на промени, като се започва от $3 $2",
"recentchanges-page-added-to-category": "[[:$1]] е добавена към категория",
"recentchanges-page-added-to-category-bundled": "[[:$1]] е добавена към категория, [[Special:WhatLinksHere/$1|към страницата сочат други страници]]",
"recentchanges-page-removed-from-category": "[[:$1]] е премахната от категория",
+ "recentchanges-page-removed-from-category-bundled": "[[:$1]] е премахната от категория, [[Special:WhatLinksHere/$1|тази страница е включена в други страници]]",
"autochange-username": "Автоматична промяна на МедияУики",
"upload": "Качи файл",
"uploadbtn": "Качване на файл",
"userjspreview": "'''Recordeu que només estau provant/previsualitzant el vostre JavaScript, encara no ho heu desat!'''",
"sitecsspreview": "'''Adoneu-vos que esteu veient una vista prèvia d'aquest full d'estil CSS.'''\n'''Encara no s'ha desat!'''",
"sitejspreview": "'''Tingueu present que esteu previsualitzant aquest codi Javascript.'''\n'''Encara no s'ha desat!'''",
- "userinvalidconfigtitle": "'''Atenció:''' No existeix l'aparença «$1». Recordeu que les subpàgines personalitzades amb extensions .css i .js utilitzen el títol en minúscules, per exemple, {{ns:user}}:NOM/vector.css no és el mateix que {{ns:user}}:NOM/Vector.css.",
+ "userinvalidconfigtitle": "<strong>Atenció:</strong> no existeix l’aparença «$1».\nLes subpàgines personals amb extensions .css, .json i .js utilitzen el títol en minúscules; per exemple, {{ns:user}}:NOM/vector.css no és el mateix que {{ns:user}}:NOM/Vector.css.",
"updated": "(Actualitzat)",
"note": "'''Nota:'''",
"previewnote": "<strong>Recordeu que això és només una previsualització.</strong>\nEls vostres canvis encara no s’han desat!",
"savechanges": "Ӏалашбе хийцамаш",
"publishpage": "АгӀо кхолла",
"publishchanges": "АгӀо дӀаязъян",
+ "savearticle-start": "Ӏалашъе агӀо:",
+ "savechanges-start": "Ӏалашбе хийцамаш...",
+ "publishpage-start": "Арахеца агӀо…",
+ "publishchanges-start": "Арахеца хийцамаш…",
"preview": "Хьалххе хьажар",
"showpreview": "Хьалха хьажар",
"showdiff": "Бина болу хийцамашка хьажар",
"password-login-forbidden": "The use of this username and password has been forbidden.",
"mailmypassword": "Reset password",
"passwordremindertitle": "New temporary password for {{SITENAME}}",
- "passwordremindertext": "Someone (probably you, from IP address $1) requested a new\npassword for {{SITENAME}} ($4). A temporary password for user\n\"$2\" has been created and was set to \"$3\". If this was your\nintent, you will need to log in and choose a new password now.\nYour temporary password will expire in {{PLURAL:$5|one day|$5 days}}.\n\nIf someone else made this request, or if you have remembered your password,\nand you no longer wish to change it, you may ignore this message and\ncontinue using your old password.",
+ "passwordremindertext": "Someone (from IP address $1) requested a new\npassword for {{SITENAME}} ($4). A temporary password for user\n\"$2\" has been created and was set to \"$3\". If this was your\nintent, you will need to log in and choose a new password now.\nYour temporary password will expire in {{PLURAL:$5|one day|$5 days}}.\n\nIf someone else made this request, or if you have remembered your password,\nand you no longer wish to change it, you may ignore this message and\ncontinue using your old password.",
"noemail": "There is no email address recorded for user \"$1\".",
"noemailcreate": "You need to provide a valid email address.",
"passwordsent": "A new password has been sent to the email address registered for \"$1\".\nPlease log in again after you receive it.",
"userjspreview": "<strong>¡Recuerda que solo estás previsualizando tu JavaScript de usuario.\n¡Aún no se ha guardado!</strong>",
"sitecsspreview": "<strong>Recuerda que solo estás previsualizando este CSS.\n¡Aún no se ha guardado!</strong>",
"sitejspreview": "<strong>Recuerda que solo estás previsualizando este código JavaScript.\n¡Aún no se ha guardado!</strong>",
- "userinvalidconfigtitle": "<strong>Advertencia:</strong> no existe la apariencia «$1».\nRecuerda que las páginas personalizadas .css y .js tienen un título en minúsculas. Por ejemplo, se usa {{ns:user}}:Ejemplo/vector.css en vez de {{ns:user}}:Ejemplo/Vector.css.",
+ "userinvalidconfigtitle": "<strong>Atención:</strong> no existe la apariencia «$1».\nLas páginas de archivos .css, .json y .js personalizados comienzan por minúscula; p. ej., se usa «{{ns:user}}:Ejemplo/vector.css» en vez de «{{ns:user}}:Ejemplo/Vector.css».",
"updated": "(Actualizado)",
"note": "<strong>Nota:</strong>",
"previewnote": "<strong>Recuerda que esto no es más que una previsualización.</strong>\nAún no se han guardado tus cambios.",
"apisandbox-dynamic-error-exists": "Ya existe un parámetro llamado \"$1\".",
"apisandbox-deprecated-parameters": "Parámetros desaconsejados",
"apisandbox-fetch-token": "Auto-llenar el token",
+ "apisandbox-add-multi": "Añadir",
"apisandbox-submit-invalid-fields-title": "Algunos campos no son válidos",
"apisandbox-submit-invalid-fields-message": "Corrige los campos señalados e inténtalo de nuevo.",
"apisandbox-results": "Resultados",
"cascadeprotected": "این صفحه در مقابل ویرایش محافظت شدهاست چون در {{PLURAL:$1|صفحهٔ|صفحههای}} محافظتشدهٔ زیر که گزینهٔ «آبشاری» در {{PLURAL:$1|آن|آنها}} انتخاب شده قرار گرفتهاست:\n$2",
"namespaceprotected": "شما اجازهٔ ویرایش صفحههای فضای نام '''$1''' را ندارید.",
"customcssprotected": "شما اجازهٔ ویرایش این صفحهٔ سیاساس را ندارید، زیرا حاوی تنظیمهای شخصی یک کاربر دیگر است.",
+ "customjsonprotected": "شما اجازهٔ ویرایش در این صفحهٔ JSON را ندارید چون دارای تنظیمات شخصی کاربران است.",
"customjsprotected": "شما اجازهٔ ویرایش این صفحهٔ جاوااسکریپت را ندارید، زیرا حاوی تنظیمهای شخصی یک کاربر دیگر است.",
"mycustomcssprotected": "شما دارای مجوز ویرایش این صفحهٔ سیاساس نیستید.",
"mycustomjsprotected": "شما دارای مجوز ویرایش این صفحهٔ جاوااسکریپت نیستید.",
"wrongpasswordempty": "Et voi antaa tyhjää salasanaa.",
"passwordtooshort": "Salasanan täytyy olla vähintään {{PLURAL:$1|yhden merkin pituinen|$1 merkkiä pitkä}}.",
"passwordtoolong": "Salasanat saavat olla enintään $1 {{PLURAL:$1|merkin}} pituisia.",
- "passwordtoopopular": "Tavanomaisen kaltaisia salasanoja ei saa käyttää. Valitse parempi ja yksilöllisempi salasana.",
+ "passwordtoopopular": "Tavanomaisen kaltaisia salasanoja ei saa käyttää. Valitse salasana, joka on vaikeampi arvata.",
"password-name-match": "Salasanasi täytyy olla eri kuin käyttäjätunnuksesi.",
"password-login-forbidden": "Tämän käyttäjänimen ja salasanan käyttö on estetty.",
"mailmypassword": "Hanki uusi salasana",
"savechanges": "Tallenna muutokset",
"publishpage": "Julkaise sivu",
"publishchanges": "Julkaise muutokset",
+ "savearticle-start": "Tallenna sivu",
+ "savechanges-start": "Tallenna muutokset",
+ "publishpage-start": "Julkaise sivu",
+ "publishchanges-start": "Julkaise muutokset",
"preview": "Esikatselu",
"showpreview": "Esikatsele",
"showdiff": "Näytä muutokset",
"userjspreview": "'''Tämä on JavaScriptin esikatselu.'''",
"sitecsspreview": "'''Huomaa, että tämä on vasta CSS:n esikatselu.''' \n'''Muutoksia ei ole vielä tallennettu.'''",
"sitejspreview": "'''Huomaa, että tämä on vasta JavaScript-koodin esikatselu.'''\n'''Muutoksia ei ole vielä tallennettu.'''",
- "userinvalidconfigtitle": "'''Varoitus:''' Tyyliä nimeltä ”$1” ei ole olemassa. Muista, että käyttäjän määrittelemät .css- ja .js-sivut alkavat pienellä alkukirjaimella, esim. {{ns:user}}:Matti Meikäläinen/vector.css eikä {{ns:user}}:Matti Meikäläinen/Vector.css.",
+ "userinvalidconfigtitle": "<strong>Varoitus:</strong> Tyyliä nimeltä ”$1” ei ole olemassa. Muista, että käyttäjän määrittelemät .css-, -json- ja .js-sivut alkavat pienellä alkukirjaimella, esim. {{ns:user}}:Matti Meikäläinen/vector.css eikä {{ns:user}}:Matti Meikäläinen/Vector.css.",
"updated": "(Päivitetty)",
"note": "'''Huomautus:'''",
"previewnote": "'''Tämä on vasta sivun esikatselu.'''\nTekemiäsi muutoksia ei ole vielä tallennettu.",
"right-editcontentmodel": "Muokata sivun sisältömallia (content model)",
"right-editinterface": "Muokata käyttöliittymätekstejä",
"right-editusercss": "Muokata toisten käyttäjien CSS-tiedostoja",
+ "right-edituserjson": "Muokkaa toisten käyttäjien JSON-tiedostoja",
"right-edituserjs": "Muokata toisten käyttäjien JavaScript-tiedostoja",
"right-editmyusercss": "Muokata omia CSS-tiedostoja",
+ "right-editmyuserjson": "Muokkaa omia JSON-tiedostoja",
"right-editmyuserjs": "Muokata omia JavaScript-tiedostoja",
"right-viewmywatchlist": "Nähdä oma tarkkailulista",
"right-editmywatchlist": "Muokata omaa tarkkailulistaasi. (Jotkut toiminnot lisäävät edelleen sivuja listallesi ilmankin tätä oikeutta.)",
"grant-createaccount": "Luoda käyttäjätunnuksia",
"grant-createeditmovepage": "Luoda, muokata ja siirtää sivuja",
"grant-delete": "Poistaa sivuja, yksittäisiä versioita ja lokimerkintöjä",
- "grant-editinterface": "Muokata järjestelmäviesti-nimiavaruutta ja käyttäjien CSS/JavaScript-sivuja",
- "grant-editmycssjs": "Muokata käyttäjän omia CSS/JavaScript-sivuja",
+ "grant-editinterface": "Muokata järjestelmäviesti-nimiavaruutta ja käyttäjien CSS/JSON/JavaScript-sivuja",
+ "grant-editmycssjs": "Muokata käyttäjän omia CSS/JSON/JavaScript-sivuja",
"grant-editmyoptions": "Muokata käyttäjän omia asetuksia",
"grant-editmywatchlist": "Muokata tarkkailulistaasi",
"grant-editpage": "Muokata olemassa olevia sivuja",
"apisandbox-dynamic-error-exists": "Parametri nimellä ”$1” on jo olemassa.",
"apisandbox-deprecated-parameters": "Käytöstä poistuneet parametrit",
"apisandbox-fetch-token": "Lisää token automaattisesti",
+ "apisandbox-add-multi": "Lisää",
"apisandbox-submit-invalid-fields-title": "Jotkin kentät ovat epäkelpoja",
"apisandbox-submit-invalid-fields-message": "Korjaa merkityt kentät ja yritä uudestaan.",
"apisandbox-results": "Tulokset",
"limitreport-postexpandincludesize-value": "$1/$2 {{PLURAL:$2|tavu|tavua}}",
"limitreport-templateargumentsize": "Mallineen argumenttien koko<br />(template argument size)",
"limitreport-templateargumentsize-value": "$1/$2 {{PLURAL:$2|tavu|tavua}}",
- "limitreport-expansiondepth": " Highest expansion depth",
+ "limitreport-expansiondepth": " Suurin laajennussyvyys.",
"limitreport-expensivefunctioncount": "Vaativien jäsenninfunktioiden lukumäärä",
"expandtemplates": "Laajenna mallineet",
"expand_templates_intro": "Tämä toimintosivu ottaa syötteeksi wikitekstiä ja laajentaa kaikki siinä olevat mallineet rekursiivisesti.\nSe myös laajentaa tuetut parserifunktiot kuten\n<code><nowiki>{{</nowiki>#language:...}}</code> ja -muuttujat kuten\n<code><nowiki>{{</nowiki>CURRENTDAY}}</code>.\nKäytännössä se laajentaa melkein kaiken, joka on kaksoisaaltosulkeiden sisällä.",
"apisandbox-dynamic-error-exists": "Un paramètre nommé \"$1\" existe déjà.",
"apisandbox-deprecated-parameters": "Paramètres désuets",
"apisandbox-fetch-token": "Auto-remplissage du jeton",
+ "apisandbox-add-multi": "Ajouter",
"apisandbox-submit-invalid-fields-title": "Certains champs ne sont pas valides",
"apisandbox-submit-invalid-fields-message": "Veuillez corriger les champs marqués et essayez de nouveau.",
"apisandbox-results": "Résultats",
"아라",
"Soul Train",
"Macofe",
- "Matma Rex"
+ "Matma Rex",
+ "Fitoschido"
]
},
"tog-underline": "Solegnér los lims :",
"userjspreview": "<strong>Rapelâd-vos que vos éte ren qu’aprés èprovar prèvêre voutron code JavaScript.\nIl est p’oncor étâ encartâ !</strong>",
"sitecsspreview": "<strong>Rapelâd-vos que vos éte ren qu’aprés prèvêre cela fôlye CSS.\nEl est p’oncor étâye encartâye !</strong>",
"sitejspreview": "<strong>Rapelâd-vos que vos éte ren qu’aprés prèvêre cél code JavaScript.\nIl est p’oncor étâ encartâ !</strong>",
- "userinvalidconfigtitle": "<strong>Atencion :</strong> ègziste gins d’habelyâjo « $1 ».\nRapelâd-vos que les pâges a sè avouéc èxtensions .css et .js emplèyont de titros en petiôtes lètres, per ègzemplo {{ns:user}}:Foo/vector.css et pas {{ns:user}}:Foo/Vector.css.",
+ "userinvalidconfigtitle": "<strong>Atencion :</strong> ègziste gins d’habelyâjo « $1 ».\nRapelâd-vos que les pâges a sè avouéc èxtensions .css, .json et .js emplèyont de titros en petiôtes lètres, per ègzemplo {{ns:user}}:Foo/vector.css et pas {{ns:user}}:Foo/Vector.css.",
"updated": "(Betâ a jorn)",
"note": "<strong>Nota :</strong>",
"previewnote": "<strong>Rapelâd-vos qu’o est ren qu’un apèrçu.</strong>\nVoutros changements sont p’oncor étâs encartâs !",
"translateinterface": "Pou ajouté oben modifyé dé amòrfwazaj pou tout wiki, souplé, itilizé [https://translatewiki.net/ translatewiki.net], projè-a di lokalizasyon lengwistik di MediaWiki.",
"cascadeprotected": "Sa paj protéjé kont modifikasyon-yan pas li sa transkliz pa {{PLURAL:$1|paj-a ki ka swiv, ki té protéjé|paj-ya ki ka swiv, ki té protéjé}} ké lòpsyon « protèksyon an kaskad » aktivé :\n$2",
"namespaceprotected": "Zòt pa gen pèrmisyon-an di modifyé paj-ya di lèspas di non « <strong>$1</strong> ».",
+ "customcssprotected": "Zòt pa gen pèrmisyon-an di modifyé sa féy di stil CSS, pas li ka kontni paramèt pèrsonèl di rounòt itilizatò.",
+ "customjsonprotected": "Zòt pa gen drwè di modifyé sa paj JSON pas li ka kontni paramèt pèrsonèl di rounòt itilizatò.",
+ "customjsprotected": "Zòt pa gen pèrmisyon-an di modifyé sa paj di JavaScript, pas li ka kontni paramèt pèrsonèl di rounòt itilizatò.",
+ "mycustomcssprotected": "Zòt pa gen drwè di modifyé sa paj CSS.",
+ "mycustomjsonprotected": "Zòt pa gen drwè di modifyé sa paj JSON.",
+ "mycustomjsprotected": "Zòt pa gen drwè di modifyé sa paj JavaScript.",
+ "myprivateinfoprotected": "Zòt pa gen drwè di modifyé zòt enfòrmasyon pèrsonèl.",
+ "mypreferencesprotected": "Zòt pa gen drwè di modifyé zòt préférans.",
+ "ns-specialprotected": "Paj spésyal-ya pa pouvé sa modifyé.",
+ "titleprotected": "Sa tit té protéjé kont tout kréyasyon pa [[User:$1|$1]].\nMotif fourni sa <em>$2</em>.",
+ "filereadonlyerror": "Enposib di modifyé fiché-a « $1 » pas répèrtwar-a di fiché « $2 » sa an lèktir sèl.\n\nAdministratò sistèm ki li vérouyé té fourni sa motif : « $3 ».",
+ "invalidtitle-knownnamespace": "Tit pa valid ké lèspas di non « $2 » é entitilé-a « $3 »",
+ "invalidtitle-unknownnamespace": "Tit pa valid ké niméro-a di lèspas di non $1 enkonèt é entitilé-a « $2 »",
+ "exception-nologin": "Pa konèkté",
+ "exception-nologin-text": "Souplé, konèkté zòt kò pou aksédé à sa paj oben sa aksyon.",
+ "exception-nologin-text-manual": "Souplé zòt $1 pou aksédé à sa paj oben sa aksyon.",
+ "virus-badscanner": "Movèz konfigirasyon : analizò di viris enkonèt : <em>$1</em>",
+ "virus-scanfailed": "échèk di analiz-a (kod $1)",
+ "virus-unknownscanner": "antiviris enkonèt :",
+ "logouttext": "<strong>Zòt atchwèlman dékonèkté.</strong>\n\nNoté ki sèrtenn paj pouvé fika òkò afiché kou si zòt toujou konèkté, jouk zòt désidé di éfasé kach-a di zòt navigatò.",
+ "cannotlogoutnow-title": "Enposib di konèkté so kò atchwèlman",
+ "cannotlogoutnow-text": "Dékonèksyon-an pa posib an itilizan $1.",
+ "welcomeuser": "Bèlvini, $1 !",
+ "welcomecreation-msg": "Zòt kont té kréyé.\nZòt pouvé modifyé [[Special:Preferences|zòt préférans]] pou {{SITENAME}} si zòt swété.",
+ "yourname": "Non di itilizatò :",
"userlogin-yourname": "Non di itilizatò",
"userlogin-yourname-ph": "Antré zòt non di itilizatò",
+ "createacct-another-username-ph": "Antré non-an di itilizatò",
+ "yourpassword": "Mo di pas :",
"userlogin-yourpassword": "Mo di pas",
"userlogin-yourpassword-ph": "Antré zòt mo di pas",
"createacct-yourpassword-ph": "Antré oun mo di pas",
+ "yourpasswordagain": "Konfirmé mo di pas :",
"createacct-yourpasswordagain": "Konfirmé mo di pas",
"createacct-yourpasswordagain-ph": "Antré òkò menm mo di pas",
"userlogin-remembermypassword": "Gardé mo sésyon aktiv",
+ "userlogin-signwithsecure": "Itilizé roun konèksyon sékirizé",
+ "cannotlogin-title": "Enposib di konèkté so kò",
+ "cannotlogin-text": "Konèksyon-an pa posib",
+ "cannotloginnow-title": "Enposib di konèkté so kò atchwèlman",
+ "cannotloginnow-text": "Konèksyon-an pa posib an itilizan $1.",
+ "cannotcreateaccount-title": "Kréyasyon di kont enposib",
+ "cannotcreateaccount-text": "Kréyasyon-an dirèk di kont itilizatò pa aktivé asou sa wiki.",
+ "yourdomainname": "Zòt domenn :",
+ "password-change-forbidden": "Zòt pa pouvé modifyé mo di pas asou sa wiki.",
+ "externaldberror": "Swé roun érò prodjwi so kò asou baz di doné di otantifikasyon, swé zòt pa otorizé à mété à jou zòt kont èkstèrn.",
"login": "Konèksyon",
+ "login-security": "Vérifyé zòt idantité",
+ "nav-login-createaccount": "Kréyé roun kont oben konèkté so kò",
+ "logout": "Dékonèkté so kò",
+ "userlogout": "Dékonèksyon",
+ "notloggedin": "Pa konèkté",
"userlogin-noaccount": "Zòt pa gen roun kont ?",
"userlogin-joinproject": "Roujwenn {{SITENAME}}",
"createaccount": "Kréyé roun kont",
"userlogin-resetpassword-link": "Zòt bliyé zòt mo di pas ?",
"userlogin-helplink2": "Èd pou konèkté so kò",
+ "userlogin-loggedin": "Zòt ja konèkté an tan ki $1.\nItilizé fòrmilèr-a ki anba pou konèkté zòt kò ké rounòt kont itilizatò.",
+ "userlogin-reauth": "Zòt divèt roukonèkté zòt kò pou vérifyé ki zòt sa {{GENDER:$1|$1}}.",
+ "userlogin-createanother": "Kréyé rounòt kont",
+ "createacct-emailrequired": "Adrès di kouryé",
"createacct-emailoptional": "Adrès di kouryé (fakiltativ)",
"createacct-email-ph": "Zòt adrès di kouryé",
+ "createacct-another-email-ph": "Antré adrès-a di kouryé",
+ "createaccountmail": "Itilizé roun mo di pas aléyatwar tanporèr é voyé li pou adrès-a di kouryé spésifyé",
+ "createaccountmail-help": "Pé sa itilizé pou kréyé roun kont pou rounòt moun san konèt mo di pas.",
+ "createacct-realname": "Non réyèl (fakiltatif)",
+ "createacct-reason": "Motif",
+ "createacct-reason-ph": "Poukisa zòt kréyé rounòt kont",
+ "createacct-reason-help": "Mésaj afiché annan journal di kréyasyon di kont",
"createacct-submit": "Kréyé zòt kont",
+ "createacct-another-submit": "Kréyé kont-a",
+ "createacct-continue-submit": "Kontinwé kréyasyon-an di kont",
+ "createacct-another-continue-submit": "Kontinwé kréyasyon-an di kont",
"createacct-benefit-heading": "{{SITENAME}} sa ékri pa dé moun kou zòt.",
"createacct-benefit-body1": "modifikasyon{{PLURAL:$1|}}",
"createacct-benefit-body2": "paj{{PLURAL:$1|}}",
"createacct-benefit-body3": "{{PLURAL:$1|kontribitò résant}}",
+ "badretype": "Mo di pas ki zòt sézi pa ka korèsponn.",
+ "usernameinprogress": "Oun kréyasyon di kont pou sa non d'itilizatò ja an kour.\nSouplé, pasyanté.",
+ "userexists": "Non d'itilizatò sézi ja itilizé.\nSouplé, chwézi roun non diféran.",
+ "loginerror": "Érò di konèksyon",
+ "createacct-error": "Érò lò kréyasyon-an di kont",
+ "createaccounterror": "Enposib di kréyé kont-a : $1",
+ "nocookiesnew": "Kont itilizatò té kréyé, mè zòt pa konèkté.\n{{SITENAME}} ka itilizé dé témwen (''cookies'') pou konsèrvé konèksyon-an mè zòt dézaktivé yé.\nSouplé, aktivé yé é roukonèkté zòt kò ké menm non é menm mo di pas.",
+ "nocookieslogin": "{{SITENAME}} itilizé dé témwen (''cookies'') pou konsèrvé konèksyon-an mè zòt dézaktivé yé.\nSouplé, aktivé yé é roukonèkté zòt kò.",
+ "nocookiesfornew": "Kont itilizatò pa té kréyé, pas nou pa té pouvé idantifyé so lorijin.\nVérifyé ki zòt aktivé témwen-yan (''cookies''), roucharjé paj-a é éséyé òkò.",
+ "createacct-loginerror": "Kont-a té byen kréyé mè zòt pa pouvé konèkté zòt kò otomatikman.\nSouplé, [[Special:UserLogin|konèkté zòt kò manwèlman]].",
+ "noname": "Zòt pa sézi roun non d'itilizatò valid.",
+ "loginsuccesstitle": "Konèkté",
+ "mailmypassword": "Réyinisyalizé mo di pas",
+ "noemailcreate": "Zòt divèt fourni roun adrès di kouryé valid",
+ "accountcreated": "Kont kréyé",
"loginlanguagelabel": "Lanng : $1",
"pt-login": "Konèkté so kò",
"pt-login-button": "Konèkté so kò",
"savechanges": "Gardar os cambios",
"publishpage": "Publicar a páxina",
"publishchanges": "Publicar os cambios",
+ "savearticle-start": "Gardar a páxina…",
+ "savechanges-start": "Gardar os cambios…",
+ "publishpage-start": "Publicar a páxina…",
+ "publishchanges-start": "Publicar os cambios…",
"preview": "Vista previa",
"showpreview": "Mostrar a vista previa",
"showdiff": "Mostrar os cambios",
"blocked-notice-logextract": "Este usuario está bloqueado.\nVelaquí está a última entrada do rexistro de bloqueos, por se quere consultala:",
"clearyourcache": "<strong>Nota:</strong> Despois de gardar, cómpre limpar a memoria caché do seu navegador para ver os cambios.\n* <strong>Firefox/Safari:</strong> Prema <em>Maiúsculas</em> á vez que en <em>Recargar</em>, ou prema en <em>Ctrl-F5</em> ou <em>Ctrl-R</em> (<em>⌘-R</em> nos Mac).\n* <strong>Google Chrome:</strong> Prema en <em>Ctrl-Maiús-R</em> (<em>⌘-Maiús-R</em> nos Mac).\n* <strong>Internet Explorer:</strong> Prema <em>Ctrl</em> ao tempo que fai clic en <em>Refrescar</em>, ou prema en <em>Ctrl-F5</em>.\n* <strong>Opera:</strong> Vaia a <em>Menú → Configuración</em> (<em>Opera → Preferencias</em> nos Mac) e logo a <em>Privacidade e seguridade → Limpar os datos de navegación → Ficheiros e imaxes na caché</em>.",
"usercssyoucanpreview": "'''Nota:''' Use o botón \"{{int:showpreview}}\" para verificar o novo CSS antes de gardalo.",
+ "userjsonyoucanpreview": "<strong>Consello:</strong> use o botón «{{int:showpreview}}» para probar o seu novo código JSON antes de gardalo.",
"userjsyoucanpreview": "<strong>Nota:</strong> Use o botón \"{{int:showpreview}}\" para verificar o novo JavaScript antes de gardalo.",
"usercsspreview": "'''Lembre que só está vendo a vista previa do seu CSS de usuario.'''\n'''Este aínda non foi gardado!'''",
+ "userjsonpreview": "<strong>Lembre que tan só está probando/previsualizando a súa configuración de usuario JSON.\nAínda non foi gardada!</strong>",
"userjspreview": "'''Lembre que só está probando/previsualizando o seu JavaScript de usuario.'''\n'''Este aínda non foi gardado!'''",
"sitecsspreview": "'''Lembre que só está vendo a vista previa deste CSS.'''\n'''Este aínda non foi gardado!'''",
+ "sitejsonpreview": "<strong>Lembre que tan só está previsualizando esta configuración JSON.\nAínda non foi gardada!</strong>",
"sitejspreview": "'''Lembre que só está vendo a vista previa deste código JavaScript.'''\n'''Este aínda non foi gardado!'''",
"userinvalidconfigtitle": "<strong>Aviso:</strong> Non hai ningunha aparencia chamada \"$1\".\nLembre que as páxinas .css e .js personalizadas utilizan un título en minúsculas, como por exemplo \"{{ns:user}}:Exemplo/vector.css\" no canto de \"{{ns:user}}:Exemplo/Vector.css\".",
"updated": "(Actualizado)",
"recentchangesdays": "Número de días a mostrar nos cambios recentes:",
"recentchangesdays-max": "Máximo: $1 {{PLURAL:$1|día|días}}",
"recentchangescount": "Número de edicións a mostrar por defecto:",
- "prefs-help-recentchangescount": "Isto inclúe os cambios recentes, os historiais e mais os rexistros.",
+ "prefs-help-recentchangescount": "Número máximo: 1000",
"prefs-help-watchlist-token2": "Esta é a clave secreta da fonte de novas web para a súa lista de vixilancia.\nCalquera persoa que a saiba poderá ler a súa lista de vixilancia; non comparta esta clave.\nSe o precisa, [[Special:ResetTokens|pode restablecela]].",
"savedprefs": "Gardáronse as súas preferencias.",
"savedrights": "Gardáronse os grupos de {{GENDER:$1|usuario|usuaria}} de $1.",
"default": "predeterminado",
"prefs-files": "Ficheiros",
"prefs-custom-css": "CSS personalizado",
+ "prefs-custom-json": "JSON personalizado",
"prefs-custom-js": "JavaScript personalizado",
- "prefs-common-config": "CSS/JavaScript compartido por todas as aparencias:",
+ "prefs-common-config": "CSS/JSON/JavaScript compartido por todas as aparencias:",
"prefs-reset-intro": "Pode usar esta páxina para restablecer as súas preferencias ás que veñen dadas por defecto.\nEste cambio non se poderá desfacer.",
"prefs-emailconfirm-label": "Confirmación do correo:",
"youremail": "Correo electrónico:",
"right-editcontentmodel": "Editar o modelo de contido dunha páxina",
"right-editinterface": "Editar a interface de usuario",
"right-editusercss": "Editar os ficheiros CSS doutros usuarios",
+ "right-edituserjson": "Editar ficheiros JSON doutros usuarios",
"right-edituserjs": "Editar os ficheiros JavaScript doutros usuarios",
"right-editmyusercss": "Editar os ficheiros CSS propios",
+ "right-editmyuserjson": "Editar os ficheiros JSON do propio usuario",
"right-editmyuserjs": "Editar os ficheiros JavaScript propios",
"right-viewmywatchlist": "Ver a lista de vixilancia propia",
"right-editmywatchlist": "Editar a lista de vixilancia propia. Teña en conta que algunhas accións engadirán páxinas igualmente mesmo sen este dereito.",
"grant-createaccount": "Crear contas",
"grant-createeditmovepage": "Crear, editar e mover páxinas",
"grant-delete": "Borrar páxinas, revisións e entradas de rexistro",
- "grant-editinterface": "Editar o espazo de nomes MediaWiki e o CSS/JavaScript de usuario",
- "grant-editmycssjs": "Editar o seu CSS/JavaScript de usuario",
+ "grant-editinterface": "Editar o espazo de nomes MediaWiki e o CSS/JSON/JavaScript de usuario",
+ "grant-editmycssjs": "Editar o seu CSS/JSON/JavaScript de usuario",
"grant-editmyoptions": "Editar as súas preferencias de usuario",
"grant-editmywatchlist": "Editar a súa lista de vixilancia",
"grant-editpage": "Editar páxinas existentes",
"apisandbox-dynamic-error-exists": "Xa existe un parámetro co nome \"$1\".",
"apisandbox-deprecated-parameters": "Parámetros obsoletos",
"apisandbox-fetch-token": "Encher automaticamente o identificador",
+ "apisandbox-add-multi": "Engadir",
"apisandbox-submit-invalid-fields-title": "Algúns campos non son válidos",
"apisandbox-submit-invalid-fields-message": "Por favor, amañe os campos marcados e inténteo de novo.",
"apisandbox-results": "Resultados",
"thumbnail_dest_directory": "Non se puido crear o directorio de destino",
"thumbnail_image-type": "Tipo de imaxe non soportado",
"thumbnail_gd-library": "Configuración da libraría GD incompleta: Falta a función $1",
+ "thumbnail_image-size-zero": "O tamaño do ficheiro de imaxe semella ser cero.",
"thumbnail_image-missing": "Parece que falta o ficheiro: $1",
"thumbnail_image-failure-limit": "Producíronse demasiados ($1 ou máis) intentos fallidos recentes de renderizar esta miniatura. Inténteo de novo máis tarde.",
"import": "Importar páxinas",
"watchlistedit-clear-titles": "Títulos:",
"watchlistedit-clear-submit": "Limpar a lista de vixilancia (isto é permanente!)",
"watchlistedit-clear-done": "Limpouse a súa lista de vixilancia.",
+ "watchlistedit-clear-jobqueue": "A súa lista de vixilancia está a ser eliminada. Isto pode levar algún tempoǃ",
"watchlistedit-clear-removed": "{{PLURAL:$1|Eliminouse un título|Elimináronse $1 títulos}}:",
"watchlistedit-too-many": "Hai demasiadas páxinas para mostrar.",
"watchlisttools-clear": "Limpar a lista de vixilancia",
"limitreport-templateargumentsize-value": "$1/$2 {{PLURAL:$2|byte|bytes}}",
"limitreport-expansiondepth": "Máxima profundidade de expansión",
"limitreport-expensivefunctioncount": "Número de funcións analíticas custosas",
+ "limitreport-unstrip-size-value": "$1/$2 {{PLURAL:$2|byte|bytes}}",
"expandtemplates": "Expandir os modelos",
"expand_templates_intro": "Esta páxina especial toma texto wiki e expande todos os modelos dentro del recursivamente.\nTamén expande as funcións de análise como\n<code><nowiki>{{</nowiki>#language:…}}</code> e variables como\n<code><nowiki>{{</nowiki>CURRENTDAY}}</code>.\nDe feito, expande case calquera cousa entre dúas chaves.",
"expand_templates_title": "Título do contexto, para {{FULLPAGENAME}} etc.:",
"october": "Oktober",
"november": "Nopember",
"december": "Desember",
- "january-gen": "Januari",
+ "january-gen": "Januwari",
"february-gen": "Pebruari",
"march-gen": "Maret",
"april-gen": "April",
"category_header": "Halaman to delomo dalala \"$1\"",
"subcategories": "Subkategori",
"category-media-header": "Media to delomo dalala \"$1\"",
- "category-empty": "<em>Kategori botiye ja o halaman meyalo media.<em>",
- "hidden-categories": "{{PLURAL:$1|Tayadu wanto-wanto'o}}",
- "hidden-category-category": "Kategori wanto-wanto'o",
+ "category-empty": "<em>Dalala botiye ja o halaman meyalo media.<em>",
+ "hidden-categories": "{{PLURAL:$1|Dalala wanto-wanto'o}}",
+ "hidden-category-category": "Dalala wanto-wanto'o",
"category-subcat-count": "{{PLURAL:$2|Kategori boti woluwo subkategori|Kategori boti woluwo {{PLURAL:$1|subkategori|$1 subkategori}} lonto nga'amila $2.}}",
"category-subcat-count-limited": "Kategori boti woluwo {{PLURAL:$1|subkategori|$1 subkategori}}",
"category-article-count": "{{PLURAL:$2|Kategori botiye o tuwango halaman.|Woluwo {{PLURAL:$|$1 halaman}} to delomo kategori, lonto $2 nga'amila.}}",
"poolcounter-usage-error": "Tilala lopohuna:$1",
"aboutsite": "Tomimbihu {{SITENAME}}",
"aboutpage": "Proyek:Tomimbihu",
- "copyright": "Tuwango woluwo sadi-sadia odelo to tibawa $1",
+ "copyright": "Tuwanga botiya sadi-sadia odelo to tibawa $1",
"copyrightpage": "{{ns:project}}:Haku lohutu",
"currentevents": "U yilowali baharu",
"currentevents-url": "Project:U yilowali baharu",
"allarticles": "Nga'amila halaman",
"allpagessubmit": "Ntali",
"allpages-hide-redirects": "Wanto'a mopobale",
- "categories": "Kategori",
+ "categories": "Daputari Dalala",
"listgrouprights-members": "(daputari lo anggota)",
"emailuser": "Lawola surel ta ohu'uwo botiye",
"usermessage-editor": "Sistem lo tahuli",
"sitejspreview": "<strong>זו רק תצוגה מקדימה של סקריפט ה־JavaScript הזה.\nהוא עדיין לא נשמר!</strong>",
"userinvalidconfigtitle": "<strong>אזהרה:</strong> העיצוב \"$1\" אינו קיים.\nדפי <span dir=\"ltr\">.css</span>, דפי <span dir=\"ltr\"><span dir=\"ltr\">.js</span>on</span> ודפי <span dir=\"ltr\">.js</span> מותאמים אישית משתמשים בכותרת עם אותיות קטנות – למשל, {{ns:user}}:דוגמה/vector.css ולא {{ns:user}}:דוגמה/Vector.css.",
"updated": "(מעודכן)",
- "note": "'''הערה:'''",
- "previewnote": "<strong>{{GENDER:|זכור|זִכרי|זִכרו}} שזו רק תצוגה מקדימה.</strong>\nהשינויים {{GENDER:|שלך|שלך|שלכם}} עדיין לא נשמרו!",
+ "note": "<strong>הערה:</strong>",
+ "previewnote": "<strong>זו רק תצוגה מקדימה.</strong>\nהשינויים שלך עדיין לא נשמרו!",
"continue-editing": "מעבר לאזור העריכה",
- "previewconflict": "תצ×\95×\92×\94 ×\9eק×\93×\99×\9e×\94 ×\96×\95 ×\9eצ×\99×\92×\94 ×\9b×\99צ×\93 ×\99×\99ר×\90×\94 ×\94×\98קס×\98 ×\91×\97×\9c×\95×\9f ×\94ער×\99×\9b×\94 ×\94×¢×\9c×\99×\95×\9f, ×\90×\9d ת×\91×\97ר×\95 ×\9cש×\9e×\95ר ×\90×\95ת×\95.",
+ "previewconflict": "תצ×\95×\92×\94 ×\9eק×\93×\99×\9e×\94 ×\96×\95 ×\9eצ×\99×\92×\94 ×\9b×\99צ×\93 ×\99×\99ר×\90×\94 ×\94×\98קס×\98 ×\91×\97×\9c×\95×\9f ×\94ער×\99×\9b×\94 ×\94×¢×\9c×\99×\95×\9f, ×\90×\9d ×\94×\95×\90 ×\99×\99ש×\9eר.",
"session_fail_preview": "מצטערים! לא ניתן לבצע את עריכתכם עקב אובדן מידע הכניסה.\n\nייתכן שנותקתם מהחשבון. <strong>אנא ודאו שאתם עדיין מחוברים לחשבון ונסו שוב.</strong>\nאם זה עדיין לא עובד, נסו [[Special:UserLogout|לצאת מהחשבון]] ולהיכנס אליו שנית, וודאו שהדפדפן שלכם מאפשר קבלת עוגיות מאתר זה.",
"session_fail_preview_html": "מצטערים! לא ניתן לבצע את עריכתם עקב אובדן מידע הכניסה.\n\n<em>כיוון שב{{grammar:תחילית|{{SITENAME}}}} אפשרות השימוש ב־HTML גולמי מופעלת, התצוגה המקדימה מוסתרת כדי למנוע התקפות JavaScript.</em>\n\n<strong>אם זהו ניסיון עריכה לגיטימי, אנא נסו שוב.</strong>\nאם זה עדיין לא עובד, נסו [[Special:UserLogout|לצאת מהחשבון]] ולהיכנס אליו שנית, וודאו שהדפדפן שלכם מאפשר קבלת עוגיות מאתר זה.",
- "token_suffix_mismatch": "'''עריכתך נדחתה כיוון שהדפדפן שלך מחק את תווי הפיסוק באסימון העריכה.'''\nהעריכה נדחתה כדי למנוע בעיות כאלה בטקסט של הדף.\nלעתים התקלה מתרחשת עקב שימוש בשירות פרוקסי אנונימי פגום.",
+ "token_suffix_mismatch": "<strong>עריכתך נדחתה כיוון שהדפדפן שלך מחק את תווי הפיסוק באסימון העריכה.</strong>\nהעריכה נדחתה כדי למנוע בעיות כאלה בטקסט של הדף.\nלעתים התקלה מתרחשת עקב שימוש בשירות פרוקסי אנונימי פגום.",
"edit_form_incomplete": "<strong>חלקים מסוימים מטופס העריכה לא הגיעו לשרת; אנא {{GENDER:|בדוק|בדקי|בדקו}} שהעריכה לא נפגעה ו{{GENDER:|נסה|נסי|נסו}} שוב.</strong>",
"editing": "עריכת הדף \"$1\"",
"creating": "יצירת הדף \"$1\"",
"yourdiff": "Diferi",
"copyrightwarning": "Voluntez memorar ke omna kontributi a {{SITENAME}} esas sub la $2 (Videz $1 por detali).\nSe vu ne deziras ke altri modifikez vua artikli od oli distributesez libere, lore voluntez ne skribar oli hike.<br />\nPublikigante vua skribajo hike, vu asertas ke olu skribesis da vu ipsa o kopiesis de libera fonto.\n'''NE SENDEZ ARTIKLI KUN ''COPYRIGHT'' SEN PERMISO!'''",
"protectedpagewarning": "<strong>Averto: Ica pagino esas protektita por ke nur uzeri kun administero-yuri povas redaktar ol.</strong>\nLa maxim recenta en-registrago provizesas:",
+ "semiprotectedpagewarning": "<strong>Noto:</strong> Ica pagino protektesis, do nur enrejistrita uzeri povos modifikar ol.\nLa lasta modifiko en lua stando ('log') montresas adinfre, quale refero:",
"templatesused": "{{PLURAL:$1|Shablono|Shabloni}} uzata en ica pagino:",
"templatesusedpreview": "{{PLURAL:$1|Shablono|Shabloni}} uzata en ica prevido:",
"templatesusedsection": "{{PLURAL:$1|Shablono|Shabloni}} uzata en ica seciono:",
"permissionserrorstext-withaction": "Vu ne darfas $2, pro la {{PLURAL:$1|kauzo|kauzi}} sequanta:",
"recreate-moveddeleted-warn": "<strong>Atencez: Vu rikreos pagino qua antee efacesis.</strong>\n\nVu mustas konsiderar se esos konvenanta o ne riskribor ol.\nPor vua konoco, la motivo dil antea efaco montresas hike:",
"moveddeleted-notice": "Ica pagino efacesis.\nL'efaco-registraro e la movo-registraro di la pagino povas videsar sequante, por konsulto.",
+ "log-fulllog": "Videz kompleta protokolo ('log')",
"edit-conflict": "Konflikto di editi.",
"postedit-confirmation-created": "La pagino kreesis.",
"postedit-confirmation-saved": "Vua redakto konservesis",
"activeusers-intro": "Yen listo pri uzeri qui laboris en la Wiki dum la lasta $1 {{PLURAL:$1|dio|dii}}.",
"activeusers-from": "Montrez uzeri komencante de:",
"activeusers-noresult": "Nula uzero trovesis.",
+ "listgrouprights": "Permisi dil grupo di uzeri",
"listgrouprights-group": "Grupo",
"listgrouprights-members": "(listo di membri)",
"mailnologin": "Ne sendar adreso",
"logentry-patrol-patrol-auto": "$1 automatale {{GENDER:$2|indikis}} ke la revizo $4 de la pagino $3 surveyesas",
"logentry-newusers-create": "La konto dil uzero $1 kreesis.",
"logentry-newusers-autocreate": "L'uzanto $1 {{GENDER:$2|kreesis}} automatale",
+ "logentry-protect-modify": "$1 {{GENDER:$2|modifikis}} la nivelo di protekto por $3 $4",
+ "logentry-protect-modify-cascade": "$1 {{GENDER:$2|modifikis}} la nivelo di protekto di $3 $4 [kaskade]",
"logentry-upload-upload": "$1 {{GENDER:$2|uploaded}} $3",
"logentry-upload-overwrite": "$1 {{GENDER:$2|parsendis}} nova versiono di $3",
"rightsnone": "(nula)",
"apisandbox-dynamic-error-exists": "Un parametro denominato \"$1\" esiste già.",
"apisandbox-deprecated-parameters": "Parametri sconsigliati",
"apisandbox-fetch-token": "Auto-compila il token",
+ "apisandbox-add-multi": "Aggiungi",
"apisandbox-submit-invalid-fields-title": "Alcuni campi non sono validi",
"apisandbox-submit-invalid-fields-message": "Correggi i campi evidenziati e riprova.",
"apisandbox-results": "Risultati",
"apisandbox-dynamic-parameters-add-placeholder": "Numm vum Parameter",
"apisandbox-dynamic-error-exists": "Et gëtt schonn e Parameter mam Numm \"$1\".",
"apisandbox-deprecated-parameters": "Vereelst Parameter",
+ "apisandbox-add-multi": "Derbäisetzen",
"apisandbox-submit-invalid-fields-title": "E puer Felder sinn net valabel.",
"apisandbox-submit-invalid-fields-message": "Verbessert w.e.g. déi markéiert Felder a probéiert nach eng Kéier.",
"apisandbox-results": "Resultater",
"apisandbox-dynamic-error-exists": "Er bestaat al een parameter met de naam \"$1\".",
"apisandbox-deprecated-parameters": "Verouderde parameters",
"apisandbox-fetch-token": "Het token automatisch invullen",
+ "apisandbox-add-multi": "Toevoegen",
"apisandbox-submit-invalid-fields-title": "Sommige velden zijn ongeldig",
"apisandbox-submit-invalid-fields-message": "Corrigeer de gemarkeerde velden en probeer het opnieuw.",
"apisandbox-results": "Resultaten",
"title-invalid-talk-namespace": "O título de página solicitado refere-se a uma página de discussão que não pode existir.",
"title-invalid-characters": "O título de página solicitado contém carateres inválidos: \"$1\".",
"title-invalid-relative": "O título contém um caminho relativo. Os títulos relativos (./, ../) são inválidos porque normalmente são inacessíveis quando tratados pelo navegador do utilizador.",
- "title-invalid-magic-tilde": "O título de página solicitado contém uma sequência de tis inválida (<nowiki>~~~</nowiki>).",
+ "title-invalid-magic-tilde": "O título de página solicitado contém uma sequência de tiles inválida (<nowiki>~~~</nowiki>).",
"title-invalid-too-long": "O título de página solicitado é demasiado longo. Não pode exceder $1 {{PLURAL:$1|byte|bytes}} em codificação UTF-8.",
"title-invalid-leading-colon": "O título de página solicitado contém um sinal de dois pontos (:) inválido no início.",
"perfcached": "Os seguintes dados encontram-se armazenados na cache e podem não estar atualizados. {{PLURAL:$1|Está disponível na cache um máximo de um resultado|Estão disponíveis na cache um máximo de $1 resultados}}.",
"apisandbox-dynamic-error-exists": "Um parâmetro com o nome \"$1\" já existe.",
"apisandbox-deprecated-parameters": "Parâmetros obsoletos",
"apisandbox-fetch-token": "Auto-preencher o token",
+ "apisandbox-add-multi": "Adicionar",
"apisandbox-submit-invalid-fields-title": "Alguns campos são inválidos",
"apisandbox-submit-invalid-fields-message": "Por favor, corrija os campos marcados e tente novamente.",
"apisandbox-results": "Resultados",
"viewhelppage": "Погледај страницу помоћи",
"categorypage": "Погледај страницу категорије",
"viewtalkpage": "Погледај разговор",
- "otherlanguages": "Ð\94Ñ\80Ñ\83ги Ñ\98езиÑ\86и",
+ "otherlanguages": "Ð\9dа дÑ\80Ñ\83гим Ñ\98езиÑ\86има",
"redirectedfrom": "(преусмерено са $1)",
"redirectpagesub": "Преусмерење",
"redirectto": "Преусмерава на:",
"apisandbox-dynamic-error-exists": "已存在名为“$1”的参数。",
"apisandbox-deprecated-parameters": "弃用参数",
"apisandbox-fetch-token": "自动填充令牌",
+ "apisandbox-add-multi": "添加",
"apisandbox-submit-invalid-fields-title": "一些字段无效",
"apisandbox-submit-invalid-fields-message": "请改正标记的字段并重试。",
"apisandbox-results": "结果",
$this->addOption(
'memory-limit',
'Set a specific memory limit for the script, '
- . '"max" for no limit or "default" to avoid changing it'
+ . '"max" for no limit or "default" to avoid changing it',
+ false,
+ true
);
$this->addOption( 'server', "The protocol and server name to use in URLs, e.g. " .
"http://en.wikipedia.org. This is sometimes necessary because " .
// Remove unknown preferences. Special-case gadget- and userjs- as we can't
// control those names.
if ( $unknown ) {
- $this->deleteByWhere(
- $dbw,
- 'Dropping unknown preferences',
- [
- 'up_property NOT' . $dbw->buildLike( 'gadget-', $dbw->anyString() ),
- 'up_property NOT' . $dbw->buildLike( 'userjs-', $dbw->anyString() ),
- 'up_property NOT IN (' . $dbw->makeList( array_keys( $wgDefaultUserOptions ) ) . ')',
- ]
- );
+ $where = [
+ 'up_property NOT' . $dbw->buildLike( 'gadget-', $dbw->anyString() ),
+ 'up_property NOT' . $dbw->buildLike( 'userjs-', $dbw->anyString() ),
+ 'up_property NOT IN (' . $dbw->makeList( array_keys( $wgDefaultUserOptions ) ) . ')',
+ ];
+ // Allow extensions to add to the where clause to prevent deletion of their own prefs.
+ Hooks::run( 'DeleteUnknownPreferences', [ &$where, $dbw ] );
+ $this->deleteByWhere( $dbw, 'Dropping unknown preferences', $where );
}
// Something something phase 3
);
$last = null;
- $autopatrolls = [];
+ $autopatrols = [];
foreach ( $result as $row ) {
$last = $row->log_id;
Wikimedia\suppressWarnings();
$auto = $params['6::auto'];
if ( $auto ) {
- $autopatrolls[] = $row->log_id;
+ $autopatrols[] = $row->log_id;
}
}
return null;
}
- return [ 'rows' => $autopatrolls, 'lastId' => $last ];
+ return [ 'rows' => $autopatrols, 'lastId' => $last ];
}
private function deleteRows( array $rows ) {
$mw$
BEGIN
IF TG_OP = 'INSERT' THEN
- NEW.titlevector = to_tsvector('default',REPLACE(NEW.page_title,'/',' '));
+ NEW.titlevector = to_tsvector(REPLACE(NEW.page_title,'/',' '));
ELSIF NEW.page_title != OLD.page_title THEN
- NEW.titlevector := to_tsvector('default',REPLACE(NEW.page_title,'/',' '));
+ NEW.titlevector := to_tsvector(REPLACE(NEW.page_title,'/',' '));
END IF;
RETURN NEW;
END;
CREATE INDEX job_timestamp_idx ON job (job_timestamp);
-- Tsearch2 2 stuff. Will fail if we don't have proper access to the tsearch2 tables
--- Version 8.3 or higher only. Previous versions would need another parmeter for to_tsvector.
-- Make sure you also change patch-tsearch2funcs.sql if the funcs below change.
ALTER TABLE page ADD titlevector tsvector;
CREATE TRIGGER ts2_page_text BEFORE INSERT OR UPDATE ON pagecontent
FOR EACH ROW EXECUTE PROCEDURE ts2_page_text();
--- These are added by the setup script due to version compatibility issues
--- If using 8.1, we switch from "gin" to "gist"
-
CREATE INDEX ts2_page_title ON page USING gin(titlevector);
CREATE INDEX ts2_page_text ON pagecontent USING gin(textvector);
'TestUserRegistry' => "$testDir/phpunit/includes/TestUserRegistry.php",
'LessFileCompilationTest' => "$testDir/phpunit/LessFileCompilationTest.php",
'MediaWikiCoversValidator' => "$testDir/phpunit/MediaWikiCoversValidator.php",
+ 'PHPUnit4And6Compat' => "$testDir/phpunit/PHPUnit4And6Compat.php",
# tests/phpunit/includes
'RevisionDbTestBase' => "$testDir/phpunit/includes/RevisionDbTestBase.php",
'ParserTestTopLevelSuite' => "$testDir/phpunit/suites/ParserTestTopLevelSuite.php",
];
// phpcs:enable
+
+/**
+ * Alias any PHPUnit 4 era PHPUnit_... class
+ * to it's PHPUnit 6 replacement. For most classes
+ * this is a direct _ -> \ replacement, but for
+ * some others we might need to maintain a manual
+ * mapping. Once we drop support for PHPUnit 4 this
+ * should be considered deprecated and eventually removed.
+ */
+spl_autoload_register( function ( $class ) {
+ if ( strpos( $class, 'PHPUnit_' ) !== 0 ) {
+ // Skip if it doesn't start with the old prefix
+ return;
+ }
+
+ // Classes that don't map 100%
+ $map = [
+ 'PHPUnit_Framework_TestSuite_DataProvider' => 'PHPUnit\Framework\DataProviderTestSuite'
+ ];
+
+ if ( isset( $map[$class] ) ) {
+ $newForm = $map[$class];
+ } else {
+ $newForm = str_replace( '_', '\\', $class );
+ }
+
+ if ( class_exists( $newForm ) ) {
+ // If the new class name exists, alias
+ // the old name to it.
+ class_alias( $newForm, $class );
+ }
+} );
abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
use MediaWikiCoversValidator;
+ use PHPUnit4And6Compat;
/**
* The service locator created by prepareServices(). This service locator will
$db->delete( $tbl, '*', __METHOD__ );
}
+ if ( $db->getType() === 'postgres' ) {
+ // Reset the table's sequence too.
+ $db->resetSequenceForTable( $tbl, __METHOD__ );
+ }
+
if ( $tbl === 'page' ) {
// Forget about the pages since they don't
// exist in the DB.
}
}
- /**
- * @since 1.18
- *
- * @param string $func
- * @param array $args
- *
- * @return mixed
- * @throws MWException
- */
- public function __call( $func, $args ) {
- static $compatibility = [
- 'createMock' => 'createMock2',
- ];
-
- if ( isset( $compatibility[$func] ) ) {
- return call_user_func_array( [ $this, $compatibility[$func] ], $args );
- } else {
- throw new MWException( "Called non-existent $func method on " . static::class );
- }
- }
-
- /**
- * Return a test double for the specified class.
- *
- * @param string $originalClassName
- * @return PHPUnit_Framework_MockObject_MockObject
- * @throws Exception
- */
- private function createMock2( $originalClassName ) {
- return $this->getMockBuilder( $originalClassName )
- ->disableOriginalConstructor()
- ->disableOriginalClone()
- ->disableArgumentCloning()
- // New in phpunit-mock-objects 3.2 (phpunit 5.4.0)
- // ->disallowMockingUnknownTypes()
- ->getMock();
- }
-
private static function unprefixTable( &$tableName, $ind, $prefix ) {
$tableName = substr( $tableName, strlen( $prefix ) );
}
--- /dev/null
+<?php
+/**
+ * Copyright (C) 2018 Kunal Mehta <legoktm@member.fsf.org>
+ *
+ * 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.
+ *
+ */
+
+/**
+ * @since 1.31
+ */
+trait PHPUnit4And6Compat {
+ /**
+ * @see PHPUnit_Framework_TestCase::setExpectedException
+ *
+ * This function was renamed to expectException() in PHPUnit 6, so this
+ * is a temporary backwards-compatibility layer while we transition.
+ */
+ public function setExpectedException( $name, $message = '', $code = null ) {
+ if ( is_callable( [ $this, 'expectException' ] ) ) {
+ $this->expectException( $name );
+ if ( $message !== '' ) {
+ $this->expectExceptionMessage( $message );
+ }
+ if ( $code !== null ) {
+ $this->expectExceptionCode( $code );
+ }
+ } else {
+ parent::setExpectedException( $name, $message, $code );
+ }
+ }
+
+ /**
+ * @see PHPUnit_Framework_TestCase::getMock
+ *
+ * @return PHPUnit_Framework_MockObject_MockObject
+ */
+ public function getMock( $originalClassName, $methods = [], array $arguments = [],
+ $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true,
+ $callAutoload = true, $cloneArguments = false, $callOriginalMethods = false,
+ $proxyTarget = null
+ ) {
+ if ( is_callable( 'parent::getMock' ) ) {
+ return parent::getMock(
+ $originalClassName, $methods, $arguments, $mockClassName,
+ $callOriginalConstructor, $callOriginalClone, $callAutoload,
+ $cloneArguments, $callOriginalMethods, $proxyTarget
+ );
+ } else {
+ $builder = $this->getMockBuilder( $originalClassName )
+ ->setMethods( $methods )
+ ->setConstructorArgs( $arguments )
+ ->setMockClassName( $mockClassName )
+ ->setProxyTarget( $proxyTarget );
+ if ( $callOriginalConstructor ) {
+ $builder->enableOriginalConstructor();
+ } else {
+ $builder->disableOriginalConstructor();
+ }
+ if ( $callOriginalClone ) {
+ $builder->enableOriginalClone();
+ } else {
+ $builder->disableOriginalClone();
+ }
+ if ( $callAutoload ) {
+ $builder->enableAutoload();
+ } else {
+ $builder->disableAutoload();
+ }
+ if ( $cloneArguments ) {
+ $builder->enableArgumentCloning();
+ } else {
+ $builder->disableArgumentCloning();
+ }
+ if ( $callOriginalMethods ) {
+ $builder->enableProxyingToOriginalMethods();
+ } else {
+ $builder->disableProxyingToOriginalMethods();
+ }
+
+ return $builder->getMock();
+ }
+ }
+
+ /**
+ * Return a test double for the specified class. This
+ * is a forward port of the createMock function that
+ * was introduced in PHPUnit 5.4.
+ *
+ * @param string $originalClassName
+ * @return PHPUnit_Framework_MockObject_MockObject
+ * @throws Exception
+ */
+ public function createMock( $originalClassName ) {
+ if ( is_callable( 'parent::createMock' ) ) {
+ return parent::createMock( $originalClassName );
+ }
+ // Compat for PHPUnit <= 5.4
+ return $this->getMockBuilder( $originalClassName )
+ ->disableOriginalConstructor()
+ ->disableOriginalClone()
+ ->disableArgumentCloning()
+ // New in phpunit-mock-objects 3.2 (phpunit 5.4.0)
+ // ->disallowMockingUnknownTypes()
+ ->getMock();
+ }
+}
$this->assertFalse( MWNamespace::isMovable( NS_SPECIAL ) );
}
+ private function assertIsSubject( $ns ) {
+ $this->assertTrue( MWNamespace::isSubject( $ns ) );
+ }
+
+ private function assertIsNotSubject( $ns ) {
+ $this->assertFalse( MWNamespace::isSubject( $ns ) );
+ }
+
/**
* Please make sure to change testIsTalk() if you change the assertions below
* @covers MWNamespace::isSubject
$this->assertIsNotSubject( 101 ); # user defined
}
+ private function assertIsTalk( $ns ) {
+ $this->assertTrue( MWNamespace::isTalk( $ns ) );
+ }
+
+ private function assertIsNotTalk( $ns ) {
+ $this->assertFalse( MWNamespace::isTalk( $ns ) );
+ }
+
/**
* Reverse of testIsSubject().
* Please update testIsSubject() if you change assertions below
$this->assertSame( $actual, $expected, "NS $index" );
}
+ private function assertIsContent( $ns ) {
+ $this->assertTrue( MWNamespace::isContent( $ns ) );
+ }
+
+ private function assertIsNotContent( $ns ) {
+ $this->assertFalse( MWNamespace::isContent( $ns ) );
+ }
+
/**
* @covers MWNamespace::isContent
*/
$this->assertIsContent( NS_MAIN );
}
+ private function assertIsWatchable( $ns ) {
+ $this->assertTrue( MWNamespace::isWatchable( $ns ) );
+ }
+
+ private function assertIsNotWatchable( $ns ) {
+ $this->assertFalse( MWNamespace::isWatchable( $ns ) );
+ }
+
/**
* @covers MWNamespace::isWatchable
*/
$this->assertIsWatchable( 101 );
}
+ private function assertHasSubpages( $ns ) {
+ $this->assertTrue( MWNamespace::hasSubpages( $ns ) );
+ }
+
+ private function assertHasNotSubpages( $ns ) {
+ $this->assertFalse( MWNamespace::hasSubpages( $ns ) );
+ }
+
/**
* @covers MWNamespace::hasSubpages
*/
"Subject namespaces should not have NS_SPECIAL" );
}
+ private function assertIsCapitalized( $ns ) {
+ $this->assertTrue( MWNamespace::isCapitalized( $ns ) );
+ }
+
+ private function assertIsNotCapitalized( $ns ) {
+ $this->assertFalse( MWNamespace::isCapitalized( $ns ) );
+ }
+
/**
* Some namespaces are always capitalized per code definition
* in MWNamespace::$alwaysCapitalizedNamespaces
$this->assertFalse( MWNamespace::isNonincludable( NS_TEMPLATE ) );
}
- # ###### HELPERS ###########################################################
- function __call( $method, $args ) {
- // Call the real method if it exists
- if ( method_exists( $this, $method ) ) {
- return $this->$method( $args );
- }
-
- if ( preg_match(
- '/^assert(Has|Is|Can)(Not|)(Subject|Talk|Watchable|Content|Subpages|Capitalized)$/',
- $method,
- $m
- ) ) {
- # Interprets arguments:
- $ns = $args[0];
- $msg = isset( $args[1] ) ? $args[1] : " dummy message";
-
- # Forge the namespace constant name:
- if ( $ns === 0 ) {
- $ns_name = "NS_MAIN";
- } else {
- $ns_name = "NS_" . strtoupper( MWNamespace::getCanonicalName( $ns ) );
- }
- # ... and the MWNamespace method name
- $nsMethod = strtolower( $m[1] ) . $m[3];
-
- $expect = ( $m[2] === '' );
- $expect_name = $expect ? 'TRUE' : 'FALSE';
-
- return $this->assertEquals( $expect,
- MWNamespace::$nsMethod( $ns, $msg ),
- "MWNamespace::$nsMethod( $ns_name ) should returns $expect_name"
- );
- }
-
- throw new Exception( __METHOD__ . " could not find a method named $method\n" );
- }
-
- function assertSameSubject( $ns1, $ns2, $msg = '' ) {
- $this->assertTrue( MWNamespace::subjectEquals( $ns1, $ns2, $msg ) );
+ private function assertSameSubject( $ns1, $ns2, $msg = '' ) {
+ $this->assertTrue( MWNamespace::subjectEquals( $ns1, $ns2 ), $msg );
}
- function assertDifferentSubject( $ns1, $ns2, $msg = '' ) {
- $this->assertFalse( MWNamespace::subjectEquals( $ns1, $ns2, $msg ) );
+ private function assertDifferentSubject( $ns1, $ns2, $msg = '' ) {
+ $this->assertFalse( MWNamespace::subjectEquals( $ns1, $ns2 ), $msg );
}
}
'trxProfiler' => new TransactionProfiler(),
'connLogger' => new \Psr\Log\NullLogger(),
'queryLogger' => new \Psr\Log\NullLogger(),
- 'errorLogger' => new \Psr\Log\NullLogger(),
+ 'errorLogger' => function () {
+ },
+ 'deprecationLogger' => function () {
+ },
'type' => 'test',
'dbname' => $dbName,
'tablePrefix' => $dbPrefix,
--- /dev/null
+<?php
+
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\DatabasePostgres;
+use Wikimedia\ScopedCallback;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group Database
+ */
+class DatabasePostgresTest extends MediaWikiTestCase {
+
+ private function doTestInsertIgnore() {
+ $reset = new ScopedCallback( function () {
+ if ( $this->db->explicitTrxActive() ) {
+ $this->db->rollback( __METHOD__ );
+ }
+ $this->db->query( 'DROP TABLE IF EXISTS ' . $this->db->tableName( 'foo' ) );
+ } );
+
+ $this->db->query(
+ "CREATE TEMPORARY TABLE {$this->db->tableName( 'foo' )} (i INTEGER NOT NULL PRIMARY KEY)"
+ );
+ $this->db->insert( 'foo', [ [ 'i' => 1 ], [ 'i' => 2 ] ], __METHOD__ );
+
+ // Normal INSERT IGNORE
+ $this->db->begin( __METHOD__ );
+ $this->db->insert(
+ 'foo', [ [ 'i' => 3 ], [ 'i' => 2 ], [ 'i' => 5 ] ], __METHOD__, [ 'IGNORE' ]
+ );
+ $this->assertSame( 2, $this->db->affectedRows() );
+ $this->assertSame(
+ [ '1', '2', '3', '5' ],
+ $this->db->selectFieldValues( 'foo', 'i', [], __METHOD__, [ 'ORDER BY' => 'i' ] )
+ );
+ $this->db->rollback( __METHOD__ );
+
+ // INSERT IGNORE doesn't ignore stuff like NOT NULL violations
+ $this->db->begin( __METHOD__ );
+ $this->db->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+ try {
+ $this->db->insert(
+ 'foo', [ [ 'i' => 7 ], [ 'i' => null ] ], __METHOD__, [ 'IGNORE' ]
+ );
+ $this->db->endAtomic( __METHOD__ );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( DBQueryError $e ) {
+ $this->assertSame( 0, $this->db->affectedRows() );
+ $this->db->cancelAtomic( __METHOD__ );
+ }
+ $this->assertSame(
+ [ '1', '2' ],
+ $this->db->selectFieldValues( 'foo', 'i', [], __METHOD__, [ 'ORDER BY' => 'i' ] )
+ );
+ $this->db->rollback( __METHOD__ );
+ }
+
+ /**
+ * @covers Wikimedia\Rdbms\DatabasePostgres::insert
+ */
+ public function testInsertIgnoreOld() {
+ if ( !$this->db instanceof DatabasePostgres ) {
+ $this->markTestSkipped( 'Not PostgreSQL' );
+ }
+ if ( $this->db->getServerVersion() < 9.5 ) {
+ $this->doTestInsertIgnore();
+ } else {
+ // Hack version to make it take the old code path
+ $w = TestingAccessWrapper::newFromObject( $this->db );
+ $oldVer = $w->numericVersion;
+ $w->numericVersion = 9.4;
+ try {
+ $this->doTestInsertIgnore();
+ } finally {
+ $w->numericVersion = $oldVer;
+ }
+ }
+ }
+
+ /**
+ * @covers Wikimedia\Rdbms\DatabasePostgres::insert
+ */
+ public function testInsertIgnoreNew() {
+ if ( !$this->db instanceof DatabasePostgres ) {
+ $this->markTestSkipped( 'Not PostgreSQL' );
+ }
+ if ( $this->db->getServerVersion() < 9.5 ) {
+ $this->markTestSkipped( 'PostgreSQL version is ' . $this->db->getServerVersion() );
+ }
+
+ $this->doTestInsertIgnore();
+ }
+
+ private function doTestInsertSelectIgnore() {
+ $reset = new ScopedCallback( function () {
+ if ( $this->db->explicitTrxActive() ) {
+ $this->db->rollback( __METHOD__ );
+ }
+ $this->db->query( 'DROP TABLE IF EXISTS ' . $this->db->tableName( 'foo' ) );
+ $this->db->query( 'DROP TABLE IF EXISTS ' . $this->db->tableName( 'bar' ) );
+ } );
+
+ $this->db->query(
+ "CREATE TEMPORARY TABLE {$this->db->tableName( 'foo' )} (i INTEGER)"
+ );
+ $this->db->query(
+ "CREATE TEMPORARY TABLE {$this->db->tableName( 'bar' )} (i INTEGER NOT NULL PRIMARY KEY)"
+ );
+ $this->db->insert( 'bar', [ [ 'i' => 1 ], [ 'i' => 2 ] ], __METHOD__ );
+
+ // Normal INSERT IGNORE
+ $this->db->begin( __METHOD__ );
+ $this->db->insert( 'foo', [ [ 'i' => 3 ], [ 'i' => 2 ], [ 'i' => 5 ] ], __METHOD__ );
+ $this->db->insertSelect( 'bar', 'foo', [ 'i' => 'i' ], [], __METHOD__, [ 'IGNORE' ] );
+ $this->assertSame( 2, $this->db->affectedRows() );
+ $this->assertSame(
+ [ '1', '2', '3', '5' ],
+ $this->db->selectFieldValues( 'bar', 'i', [], __METHOD__, [ 'ORDER BY' => 'i' ] )
+ );
+ $this->db->rollback( __METHOD__ );
+
+ // INSERT IGNORE doesn't ignore stuff like NOT NULL violations
+ $this->db->begin( __METHOD__ );
+ $this->db->insert( 'foo', [ [ 'i' => 7 ], [ 'i' => null ] ], __METHOD__ );
+ $this->db->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+ try {
+ $this->db->insertSelect( 'bar', 'foo', [ 'i' => 'i' ], [], __METHOD__, [ 'IGNORE' ] );
+ $this->db->endAtomic( __METHOD__ );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( DBQueryError $e ) {
+ $this->assertSame( 0, $this->db->affectedRows() );
+ $this->db->cancelAtomic( __METHOD__ );
+ }
+ $this->assertSame(
+ [ '1', '2' ],
+ $this->db->selectFieldValues( 'bar', 'i', [], __METHOD__, [ 'ORDER BY' => 'i' ] )
+ );
+ $this->db->rollback( __METHOD__ );
+ }
+
+ /**
+ * @covers Wikimedia\Rdbms\DatabasePostgres::nativeInsertSelect
+ */
+ public function testInsertSelectIgnoreOld() {
+ if ( !$this->db instanceof DatabasePostgres ) {
+ $this->markTestSkipped( 'Not PostgreSQL' );
+ }
+ if ( $this->db->getServerVersion() < 9.5 ) {
+ $this->doTestInsertSelectIgnore();
+ } else {
+ // Hack version to make it take the old code path
+ $w = TestingAccessWrapper::newFromObject( $this->db );
+ $oldVer = $w->numericVersion;
+ $w->numericVersion = 9.4;
+ try {
+ $this->doTestInsertSelectIgnore();
+ } finally {
+ $w->numericVersion = $oldVer;
+ }
+ }
+ }
+
+ /**
+ * @covers Wikimedia\Rdbms\DatabasePostgres::nativeInsertSelect
+ */
+ public function testInsertSelectIgnoreNew() {
+ if ( !$this->db instanceof DatabasePostgres ) {
+ $this->markTestSkipped( 'Not PostgreSQL' );
+ }
+ if ( $this->db->getServerVersion() < 9.5 ) {
+ $this->markTestSkipped( 'PostgreSQL version is ' . $this->db->getServerVersion() );
+ }
+
+ $this->doTestInsertSelectIgnore();
+ }
+
+}
use Page;
use User;
use XMLReader;
+use MWException;
/**
* Base TestCase for dumps