MediaWiki master
uppercaseTitlesForUnicodeTransition.php
Go to the documentation of this file.
1<?php
33
34require_once __DIR__ . '/Maintenance.php';
35
43
44 private const MOVE = 0;
45 private const INPLACE_MOVE = 1;
46 private const UPPERCASE = 2;
47
49 private $run = false;
50
52 private $charmap = [];
53
55 private $user;
56
58 private $reason = 'Uppercasing title for Unicode upgrade';
59
61 private $tags = [];
62
64 private $seenUsers = [];
65
67 private $namespaces = null;
68
70 private $prefix = null, $suffix = null;
71
73 private $prefixNs = null;
74
76 private $tables = null;
77
78 public function __construct() {
79 parent::__construct();
80 $this->addDescription(
81 "Rename titles when changing behavior of Language::ucfirst().\n"
82 . "\n"
83 . "This script skips User and User_talk pages for registered users, as renaming of users "
84 . "is too complex to try to implement here. Use something like Extension:Renameuser to "
85 . "clean those up; this script can provide a list of user names affected."
86 );
87 $this->addOption(
88 'charmap', 'Character map generated by maintenance/language/generateUcfirstOverrides.php',
89 true, true
90 );
91 $this->addOption(
92 'user', 'System user to use to do the renames. Default is "Maintenance script".', false, true
93 );
94 $this->addOption(
95 'steal',
96 'If the username specified by --user exists, specify this to force conversion to a system user.'
97 );
98 $this->addOption(
99 'run', 'If not specified, the script will not actually perform any moves (i.e. it will dry-run).'
100 );
101 $this->addOption(
102 'prefix', 'When the new title already exists, add this prefix.', false, true
103 );
104 $this->addOption(
105 'suffix', 'When the new title already exists, add this suffix.', false, true
106 );
107 $this->addOption( 'reason', 'Reason to use when moving pages.', false, true );
108 $this->addOption( 'tag', 'Change tag to apply when moving pages.', false, true );
109 $this->addOption( 'tables', 'Comma-separated list of database tables to process.', false, true );
110 $this->addOption(
111 'userlist', 'Filename to which to output usernames needing rename. ' .
112 'This file can then be used directly by renameInvalidUsernames.php maintenance script',
113 false,
114 true
115 );
116 $this->setBatchSize( 1000 );
117 }
118
119 public function execute() {
120 $this->run = $this->getOption( 'run', false );
121
122 if ( $this->run ) {
123 $username = $this->getOption( 'user', User::MAINTENANCE_SCRIPT_USER );
124 $steal = $this->getOption( 'steal', false );
125 $this->user = User::newSystemUser( $username, [ 'steal' => $steal ] );
126 if ( !$this->user ) {
127 $user = User::newFromName( $username );
128 if ( !$steal && $user && $user->isRegistered() ) {
129 $this->fatalError( "User $username already exists.\n"
130 . "Use --steal if you really want to steal it from the human who currently owns it."
131 );
132 }
133 $this->fatalError( "Could not obtain system user $username." );
134 }
135 }
136
137 $tables = $this->getOption( 'tables' );
138 if ( $tables !== null ) {
139 $this->tables = explode( ',', $tables );
140 }
141
142 $prefix = $this->getOption( 'prefix' );
143 if ( $prefix !== null ) {
144 $title = Title::newFromText( $prefix . 'X' );
145 if ( !$title || substr( $title->getDBkey(), -1 ) !== 'X' ) {
146 $this->fatalError( 'Invalid --prefix.' );
147 }
148 if ( $title->getNamespace() <= NS_MAIN || $title->isExternal() ) {
149 $this->fatalError( 'Invalid --prefix. It must not be in namespace 0 and must not be external' );
150 }
151 $this->prefixNs = $title->getNamespace();
152 $this->prefix = substr( $title->getText(), 0, -1 );
153 }
154 $this->suffix = $this->getOption( 'suffix' );
155
156 $this->reason = $this->getOption( 'reason' ) ?: $this->reason;
157 $this->tags = (array)$this->getOption( 'tag', null );
158
159 $charmapFile = $this->getOption( 'charmap' );
160 if ( !file_exists( $charmapFile ) ) {
161 $this->fatalError( "Charmap file $charmapFile does not exist." );
162 }
163 if ( !is_file( $charmapFile ) || !is_readable( $charmapFile ) ) {
164 $this->fatalError( "Charmap file $charmapFile is not readable." );
165 }
166 $this->charmap = require $charmapFile;
167 if ( !is_array( $this->charmap ) ) {
168 $this->fatalError( "Charmap file $charmapFile did not return a PHP array." );
169 }
170 $this->charmap = array_filter(
171 $this->charmap,
172 function ( $v, $k ) {
173 if ( mb_strlen( $k ) !== 1 ) {
174 $this->error( "Ignoring mapping from multi-character key '$k' to '$v'" );
175 return false;
176 }
177 return $k !== $v;
178 },
179 ARRAY_FILTER_USE_BOTH
180 );
181 if ( !$this->charmap ) {
182 $this->fatalError( "Charmap file $charmapFile did not contain any usable character mappings." );
183 }
184
185 $db = $this->run ? $this->getPrimaryDB() : $this->getReplicaDB();
186
187 // Process inplace moves first, before actual moves, so mungeTitle() doesn't get confused
188 $this->processTable(
189 $db, self::INPLACE_MOVE, 'archive', 'ar_namespace', 'ar_title', [ 'ar_timestamp', 'ar_id' ]
190 );
191 $this->processTable(
192 $db, self::INPLACE_MOVE, 'filearchive', NS_FILE, 'fa_name', [ 'fa_timestamp', 'fa_id' ]
193 );
194 $this->processTable(
195 $db, self::INPLACE_MOVE, 'logging', 'log_namespace', 'log_title', [ 'log_id' ]
196 );
197 $this->processTable(
198 $db, self::INPLACE_MOVE, 'protected_titles', 'pt_namespace', 'pt_title', []
199 );
200 $this->processTable( $db, self::MOVE, 'page', 'page_namespace', 'page_title', [ 'page_id' ] );
201 $this->processTable( $db, self::MOVE, 'image', NS_FILE, 'img_name', [] );
202 $this->processTable(
203 $db, self::UPPERCASE, 'redirect', 'rd_namespace', 'rd_title', [ 'rd_from' ]
204 );
205 $this->processUsers( $db );
206 }
207
215 private function getLikeBatches( IReadableDatabase $db, $field, $batchSize = 100 ) {
216 $ret = [];
217 $likes = [];
218 foreach ( $this->charmap as $from => $to ) {
219 $likes[] = $db->expr(
220 $field,
221 IExpression::LIKE,
222 new LikeValue( $from, $db->anyString() )
223 );
224 if ( count( $likes ) >= $batchSize ) {
225 $ret[] = new OrExpressionGroup( ...$likes );
226 $likes = [];
227 }
228 }
229 if ( $likes ) {
230 $ret[] = new OrExpressionGroup( ...$likes );
231 }
232 return $ret;
233 }
234
243 private function getNamespaces() {
244 if ( $this->namespaces === null ) {
245 $nsinfo = $this->getServiceContainer()->getNamespaceInfo();
246 $this->namespaces = array_filter(
247 array_keys( $nsinfo->getCanonicalNamespaces() ),
248 static function ( $ns ) use ( $nsinfo ) {
249 return $nsinfo->isMovable( $ns ) && $nsinfo->isCapitalized( $ns );
250 }
251 );
252 usort( $this->namespaces, static function ( $ns1, $ns2 ) use ( $nsinfo ) {
253 if ( $ns1 === $ns2 ) {
254 return 0;
255 }
256
257 $s1 = $nsinfo->getSubject( $ns1 );
258 $s2 = $nsinfo->getSubject( $ns2 );
259
260 // Order by subject namespace number first
261 if ( $s1 !== $s2 ) {
262 return $s1 < $s2 ? -1 : 1;
263 }
264
265 // Second, put subject namespaces before non-subject namespaces
266 if ( $s1 === $ns1 ) {
267 return -1;
268 }
269 if ( $s2 === $ns2 ) {
270 return 1;
271 }
272
273 // Don't care about the relative order if there are somehow
274 // multiple non-subject namespaces for a namespace.
275 return 0;
276 } );
277 }
278
279 return $this->namespaces;
280 }
281
289 private function isUserPage( IReadableDatabase $db, $ns, $title ) {
290 if ( $ns !== NS_USER && $ns !== NS_USER_TALK ) {
291 return false;
292 }
293
294 [ $base ] = explode( '/', $title, 2 );
295 if ( !isset( $this->seenUsers[$base] ) ) {
296 // Can't use User directly because it might uppercase the name
297 $this->seenUsers[$base] = (bool)$db->newSelectQueryBuilder()
298 ->select( 'user_id' )
299 ->from( 'user' )
300 ->where( [ 'user_name' => strtr( $base, '_', ' ' ) ] )
301 ->caller( __METHOD__ )->fetchField();
302 }
303 return $this->seenUsers[$base];
304 }
305
313 private function mungeTitle( IReadableDatabase $db, Title $oldTitle, Title &$newTitle ) {
314 $nt = $newTitle->getPrefixedText();
315
316 $munge = false;
317 if ( $this->isUserPage( $db, $newTitle->getNamespace(), $newTitle->getText() ) ) {
318 $munge = 'Target title\'s user exists';
319 } else {
320 $mpFactory = $this->getServiceContainer()->getMovePageFactory();
321 $status = $mpFactory->newMovePage( $oldTitle, $newTitle )->isValidMove();
322 if ( !$status->isOK() && (
323 $status->hasMessage( 'articleexists' ) || $status->hasMessage( 'redirectexists' ) ) ) {
324 $munge = 'Target title exists';
325 }
326 }
327 if ( !$munge ) {
328 return true;
329 }
330
331 if ( $this->prefix !== null ) {
332 $newTitle = Title::makeTitle(
333 $this->prefixNs,
334 $this->prefix . $oldTitle->getPrefixedText() . ( $this->suffix ?? '' )
335 );
336 } elseif ( $this->suffix !== null ) {
337 $dbkey = $newTitle->getText();
338 $i = $newTitle->getNamespace() === NS_FILE ? strrpos( $dbkey, '.' ) : false;
339 if ( $i !== false ) {
340 $newTitle = Title::makeTitle(
341 $newTitle->getNamespace(),
342 substr( $dbkey, 0, $i ) . $this->suffix . substr( $dbkey, $i )
343 );
344 } else {
345 $newTitle = Title::makeTitle( $newTitle->getNamespace(), $dbkey . $this->suffix );
346 }
347 } else {
348 $this->error(
349 "Cannot move {$oldTitle->getPrefixedText()} → $nt: "
350 . "$munge and no --prefix or --suffix was given"
351 );
352 return false;
353 }
354
355 if ( !$newTitle->canExist() ) {
356 $this->error(
357 "Cannot move {$oldTitle->getPrefixedText()} → $nt: "
358 . "$munge and munged title '{$newTitle->getPrefixedText()}' is not valid"
359 );
360 return false;
361 }
362 if ( $newTitle->exists() ) {
363 $this->error(
364 "Cannot move {$oldTitle->getPrefixedText()} → $nt: "
365 . "$munge and munged title '{$newTitle->getPrefixedText()}' also exists"
366 );
367 return false;
368 }
369
370 return true;
371 }
372
380 private function doMove( IDatabase $db, $ns, $title ) {
381 $char = mb_substr( $title, 0, 1 );
382 if ( !array_key_exists( $char, $this->charmap ) ) {
383 $this->error(
384 "Query returned NS$ns $title, which does not begin with a character in the charmap."
385 );
386 return false;
387 }
388
389 if ( $this->isUserPage( $db, $ns, $title ) ) {
390 $this->output( "... Skipping user page NS$ns $title\n" );
391 return null;
392 }
393
394 $oldTitle = Title::makeTitle( $ns, $title );
395 $newTitle = Title::makeTitle( $ns, $this->charmap[$char] . mb_substr( $title, 1 ) );
396 $deletionReason = $this->shouldDelete( $db, $oldTitle, $newTitle );
397 if ( !$this->mungeTitle( $db, $oldTitle, $newTitle ) ) {
398 return false;
399 }
400
401 $services = $this->getServiceContainer();
402 $mpFactory = $services->getMovePageFactory();
403 $movePage = $mpFactory->newMovePage( $oldTitle, $newTitle );
404 $status = $movePage->isValidMove();
405 if ( !$status->isOK() ) {
406 $this->error(
407 "Invalid move {$oldTitle->getPrefixedText()} → {$newTitle->getPrefixedText()}: "
408 . $status->getMessage( false, false, 'en' )->useDatabase( false )->plain()
409 );
410 return false;
411 }
412
413 if ( !$this->run ) {
414 $this->output(
415 "Would rename {$oldTitle->getPrefixedText()} → {$newTitle->getPrefixedText()}\n"
416 );
417 if ( $deletionReason ) {
418 $this->output(
419 "Would then delete {$newTitle->getPrefixedText()}: $deletionReason\n"
420 );
421 }
422 return true;
423 }
424
425 $status = $movePage->move( $this->user, $this->reason, false, $this->tags );
426 if ( !$status->isOK() ) {
427 $this->error(
428 "Move {$oldTitle->getPrefixedText()} → {$newTitle->getPrefixedText()} failed: "
429 . $status->getMessage( false, false, 'en' )->useDatabase( false )->plain()
430 );
431 }
432 $this->output( "Renamed {$oldTitle->getPrefixedText()} → {$newTitle->getPrefixedText()}\n" );
433
434 // The move created a log entry under the old invalid title. Fix it.
436 ->update( 'logging' )
437 ->set( [
438 'log_title' => $this->charmap[$char] . mb_substr( $title, 1 ),
439 ] )
440 ->where( [
441 'log_namespace' => $oldTitle->getNamespace(),
442 'log_title' => $oldTitle->getDBkey(),
443 'log_page' => $newTitle->getArticleID(),
444 ] )
445 ->caller( __METHOD__ )
446 ->execute();
447
448 if ( $deletionReason !== null ) {
449 $page = $services->getWikiPageFactory()->newFromTitle( $newTitle );
450 $delPage = $services->getDeletePageFactory()->newDeletePage( $page, $this->user );
451 $status = $delPage
452 ->forceImmediate( true )
453 ->deleteUnsafe( $deletionReason );
454 if ( !$status->isOK() ) {
455 $this->error(
456 "Deletion of {$newTitle->getPrefixedText()} failed: "
457 . $status->getMessage( false, false, 'en' )->useDatabase( false )->plain()
458 );
459 return false;
460 }
461 $this->output( "Deleted {$newTitle->getPrefixedText()}\n" );
462 }
463
464 return true;
465 }
466
481 private function shouldDelete( IReadableDatabase $db, Title $oldTitle, Title $newTitle ) {
482 $oldRow = $db->newSelectQueryBuilder()
483 ->select( [ 'ns' => 'rd_namespace', 'title' => 'rd_title' ] )
484 ->from( 'page' )
485 ->join( 'redirect', null, 'rd_from = page_id' )
486 ->where( [ 'page_namespace' => $oldTitle->getNamespace(), 'page_title' => $oldTitle->getDBkey() ] )
487 ->caller( __METHOD__ )->fetchRow();
488 if ( !$oldRow ) {
489 // Not a redirect
490 return null;
491 }
492
493 if ( (int)$oldRow->ns === $newTitle->getNamespace() &&
494 $oldRow->title === $newTitle->getDBkey()
495 ) {
496 return $this->reason . ", and found that [[{$oldTitle->getPrefixedText()}]] is "
497 . "already a redirect to [[{$newTitle->getPrefixedText()}]]";
498 } else {
499 $newRow = $db->newSelectQueryBuilder()
500 ->select( [ 'ns' => 'rd_namespace', 'title' => 'rd_title' ] )
501 ->from( 'page' )
502 ->join( 'redirect', null, 'rd_from = page_id' )
503 ->where( [ 'page_namespace' => $newTitle->getNamespace(), 'page_title' => $newTitle->getDBkey() ] )
504 ->caller( __METHOD__ )->fetchRow();
505 if ( $newRow && $oldRow->ns === $newRow->ns && $oldRow->title === $newRow->title ) {
506 $nt = Title::makeTitle( $newRow->ns, $newRow->title );
507 return $this->reason . ", and found that [[{$oldTitle->getPrefixedText()}]] and "
508 . "[[{$newTitle->getPrefixedText()}]] both redirect to [[{$nt->getPrefixedText()}]].";
509 }
510 }
511
512 return null;
513 }
514
527 private function doUpdate( IDatabase $db, $op, $table, $nsField, $titleField, $row ) {
528 $ns = is_int( $nsField ) ? $nsField : (int)$row->$nsField;
529 $title = $row->$titleField;
530
531 $char = mb_substr( $title, 0, 1 );
532 if ( !array_key_exists( $char, $this->charmap ) ) {
533 $r = json_encode( $row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
534 $this->error(
535 "Query returned $r, but title does not begin with a character in the charmap."
536 );
537 return false;
538 }
539
540 $oldTitle = Title::makeTitle( $ns, $title );
541 $newTitle = Title::makeTitle( $ns, $this->charmap[$char] . mb_substr( $title, 1 ) );
542 if ( $op !== self::UPPERCASE && !$this->mungeTitle( $db, $oldTitle, $newTitle ) ) {
543 return false;
544 }
545
546 if ( $this->run ) {
547 $db->newUpdateQueryBuilder()
548 ->update( $table )
549 ->set( array_merge(
550 is_int( $nsField ) ? [] : [ $nsField => $newTitle->getNamespace() ],
551 [ $titleField => $newTitle->getDBkey() ]
552 ) )
553 ->where( (array)$row )
554 ->caller( __METHOD__ )
555 ->execute();
556 $r = json_encode( $row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
557 $this->output( "Set $r to {$newTitle->getPrefixedText()}\n" );
558 } else {
559 $r = json_encode( $row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
560 $this->output( "Would set $r to {$newTitle->getPrefixedText()}\n" );
561 }
562
563 return true;
564 }
565
579 private function processTable( IDatabase $db, $op, $table, $nsField, $titleField, $pkFields ) {
580 if ( $this->tables !== null && !in_array( $table, $this->tables, true ) ) {
581 $this->output( "Skipping table `$table`, not in --tables.\n" );
582 return;
583 }
584
585 $batchSize = $this->getBatchSize();
586 $namespaces = $this->getNamespaces();
587 $likes = $this->getLikeBatches( $db, $titleField );
588
589 if ( is_int( $nsField ) ) {
590 $namespaces = array_intersect( $namespaces, [ $nsField ] );
591 }
592
593 if ( !$namespaces ) {
594 $this->output( "Skipping table `$table`, no valid namespaces.\n" );
595 return;
596 }
597
598 $this->output( "Processing table `$table`...\n" );
599
600 $selectFields = array_merge(
601 is_int( $nsField ) ? [] : [ $nsField ],
602 [ $titleField ],
603 $pkFields
604 );
605 $contFields = array_merge( [ $titleField ], $pkFields );
606
607 $lastReplicationWait = 0.0;
608 $count = 0;
609 $errors = 0;
610 foreach ( $namespaces as $ns ) {
611 foreach ( $likes as $like ) {
612 $cont = [];
613 do {
614 $res = $db->newSelectQueryBuilder()
615 ->select( $selectFields )
616 ->from( $table )
617 ->where( [ "$nsField = $ns", $like, $cont ? $db->buildComparison( '>', $cont ) : '1=1' ] )
618 ->orderBy( array_merge( [ $titleField ], $pkFields ) )
619 ->limit( $batchSize )
620 ->caller( __METHOD__ )->fetchResultSet();
621 $cont = [];
622 foreach ( $res as $row ) {
623 $cont = [];
624 foreach ( $contFields as $field ) {
625 $cont[ $field ] = $row->$field;
626 }
627
628 if ( $op === self::MOVE ) {
629 $ns = is_int( $nsField ) ? $nsField : (int)$row->$nsField;
630 $ret = $this->doMove( $db, $ns, $row->$titleField );
631 } else {
632 $ret = $this->doUpdate( $db, $op, $table, $nsField, $titleField, $row );
633 }
634 if ( $ret === true ) {
635 $count++;
636 } elseif ( $ret === false ) {
637 $errors++;
638 }
639 }
640
641 if ( $this->run ) {
642 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable rows contains at least one item
643 $r = $cont ? json_encode( $row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) : '<end>';
644 $this->output( "... $table: $count renames, $errors errors at $r\n" );
645 $this->waitForReplication();
646 }
647 } while ( $cont );
648 }
649 }
650
651 $this->output( "Done processing table `$table`.\n" );
652 }
653
658 private function processUsers( IReadableDatabase $db ) {
659 $userlistFile = $this->getOption( 'userlist' );
660 if ( $userlistFile === null ) {
661 $this->output( "Not generating user list, --userlist was not specified.\n" );
662 return;
663 }
664
665 $fh = fopen( $userlistFile, 'ab' );
666 if ( !$fh ) {
667 $this->error( "Could not open user list file $userlistFile" );
668 return;
669 }
670
671 $this->output( "Generating user list...\n" );
672 $count = 0;
673 $batchSize = $this->getBatchSize();
674 foreach ( $this->getLikeBatches( $db, 'user_name' ) as $like ) {
675 $cont = [];
676 while ( true ) {
677 $rows = $db->newSelectQueryBuilder()
678 ->select( [ 'user_id', 'user_name' ] )
679 ->from( 'user' )
680 ->where( $like )
681 ->andWhere( $cont )
682 ->orderBy( 'user_name' )
683 ->limit( $batchSize )
684 ->caller( __METHOD__ )->fetchResultSet();
685
686 if ( !$rows->numRows() ) {
687 break;
688 }
689
690 foreach ( $rows as $row ) {
691 $char = mb_substr( $row->user_name, 0, 1 );
692 if ( !array_key_exists( $char, $this->charmap ) ) {
693 $this->error(
694 "Query returned $row->user_name, but user name does not " .
695 "begin with a character in the charmap."
696 );
697 continue;
698 }
699 $newName = $this->charmap[$char] . mb_substr( $row->user_name, 1 );
700 fprintf( $fh, "%s\t%s\t%s\n", WikiMap::getCurrentWikiId(), $row->user_id, $newName );
701 $count++;
702 $cont = [ $db->expr( 'user_name', '>', $row->user_name ) ];
703 }
704 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable rows contains at least one item
705 $this->output( "... at $row->user_name, $count names so far\n" );
706 }
707 }
708
709 if ( !fclose( $fh ) ) {
710 $this->error( "fclose on $userlistFile failed" );
711 }
712 $this->output( "User list output to $userlistFile, $count users need renaming.\n" );
713 }
714}
715
716$maintClass = UppercaseTitlesForUnicodeTransition::class;
717require_once RUN_MAINTENANCE_IF_MAIN;
const NS_USER
Definition Defines.php:66
const NS_FILE
Definition Defines.php:70
const NS_MAIN
Definition Defines.php:64
const NS_USER_TALK
Definition Defines.php:67
run()
Run the job.
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
error( $err, $die=0)
Throw an error to the user.
output( $out, $channel=null)
Throw some output to the user.
waitForReplication()
Wait for replica DBs to catch up.
getServiceContainer()
Returns the main service container.
getBatchSize()
Returns batch size.
addDescription( $text)
Set the description text.
addOption( $name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
getOption( $name, $default=null)
Get an option, or return the default.
setBatchSize( $s=0)
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.
Represents a title within MediaWiki.
Definition Title.php:78
canExist()
Can this title represent a page in the wiki's database?
Definition Title.php:1212
exists( $flags=0)
Check if page exists.
Definition Title.php:3204
getArticleID( $flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition Title.php:2593
getNamespace()
Get the namespace index, i.e.
Definition Title.php:1044
getDBkey()
Get the main part with underscores.
Definition Title.php:1035
getText()
Get the text form (spaces not underscores) of the main part.
Definition Title.php:1017
getPrefixedText()
Get the prefixed title with spaces.
Definition Title.php:1861
internal since 1.36
Definition User.php:93
isRegistered()
Get whether the user is registered.
Definition User.php:2234
Tools for dealing with other locally-hosted wikis.
Definition WikiMap.php:31
Maintenance script to rename titles affected by changes to Unicode (or otherwise to Language::ucfirst...
Content of like value.
Definition LikeValue.php:14
Representing a group of expressions chained via OR.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:36
newUpdateQueryBuilder()
Get an UpdateQueryBuilder bound to this connection.
A database connection without write operations.
newSelectQueryBuilder()
Create an empty SelectQueryBuilder which can be used to run queries against this connection.
expr(string $field, string $op, $value)
See Expression::__construct()
anyString()
Returns a token for buildLike() that denotes a '' to be used in a LIKE query.
buildComparison(string $op, array $conds)
Build a condition comparing multiple values, for use with indexes that cover multiple fields,...