9 private bool $dryRun =
false;
12 parent::__construct();
13 $this->
addDescription(
'Fix referential integrity issues in block and block_target tables' );
14 $this->
addOption(
'dry-run',
'Just report, don\'t fix anything' );
18 $this->dryRun = $this->
hasOption(
'dry-run' );
20 $this->deleteOrphanBlockTargets();
21 $this->deleteTargetlessBlocks();
22 $this->normalizeAddresses();
23 $this->mergeDuplicateBlockTargets();
24 $this->fixTargetCounts();
30 private function deleteOrphanBlockTargets() {
32 $badIds = $dbr->newSelectQueryBuilder()
34 ->from(
'block_target' )
35 ->leftJoin(
'block',
null,
'bt_id=bl_target' )
36 ->where( [
'bl_target' =>
null ] )
37 ->caller( __METHOD__ )
40 foreach ( $badIds as $id ) {
41 $this->deleteOrphanBlockTarget( (
int)$id );
49 private function deleteOrphanBlockTarget(
int $id ) {
50 $this->
output(
"Deleting orphan bt_id=$id: " );
51 if ( $this->dryRun ) {
52 $this->
output(
"dry run\n" );
56 $dbw->startAtomic( __METHOD__ );
57 $lockingUsage = $dbw->newSelectQueryBuilder()
58 ->select(
'COUNT(*)' )
60 ->where( [
'bl_target' => $id ] )
62 ->caller( __METHOD__ )
64 if ( $lockingUsage ) {
65 $dbw->endAtomic( __METHOD__ );
66 $this->
output(
"primary usage count is non-zero\n" );
69 $dbw->newDeleteQueryBuilder()
70 ->deleteFrom(
'block_target' )
71 ->where( [
'bt_id' => $id ] )
72 ->caller( __METHOD__ )
74 $affected = $dbw->affectedRows();
75 $dbw->endAtomic( __METHOD__ );
76 $this->
output( $affected ?
"OK\n" :
"no rows affected\n" );
82 private function deleteTargetlessBlocks() {
84 $res = $dbr->newSelectQueryBuilder()
85 ->select( [
'bl_id',
'bl_target' ] )
87 ->leftJoin(
'block_target',
null,
'bt_id=bl_target' )
88 ->where( [
'bt_id' =>
null ] )
89 ->caller( __METHOD__ )
91 foreach ( $res as $row ) {
92 $this->deleteTargetlessBlock( (
int)$row->bl_id, (
int)$row->bl_target );
102 private function deleteTargetlessBlock(
int $blockId,
int $targetId ) {
103 $this->
output(
"Deleting block $blockId on non-existent target $targetId: " );
104 if ( $this->dryRun ) {
105 $this->
output(
"dry run\n" );
109 $dbw->startAtomic( __METHOD__ );
110 $lockingTargetCount = $dbw->newSelectQueryBuilder()
111 ->select(
'COUNT(*)' )
112 ->from(
'block_target' )
113 ->where( [
'bt_id' => $targetId ] )
115 ->caller( __METHOD__ )
117 if ( $lockingTargetCount ) {
118 $this->
output(
"target exists in primary\n" );
119 $dbw->endAtomic( __METHOD__ );
122 $dbw->newDeleteQueryBuilder()
123 ->deleteFrom(
'block' )
124 ->where( [
'bl_id' => $blockId,
'bl_target' => $targetId ] )
125 ->caller( __METHOD__ )
127 $affected = $dbw->affectedRows();
128 $dbw->endAtomic( __METHOD__ );
129 $this->
output( $affected ?
"OK\n" :
"no rows affected\n" );
138 private function normalizeAddresses() {
140 $dbType = $dbr->getType();
141 if ( $dbType !==
'mysql' ) {
142 $this->
output(
"Skipping IP address normalization: not implemented on $dbType\n" );
145 $res = $dbr->newSelectQueryBuilder()
146 ->select( [
'bt_id',
'bt_address' ] )
147 ->from(
'block_target' )
148 ->where( [
'bt_user' =>
null ] )
149 ->andWhere(
'bt_range_start IS NOT NULL OR ' .
150 'bt_address RLIKE \'(^|[.:])0[0-9]|[a-f]|::\'' )
151 ->caller( __METHOD__ )
154 foreach ( $res as $row ) {
155 $addr = $row->bt_address;
156 if ( IPUtils::isValid( $addr ) ) {
157 $norm = IPUtils::sanitizeIP( $addr );
158 } elseif ( IPUtils::isValidRange( $addr ) ) {
159 $norm = IPUtils::sanitizeRange( $addr );
163 if ( $addr !== $norm && is_string( $norm ) ) {
164 $this->normalizeAddress( (
int)$row->bt_id, $addr, $norm );
181 private function normalizeAddress(
int $targetId,
string $address,
string $normalizedAddress ) {
182 $this->
output(
"Normalizing bt_id=$targetId $address -> $normalizedAddress: " );
183 if ( $this->dryRun ) {
184 $this->
output(
"dry run\n" );
188 $dbw->startAtomic( __METHOD__ );
189 $primaryAddr = $dbw->newSelectQueryBuilder()
190 ->select(
'bt_address' )
191 ->from(
'block_target' )
192 ->where( [
'bt_id' => $targetId ] )
194 ->caller( __METHOD__ )
196 if ( $primaryAddr ===
false ) {
197 $this->
output(
"missing in primary\n" );
200 if ( $primaryAddr !== $address ) {
201 $this->
output(
"changed in primary\n" );
204 $dbw->newUpdateQueryBuilder()
205 ->update(
'block_target' )
206 ->set( [
'bt_address' => $normalizedAddress ] )
207 ->where( [
'bt_id' => $targetId ] )
208 ->caller( __METHOD__ )
210 $dbw->endAtomic( __METHOD__ );
211 $this->
output(
"done\n" );
217 private function mergeDuplicateBlockTargets() {
219 $rawGroups = $this->
getReplicaDB()->newSelectQueryBuilder()
220 ->select(
'GROUP_CONCAT(bt_id)' )
221 ->from(
'block_target' )
222 ->where( $dbr->expr(
'bt_user',
'!=',
null ) )
223 ->groupBy(
'bt_user' )
224 ->having(
'COUNT(*) > 1' )
225 ->caller( __METHOD__ )
226 ->fetchFieldValues();
227 $this->processIdGroups( $rawGroups );
229 $rawGroups = $this->
getReplicaDB()->newSelectQueryBuilder()
230 ->select(
'GROUP_CONCAT(bt_id)' )
231 ->from(
'block_target' )
232 ->where( $dbr->expr(
'bt_address',
'!=',
null ) )
233 ->groupBy( [
'bt_auto',
'bt_address' ] )
234 ->having(
'COUNT(*) > 1' )
235 ->caller( __METHOD__ )
236 ->fetchFieldValues();
237 $this->processIdGroups( $rawGroups );
244 private function processIdGroups( $rawGroups ) {
245 foreach ( $rawGroups as $blob ) {
246 $group = array_map(
'intval', explode(
',', $blob ) );
248 $main = array_shift( $group );
249 $this->mergeGroup( $main, $group );
258 private function mergeGroup(
int $mainId, array $badIds ) {
259 $this->
output(
'Merging bt_id ' . implode(
',', $badIds ) .
" into $mainId: " );
260 if ( $this->dryRun ) {
261 $this->
output(
"dry run\n" );
266 $dbw->startAtomic( __METHOD__ );
269 $fieldsToTest = [
'bt_address',
'bt_user',
'bt_user_text',
'bt_auto' ];
270 $mainRow = $dbw->newSelectQueryBuilder()
271 ->select( $fieldsToTest )
272 ->from(
'block_target' )
273 ->where( [
'bt_id' => $mainId ] )
275 ->caller( __METHOD__ )
277 $badRows = $dbw->newSelectQueryBuilder()
278 ->select( $fieldsToTest )
280 ->from(
'block_target' )
281 ->where( [
'bt_id' => $badIds ] )
283 ->caller( __METHOD__ )
286 if ( $badRows->numRows() !== count( $badIds ) ) {
287 $this->
output(
"some IDs are not present in the primary\n" );
288 $dbw->endAtomic( __METHOD__ );
292 foreach ( $badRows as $badRow ) {
293 foreach ( $fieldsToTest as $field ) {
294 if ( $mainRow->$field !== $badRow->$field ) {
295 $this->
output(
"mismatch in $field for bt_id={$badRow->bt_id}\n" );
296 $dbw->endAtomic( __METHOD__ );
303 $dbw->newUpdateQueryBuilder()
305 ->set( [
'bl_target' => $mainId ] )
306 ->where( [
'bl_target' => $badIds ] )
307 ->caller( __METHOD__ )
309 $blockCount = $dbw->affectedRows();
312 $dbw->newDeleteQueryBuilder()
313 ->delete(
'block_target' )
314 ->where( [
'bt_id' => $badIds ] )
315 ->caller( __METHOD__ )
319 $dbw->newUpdateQueryBuilder()
320 ->update(
'block_target' )
321 ->set(
'bt_count=bt_count + ' . $blockCount )
322 ->where( [
'bt_id' => $mainId ] )
323 ->caller( __METHOD__ )
326 $dbw->endAtomic( __METHOD__ );
327 $this->
output(
"done\n" );
333 private function fixTargetCounts() {
335 $res = $dbr->newSelectQueryBuilder()
336 ->select( [
'bt_id',
'bt_count',
'real_count' =>
'COUNT(*)' ] )
338 ->join(
'block_target',
null,
'bt_id=bl_target' )
339 ->groupBy( [
'bt_id',
'bt_count' ] )
340 ->having(
'COUNT(*) != bt_count' )
341 ->caller( __METHOD__ )
344 foreach ( $res as $row ) {
345 $this->fixTargetCount( (
int)$row->bt_id, (
int)$row->bt_count, (
int)$row->real_count );
356 private function fixTargetCount(
int $targetId,
int $badCount,
int $replicaCount ) {
357 $this->
output(
"Fixing bt_id=$targetId count $badCount -> $replicaCount: " );
358 if ( $this->dryRun ) {
359 $this->
output(
"dry run\n" );
364 $dbw->startAtomic( __METHOD__ );
365 $primaryCount = (int)$dbw->newSelectQueryBuilder()
366 ->select(
'COUNT(*)' )
368 ->where( [
'bl_target' => $targetId ] )
370 ->caller( __METHOD__ )
373 if ( $primaryCount !== $replicaCount ) {
374 $dbw->endAtomic( __METHOD__ );
375 $this->
output(
"changed in primary, skipping\n" );
379 $dbw->newUpdateQueryBuilder()
380 ->update(
'block_target' )
381 ->set( [
'bt_count' => $primaryCount ] )
383 'bt_id' => $targetId,
384 'bt_count' => $badCount
386 ->caller( __METHOD__ )
388 $affected = $dbw->affectedRows();
389 $dbw->endAtomic( __METHOD__ );
390 $this->
output( $affected ?
"OK\n" :
"no rows affected\n" );