Merge "rdbms: make LoadBalancer::getConnection() ignore CONN_TRX_AUTO when unusable"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 2 Mar 2018 19:26:49 +0000 (19:26 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 2 Mar 2018 19:26:49 +0000 (19:26 +0000)
40 files changed:
RELEASE-NOTES-1.31
includes/EditPage.php
includes/Title.php
includes/config/EtcdConfig.php
includes/installer/DatabaseUpdater.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseMssql.php
includes/libs/rdbms/database/DatabaseMysqlBase.php
includes/libs/rdbms/database/DatabasePostgres.php
includes/parser/Parser.php
includes/resourceloader/ResourceLoader.php
includes/specials/SpecialRecentchanges.php
includes/specials/pagers/ContribsPager.php
includes/specials/pagers/NewFilesPager.php
includes/user/User.php
maintenance/archives/upgradeLogging.php
maintenance/clearInterwikiCache.php
maintenance/migrateActors.php
maintenance/populateBacklinkNamespace.php
maintenance/populateFilearchiveSha1.php
maintenance/populateIpChanges.php
maintenance/populateLogSearch.php
maintenance/populateLogUsertext.php
maintenance/populateParentId.php
maintenance/populateRecentChangesSource.php
maintenance/populateRevisionLength.php
maintenance/populateRevisionSha1.php
maintenance/rebuildFileCache.php
maintenance/refreshLinks.php
maintenance/storage/checkStorage.php
maintenance/storage/fixT22757.php
maintenance/storage/moveToExternal.php
maintenance/storage/orphanStats.php
maintenance/storage/resolveStubs.php
maintenance/storage/storageTypeStats.php
maintenance/storage/trackBlobs.php
maintenance/updateRestrictions.php
tests/phpunit/includes/config/EtcdConfigTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php

index 8113314..0c4bc68 100644 (file)
@@ -256,15 +256,25 @@ changes to languages because of Phabricator reports.
   * Title::isCssJsSubpage – use ::isUserConfigPage
   * Title::isCssSubpage – use ::isUserCssConfigPage
   * Title::isJsSubpage – use ::isUserJsConfigPage
-* The following variables and method in EditPage, deprecated in MediaWiki 1.30, were removed:
+* The following variables and methods in EditPage, deprecated in MediaWiki 1.30, were removed:
   * $isCssJsSubpage — use ::isUserConfigPage()
   * $isCssSubpage — use ::isUserCssConfigPage()
   * $isJsSubpage — use ::isUserJsConfigPage()
   * $isWrongCaseCssJsPage – use ::isWrongCaseUserConfigPage()
+  * ::getSummaryInput() – use ::getSummaryInputWidget()
+  * ::getSummaryInputOOUI() – use ::getSummaryInputWidget()
+  * ::getCheckboxes() – use ::getCheckboxesWidget() or ::getCheckboxesDefinition()
+  * ::getCheckboxesOOUI() – use ::getCheckboxesWidget() or ::getCheckboxesDefinition()
 * The method ResourceLoaderModule::getPosition(), deprecated in 1.29, has been removed.
 * The DeferredStringifier class is deprecated, use Message::listParam() instead.
 * The type string for the parameter $lang of DateFormatter::getInstance is
   deprecated.
+* In User, the cookie-related methods which were wrappers for the functions on the response
+  object, and were deprecated in 1.27, have been removed:
+  * ::setCookie()
+  * ::clearCookie()
+  * ::setExtendedLoginCookie()
+  Note that User::setCookies() remains, and is not deprecated.
 * The global functions wfProfileIn and wfProfileOut, deprecated in 1.25, have been removed.
 
 == Compatibility ==
index 08c4a72..ad5f75d 100644 (file)
@@ -3153,62 +3153,6 @@ ERROR;
                ];
        }
 
