Merge "Introducing pp_sortkey."
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 24 Apr 2014 21:42:07 +0000 (21:42 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 24 Apr 2014 21:42:07 +0000 (21:42 +0000)
1  2 
RELEASE-NOTES-1.23
includes/DefaultSettings.php
includes/deferred/LinksUpdate.php
includes/installer/PostgresUpdater.php
includes/parser/ParserOutput.php
tests/phpunit/includes/LinksUpdateTest.php

diff --combined RELEASE-NOTES-1.23
@@@ -9,11 -9,12 +9,14 @@@ MediaWiki 1.23 is an alpha-quality bran
  production.
  
  === Configuration changes in 1.23 ===
+ * Introduced $wgPagePropsHaveSortkey as a backwards-compatibility switch,
+   for using the old schema of the page_props table, in case the respective
+   schema update was not applied.
  * When $wgJobRunRate is higher that zero, jobs are now executed via an
    asynchronous HTTP request to a MediaWiki entry point. This may require
 -  increasing the number of server worker threads.
 +  increasing the number of server worker threads. $wgRunJobsAsync has been
 +  added to disable this feature if needed, falling back to executing the job
 +  on the same process but making the execution synchronously.
  * $wgDebugLogGroups values may be set to an associative array with a
    'destination' key specifying the log destination. The array may also contain
    a 'sample' key with a positive integer value N indicating that the log group
    wgQueryPages hook.
  * $wgHttpOnlyBlacklist has been removed.
  * $wgLicenseTerms has been removed as it was unused.
 +* $wgProfileOnly is now deprecated; set the log file in
 +  $wgDebugLogGroups['profileoutput'] to replace it.
 +* $wgMaxBacklinksInvalidate was removed; use $wgJobBackoffThrottling instead
 +* Deprecated ResourceLoaderGetStartupModules hook.
  
  === New features in 1.23 ===
  * ResourceLoader can utilize the Web Storage API to cache modules client-side.
    installer has been updated to use it.
  * Changes to content typography (fonts, line-height, etc.). See
    https://www.mediawiki.org/wiki/Typography_refresh for further information.
 +* The Vector skin's visual treatment of external links has been simplified to a
 +  single icon (from nine). This should not affect local rules unless they were
 +  re-using these icons, which have now been deleted.
  * ResourceLoader: mw.loader.using() now implements a Promise interface.
 +* Add new hook ChangesListInitRows accessed via ChangesList::initChangesListRows.
 +  If called by the ChangesList consumer this gives extensions a chance to batch
 +  process the result set prior to rendering.
 +* A PoolCounterRedis class was added which can be make use of in $wgPoolCounterConf.
 +  This requires at least one Redis 2.6+ server.
 +* $wgProfileToDatabase was removed. Set $wgProfiler to ProfilerSimpleDB
 +  in StartProfiler.php instead of using this.
 +* (bug 63444) Made it possible to change the indent string (default: 4 spaces)
 +  used by FormatJson::encode().
  
  === Bug fixes in 1.23 ===
  * (bug 41759) The "updated since last visit" markers (on history pages, recent
  * (bug 42026) Deprecated uctoponly in favor of ucshow=top.
  * list=search no longer has a "srredirects" parameter. Redirects are now
    included in all searches.
 +* Added list=prefixsearch that works like action=opensearch but can be used as
 +  a generator.
 +* (bug 24782) Various modules will now use unique continuation parameters.
 +* (bug 63249) Cache RecentChanges Atom feed in varnish for 15 seconds.
  
  === Languages updated in 1.23 ===
  
@@@ -267,6 -248,9 +270,9 @@@ changes to languages because of Bugzill
  * Support was added for Northern Luri (lrc).
  
  === Other changes in 1.23 ===
+ * Added pp_sortkey column to page_props table, so pages can be efficiently
+   queried and sorted by property value (bug 58032).
+   See $wgPagePropsHaveSortkey if you want to postpone the schema change.
  * The rc_type field in the recentchanges table has been superseded by a new
    rc_source field.  The rc_source field is a string representation of the
    change type where rc_type was a numeric constant.  This field is not yet
  * Special:Search no longer has an "include redirects" option on the advanced
    tab. Redirects are now included in all searches.
  * mediawiki.api.category's getCategories() 'async' parameter was deprecated.
 +* The locations of resources have been split between upstream libraries, now in
 +  resources/lib/, local libaries in resources/src/, and local forks of upstream
 +  libraries, also in resources/src/.
 +* BREAKING CHANGE: The automatically-generated function closure with which
 +  ResourceLoader wraps all modules' JavaScript code now binds the identifier
 +  names 'jQuery' and '$' to the jQuery object of the version of jQuery that is
 +  bundled with MediaWiki. If you bind these names to other objects in global
 +  scope (like Zepto.js or document.querySelectorAll, for example) you will need
 +  to use different names to or re-bind them at the top of each
 +  ResourceLoader-loaded module.
 +* (bug 52342) Preference "Remember my login" was removed.
  
  ==== Removed classes ====
  * FakeMemCachedClient (deprecated in 1.18)
@@@ -70,7 -70,7 +70,7 @@@ $wgConfigClass = 'GlobalConfig'
   * MediaWiki version number
   * @since 1.2
   */
 -$wgVersion = '1.23alpha';
 +$wgVersion = '1.24alpha';
  
  /**
   * Name of the site. It must be changed in LocalSettings.php
@@@ -361,6 -361,13 +361,6 @@@ $wgDeletedDirectory = false
   */
  $wgImgAuthDetails = false;
  
 -/**
 - * If this is enabled, img_auth.php will not allow image access unless the wiki
 - * is private. This improves security when image uploads are hosted on a
 - * separate domain.
 - */
 -$wgImgAuthPublicTest = true;
 -
  /**
   * Map of relative URL directories to match to internal mwstore:// base storage paths.
   * For img_auth.php requests, everything after "img_auth.php/" is checked to see
@@@ -1618,10 -1625,10 +1618,10 @@@ $wgAllDBsAreLocalhost = false
   * $wgSharedPrefix is the table prefix for the shared database. It defaults to
   * $wgDBprefix.
   *
 - * @deprecated In new code, use the $wiki parameter to wfGetLB() to access
 - *   remote databases. Using wfGetLB() allows the shared database to reside on
 - *   separate servers to the wiki's own database, with suitable configuration
 - *   of $wgLBFactoryConf.
 + * @deprecated since 1.21 In new code, use the $wiki parameter to wfGetLB() to
 + *   access remote databases. Using wfGetLB() allows the shared database to
 + *   reside on separate servers to the wiki's own database, with suitable
 + *   configuration of $wgLBFactoryConf.
   */
  $wgSharedDB = null;
  
@@@ -3355,7 -3362,7 +3355,7 @@@ $wgResourceLoaderLESSFunctions = array
   * @since 1.22
   */
  $wgResourceLoaderLESSImportPaths = array(
 -      "$IP/resources/mediawiki.less/",
 +      "$IP/resources/src/mediawiki.less/",
        "$IP/skins/vector/",
  );
  
@@@ -4069,6 -4076,7 +4069,6 @@@ $wgDefaultUserOptions = array
        'previewontop' => 1,
        'rcdays' => 7,
        'rclimit' => 50,
 -      'rememberpassword' => 0,
        'rows' => 25,
        'showhiddencats' => 0,
        'shownumberswatching' => 1,
@@@ -4764,13 -4772,6 +4764,13 @@@ $wgRateLimits = array
                'ip' => null,
                'subnet' => null,
        ),
 +      'renderfile-nonstandard' => array( // same as above but for non-standard thumbnails
 +              'anon' => null,
 +              'user' => null,
 +              'newbie' => null,
 +              'ip' => null,
 +              'subnet' => null,
 +      ),
  );
  
  /**
@@@ -5085,11 -5086,20 +5085,11 @@@ $wgProfileLimit = 0.0
  
  /**
   * Don't put non-profiling info into log file
 - */
 -$wgProfileOnly = false;
 -
 -/**
 - * Log sums from profiling into "profiling" table in db.
 - *
 - * You have to create a 'profiling' table in your database before using
 - * this feature.  Run set $wgProfileToDatabase to true in
 - * LocalSettings.php and run maintenance/update.php or otherwise
 - * manually add patch-profiling.sql to your database.
   *
 - * To enable profiling, edit StartProfiler.php
 + * @deprecated since 1.23, set the log file in
 + *   $wgDebugLogGroups['profileoutput'] instead.
   */
 -$wgProfileToDatabase = false;
 +$wgProfileOnly = false;
  
  /**
   * If true, print a raw call tree instead of per-function report
@@@ -5624,10 -5634,6 +5624,10 @@@ $wgRC2UDPOmitBots = false
   *   * 'formatter' -- the class name (implementing RCFeedFormatter) which will
   *     produce the text to send.
   *   * 'omit_bots' -- whether the bot edits should be in the feed
 + *   * 'omit_anon' -- whether anonymous edits should be in the feed
 + *   * 'omit_user' -- whether edits by registered users should be in the feed
 + *   * 'omit_minor' -- whether minor edits should be in the feed
 + *   * 'omit_patrolled' -- whether patrolled edits should be in the feed
   *  The IRC-specific options are:
   *   * 'add_interwiki_prefix' -- whether the titles should be prefixed with
   *     the first entry in the $wgLocalInterwikis array (or the value of
@@@ -5979,13 -5985,13 +5979,13 @@@ $wgExtensionFunctions = array()
   *
   * Since MediaWiki 1.23, use of this variable to define messages is discouraged; instead, store
   * messages in JSON format and use $wgMessagesDirs. For setting other variables than
 - * $messages, $wgExtensionMessagesFiles should still be used.
 + * $messages, $wgExtensionMessagesFiles should still be used. Use a DIFFERENT key because
 + * any entry having a key that also exists in $wgMessagesDirs will be ignored.
   *
 - * If there is an entry in $wgMessagesDirs with the same key as one in
 - * $wgExtensionMessagesFiles, then any $messages variables set in the $wgExtensionMessagesFiles file
 - * will be ignored. This means an extension that only provides messages can be backwards compatible
 - * by using both $wgExtensionMessagesFiles and $wgMessagesDirs, and only one of the two
 - * will be used depending on what the version of MediaWiki supports.
 + * Extensions using the JSON message format can preserve backward compatibility with
 + * earlier versions of MediaWiki by using a compatibility shim, such as one generated
 + * by the generateJsonI18n.php maintenance script, listing it under the SAME key
 + * as for the $wgMessagesDirs entry.
   *
   * @par Example:
   * @code
@@@ -6018,8 -6024,7 +6018,8 @@@ $wgExtensionMessagesFiles = array()
   * @since 1.23
   */
  $wgMessagesDirs = array(
 -      'oojs-ui' => "$IP/resources/oojs-ui/i18n",
 +      'core' => "$IP/languages/i18n",
 +      'oojs-ui' => "$IP/resources/lib/oojs-ui/i18n",
  );
  
  /**
@@@ -6198,7 -6203,6 +6198,7 @@@ $wgJobTypesExcludedFromDefaultQueue = a
   * on each job runner process. The meaning of "work items" varies per job,
   * but typically would be something like "pages to update". A single job
   * may have a variable number of work items, as is the case with batch jobs.
 + * This is used by runJobs.php and not jobs run via $wgJobRunRate.
   * These settings should be global to all wikis.
   */
  $wgJobBackoffThrottling = array();
