Index: trunk/extensions/LiquidThreads/LiquidThreads.php |
— | — | @@ -67,9 +67,11 @@ |
68 | 68 | $wgHooks['ArticleRevisionUndeleted'][] = 'LqtDeletionController::onArticleRevisionUndeleted'; |
69 | 69 | $wgHooks['ArticleUndelete'][] = 'LqtDeletionController::onArticleUndelete'; |
70 | 70 | $wgHooks['ArticleConfirmDelete'][] = 'LqtDeletionController::onArticleConfirmDelete'; |
| 71 | +$wgHooks['ArticleDelete'][] = 'LqtDeletionController::onArticleDelete'; |
71 | 72 | |
72 | 73 | // Moving |
73 | | -$wgHooks['SpecialMovepageAfterMove'][] = 'LqtHooks::onArticleMove'; |
| 74 | +$wgHooks['SpecialMovepageAfterMove'][] = 'LqtHooks::onArticleMoveComplete'; |
| 75 | +$wgHooks['AbortMove'][] = 'LqtHooks::onArticleMove'; |
74 | 76 | |
75 | 77 | // Search |
76 | 78 | $wgHooks['ShowSearchHitTitle'][] = 'LqtHooks::customiseSearchResultTitle'; |
— | — | @@ -106,6 +108,7 @@ |
107 | 109 | $wgAutoloadClasses['LqtDeletionController'] = $dir . 'classes/DeletionController.php'; |
108 | 110 | $wgAutoloadClasses['LqtHooks'] = $dir . 'classes/Hooks.php'; |
109 | 111 | $wgAutoloadClasses['ThreadRevision'] = $dir."/classes/ThreadRevision.php"; |
| 112 | +$wgAutoloadClasses['SynchroniseThreadArticleDataJob'] = "$dir/classes/SynchroniseThreadArticleDataJob.php"; |
110 | 113 | |
111 | 114 | // View classes |
112 | 115 | $wgAutoloadClasses['TalkpageView'] = $dir . 'pages/TalkpageView.php'; |
— | — | @@ -127,6 +130,9 @@ |
128 | 131 | $wgAutoloadClasses['SpecialSplitThread'] = $dir . 'pages/SpecialSplitThread.php'; |
129 | 132 | $wgAutoloadClasses['SpecialMergeThread'] = $dir . 'pages/SpecialMergeThread.php'; |
130 | 133 | |
| 134 | +// Job queue |
| 135 | +$wgJobClasses['synchroniseThreadArticleData'] = 'SynchroniseThreadArticleDataJob'; |
| 136 | + |
131 | 137 | // Backwards-compatibility |
132 | 138 | $wgAutoloadClasses['Article_LQT_Compat'] = $dir . 'compat/LqtCompatArticle.php'; |
133 | 139 | if ( version_compare( $wgVersion, '1.16', '<' ) ) { |
Index: trunk/extensions/LiquidThreads/classes/Threads.php |
— | — | @@ -162,10 +162,18 @@ |
163 | 163 | static function articleClause( $article ) { |
164 | 164 | $dbr = wfGetDB( DB_SLAVE ); |
165 | 165 | |
166 | | - $arr = array( 'thread_article_title' => $article->getTitle()->getDBKey(), |
| 166 | + $titleCond = array( 'thread_article_title' => $article->getTitle()->getDBKey(), |
167 | 167 | 'thread_article_namespace' => $article->getTitle()->getNamespace() ); |
| 168 | + $titleCond = $dbr->makeList( $titleCond, LIST_AND ); |
168 | 169 | |
169 | | - return $dbr->makeList( $arr, LIST_AND ); |
| 170 | + $conds = array( $titleCond ); |
| 171 | + |
| 172 | + if ( $article->getId() ) { |
| 173 | + $idCond = array( 'thread_article_id' => $article->getId() ); |
| 174 | + $conds[] = $dbr->makeList( $idCond, LIST_AND ); |
| 175 | + } |
| 176 | + |
| 177 | + return $dbr->makeList( $conds, LIST_OR ); |
170 | 178 | } |
171 | 179 | |
172 | 180 | static function topLevelClause() { |
— | — | @@ -241,5 +249,65 @@ |
242 | 250 | $i++; |
243 | 251 | } |
244 | 252 | return $t; |
245 | | - } |
| 253 | + } |
| 254 | + |
| 255 | + // Called just before any function that might cause a loss of article association. |
| 256 | + // by breaking either a NS-title reference (by moving the article), or a page-id |
| 257 | + // reference (by deleting the article). |
| 258 | + // Basically ensures that all subthreads have the two stores of article association |
| 259 | + // synchronised. |
| 260 | + // Can also be called with a "limit" parameter to slowly convert old threads. This |
| 261 | + // is intended to be used by jobs created by move and create operations to slowly |
| 262 | + // propagate the change through the data set without rushing the whole conversion |
| 263 | + // when a second breaking change is made. If a limit is set and more rows require |
| 264 | + // conversion, this function will return false. Otherwise, true will be returned. |
| 265 | + // If the queueMore parameter is set and rows are left to update, a job queue item |
| 266 | + // will then be added with the same limit, to finish the remainder of the update. |
| 267 | + static function synchroniseArticleData( $article, $limit = false, $queueMore = false ) { |
| 268 | + $dbr = wfGetDB( DB_SLAVE ); |
| 269 | + $dbw = wfGetDB( DB_MASTER ); |
| 270 | + |
| 271 | + $title = $article->getTitle(); |
| 272 | + $id = $article->getId(); |
| 273 | + |
| 274 | + $titleCond = array( 'thread_article_namespace' => $title->getNamespace(), |
| 275 | + 'thread_article_title' => $title->getDBkey() ); |
| 276 | + $titleCondText = $dbr->makeList( $titleCond, LIST_AND ); |
| 277 | + |
| 278 | + $idCond = array( 'thread_article_id' => $id ); |
| 279 | + $idCondText = $dbr->makeList( $idCond, LIST_AND ); |
| 280 | + |
| 281 | + $fixTitleCond = array( $idCondText, "NOT ($titleCondText)" ); |
| 282 | + $fixIdCond = array( $titleCondText, "NOT ($idCondText)" ); |
| 283 | + |
| 284 | + // Try to hit the most recent threads first. |
| 285 | + $options = array( 'LIMIT' => 500, 'ORDER BY' => 'thread_id DESC' ); |
| 286 | + |
| 287 | + // Batch in 500s |
| 288 | + if ($limit) $options['LIMIT'] = min( $limit, 500 ); |
| 289 | + |
| 290 | + $rowsAffected = 0; |
| 291 | + $roundRowsAffected = 1; |
| 292 | + while( (!$limit || $rowsAffected < $limit) && $roundRowsAffected > 0 ) { |
| 293 | + $roundRowsAffected = 0; |
| 294 | + |
| 295 | + // Fix wrong title. |
| 296 | + $res = $dbw->update( 'thread', $titleCond, $fixTitleCond, __METHOD__, $options ); |
| 297 | + $roundRowsAffected += $dbw->affectedRows(); |
| 298 | + |
| 299 | + // Fix wrong ID |
| 300 | + $res = $dbw->update( 'thread', $idCond, $fixIdCond, __METHOD__, $options ); |
| 301 | + $roundRowsAffected += $dbw->affectedRows(); |
| 302 | + |
| 303 | + $rowsAffected += $roundRowsAffected; |
| 304 | + } |
| 305 | + |
| 306 | + if ( $limit && ($rowsAffected >= $limit) && $queueMore ) { |
| 307 | + $jobParams = array( 'limit' => $limit, 'cascade' => true ); |
| 308 | + $job = new SynchroniseThreadArticleDataJob( $article->getTitle(), $jobParams ); |
| 309 | + $job->insert(); |
| 310 | + } |
| 311 | + |
| 312 | + return $limit ? ($rowsAffected < $limit) : true; |
| 313 | + } |
246 | 314 | } |
Index: trunk/extensions/LiquidThreads/classes/Hooks.php |
— | — | @@ -326,33 +326,31 @@ |
327 | 327 | $wgExtNewFields[] = array( "thread", "thread_author_name", "$dir/schema-changes/store_subject-author.sql" ); |
328 | 328 | $wgExtNewFields[] = array( "thread", "thread_sortkey", "$dir/schema-changes/new-sortkey.sql" ); |
329 | 329 | $wgExtNewFields[] = array( 'thread', 'thread_replies', "$dir/schema-changes/store_reply_count.sql" ); |
| 330 | + $wgExtNewFields[] = array( 'thread', 'thread_article_id', "$dir/schema-changes/store_article_id.sql" ); |
330 | 331 | |
331 | 332 | $wgExtNewIndexes[] = array( 'thread', 'thread_summary_page', '(thread_summary_page)' ); |
332 | 333 | |
333 | 334 | return true; |
334 | 335 | } |
335 | 336 | |
336 | | - static function onArticleMove( &$form, &$ot, &$nt ) { |
| 337 | + static function onArticleMoveComplete( &$form, &$ot, &$nt ) { |
337 | 338 | // Check if it's a talk page. |
338 | 339 | if ( !LqtDispatch::isLqtPage( $ot ) && !LqtDispatch::isLqtPage( $nt ) ) { |
339 | 340 | return true; |
340 | 341 | } |
341 | 342 | |
342 | | - // Find and move all threads, using the job queue if necessary. |
343 | | - $otCond = array( 'thread_article_namespace' => $ot->getNamespace(), |
344 | | - 'thread_article_title' => $ot->getDBkey() ); |
| 343 | + // Synchronise the first 500 threads, in reverse order by thread id. If |
| 344 | + // there are more threads to synchronise, the job queue will take over. |
| 345 | + Threads::synchroniseArticleData( new Article( $nt ), 500, 'cascade' ); |
345 | 346 | |
346 | | - $ntCond = array( 'thread_article_namespace' => $nt->getNamespace(), |
347 | | - 'thread_article_title' => $nt->getDBkey() ); |
348 | | - |
349 | | - $dbw = wfGetDB( DB_MASTER ); |
350 | | - $rowCountEst = $dbw->estimateRowCount( 'thread', '*', $otCond, __METHOD__ ); |
| 347 | + return true; |
| 348 | + } |
| 349 | + |
| 350 | + static function onArticleMove( $ot, $nt, $user, &$err, $reason ) { |
| 351 | + // Synchronise article data so that moving the article doesn't break any |
| 352 | + // article association. |
| 353 | + Threads::synchroniseArticleData( new Article( $ot ) ); |
351 | 354 | |
352 | | - wfDebug( "Row count estimate is $rowCountEst\n" ); |
353 | | - |
354 | | - |
355 | | - $dbw->update( 'thread', $ntCond, $otCond, __METHOD__ ); |
356 | | - |
357 | 355 | return true; |
358 | 356 | } |
359 | 357 | |
Index: trunk/extensions/LiquidThreads/classes/DeletionController.php |
— | — | @@ -36,6 +36,10 @@ |
37 | 37 | } |
38 | 38 | } |
39 | 39 | |
| 40 | + // Synchronise the first 500 threads, in reverse order by thread id. If |
| 41 | + // there are more threads to synchronise, the job queue will take over. |
| 42 | + Threads::synchroniseArticleData( $article, 500, 'cascade' ); |
| 43 | + |
40 | 44 | return true; |
41 | 45 | } |
42 | 46 | |
— | — | @@ -85,6 +89,10 @@ |
86 | 90 | } |
87 | 91 | } |
88 | 92 | |
| 93 | + // Synchronise the first 500 threads, in reverse order by thread id. If |
| 94 | + // there are more threads to synchronise, the job queue will take over. |
| 95 | + Threads::synchroniseArticleData( new Article( $udTitle ), 500, 'cascade' ); |
| 96 | + |
89 | 97 | return true; |
90 | 98 | } |
91 | 99 | |
— | — | @@ -103,4 +111,12 @@ |
104 | 112 | |
105 | 113 | return true; |
106 | 114 | } |
| 115 | + |
| 116 | + static function onArticleDelete( $article ) { |
| 117 | + // Synchronise article data so that moving the article doesn't break any |
| 118 | + // article association. |
| 119 | + Threads::synchroniseArticleData( $article ); |
| 120 | + |
| 121 | + return true; |
| 122 | + } |
107 | 123 | } |
Index: trunk/extensions/LiquidThreads/classes/Thread.php |
— | — | @@ -360,6 +360,7 @@ |
361 | 361 | 'thread_root' => 'rootId', |
362 | 362 | 'thread_article_namespace' => 'articleNamespace', |
363 | 363 | 'thread_article_title' => 'articleTitle', |
| 364 | + 'thread_article_id' => 'articleId', |
364 | 365 | 'thread_summary_page' => 'summaryId', |
365 | 366 | 'thread_ancestor' => 'ancestorId', |
366 | 367 | 'thread_parent' => 'parentId', |
— | — | @@ -696,6 +697,42 @@ |
697 | 698 | $set['thread_replies'] = $count; |
698 | 699 | } |
699 | 700 | |
| 701 | + // Check for invalid/missing articleId |
| 702 | + $articleTitle = null; |
| 703 | + $dbTitle = Title::makeTitleSafe( $this->articleNamespace, $this->articleTitle ); |
| 704 | + if ($this->articleId && isset(self::$titleCacheById[$this->articleId]) ) { |
| 705 | + // If it corresponds to a title, the article obviously exists. |
| 706 | + $articleTitle = self::$titleCacheById[$this->articleId]; |
| 707 | + $this->article = new Article_LQT_Compat( $articleTitle ); |
| 708 | + } elseif( $this->articleId ) { |
| 709 | + $articleTitle = Title::newFromID( $this->articleId ); |
| 710 | + } |
| 711 | + |
| 712 | + // If still unfilled, the article ID referred to is no longer valid. Re-fill it |
| 713 | + // from the namespace/title pair if an article ID is provided |
| 714 | + if ( !$articleTitle && ( $this->articleId != 0 || $dbTitle->getArticleId() != 0 ) ) { |
| 715 | + $articleTitle = $dbTitle; |
| 716 | + $this->articleId = $articleTitle->getArticleId(); |
| 717 | + $this->article = new Article_LQT_Compat( $dbTitle ); |
| 718 | + |
| 719 | + $set['thread_article_id'] = $this->articleId; |
| 720 | + wfDebug( "Unfilled or non-existent thread_article_id, refilling to {$this->articleId}\n" ); |
| 721 | + |
| 722 | + // There are probably problems on the rest of the article, trigger a small update |
| 723 | + Threads::synchroniseArticleData( $this->article, 100, 'cascade' ); |
| 724 | + } elseif ( $articleTitle && !$articleTitle->equals( $dbTitle ) ) { |
| 725 | + // The page was probably moved and this was probably not updated. |
| 726 | + wfDebug( "Article ID/Title discrepancy, resetting NS/Title to article provided by ID\n" ); |
| 727 | + $this->articleNamespace = $articleTitle->getNamespace(); |
| 728 | + $this->articleTitle = $articleTitle->getDBkey(); |
| 729 | + |
| 730 | + $set['thread_article_namespace'] = $articleTitle->getNamespace(); |
| 731 | + $set['thread_article_title'] = $articleTitle->getDBkey(); |
| 732 | + |
| 733 | + // There are probably problems on the rest of the article, trigger a small update |
| 734 | + Threads::synchroniseArticleData( $this->article, 100, 'cascade' ); |
| 735 | + } |
| 736 | + |
700 | 737 | if ( count($set) ) { |
701 | 738 | $dbw = wfGetDB( DB_MASTER ); |
702 | 739 | |
Index: trunk/extensions/LiquidThreads/lqt.sql |
— | — | @@ -15,6 +15,7 @@ |
16 | 16 | |
17 | 17 | thread_article_namespace int NOT NULL, |
18 | 18 | thread_article_title varchar(255) binary NOT NULL, |
| 19 | + thread_article_id int(8) unsigned NOT NULL, |
19 | 20 | |
20 | 21 | -- Special thread types (deleted/move trace/normal) |
21 | 22 | thread_type int(4) unsigned NOT NULL default 0, |
— | — | @@ -29,6 +30,7 @@ |
30 | 31 | UNIQUE INDEX thread_root_page (thread_root), |
31 | 32 | INDEX thread_ancestor (thread_ancestor, thread_parent), |
32 | 33 | INDEX thread_article_title (thread_article_namespace, thread_article_title, thread_sortkey), |
| 34 | + INDEX thread_article (thread_article_id, thread_sortkey), |
33 | 35 | INDEX thread_modified (thread_modified), |
34 | 36 | INDEX thread_created (thread_created), |
35 | 37 | INDEX thread_summary_page (thread_summary_page), |
Index: trunk/extensions/LiquidThreads/migrateDatabase.php |
— | — | @@ -9,6 +9,8 @@ |
10 | 10 | |
11 | 11 | $db = wfGetDB( DB_MASTER ); |
12 | 12 | |
| 13 | +$wgTitle = Title::makeTitleSafe( NS_SPECIAL, 'LiquidThreads' ); |
| 14 | + |
13 | 15 | // Do database updates |
14 | 16 | $threadFieldUpdates = array('thread_article_namespace' => 'split-thread_article.sql', |
15 | 17 | 'thread_article_title' => 'split-thread_article.sql', |
— | — | @@ -22,6 +24,7 @@ |
23 | 25 | 'thread_author_name' => 'store_subject-author.sql', |
24 | 26 | 'thread_sortkey' => 'new-sortkey.sql', |
25 | 27 | 'thread_replies' => 'store_reply_count.sql', |
| 28 | + 'thread_article_id' => 'store_article_id.sql', |
26 | 29 | ); |
27 | 30 | $threadIndexUpdates = array('thread_summary_page' => 'index-summary_page.sql'); |
28 | 31 | |
Index: trunk/extensions/LiquidThreads/schema-changes/store_article_id.sql |
— | — | @@ -0,0 +1,3 @@ |
| 2 | +-- Add the thread_article_id field. Populated on-demand in Thread.php |
| 3 | +ALTER TABLE /*_*/thread ADD COLUMN thread_article_id int(8) unsigned NOT NULL; |
| 4 | +ALTER TABLE /*_*/thread ADD INDEX thread_article (thread_article_id, thread_sortkey); |