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