@@@ -6909,14 -6913,6 +6909,14 @@@ $wgHTTPConnectTimeout = 5e0
   */
  $wgJobRunRate = 1;
  
 +/**
 + * When $wgJobRunRate > 0, try to run jobs asynchronously, spawning a new process
 + * to handle the job execution, instead of blocking the request until the job
 + * execution finishes.
 + * @since 1.23
 + */
 +$wgRunJobsAsync = true;
 +
  /**
   * Number of rows to update per job
   */
@@@ -6927,6 -6923,15 +6927,6 @@@ $wgUpdateRowsPerJob = 500
   */
  $wgUpdateRowsPerQuery = 100;
  
 -/**
 - * Do not purge all the pages that use a page when it is edited
 - * if there are more than this many such pages. This is used to
 - * avoid invalidating a large portion of the squid/parser cache.
 - *
 - * This setting should factor in any squid/parser cache expiry settings.
 - */
 -$wgMaxBacklinksInvalidate = false;
 -
  /** @} */ # End job queue }
  
  /************************************************************************//**
@@@ -7080,6 -7085,13 +7080,13 @@@ $wgSiteTypes = array
   */
  $wgCompiledFiles = array();
  
+ /**
+  * Whether the page_props table has a pp_sortkey column. Set to false in case
+  * the respective database schema change was not applied.
+  * @since 1.23
+  */
+ $wgPagePropsHaveSortkey = true;
  /**
   * For really cool vim folding this needs to be at the end:
   * vim: foldmarker=@{,@} foldmethod=marker
@@@ -269,7 -269,7 +269,7 @@@ class LinksUpdate extends SqlDataUpdat
        }
  
        /**
 -       * @param $cats
 +       * @param array $cats
         */
        function invalidateCategories( $cats ) {
                $this->invalidatePages( NS_CATEGORY, array_keys( $cats ) );
        }
  
        /**
 -       * @param $images
 +       * @param array $images
         */
        function invalidateImageDescriptions( $images ) {
                $this->invalidatePages( NS_FILE, array_keys( $images ) );
                                $toField = $prefix . '_to';
                        }
                        if ( count( $deletions ) ) {
 -                              $where[] = "$toField IN (" . $this->mDb->makeList( array_keys( $deletions ) ) . ')';
 +                              $where[$toField] = array_keys( $deletions );
                        } else {
                                $where = false;
                        }
        /**
         * Get an array of category insertions
         *
 -       * @param array $existing mapping existing category names to sort keys. If both
 +       * @param array $existing Mapping existing category names to sort keys. If both
         * match a link in $this, the link will be omitted from the output
         *
         * @return array
        /**
         * Get an array of interlanguage link insertions
         *
 -       * @param array $existing mapping existing language codes to titles
 +       * @param array $existing Mapping existing language codes to titles
         *
         * @return array
         */
         */
        function getPropertyInsertions( $existing = array() ) {
                $diffs = array_diff_assoc( $this->mProperties, $existing );
                $arr = array();
-               foreach ( $diffs as $name => $value ) {
-                       $arr[] = array(
-                               'pp_page' => $this->mId,
-                               'pp_propname' => $name,
-                               'pp_value' => $value,
-                       );
+               foreach ( array_keys( $diffs ) as $name ) {
+                       $arr[] = $this->getPagePropRowData( $name );
                }
  
                return $arr;
        }
  
+       /**
+        * Returns an associative array to be used for inserting a row into
+        * the page_props table. Besides the given property name, this will
+        * include the page id from $this->mId and any property value from
+        * $this->mProperties.
+        *
+        * The array returned will include the pp_sortkey field if this
+        * is present in the database (as indicated by $wgPagePropsHaveSortkey).
+        * The sortkey value is currently determined by getPropertySortKeyValue().
+        *
+        * @note: this assumes that $this->mProperties[$prop] is defined.
+        *
+        * @param string $prop The name of the property.
+        *
+        * @return array
+        */
+       private function getPagePropRowData( $prop ) {
+               global $wgPagePropsHaveSortkey;
+               $value = $this->mProperties[$prop];
+               $row = array(
+                       'pp_page' => $this->mId,
+                       'pp_propname' => $prop,
+                       'pp_value' => $value,
+               );
+               if ( $wgPagePropsHaveSortkey ) {
+                       $row['pp_sortkey'] = $this->getPropertySortKeyValue( $value );
+               }
+               return $row;
+       }
+       /**
+        * Determines the sort key for the given property value.
+        * This will return $value if it is a float or int,
+        * 1 or resp. 0 if it is a bool, and null otherwise.
+        *
+        * @note: In the future, we may allow the sortkey to be specified explicitly
+        *        in ParserOutput::setProperty.
+        *
+        * @param mixed $value
+        *
+        * @return float|null
+        */
+       private function getPropertySortKeyValue( $value ) {
+               if ( is_int( $value ) || is_float( $value ) || is_bool( $value ) ) {
+                       return floatval( $value );
+               }
+               return null;
+       }
        /**
         * Get an array of interwiki insertions for passing to the DB
         * Skips the titles specified by the 2-D array $existing
        /**
         * Get an array of existing categories, with the name in the key and sort key in the value.
         *
 -       * @return array of property names and values
 +       * @return array Array of property names and values
         */
        private function getExistingProperties() {
                $res = $this->mDb->select( 'page_props', array( 'pp_propname', 'pp_value' ),
        /**
         * Fetch page links added by this LinksUpdate.  Only available after the update is complete.
         * @since 1.22
 -       * @return null|array of Titles
 +       * @return null|array Array of Titles
         */
        public function getAddedLinks() {
                if ( $this->linkInsertions === null ) {
        /**
         * Fetch page links removed by this LinksUpdate.  Only available after the update is complete.
         * @since 1.22
 -       * @return null|array of Titles
 +       * @return null|array Array of Titles
         */
        public function getRemovedLinks() {
                if ( $this->linkDeletions === null ) {
@@@ -234,7 -234,6 +234,7 @@@ class PostgresUpdater extends DatabaseU
                        array( 'changeNullableField', 'image', 'img_metadata', 'NOT NULL' ),
                        array( 'changeNullableField', 'filearchive', 'fa_metadata', 'NOT NULL' ),
                        array( 'changeNullableField', 'recentchanges', 'rc_cur_id', 'NULL' ),
 +                      array( 'changeNullableField', 'recentchanges', 'rc_cur_time', 'NULL' ),
  
                        array( 'checkOiDeleted' ),
  
                        array( 'addPgField', 'recentchanges', 'rc_source', "TEXT NOT NULL DEFAULT ''" ),
                        array( 'addPgField', 'page', 'page_links_updated', "TIMESTAMPTZ NULL" ),
                        array( 'addPgField', 'mwuser', 'user_password_expires', 'TIMESTAMPTZ NULL' ),
+                       array( 'addPgField', 'page_props', 'pp_sortkey', 'float NULL' ),
+                       array( 'addPgIndex', 'page_props', 'pp_propname_sortkey_page',
+                                       '( pp_propname, pp_sortkey, pp_page ) WHERE ( pp_sortkey NOT NULL )' ),
                );
        }
  
@@@ -96,7 -96,7 +96,7 @@@ class ParserOutput extends CacheTime 
        /**
         * callback used by getText to replace editsection tokens
         * @private
 -       * @param $m
 +       * @param array $m
         * @throws MWException
         * @return mixed
         */
        /**
         * Checks, if a url is pointing to the own server
         *
 -       * @param string $internal the server to check against
 -       * @param string $url the url to check
 +       * @param string $internal The server to check against
 +       * @param string $url The url to check
         * @return bool
         */
        static function isLinkInternal( $internal, $url ) {
        /**
         * Record a local or interwiki inline link for saving in future link tables.
         *
 -       * @param $title Title object
 -       * @param $id Mixed: optional known page_id so we can skip the lookup
 +       * @param Title $title
 +       * @param int|null $id Optional known page_id so we can skip the lookup
         */
        function addLink( Title $title, $id = null ) {
                if ( $title->isExternal() ) {
         * Register a file dependency for this output
         * @param string $name Title dbKey
         * @param string $timestamp MW timestamp of file creation (or false if non-existing)
 -       * @param string $sha1 base 36 SHA-1 of file (or false if non-existing)
 +       * @param string $sha1 Base 36 SHA-1 of file (or false if non-existing)
         * @return void
         */
        function addImage( $name, $timestamp = null, $sha1 = null ) {
  
        /**
         * Register a template dependency for this output
 -       * @param $title Title
 -       * @param $page_id
 -       * @param $rev_id
 +       * @param Title $title
 +       * @param int $page_id
 +       * @param int $rev_id
         * @return void
         */
        function addTemplate( $title, $page_id, $rev_id ) {
        }
  
        /**
 -       * @param $title Title object, must be an interwiki link
 +       * @param Title $title Title object, must be an interwiki link
         * @throws MWException if given invalid input
         */
        function addInterwikiLink( $title ) {
         * Add some text to the "<head>".
         * If $tag is set, the section with that tag will only be included once
         * in a given page.
 +       * @param string $section
 +       * @param string|bool $tag
         */
        function addHeadItem( $section, $tag = false ) {
                if ( $tag !== false ) {
        /**
         * Add one or more variables to be set in mw.config in JavaScript.
         *
 -       * @param $keys {String|Array} Key or array of key/value pairs.
 -       * @param $value {Mixed} [optional] Value of the configuration variable.
 +       * @param string|array $keys Key or array of key/value pairs.
 +       * @param mixed $value [optional] Value of the configuration variable.
         * @since 1.23
         */
        public function addJsConfigVars( $keys, $value = null ) {
        /**
         * Copy items from the OutputPage object into this one
         *
 -       * @param $out OutputPage object
 +       * @param OutputPage $out
         */
        public function addOutputPageMetadata( OutputPage $out ) {
                $this->addModules( $out->getModules() );
        /**
         * Get the title to be used for display
         *
 -       * @return String
 +       * @return string
         */
        public function getDisplayTitle() {
                $t = $this->getTitleText();
         * retrieved given the page ID or via a DB join when given the page
         * title.
         *
+        * Since 1.23, page_props are also indexed by numeric value, to allow
+        * for efficient "top k" queries of pages wrt a given property.
+        *
         * setProperty() is thus used to propagate properties from the parsed
         * page to request contexts other than a page view of the currently parsed
         * article.
        /**
         * Returns the options from its ParserOptions which have been taken
         * into account to produce this output or false if not available.
 -       * @return mixed Array
 +       * @return array
         */
        public function getUsedOptions() {
                if ( !isset( $this->mAccessedOptions ) ) {
         * @see ParserCache::save
         * @see ParserOptions::addExtraKey
         * @see ParserOptions::optionsHash
 +       * @param string $option
         */
        public function recordOption( $option ) {
                $this->mAccessedOptions[$option] = true;
         *
         * @since 1.20
         *
 -       * @param $title Title The title of the page we're updating. If not given, a title object will be created
 -       *                      based on $this->getTitleText()
 -       * @param $recursive Boolean: queue jobs for recursive updates?
 +       * @param Title $title The title of the page we're updating. If not given, a title object will
 +       *    be created based on $this->getTitleText()
 +       * @param bool $recursive Queue jobs for recursive updates?
         *
 -       * @return Array. An array of instances of DataUpdate
 +       * @return array An array of instances of DataUpdate
         */
        public function getSecondaryDataUpdates( Title $title = null, $recursive = true ) {
                if ( is_null( $title ) ) {
         * @since 1.21
         *
         * @param string $key The key for accessing the data. Extensions should take care to avoid
 -       *               conflicts in naming keys. It is suggested to use the extension's name as a
 -       *               prefix.
 +       *   conflicts in naming keys. It is suggested to use the extension's name as a prefix.
         *
         * @param mixed $value The value to set. Setting a value to null is equivalent to removing
 -       *              the value.
 +       *   the value.
         */
        public function setExtensionData( $key, $value ) {
                if ( $value === null ) {
@@@ -63,15 -63,9 +63,15 @@@ class LinksUpdateTest extends MediaWiki
                $po->addLink( Title::newFromText( "linksupdatetest:Foo" ) ); // interwiki link should be ignored
                $po->addLink( Title::newFromText( "#Foo" ) ); // hash link should be ignored
  
 -              $update = $this->assertLinksUpdate( $t, $po, 'pagelinks', 'pl_namespace, pl_title', 'pl_from = 111', array(
 -                      array( NS_MAIN, 'Foo' ),
 -              ) );
 +              $update = $this->assertLinksUpdate(
 +                      $t,
 +                      $po,
 +                      'pagelinks',
 +                      'pl_namespace,
 +                      pl_title',
 +                      'pl_from = 111',
 +                      array( array( NS_MAIN, 'Foo' ) )
 +              );
                $this->assertArrayEquals( array(
                        Title::makeTitle( NS_MAIN, 'Foo' ),  // newFromText doesn't yield the same internal state....
                ), $update->getAddedLinks() );
                $po->addLink( Title::newFromText( "Bar" ) );
                $po->addLink( Title::newFromText( "Talk:Bar" ) );
  
 -              $update = $this->assertLinksUpdate( $t, $po, 'pagelinks', 'pl_namespace, pl_title', 'pl_from = 111', array(
 -                      array( NS_MAIN, 'Bar' ),
 -                      array( NS_TALK, 'Bar' ),
 -              ) );
 +              $update = $this->assertLinksUpdate(
 +                      $t,
 +                      $po,
 +                      'pagelinks',
 +                      'pl_namespace,
 +                      pl_title',
 +                      'pl_from = 111',
 +                      array(
 +                              array( NS_MAIN, 'Bar' ),
 +                              array( NS_TALK, 'Bar' ),
 +                      )
 +              );
                $this->assertArrayEquals( array(
                        Title::makeTitle( NS_MAIN, 'Bar' ),
                        Title::makeTitle( NS_TALK, 'Bar' ),
  
                $po->addTemplate( Title::newFromText( "Template:Foo" ), 23, 42 );
  
 -              $this->assertLinksUpdate( $t, $po, 'templatelinks', 'tl_namespace, tl_title', 'tl_from = 111', array(
 -                      array( NS_TEMPLATE, 'Foo' ),
 -              ) );
 +              $this->assertLinksUpdate(
 +                      $t,
 +                      $po,
 +                      'templatelinks',
 +                      'tl_namespace,
 +                      tl_title',
 +                      'tl_from = 111',
 +                      array( array( NS_TEMPLATE, 'Foo' ) )
 +              );
        }
  
        /**
         * @covers ParserOutput::setProperty
         */
        public function testUpdate_page_props() {
+               global $wgPagePropsHaveSortkey;
                /** @var ParserOutput $po */
                list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
  
-               $po->setProperty( "foo", "bar" );
+               $fields = array( 'pp_propname', 'pp_value' );
+               $expected = array();
  
-               $this->assertLinksUpdate( $t, $po, 'page_props', 'pp_propname, pp_value', 'pp_page = 111', array(
-                       array( 'foo', 'bar' ),
-               ) );
+               $po->setProperty( "bool", true );
+               $expected[] = array( "bool", true );
+               $po->setProperty( "float", 4.0 + 1.0/4.0 );
+               $expected[] = array( "float", 4.0 + 1.0/4.0 );
+               $po->setProperty( "int", -7 );
+               $expected[] = array( "int", -7 );
+               $po->setProperty( "string", "33 bar" );
+               $expected[] = array( "string", "33 bar" );
+               // compute expected sortkey values
+               if ( $wgPagePropsHaveSortkey ) {
+                       $fields[] = 'pp_sortkey';
+                       foreach ( $expected as &$row ) {
+                               $value = $row[1];
+                               if ( is_int( $value ) || is_float( $value ) || is_bool( $value ) ) {
+                                       $row[] = floatval( $value );
+                               } else {
+                                       $row[] = null;
+                               }
+                       }
+               }
+               $this->assertLinksUpdate( $t, $po, 'page_props', $fields, 'pp_page = 111', $expected );
+       }
+       public function testUpdate_page_props_without_sortkey() {
+               $this->setMwGlobals( 'wgPagePropsHaveSortkey', false );
+               $this->testUpdate_page_props();
        }
  
        // @todo test recursive, too!
  
 -      protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput, $table, $fields, $condition, array $expectedRows ) {
 +      protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput,
 +              $table, $fields, $condition, array $expectedRows
 +      ) {
                $update = new LinksUpdate( $title, $parserOutput );
  
                //NOTE: make sure LinksUpdate does not generate warnings when called inside a transaction.