Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 89 |
|
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 | // @codeCoverageIgnoreStart |
29 | require_once __DIR__ . '/../Maintenance.php'; |
30 | // @codeCoverageIgnoreEnd |
31 | |
32 | class ResolveStubs extends Maintenance { |
33 | /** @var UndoLog|null */ |
34 | private $undoLog; |
35 | |
36 | public function __construct() { |
37 | parent::__construct(); |
38 | $this->setBatchSize( 1000 ); |
39 | $this->addOption( 'dry-run', 'Don\'t update any rows' ); |
40 | $this->addOption( 'undo', 'Undo log location', false, true ); |
41 | } |
42 | |
43 | /** |
44 | * Convert history stubs that point to an external row to direct |
45 | * external pointers |
46 | */ |
47 | public function execute() { |
48 | $dbw = $this->getPrimaryDB(); |
49 | $dbr = $this->getReplicaDB(); |
50 | $maxID = $dbr->newSelectQueryBuilder() |
51 | ->select( 'MAX(old_id)' ) |
52 | ->from( 'text' ) |
53 | ->caller( __METHOD__ )->fetchField(); |
54 | $blockSize = $this->getBatchSize(); |
55 | $dryRun = $this->getOption( 'dry-run' ); |
56 | $this->setUndoLog( new UndoLog( $this->getOption( 'undo' ), $dbw ) ); |
57 | |
58 | $numBlocks = intval( $maxID / $blockSize ) + 1; |
59 | $numResolved = 0; |
60 | $numTotal = 0; |
61 | |
62 | for ( $b = 0; $b < $numBlocks; $b++ ) { |
63 | $this->waitForReplication(); |
64 | |
65 | $this->output( sprintf( "%5.2f%%\n", $b / $numBlocks * 100 ) ); |
66 | $start = $blockSize * $b + 1; |
67 | $end = $blockSize * ( $b + 1 ); |
68 | |
69 | $res = $dbr->newSelectQueryBuilder() |
70 | ->select( [ 'old_id', 'old_text', 'old_flags' ] ) |
71 | ->from( 'text' ) |
72 | ->where( |
73 | "old_id>=$start AND old_id<=$end " . |
74 | "AND old_flags LIKE '%object%' AND old_flags NOT LIKE '%external%' " . |
75 | // LOWER() doesn't work on binary text, need to convert |
76 | 'AND LOWER(CONVERT(LEFT(old_text,22) USING latin1)) = \'o:15:"historyblobstub"\'' |
77 | ) |
78 | ->caller( __METHOD__ )->fetchResultSet(); |
79 | foreach ( $res as $row ) { |
80 | $numResolved += $this->resolveStub( $row, $dryRun ) ? 1 : 0; |
81 | $numTotal++; |
82 | } |
83 | } |
84 | $this->output( "100%\n" ); |
85 | $this->output( "$numResolved of $numTotal stubs resolved\n" ); |
86 | } |
87 | |
88 | /** |
89 | * @param UndoLog $undoLog |
90 | */ |
91 | public function setUndoLog( UndoLog $undoLog ) { |
92 | $this->undoLog = $undoLog; |
93 | } |
94 | |
95 | /** |
96 | * Resolve a history stub. |
97 | * |
98 | * This is called by MoveToExternal |
99 | * |
100 | * @param stdClass $row The existing text row |
101 | * @param bool $dryRun |
102 | * @return bool |
103 | */ |
104 | public function resolveStub( $row, $dryRun ) { |
105 | $id = $row->old_id; |
106 | $stub = unserialize( $row->old_text ); |
107 | $flags = SqlBlobStore::explodeFlags( $row->old_flags ); |
108 | |
109 | $dbr = $this->getReplicaDB(); |
110 | |
111 | if ( !( $stub instanceof HistoryBlobStub ) ) { |
112 | print "Error at old_id $id: found object of class " . get_class( $stub ) . |
113 | ", expecting HistoryBlobStub\n"; |
114 | return false; |
115 | } |
116 | |
117 | $mainId = $stub->getLocation(); |
118 | if ( !$mainId ) { |
119 | print "Error at old_id $id: falsey location\n"; |
120 | return false; |
121 | } |
122 | |
123 | # Get the main text row |
124 | $mainTextRow = $dbr->newSelectQueryBuilder() |
125 | ->select( [ 'old_text', 'old_flags' ] ) |
126 | ->from( 'text' ) |
127 | ->where( [ 'old_id' => $mainId ] ) |
128 | ->caller( __METHOD__ )->fetchRow(); |
129 | |
130 | if ( !$mainTextRow ) { |
131 | print "Error at old_id $id: can't find main text row old_id $mainId\n"; |
132 | return false; |
133 | } |
134 | |
135 | $mainFlags = SqlBlobStore::explodeFlags( $mainTextRow->old_flags ); |
136 | $mainText = $mainTextRow->old_text; |
137 | |
138 | if ( !in_array( 'external', $mainFlags ) ) { |
139 | print "Error at old_id $id: target $mainId is not external\n"; |
140 | return false; |
141 | } |
142 | if ( preg_match( '!^DB://([^/]*)/([^/]*)/[0-9a-f]{32}$!', $mainText ) ) { |
143 | print "Error at old_id $id: target $mainId is a CGZ pointer\n"; |
144 | return false; |
145 | } |
146 | if ( preg_match( '!^DB://([^/]*)/([^/]*)/[0-9]{1,6}$!', $mainText ) ) { |
147 | print "Error at old_id $id: target $mainId is a DHB pointer\n"; |
148 | return false; |
149 | } |
150 | if ( !preg_match( '!^DB://([^/]*)/([^/]*)$!', $mainText ) ) { |
151 | print "Error at old_id $id: target $mainId has unrecognised text\n"; |
152 | return false; |
153 | } |
154 | |
155 | # Preserve the legacy encoding flag, but switch from object to external |
156 | if ( in_array( 'utf-8', $flags ) ) { |
157 | $newFlags = 'utf-8,external'; |
158 | } else { |
159 | $newFlags = 'external'; |
160 | } |
161 | $newText = $mainText . '/' . $stub->getHash(); |
162 | |
163 | # Update the row |
164 | if ( $dryRun ) { |
165 | $this->output( "Resolve $id => $newFlags $newText\n" ); |
166 | } else { |
167 | $updated = $this->undoLog->update( |
168 | 'text', |
169 | [ |
170 | 'old_flags' => $newFlags, |
171 | 'old_text' => $newText |
172 | ], |
173 | (array)$row, |
174 | __METHOD__ |
175 | ); |
176 | if ( !$updated ) { |
177 | $this->output( "Updated of old_id $id failed to match\n" ); |
178 | return false; |
179 | } |
180 | } |
181 | return true; |
182 | } |
183 | } |
184 | |
185 | // @codeCoverageIgnoreStart |
186 | $maintClass = ResolveStubs::class; |
187 | require_once RUN_MAINTENANCE_IF_MAIN; |
188 | // @codeCoverageIgnoreEnd |