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 | public function setUndoLog( UndoLog $undoLog ) { |
89 | $this->undoLog = $undoLog; |
90 | } |
91 | |
92 | /** |
93 | * Resolve a history stub. |
94 | * |
95 | * This is called by MoveToExternal |
96 | * |
97 | * @param stdClass $row The existing text row |
98 | * @param bool $dryRun |
99 | * @return bool |
100 | */ |
101 | public function resolveStub( $row, $dryRun ) { |
102 | $id = $row->old_id; |
103 | $stub = unserialize( $row->old_text ); |
104 | $flags = SqlBlobStore::explodeFlags( $row->old_flags ); |
105 | |
106 | $dbr = $this->getReplicaDB(); |
107 | |
108 | if ( !( $stub instanceof HistoryBlobStub ) ) { |
109 | print "Error at old_id $id: found object of class " . get_class( $stub ) . |
110 | ", expecting HistoryBlobStub\n"; |
111 | return false; |
112 | } |
113 | |
114 | $mainId = $stub->getLocation(); |
115 | if ( !$mainId ) { |
116 | print "Error at old_id $id: falsey location\n"; |
117 | return false; |
118 | } |
119 | |
120 | # Get the main text row |
121 | $mainTextRow = $dbr->newSelectQueryBuilder() |
122 | ->select( [ 'old_text', 'old_flags' ] ) |
123 | ->from( 'text' ) |
124 | ->where( [ 'old_id' => $mainId ] ) |
125 | ->caller( __METHOD__ )->fetchRow(); |
126 | |
127 | if ( !$mainTextRow ) { |
128 | print "Error at old_id $id: can't find main text row old_id $mainId\n"; |
129 | return false; |
130 | } |
131 | |
132 | $mainFlags = SqlBlobStore::explodeFlags( $mainTextRow->old_flags ); |
133 | $mainText = $mainTextRow->old_text; |
134 | |
135 | if ( !in_array( 'external', $mainFlags ) ) { |
136 | print "Error at old_id $id: target $mainId is not external\n"; |
137 | return false; |
138 | } |
139 | if ( preg_match( '!^DB://([^/]*)/([^/]*)/[0-9a-f]{32}$!', $mainText ) ) { |
140 | print "Error at old_id $id: target $mainId is a CGZ pointer\n"; |
141 | return false; |
142 | } |
143 | if ( preg_match( '!^DB://([^/]*)/([^/]*)/[0-9]{1,6}$!', $mainText ) ) { |
144 | print "Error at old_id $id: target $mainId is a DHB pointer\n"; |
145 | return false; |
146 | } |
147 | if ( !preg_match( '!^DB://([^/]*)/([^/]*)$!', $mainText ) ) { |
148 | print "Error at old_id $id: target $mainId has unrecognised text\n"; |
149 | return false; |
150 | } |
151 | |
152 | # Preserve the legacy encoding flag, but switch from object to external |
153 | if ( in_array( 'utf-8', $flags ) ) { |
154 | $newFlags = 'utf-8,external'; |
155 | } else { |
156 | $newFlags = 'external'; |
157 | } |
158 | $newText = $mainText . '/' . $stub->getHash(); |
159 | |
160 | # Update the row |
161 | if ( $dryRun ) { |
162 | $this->output( "Resolve $id => $newFlags $newText\n" ); |
163 | } else { |
164 | $updated = $this->undoLog->update( |
165 | 'text', |
166 | [ |
167 | 'old_flags' => $newFlags, |
168 | 'old_text' => $newText |
169 | ], |
170 | (array)$row, |
171 | __METHOD__ |
172 | ); |
173 | if ( !$updated ) { |
174 | $this->output( "Updated of old_id $id failed to match\n" ); |
175 | return false; |
176 | } |
177 | } |
178 | return true; |
179 | } |
180 | } |
181 | |
182 | // @codeCoverageIgnoreStart |
183 | $maintClass = ResolveStubs::class; |
184 | require_once RUN_MAINTENANCE_IF_MAIN; |
185 | // @codeCoverageIgnoreEnd |