* (bug 14473) Add iwlinks table to track inline interwiki link usage
authorBrion Vibber <brion@users.mediawiki.org>
Fri, 16 Apr 2010 01:40:05 +0000 (01:40 +0000)
committerBrion Vibber <brion@users.mediawiki.org>
Fri, 16 Apr 2010 01:40:05 +0000 (01:40 +0000)
Like langlinks, this stores the interwiki prefix (as iwl_prefix) and full page title (as iwl_title), attached to the page doing the liking (as iwl_from -> page_id).
Unlike langlinks, there can be multiple entries stored per interwiki prefix.

Updater to add the table confirmed on MySQL, untested on SQLite but should work.
Someone may still need to add and test a PostgreSQL updater.

Refactored makeWhereFrom2d() out of LinkBatch to Database so it could be re-used for the similar mapping for the interwiki links, which need a string prefix rather than an int namespace key.
Also cleaned it up internally to reuse existing code for building where clauses from arrays. (Tim & Domas -- if the previous more verbose code was there to reduce function call and array processing overhead on very large link lists, feel free to unroll it again if the difference is measurable. Just swap the var names around from the old LinkBatch code and escape the base key value if it's not an integer, it'll be functionally equivalent.)

includes/LinkBatch.php
includes/LinksUpdate.php
includes/db/Database.php
includes/parser/LinkHolderArray.php
includes/parser/Parser.php
includes/parser/ParserOutput.php
maintenance/archives/patch-iwlinks.sql [new file with mode: 0644]
maintenance/parserTests.inc
maintenance/tables.sql
maintenance/updaters.inc

