Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
6.38% |
30 / 470 |
|
3.39% |
2 / 59 |
CRAP | |
0.00% |
0 / 1 |
DatabaseUpdater | |
6.41% |
30 / 468 |
|
3.39% |
2 / 59 |
23029.18 | |
0.00% |
0 / 1 |
__construct | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
2.06 | |||
loadExtensionSchemaUpdates | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
loadExtensions | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
156 | |||
newForDB | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
setAutoExtensionHookContainer | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDB | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
output | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
addExtensionUpdate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addExtensionUpdateOnVirtualDomain | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addExtensionTable | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addExtensionIndex | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addExtensionField | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
dropExtensionField | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
dropExtensionIndex | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
dropExtensionTable | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
renameExtensionIndex | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
modifyExtensionField | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
modifyExtensionTable | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
tableExists | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
fieldExists | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addPostDatabaseUpdateMaintenance | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getExtensionUpdates | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPostDatabaseUpdateMaintenance | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
writeSchemaUpdateFile | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getSchemaVars | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doUpdates | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
56 | |||
runUpdates | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
110 | |||
getLBFactory | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
updateRowExists | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
insertUpdateRow | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
3 | |||
insertInitialUpdateKeys | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
doTable | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
42 | |||
getCoreUpdateList | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getInitialUpdateKeys | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
copyFile | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
appendLine | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
applyPatch | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
patchPath | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
addTable | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
addField | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
addIndex | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
dropField | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
dropIndex | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
renameIndex | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
56 | |||
dropTable | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
modifyField | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
modifyTable | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
modifyTableIfFieldNotExists | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
90 | |||
modifyFieldIfNullable | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
modifyFieldWithCondition | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
56 | |||
runMaintenance | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
setFileAccess | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
purgeCache | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
checkStats | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
doCollationUpdate | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
doConvertDjvuMetadata | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
12 | |||
rebuildLocalisationCache | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
migrateTemplatelinks | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
migratePagelinks | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
ifTableNotExists | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 | |||
ifFieldExists | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
56 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\Installer; |
22 | |
23 | use AutoLoader; |
24 | use CleanupEmptyCategories; |
25 | use DeleteDefaultMessages; |
26 | use LogicException; |
27 | use MediaWiki\HookContainer\HookContainer; |
28 | use MediaWiki\HookContainer\HookRunner; |
29 | use MediaWiki\HookContainer\StaticHookRegistry; |
30 | use MediaWiki\Maintenance\FakeMaintenance; |
31 | use MediaWiki\Maintenance\Maintenance; |
32 | use MediaWiki\MediaWikiServices; |
33 | use MediaWiki\Registration\ExtensionRegistry; |
34 | use MediaWiki\ResourceLoader\MessageBlobStore; |
35 | use MediaWiki\SiteStats\SiteStatsInit; |
36 | use MigrateLinksTable; |
37 | use RebuildLocalisationCache; |
38 | use RefreshImageMetadata; |
39 | use RuntimeException; |
40 | use UnexpectedValueException; |
41 | use UpdateCollation; |
42 | use Wikimedia\Rdbms\IDatabase; |
43 | use Wikimedia\Rdbms\IMaintainableDatabase; |
44 | use Wikimedia\Rdbms\LBFactory; |
45 | use Wikimedia\Rdbms\Platform\ISQLPlatform; |
46 | |
47 | require_once __DIR__ . '/../../maintenance/Maintenance.php'; |
48 | |
49 | /** |
50 | * Apply database changes after updating MediaWiki. |
51 | * |
52 | * @ingroup Installer |
53 | * @since 1.17 |
54 | */ |
55 | abstract class DatabaseUpdater { |
56 | public const REPLICATION_WAIT_TIMEOUT = 300; |
57 | |
58 | /** |
59 | * Array of updates to perform on the database |
60 | * |
61 | * @var array |
62 | */ |
63 | protected $updates = []; |
64 | |
65 | /** |
66 | * Array of updates that were skipped |
67 | * |
68 | * @var array |
69 | */ |
70 | protected $updatesSkipped = []; |
71 | |
72 | /** |
73 | * List of extension-provided database updates |
74 | * @var array |
75 | */ |
76 | protected $extensionUpdates = []; |
77 | |
78 | /** |
79 | * List of extension-provided database updates on virtual domain dbs |
80 | * @var array |
81 | */ |
82 | protected $extensionUpdatesWithVirtualDomains = []; |
83 | |
84 | /** |
85 | * Handle to the database subclass |
86 | * |
87 | * @var IMaintainableDatabase |
88 | */ |
89 | protected $db; |
90 | |
91 | /** |
92 | * @var Maintenance |
93 | */ |
94 | protected $maintenance; |
95 | |
96 | /** @var bool */ |
97 | protected $shared = false; |
98 | |
99 | /** @var HookContainer|null */ |
100 | protected $autoExtensionHookContainer; |
101 | |
102 | /** |
103 | * @var string[] Scripts to run after database update |
104 | * Should be a subclass of LoggedUpdateMaintenance |
105 | */ |
106 | protected $postDatabaseUpdateMaintenance = [ |
107 | DeleteDefaultMessages::class, |
108 | CleanupEmptyCategories::class, |
109 | ]; |
110 | |
111 | /** |
112 | * File handle for SQL output. |
113 | * |
114 | * @var resource|null |
115 | */ |
116 | protected $fileHandle = null; |
117 | |
118 | /** |
119 | * Flag specifying whether to skip schema (e.g., SQL-only) updates. |
120 | * |
121 | * @var bool |
122 | */ |
123 | protected $skipSchema = false; |
124 | |
125 | /** |
126 | * The virtual domain currently being acted on |
127 | * @var string|null |
128 | */ |
129 | private $currentVirtualDomain = null; |
130 | |
131 | /** |
132 | * @param IMaintainableDatabase &$db To perform updates on |
133 | * @param bool $shared Whether to perform updates on shared tables |
134 | * @param Maintenance|null $maintenance Maintenance object which created us |
135 | */ |
136 | protected function __construct( |
137 | IMaintainableDatabase &$db, |
138 | $shared, |
139 | ?Maintenance $maintenance = null |
140 | ) { |
141 | $this->db = $db; |
142 | $this->db->setFlag( DBO_DDLMODE ); |
143 | $this->shared = $shared; |
144 | if ( $maintenance ) { |
145 | $this->maintenance = $maintenance; |
146 | $this->fileHandle = $maintenance->fileHandle; |
147 | } else { |
148 | $this->maintenance = new FakeMaintenance; |
149 | } |
150 | $this->maintenance->setDB( $db ); |
151 | } |
152 | |
153 | /** |
154 | * Cause extensions to register any updates they need to perform. |
155 | */ |
156 | private function loadExtensionSchemaUpdates() { |
157 | $hookContainer = $this->loadExtensions(); |
158 | ( new HookRunner( $hookContainer ) )->onLoadExtensionSchemaUpdates( $this ); |
159 | } |
160 | |
161 | /** |
162 | * Loads LocalSettings.php, if needed, and initialises everything needed for |
163 | * LoadExtensionSchemaUpdates hook. |
164 | * |
165 | * @return HookContainer |
166 | */ |
167 | private function loadExtensions() { |
168 | if ( $this->autoExtensionHookContainer ) { |
169 | // Already injected by installer |
170 | return $this->autoExtensionHookContainer; |
171 | } |
172 | if ( defined( 'MW_EXTENSIONS_LOADED' ) ) { |
173 | throw new LogicException( __METHOD__ . |
174 | ' apparently called from installer but no hook container was injected' ); |
175 | } |
176 | if ( !defined( 'MEDIAWIKI_INSTALL' ) ) { |
177 | // Running under update.php: use the global locator |
178 | return MediaWikiServices::getInstance()->getHookContainer(); |
179 | } |
180 | $vars = Installer::getExistingLocalSettings(); |
181 | |
182 | $registry = ExtensionRegistry::getInstance(); |
183 | $queue = $registry->getQueue(); |
184 | // Don't accidentally load extensions in the future |
185 | $registry->clearQueue(); |
186 | |
187 | // Read extension.json files |
188 | $extInfo = $registry->readFromQueue( $queue ); |
189 | |
190 | // Merge extension attribute hooks with hooks defined by a .php |
191 | // registration file included from LocalSettings.php |
192 | $legacySchemaHooks = $extInfo['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ?? []; |
193 | if ( $vars && isset( $vars['wgHooks']['LoadExtensionSchemaUpdates'] ) ) { |
194 | $legacySchemaHooks = array_merge( $legacySchemaHooks, $vars['wgHooks']['LoadExtensionSchemaUpdates'] ); |
195 | } |
196 | |
197 | // Register classes defined by extensions that are loaded by including of a file that |
198 | // updates global variables, rather than having an extension.json manifest. |
199 | if ( $vars && isset( $vars['wgAutoloadClasses'] ) ) { |
200 | AutoLoader::registerClasses( $vars['wgAutoloadClasses'] ); |
201 | } |
202 | |
203 | // Register class definitions from extension.json files |
204 | if ( !isset( $extInfo['autoloaderPaths'] ) |
205 | || !isset( $extInfo['autoloaderClasses'] ) |
206 | || !isset( $extInfo['autoloaderNS'] ) |
207 | ) { |
208 | // NOTE: protect against changes to the structure of $extInfo. |
209 | // It's volatile, and this usage is easy to miss. |
210 | throw new LogicException( 'Missing autoloader keys from extracted extension info' ); |
211 | } |
212 | AutoLoader::loadFiles( $extInfo['autoloaderPaths'] ); |
213 | AutoLoader::registerClasses( $extInfo['autoloaderClasses'] ); |
214 | AutoLoader::registerNamespaces( $extInfo['autoloaderNS'] ); |
215 | |
216 | $legacyHooks = $legacySchemaHooks ? [ 'LoadExtensionSchemaUpdates' => $legacySchemaHooks ] : []; |
217 | return new HookContainer( |
218 | new StaticHookRegistry( |
219 | $legacyHooks, |
220 | $extInfo['attributes']['Hooks'] ?? [], |
221 | $extInfo['attributes']['DeprecatedHooks'] ?? [] |
222 | ), |
223 | MediaWikiServices::getInstance()->getObjectFactory() |
224 | ); |
225 | } |
226 | |
227 | /** |
228 | * @param IMaintainableDatabase $db |
229 | * @param bool $shared |
230 | * @param Maintenance|null $maintenance |
231 | * @return DatabaseUpdater |
232 | */ |
233 | public static function newForDB( |
234 | IMaintainableDatabase $db, |
235 | $shared = false, |
236 | ?Maintenance $maintenance = null |
237 | ) { |
238 | $type = $db->getType(); |
239 | if ( in_array( $type, Installer::getDBTypes() ) ) { |
240 | $class = '\\MediaWiki\\Installer\\' . ucfirst( $type ) . 'Updater'; |
241 | |
242 | return new $class( $db, $shared, $maintenance ); |
243 | } |
244 | |
245 | throw new UnexpectedValueException( __METHOD__ . ' called for unsupported DB type' ); |
246 | } |
247 | |
248 | /** |
249 | * Set the HookContainer to use for loading extension schema updates. |
250 | * |
251 | * @internal For use by DatabaseInstaller |
252 | * @since 1.36 |
253 | * @param HookContainer $hookContainer |
254 | */ |
255 | public function setAutoExtensionHookContainer( HookContainer $hookContainer ) { |
256 | $this->autoExtensionHookContainer = $hookContainer; |
257 | } |
258 | |
259 | /** |
260 | * Get a database connection to run updates |
261 | * |
262 | * @return IMaintainableDatabase |
263 | */ |
264 | public function getDB() { |
265 | return $this->db; |
266 | } |
267 | |
268 | /** |
269 | * Output some text. If we're running via the web, escape the text first. |
270 | * |
271 | * @param string $str Text to output |
272 | * @param-taint $str escapes_html |
273 | */ |
274 | public function output( $str ) { |
275 | if ( $this->maintenance->isQuiet() ) { |
276 | return; |
277 | } |
278 | if ( MW_ENTRY_POINT !== 'cli' ) { |
279 | $str = htmlspecialchars( $str ); |
280 | } |
281 | echo $str; |
282 | flush(); |
283 | } |
284 | |
285 | /** |
286 | * Add a new update coming from an extension. |
287 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
288 | * |
289 | * @since 1.17 |
290 | * |
291 | * @param array $update The update to run. Format is [ $callback, $params... ] |
292 | * $callback is the method to call; either a DatabaseUpdater method name or a callable. |
293 | * Must be serializable (i.e., no anonymous functions allowed). The rest of the parameters |
294 | * (if any) will be passed to the callback. The first parameter passed to the callback |
295 | * is always this object. |
296 | */ |
297 | public function addExtensionUpdate( array $update ) { |
298 | $this->extensionUpdates[] = $update; |
299 | } |
300 | |
301 | /** |
302 | * Add a new update coming from an extension on virtual domain databases. |
303 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
304 | * |
305 | * @since 1.42 |
306 | * |
307 | * @param array $update The update to run. The format is [ $virtualDomain, $callback, $params... ] |
308 | * similarly to addExtensionUpdate() |
309 | */ |
310 | public function addExtensionUpdateOnVirtualDomain( array $update ) { |
311 | $this->extensionUpdatesWithVirtualDomains[] = $update; |
312 | } |
313 | |
314 | /** |
315 | * Convenience wrapper for addExtensionUpdate() when adding a new table (which |
316 | * is the most common usage of updaters in an extension) |
317 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
318 | * |
319 | * @since 1.18 |
320 | * |
321 | * @param string $tableName Name of table to create |
322 | * @param string $sqlPath Full path to the schema file |
323 | */ |
324 | public function addExtensionTable( $tableName, $sqlPath ) { |
325 | $this->extensionUpdates[] = [ 'addTable', $tableName, $sqlPath, true ]; |
326 | } |
327 | |
328 | /** |
329 | * Add an index to an existing extension table. |
330 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
331 | * |
332 | * @since 1.19 |
333 | * |
334 | * @param string $tableName |
335 | * @param string $indexName |
336 | * @param string $sqlPath |
337 | */ |
338 | public function addExtensionIndex( $tableName, $indexName, $sqlPath ) { |
339 | $this->extensionUpdates[] = [ 'addIndex', $tableName, $indexName, $sqlPath, true ]; |
340 | } |
341 | |
342 | /** |
343 | * Add a field to an existing extension table. |
344 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
345 | * |
346 | * @since 1.19 |
347 | * |
348 | * @param string $tableName |
349 | * @param string $columnName |
350 | * @param string $sqlPath |
351 | */ |
352 | public function addExtensionField( $tableName, $columnName, $sqlPath ) { |
353 | $this->extensionUpdates[] = [ 'addField', $tableName, $columnName, $sqlPath, true ]; |
354 | } |
355 | |
356 | /** |
357 | * Drop a field from an extension table. |
358 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
359 | * |
360 | * @since 1.20 |
361 | * |
362 | * @param string $tableName |
363 | * @param string $columnName |
364 | * @param string $sqlPath |
365 | */ |
366 | public function dropExtensionField( $tableName, $columnName, $sqlPath ) { |
367 | $this->extensionUpdates[] = [ 'dropField', $tableName, $columnName, $sqlPath, true ]; |
368 | } |
369 | |
370 | /** |
371 | * Drop an index from an extension table |
372 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
373 | * |
374 | * @since 1.21 |
375 | * |
376 | * @param string $tableName |
377 | * @param string $indexName |
378 | * @param string $sqlPath The path to the SQL change path |
379 | */ |
380 | public function dropExtensionIndex( $tableName, $indexName, $sqlPath ) { |
381 | $this->extensionUpdates[] = [ 'dropIndex', $tableName, $indexName, $sqlPath, true ]; |
382 | } |
383 | |
384 | /** |
385 | * Drop an extension table. |
386 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
387 | * |
388 | * @since 1.20 |
389 | * |
390 | * @param string $tableName |
391 | * @param string|bool $sqlPath |
392 | */ |
393 | public function dropExtensionTable( $tableName, $sqlPath = false ) { |
394 | $this->extensionUpdates[] = [ 'dropTable', $tableName, $sqlPath, true ]; |
395 | } |
396 | |
397 | /** |
398 | * Rename an index on an extension table |
399 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
400 | * |
401 | * @since 1.21 |
402 | * |
403 | * @param string $tableName |
404 | * @param string $oldIndexName |
405 | * @param string $newIndexName |
406 | * @param string $sqlPath The path to the SQL change file |
407 | * @param bool $skipBothIndexExistWarning Whether to warn if both the old |
408 | * and the new indexes exist. [facultative; by default, false] |
409 | */ |
410 | public function renameExtensionIndex( $tableName, $oldIndexName, $newIndexName, |
411 | $sqlPath, $skipBothIndexExistWarning = false |
412 | ) { |
413 | $this->extensionUpdates[] = [ |
414 | 'renameIndex', |
415 | $tableName, |
416 | $oldIndexName, |
417 | $newIndexName, |
418 | $skipBothIndexExistWarning, |
419 | $sqlPath, |
420 | true |
421 | ]; |
422 | } |
423 | |
424 | /** |
425 | * Modify an existing field in an extension table. |
426 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
427 | * |
428 | * @since 1.21 |
429 | * |
430 | * @param string $tableName |
431 | * @param string $fieldName The field to be modified |
432 | * @param string $sqlPath The path to the SQL patch |
433 | */ |
434 | public function modifyExtensionField( $tableName, $fieldName, $sqlPath ) { |
435 | $this->extensionUpdates[] = [ 'modifyField', $tableName, $fieldName, $sqlPath, true ]; |
436 | } |
437 | |
438 | /** |
439 | * Modify an existing extension table. |
440 | * Intended for use in LoadExtensionSchemaUpdates hook handlers. |
441 | * |
442 | * @since 1.31 |
443 | * |
444 | * @param string $tableName |
445 | * @param string $sqlPath The path to the SQL patch |
446 | */ |
447 | public function modifyExtensionTable( $tableName, $sqlPath ) { |
448 | $this->extensionUpdates[] = [ 'modifyTable', $tableName, $sqlPath, true ]; |
449 | } |
450 | |
451 | /** |
452 | * @since 1.20 |
453 | * |
454 | * @param string $tableName |
455 | * @return bool |
456 | */ |
457 | public function tableExists( $tableName ) { |
458 | return ( $this->db->tableExists( $tableName, __METHOD__ ) ); |
459 | } |
460 | |
461 | /** |
462 | * @since 1.40 |
463 | * |
464 | * @param string $tableName |
465 | * @param string $fieldName |
466 | * @return bool |
467 | */ |
468 | public function fieldExists( $tableName, $fieldName ) { |
469 | return ( $this->db->fieldExists( $tableName, $fieldName, __METHOD__ ) ); |
470 | } |
471 | |
472 | /** |
473 | * Add a maintenance script to be run after the database updates are complete. |
474 | * |
475 | * Script should subclass LoggedUpdateMaintenance |
476 | * |
477 | * @since 1.19 |
478 | * |
479 | * @param string $class Name of a Maintenance subclass |
480 | */ |
481 | public function addPostDatabaseUpdateMaintenance( $class ) { |
482 | $this->postDatabaseUpdateMaintenance[] = $class; |
483 | } |
484 | |
485 | /** |
486 | * Get the list of extension-defined updates |
487 | * |
488 | * @return array |
489 | */ |
490 | protected function getExtensionUpdates() { |
491 | return $this->extensionUpdates; |
492 | } |
493 | |
494 | /** |
495 | * @since 1.17 |
496 | * |
497 | * @return string[] |
498 | */ |
499 | public function getPostDatabaseUpdateMaintenance() { |
500 | return $this->postDatabaseUpdateMaintenance; |
501 | } |
502 | |
503 | /** |
504 | * @since 1.21 |
505 | * |
506 | * Writes the schema updates desired to a file for the DB Admin to run. |
507 | */ |
508 | private function writeSchemaUpdateFile() { |
509 | $updates = $this->updatesSkipped; |
510 | $this->updatesSkipped = []; |
511 | |
512 | foreach ( $updates as [ $func, $args, $origParams ] ) { |
513 | // @phan-suppress-next-line PhanUndeclaredInvokeInCallable |
514 | $func( ...$args ); |
515 | flush(); |
516 | $this->updatesSkipped[] = $origParams; |
517 | } |
518 | } |
519 | |
520 | /** |
521 | * Get appropriate schema variables in the current database connection. |
522 | * |
523 | * This should be called after any request data has been imported, but before |
524 | * any write operations to the database. The result should be passed to the DB |
525 | * setSchemaVars() method. |
526 | * |
527 | * @return array |
528 | * @since 1.28 |
529 | */ |
530 | public function getSchemaVars() { |
531 | return []; // DB-type specific |
532 | } |
533 | |
534 | /** |
535 | * Do all the updates |
536 | * |
537 | * @param array $what What updates to perform |
538 | */ |
539 | public function doUpdates( array $what = [ 'core', 'extensions', 'stats' ] ) { |
540 | $this->db->setSchemaVars( $this->getSchemaVars() ); |
541 | |
542 | $what = array_fill_keys( $what, true ); |
543 | $this->skipSchema = isset( $what['noschema'] ) || $this->fileHandle !== null; |
544 | |
545 | if ( isset( $what['initial'] ) ) { |
546 | $this->output( 'Inserting initial update keys...' ); |
547 | $this->insertInitialUpdateKeys(); |
548 | $this->output( "done.\n" ); |
549 | } |
550 | if ( isset( $what['core'] ) ) { |
551 | $this->doCollationUpdate(); |
552 | $this->runUpdates( $this->getCoreUpdateList(), false ); |
553 | } |
554 | if ( isset( $what['extensions'] ) ) { |
555 | $this->loadExtensionSchemaUpdates(); |
556 | $this->runUpdates( $this->getExtensionUpdates(), true ); |
557 | $this->runUpdates( $this->extensionUpdatesWithVirtualDomains, true, true ); |
558 | } |
559 | |
560 | if ( isset( $what['stats'] ) ) { |
561 | $this->checkStats(); |
562 | } |
563 | |
564 | if ( $this->fileHandle ) { |
565 | $this->skipSchema = false; |
566 | $this->writeSchemaUpdateFile(); |
567 | } |
568 | } |
569 | |
570 | /** |
571 | * Helper function for doUpdates() |
572 | * |
573 | * @param array $updates Array of updates to run |
574 | * @param bool $passSelf Whether to pass this object when calling external functions |
575 | * @param bool $hasVirtualDomain Whether the updates' array include virtual domains |
576 | */ |
577 | private function runUpdates( array $updates, $passSelf, $hasVirtualDomain = false ) { |
578 | $lbFactory = $this->getLBFactory(); |
579 | $updatesDone = []; |
580 | $updatesSkipped = []; |
581 | foreach ( $updates as $params ) { |
582 | $origParams = $params; |
583 | $oldDb = null; |
584 | $this->currentVirtualDomain = null; |
585 | if ( $hasVirtualDomain === true ) { |
586 | $this->currentVirtualDomain = array_shift( $params ); |
587 | $oldDb = $this->db; |
588 | $virtualDb = $lbFactory->getPrimaryDatabase( $this->currentVirtualDomain ); |
589 | '@phan-var IMaintainableDatabase $virtualDb'; |
590 | $this->maintenance->setDB( $virtualDb ); |
591 | $this->db = $virtualDb; |
592 | } |
593 | $func = array_shift( $params ); |
594 | if ( !is_array( $func ) && method_exists( $this, $func ) ) { |
595 | $func = [ $this, $func ]; |
596 | } elseif ( $passSelf ) { |
597 | array_unshift( $params, $this ); |
598 | } |
599 | $ret = $func( ...$params ); |
600 | if ( $hasVirtualDomain === true && $oldDb ) { |
601 | $this->db = $oldDb; |
602 | $this->maintenance->setDB( $oldDb ); |
603 | $this->currentVirtualDomain = null; |
604 | } |
605 | |
606 | flush(); |
607 | if ( $ret !== false ) { |
608 | $updatesDone[] = $origParams; |
609 | $lbFactory->waitForReplication( [ 'timeout' => self::REPLICATION_WAIT_TIMEOUT ] ); |
610 | } else { |
611 | if ( $hasVirtualDomain === true ) { |
612 | $params = $origParams; |
613 | $func = array_shift( $params ); |
614 | } |
615 | $updatesSkipped[] = [ $func, $params, $origParams ]; |
616 | } |
617 | } |
618 | $this->updatesSkipped = array_merge( $this->updatesSkipped, $updatesSkipped ); |
619 | $this->updates = array_merge( $this->updates, $updatesDone ); |
620 | } |
621 | |
622 | private function getLBFactory(): LBFactory { |
623 | return MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); |
624 | } |
625 | |
626 | /** |
627 | * Helper function: check if the given key is present in the updatelog table. |
628 | * |
629 | * @param string $key Name of the key to check for |
630 | * @return bool |
631 | */ |
632 | public function updateRowExists( $key ) { |
633 | // Return false if the updatelog table does not exist. This can occur if performing schema changes for tables |
634 | // that are on a virtual database domain. |
635 | if ( !$this->db->tableExists( 'updatelog', __METHOD__ ) ) { |
636 | return false; |
637 | } |
638 | |
639 | $row = $this->db->newSelectQueryBuilder() |
640 | ->select( '1 AS X' ) // T67813 |
641 | ->from( 'updatelog' ) |
642 | ->where( [ 'ul_key' => $key ] ) |
643 | ->caller( __METHOD__ )->fetchRow(); |
644 | |
645 | return (bool)$row; |
646 | } |
647 | |
648 | /** |
649 | * Helper function: Add a key to the updatelog table |
650 | * |
651 | * @note Extensions must only use this from within callbacks registered with |
652 | * addExtensionUpdate(). In particular, this method must not be called directly |
653 | * from a LoadExtensionSchemaUpdates handler. |
654 | * |
655 | * @param string $key Name of the key to insert |
656 | * @param string|null $val [optional] Value to insert along with the key |
657 | */ |
658 | public function insertUpdateRow( $key, $val = null ) { |
659 | // We cannot insert anything to the updatelog table if it does not exist. This can occur for schema changes |
660 | // on tables that are on a virtual database domain. |
661 | if ( !$this->db->tableExists( 'updatelog', __METHOD__ ) ) { |
662 | return; |
663 | } |
664 | |
665 | $this->db->clearFlag( DBO_DDLMODE ); |
666 | $values = [ 'ul_key' => $key ]; |
667 | if ( $val ) { |
668 | $values['ul_value'] = $val; |
669 | } |
670 | $this->db->newInsertQueryBuilder() |
671 | ->insertInto( 'updatelog' ) |
672 | ->ignore() |
673 | ->row( $values ) |
674 | ->caller( __METHOD__ )->execute(); |
675 | $this->db->setFlag( DBO_DDLMODE ); |
676 | } |
677 | |
678 | /** |
679 | * Add initial keys to the updatelog table. Should be called during installation. |
680 | */ |
681 | public function insertInitialUpdateKeys() { |
682 | $this->db->clearFlag( DBO_DDLMODE ); |
683 | $iqb = $this->db->newInsertQueryBuilder() |
684 | ->insertInto( 'updatelog' ) |
685 | ->ignore() |
686 | ->caller( __METHOD__ ); |
687 | foreach ( $this->getInitialUpdateKeys() as $key ) { |
688 | $iqb->row( [ 'ul_key' => $key ] ); |
689 | } |
690 | $iqb->execute(); |
691 | $this->db->setFlag( DBO_DDLMODE ); |
692 | } |
693 | |
694 | /** |
695 | * Returns whether updates should be executed on the database table $name. |
696 | * Updates will be prevented if the table is a shared table, and it is not |
697 | * specified to run updates on shared tables. |
698 | * |
699 | * @param string $name Table name |
700 | * @return bool |
701 | */ |
702 | protected function doTable( $name ) { |
703 | global $wgSharedDB, $wgSharedTables; |
704 | |
705 | if ( $this->shared ) { |
706 | // Shared updates are enabled |
707 | return true; |
708 | } |
709 | if ( $this->currentVirtualDomain |
710 | && $this->getLBFactory()->isSharedVirtualDomain( $this->currentVirtualDomain ) |
711 | ) { |
712 | $this->output( "...skipping update to table $name in shared virtual domain.\n" ); |
713 | return false; |
714 | } |
715 | if ( $wgSharedDB !== null && in_array( $name, $wgSharedTables ) ) { |
716 | $this->output( "...skipping update to shared table $name.\n" ); |
717 | return false; |
718 | } |
719 | |
720 | return true; |
721 | } |
722 | |
723 | /** |
724 | * Get an array of updates to perform on the database. Should return a |
725 | * multidimensional array. The main key is the MediaWiki version (1.12, |
726 | * 1.13...) with the values being arrays of updates. |
727 | * |
728 | * @return array[] |
729 | */ |
730 | abstract protected function getCoreUpdateList(); |
731 | |
732 | /** |
733 | * Get an array of update keys to insert into the updatelog table after a |
734 | * new installation. The named operations will then be skipped by a |
735 | * subsequent update. |
736 | * |
737 | * Add keys here to skip updates that are redundant or harmful on a new |
738 | * installation, for example reducing field sizes, adding constraints, etc. |
739 | * |
740 | * @return string[] |
741 | */ |
742 | abstract protected function getInitialUpdateKeys(); |
743 | |
744 | /** |
745 | * Append an SQL fragment to the open file handle. |
746 | * |
747 | * @note protected since 1.35 |
748 | * |
749 | * @param string $filename File name to open |
750 | */ |
751 | protected function copyFile( $filename ) { |
752 | $this->db->sourceFile( |
753 | $filename, |
754 | null, |
755 | null, |
756 | __METHOD__, |
757 | function ( $line ) { |
758 | return $this->appendLine( $line ); |
759 | } |
760 | ); |
761 | } |
762 | |
763 | /** |
764 | * Append a line to the open file handle. The line is assumed to |
765 | * be a complete SQL statement. |
766 | * |
767 | * This is used as a callback for sourceLine(). |
768 | * |
769 | * @note protected since 1.35 |
770 | * |
771 | * @param string $line Text to append to the file |
772 | * @return bool False to skip actually executing the file |
773 | */ |
774 | protected function appendLine( $line ) { |
775 | $line = rtrim( $line ) . ";\n"; |
776 | if ( fwrite( $this->fileHandle, $line ) === false ) { |
777 | throw new RuntimeException( "trouble writing file" ); |
778 | } |
779 | |
780 | return false; |
781 | } |
782 | |
783 | /** |
784 | * Applies a SQL patch |
785 | * |
786 | * @note Do not use this in a LoadExtensionSchemaUpdates handler, |
787 | * use addExtensionUpdate instead! |
788 | * |
789 | * @param string $path Path to the patch file |
790 | * @param bool $isFullPath Whether to treat $path as a relative or not |
791 | * @param string|null $msg Description of the patch |
792 | * @return bool False if the patch was skipped. |
793 | */ |
794 | protected function applyPatch( $path, $isFullPath = false, $msg = null ) { |
795 | $msg ??= "Applying $path patch"; |
796 | if ( $this->skipSchema ) { |
797 | $this->output( "...skipping schema change ($msg).\n" ); |
798 | |
799 | return false; |
800 | } |
801 | |
802 | $this->output( "{$msg}..." ); |
803 | |
804 | if ( !$isFullPath ) { |
805 | $path = $this->patchPath( $this->db, $path ); |
806 | } |
807 | if ( $this->fileHandle !== null ) { |
808 | $this->copyFile( $path ); |
809 | } else { |
810 | $this->db->sourceFile( $path ); |
811 | } |
812 | $this->output( "done.\n" ); |
813 | |
814 | return true; |
815 | } |
816 | |
817 | /** |
818 | * Get the full path to a patch file. |
819 | * |
820 | * @param IDatabase $db |
821 | * @param string $patch The basename of the patch, like patch-something.sql |
822 | * @return string Full path to patch file. It fails back to MySQL |
823 | * if no DB-specific patch exists. |
824 | */ |
825 | public function patchPath( IDatabase $db, $patch ) { |
826 | $baseDir = MW_INSTALL_PATH; |
827 | |
828 | $dbType = $db->getType(); |
829 | if ( file_exists( "$baseDir/sql/$dbType/$patch" ) ) { |
830 | return "$baseDir/sql/$dbType/$patch"; |
831 | } |
832 | |
833 | // TODO: Is the fallback still needed after the changes from T382030? |
834 | return "$baseDir/sql/mysql/$patch"; |
835 | } |
836 | |
837 | /** |
838 | * Add a new table to the database |
839 | * |
840 | * @note Code in a LoadExtensionSchemaUpdates handler should |
841 | * use addExtensionTable instead! |
842 | * |
843 | * @param string $name Name of the new table |
844 | * @param string $patch Path to the patch file |
845 | * @param bool $fullpath Whether to treat $patch path as a relative or not |
846 | * @return bool False if this was skipped because schema changes are skipped |
847 | */ |
848 | protected function addTable( $name, $patch, $fullpath = false ) { |
849 | if ( !$this->doTable( $name ) ) { |
850 | return true; |
851 | } |
852 | |
853 | if ( $this->db->tableExists( $name, __METHOD__ ) ) { |
854 | $this->output( "...$name table already exists.\n" ); |
855 | return true; |
856 | } |
857 | |
858 | return $this->applyPatch( $patch, $fullpath, "Creating $name table" ); |
859 | } |
860 | |
861 | /** |
862 | * Add a new field to an existing table |
863 | * |
864 | * @note Code in a LoadExtensionSchemaUpdates handler should |
865 | * use addExtensionField instead! |
866 | * |
867 | * @param string $table Name of the table to modify |
868 | * @param string $field Name of the new field |
869 | * @param string $patch Path to the patch file |
870 | * @param bool $fullpath Whether to treat $patch path as a relative or not |
871 | * @return bool False if this was skipped because schema changes are skipped |
872 | */ |
873 | protected function addField( $table, $field, $patch, $fullpath = false ) { |
874 | if ( !$this->doTable( $table ) ) { |
875 | return true; |
876 | } |
877 | |
878 | if ( !$this->db->tableExists( $table, __METHOD__ ) ) { |
879 | $this->output( "...$table table does not exist, skipping new field patch.\n" ); |
880 | } elseif ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) { |
881 | $this->output( "...have $field field in $table table.\n" ); |
882 | } else { |
883 | return $this->applyPatch( $patch, $fullpath, "Adding $field field to table $table" ); |
884 | } |
885 | |
886 | return true; |
887 | } |
888 | |
889 | /** |
890 | * Add a new index to an existing table |
891 | * |
892 | * @note Code in a LoadExtensionSchemaUpdates handler should |
893 | * use addExtensionIndex instead! |
894 | * |
895 | * @param string $table Name of the table to modify |
896 | * @param string $index Name of the new index |
897 | * @param string $patch Path to the patch file |
898 | * @param bool $fullpath Whether to treat $patch path as a relative or not |
899 | * @return bool False if this was skipped because schema changes are skipped |
900 | */ |
901 | protected function addIndex( $table, $index, $patch, $fullpath = false ) { |
902 | if ( !$this->doTable( $table ) ) { |
903 | return true; |
904 | } |
905 | |
906 | if ( !$this->db->tableExists( $table, __METHOD__ ) ) { |
907 | $this->output( "...skipping: '$table' table doesn't exist yet.\n" ); |
908 | } elseif ( $this->db->indexExists( $table, $index, __METHOD__ ) ) { |
909 | $this->output( "...index $index already set on $table table.\n" ); |
910 | } else { |
911 | return $this->applyPatch( $patch, $fullpath, "Adding index $index to table $table" ); |
912 | } |
913 | |
914 | return true; |
915 | } |
916 | |
917 | /** |
918 | * Drop a field from an existing table |
919 | * |
920 | * @note Code in a LoadExtensionSchemaUpdates handler should |
921 | * use dropExtensionField instead! |
922 | * |
923 | * @param string $table Name of the table to modify |
924 | * @param string $field Name of the old field |
925 | * @param string $patch Path to the patch file |
926 | * @param bool $fullpath Whether to treat $patch path as a relative or not |
927 | * @return bool False if this was skipped because schema changes are skipped |
928 | */ |
929 | protected function dropField( $table, $field, $patch, $fullpath = false ) { |
930 | if ( !$this->doTable( $table ) ) { |
931 | return true; |
932 | } |
933 | |
934 | if ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) { |
935 | return $this->applyPatch( $patch, $fullpath, "Table $table contains $field field. Dropping" ); |
936 | } |
937 | |
938 | $this->output( "...$table table does not contain $field field.\n" ); |
939 | return true; |
940 | } |
941 | |
942 | /** |
943 | * Drop an index from an existing table |
944 | * |
945 | * @note Code in a LoadExtensionSchemaUpdates handler should |
946 | * use dropExtensionIndex instead! |
947 | * |
948 | * @param string $table Name of the table to modify |
949 | * @param string $index Name of the index |
950 | * @param string $patch Path to the patch file |
951 | * @param bool $fullpath Whether to treat $patch path as a relative or not |
952 | * @return bool False if this was skipped because schema changes are skipped |
953 | */ |
954 | protected function dropIndex( $table, $index, $patch, $fullpath = false ) { |
955 | if ( !$this->doTable( $table ) ) { |
956 | return true; |
957 | } |
958 | |
959 | if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) { |
960 | return $this->applyPatch( $patch, $fullpath, "Dropping $index index from table $table" ); |
961 | } |
962 | |
963 | $this->output( "...$index key doesn't exist.\n" ); |
964 | return true; |
965 | } |
966 | |
967 | /** |
968 | * Rename an index from an existing table |
969 | * |
970 | * @note Code in a LoadExtensionSchemaUpdates handler should |
971 | * use renameExtensionIndex instead! |
972 | * |
973 | * @param string $table Name of the table to modify |
974 | * @param string $oldIndex Old name of the index |
975 | * @param string $newIndex New name of the index |
976 | * @param bool $skipBothIndexExistWarning Whether to warn if both the old and new indexes exist. |
977 | * @param string $patch Path to the patch file |
978 | * @param bool $fullpath Whether to treat $patch path as a relative or not |
979 | * @return bool False if this was skipped because schema changes are skipped |
980 | */ |
981 | protected function renameIndex( $table, $oldIndex, $newIndex, |
982 | $skipBothIndexExistWarning, $patch, $fullpath = false |
983 | ) { |
984 | if ( !$this->doTable( $table ) ) { |
985 | return true; |
986 | } |
987 | |
988 | // First requirement: the table must exist |
989 | if ( !$this->db->tableExists( $table, __METHOD__ ) ) { |
990 | $this->output( "...skipping: '$table' table doesn't exist yet.\n" ); |
991 | |
992 | return true; |
993 | } |
994 | |
995 | // Second requirement: the new index must be missing |
996 | if ( $this->db->indexExists( $table, $newIndex, __METHOD__ ) ) { |
997 | $this->output( "...index $newIndex already set on $table table.\n" ); |
998 | if ( !$skipBothIndexExistWarning && |
999 | $this->db->indexExists( $table, $oldIndex, __METHOD__ ) |
1000 | ) { |
1001 | $this->output( "...WARNING: $oldIndex still exists, despite it has " . |
1002 | "been renamed into $newIndex (which also exists).\n" . |
1003 | " $oldIndex should be manually removed if not needed anymore.\n" ); |
1004 | } |
1005 | |
1006 | return true; |
1007 | } |
1008 | |
1009 | // Third requirement: the old index must exist |
1010 | if ( !$this->db->indexExists( $table, $oldIndex, __METHOD__ ) ) { |
1011 | $this->output( "...skipping: index $oldIndex doesn't exist.\n" ); |
1012 | |
1013 | return true; |
1014 | } |
1015 | |
1016 | // Requirements have been satisfied, the patch can be applied |
1017 | return $this->applyPatch( |
1018 | $patch, |
1019 | $fullpath, |
1020 | "Renaming index $oldIndex into $newIndex to table $table" |
1021 | ); |
1022 | } |
1023 | |
1024 | /** |
1025 | * If the specified table exists, drop it, or execute the |
1026 | * patch if one is provided. |
1027 | * |
1028 | * @note Code in a LoadExtensionSchemaUpdates handler should |
1029 | * use dropExtensionTable instead! |
1030 | * |
1031 | * @note protected since 1.35 |
1032 | * |
1033 | * @param string $table Table to drop. |
1034 | * @param string|false $patch String of patch file that will drop the table. Default: false. |
1035 | * @param bool $fullpath Whether $patch is a full path. Default: false. |
1036 | * @return bool False if this was skipped because schema changes are skipped |
1037 | */ |
1038 | protected function dropTable( $table, $patch = false, $fullpath = false ) { |
1039 | if ( !$this->doTable( $table ) ) { |
1040 | return true; |
1041 | } |
1042 | |
1043 | if ( $this->db->tableExists( $table, __METHOD__ ) ) { |
1044 | $msg = "Dropping table $table"; |
1045 | |
1046 | if ( $patch === false ) { |
1047 | $this->output( "$msg ..." ); |
1048 | $this->db->dropTable( $table, __METHOD__ ); |
1049 | $this->output( "done.\n" ); |
1050 | } else { |
1051 | return $this->applyPatch( $patch, $fullpath, $msg ); |
1052 | } |
1053 | } else { |
1054 | $this->output( "...$table doesn't exist.\n" ); |
1055 | } |
1056 | |
1057 | return true; |
1058 | } |
1059 | |
1060 | /** |
1061 | * Modify an existing field |
1062 | * |
1063 | * @note Code in a LoadExtensionSchemaUpdates handler should |
1064 | * use modifyExtensionField instead! |
1065 | * |
1066 | * @note protected since 1.35 |
1067 | * |
1068 | * @param string $table Name of the table to which the field belongs |
1069 | * @param string $field Name of the field to modify |
1070 | * @param string $patch Path to the patch file |
1071 | * @param bool $fullpath Whether to treat $patch path as a relative or not |
1072 | * @return bool False if this was skipped because schema changes are skipped |
1073 | */ |
1074 | protected function modifyField( $table, $field, $patch, $fullpath = false ) { |
1075 | return $this->modifyFieldWithCondition( |
1076 | $table, $field, |
1077 | static function () { |
1078 | return true; |
1079 | }, |
1080 | $patch, $fullpath |
1081 | ); |
1082 | } |
1083 | |
1084 | /** |
1085 | * Modify an existing table, similar to modifyField. Intended for changes that |
1086 | * touch more than one column on a table. |
1087 | * |
1088 | * @note Code in a LoadExtensionSchemaUpdates handler should |
1089 | * use modifyExtensionTable instead! |
1090 | * |
1091 | * @note protected since 1.35 |
1092 | * |
1093 | * @param string $table Name of the table to modify |
1094 | * @param string $patch Name of the patch file to apply |
1095 | * @param string|bool $fullpath Whether to treat $patch path as relative or not, defaults to false |
1096 | * @return bool False if this was skipped because of schema changes being skipped |
1097 | */ |
1098 | protected function modifyTable( $table, $patch, $fullpath = false ) { |
1099 | if ( !$this->doTable( $table ) ) { |
1100 | return true; |
1101 | } |
1102 | |
1103 | $updateKey = "$table-$patch"; |
1104 | if ( !$this->db->tableExists( $table, __METHOD__ ) ) { |
1105 | $this->output( "...$table table does not exist, skipping modify table patch.\n" ); |
1106 | } elseif ( $this->updateRowExists( $updateKey ) ) { |
1107 | $this->output( "...table $table already modified by patch $patch.\n" ); |
1108 | } else { |
1109 | $apply = $this->applyPatch( $patch, $fullpath, "Modifying table $table with patch $patch" ); |
1110 | if ( $apply ) { |
1111 | $this->insertUpdateRow( $updateKey ); |
1112 | } |
1113 | return $apply; |
1114 | } |
1115 | return true; |
1116 | } |
1117 | |
1118 | /** |
1119 | * Modify a table if a field doesn't exist. This helps extensions to avoid |
1120 | * running updates on SQLite that are destructive because they don't copy |
1121 | * new fields. |
1122 | * |
1123 | * @since 1.44 |
1124 | * @param string $table Name of the table to which the field belongs |
1125 | * @param string $field Name of the field to check |
1126 | * @param string $patch Path to the patch file |
1127 | * @param bool $fullpath Whether to treat $patch path as a relative or not |
1128 | * @param string|null $fieldBeingModified The field being modified. If this |
1129 | * is specified, the updatelog key will match that used by modifyField(), |
1130 | * so if the patch was previously applied via modifyField(), it won't be |
1131 | * applied again. Also, if the field doesn't exist, the patch will not be |
1132 | * applied. If this is null, the updatelog key will match that used by |
1133 | * modifyTable(). |
1134 | * @return bool False if this was skipped because schema changes are skipped |
1135 | */ |
1136 | protected function modifyTableIfFieldNotExists( $table, $field, $patch, $fullpath = false, |
1137 | $fieldBeingModified = null |
1138 | ) { |
1139 | if ( !$this->doTable( $table ) ) { |
1140 | return true; |
1141 | } |
1142 | |
1143 | if ( $fieldBeingModified === null ) { |
1144 | $updateKey = "$table-$patch"; |
1145 | } else { |
1146 | $updateKey = "$table-$fieldBeingModified-$patch"; |
1147 | } |
1148 | |
1149 | if ( !$this->db->tableExists( $table, __METHOD__ ) ) { |
1150 | $this->output( "...$table table does not exist, skipping patch $patch.\n" ); |
1151 | } elseif ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) { |
1152 | $this->output( "...$field field exists in $table table, skipping obsolete patch $patch.\n" ); |
1153 | } elseif ( $fieldBeingModified !== null |
1154 | && !$this->db->fieldExists( $table, $fieldBeingModified, __METHOD__ ) |
1155 | ) { |
1156 | $this->output( "...$fieldBeingModified field does not exist in $table table, " . |
1157 | "skipping patch $patch.\n" ); |
1158 | } elseif ( $this->updateRowExists( $updateKey ) ) { |
1159 | $this->output( "...table $table already modified by patch $patch.\n" ); |
1160 | } else { |
1161 | $apply = $this->applyPatch( $patch, $fullpath, "Modifying table $table with patch $patch" ); |
1162 | if ( $apply ) { |
1163 | $this->insertUpdateRow( $updateKey ); |
1164 | } |
1165 | return $apply; |
1166 | } |
1167 | return true; |
1168 | } |
1169 | |
1170 | /** |
1171 | * Modify a field if the field exists and is nullable |
1172 | * |
1173 | * @since 1.44 |
1174 | * @param string $table Name of the table to which the field belongs |
1175 | * @param string $field Name of the field to modify |
1176 | * @param string $patch Path to the patch file |
1177 | * @param bool $fullpath Whether to treat $patch path as a relative or not |
1178 | * @return bool False if this was skipped because schema changes are skipped |
1179 | */ |
1180 | protected function modifyFieldIfNullable( $table, $field, $patch, $fullpath = false ) { |
1181 | return $this->modifyFieldWithCondition( |
1182 | $table, $field, |
1183 | static function ( $fieldInfo ) { |
1184 | return $fieldInfo->isNullable(); |
1185 | }, |
1186 | $patch, |
1187 | $fullpath |
1188 | ); |
1189 | } |
1190 | |
1191 | /** |
1192 | * Modify a field if a field exists and a callback returns true. The callback |
1193 | * is called with the FieldInfo of the field in question. |
1194 | * |
1195 | * @internal |
1196 | * @param string $table Name of the table to modify |
1197 | * @param string $field Name of the field to modify |
1198 | * @param callable $condCallback A callback which will be called with the |
1199 | * \Wikimedia\Rdbms\Field object for the specified field. If the callback returns |
1200 | * true, the update will proceed. |
1201 | * @param string $patch Name of the patch file to apply |
1202 | * @param string|bool $fullpath Whether to treat $patch path as relative or not, defaults to false |
1203 | * @return bool False if this was skipped because of schema changes being skipped |
1204 | */ |
1205 | private function modifyFieldWithCondition( |
1206 | $table, $field, $condCallback, $patch, $fullpath = false |
1207 | ) { |
1208 | if ( !$this->doTable( $table ) ) { |
1209 | return true; |
1210 | } |
1211 | |
1212 | $updateKey = "$table-$field-$patch"; |
1213 | if ( !$this->db->tableExists( $table, __METHOD__ ) ) { |
1214 | $this->output( "...$table table does not exist, skipping modify field patch.\n" ); |
1215 | return true; |
1216 | } |
1217 | $fieldInfo = $this->db->fieldInfo( $table, $field ); |
1218 | if ( !$fieldInfo ) { |
1219 | $this->output( "...$field field does not exist in $table table, " . |
1220 | "skipping modify field patch.\n" ); |
1221 | return true; |
1222 | } |
1223 | if ( $this->updateRowExists( $updateKey ) ) { |
1224 | $this->output( "...$field in table $table already modified by patch $patch.\n" ); |
1225 | return true; |
1226 | } |
1227 | if ( !$condCallback( $fieldInfo ) ) { |
1228 | $this->output( "...$field in table $table already has the required properties.\n" ); |
1229 | return true; |
1230 | } |
1231 | |
1232 | $apply = $this->applyPatch( $patch, $fullpath, "Modifying $field field of table $table" ); |
1233 | if ( $apply ) { |
1234 | $this->insertUpdateRow( $updateKey ); |
1235 | } |
1236 | return $apply; |
1237 | } |
1238 | |
1239 | /** |
1240 | * Run a maintenance script |
1241 | * |
1242 | * This should only be used when the maintenance script must run before |
1243 | * later updates. If later updates don't depend on the script, add it to |
1244 | * DatabaseUpdater::$postDatabaseUpdateMaintenance instead. |
1245 | * |
1246 | * The script's execute() method must return true to indicate successful |
1247 | * completion, and must return false (or throw an exception) to indicate |
1248 | * unsuccessful completion. |
1249 | * |
1250 | * @note Code in a LoadExtensionSchemaUpdates handler should |
1251 | * use addExtensionUpdate instead! |
1252 | * |
1253 | * @note protected since 1.35 |
1254 | * |
1255 | * @since 1.32 |
1256 | * @param string $class Maintenance subclass |
1257 | * @param string $unused Unused, kept for compatibility |
1258 | */ |
1259 | protected function runMaintenance( $class, $unused = '' ) { |
1260 | $this->output( "Running $class...\n" ); |
1261 | $task = $this->maintenance->runChild( $class ); |
1262 | $ok = $task->execute(); |
1263 | if ( !$ok ) { |
1264 | throw new RuntimeException( "Execution of $class did not complete successfully." ); |
1265 | } |
1266 | $this->output( "done.\n" ); |
1267 | } |
1268 | |
1269 | /** |
1270 | * Set any .htaccess files or equivalent for storage repos |
1271 | * |
1272 | * Some zones (e.g. "temp") used to be public and may have been initialized as such |
1273 | */ |
1274 | public function setFileAccess() { |
1275 | $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo(); |
1276 | $zonePath = $repo->getZonePath( 'temp' ); |
1277 | if ( $repo->getBackend()->directoryExists( [ 'dir' => $zonePath ] ) ) { |
1278 | // If the directory was never made, then it will have the right ACLs when it is made |
1279 | $status = $repo->getBackend()->secure( [ |
1280 | 'dir' => $zonePath, |
1281 | 'noAccess' => true, |
1282 | 'noListing' => true |
1283 | ] ); |
1284 | if ( $status->isOK() ) { |
1285 | $this->output( "Set the local repo temp zone container to be private.\n" ); |
1286 | } else { |
1287 | $this->output( "Failed to set the local repo temp zone container to be private.\n" ); |
1288 | } |
1289 | } |
1290 | } |
1291 | |
1292 | /** |
1293 | * Purge various database caches |
1294 | */ |
1295 | public function purgeCache() { |
1296 | global $wgLocalisationCacheConf; |
1297 | // We can't guarantee that the user will be able to use TRUNCATE, |
1298 | // but we know that DELETE is available to us |
1299 | $this->output( "Purging caches..." ); |
1300 | |
1301 | // ObjectCache |
1302 | $this->db->newDeleteQueryBuilder() |
1303 | ->deleteFrom( 'objectcache' ) |
1304 | ->where( ISQLPlatform::ALL_ROWS ) |
1305 | ->caller( __METHOD__ ) |
1306 | ->execute(); |
1307 | |
1308 | // LocalisationCache |
1309 | if ( $wgLocalisationCacheConf['manualRecache'] ) { |
1310 | $this->rebuildLocalisationCache(); |
1311 | } |
1312 | |
1313 | // ResourceLoader: Message cache |
1314 | $services = MediaWikiServices::getInstance(); |
1315 | MessageBlobStore::clearGlobalCacheEntry( |
1316 | $services->getMainWANObjectCache() |
1317 | ); |
1318 | } |
1319 | |
1320 | /** |
1321 | * Check the site_stats table is not properly populated. |
1322 | */ |
1323 | protected function checkStats() { |
1324 | $this->output( "...site_stats is populated..." ); |
1325 | $row = $this->db->newSelectQueryBuilder() |
1326 | ->select( '*' ) |
1327 | ->from( 'site_stats' ) |
1328 | ->where( [ 'ss_row_id' => 1 ] ) |
1329 | ->caller( __METHOD__ )->fetchRow(); |
1330 | if ( $row === false ) { |
1331 | $this->output( "data is missing! rebuilding...\n" ); |
1332 | } elseif ( isset( $row->site_stats ) && $row->ss_total_pages == -1 ) { |
1333 | $this->output( "missing ss_total_pages, rebuilding...\n" ); |
1334 | } else { |
1335 | $this->output( "done.\n" ); |
1336 | |
1337 | return; |
1338 | } |
1339 | SiteStatsInit::doAllAndCommit( $this->db ); |
1340 | } |
1341 | |
1342 | # Common updater functions |
1343 | |
1344 | /** |
1345 | * Update CategoryLinks collation |
1346 | */ |
1347 | protected function doCollationUpdate() { |
1348 | global $wgCategoryCollation; |
1349 | if ( $this->updateRowExists( 'UpdateCollation::' . $wgCategoryCollation ) ) { |
1350 | $this->output( "...collations up-to-date.\n" ); |
1351 | return; |
1352 | } |
1353 | $this->output( "Updating category collations...\n" ); |
1354 | $task = $this->maintenance->runChild( UpdateCollation::class ); |
1355 | $ok = $task->execute(); |
1356 | if ( $ok !== false ) { |
1357 | $this->output( "...done.\n" ); |
1358 | $this->insertUpdateRow( 'UpdateCollation::' . $wgCategoryCollation ); |
1359 | } |
1360 | } |
1361 | |
1362 | protected function doConvertDjvuMetadata() { |
1363 | if ( $this->updateRowExists( 'ConvertDjvuMetadata' ) ) { |
1364 | return; |
1365 | } |
1366 | $this->output( "Converting djvu metadata..." ); |
1367 | $task = $this->maintenance->runChild( RefreshImageMetadata::class ); |
1368 | '@phan-var RefreshImageMetadata $task'; |
1369 | $task->loadParamsAndArgs( RefreshImageMetadata::class, [ |
1370 | 'force' => true, |
1371 | 'mediatype' => 'OFFICE', |
1372 | 'mime' => 'image/*', |
1373 | 'batch-size' => 1, |
1374 | 'sleep' => 1 |
1375 | ] ); |
1376 | $ok = $task->execute(); |
1377 | if ( $ok !== false ) { |
1378 | $this->output( "...done.\n" ); |
1379 | $this->insertUpdateRow( 'ConvertDjvuMetadata' ); |
1380 | } |
1381 | } |
1382 | |
1383 | /** |
1384 | * Rebuilds the localisation cache |
1385 | */ |
1386 | protected function rebuildLocalisationCache() { |
1387 | /** |
1388 | * @var RebuildLocalisationCache $cl |
1389 | */ |
1390 | $cl = $this->maintenance->runChild( |
1391 | RebuildLocalisationCache::class, 'rebuildLocalisationCache.php' |
1392 | ); |
1393 | '@phan-var RebuildLocalisationCache $cl'; |
1394 | $this->output( "Rebuilding localisation cache...\n" ); |
1395 | $cl->setForce(); |
1396 | $cl->execute(); |
1397 | $this->output( "done.\n" ); |
1398 | } |
1399 | |
1400 | protected function migrateTemplatelinks() { |
1401 | if ( $this->updateRowExists( MigrateLinksTable::class . 'templatelinks' ) ) { |
1402 | $this->output( "...templatelinks table has already been migrated.\n" ); |
1403 | return; |
1404 | } |
1405 | /** |
1406 | * @var MigrateLinksTable $task |
1407 | */ |
1408 | $task = $this->maintenance->runChild( |
1409 | MigrateLinksTable::class, 'migrateLinksTable.php' |
1410 | ); |
1411 | '@phan-var MigrateLinksTable $task'; |
1412 | $task->loadParamsAndArgs( MigrateLinksTable::class, [ |
1413 | 'force' => true, |
1414 | 'table' => 'templatelinks' |
1415 | ] ); |
1416 | $this->output( "Running migrateLinksTable.php on templatelinks...\n" ); |
1417 | $task->execute(); |
1418 | $this->output( "done.\n" ); |
1419 | } |
1420 | |
1421 | protected function migratePagelinks() { |
1422 | if ( $this->updateRowExists( MigrateLinksTable::class . 'pagelinks' ) ) { |
1423 | $this->output( "...pagelinks table has already been migrated.\n" ); |
1424 | return; |
1425 | } |
1426 | /** |
1427 | * @var MigrateLinksTable $task |
1428 | */ |
1429 | $task = $this->maintenance->runChild( |
1430 | MigrateLinksTable::class, 'migrateLinksTable.php' |
1431 | ); |
1432 | '@phan-var MigrateLinksTable $task'; |
1433 | $task->loadParamsAndArgs( MigrateLinksTable::class, [ |
1434 | 'force' => true, |
1435 | 'table' => 'pagelinks' |
1436 | ] ); |
1437 | $this->output( "Running migrateLinksTable.php on pagelinks...\n" ); |
1438 | $task->execute(); |
1439 | $this->output( "done.\n" ); |
1440 | } |
1441 | |
1442 | /** |
1443 | * Only run a function if a table does not exist |
1444 | * |
1445 | * @since 1.35 |
1446 | * @param string $table Table to check. |
1447 | * If passed $this, it's assumed to be a call from runUpdates() with |
1448 | * $passSelf = true: all other parameters are shifted and $this is |
1449 | * prepended to the rest of $params. |
1450 | * @param string|array|static $func Normally this is the string naming the method on $this to |
1451 | * call. It may also be an array style callable. |
1452 | * @param mixed ...$params Parameters for `$func` |
1453 | * @return mixed Whatever $func returns, or null when skipped. |
1454 | */ |
1455 | protected function ifTableNotExists( $table, $func, ...$params ) { |
1456 | // Handle $passSelf from runUpdates(). |
1457 | $passSelf = false; |
1458 | if ( $table === $this ) { |
1459 | $passSelf = true; |
1460 | $table = $func; |
1461 | $func = array_shift( $params ); |
1462 | } |
1463 | |
1464 | if ( $this->db->tableExists( $table, __METHOD__ ) ) { |
1465 | return null; |
1466 | } |
1467 | |
1468 | if ( !is_array( $func ) && method_exists( $this, $func ) ) { |
1469 | $func = [ $this, $func ]; |
1470 | } elseif ( $passSelf ) { |
1471 | array_unshift( $params, $this ); |
1472 | } |
1473 | |
1474 | // @phan-suppress-next-line PhanUndeclaredInvokeInCallable Phan is confused |
1475 | return $func( ...$params ); |
1476 | } |
1477 | |
1478 | /** |
1479 | * Only run a function if the named field exists |
1480 | * |
1481 | * @since 1.35 |
1482 | * @param string $table Table to check. |
1483 | * If passed $this, it's assumed to be a call from runUpdates() with |
1484 | * $passSelf = true: all other parameters are shifted and $this is |
1485 | * prepended to the rest of $params. |
1486 | * @param string $field Field to check |
1487 | * @param string|array|static $func Normally this is the string naming the method on $this to |
1488 | * call. It may also be an array style callable. |
1489 | * @param mixed ...$params Parameters for `$func` |
1490 | * @return mixed Whatever $func returns, or null when skipped. |
1491 | */ |
1492 | protected function ifFieldExists( $table, $field, $func, ...$params ) { |
1493 | // Handle $passSelf from runUpdates(). |
1494 | $passSelf = false; |
1495 | if ( $table === $this ) { |
1496 | $passSelf = true; |
1497 | $table = $field; |
1498 | $field = $func; |
1499 | $func = array_shift( $params ); |
1500 | } |
1501 | |
1502 | if ( !$this->db->tableExists( $table, __METHOD__ ) || |
1503 | !$this->db->fieldExists( $table, $field, __METHOD__ ) |
1504 | ) { |
1505 | return null; |
1506 | } |
1507 | |
1508 | if ( !is_array( $func ) && method_exists( $this, $func ) ) { |
1509 | $func = [ $this, $func ]; |
1510 | } elseif ( $passSelf ) { |
1511 | array_unshift( $params, $this ); |
1512 | } |
1513 | |
1514 | // @phan-suppress-next-line PhanUndeclaredInvokeInCallable Phan is confused |
1515 | return $func( ...$params ); |
1516 | } |
1517 | |
1518 | } |
1519 | |
1520 | /** @deprecated class alias since 1.42 */ |
1521 | class_alias( DatabaseUpdater::class, 'DatabaseUpdater' ); |