Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 92 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
ResolveStubs | |
0.00% |
0 / 89 |
|
0.00% |
0 / 4 |
306 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
20 | |||
setUndoLog | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
resolveStub | |
0.00% |
0 / 53 |
|
0.00% |
0 / 1 |
132 |
1 | <?php |
2 | /** |
3 | * Convert history stubs that point to an external row to direct external |
4 | * pointers. |
5 | * |
6 | * This program is free software; you can redistribute it and/or modify |
7 | * it under the terms of the GNU General Public License as published by |
8 | * the Free Software Foundation; either version 2 of the License, or |
9 | * (at your option) any later version. |
10 | * |
11 | * This program is distributed in the hope that it will be useful, |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 | * GNU General Public License for more details. |
15 | * |
16 | * You should have received a copy of the GNU General Public License along |
17 | * with this program; if not, write to the Free Software Foundation, Inc., |
18 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
19 | * http://www.gnu.org/copyleft/gpl.html |
20 | * |
21 | * @file |
22 | * @ingroup Maintenance ExternalStorage |
23 | */ |
24 | |
25 | use MediaWiki\Maintenance\UndoLog; |
26 | use MediaWiki\Storage\SqlBlobStore; |
27 | |
28 | require_once __DIR__ . '/../Maintenance.php'; |
29 | |
30 | class ResolveStubs extends Maintenance { |
31 | /** @var UndoLog|null */ |
32 | private $undoLog; |
33 | |
34 | public function __construct() { |
35 | parent::__construct(); |
36 | $this->setBatchSize( 1000 ); |
37 | $this->addOption( 'dry-run', 'Don\'t update any rows' ); |
38 | $this->addOption( 'undo', 'Undo log location', false, true ); |
39 | } |
40 | |
41 | /** |
42 | * Convert history stubs that point to an external row to direct |
43 | * external pointers |
44 | */ |
45 | public function execute() { |
46 | $dbw = $this->getPrimaryDB(); |
47 | $dbr = $this->getReplicaDB(); |
48 | $maxID = $dbr->newSelectQueryBuilder() |
49 | ->select( 'MAX(old_id)' ) |
50 | ->from( 'text' ) |
51 | ->caller( __METHOD__ )->fetchField(); |
52 | $blockSize = $this->getBatchSize(); |
53 | $dryRun = $this->getOption( 'dry-run' ); |
54 | $this->setUndoLog( new UndoLog( $this->getOption( 'undo' ), $dbw ) ); |
55 | |
56 | $numBlocks = intval( $maxID / $blockSize ) + 1; |
57 | $numResolved = 0; |
58 | $numTotal = 0; |
59 | |
60 | for ( $b = 0; $b < $numBlocks; $b++ ) { |
61 | $this->waitForReplication(); |
62 | |
63 | $this->output( sprintf( "%5.2f%%\n", $b / $numBlocks * 100 ) ); |
64 | $start = $blockSize * $b + 1; |
65 | $end = $blockSize * ( $b + 1 ); |
66 | |
67 | $res = $dbr->newSelectQueryBuilder() |
68 | ->select( [ 'old_id', 'old_text', 'old_flags' ] ) |
69 | ->from( 'text' ) |
70 | ->where( |
71 | "old_id>=$start AND old_id<=$end " . |
72 | "AND old_flags LIKE '%object%' AND old_flags NOT LIKE '%external%' " . |
73 | // LOWER() doesn't work on binary text, need to convert |
74 | 'AND LOWER(CONVERT(LEFT(old_text,22) USING latin1)) = \'o:15:"historyblobstub"\'' |
75 | ) |
76 | ->caller( __METHOD__ )->fetchResultSet(); |
77 | foreach ( $res as $row ) { |
78 | $numResolved += $this->resolveStub( $row, $dryRun ) ? 1 : 0; |
79 | $numTotal++; |
80 | } |
81 | } |
82 | $this->output( "100%\n" ); |
83 | $this->output( "$numResolved of $numTotal stubs resolved\n" ); |
84 | } |
85 | |
86 | /** |
87 | * @param UndoLog $undoLog |
88 | */ |
89 | public function setUndoLog( UndoLog $undoLog ) { |
90 | $this->undoLog = $undoLog; |
91 | } |
92 | |
93 | /** |
94 | * Resolve a history stub. |
95 | * |
96 | * This is called by MoveToExternal |
97 | * |
98 | * @param stdClass $row The existing text row |
99 | * @param bool $dryRun |
100 | * @return bool |
101 | */ |
102 | public function resolveStub( $row, $dryRun ) { |
103 | $id = $row->old_id; |
104 | $stub = unserialize( $row->old_text ); |
105 | $flags = SqlBlobStore::explodeFlags( $row->old_flags ); |
106 | |
107 | $dbr = $this->getReplicaDB(); |
108 | |
109 | if ( !( $stub instanceof HistoryBlobStub ) ) { |
110 | print "Error at old_id $id: found object of class " . get_class( $stub ) . |
111 | ", expecting HistoryBlobStub\n"; |
112 | return false; |
113 | } |
114 | |
115 | $mainId = $stub->getLocation(); |
116 | if ( !$mainId ) { |
117 | print "Error at old_id $id: falsey location\n"; |
118 | return false; |
119 | } |
120 | |
121 | # Get the main text row |
122 | $mainTextRow = $dbr->newSelectQueryBuilder() |
123 | ->select( [ 'old_text', 'old_flags' ] ) |
124 | ->from( 'text' ) |
125 | ->where( [ 'old_id' => $mainId ] ) |
126 | ->caller( __METHOD__ )->fetchRow(); |
127 | |
128 | if ( !$mainTextRow ) { |
129 | print "Error at old_id $id: can't find main text row old_id $mainId\n"; |
130 | return false; |
131 | } |
132 | |
133 | $mainFlags = SqlBlobStore::explodeFlags( $mainTextRow->old_flags ); |
134 | $mainText = $mainTextRow->old_text; |
135 | |
136 | if ( !in_array( 'external', $mainFlags ) ) { |
137 | print "Error at old_id $id: target $mainId is not external\n"; |
138 | return false; |
139 | } |
140 | if ( preg_match( '!^DB://([^/]*)/([^/]*)/[0-9a-f]{32}$!', $mainText ) ) { |
141 | print "Error at old_id $id: target $mainId is a CGZ pointer\n"; |
142 | return false; |
143 | } |
144 | if ( preg_match( '!^DB://([^/]*)/([^/]*)/[0-9]{1,6}$!', $mainText ) ) { |
145 | print "Error at old_id $id: target $mainId is a DHB pointer\n"; |
146 | return false; |
147 | } |
148 | if ( !preg_match( '!^DB://([^/]*)/([^/]*)$!', $mainText ) ) { |
149 | print "Error at old_id $id: target $mainId has unrecognised text\n"; |
150 | return false; |
151 | } |
152 | |
153 | # Preserve the legacy encoding flag, but switch from object to external |
154 | if ( in_array( 'utf-8', $flags ) ) { |
155 | $newFlags = 'utf-8,external'; |
156 | } else { |
157 | $newFlags = 'external'; |
158 | } |
159 | $newText = $mainText . '/' . $stub->getHash(); |
160 | |
161 | # Update the row |
162 | if ( $dryRun ) { |
163 | $this->output( "Resolve $id => $newFlags $newText\n" ); |
164 | } else { |
165 | $updated = $this->undoLog->update( |
166 | 'text', |
167 | [ |
168 | 'old_flags' => $newFlags, |
169 | 'old_text' => $newText |
170 | ], |
171 | (array)$row, |
172 | __METHOD__ |
173 | ); |
174 | if ( !$updated ) { |
175 | $this->output( "Updated of old_id $id failed to match\n" ); |
176 | return false; |
177 | } |
178 | } |
179 | return true; |
180 | } |
181 | } |
182 | |
183 | $maintClass = ResolveStubs::class; |
184 | require_once RUN_MAINTENANCE_IF_MAIN; |