index d448c80..b8d90eb 100644 (file)
@@ -144,48 +144,9 @@ class LinkBatch {
         *
         * @param $prefix String: the appropriate table's field name prefix ('page', 'pl', etc)
         * @param $db DatabaseBase object to use
-        * @return String
+        * @return mixed string with SQL where clause fragment, or false if no items.
         */
-       public function constructSet( $prefix, &$db ) {
-               $first = true;
-               $firstTitle = true;
-               $sql = '';
-               foreach ( $this->data as $ns => $dbkeys ) {
-                       if ( !count( $dbkeys ) ) {
-                               continue;
-                       }
-
-                       if ( $first ) {
-                               $first = false;
-                       } else {
-                               $sql .= ' OR ';
-                       }
-
-                       if (count($dbkeys)==1) { // avoid multiple-reference syntax if simple equality can be used
-                               $singleKey = array_keys($dbkeys);
-                               $sql .= "({$prefix}_namespace=$ns AND {$prefix}_title=".
-                                       $db->addQuotes($singleKey[0]).
-                                       ")";
-                       } else {
-                               $sql .= "({$prefix}_namespace=$ns AND {$prefix}_title IN (";
-
-                               $firstTitle = true;
-                               foreach( $dbkeys as $dbkey => $unused ) {
-                                       if ( $firstTitle ) {
-                                               $firstTitle = false;
-                                       } else {
-                                               $sql .= ',';
-                                       }
-                                       $sql .= $db->addQuotes( $dbkey );
-                               }
-                               $sql .= '))';
-                       }
-               }
-               if ( $first && $firstTitle ) {
-                       # No titles added
-                       return false;
-               } else {
-                       return $sql;
-               }
+       public function constructSet( $prefix, $db ) {
+           return $db->makeWhereFrom2d( $this->data, "{$prefix}_namespace", "{$prefix}_title" );
        }
 }
index ef3374d..4e31fcd 100644 (file)
@@ -54,6 +54,7 @@ class LinksUpdate {
                $this->mExternals = $parserOutput->getExternalLinks();
                $this->mCategories = $parserOutput->getCategories();
                $this->mProperties = $parserOutput->getProperties();
+               $this->mInterwikis = $parserOutput->getInterwikiLinks();
 
                # Convert the format of the interlanguage links
                # I didn't want to change it in the ParserOutput, because that array is passed all
@@ -115,6 +116,11 @@ class LinksUpdate {
                $this->incrTableUpdate( 'langlinks', 'll', $this->getInterlangDeletions( $existing ),
                        $this->getInterlangInsertions( $existing ) );
 
+               # Inline interwiki links
+               $existing = $this->getExistingInterwikis();
+               $this->incrTableUpdate( 'iwlinks', 'iwl', $this->getInterwikiDeletions( $existing ),
+                       $this->getInterwikiInsertions( $existing ) );
+
                # Template links
                $existing = $this->getExistingTemplates();
                $this->incrTableUpdate( 'templatelinks', 'tl', $this->getTemplateDeletions( $existing ),
@@ -175,6 +181,7 @@ class LinksUpdate {
                $this->dumbTableUpdate( 'templatelinks', $this->getTemplateInsertions(), 'tl_from' );
                $this->dumbTableUpdate( 'externallinks', $this->getExternalInsertions(), 'el_from' );
                $this->dumbTableUpdate( 'langlinks',     $this->getInterlangInsertions(),'ll_from' );
+               $this->dumbTableUpdate( 'iwlinks',       $this->getInterwikiInsertions(),'iwl_from' );
                $this->dumbTableUpdate( 'page_props',    $this->getPropertyInsertions(), 'pp_page' );
 
                # Update the cache of all the category pages and image description
@@ -291,18 +298,6 @@ class LinksUpdate {
                }
        }
 
-       /**
-        * Make a WHERE clause from a 2-d NS/dbkey array
-        *
-        * @param array $arr 2-d array indexed by namespace and DB key
-        * @param string $prefix Field name prefix, without the underscore
-        */
-       function makeWhereFrom2d( &$arr, $prefix ) {
-               $lb = new LinkBatch;
-               $lb->setArray( $arr );
-               return $lb->constructSet( $prefix, $this->mDb );
-       }
-
        /**
         * Update a table by doing a delete query then an insert query
         * @private
@@ -314,8 +309,13 @@ class LinksUpdate {
                        $fromField = "{$prefix}_from";
                }
                $where = array( $fromField => $this->mId );
-               if ( $table == 'pagelinks' || $table == 'templatelinks' ) {
-                       $clause = $this->makeWhereFrom2d( $deletions, $prefix );
+               if ( $table == 'pagelinks' || $table == 'templatelinks' || $table == 'iwlinks' ) {
+                       if ( $table == 'iwlinks' ) {
+                               $baseKey = 'iwl_prefix';
+                       } else {
+                               $baseKey = "{$prefix}_namespace";
+                       }
+                       $clause = $this->mDb->makeWhereFrom2d( $deletions, $baseKey, "{$prefix}_title" );
                        if ( $clause ) {
                                $where[] = $clause;
                        } else {
@@ -476,6 +476,29 @@ class LinksUpdate {
                return $arr;
        }
 
+       /**
+        * Get an array of interwiki insertions for passing to the DB
+        * Skips the titles specified by the 2-D array $existing
+        * @private
+        */
+       function getInterwikiInsertions( $existing = array() ) {
+               $arr = array();
+               foreach( $this->mInterwikis as $prefix => $dbkeys ) {
+                       # array_diff_key() was introduced in PHP 5.1, there is a compatibility function
+                       # in GlobalFunctions.php
+                       $diffs = isset( $existing[$prefix] ) ? array_diff_key( $dbkeys, $existing[$prefix] ) : $dbkeys;
+                       foreach ( $diffs as $dbk => $id ) {
+                               $arr[] = array(
+                                       'iwl_from'   => $this->mId,
+                                       'iwl_prefix' => $prefix,
+                                       'iwl_title'  => $dbk
+                               );
+                       }
+               }
+               return $arr;
+       }
+
+
 
        /**
         * Given an array of existing links, returns those links which are not in $this
@@ -555,6 +578,23 @@ class LinksUpdate {
                return array_diff_assoc( $existing, $this->mProperties );
        }
 
+       /**
+        * Given an array of existing interwiki links, returns those links which are not in $this
+        * and thus should be deleted.
+        * @private
+        */
+       function getInterwikiDeletions( $existing ) {
+               $del = array();
+               foreach ( $existing as $prefix => $dbkeys ) {
+                       if ( isset( $this->mInterwikis[$prefix] ) ) {
+                               $del[$prefix] = array_diff_key( $existing[$prefix], $this->mInterwikis[$prefix] );
+                       } else {
+                               $del[$prefix] = $existing[$prefix];
+                       }
+               }
+               return $del;
+       }
+
        /**
         * Get an array of existing links, as a 2-D array
         * @private
@@ -651,6 +691,24 @@ class LinksUpdate {
                return $arr;
        }
 
+       /**
+        * Get an array of existing inline interwiki links, as a 2-D array
+        * @return array (prefix => array(dbkey => 1))
+        */
+       protected function getExistingInterwikis() {
+               $res = $this->mDb->select( 'iwlinks', array( 'iwl_prefix', 'iwl_title' ),
+                       array( 'iwl_from' => $this->mId ), __METHOD__, $this->mOptions );
+               $arr = array();
+               while ( $row = $this->mDb->fetchObject( $res ) ) {
+                       if ( !isset( $arr[$row->iwl_prefix] ) ) {
+                               $arr[$row->iwl_prefix] = array();
+                       }
+                       $arr[$row->iwl_prefix][$row->iwl_title] = 1;
+               }
+               $this->mDb->freeResult( $res );
+               return $arr;
+       }
+
        /**
         * Get an array of existing categories, with the name in the key and sort key in the value.
         * @private
index c997544..1bfb8e0 100644 (file)
@@ -1273,6 +1273,33 @@ abstract class DatabaseBase {
                return $list;
        }
 
+       /**
+        * Build a partial where clause from a 2-d array such as used for LinkBatch.
+        * The keys on each level may be either integers or strings.
+        *
+        * @param array $data organized as 2-d array(baseKeyVal => array(subKeyVal => <ignored>, ...), ...)
+        * @param string $baseKey field name to match the base-level keys to (eg 'pl_namespace')
+        * @param string $subKey field name to match the sub-level keys to (eg 'pl_title')
+        * @return mixed string SQL fragment, or false if no items in array.
+        */
+       function makeWhereFrom2d( $data, $baseKey, $subKey ) {
+               $conds = array();
+               foreach ( $data as $base => $sub ) {
+                       if ( count( $sub ) ) {
+                               $conds[] = $this->makeList(
+                                       array( $baseKey => $base, $subKey => array_keys( $sub ) ),
+                                       LIST_AND);
+                       }
+               }
+
+               if ( $conds ) {
+                       return $this->makeList( $conds, LIST_OR );
+               } else {
+                       // Nothing to search for...
+                       return false;
+               }
+       }
+
        /**
         * Bitwise operations
         */
index 4f382a4..3fa88bc 100644 (file)
@@ -162,6 +162,9 @@ class LinkHolderArray {
                                # Check if it's a static known link, e.g. interwiki
                                if ( $title->isAlwaysKnown() ) {
                                        $colours[$pdbk] = '';
+                                       if( $title->getInterwiki() != '' ) {
+                                               $output->addInterwikiLink( $title );
+                                       }
                                } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) {
                                        $colours[$pdbk] = $sk->getLinkColour( $title, $threshold );
                                        $output->addLink( $title, $id );
index 8a59b65..2b9cbef 100644 (file)
@@ -1830,7 +1830,7 @@ class Parser {
                        # 
                        # FIXME: isAlwaysKnown() can be expensive for file links; we should really do
                        # batch file existence checks for NS_FILE and NS_MEDIA
-                       if ( $iw == '' && $nt->isAlwaysKnown() ) {
+                       if ( $iw = '' && $nt->isAlwaysKnown() ) {
                                $this->mOutput->addLink( $nt );
                                $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
                        } else {
index ea5840e..4c259f2 100644 (file)
@@ -17,6 +17,7 @@ class ParserOutput
                $mTemplateIds = array(),      # 2-D map of NS/DBK to rev ID for the template references. ID=zero for broken.
                $mImages = array(),           # DB keys of the images used, in the array key only
                $mExternalLinks = array(),    # External link URLs, in the key only
+               $mInterwikiLinks = array(),   # 2-D map of prefix/DBK (in keys only) for the inline interwiki links in the document.
                $mNewSection = false,         # Show a new section link?
                $mHideNewSection = false,     # Hide the new section link?
                $mNoGallery = false,          # No gallery on category page? (__NOGALLERY__)
@@ -40,6 +41,7 @@ class ParserOutput
 
        function getText()                   { return $this->mText; }
        function &getLanguageLinks()         { return $this->mLanguageLinks; }
+       function getInterwikiLinks()         { return $this->mInterwikiLinks; }
        function getCategoryLinks()          { return array_keys( $this->mCategories ); }
        function &getCategories()            { return $this->mCategories; }
        function getCacheTime()              { return $this->mCacheTime; }
@@ -96,9 +98,17 @@ class ParserOutput
                        $this->mExternalLinks[$url] = 1; 
        }
 
+       /**
+        * Record a local or interwiki inline link for saving in future link tables.
+        *
+        * @param Title $title
+        * @param mixed $id optional known page_id so we can skip the lookup
+        */
        function addLink( $title, $id = null ) {
+               wfDebug(__METHOD__ . " got: " . $title->getPrefixedText() . "\n");
                if ( $title->isExternal() ) {
                        // Don't record interwikis in pagelinks
+                       $this->addInterwikiLink( $title );
                        return;
                }
                $ns = $title->getNamespace();
@@ -139,6 +149,21 @@ class ParserOutput
                }
                $this->mTemplateIds[$ns][$dbk] = $rev_id; // For versioning
        }
+       
+       /**
+        * @param Title $title object, must be an interwiki link
+        * @throws MWException if given invalid input
+        */
+       function addInterwikiLink( $title ) {
+               $prefix = $title->getInterwiki();
+               if( $prefix == '' ) {
+                       throw new MWException( 'Non-interwiki link passed, internal parser error.' );
+               }
+               if (!isset($this->mInterwikiLinks[$prefix])) {
+                       $this->mInterwikiLinks[$prefix] = array();
+               }
+               $this->mInterwikiLinks[$prefix][$title->getDBkey()] = 1;
+       }
 
        /**
         * Return true if this cached output object predates the global or
diff --git a/maintenance/archives/patch-iwlinks.sql b/maintenance/archives/patch-iwlinks.sql
new file mode 100644 (file)
index 0000000..463c7b3
--- /dev/null
@@ -0,0 +1,16 @@
+-- 
+-- Track inline interwiki links
+--
+CREATE TABLE /*_*/iwlinks (
+  -- page_id of the referring page
+  iwl_from int unsigned NOT NULL default 0,
+  
+  -- Interwiki prefix code of the target
+  iwl_prefix varbinary(20) NOT NULL default '',
+
+  -- Title of the target, including namespace
+  iwl_title varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+
+CREATE UNIQUE INDEX /*i*/iwl_from ON /*_*/iwlinks (iwl_from, iwl_prefix, iwl_title);
+CREATE INDEX /*i*/iwl_prefix ON /*_*/iwlinks (iwl_prefix, iwl_title);
index ff934ba..29b7fc7 100644 (file)
@@ -602,7 +602,7 @@ class ParserTest {
                global $wgDBtype;
                $tables = array('user', 'page', 'page_restrictions',
                        'protected_titles', 'revision', 'text', 'pagelinks', 'imagelinks',
-                       'categorylinks', 'templatelinks', 'externallinks', 'langlinks',
+                       'categorylinks', 'templatelinks', 'externallinks', 'langlinks', 'iwlinks',
                        'site_stats', 'hitcounter',     'ipblocks', 'image', 'oldimage',
                        'recentchanges', 'watchlist', 'math', 'interwiki',
                        'querycache', 'objectcache', 'job', 'l10n_cache', 'redirect', 'querycachetwo',
index 020343b..661955b 100644 (file)
@@ -607,6 +607,24 @@ CREATE UNIQUE INDEX /*i*/ll_from ON /*_*/langlinks (ll_from, ll_lang);
 CREATE INDEX /*i*/ll_lang ON /*_*/langlinks (ll_lang, ll_title);
 
 
+-- 
+-- Track inline interwiki links
+--
+CREATE TABLE /*_*/iwlinks (
+  -- page_id of the referring page
+  iwl_from int unsigned NOT NULL default 0,
+  
+  -- Interwiki prefix code of the target
+  iwl_prefix varbinary(20) NOT NULL default '',
+
+  -- Title of the target, including namespace
+  iwl_title varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+
+CREATE UNIQUE INDEX /*i*/iwl_from ON /*_*/iwlinks (iwl_from, iwl_prefix, iwl_title);
+CREATE INDEX /*i*/iwl_prefix ON /*_*/iwlinks (iwl_prefix, iwl_title);
+
+
 --
 -- Contains a single row with some aggregate info
 -- on the state of the site.
index 5127675..8bdfea1 100644 (file)
@@ -172,6 +172,9 @@ $wgUpdates = array(
                array( 'do_update_mime_minor_field' ),
                // Should've done this back in 1.10, but better late than never:
                array( 'do_populate_rev_len' ),
+
+               // 1.17
+               array( 'add_table', 'iwlinks',                           'patch-iwlinks.sql' ),
        ),
 
        'sqlite' => array(
@@ -199,6 +202,9 @@ $wgUpdates = array(
                array( 'do_update_transcache_field' ),
                // version-independent searchindex setup, added in 1.16
                array( 'sqlite_setup_searchindex' ),
+
+               // 1.17
+               array( 'add_table', 'iwlinks',                           'patch-iwlinks.sql' ), // @fixme so far untested on sqlite 2010-04-16
        ),
 );
 
@@ -1590,6 +1596,7 @@ function do_postgres_updates() {
                array('user_properties',   'patch-user_properties.sql'),
                array('log_search',        'patch-log_search.sql'),
                array('l10n_cache',        'patch-l10n_cache.sql'),
+               // @fixme add iwlinks table
        );
 
        $newcols = array(