Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.15% covered (success)
96.15%
75 / 78
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
UpdateAutoBlockParentIdColumn
96.15% covered (success)
96.15%
75 / 78
66.67% covered (warning)
66.67%
2 / 3
11
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 getUpdateKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doDbUpdates
95.65% covered (success)
95.65%
66 / 69
0.00% covered (danger)
0.00%
0 / 1
9
1<?php
2
3namespace MediaWiki\Extension\GlobalBlocking\Maintenance;
4
5use MediaWiki\Maintenance\LoggedUpdateMaintenance;
6use Wikimedia\Rdbms\IMaintainableDatabase;
7use Wikimedia\Rdbms\SelectQueryBuilder;
8
9// @codeCoverageIgnoreStart
10$IP = getenv( 'MW_INSTALL_PATH' );
11if ( $IP === false ) {
12    $IP = __DIR__ . '/../../..';
13}
14require_once "$IP/maintenance/Maintenance.php";
15// @codeCoverageIgnoreEnd
16
17/**
18 * Maintenance script for updating gb_autoblock_parent_id to replace NULL values with 0.
19 */
20class UpdateAutoBlockParentIdColumn extends LoggedUpdateMaintenance {
21
22    public function __construct() {
23        parent::__construct();
24
25        $this->addDescription(
26            "Used to update the values of NULL for gb_autoblock_parent_id to 0 (T376340). Necessary because " .
27            "the gb_autoblock_parent_id is used in a unique index which does not work as intended with NULL values. " .
28            "If you use a central globalblocks table, you only need to run this script once for the wikis which " .
29            "use the central table."
30        );
31
32        $this->requireExtension( 'GlobalBlocking' );
33    }
34
35    /** @inheritDoc */
36    public function getUpdateKey() {
37        return __CLASS__;
38    }
39
40    /** @inheritDoc */
41    public function doDbUpdates() {
42        $connectionProvider = $this->getServiceContainer()->getConnectionProvider();
43        $dbr = $connectionProvider->getReplicaDatabase( 'virtual-globalblocking' );
44        if ( !( $dbr instanceof IMaintainableDatabase ) ) {
45            throw new \RuntimeException( 'Wrong database class' );
46        }
47        $autoBlockParentIdFieldInfo = $dbr->fieldInfo( 'globalblocks', 'gb_autoblock_parent_id' );
48        if ( !$autoBlockParentIdFieldInfo || !$autoBlockParentIdFieldInfo->isNullable() ) {
49            $this->output( "The field globalblocks.gb_autoblock_parent_id is not nullable, nothing to do.\n" );
50            return true;
51        }
52
53        $hasRowsToUpdate = $dbr->newSelectQueryBuilder()
54            ->select( '1' )
55            ->from( 'globalblocks' )
56            ->where( [ 'gb_autoblock_parent_id' => null ] )
57            ->caller( __METHOD__ )
58            ->fetchField();
59        if ( !$hasRowsToUpdate ) {
60            $this->output( "The globalblocks table has no rows to update.\n" );
61            return true;
62        }
63
64        $success = 0;
65        $failed = 0;
66        $lastProcessedRowId = 0;
67        $dbw = $connectionProvider->getPrimaryDatabase( 'virtual-globalblocking' );
68        do {
69            // Fetch a batch of rows with gb_autoblock_parent_id as NULL
70            $batchToProcess = $dbr->newSelectQueryBuilder()
71                ->select( 'gb_id' )
72                ->from( 'globalblocks' )
73                ->where( [
74                    'gb_autoblock_parent_id' => null,
75                    $dbr->expr( 'gb_id', '>', $lastProcessedRowId ),
76                ] )
77                ->orderBy( 'gb_id', SelectQueryBuilder::SORT_ASC )
78                ->limit( $this->getBatchSize() )
79                ->caller( __METHOD__ )
80                ->fetchFieldValues();
81
82            if ( count( $batchToProcess ) ) {
83                $lastId = end( $batchToProcess );
84                $firstId = reset( $batchToProcess );
85                $this->output( "Now processing global blocks with id between $firstId and $lastId...\n" );
86
87                foreach ( $batchToProcess as $id ) {
88                    // Check if the gb_address used by this globalblocks row is used for any other globalblocks
89                    // row where the gb_autoblock_parent_id is 0. If it is, then we cannot update the row with ID
90                    // $id because it would cause a unique index constrant violation.
91                    // We cannot use an UPDATE IGNORE for this as postgres will still throw an error.
92                    $targetForRow = $dbr->newSelectQueryBuilder()
93                        ->select( 'gb_address' )
94                        ->from( 'globalblocks' )
95                        ->where( [ 'gb_id' => $id ] )
96                        ->caller( __METHOD__ )
97                        ->fetchField();
98
99                    $collidingRowExists = $dbw->newSelectQueryBuilder()
100                        ->select( '1' )
101                        ->from( 'globalblocks' )
102                        ->where( [ 'gb_address' => $targetForRow, 'gb_autoblock_parent_id' => 0 ] )
103                        ->fetchField();
104
105                    if ( !$collidingRowExists ) {
106                        $dbw->newUpdateQueryBuilder()
107                            ->update( 'globalblocks' )
108                            ->set( [ 'gb_autoblock_parent_id' => 0 ] )
109                            ->where( [ 'gb_id' => $id ] )
110                            ->caller( __METHOD__ )
111                            ->execute();
112                    }
113
114                    $newValue = $dbw->newSelectQueryBuilder()
115                        ->select( 'gb_autoblock_parent_id' )
116                        ->from( 'globalblocks' )
117                        ->where( [ 'gb_id' => $id ] )
118                        ->fetchField();
119                    if ( $newValue !== null ) {
120                        $success += 1;
121                    } else {
122                        $this->output( "...Failed to update row with ID $id.\n" );
123                        $failed += 1;
124                    }
125                }
126
127                $lastProcessedRowId = end( $batchToProcess );
128                $this->waitForReplication();
129            }
130        } while ( count( $batchToProcess ) === $this->getBatchSize() );
131
132        $this->output( "Completed migration, updated $success row(s), failed to update $failed row(s).\n" );
133        return true;
134    }
135}
136
137// @codeCoverageIgnoreStart
138$maintClass = UpdateAutoBlockParentIdColumn::class;
139require_once RUN_MAINTENANCE_IF_MAIN;
140// @codeCoverageIgnoreEnd