-       /**
-        * Standard summary input and label (wgSummary), abstracted so EditPage
-        * subclasses may reorganize the form.
-        * Note that you do not need to worry about the label's for=, it will be
-        * inferred by the id given to the input. You can remove them both by
-        * passing [ 'id' => false ] to $userInputAttrs.
-        *
-        * @deprecated since 1.30 Use getSummaryInputWidget() instead
-        * @param string $summary The value of the summary input
-        * @param string $labelText The html to place inside the label
-        * @param array $inputAttrs Array of attrs to use on the input
-        * @param array $spanLabelAttrs Array of attrs to use on the span inside the label
-        * @return array An array in the format [ $label, $input ]
-        */
-       public function getSummaryInput( $summary = "", $labelText = null,
-               $inputAttrs = null, $spanLabelAttrs = null
-       ) {
-               wfDeprecated( __METHOD__, '1.30' );
-               $inputAttrs = $this->getSummaryInputAttributes( $inputAttrs );
-               $inputAttrs += Linker::tooltipAndAccesskeyAttribs( 'summary' );
-
-               $spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : [] ) + [
-                       'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary',
-                       'id' => "wpSummaryLabel"
-               ];
-
-               $label = null;
-               if ( $labelText ) {
-                       $label = Xml::tags(
-                               'label',
-                               $inputAttrs['id'] ? [ 'for' => $inputAttrs['id'] ] : null,
-                               $labelText
-                       );
-                       $label = Xml::tags( 'span', $spanLabelAttrs, $label );
-               }
-
-               $input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs );
-
-               return [ $label, $input ];
-       }
-
-       /**
-        * Builds a standard summary input with a label.
-        *
-        * @deprecated since 1.30 Use getSummaryInputWidget() instead
-        * @param string $summary The value of the summary input
-        * @param string $labelText The html to place inside the label
-        * @param array $inputAttrs Array of attrs to use on the input
-        *
-        * @return OOUI\FieldLayout OOUI FieldLayout with Label and Input
-        */
-       function getSummaryInputOOUI( $summary = "", $labelText = null, $inputAttrs = null ) {
-               wfDeprecated( __METHOD__, '1.30' );
-               return $this->getSummaryInputWidget( $summary, $labelText, $inputAttrs );
-       }
-
        /**
         * Builds a standard summary input with a label.
         *
@@ -4225,76 +4169,6 @@ ERROR;
                return $checkboxes;
        }
 
-       /**
-        * Returns an array of html code of the following checkboxes old style:
-        * minor and watch
-        *
-        * @deprecated since 1.30 Use getCheckboxesWidget() or getCheckboxesDefinition() instead
-        * @param int &$tabindex Current tabindex
-        * @param array $checked See getCheckboxesDefinition()
-        * @return array
-        */
-       public function getCheckboxes( &$tabindex, $checked ) {
-               wfDeprecated( __METHOD__, '1.30' );
-               $checkboxes = [];
-               $checkboxesDef = $this->getCheckboxesDefinition( $checked );
-
-               // Backwards-compatibility for the EditPageBeforeEditChecks hook
-               if ( !$this->isNew ) {
-                       $checkboxes['minor'] = '';
-               }
-               $checkboxes['watch'] = '';
-
-               foreach ( $checkboxesDef as $name => $options ) {
-                       $legacyName = isset( $options['legacy-name'] ) ? $options['legacy-name'] : $name;
-                       $label = $this->context->msg( $options['label-message'] )->parse();
-                       $attribs = [
-                               'tabindex' => ++$tabindex,
-                               'id' => $options['id'],
-                       ];
-                       $labelAttribs = [
-                               'for' => $options['id'],
-                       ];
-                       if ( isset( $options['tooltip'] ) ) {
-                               $attribs['accesskey'] = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
-                               $labelAttribs['title'] = Linker::titleAttrib( $options['tooltip'], 'withaccess' );
-                       }
-                       if ( isset( $options['title-message'] ) ) {
-                               $labelAttribs['title'] = $this->context->msg( $options['title-message'] )->text();
-                       }
-                       if ( isset( $options['label-id'] ) ) {
-                               $labelAttribs['id'] = $options['label-id'];
-                       }
-                       $checkboxHtml =
-                               Xml::check( $name, $options['default'], $attribs ) .
-                               '&#160;' .
-                               Xml::tags( 'label', $labelAttribs, $label );
-
-                       $checkboxes[ $legacyName ] = $checkboxHtml;
-               }
-
-               // Avoid PHP 7.1 warning of passing $this by reference
-               $editPage = $this;
-               Hooks::run( 'EditPageBeforeEditChecks', [ &$editPage, &$checkboxes, &$tabindex ], '1.29' );
-               return $checkboxes;
-       }
-
-       /**
-        * Returns an array of checkboxes for the edit form, including 'minor' and 'watch' checkboxes and
-        * any other added by extensions.
-        *
-        * @deprecated since 1.30 Use getCheckboxesWidget() or getCheckboxesDefinition() instead
-        * @param int &$tabindex Current tabindex
-        * @param array $checked Array of checkbox => bool, where bool indicates the checked
-        *                 status of the checkbox
-        *
-        * @return array Associative array of string keys to OOUI\FieldLayout instances
-        */
-       public function getCheckboxesOOUI( &$tabindex, $checked ) {
-               wfDeprecated( __METHOD__, '1.30' );
-               return $this->getCheckboxesWidget( $tabindex, $checked );
-       }
-
        /**
         * Returns an array of checkboxes for the edit form, including 'minor' and 'watch' checkboxes and
         * any other added by extensions.
index 6dc7db5..66aadeb 100644 (file)
@@ -1320,7 +1320,7 @@ class Title implements LinkTarget {
         * @deprecated Since 1.31; use ::isSiteConfigPage() instead
         */
        public function isCssOrJsPage() {
-               // wfDeprecated( __METHOD__, '1.31' );
+               wfDeprecated( __METHOD__, '1.31' );
                return ( NS_MEDIAWIKI == $this->mNamespace
                                && ( $this->hasContentModel( CONTENT_MODEL_CSS )
                                        || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
@@ -1348,7 +1348,7 @@ class Title implements LinkTarget {
         * @deprecated Since 1.31; use ::isUserConfigPage() instead
         */
        public function isCssJsSubpage() {
-               // wfDeprecated( __METHOD__, '1.31' );
+               wfDeprecated( __METHOD__, '1.31' );
                return ( NS_USER == $this->mNamespace && $this->isSubpage()
                                && ( $this->hasContentModel( CONTENT_MODEL_CSS )
                                        || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
@@ -1398,7 +1398,7 @@ class Title implements LinkTarget {
         * @return bool
         */
        public function isCssSubpage() {
-               // wfDeprecated( __METHOD__, '1.31' );
+               wfDeprecated( __METHOD__, '1.31' );
                return $this->isUserCssConfigPage();
        }
 
@@ -1417,11 +1417,11 @@ class Title implements LinkTarget {
        }
 
        /**
-        * @deprecated Since 1.31; use ::isUserCssConfigPage()
+        * @deprecated Since 1.31; use ::isUserJsConfigPage()
         * @return bool
         */
        public function isJsSubpage() {
-               // wfDeprecated( __METHOD__, '1.31' );
+               wfDeprecated( __METHOD__, '1.31' );
                return $this->isUserJsConfigPage();
        }
 
index 3811da3..7020159 100644 (file)
@@ -119,6 +119,11 @@ class EtcdConfig implements Config, LoggerAwareInterface {
                return $this->procCache['config'][$name];
        }
 
+       public function getModifiedIndex() {
+               $this->load();
+               return $this->procCache['modifiedIndex'];
+       }
+
        /**
         * @throws ConfigException
         */
@@ -151,13 +156,17 @@ class EtcdConfig implements Config, LoggerAwareInterface {
                                // refresh the cache from etcd, using a mutex to reduce stampedes...
                                if ( $this->srvCache->lock( $key, 0, $this->baseCacheTTL ) ) {
                                        try {
-                                               list( $config, $error, $retry ) = $this->fetchAllFromEtcd();
-                                               if ( is_array( $config ) ) {
+                                               $etcdResponse = $this->fetchAllFromEtcd();
+                                               $error = $etcdResponse['error'];
+                                               if ( is_array( $etcdResponse['config'] ) ) {
                                                        // Avoid having all servers expire cache keys at the same time
                                                        $expiry = microtime( true ) + $this->baseCacheTTL;
                                                        $expiry += mt_rand( 0, 1e6 ) / 1e6 * $this->skewCacheTTL;
-
-                                                       $data = [ 'config' => $config, 'expires' => $expiry ];
+                                                       $data = [
+                                                               'config' => $etcdResponse['config'],
+                                                               'expires' => $expiry,
+                                                               'modifiedIndex' => $etcdResponse['modifiedIndex']
+                                                       ];
                                                        $this->srvCache->set( $key, $data, BagOStuff::TTL_INDEFINITE );
 
                                                        $this->logger->info( "Refreshed stale etcd configuration cache." );
@@ -165,7 +174,7 @@ class EtcdConfig implements Config, LoggerAwareInterface {
                                                        return WaitConditionLoop::CONDITION_REACHED;
                                                } else {
                                                        $this->logger->error( "Failed to fetch configuration: $error" );
-                                                       if ( !$retry ) {
+                                                       if ( !$etcdResponse['retry'] ) {
                                                                // Fail fast since the error is likely to keep happening
                                                                return WaitConditionLoop::CONDITION_FAILED;
                                                        }
@@ -195,9 +204,10 @@ class EtcdConfig implements Config, LoggerAwareInterface {
        }
 
        /**
-        * @return array (config array or null, error string, allow retries)
+        * @return array (containing the keys config, error, retry, modifiedIndex)
         */
        public function fetchAllFromEtcd() {
+               // TODO: inject DnsSrvDiscoverer in order to be able to test this method
                $dsd = new DnsSrvDiscoverer( $this->host );
                $servers = $dsd->getServers();
                if ( !$servers ) {
@@ -209,8 +219,8 @@ class EtcdConfig implements Config, LoggerAwareInterface {
                        $server = $dsd->pickServer( $servers );
                        $host = IP::combineHostAndPort( $server['target'], $server['port'] );
                        // Try to load the config from this particular server
-                       list( $config, $error, $retry ) = $this->fetchAllFromEtcdServer( $host );
-                       if ( is_array( $config ) || !$retry ) {
+                       $response = $this->fetchAllFromEtcdServer( $host );
+                       if ( is_array( $response['config'] ) || $response['retry'] ) {
                                break;
                        }
 
@@ -218,12 +228,12 @@ class EtcdConfig implements Config, LoggerAwareInterface {
                        $servers = $dsd->removeServer( $server, $servers );
                } while ( $servers );
 
-               return [ $config, $error, $retry ];
+               return $response;
        }
 
        /**
         * @param string $address Host and port
-        * @return array (config array or null, error string, whether to allow retries)
+        * @return array (containing the keys config, error, retry, modifiedIndex)
         */
        protected function fetchAllFromEtcdServer( $address ) {
                // Retrieve all the values under the MediaWiki config directory
@@ -233,19 +243,21 @@ class EtcdConfig implements Config, LoggerAwareInterface {
                        'headers' => [ 'content-type' => 'application/json' ]
                ] );
 
+               $response = [ 'config' => null, 'error' => null, 'retry' => false, 'modifiedIndex' => 0 ];
+
                static $terminalCodes = [ 404 => true ];
                if ( $rcode < 200 || $rcode > 399 ) {
-                       return [
-                               null,
-                               strlen( $rerr ) ? $rerr : "HTTP $rcode ($rdesc)",
-                               empty( $terminalCodes[$rcode] )
-                       ];
+                       $response['error'] = strlen( $rerr ) ? $rerr : "HTTP $rcode ($rdesc)";
+                       $response['retry'] = empty( $terminalCodes[$rcode] );
+                       return $response;
                }
+
                try {
-                       return [ $this->parseResponse( $rbody ), null, false ];
+                       $parsedResponse = $this->parseResponse( $rbody );
                } catch ( EtcdConfigParseError $e ) {
-                       return [ null, $e->getMessage(), false ];
+                       $parsedResponse = [ 'error' => $e->getMessage() ];
                }
+               return array_merge( $response, $parsedResponse );
        }
 
        /**
@@ -264,8 +276,8 @@ class EtcdConfig implements Config, LoggerAwareInterface {
                                "Unexpected JSON response: Missing or invalid node at top level." );
                }
                $config = [];
-               $this->parseDirectory( '', $info['node'], $config );
-               return $config;
+               $lastModifiedIndex = $this->parseDirectory( '', $info['node'], $config );
+               return [ 'modifiedIndex' => $lastModifiedIndex, 'config' => $config ];
        }
 
        /**
@@ -275,8 +287,10 @@ class EtcdConfig implements Config, LoggerAwareInterface {
         * @param string $dirName The relative directory name
         * @param array $dirNode The decoded directory node
         * @param array &$config The output array
+        * @return int lastModifiedIndex The maximum last modified index across all keys in the directory
         */
        protected function parseDirectory( $dirName, $dirNode, &$config ) {
+               $lastModifiedIndex = 0;
                if ( !isset( $dirNode['nodes'] ) ) {
                        throw new EtcdConfigParseError(
                                "Unexpected JSON response in dir '$dirName'; missing 'nodes' list." );
@@ -290,16 +304,19 @@ class EtcdConfig implements Config, LoggerAwareInterface {
                        $baseName = basename( $node['key'] );
                        $fullName = $dirName === '' ? $baseName : "$dirName/$baseName";
                        if ( !empty( $node['dir'] ) ) {
-                               $this->parseDirectory( $fullName, $node, $config );
+                               $lastModifiedIndex = max(
+                                       $this->parseDirectory( $fullName, $node, $config ),
+                                       $lastModifiedIndex );
                        } else {
                                $value = $this->unserialize( $node['value'] );
                                if ( !is_array( $value ) || !array_key_exists( 'val', $value ) ) {
                                        throw new EtcdConfigParseError( "Failed to parse value for '$fullName'." );
                                }
-
+                               $lastModifiedIndex = max( $node['modifiedIndex'], $lastModifiedIndex );
                                $config[$fullName] = $value['val'];
                        }
                }
+               return $lastModifiedIndex;
        }
 
        /**
index 500bc5a..0d55454 100644 (file)
@@ -1047,7 +1047,7 @@ abstract class DatabaseUpdater {
         * Sets the number of active users in the site_stats table
         */
        protected function doActiveUsersInit() {
-               $activeUsers = $this->db->selectField( 'site_stats', 'ss_active_users', false, __METHOD__ );
+               $activeUsers = $this->db->selectField( 'site_stats', 'ss_active_users', '', __METHOD__ );
                if ( $activeUsers == -1 ) {
                        $activeUsers = $this->db->selectField( 'recentchanges',
                                'COUNT( DISTINCT rc_user_text )',
@@ -1227,7 +1227,7 @@ abstract class DatabaseUpdater {
                                "maintenance/migrateComments.php.\n"
                        );
                        $task = $this->maintenance->runChild( MigrateComments::class, 'migrateComments.php' );
-                       $task->execute();
+                       $ok = $task->execute();
                        $this->output( $ok ? "done.\n" : "errors were encountered.\n" );
                }
        }
index c699374..8ccccc3 100644 (file)
@@ -1461,14 +1461,27 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
                        $this->makeSelectOptions( $options );
 
-               if ( !empty( $conds ) ) {
-                       if ( is_array( $conds ) ) {
-                               $conds = $this->makeList( $conds, self::LIST_AND );
-                       }
+               if ( is_array( $conds ) ) {
+                       $conds = $this->makeList( $conds, self::LIST_AND );
+               }
+
+               if ( $conds === null || $conds === false ) {
+                       $this->queryLogger->warning(
+                               __METHOD__
+                               . ' called from '
+                               . $fname
+                               . ' with incorrect parameters: $conds must be a string or an array'
+                       );
+                       $conds = '';
+               }
+
+               if ( $conds === '' ) {
+                       $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
+               } elseif ( is_string( $conds ) ) {
                        $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex " .
                                "WHERE $conds $preLimitTail";
                } else {
-                       $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
+                       throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
                }
 
                if ( isset( $options['LIMIT'] ) ) {
@@ -2145,8 +2158,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
 
                // We can't separate explicit JOIN clauses with ',', use ' ' for those
-               $implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
-               $explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
+               $implicitJoins = $ret ? implode( ',', $ret ) : "";
+               $explicitJoins = $retJOIN ? implode( ' ', $retJOIN ) : "";
 
                // Compile our final table clause
                return implode( ' ', [ $implicitJoins, $explicitJoins ] );
@@ -2292,11 +2305,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $rows = [ $rows ];
                }
 
-               $useTrx = !$this->trxLevel;
-               if ( $useTrx ) {
-                       $this->begin( $fname, self::TRANSACTION_INTERNAL );
-               }
                try {
+                       $this->startAtomic( $fname );
                        $affectedRowCount = 0;
                        foreach ( $rows as $row ) {
                                // Delete rows which collide with this one
@@ -2329,17 +2339,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->insert( $table, $row, $fname );
                                $affectedRowCount += $this->affectedRows();
                        }
+                       $this->endAtomic( $fname );
+                       $this->affectedRowCount = $affectedRowCount;
                } catch ( Exception $e ) {
-                       if ( $useTrx ) {
-                               $this->rollback( $fname, self::FLUSHING_INTERNAL );
-                       }
+                       $this->rollback( $fname, self::FLUSHING_INTERNAL );
                        throw $e;
                }
-               if ( $useTrx ) {
-                       $this->commit( $fname, self::FLUSHING_INTERNAL );
-               }
-
-               $this->affectedRowCount = $affectedRowCount;
        }
 
        /**
@@ -2405,11 +2410,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
 
                $affectedRowCount = 0;
-               $useTrx = !$this->trxLevel;
-               if ( $useTrx ) {
-                       $this->begin( $fname, self::TRANSACTION_INTERNAL );
-               }
                try {
+                       $this->startAtomic( $fname );
                        # Update any existing conflicting row(s)
                        if ( $where !== false ) {
                                $ok = $this->update( $table, $set, $where, $fname );
@@ -2420,16 +2422,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        # Now insert any non-conflicting row(s)
                        $ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
                        $affectedRowCount += $this->affectedRows();
+                       $this->endAtomic( $fname );
+                       $this->affectedRowCount = $affectedRowCount;
                } catch ( Exception $e ) {
-                       if ( $useTrx ) {
-                               $this->rollback( $fname, self::FLUSHING_INTERNAL );
-                       }
+                       $this->rollback( $fname, self::FLUSHING_INTERNAL );
                        throw $e;
                }
-               if ( $useTrx ) {
-                       $this->commit( $fname, self::FLUSHING_INTERNAL );
-               }
-               $this->affectedRowCount = $affectedRowCount;
 
                return $ok;
        }
@@ -2487,11 +2485,16 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                return $this->query( $sql, $fname );
        }
 
-       public function insertSelect(
+       final public function insertSelect(
                $destTable, $srcTable, $varMap, $conds,
                $fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = []
        ) {
-               if ( $this->cliMode ) {
+               static $hints = [ 'NO_AUTO_COLUMNS' ];
+
+               $insertOptions = (array)$insertOptions;
+               $selectOptions = (array)$selectOptions;
+
+               if ( $this->cliMode && $this->isInsertSelectSafe( $insertOptions, $selectOptions ) ) {
                        // For massive migrations with downtime, we don't want to select everything
                        // into memory and OOM, so do all this native on the server side if possible.
                        return $this->nativeInsertSelect(
@@ -2500,7 +2503,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $varMap,
                                $conds,
                                $fname,
-                               $insertOptions,
+                               array_diff( $insertOptions, $hints ),
                                $selectOptions,
                                $selectJoinConds
                        );
@@ -2512,12 +2515,22 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $varMap,
                        $conds,
                        $fname,
-                       $insertOptions,
+                       array_diff( $insertOptions, $hints ),
                        $selectOptions,
                        $selectJoinConds
                );
        }
 
+       /**
+        * @param array $insertOptions INSERT options
+        * @param array $selectOptions SELECT options
+        * @return bool Whether an INSERT SELECT with these options will be replication safe
+        * @since 1.31
+        */
+       protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
+               return true;
+       }
+
        /**
         * Implementation of insertSelect() based on select() and insert()
         *
@@ -2537,8 +2550,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $fname = __METHOD__,
                $insertOptions = [], $selectOptions = [], $selectJoinConds = []
        ) {
-               $insertOptions = array_diff( (array)$insertOptions, [ 'NO_AUTO_COLUMNS' ] );
-
                // For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
                // on only the master (without needing row-based-replication). It also makes it easy to
                // know how big the INSERT is going to be.
@@ -2583,12 +2594,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->affectedRowCount = $affectedRowCount;
                        } else {
                                $this->rollback( $fname, self::FLUSHING_INTERNAL );
-                               $this->affectedRowCount = 0;
                        }
                        return $ok;
                } catch ( Exception $e ) {
                        $this->rollback( $fname, self::FLUSHING_INTERNAL );
-                       $this->affectedRowCount = 0;
                        throw $e;
                }
        }
@@ -2617,7 +2626,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                if ( !is_array( $insertOptions ) ) {
                        $insertOptions = [ $insertOptions ];
                }
-               $insertOptions = array_diff( (array)$insertOptions, [ 'NO_AUTO_COLUMNS' ] );
 
                $insertOptions = $this->makeInsertOptions( $insertOptions );
 
@@ -3220,6 +3228,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                } catch ( Exception $e ) {
                        // already logged; let LoadBalancer move on during mass-rollback
                }
+
+               $this->affectedRowCount = 0; // for the sake of consistency
        }
 
        /**
index b1c8909..771e2e5 100644 (file)
@@ -569,7 +569,7 @@ class DatabaseMssql extends Database {
                        }
                }
 
-               return empty( $result ) ? false : $result;
+               return $result ?: false;
        }
 
        /**
index 454e0c2..a5220b9 100644 (file)
@@ -67,6 +67,8 @@ abstract class DatabaseMysqlBase extends Database {
        private $serverVersion = null;
        /** @var bool|null */
        private $insertSelectIsSafe = null;
+       /** @var stdClass|null */
+       private $replicationInfoRow = null;
 
        /**
         * Additional $params include:
@@ -508,20 +510,35 @@ abstract class DatabaseMysqlBase extends Database {
                return $this->nativeReplace( $table, $rows, $fname );
        }
 
-       protected function nativeInsertSelect(
-               $destTable, $srcTable, $varMap, $conds,
-               $fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = []
-       ) {
-               $isSafe = in_array( 'NO_AUTO_COLUMNS', $insertOptions, true );
-               if ( !$isSafe && $this->insertSelectIsSafe === null ) {
-                       // In MySQL, an INSERT SELECT is only replication safe with row-based
-                       // replication or if innodb_autoinc_lock_mode is 0. When those
-                       // conditions aren't met, use non-native mode.
-                       // While we could try to determine if the insert is safe anyway by
-                       // checking if the target table has an auto-increment column that
-                       // isn't set in $varMap, that seems unlikely to be worth the extra
-                       // complexity.
-                       $row = $this->selectRow(
+       protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
+               $row = $this->getReplicationSafetyInfo();
+               // For row-based-replication, the resulting changes will be relayed, not the query
+               if ( $row->binlog_format === 'ROW' ) {
+                       return true;
+               }
+               // LIMIT requires ORDER BY on a unique key or it is non-deterministic
+               if ( isset( $selectOptions['LIMIT'] ) ) {
+                       return false;
+               }
+               // In MySQL, an INSERT SELECT is only replication safe with row-based
+               // replication or if innodb_autoinc_lock_mode is 0. When those
+               // conditions aren't met, use non-native mode.
+               // While we could try to determine if the insert is safe anyway by
+               // checking if the target table has an auto-increment column that
+               // isn't set in $varMap, that seems unlikely to be worth the extra
+               // complexity.
+               return (
+                       in_array( 'NO_AUTO_COLUMNS', $insertOptions ) ||
+                       (int)$row->innodb_autoinc_lock_mode === 0
+               );
+       }
+
+       /**
+        * @return stdClass Process cached row
+        */
+       protected function getReplicationSafetyInfo() {
+               if ( $this->replicationInfoRow === null ) {
+                       $this->replicationInfoRow = $this->selectRow(
                                false,
                                [
                                        'innodb_autoinc_lock_mode' => '@@innodb_autoinc_lock_mode',
@@ -530,33 +547,9 @@ abstract class DatabaseMysqlBase extends Database {
                                [],
                                __METHOD__
                        );
-                       $this->insertSelectIsSafe = $row->binlog_format === 'ROW' ||
-                               (int)$row->innodb_autoinc_lock_mode === 0;
-               }
-
-               if ( !$isSafe && !$this->insertSelectIsSafe ) {
-                       return $this->nonNativeInsertSelect(
-                               $destTable,
-                               $srcTable,
-                               $varMap,
-                               $conds,
-                               $fname,
-                               $insertOptions,
-                               $selectOptions,
-                               $selectJoinConds
-                       );
-               } else {
-                       return parent::nativeInsertSelect(
-                               $destTable,
-                               $srcTable,
-                               $varMap,
-                               $conds,
-                               $fname,
-                               $insertOptions,
-                               $selectOptions,
-                               $selectJoinConds
-                       );
                }
+
+               return $this->replicationInfoRow;
        }
 
        /**
@@ -678,7 +671,7 @@ abstract class DatabaseMysqlBase extends Database {
                        }
                }
 
-               return empty( $result ) ? false : $result;
+               return $result ?: false;
        }
 
        /**
index 7d34641..38cc4ae 100644 (file)
@@ -394,7 +394,7 @@ class DatabasePostgres extends Database {
                        // Forced result for simulated queries
                        return $this->lastAffectedRowCount;
                }
-               if ( empty( $this->lastResultHandle ) ) {
+               if ( !$this->lastResultHandle ) {
                        return 0;
                }
 
index 2adfd0a..f6526ac 100644 (file)
@@ -2215,8 +2215,14 @@ class Parser {
                                $link = $origLink;
                        }
 
-                       $unstrip = $this->mStripState->unstripNoWiki( $link );
-                       $nt = is_string( $unstrip ) ? Title::newFromText( $unstrip ) : null;
+                       // \x7f isn't a default legal title char, so most likely strip
+                       // markers will force us into the "invalid form" path above.  But,
+                       // just in case, let's assert that xmlish tags aren't valid in
+                       // the title position.
+                       $unstrip = $this->mStripState->killMarkers( $link );
+                       $noMarkers = ( $unstrip === $link );
+
+                       $nt = $noMarkers ? Title::newFromText( $link ) : null;
                        if ( $nt === null ) {
                                $s .= $prefix . '[[' . $line;
                                continue;
index 830dbb4..f9b03c7 100644 (file)
@@ -1486,10 +1486,8 @@ MESSAGE;
        }
 
        /**
-        * Returns JS code which runs given JS code if the client-side framework is
-        * present.
+        * Wraps JavaScript code to run after startup and base modules.
         *
-        * @deprecated since 1.25; use makeInlineScript instead
         * @param string $script JavaScript code
         * @return string JavaScript code
         */
@@ -1499,10 +1497,10 @@ MESSAGE;
        }
 
        /**
-        * Construct an inline script tag with given JS code.
+        * Returns an HTML script tag that runs given JS code after startup and base modules.
         *
-        * The code will be wrapped in a closure, and it will be executed by ResourceLoader
-        * only if the client has adequate support for MediaWiki JavaScript code.
+        * The code will be wrapped in a closure, and it will be executed by ResourceLoader's
+        * startup module if the client has adequate support for MediaWiki JavaScript code.
         *
         * @param string $script JavaScript code
         * @return WrappedString HTML
index 4abdebf..d6d4c27 100644 (file)
@@ -686,7 +686,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
         */
        public function checkLastModified() {
                $dbr = $this->getDB();
-               $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __METHOD__ );
+               $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', '', __METHOD__ );
 
                return $lastmod;
        }
index 6fdf05f..cd04995 100644 (file)
@@ -185,7 +185,7 @@ class ContribsPager extends RangeChronologicalPager {
                ];
 
                if ( $this->contribs == 'newbie' ) {
-                       $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ );
+                       $max = $this->mDb->selectField( 'user', 'max(user_id)', '', __METHOD__ );
                        $queryInfo['conds'][] = $revQuery['fields']['rev_user'] . ' >' . (int)( $max - $max / 100 );
                        # ignore local groups with the bot right
                        # @todo FIXME: Global groups may have 'bot' rights
index 4815189..e764e8b 100644 (file)
@@ -75,7 +75,7 @@ class NewFilesPager extends RangeChronologicalPager {
                if ( $opts->getValue( 'newbies' ) ) {
                        // newbie = most recent 1% of users
                        $dbr = wfGetDB( DB_REPLICA );
-                       $max = $dbr->selectField( 'user', 'max(user_id)', false, __METHOD__ );
+                       $max = $dbr->selectField( 'user', 'max(user_id)', '', __METHOD__ );
                        $conds[] = $imgQuery['fields']['img_user'] . ' >' . (int)( $max - $max / 100 );
 
                        // there's no point in looking for new user activity in a far past;
index 3102cfc..ab791b4 100644 (file)
@@ -4052,76 +4052,6 @@ class User implements IDBAccessObject, UserIdentity {
                }
        }
 
-       /**
-        * Set a cookie on the user's client. Wrapper for
-        * WebResponse::setCookie
-        * @deprecated since 1.27
-        * @param string $name Name of the cookie to set
-        * @param string $value Value to set
-        * @param int $exp Expiration time, as a UNIX time value;
-        *                   if 0 or not specified, use the default $wgCookieExpiration
-        * @param bool $secure
-        *  true: Force setting the secure attribute when setting the cookie
-        *  false: Force NOT setting the secure attribute when setting the cookie
-        *  null (default): Use the default ($wgCookieSecure) to set the secure attribute
-        * @param array $params Array of options sent passed to WebResponse::setcookie()
-        * @param WebRequest|null $request WebRequest object to use; $wgRequest will be used if null
-        *        is passed.
-        */
-       protected function setCookie(
-               $name, $value, $exp = 0, $secure = null, $params = [], $request = null
-       ) {
-               wfDeprecated( __METHOD__, '1.27' );
-               if ( $request === null ) {
-                       $request = $this->getRequest();
-               }
-               $params['secure'] = $secure;
-               $request->response()->setCookie( $name, $value, $exp, $params );
-       }
-
-       /**
-        * Clear a cookie on the user's client
-        * @deprecated since 1.27
-        * @param string $name Name of the cookie to clear
-        * @param bool $secure
-        *  true: Force setting the secure attribute when setting the cookie
-        *  false: Force NOT setting the secure attribute when setting the cookie
-        *  null (default): Use the default ($wgCookieSecure) to set the secure attribute
-        * @param array $params Array of options sent passed to WebResponse::setcookie()
-        */
-       protected function clearCookie( $name, $secure = null, $params = [] ) {
-               wfDeprecated( __METHOD__, '1.27' );
-               $this->setCookie( $name, '', time() - 86400, $secure, $params );
-       }
-
-       /**
-        * Set an extended login cookie on the user's client. The expiry of the cookie
-        * is controlled by the $wgExtendedLoginCookieExpiration configuration
-        * variable.
-        *
-        * @see User::setCookie
-        *
-        * @deprecated since 1.27
-        * @param string $name Name of the cookie to set
-        * @param string $value Value to set
-        * @param bool $secure
-        *  true: Force setting the secure attribute when setting the cookie
-        *  false: Force NOT setting the secure attribute when setting the cookie
-        *  null (default): Use the default ($wgCookieSecure) to set the secure attribute
-        */
-       protected function setExtendedLoginCookie( $name, $value, $secure ) {
-               global $wgExtendedLoginCookieExpiration, $wgCookieExpiration;
-
-               wfDeprecated( __METHOD__, '1.27' );
-
-               $exp = time();
-               $exp += $wgExtendedLoginCookieExpiration !== null
-                       ? $wgExtendedLoginCookieExpiration
-                       : $wgCookieExpiration;
-
-               $this->setCookie( $name, $value, $exp, $secure );
-       }
-
        /**
         * Persist this user's session (e.g. set cookies)
         *
index 13362e0..bcf7023 100644 (file)
@@ -132,13 +132,13 @@ EOT;
         */
        function sync( $srcTable, $dstTable ) {
                $batchSize = 1000;
-               $minTs = $this->dbw->selectField( $srcTable, 'MIN(log_timestamp)', false, __METHOD__ );
+               $minTs = $this->dbw->selectField( $srcTable, 'MIN(log_timestamp)', '', __METHOD__ );
                $minTsUnix = wfTimestamp( TS_UNIX, $minTs );
                $numRowsCopied = 0;
 
                while ( true ) {
-                       $maxTs = $this->dbw->selectField( $srcTable, 'MAX(log_timestamp)', false, __METHOD__ );
-                       $copyPos = $this->dbw->selectField( $dstTable, 'MAX(log_timestamp)', false, __METHOD__ );
+                       $maxTs = $this->dbw->selectField( $srcTable, 'MAX(log_timestamp)', '', __METHOD__ );
+                       $copyPos = $this->dbw->selectField( $dstTable, 'MAX(log_timestamp)', '', __METHOD__ );
                        $maxTsUnix = wfTimestamp( TS_UNIX, $maxTs );
                        $copyPosUnix = wfTimestamp( TS_UNIX, $copyPos );
 
index 2e1f7c9..8579f0f 100644 (file)
@@ -38,7 +38,7 @@ class ClearInterwikiCache extends Maintenance {
        public function execute() {
                global $wgLocalDatabases, $wgMemc;
                $dbr = $this->getDB( DB_REPLICA );
-               $res = $dbr->select( 'interwiki', [ 'iw_prefix' ], false );
+               $res = $dbr->select( 'interwiki', [ 'iw_prefix' ], '', __METHOD__ );
                $prefixes = [];
                foreach ( $res as $row ) {
                        $prefixes[] = $row->iw_prefix;
index 5b144fc..edd5dda 100644 (file)
@@ -55,7 +55,7 @@ class MigrateActors extends LoggedUpdateMaintenance {
                $this->output( "Creating actor entries for all registered users\n" );
                $end = 0;
                $dbw = $this->getDB( DB_MASTER );
-               $max = $dbw->selectField( 'user', 'MAX(user_id)', false, __METHOD__ );
+               $max = $dbw->selectField( 'user', 'MAX(user_id)', '', __METHOD__ );
                $count = 0;
                while ( $end < $max ) {
                        $start = $end + 1;
index 23144e9..e2fd8b5 100644 (file)
@@ -52,13 +52,13 @@ class PopulateBacklinkNamespace extends LoggedUpdateMaintenance {
 
                $start = $this->getOption( 'lastUpdatedId' );
                if ( !$start ) {
-                       $start = $db->selectField( 'page', 'MIN(page_id)', false, __METHOD__ );
+                       $start = $db->selectField( 'page', 'MIN(page_id)', '', __METHOD__ );
                }
                if ( !$start ) {
                        $this->output( "Nothing to do." );
                        return false;
                }
-               $end = $db->selectField( 'page', 'MAX(page_id)', false, __METHOD__ );
+               $end = $db->selectField( 'page', 'MAX(page_id)', '', __METHOD__ );
                $batchSize = $this->getBatchSize();
 
                # Do remaining chunk
index 7c094be..ef57640 100644 (file)
@@ -56,7 +56,7 @@ class PopulateFilearchiveSha1 extends LoggedUpdateMaintenance {
                }
 
                $this->output( "Populating fa_sha1 field from fa_storage_key\n" );
-               $endId = $dbw->selectField( $table, 'MAX(fa_id)', false, __METHOD__ );
+               $endId = $dbw->selectField( $table, 'MAX(fa_id)', '', __METHOD__ );
 
                $batchSize = $this->getBatchSize();
                $done = 0;
index 7bb1605..6e88dfa 100644 (file)
@@ -75,7 +75,7 @@ TEXT
                $start = $this->getOption( 'rev-id', 0 );
                $end = $maxRevId > 0
                        ? $maxRevId
-                       : $dbw->selectField( 'revision', 'MAX(rev_id)', false, __METHOD__ );
+                       : $dbw->selectField( 'revision', 'MAX(rev_id)', '', __METHOD__ );
 
                if ( empty( $end ) ) {
                        $this->output( "No revisions found, aborting.\n" );
index 332d7c5..589be48 100644 (file)
@@ -62,13 +62,13 @@ class PopulateLogSearch extends LoggedUpdateMaintenance {
 
                        return false;
                }
-               $start = $db->selectField( 'logging', 'MIN(log_id)', false, __FUNCTION__ );
+               $start = $db->selectField( 'logging', 'MIN(log_id)', '', __FUNCTION__ );
                if ( !$start ) {
                        $this->output( "Nothing to do.\n" );
 
                        return true;
                }
-               $end = $db->selectField( 'logging', 'MAX(log_id)', false, __FUNCTION__ );
+               $end = $db->selectField( 'logging', 'MAX(log_id)', '', __FUNCTION__ );
 
                # Do remaining chunk
                $end += $batchSize - 1;
index cacd067..3c0bba9 100644 (file)
@@ -50,13 +50,13 @@ class PopulateLogUsertext extends LoggedUpdateMaintenance {
        protected function doDBUpdates() {
                $batchSize = $this->getBatchSize();
                $db = $this->getDB( DB_MASTER );
-               $start = $db->selectField( 'logging', 'MIN(log_id)', false, __METHOD__ );
+               $start = $db->selectField( 'logging', 'MIN(log_id)', '', __METHOD__ );
                if ( !$start ) {
                        $this->output( "Nothing to do.\n" );
 
                        return true;
                }
-               $end = $db->selectField( 'logging', 'MAX(log_id)', false, __METHOD__ );
+               $end = $db->selectField( 'logging', 'MAX(log_id)', '', __METHOD__ );
 
                // If this is being run during an upgrade from 1.16 or earlier, this
                // will be run before the actor table change and should continue. But
index 39bc733..2ef58b7 100644 (file)
@@ -54,8 +54,8 @@ class PopulateParentId extends LoggedUpdateMaintenance {
                        return false;
                }
                $this->output( "Populating rev_parent_id column\n" );
-               $start = $db->selectField( 'revision', 'MIN(rev_id)', false, __FUNCTION__ );
-               $end = $db->selectField( 'revision', 'MAX(rev_id)', false, __FUNCTION__ );
+               $start = $db->selectField( 'revision', 'MIN(rev_id)', '', __FUNCTION__ );
+               $end = $db->selectField( 'revision', 'MAX(rev_id)', '', __FUNCTION__ );
                if ( is_null( $start ) || is_null( $end ) ) {
                        $this->output( "...revision table seems to be empty, nothing to do.\n" );
 
index 4ac3486..8a56d7d 100644 (file)
@@ -46,13 +46,13 @@ class PopulateRecentChangesSource extends LoggedUpdateMaintenance {
                        $this->error( 'rc_source field in recentchanges table does not exist.' );
                }
 
-               $start = $dbw->selectField( 'recentchanges', 'MIN(rc_id)', false, __METHOD__ );
+               $start = $dbw->selectField( 'recentchanges', 'MIN(rc_id)', '', __METHOD__ );
                if ( !$start ) {
                        $this->output( "Nothing to do.\n" );
 
                        return true;
                }
-               $end = $dbw->selectField( 'recentchanges', 'MAX(rc_id)', false, __METHOD__ );
+               $end = $dbw->selectField( 'recentchanges', 'MAX(rc_id)', '', __METHOD__ );
                $end += $batchSize - 1;
                $blockStart = $start;
                $blockEnd = $start + $batchSize - 1;
index bcc4999..8895c9f 100644 (file)
@@ -76,8 +76,8 @@ class PopulateRevisionLength extends LoggedUpdateMaintenance {
                $dbr = $this->getDB( DB_REPLICA );
                $dbw = $this->getDB( DB_MASTER );
                $batchSize = $this->getBatchSize();
-               $start = $dbw->selectField( $table, "MIN($idCol)", false, __METHOD__ );
-               $end = $dbw->selectField( $table, "MAX($idCol)", false, __METHOD__ );
+               $start = $dbw->selectField( $table, "MIN($idCol)", '', __METHOD__ );
+               $end = $dbw->selectField( $table, "MAX($idCol)", '', __METHOD__ );
                if ( !$start || !$end ) {
                        $this->output( "...$table table seems to be empty.\n" );
 
index d2372a9..9662044 100644 (file)
@@ -78,8 +78,8 @@ class PopulateRevisionSha1 extends LoggedUpdateMaintenance {
        protected function doSha1Updates( $table, $idCol, $queryInfo, $prefix ) {
                $db = $this->getDB( DB_MASTER );
                $batchSize = $this->getBatchSize();
-               $start = $db->selectField( $table, "MIN($idCol)", false, __METHOD__ );
-               $end = $db->selectField( $table, "MAX($idCol)", false, __METHOD__ );
+               $start = $db->selectField( $table, "MIN($idCol)", '', __METHOD__ );
+               $end = $db->selectField( $table, "MAX($idCol)", '', __METHOD__ );
                if ( !$start || !$end ) {
                        $this->output( "...$table table seems to be empty.\n" );
 
index ae6a75e..ecdec29 100644 (file)
@@ -82,10 +82,10 @@ class RebuildFileCache extends Maintenance {
                $overwrite = $this->hasOption( 'overwrite' );
                $start = ( $start > 0 )
                        ? $start
-                       : $dbr->selectField( 'page', 'MIN(page_id)', false, __METHOD__ );
+                       : $dbr->selectField( 'page', 'MIN(page_id)', '', __METHOD__ );
                $end = ( $end > 0 )
                        ? $end
-                       : $dbr->selectField( 'page', 'MAX(page_id)', false, __METHOD__ );
+                       : $dbr->selectField( 'page', 'MAX(page_id)', '', __METHOD__ );
                if ( !$start ) {
                        $this->fatalError( "Nothing to do." );
                }
index 9d5d39f..49f1cd1 100644 (file)
@@ -170,8 +170,8 @@ class RefreshLinks extends Maintenance {
                        }
                } else {
                        if ( !$end ) {
-                               $maxPage = $dbr->selectField( 'page', 'max(page_id)', false );
-                               $maxRD = $dbr->selectField( 'redirect', 'max(rd_from)', false );
+                               $maxPage = $dbr->selectField( 'page', 'max(page_id)', '', __METHOD__ );
+                               $maxRD = $dbr->selectField( 'redirect', 'max(rd_from)', '', __METHOD__ );
                                $end = max( $maxPage, $maxRD );
                        }
                        $this->output( "Refreshing redirects table.\n" );
index 8f55b88..bd0556a 100644 (file)
@@ -65,7 +65,7 @@ class CheckStorage {
                } else {
                        print "Checking...\n";
                }
-               $maxRevId = $dbr->selectField( 'revision', 'MAX(rev_id)', false, __METHOD__ );
+               $maxRevId = $dbr->selectField( 'revision', 'MAX(rev_id)', '', __METHOD__ );
                $chunkSize = 1000;
                $flagStats = [];
                $objectStats = [];
index da3ada7..6bc2f98 100644 (file)
@@ -55,7 +55,7 @@ class FixT22757 extends Maintenance {
                $numFixed = 0;
                $numBad = 0;
 
-               $totalRevs = $dbr->selectField( 'text', 'MAX(old_id)', false, __METHOD__ );
+               $totalRevs = $dbr->selectField( 'text', 'MAX(old_id)', '', __METHOD__ );
 
                // In MySQL 4.1+, the binary field old_text has a non-working LOWER() function
                $lowerLeft = 'LOWER(CONVERT(LEFT(old_text,22) USING latin1))';
index e117992..9bb554c 100644 (file)
@@ -41,7 +41,7 @@ if ( !defined( 'MEDIAWIKI' ) ) {
        if ( isset( $options['e'] ) ) {
                $maxID = $options['e'];
        } else {
-               $maxID = $dbw->selectField( 'text', 'MAX(old_id)', false, $fname );
+               $maxID = $dbw->selectField( 'text', 'MAX(old_id)', '', $fname );
        }
        $minID = isset( $options['s'] ) ? $options['s'] : 1;
 
index 4feb95e..9c1b538 100644 (file)
@@ -47,7 +47,7 @@ class OrphanStats extends Maintenance {
                if ( !$dbr->tableExists( 'blob_orphans' ) ) {
                        $this->fatalError( "blob_orphans doesn't seem to exist, need to run trackBlobs.php first" );
                }
-               $res = $dbr->select( 'blob_orphans', '*', false, __METHOD__ );
+               $res = $dbr->select( 'blob_orphans', '*', '', __METHOD__ );
 
                $num = 0;
                $totalSize = 0;
index 8ca8bb2..f9ec398 100644 (file)
@@ -38,7 +38,7 @@ function resolveStubs() {
        $fname = 'resolveStubs';
 
        $dbr = wfGetDB( DB_REPLICA );
-       $maxID = $dbr->selectField( 'text', 'MAX(old_id)', false, $fname );
+       $maxID = $dbr->selectField( 'text', 'MAX(old_id)', '', $fname );
        $blockSize = 10000;
        $numBlocks = intval( $maxID / $blockSize ) + 1;
 
index 6dee1a5..9ba3d1b 100644 (file)
@@ -25,7 +25,7 @@ class StorageTypeStats extends Maintenance {
        function execute() {
                $dbr = $this->getDB( DB_REPLICA );
 
-               $endId = $dbr->selectField( 'text', 'MAX(old_id)', false, __METHOD__ );
+               $endId = $dbr->selectField( 'text', 'MAX(old_id)', '', __METHOD__ );
                if ( !$endId ) {
                        echo "No text rows!\n";
                        exit( 1 );
index b4514ec..ae6d2ff 100644 (file)
@@ -153,7 +153,7 @@ class TrackBlobs {
 
                $textClause = $this->getTextClause();
                $startId = 0;
-               $endId = $dbr->selectField( 'revision', 'MAX(rev_id)', false, __METHOD__ );
+               $endId = $dbr->selectField( 'revision', 'MAX(rev_id)', '', __METHOD__ );
                $batchesDone = 0;
                $rowsInserted = 0;
 
@@ -229,7 +229,7 @@ class TrackBlobs {
 
                $textClause = $this->getTextClause( $this->clusters );
                $startId = 0;
-               $endId = $dbr->selectField( 'text', 'MAX(old_id)', false, __METHOD__ );
+               $endId = $dbr->selectField( 'text', 'MAX(old_id)', '', __METHOD__ );
                $rowsInserted = 0;
                $batchesDone = 0;
 
@@ -339,7 +339,7 @@ class TrackBlobs {
                        $startId = 0;
                        $batchesDone = 0;
                        $actualBlobs = gmp_init( 0 );
-                       $endId = $extDB->selectField( $table, 'MAX(blob_id)', false, __METHOD__ );
+                       $endId = $extDB->selectField( $table, 'MAX(blob_id)', '', __METHOD__ );
 
                        // Build a bitmap of actual blob rows
                        while ( true ) {
index cb40af3..668ba79 100644 (file)
@@ -46,11 +46,11 @@ class UpdateRestrictions extends Maintenance {
                        $this->fatalError( "page_restrictions table does not exist" );
                }
 
-               $start = $db->selectField( 'page', 'MIN(page_id)', false, __METHOD__ );
+               $start = $db->selectField( 'page', 'MIN(page_id)', '', __METHOD__ );
                if ( !$start ) {
                        $this->fatalError( "Nothing to do." );
                }
-               $end = $db->selectField( 'page', 'MAX(page_id)', false, __METHOD__ );
+               $end = $db->selectField( 'page', 'MAX(page_id)', '', __METHOD__ );
 
                # Do remaining chunk
                $end += $batchSize - 1;
index c833934..07dbd00 100644 (file)
@@ -17,14 +17,23 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                        ->getMock();
        }
 
-       private function createSimpleConfigMock( array $config ) {
+       private static function createEtcdResponse( array $response ) {
+               $baseResponse = [
+                       'config' => null,
+                       'error' => null,
+                       'retry' => false,
+                       'modifiedIndex' => 0,
+               ];
+               return array_merge( $baseResponse, $response );
+       }
+
+       private function createSimpleConfigMock( array $config, $index = 0 ) {
                $mock = $this->createConfigMock();
                $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
-                       ->willReturn( [
-                               $config,
-                               null, // error
-                               false // retry?
-                       ] );
+                       ->willReturn( self::createEtcdResponse( [
+                               'config' => $config,
+                               'modifiedIndex' => $index,
+                       ] ) );
                return $mock;
        }
 
@@ -70,6 +79,17 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                $config->get( 'unknown' );
        }
 
+       /**
+        * @covers EtcdConfig::getModifiedIndex
+        */
+       public function testGetModifiedIndex() {
+               $config = $this->createSimpleConfigMock(
+                       [ 'some' => 'value' ],
+                       123
+               );
+               $this->assertSame( 123, $config->getModifiedIndex() );
+       }
+
        /**
         * @covers EtcdConfig::__construct
         */
@@ -81,6 +101,7 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                        ->willReturn( [
                                'config' => [ 'known' => 'from-cache' ],
                                'expires' => INF,
+                               'modifiedIndex' => 123
                        ] );
                $config = $this->createConfigMock( [ 'cache' => $cache ] );
 
@@ -95,11 +116,8 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                        'class' => HashBagOStuff::class
                ] ] );
                $config->expects( $this->once() )->method( 'fetchAllFromEtcd' )
-                       ->willReturn( [
-                               [ 'known' => 'from-fetch' ],
-                               null, // error
-                               false // retry?
-                       ] );
+                       ->willReturn( self::createEtcdResponse(
+                               [ 'config' => [ 'known' => 'from-fetch' ], ] ) );
 
                $this->assertSame( 'from-fetch', $config->get( 'known' ) );
        }
@@ -166,7 +184,8 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                        'cache' => $cache,
                ] );
                $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
-                       ->willReturn( [ [ 'known' => 'from-fetch' ], null, false ] );
+                       ->willReturn(
+                               self::createEtcdResponse( [ 'config' => [ 'known' => 'from-fetch' ] ] ) );
 
                $this->assertSame( 'from-fetch', $mock->get( 'known' ) );
        }
@@ -191,7 +210,7 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                        'cache' => $cache,
                ] );
                $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
-                       ->willReturn( [ null, 'Fake error', false ] );
+                       ->willReturn( self::createEtcdResponse( [ 'error' => 'Fake error', ] ) );
 
                $this->setExpectedException( ConfigException::class );
                $mock->get( 'key' );
@@ -213,6 +232,7 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                                [
                                        'config' => [ 'known' => 'from-cache' ],
                                        'expires' => INF,
+                                       'modifiedIndex' => 123
                                ]
                        ) );
                // .. misses lock
@@ -241,6 +261,7 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                        ->willReturn( [
                                'config' => [ 'known' => 'from-cache' ],
                                'expires' => INF,
+                               'modifiedIndex' => 0,
                        ] );
                $cache->expects( $this->never() )->method( 'lock' );
 
@@ -266,6 +287,7 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                        ->willReturn( [
                                'config' => [ 'known' => 'from-cache' ],
                                'expires' => INF,
+                               'modifiedIndex' => 0,
                        ] );
                $cache->expects( $this->never() )->method( 'lock' );
 
@@ -292,6 +314,7 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                        [
                                'config' => [ 'known' => 'from-cache-expired' ],
                                'expires' => -INF,
+                               'modifiedIndex' => 0,
                        ]
                );
                // .. gets lock
@@ -303,7 +326,7 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                        'cache' => $cache,
                ] );
                $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
-                       ->willReturn( [ [ 'known' => 'from-fetch' ], null, false ] );
+                       ->willReturn( self::createEtcdResponse( [ 'config' => [ 'known' => 'from-fetch' ] ] ) );
 
                $this->assertSame( 'from-fetch', $mock->get( 'known' ) );
        }
@@ -321,6 +344,7 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                        [
                                'config' => [ 'known' => 'from-cache-expired' ],
                                'expires' => -INF,
+                               'modifiedIndex' => 0,
                        ]
                );
                // .. gets lock
@@ -332,7 +356,7 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                        'cache' => $cache,
                ] );
                $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
-                       ->willReturn( [ null, 'Fake failure', true ] );
+                       ->willReturn( self::createEtcdResponse( [ 'error' => 'Fake failure', 'retry' => true ] ) );
 
                $this->assertSame( 'from-cache-expired', $mock->get( 'known' ) );
        }
@@ -350,6 +374,7 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                        ->willReturn( [
                                'config' => [ 'known' => 'from-cache-expired' ],
                                'expires' => -INF,
+                               'modifiedIndex' => 0,
                        ] );
                // .. misses lock
                $cache->expects( $this->once() )->method( 'lock' )
@@ -374,16 +399,16 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                                        'body' => json_encode( [ 'node' => [ 'nodes' => [
                                                [
                                                        'key' => '/example/foo',
-                                                       'value' => json_encode( [ 'val' => true ] )
+                                                       'value' => json_encode( [ 'val' => true ] ),
+                                                       'modifiedIndex' => 123
                                                ],
                                        ] ] ] ),
                                        'error' => '',
                                ],
-                               'expect' => [
-                                       [ 'foo' => true ], // data
-                                       null,
-                                       false // retry
-                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'config' => [ 'foo' => true ], // data
+                                       'modifiedIndex' => 123
+                               ] ),
                        ],
                        '200 OK - Empty dir' => [
                                'http' => [
@@ -393,25 +418,27 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                                        'body' => json_encode( [ 'node' => [ 'nodes' => [
                                                [
                                                        'key' => '/example/foo',
-                                                       'value' => json_encode( [ 'val' => true ] )
+                                                       'value' => json_encode( [ 'val' => true ] ),
+                                                       'modifiedIndex' => 123
                                                ],
                                                [
                                                        'key' => '/example/sub',
                                                        'dir' => true,
+                                                       'modifiedIndex' => 234,
                                                        'nodes' => [],
                                                ],
                                                [
                                                        'key' => '/example/bar',
-                                                       'value' => json_encode( [ 'val' => false ] )
+                                                       'value' => json_encode( [ 'val' => false ] ),
+                                                       'modifiedIndex' => 125
                                                ],
                                        ] ] ] ),
                                        'error' => '',
                                ],
-                               'expect' => [
-                                       [ 'foo' => true, 'bar' => false ], // data
-                                       null,
-                                       false // retry
-                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'config' => [ 'foo' => true, 'bar' => false ], // data
+                                       'modifiedIndex' => 125 // largest modified index
+                               ] ),
                        ],
                        '200 OK - Recursive' => [
                                'http' => [
@@ -422,25 +449,28 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                                                [
                                                        'key' => '/example/a',
                                                        'dir' => true,
+                                                       'modifiedIndex' => 124,
                                                        'nodes' => [
                                                                [
                                                                        'key' => 'b',
                                                                        'value' => json_encode( [ 'val' => true ] ),
+                                                                       'modifiedIndex' => 123,
+
                                                                ],
                                                                [
                                                                        'key' => 'c',
                                                                        'value' => json_encode( [ 'val' => false ] ),
+                                                                       'modifiedIndex' => 123,
                                                                ],
                                                        ],
                                                ],
                                        ] ] ] ),
                                        'error' => '',
                                ],
-                               'expect' => [
-                                       [ 'a/b' => true, 'a/c' => false ], // data
-                                       null,
-                                       false // retry
-                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'config' => [ 'a/b' => true, 'a/c' => false ], // data
+                                       'modifiedIndex' => 123 // largest modified index
+                               ] ),
                        ],
                        '200 OK - Missing nodes at second level' => [
                                'http' => [
@@ -451,15 +481,14 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                                                [
                                                        'key' => '/example/a',
                                                        'dir' => true,
+                                                       'modifiedIndex' => 0,
                                                ],
                                        ] ] ] ),
                                        'error' => '',
                                ],
-                               'expect' => [
-                                       null,
-                                       "Unexpected JSON response in dir 'a'; missing 'nodes' list.",
-                                       false // retry
-                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'error' => "Unexpected JSON response in dir 'a'; missing 'nodes' list.",
+                               ] ),
                        ],
                        '200 OK - Directory with non-array "nodes" key' => [
                                'http' => [
@@ -475,11 +504,9 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                                        ] ] ] ),
                                        'error' => '',
                                ],
-                               'expect' => [
-                                       null,
-                                       "Unexpected JSON response in dir 'a'; 'nodes' is not an array.",
-                                       false // retry
-                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'error' => "Unexpected JSON response in dir 'a'; 'nodes' is not an array.",
+                               ] ),
                        ],
                        '200 OK - Correctly encoded garbage response' => [
                                'http' => [
@@ -489,11 +516,9 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                                        'body' => json_encode( [ 'foo' => 'bar' ] ),
                                        'error' => '',
                                ],
-                               'expect' => [
-                                       null,
-                                       "Unexpected JSON response: Missing or invalid node at top level.",
-                                       false // retry
-                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'error' => "Unexpected JSON response: Missing or invalid node at top level.",
+                               ] ),
                        ],
                        '200 OK - Bad value' => [
                                'http' => [
@@ -503,30 +528,27 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                                        'body' => json_encode( [ 'node' => [ 'nodes' => [
                                                [
                                                        'key' => '/example/foo',
-                                                       'value' => ';"broken{value'
+                                                       'value' => ';"broken{value',
+                                                       'modifiedIndex' => 123,
                                                ]
                                        ] ] ] ),
                                        'error' => '',
                                ],
-                               'expect' => [
-                                       null, // data
-                                       "Failed to parse value for 'foo'.",
-                                       false // retry
-                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'error' => "Failed to parse value for 'foo'.",
+                               ] ),
                        ],
                        '200 OK - Empty node list' => [
                                'http' => [
                                        'code' => 200,
                                        'reason' => 'OK',
                                        'headers' => [],
-                                       'body' => '{"node":{"nodes":[]}}',
+                                       'body' => '{"node":{"nodes":[], "modifiedIndex": 12 }}',
                                        'error' => '',
                                ],
-                               'expect' => [
-                                       [], // data
-                                       null,
-                                       false // retry
-                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'config' => [], // data
+                               ] ),
                        ],
                        '200 OK - Invalid JSON' => [
                                'http' => [
@@ -536,11 +558,9 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                                        'body' => '',
                                        'error' => '(curl error: no status set)',
                                ],
-                               'expect' => [
-                                       null, // data
-                                       "Error unserializing JSON response.",
-                                       false // retry
-                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'error' => "Error unserializing JSON response.",
+                               ] ),
                        ],
                        '404 Not Found' => [
                                'http' => [
@@ -550,11 +570,9 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                                        'body' => '',
                                        'error' => '',
                                ],
-                               'expect' => [
-                                       null, // data
-                                       'HTTP 404 (Not Found)',
-                                       false // retry
-                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'error' => 'HTTP 404 (Not Found)',
+                               ] ),
                        ],
                        '400 Bad Request - custom error' => [
                                'http' => [
@@ -564,11 +582,10 @@ class EtcdConfigTest extends PHPUnit\Framework\TestCase {
                                        'body' => '',
                                        'error' => 'No good reason',
                                ],
-                               'expect' => [
-                                       null, // data
-                                       'No good reason',
-                                       true // retry
-                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'error' => 'No good reason',
+                                       'retry' => true, // retry
+                               ] ),
                        ],
                ];
        }
index 5fcca1a..14c7057 100644 (file)
@@ -29,6 +29,7 @@ use Wikimedia\Rdbms\MySQLMasterPos;
 use Wikimedia\Rdbms\DatabaseMysqlBase;
 use Wikimedia\Rdbms\DatabaseMysqli;
 use Wikimedia\Rdbms\Database;
+use Wikimedia\TestingAccessWrapper;
 
 /**
  * Fake class around abstract class so we can call concrete methods.
@@ -510,4 +511,97 @@ class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase {
 
                $this->assertEquals( $pos, $roundtripPos );
        }
+
+       /**
+        * @covers Wikimedia\Rdbms\DatabaseMysqlBase::isInsertSelectSafe
+        * @dataProvider provideInsertSelectCases
+        */
+       public function testInsertSelectIsSafe( $insertOpts, $selectOpts, $row, $safe ) {
+               $db = $this->getMockBuilder( DatabaseMysqli::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [ 'getReplicationSafetyInfo' ] )
+                       ->getMock();
+               $db->method( 'getReplicationSafetyInfo' )->willReturn( (object)$row );
+               $dbw = TestingAccessWrapper::newFromObject( $db );
+
+               $this->assertEquals( $safe, $dbw->isInsertSelectSafe( $insertOpts, $selectOpts ) );
+       }
+
+       public function provideInsertSelectCases() {
+               return [
+                       [
+                               [],
+                               [],
+                               [
+                                       'innodb_autoinc_lock_mode' => '2',
+                                       'binlog_format' => 'ROW',
+                               ],
+                               true
+                       ],
+                       [
+                               [],
+                               [ 'LIMIT' => 100 ],
+                               [
+                                       'innodb_autoinc_lock_mode' => '2',
+                                       'binlog_format' => 'ROW',
+                               ],
+                               true
+                       ],
+                       [
+                               [],
+                               [ 'LIMIT' => 100 ],
+                               [
+                                       'innodb_autoinc_lock_mode' => '0',
+                                       'binlog_format' => 'STATEMENT',
+                               ],
+                               false
+                       ],
+                       [
+                               [],
+                               [],
+                               [
+                                       'innodb_autoinc_lock_mode' => '2',
+                                       'binlog_format' => 'STATEMENT',
+                               ],
+                               false
+                       ],
+                       [
+                               [ 'NO_AUTO_COLUMNS' ],
+                               [ 'LIMIT' => 100 ],
+                               [
+                                       'innodb_autoinc_lock_mode' => '0',
+                                       'binlog_format' => 'STATEMENT',
+                               ],
+                               false
+                       ],
+                       [
+                               [],
+                               [],
+                               [
+                                       'innodb_autoinc_lock_mode' => 0,
+                                       'binlog_format' => 'STATEMENT',
+                               ],
+                               true
+                       ],
+                       [
+                               [ 'NO_AUTO_COLUMNS' ],
+                               [],
+                               [
+                                       'innodb_autoinc_lock_mode' => 2,
+                                       'binlog_format' => 'STATEMENT',
+                               ],
+                               true
+                       ],
+                       [
+                               [ 'NO_AUTO_COLUMNS' ],
+                               [],
+                               [
+                                       'innodb_autoinc_lock_mode' => 0,
+                                       'binlog_format' => 'STATEMENT',
+                               ],
+                               true
+                       ],
+
+               ];
+       }
 }
index 3d1fe1a..5c1943b 100644 (file)
@@ -64,6 +64,44 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
                                        "FROM table " .
                                        "WHERE alias = 'text'"
                        ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'fields' => [ 'field', 'alias' => 'field2' ],
+                                       'conds' => 'alias = \'text\'',
+                               ],
+                               "SELECT field,field2 AS alias " .
+                               "FROM table " .
+                               "WHERE alias = 'text'"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'fields' => [ 'field', 'alias' => 'field2' ],
+                                       'conds' => [],
+                               ],
+                               "SELECT field,field2 AS alias " .
+                               "FROM table"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'fields' => [ 'field', 'alias' => 'field2' ],
+                                       'conds' => '',
+                               ],
+                               "SELECT field,field2 AS alias " .
+                               "FROM table"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'fields' => [ 'field', 'alias' => 'field2' ],
+                                       'conds' => '0', // T188314
+                               ],
+                               "SELECT field,field2 AS alias " .
+                               "FROM table " .
+                               "WHERE 0"
+                       ],
                        [
                                [
                                        // 'tables' with space prepended indicates pre-escaped table name