MediaWiki REL1_29
Database.php
Go to the documentation of this file.
1<?php
26namespace Wikimedia\Rdbms;
27
28use Psr\Log\LoggerAwareInterface;
29use Psr\Log\LoggerInterface;
30use Wikimedia\ScopedCallback;
31use Wikimedia\Timestamp\ConvertibleTimestamp;
35use InvalidArgumentException;
36use Exception;
37use RuntimeException;
38
45abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAwareInterface {
47 const DEADLOCK_TRIES = 4;
49 const DEADLOCK_DELAY_MIN = 500000;
51 const DEADLOCK_DELAY_MAX = 1500000;
52
54 const PING_TTL = 1.0;
55 const PING_QUERY = 'SELECT 1 AS ping';
56
57 const TINY_WRITE_SEC = .010;
58 const SLOW_WRITE_SEC = .500;
59 const SMALL_WRITE_ROWS = 100;
60
62 protected $mLastQuery = '';
64 protected $mLastWriteTime = false;
66 protected $mPHPError = false;
68 protected $mServer;
70 protected $mUser;
72 protected $mPassword;
74 protected $mDBname;
76 protected $tableAliases = [];
78 protected $cliMode;
80 protected $agent;
81
83 protected $srvCache;
85 protected $connLogger;
87 protected $queryLogger;
89 protected $errorLogger;
90
92 protected $mConn = null;
94 protected $mOpened = false;
95
97 protected $mTrxIdleCallbacks = [];
101 protected $mTrxEndCallbacks = [];
106
108 protected $mTablePrefix = '';
110 protected $mSchema = '';
112 protected $mFlags;
114 protected $mLBInfo = [];
116 protected $mDefaultBigSelects = null;
118 protected $mSchemaVars = false;
120 protected $mSessionVars = [];
122 protected $preparedArgs;
124 protected $htmlErrors;
126 protected $delimiter = ';';
128 protected $currentDomain;
129
136 protected $mTrxLevel = 0;
143 protected $mTrxShortId = '';
152 private $mTrxTimestamp = null;
154 private $mTrxReplicaLag = null;
162 private $mTrxFname = null;
169 private $mTrxDoneWrites = false;
176 private $mTrxAutomatic = false;
182 private $mTrxAtomicLevels = [];
188 private $mTrxAutomaticAtomic = false;
194 private $mTrxWriteCallers = [];
198 private $mTrxWriteDuration = 0.0;
214 private $mRTTEstimate = 0.0;
215
217 private $mNamedLocksHeld = [];
219 protected $mSessionTempTables = [];
220
223
225 protected $lastPing = 0.0;
226
228 private $priorFlags = [];
229
231 protected $profiler;
233 protected $trxProfiler;
234
244 $server = $params['host'];
245 $user = $params['user'];
246 $password = $params['password'];
247 $dbName = $params['dbname'];
248
249 $this->mSchema = $params['schema'];
250 $this->mTablePrefix = $params['tablePrefix'];
251
252 $this->cliMode = $params['cliMode'];
253 // Agent name is added to SQL queries in a comment, so make sure it can't break out
254 $this->agent = str_replace( '/', '-', $params['agent'] );
255
256 $this->mFlags = $params['flags'];
257 if ( $this->mFlags & self::DBO_DEFAULT ) {
258 if ( $this->cliMode ) {
259 $this->mFlags &= ~self::DBO_TRX;
260 } else {
261 $this->mFlags |= self::DBO_TRX;
262 }
263 }
264
265 $this->mSessionVars = $params['variables'];
266
267 $this->srvCache = isset( $params['srvCache'] )
268 ? $params['srvCache']
269 : new HashBagOStuff();
270
271 $this->profiler = $params['profiler'];
272 $this->trxProfiler = $params['trxProfiler'];
273 $this->connLogger = $params['connLogger'];
274 $this->queryLogger = $params['queryLogger'];
275 $this->errorLogger = $params['errorLogger'];
276
277 // Set initial dummy domain until open() sets the final DB/prefix
278 $this->currentDomain = DatabaseDomain::newUnspecified();
279
280 if ( $user ) {
281 $this->open( $server, $user, $password, $dbName );
282 } elseif ( $this->requiresDatabaseUser() ) {
283 throw new InvalidArgumentException( "No database user provided." );
284 }
285
286 // Set the domain object after open() sets the relevant fields
287 if ( $this->mDBname != '' ) {
288 // Domains with server scope but a table prefix are not used by IDatabase classes
289 $this->currentDomain = new DatabaseDomain( $this->mDBname, null, $this->mTablePrefix );
290 }
291 }
292
334 final public static function factory( $dbType, $p = [] ) {
335 static $canonicalDBTypes = [
336 'mysql' => [ 'mysqli', 'mysql' ],
337 'postgres' => [],
338 'sqlite' => [],
339 'oracle' => [],
340 'mssql' => [],
341 ];
342 static $classAliases = [
343 'DatabaseMssql' => DatabaseMssql::class,
344 'DatabaseMysql' => DatabaseMysql::class,
345 'DatabaseMysqli' => DatabaseMysqli::class,
346 'DatabaseSqlite' => DatabaseSqlite::class,
347 'DatabasePostgres' => DatabasePostgres::class
348 ];
349
350 $driver = false;
351 $dbType = strtolower( $dbType );
352 if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
353 $possibleDrivers = $canonicalDBTypes[$dbType];
354 if ( !empty( $p['driver'] ) ) {
355 if ( in_array( $p['driver'], $possibleDrivers ) ) {
356 $driver = $p['driver'];
357 } else {
358 throw new InvalidArgumentException( __METHOD__ .
359 " type '$dbType' does not support driver '{$p['driver']}'" );
360 }
361 } else {
362 foreach ( $possibleDrivers as $posDriver ) {
363 if ( extension_loaded( $posDriver ) ) {
364 $driver = $posDriver;
365 break;
366 }
367 }
368 }
369 } else {
370 $driver = $dbType;
371 }
372
373 if ( $driver === false || $driver === '' ) {
374 throw new InvalidArgumentException( __METHOD__ .
375 " no viable database extension found for type '$dbType'" );
376 }
377
378 $class = 'Database' . ucfirst( $driver );
379 if ( isset( $classAliases[$class] ) ) {
380 $class = $classAliases[$class];
381 }
382
383 if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
384 // Resolve some defaults for b/c
385 $p['host'] = isset( $p['host'] ) ? $p['host'] : false;
386 $p['user'] = isset( $p['user'] ) ? $p['user'] : false;
387 $p['password'] = isset( $p['password'] ) ? $p['password'] : false;
388 $p['dbname'] = isset( $p['dbname'] ) ? $p['dbname'] : false;
389 $p['flags'] = isset( $p['flags'] ) ? $p['flags'] : 0;
390 $p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
391 $p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : '';
392 $p['schema'] = isset( $p['schema'] ) ? $p['schema'] : '';
393 $p['cliMode'] = isset( $p['cliMode'] ) ? $p['cliMode'] : ( PHP_SAPI === 'cli' );
394 $p['agent'] = isset( $p['agent'] ) ? $p['agent'] : '';
395 if ( !isset( $p['connLogger'] ) ) {
396 $p['connLogger'] = new \Psr\Log\NullLogger();
397 }
398 if ( !isset( $p['queryLogger'] ) ) {
399 $p['queryLogger'] = new \Psr\Log\NullLogger();
400 }
401 $p['profiler'] = isset( $p['profiler'] ) ? $p['profiler'] : null;
402 if ( !isset( $p['trxProfiler'] ) ) {
403 $p['trxProfiler'] = new TransactionProfiler();
404 }
405 if ( !isset( $p['errorLogger'] ) ) {
406 $p['errorLogger'] = function ( Exception $e ) {
407 trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
408 };
409 }
410
411 $conn = new $class( $p );
412 } else {
413 $conn = null;
414 }
415
416 return $conn;
417 }
418
419 public function setLogger( LoggerInterface $logger ) {
420 $this->queryLogger = $logger;
421 }
422
423 public function getServerInfo() {
424 return $this->getServerVersion();
425 }
426
427 public function bufferResults( $buffer = null ) {
428 $res = !$this->getFlag( self::DBO_NOBUFFER );
429 if ( $buffer !== null ) {
430 $buffer
431 ? $this->clearFlag( self::DBO_NOBUFFER )
432 : $this->setFlag( self::DBO_NOBUFFER );
433 }
434
435 return $res;
436 }
437
450 protected function ignoreErrors( $ignoreErrors = null ) {
451 $res = $this->getFlag( self::DBO_IGNORE );
452 if ( $ignoreErrors !== null ) {
453 $ignoreErrors
454 ? $this->setFlag( self::DBO_IGNORE )
455 : $this->clearFlag( self::DBO_IGNORE );
456 }
457
458 return $res;
459 }
460
461 public function trxLevel() {
462 return $this->mTrxLevel;
463 }
464
465 public function trxTimestamp() {
466 return $this->mTrxLevel ? $this->mTrxTimestamp : null;
467 }
468
469 public function tablePrefix( $prefix = null ) {
470 $old = $this->mTablePrefix;
471 if ( $prefix !== null ) {
472 $this->mTablePrefix = $prefix;
473 $this->currentDomain = ( $this->mDBname != '' )
474 ? new DatabaseDomain( $this->mDBname, null, $this->mTablePrefix )
476 }
477
478 return $old;
479 }
480
481 public function dbSchema( $schema = null ) {
482 $old = $this->mSchema;
483 if ( $schema !== null ) {
484 $this->mSchema = $schema;
485 }
486
487 return $old;
488 }
489
490 public function getLBInfo( $name = null ) {
491 if ( is_null( $name ) ) {
492 return $this->mLBInfo;
493 } else {
494 if ( array_key_exists( $name, $this->mLBInfo ) ) {
495 return $this->mLBInfo[$name];
496 } else {
497 return null;
498 }
499 }
500 }
501
502 public function setLBInfo( $name, $value = null ) {
503 if ( is_null( $value ) ) {
504 $this->mLBInfo = $name;
505 } else {
506 $this->mLBInfo[$name] = $value;
507 }
508 }
509
510 public function setLazyMasterHandle( IDatabase $conn ) {
511 $this->lazyMasterHandle = $conn;
512 }
513
519 protected function getLazyMasterHandle() {
521 }
522
523 public function implicitGroupby() {
524 return true;
525 }
526
527 public function implicitOrderby() {
528 return true;
529 }
530
531 public function lastQuery() {
532 return $this->mLastQuery;
533 }
534
535 public function doneWrites() {
536 return (bool)$this->mLastWriteTime;
537 }
538
539 public function lastDoneWrites() {
540 return $this->mLastWriteTime ?: false;
541 }
542
543 public function writesPending() {
544 return $this->mTrxLevel && $this->mTrxDoneWrites;
545 }
546
547 public function writesOrCallbacksPending() {
548 return $this->mTrxLevel && (
549 $this->mTrxDoneWrites || $this->mTrxIdleCallbacks || $this->mTrxPreCommitCallbacks
550 );
551 }
552
553 public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
554 if ( !$this->mTrxLevel ) {
555 return false;
556 } elseif ( !$this->mTrxDoneWrites ) {
557 return 0.0;
558 }
559
560 switch ( $type ) {
561 case self::ESTIMATE_DB_APPLY:
562 $this->ping( $rtt );
563 $rttAdjTotal = $this->mTrxWriteAdjQueryCount * $rtt;
564 $applyTime = max( $this->mTrxWriteAdjDuration - $rttAdjTotal, 0 );
565 // For omitted queries, make them count as something at least
566 $omitted = $this->mTrxWriteQueryCount - $this->mTrxWriteAdjQueryCount;
567 $applyTime += self::TINY_WRITE_SEC * $omitted;
568
569 return $applyTime;
570 default: // everything
572 }
573 }
574
575 public function pendingWriteCallers() {
576 return $this->mTrxLevel ? $this->mTrxWriteCallers : [];
577 }
578
579 protected function pendingWriteAndCallbackCallers() {
580 if ( !$this->mTrxLevel ) {
581 return [];
582 }
583
584 $fnames = $this->mTrxWriteCallers;
585 foreach ( [
586 $this->mTrxIdleCallbacks,
587 $this->mTrxPreCommitCallbacks,
588 $this->mTrxEndCallbacks
589 ] as $callbacks ) {
590 foreach ( $callbacks as $callback ) {
591 $fnames[] = $callback[1];
592 }
593 }
594
595 return $fnames;
596 }
597
598 public function isOpen() {
599 return $this->mOpened;
600 }
601
602 public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
603 if ( $remember === self::REMEMBER_PRIOR ) {
604 array_push( $this->priorFlags, $this->mFlags );
605 }
606 $this->mFlags |= $flag;
607 }
608
609 public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
610 if ( $remember === self::REMEMBER_PRIOR ) {
611 array_push( $this->priorFlags, $this->mFlags );
612 }
613 $this->mFlags &= ~$flag;
614 }
615
616 public function restoreFlags( $state = self::RESTORE_PRIOR ) {
617 if ( !$this->priorFlags ) {
618 return;
619 }
620
621 if ( $state === self::RESTORE_INITIAL ) {
622 $this->mFlags = reset( $this->priorFlags );
623 $this->priorFlags = [];
624 } else {
625 $this->mFlags = array_pop( $this->priorFlags );
626 }
627 }
628
629 public function getFlag( $flag ) {
630 return !!( $this->mFlags & $flag );
631 }
632
638 public function getProperty( $name ) {
639 return $this->$name;
640 }
641
642 public function getDomainID() {
643 return $this->currentDomain->getId();
644 }
645
646 final public function getWikiID() {
647 return $this->getDomainID();
648 }
649
657 abstract function indexInfo( $table, $index, $fname = __METHOD__ );
658
665 abstract function strencode( $s );
666
667 protected function installErrorHandler() {
668 $this->mPHPError = false;
669 $this->htmlErrors = ini_set( 'html_errors', '0' );
670 set_error_handler( [ $this, 'connectionErrorLogger' ] );
671 }
672
676 protected function restoreErrorHandler() {
677 restore_error_handler();
678 if ( $this->htmlErrors !== false ) {
679 ini_set( 'html_errors', $this->htmlErrors );
680 }
681
682 return $this->getLastPHPError();
683 }
684
688 protected function getLastPHPError() {
689 if ( $this->mPHPError ) {
690 $error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
691 $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
692
693 return $error;
694 }
695
696 return false;
697 }
698
705 public function connectionErrorLogger( $errno, $errstr ) {
706 $this->mPHPError = $errstr;
707 }
708
715 protected function getLogContext( array $extras = [] ) {
716 return array_merge(
717 [
718 'db_server' => $this->mServer,
719 'db_name' => $this->mDBname,
720 'db_user' => $this->mUser,
721 ],
722 $extras
723 );
724 }
725
726 public function close() {
727 if ( $this->mConn ) {
728 if ( $this->trxLevel() ) {
729 $this->commit( __METHOD__, self::FLUSHING_INTERNAL );
730 }
731
732 $closed = $this->closeConnection();
733 $this->mConn = false;
734 } elseif ( $this->mTrxIdleCallbacks || $this->mTrxEndCallbacks ) { // sanity
735 throw new RuntimeException( "Transaction callbacks still pending." );
736 } else {
737 $closed = true;
738 }
739 $this->mOpened = false;
740
741 return $closed;
742 }
743
749 protected function assertOpen() {
750 if ( !$this->isOpen() ) {
751 throw new DBUnexpectedError( $this, "DB connection was already closed." );
752 }
753 }
754
760 abstract protected function closeConnection();
761
762 public function reportConnectionError( $error = 'Unknown error' ) {
763 $myError = $this->lastError();
764 if ( $myError ) {
765 $error = $myError;
766 }
767
768 # New method
769 throw new DBConnectionError( $this, $error );
770 }
771
779 abstract protected function doQuery( $sql );
780
788 protected function isWriteQuery( $sql ) {
789 return !preg_match(
790 '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\‍(SELECT)\b/i', $sql );
791 }
792
797 protected function getQueryVerb( $sql ) {
798 return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
799 }
800
810 protected function isTransactableQuery( $sql ) {
811 return !in_array(
812 $this->getQueryVerb( $sql ),
813 [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET', 'CREATE', 'ALTER' ],
814 true
815 );
816 }
817
822 protected function registerTempTableOperation( $sql ) {
823 if ( preg_match(
824 '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
825 $sql,
827 ) ) {
828 $this->mSessionTempTables[$matches[1]] = 1;
829
830 return true;
831 } elseif ( preg_match(
832 '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
833 $sql,
835 ) ) {
836 unset( $this->mSessionTempTables[$matches[1]] );
837
838 return true;
839 } elseif ( preg_match(
840 '/^(?:INSERT\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+[`"\']?(\w+)[`"\']?/i',
841 $sql,
843 ) ) {
844 return isset( $this->mSessionTempTables[$matches[1]] );
845 }
846
847 return false;
848 }
849
850 public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
851 $priorWritesPending = $this->writesOrCallbacksPending();
852 $this->mLastQuery = $sql;
853
854 $isWrite = $this->isWriteQuery( $sql ) && !$this->registerTempTableOperation( $sql );
855 if ( $isWrite ) {
856 $reason = $this->getReadOnlyReason();
857 if ( $reason !== false ) {
858 throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
859 }
860 # Set a flag indicating that writes have been done
861 $this->mLastWriteTime = microtime( true );
862 }
863
864 // Add trace comment to the begin of the sql string, right after the operator.
865 // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
866 $commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
867
868 # Start implicit transactions that wrap the request if DBO_TRX is enabled
869 if ( !$this->mTrxLevel && $this->getFlag( self::DBO_TRX )
870 && $this->isTransactableQuery( $sql )
871 ) {
872 $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
873 $this->mTrxAutomatic = true;
874 }
875
876 # Keep track of whether the transaction has write queries pending
877 if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWrite ) {
878 $this->mTrxDoneWrites = true;
879 $this->trxProfiler->transactionWritingIn(
880 $this->mServer, $this->mDBname, $this->mTrxShortId );
881 }
882
883 if ( $this->getFlag( self::DBO_DEBUG ) ) {
884 $this->queryLogger->debug( "{$this->mDBname} {$commentedSql}" );
885 }
886
887 # Avoid fatals if close() was called
888 $this->assertOpen();
889
890 # Send the query to the server
891 $ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
892
893 # Try reconnecting if the connection was lost
894 if ( false === $ret && $this->wasErrorReissuable() ) {
895 $recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
896 # Stash the last error values before anything might clear them
897 $lastError = $this->lastError();
898 $lastErrno = $this->lastErrno();
899 # Update state tracking to reflect transaction loss due to disconnection
900 $this->handleSessionLoss();
901 if ( $this->reconnect() ) {
902 $msg = __METHOD__ . ": lost connection to {$this->getServer()}; reconnected";
903 $this->connLogger->warning( $msg );
904 $this->queryLogger->warning(
905 "$msg:\n" . ( new RuntimeException() )->getTraceAsString() );
906
907 if ( !$recoverable ) {
908 # Callers may catch the exception and continue to use the DB
909 $this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
910 } else {
911 # Should be safe to silently retry the query
912 $ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
913 }
914 } else {
915 $msg = __METHOD__ . ": lost connection to {$this->getServer()} permanently";
916 $this->connLogger->error( $msg );
917 }
918 }
919
920 if ( false === $ret ) {
921 # Deadlocks cause the entire transaction to abort, not just the statement.
922 # https://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
923 # https://www.postgresql.org/docs/9.1/static/explicit-locking.html
924 if ( $this->wasDeadlock() ) {
925 if ( $this->explicitTrxActive() || $priorWritesPending ) {
926 $tempIgnore = false; // not recoverable
927 }
928 # Update state tracking to reflect transaction loss
929 $this->handleSessionLoss();
930 }
931
932 $this->reportQueryError(
933 $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
934 }
935
936 $res = $this->resultObject( $ret );
937
938 return $res;
939 }
940
941 private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) {
942 $isMaster = !is_null( $this->getLBInfo( 'master' ) );
943 # generalizeSQL() will probably cut down the query to reasonable
944 # logging size most of the time. The substr is really just a sanity check.
945 if ( $isMaster ) {
946 $queryProf = 'query-m: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
947 } else {
948 $queryProf = 'query: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
949 }
950
951 # Include query transaction state
952 $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
953
954 $startTime = microtime( true );
955 if ( $this->profiler ) {
956 call_user_func( [ $this->profiler, 'profileIn' ], $queryProf );
957 }
958 $ret = $this->doQuery( $commentedSql );
959 if ( $this->profiler ) {
960 call_user_func( [ $this->profiler, 'profileOut' ], $queryProf );
961 }
962 $queryRuntime = max( microtime( true ) - $startTime, 0.0 );
963
964 unset( $queryProfSection ); // profile out (if set)
965
966 if ( $ret !== false ) {
967 $this->lastPing = $startTime;
968 if ( $isWrite && $this->mTrxLevel ) {
969 $this->updateTrxWriteQueryTime( $sql, $queryRuntime );
970 $this->mTrxWriteCallers[] = $fname;
971 }
972 }
973
974 if ( $sql === self::PING_QUERY ) {
975 $this->mRTTEstimate = $queryRuntime;
976 }
977
978 $this->trxProfiler->recordQueryCompletion(
979 $queryProf, $startTime, $isWrite, $this->affectedRows()
980 );
981 $this->queryLogger->debug( $sql, [
982 'method' => $fname,
983 'master' => $isMaster,
984 'runtime' => $queryRuntime,
985 ] );
986
987 return $ret;
988 }
989
1001 private function updateTrxWriteQueryTime( $sql, $runtime ) {
1002 // Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
1003 $indicativeOfReplicaRuntime = true;
1004 if ( $runtime > self::SLOW_WRITE_SEC ) {
1005 $verb = $this->getQueryVerb( $sql );
1006 // insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
1007 if ( $verb === 'INSERT' ) {
1008 $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS;
1009 } elseif ( $verb === 'REPLACE' ) {
1010 $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS / 2;
1011 }
1012 }
1013
1014 $this->mTrxWriteDuration += $runtime;
1015 $this->mTrxWriteQueryCount += 1;
1016 if ( $indicativeOfReplicaRuntime ) {
1017 $this->mTrxWriteAdjDuration += $runtime;
1018 $this->mTrxWriteAdjQueryCount += 1;
1019 }
1020 }
1021
1022 private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
1023 # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
1024 # Dropped connections also mean that named locks are automatically released.
1025 # Only allow error suppression in autocommit mode or when the lost transaction
1026 # didn't matter anyway (aside from DBO_TRX snapshot loss).
1027 if ( $this->mNamedLocksHeld ) {
1028 return false; // possible critical section violation
1029 } elseif ( $sql === 'COMMIT' ) {
1030 return !$priorWritesPending; // nothing written anyway? (T127428)
1031 } elseif ( $sql === 'ROLLBACK' ) {
1032 return true; // transaction lost...which is also what was requested :)
1033 } elseif ( $this->explicitTrxActive() ) {
1034 return false; // don't drop atomocity
1035 } elseif ( $priorWritesPending ) {
1036 return false; // prior writes lost from implicit transaction
1037 }
1038
1039 return true;
1040 }
1041
1042 private function handleSessionLoss() {
1043 $this->mTrxLevel = 0;
1044 $this->mTrxIdleCallbacks = []; // T67263
1045 $this->mTrxPreCommitCallbacks = []; // T67263
1046 $this->mSessionTempTables = [];
1047 $this->mNamedLocksHeld = [];
1048 try {
1049 // Handle callbacks in mTrxEndCallbacks
1050 $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
1051 $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
1052 return null;
1053 } catch ( Exception $e ) {
1054 // Already logged; move on...
1055 return $e;
1056 }
1057 }
1058
1059 public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
1060 if ( $this->ignoreErrors() || $tempIgnore ) {
1061 $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
1062 } else {
1063 $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
1064 $this->queryLogger->error(
1065 "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
1066 $this->getLogContext( [
1067 'method' => __METHOD__,
1068 'errno' => $errno,
1069 'error' => $error,
1070 'sql1line' => $sql1line,
1071 'fname' => $fname,
1072 ] )
1073 );
1074 $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
1075 throw new DBQueryError( $this, $error, $errno, $sql, $fname );
1076 }
1077 }
1078
1079 public function freeResult( $res ) {
1080 }
1081
1082 public function selectField(
1083 $table, $var, $cond = '', $fname = __METHOD__, $options = []
1084 ) {
1085 if ( $var === '*' ) { // sanity
1086 throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1087 }
1088
1089 if ( !is_array( $options ) ) {
1090 $options = [ $options ];
1091 }
1092
1093 $options['LIMIT'] = 1;
1094
1095 $res = $this->select( $table, $var, $cond, $fname, $options );
1096 if ( $res === false || !$this->numRows( $res ) ) {
1097 return false;
1098 }
1099
1100 $row = $this->fetchRow( $res );
1101
1102 if ( $row !== false ) {
1103 return reset( $row );
1104 } else {
1105 return false;
1106 }
1107 }
1108
1109 public function selectFieldValues(
1110 $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1111 ) {
1112 if ( $var === '*' ) { // sanity
1113 throw new DBUnexpectedError( $this, "Cannot use a * field" );
1114 } elseif ( !is_string( $var ) ) { // sanity
1115 throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1116 }
1117
1118 if ( !is_array( $options ) ) {
1119 $options = [ $options ];
1120 }
1121
1122 $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1123 if ( $res === false ) {
1124 return false;
1125 }
1126
1127 $values = [];
1128 foreach ( $res as $row ) {
1129 $values[] = $row->$var;
1130 }
1131
1132 return $values;
1133 }
1134
1144 protected function makeSelectOptions( $options ) {
1145 $preLimitTail = $postLimitTail = '';
1146 $startOpts = '';
1147
1148 $noKeyOptions = [];
1149
1150 foreach ( $options as $key => $option ) {
1151 if ( is_numeric( $key ) ) {
1152 $noKeyOptions[$option] = true;
1153 }
1154 }
1155
1156 $preLimitTail .= $this->makeGroupByWithHaving( $options );
1157
1158 $preLimitTail .= $this->makeOrderBy( $options );
1159
1160 if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1161 $postLimitTail .= ' FOR UPDATE';
1162 }
1163
1164 if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1165 $postLimitTail .= ' LOCK IN SHARE MODE';
1166 }
1167
1168 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1169 $startOpts .= 'DISTINCT';
1170 }
1171
1172 # Various MySQL extensions
1173 if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1174 $startOpts .= ' /*! STRAIGHT_JOIN */';
1175 }
1176
1177 if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
1178 $startOpts .= ' HIGH_PRIORITY';
1179 }
1180
1181 if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1182 $startOpts .= ' SQL_BIG_RESULT';
1183 }
1184
1185 if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1186 $startOpts .= ' SQL_BUFFER_RESULT';
1187 }
1188
1189 if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1190 $startOpts .= ' SQL_SMALL_RESULT';
1191 }
1192
1193 if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1194 $startOpts .= ' SQL_CALC_FOUND_ROWS';
1195 }
1196
1197 if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
1198 $startOpts .= ' SQL_CACHE';
1199 }
1200
1201 if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
1202 $startOpts .= ' SQL_NO_CACHE';
1203 }
1204
1205 if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1206 $useIndex = $this->useIndexClause( $options['USE INDEX'] );
1207 } else {
1208 $useIndex = '';
1209 }
1210 if ( isset( $options['IGNORE INDEX'] ) && is_string( $options['IGNORE INDEX'] ) ) {
1211 $ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
1212 } else {
1213 $ignoreIndex = '';
1214 }
1215
1216 return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1217 }
1218
1227 protected function makeGroupByWithHaving( $options ) {
1228 $sql = '';
1229 if ( isset( $options['GROUP BY'] ) ) {
1230 $gb = is_array( $options['GROUP BY'] )
1231 ? implode( ',', $options['GROUP BY'] )
1232 : $options['GROUP BY'];
1233 $sql .= ' GROUP BY ' . $gb;
1234 }
1235 if ( isset( $options['HAVING'] ) ) {
1236 $having = is_array( $options['HAVING'] )
1237 ? $this->makeList( $options['HAVING'], self::LIST_AND )
1238 : $options['HAVING'];
1239 $sql .= ' HAVING ' . $having;
1240 }
1241
1242 return $sql;
1243 }
1244
1253 protected function makeOrderBy( $options ) {
1254 if ( isset( $options['ORDER BY'] ) ) {
1255 $ob = is_array( $options['ORDER BY'] )
1256 ? implode( ',', $options['ORDER BY'] )
1257 : $options['ORDER BY'];
1258
1259 return ' ORDER BY ' . $ob;
1260 }
1261
1262 return '';
1263 }
1264
1265 public function select( $table, $vars, $conds = '', $fname = __METHOD__,
1266 $options = [], $join_conds = [] ) {
1267 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1268
1269 return $this->query( $sql, $fname );
1270 }
1271
1272 public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1273 $options = [], $join_conds = []
1274 ) {
1275 if ( is_array( $vars ) ) {
1276 $vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1277 }
1278
1280 $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1281 ? $options['USE INDEX']
1282 : [];
1283 $ignoreIndexes = (
1284 isset( $options['IGNORE INDEX'] ) &&
1285 is_array( $options['IGNORE INDEX'] )
1286 )
1287 ? $options['IGNORE INDEX']
1288 : [];
1289
1290 if ( is_array( $table ) ) {
1291 $from = ' FROM ' .
1293 $table, $useIndexes, $ignoreIndexes, $join_conds );
1294 } elseif ( $table != '' ) {
1295 if ( $table[0] == ' ' ) {
1296 $from = ' FROM ' . $table;
1297 } else {
1298 $from = ' FROM ' .
1300 [ $table ], $useIndexes, $ignoreIndexes, [] );
1301 }
1302 } else {
1303 $from = '';
1304 }
1305
1306 list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
1307 $this->makeSelectOptions( $options );
1308
1309 if ( !empty( $conds ) ) {
1310 if ( is_array( $conds ) ) {
1311 $conds = $this->makeList( $conds, self::LIST_AND );
1312 }
1313 $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex " .
1314 "WHERE $conds $preLimitTail";
1315 } else {
1316 $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
1317 }
1318
1319 if ( isset( $options['LIMIT'] ) ) {
1320 $sql = $this->limitResult( $sql, $options['LIMIT'],
1321 isset( $options['OFFSET'] ) ? $options['OFFSET'] : false );
1322 }
1323 $sql = "$sql $postLimitTail";
1324
1325 if ( isset( $options['EXPLAIN'] ) ) {
1326 $sql = 'EXPLAIN ' . $sql;
1327 }
1328
1329 return $sql;
1330 }
1331
1332 public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1333 $options = [], $join_conds = []
1334 ) {
1336 $options['LIMIT'] = 1;
1337 $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1338
1339 if ( $res === false ) {
1340 return false;
1341 }
1342
1343 if ( !$this->numRows( $res ) ) {
1344 return false;
1345 }
1346
1347 $obj = $this->fetchObject( $res );
1348
1349 return $obj;
1350 }
1351
1352 public function estimateRowCount(
1353 $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
1354 ) {
1355 $rows = 0;
1356 $res = $this->select( $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options );
1357
1358 if ( $res ) {
1359 $row = $this->fetchRow( $res );
1360 $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1361 }
1362
1363 return $rows;
1364 }
1365
1366 public function selectRowCount(
1367 $tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1368 ) {
1369 $rows = 0;
1370 $sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
1371 // The identifier quotes is primarily for MSSQL.
1372 $rowCountCol = $this->addIdentifierQuotes( "rowcount" );
1373 $tableName = $this->addIdentifierQuotes( "tmp_count" );
1374 $res = $this->query( "SELECT COUNT(*) AS $rowCountCol FROM ($sql) $tableName", $fname );
1375
1376 if ( $res ) {
1377 $row = $this->fetchRow( $res );
1378 $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1379 }
1380
1381 return $rows;
1382 }
1383
1392 protected static function generalizeSQL( $sql ) {
1393 # This does the same as the regexp below would do, but in such a way
1394 # as to avoid crashing php on some large strings.
1395 # $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
1396
1397 $sql = str_replace( "\\\\", '', $sql );
1398 $sql = str_replace( "\\'", '', $sql );
1399 $sql = str_replace( "\\\"", '', $sql );
1400 $sql = preg_replace( "/'.*'/s", "'X'", $sql );
1401 $sql = preg_replace( '/".*"/s', "'X'", $sql );
1402
1403 # All newlines, tabs, etc replaced by single space
1404 $sql = preg_replace( '/\s+/', ' ', $sql );
1405
1406 # All numbers => N,
1407 # except the ones surrounded by characters, e.g. l10n
1408 $sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
1409 $sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
1410
1411 return $sql;
1412 }
1413
1414 public function fieldExists( $table, $field, $fname = __METHOD__ ) {
1415 $info = $this->fieldInfo( $table, $field );
1416
1417 return (bool)$info;
1418 }
1419
1420 public function indexExists( $table, $index, $fname = __METHOD__ ) {
1421 if ( !$this->tableExists( $table ) ) {
1422 return null;
1423 }
1424
1425 $info = $this->indexInfo( $table, $index, $fname );
1426 if ( is_null( $info ) ) {
1427 return null;
1428 } else {
1429 return $info !== false;
1430 }
1431 }
1432
1433 public function tableExists( $table, $fname = __METHOD__ ) {
1434 $tableRaw = $this->tableName( $table, 'raw' );
1435 if ( isset( $this->mSessionTempTables[$tableRaw] ) ) {
1436 return true; // already known to exist
1437 }
1438
1439 $table = $this->tableName( $table );
1440 $ignoreErrors = true;
1441 $res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname, $ignoreErrors );
1442
1443 return (bool)$res;
1444 }
1445
1446 public function indexUnique( $table, $index ) {
1447 $indexInfo = $this->indexInfo( $table, $index );
1448
1449 if ( !$indexInfo ) {
1450 return null;
1451 }
1452
1453 return !$indexInfo[0]->Non_unique;
1454 }
1455
1462 protected function makeInsertOptions( $options ) {
1463 return implode( ' ', $options );
1464 }
1465
1466 public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
1467 # No rows to insert, easy just return now
1468 if ( !count( $a ) ) {
1469 return true;
1470 }
1471
1472 $table = $this->tableName( $table );
1473
1474 if ( !is_array( $options ) ) {
1475 $options = [ $options ];
1476 }
1477
1478 $fh = null;
1479 if ( isset( $options['fileHandle'] ) ) {
1480 $fh = $options['fileHandle'];
1481 }
1483
1484 if ( isset( $a[0] ) && is_array( $a[0] ) ) {
1485 $multi = true;
1486 $keys = array_keys( $a[0] );
1487 } else {
1488 $multi = false;
1489 $keys = array_keys( $a );
1490 }
1491
1492 $sql = 'INSERT ' . $options .
1493 " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
1494
1495 if ( $multi ) {
1496 $first = true;
1497 foreach ( $a as $row ) {
1498 if ( $first ) {
1499 $first = false;
1500 } else {
1501 $sql .= ',';
1502 }
1503 $sql .= '(' . $this->makeList( $row ) . ')';
1504 }
1505 } else {
1506 $sql .= '(' . $this->makeList( $a ) . ')';
1507 }
1508
1509 if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
1510 return false;
1511 } elseif ( $fh !== null ) {
1512 return true;
1513 }
1514
1515 return (bool)$this->query( $sql, $fname );
1516 }
1517
1524 protected function makeUpdateOptionsArray( $options ) {
1525 if ( !is_array( $options ) ) {
1526 $options = [ $options ];
1527 }
1528
1529 $opts = [];
1530
1531 if ( in_array( 'IGNORE', $options ) ) {
1532 $opts[] = 'IGNORE';
1533 }
1534
1535 return $opts;
1536 }
1537
1544 protected function makeUpdateOptions( $options ) {
1545 $opts = $this->makeUpdateOptionsArray( $options );
1546
1547 return implode( ' ', $opts );
1548 }
1549
1550 public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
1551 $table = $this->tableName( $table );
1552 $opts = $this->makeUpdateOptions( $options );
1553 $sql = "UPDATE $opts $table SET " . $this->makeList( $values, self::LIST_SET );
1554
1555 if ( $conds !== [] && $conds !== '*' ) {
1556 $sql .= " WHERE " . $this->makeList( $conds, self::LIST_AND );
1557 }
1558
1559 return (bool)$this->query( $sql, $fname );
1560 }
1561
1562 public function makeList( $a, $mode = self::LIST_COMMA ) {
1563 if ( !is_array( $a ) ) {
1564 throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
1565 }
1566
1567 $first = true;
1568 $list = '';
1569
1570 foreach ( $a as $field => $value ) {
1571 if ( !$first ) {
1572 if ( $mode == self::LIST_AND ) {
1573 $list .= ' AND ';
1574 } elseif ( $mode == self::LIST_OR ) {
1575 $list .= ' OR ';
1576 } else {
1577 $list .= ',';
1578 }
1579 } else {
1580 $first = false;
1581 }
1582
1583 if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
1584 $list .= "($value)";
1585 } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
1586 $list .= "$value";
1587 } elseif (
1588 ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
1589 ) {
1590 // Remove null from array to be handled separately if found
1591 $includeNull = false;
1592 foreach ( array_keys( $value, null, true ) as $nullKey ) {
1593 $includeNull = true;
1594 unset( $value[$nullKey] );
1595 }
1596 if ( count( $value ) == 0 && !$includeNull ) {
1597 throw new InvalidArgumentException(
1598 __METHOD__ . ": empty input for field $field" );
1599 } elseif ( count( $value ) == 0 ) {
1600 // only check if $field is null
1601 $list .= "$field IS NULL";
1602 } else {
1603 // IN clause contains at least one valid element
1604 if ( $includeNull ) {
1605 // Group subconditions to ensure correct precedence
1606 $list .= '(';
1607 }
1608 if ( count( $value ) == 1 ) {
1609 // Special-case single values, as IN isn't terribly efficient
1610 // Don't necessarily assume the single key is 0; we don't
1611 // enforce linear numeric ordering on other arrays here.
1612 $value = array_values( $value )[0];
1613 $list .= $field . " = " . $this->addQuotes( $value );
1614 } else {
1615 $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
1616 }
1617 // if null present in array, append IS NULL
1618 if ( $includeNull ) {
1619 $list .= " OR $field IS NULL)";
1620 }
1621 }
1622 } elseif ( $value === null ) {
1623 if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
1624 $list .= "$field IS ";
1625 } elseif ( $mode == self::LIST_SET ) {
1626 $list .= "$field = ";
1627 }
1628 $list .= 'NULL';
1629 } else {
1630 if (
1631 $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
1632 ) {
1633 $list .= "$field = ";
1634 }
1635 $list .= $mode == self::LIST_NAMES ? $value : $this->addQuotes( $value );
1636 }
1637 }
1638
1639 return $list;
1640 }
1641
1642 public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
1643 $conds = [];
1644
1645 foreach ( $data as $base => $sub ) {
1646 if ( count( $sub ) ) {
1647 $conds[] = $this->makeList(
1648 [ $baseKey => $base, $subKey => array_keys( $sub ) ],
1649 self::LIST_AND );
1650 }
1651 }
1652
1653 if ( $conds ) {
1654 return $this->makeList( $conds, self::LIST_OR );
1655 } else {
1656 // Nothing to search for...
1657 return false;
1658 }
1659 }
1660
1661 public function aggregateValue( $valuedata, $valuename = 'value' ) {
1662 return $valuename;
1663 }
1664
1665 public function bitNot( $field ) {
1666 return "(~$field)";
1667 }
1668
1669 public function bitAnd( $fieldLeft, $fieldRight ) {
1670 return "($fieldLeft & $fieldRight)";
1671 }
1672
1673 public function bitOr( $fieldLeft, $fieldRight ) {
1674 return "($fieldLeft | $fieldRight)";
1675 }
1676
1677 public function buildConcat( $stringList ) {
1678 return 'CONCAT(' . implode( ',', $stringList ) . ')';
1679 }
1680
1681 public function buildGroupConcatField(
1682 $delim, $table, $field, $conds = '', $join_conds = []
1683 ) {
1684 $fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
1685
1686 return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
1687 }
1688
1689 public function buildStringCast( $field ) {
1690 return $field;
1691 }
1692
1693 public function selectDB( $db ) {
1694 # Stub. Shouldn't cause serious problems if it's not overridden, but
1695 # if your database engine supports a concept similar to MySQL's
1696 # databases you may as well.
1697 $this->mDBname = $db;
1698
1699 return true;
1700 }
1701
1702 public function getDBname() {
1703 return $this->mDBname;
1704 }
1705
1706 public function getServer() {
1707 return $this->mServer;
1708 }
1709
1710 public function tableName( $name, $format = 'quoted' ) {
1711 # Skip the entire process when we have a string quoted on both ends.
1712 # Note that we check the end so that we will still quote any use of
1713 # use of `database`.table. But won't break things if someone wants
1714 # to query a database table with a dot in the name.
1715 if ( $this->isQuotedIdentifier( $name ) ) {
1716 return $name;
1717 }
1718
1719 # Lets test for any bits of text that should never show up in a table
1720 # name. Basically anything like JOIN or ON which are actually part of
1721 # SQL queries, but may end up inside of the table value to combine
1722 # sql. Such as how the API is doing.
1723 # Note that we use a whitespace test rather than a \b test to avoid
1724 # any remote case where a word like on may be inside of a table name
1725 # surrounded by symbols which may be considered word breaks.
1726 if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
1727 return $name;
1728 }
1729
1730 # Split database and table into proper variables.
1731 # We reverse the explode so that database.table and table both output
1732 # the correct table.
1733 $dbDetails = explode( '.', $name, 3 );
1734 if ( count( $dbDetails ) == 3 ) {
1735 list( $database, $schema, $table ) = $dbDetails;
1736 # We don't want any prefix added in this case
1737 $prefix = '';
1738 } elseif ( count( $dbDetails ) == 2 ) {
1739 list( $database, $table ) = $dbDetails;
1740 # We don't want any prefix added in this case
1741 $prefix = '';
1742 # In dbs that support it, $database may actually be the schema
1743 # but that doesn't affect any of the functionality here
1744 $schema = '';
1745 } else {
1746 list( $table ) = $dbDetails;
1747 if ( isset( $this->tableAliases[$table] ) ) {
1748 $database = $this->tableAliases[$table]['dbname'];
1749 $schema = is_string( $this->tableAliases[$table]['schema'] )
1750 ? $this->tableAliases[$table]['schema']
1752 $prefix = is_string( $this->tableAliases[$table]['prefix'] )
1753 ? $this->tableAliases[$table]['prefix']
1755 } else {
1756 $database = '';
1757 $schema = $this->mSchema; # Default schema
1758 $prefix = $this->mTablePrefix; # Default prefix
1759 }
1760 }
1761
1762 # Quote $table and apply the prefix if not quoted.
1763 # $tableName might be empty if this is called from Database::replaceVars()
1764 $tableName = "{$prefix}{$table}";
1765 if ( $format === 'quoted'
1766 && !$this->isQuotedIdentifier( $tableName )
1767 && $tableName !== ''
1768 ) {
1769 $tableName = $this->addIdentifierQuotes( $tableName );
1770 }
1771
1772 # Quote $schema and $database and merge them with the table name if needed
1773 $tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
1774 $tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
1775
1776 return $tableName;
1777 }
1778
1785 private function prependDatabaseOrSchema( $namespace, $relation, $format ) {
1786 if ( strlen( $namespace ) ) {
1787 if ( $format === 'quoted' && !$this->isQuotedIdentifier( $namespace ) ) {
1788 $namespace = $this->addIdentifierQuotes( $namespace );
1789 }
1790 $relation = $namespace . '.' . $relation;
1791 }
1792
1793 return $relation;
1794 }
1795
1796 public function tableNames() {
1797 $inArray = func_get_args();
1798 $retVal = [];
1799
1800 foreach ( $inArray as $name ) {
1801 $retVal[$name] = $this->tableName( $name );
1802 }
1803
1804 return $retVal;
1805 }
1806
1807 public function tableNamesN() {
1808 $inArray = func_get_args();
1809 $retVal = [];
1810
1811 foreach ( $inArray as $name ) {
1812 $retVal[] = $this->tableName( $name );
1813 }
1814
1815 return $retVal;
1816 }
1817
1826 protected function tableNameWithAlias( $name, $alias = false ) {
1827 if ( !$alias || $alias == $name ) {
1828 return $this->tableName( $name );
1829 } else {
1830 return $this->tableName( $name ) . ' ' . $this->addIdentifierQuotes( $alias );
1831 }
1832 }
1833
1840 protected function tableNamesWithAlias( $tables ) {
1841 $retval = [];
1842 foreach ( $tables as $alias => $table ) {
1843 if ( is_numeric( $alias ) ) {
1844 $alias = $table;
1845 }
1846 $retval[] = $this->tableNameWithAlias( $table, $alias );
1847 }
1848
1849 return $retval;
1850 }
1851
1860 protected function fieldNameWithAlias( $name, $alias = false ) {
1861 if ( !$alias || (string)$alias === (string)$name ) {
1862 return $name;
1863 } else {
1864 return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
1865 }
1866 }
1867
1874 protected function fieldNamesWithAlias( $fields ) {
1875 $retval = [];
1876 foreach ( $fields as $alias => $field ) {
1877 if ( is_numeric( $alias ) ) {
1878 $alias = $field;
1879 }
1880 $retval[] = $this->fieldNameWithAlias( $field, $alias );
1881 }
1882
1883 return $retval;
1884 }
1885
1897 $tables, $use_index = [], $ignore_index = [], $join_conds = []
1898 ) {
1899 $ret = [];
1900 $retJOIN = [];
1901 $use_index = (array)$use_index;
1902 $ignore_index = (array)$ignore_index;
1903 $join_conds = (array)$join_conds;
1904
1905 foreach ( $tables as $alias => $table ) {
1906 if ( !is_string( $alias ) ) {
1907 // No alias? Set it equal to the table name
1908 $alias = $table;
1909 }
1910 // Is there a JOIN clause for this table?
1911 if ( isset( $join_conds[$alias] ) ) {
1912 list( $joinType, $conds ) = $join_conds[$alias];
1913 $tableClause = $joinType;
1914 $tableClause .= ' ' . $this->tableNameWithAlias( $table, $alias );
1915 if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
1916 $use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
1917 if ( $use != '' ) {
1918 $tableClause .= ' ' . $use;
1919 }
1920 }
1921 if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
1922 $ignore = $this->ignoreIndexClause(
1923 implode( ',', (array)$ignore_index[$alias] ) );
1924 if ( $ignore != '' ) {
1925 $tableClause .= ' ' . $ignore;
1926 }
1927 }
1928 $on = $this->makeList( (array)$conds, self::LIST_AND );
1929 if ( $on != '' ) {
1930 $tableClause .= ' ON (' . $on . ')';
1931 }
1932
1933 $retJOIN[] = $tableClause;
1934 } elseif ( isset( $use_index[$alias] ) ) {
1935 // Is there an INDEX clause for this table?
1936 $tableClause = $this->tableNameWithAlias( $table, $alias );
1937 $tableClause .= ' ' . $this->useIndexClause(
1938 implode( ',', (array)$use_index[$alias] )
1939 );
1940
1941 $ret[] = $tableClause;
1942 } elseif ( isset( $ignore_index[$alias] ) ) {
1943 // Is there an INDEX clause for this table?
1944 $tableClause = $this->tableNameWithAlias( $table, $alias );
1945 $tableClause .= ' ' . $this->ignoreIndexClause(
1946 implode( ',', (array)$ignore_index[$alias] )
1947 );
1948
1949 $ret[] = $tableClause;
1950 } else {
1951 $tableClause = $this->tableNameWithAlias( $table, $alias );
1952
1953 $ret[] = $tableClause;
1954 }
1955 }
1956
1957 // We can't separate explicit JOIN clauses with ',', use ' ' for those
1958 $implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
1959 $explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
1960
1961 // Compile our final table clause
1962 return implode( ' ', [ $implicitJoins, $explicitJoins ] );
1963 }
1964
1971 protected function indexName( $index ) {
1972 return $index;
1973 }
1974
1975 public function addQuotes( $s ) {
1976 if ( $s instanceof Blob ) {
1977 $s = $s->fetch();
1978 }
1979 if ( $s === null ) {
1980 return 'NULL';
1981 } elseif ( is_bool( $s ) ) {
1982 return (int)$s;
1983 } else {
1984 # This will also quote numeric values. This should be harmless,
1985 # and protects against weird problems that occur when they really
1986 # _are_ strings such as article titles and string->number->string
1987 # conversion is not 1:1.
1988 return "'" . $this->strencode( $s ) . "'";
1989 }
1990 }
1991
2001 public function addIdentifierQuotes( $s ) {
2002 return '"' . str_replace( '"', '""', $s ) . '"';
2003 }
2004
2014 public function isQuotedIdentifier( $name ) {
2015 return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
2016 }
2017
2022 protected function escapeLikeInternal( $s, $escapeChar = '`' ) {
2023 return str_replace( [ $escapeChar, '%', '_' ],
2024 [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ],
2025 $s );
2026 }
2027
2028 public function buildLike() {
2029 $params = func_get_args();
2030
2031 if ( count( $params ) > 0 && is_array( $params[0] ) ) {
2032 $params = $params[0];
2033 }
2034
2035 $s = '';
2036
2037 // We use ` instead of \ as the default LIKE escape character, since addQuotes()
2038 // may escape backslashes, creating problems of double escaping. The `
2039 // character has good cross-DBMS compatibility, avoiding special operators
2040 // in MS SQL like ^ and %
2041 $escapeChar = '`';
2042
2043 foreach ( $params as $value ) {
2044 if ( $value instanceof LikeMatch ) {
2045 $s .= $value->toString();
2046 } else {
2047 $s .= $this->escapeLikeInternal( $value, $escapeChar );
2048 }
2049 }
2050
2051 return ' LIKE ' . $this->addQuotes( $s ) . ' ESCAPE ' . $this->addQuotes( $escapeChar ) . ' ';
2052 }
2053
2054 public function anyChar() {
2055 return new LikeMatch( '_' );
2056 }
2057
2058 public function anyString() {
2059 return new LikeMatch( '%' );
2060 }
2061
2062 public function nextSequenceValue( $seqName ) {
2063 return null;
2064 }
2065
2076 public function useIndexClause( $index ) {
2077 return '';
2078 }
2079
2090 public function ignoreIndexClause( $index ) {
2091 return '';
2092 }
2093
2094 public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2095 $quotedTable = $this->tableName( $table );
2096
2097 if ( count( $rows ) == 0 ) {
2098 return;
2099 }
2100
2101 # Single row case
2102 if ( !is_array( reset( $rows ) ) ) {
2103 $rows = [ $rows ];
2104 }
2105
2106 // @FXIME: this is not atomic, but a trx would break affectedRows()
2107 foreach ( $rows as $row ) {
2108 # Delete rows which collide
2109 if ( $uniqueIndexes ) {
2110 $sql = "DELETE FROM $quotedTable WHERE ";
2111 $first = true;
2112 foreach ( $uniqueIndexes as $index ) {
2113 if ( $first ) {
2114 $first = false;
2115 $sql .= '( ';
2116 } else {
2117 $sql .= ' ) OR ( ';
2118 }
2119 if ( is_array( $index ) ) {
2120 $first2 = true;
2121 foreach ( $index as $col ) {
2122 if ( $first2 ) {
2123 $first2 = false;
2124 } else {
2125 $sql .= ' AND ';
2126 }
2127 $sql .= $col . '=' . $this->addQuotes( $row[$col] );
2128 }
2129 } else {
2130 $sql .= $index . '=' . $this->addQuotes( $row[$index] );
2131 }
2132 }
2133 $sql .= ' )';
2134 $this->query( $sql, $fname );
2135 }
2136
2137 # Now insert the row
2138 $this->insert( $table, $row, $fname );
2139 }
2140 }
2141
2152 protected function nativeReplace( $table, $rows, $fname ) {
2153 $table = $this->tableName( $table );
2154
2155 # Single row case
2156 if ( !is_array( reset( $rows ) ) ) {
2157 $rows = [ $rows ];
2158 }
2159
2160 $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
2161 $first = true;
2162
2163 foreach ( $rows as $row ) {
2164 if ( $first ) {
2165 $first = false;
2166 } else {
2167 $sql .= ',';
2168 }
2169
2170 $sql .= '(' . $this->makeList( $row ) . ')';
2171 }
2172
2173 return $this->query( $sql, $fname );
2174 }
2175
2176 public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
2177 $fname = __METHOD__
2178 ) {
2179 if ( !count( $rows ) ) {
2180 return true; // nothing to do
2181 }
2182
2183 if ( !is_array( reset( $rows ) ) ) {
2184 $rows = [ $rows ];
2185 }
2186
2187 if ( count( $uniqueIndexes ) ) {
2188 $clauses = []; // list WHERE clauses that each identify a single row
2189 foreach ( $rows as $row ) {
2190 foreach ( $uniqueIndexes as $index ) {
2191 $index = is_array( $index ) ? $index : [ $index ]; // columns
2192 $rowKey = []; // unique key to this row
2193 foreach ( $index as $column ) {
2194 $rowKey[$column] = $row[$column];
2195 }
2196 $clauses[] = $this->makeList( $rowKey, self::LIST_AND );
2197 }
2198 }
2199 $where = [ $this->makeList( $clauses, self::LIST_OR ) ];
2200 } else {
2201 $where = false;
2202 }
2203
2204 $useTrx = !$this->mTrxLevel;
2205 if ( $useTrx ) {
2206 $this->begin( $fname, self::TRANSACTION_INTERNAL );
2207 }
2208 try {
2209 # Update any existing conflicting row(s)
2210 if ( $where !== false ) {
2211 $ok = $this->update( $table, $set, $where, $fname );
2212 } else {
2213 $ok = true;
2214 }
2215 # Now insert any non-conflicting row(s)
2216 $ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
2217 } catch ( Exception $e ) {
2218 if ( $useTrx ) {
2219 $this->rollback( $fname, self::FLUSHING_INTERNAL );
2220 }
2221 throw $e;
2222 }
2223 if ( $useTrx ) {
2224 $this->commit( $fname, self::FLUSHING_INTERNAL );
2225 }
2226
2227 return $ok;
2228 }
2229
2230 public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2231 $fname = __METHOD__
2232 ) {
2233 if ( !$conds ) {
2234 throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
2235 }
2236
2237 $delTable = $this->tableName( $delTable );
2238 $joinTable = $this->tableName( $joinTable );
2239 $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2240 if ( $conds != '*' ) {
2241 $sql .= 'WHERE ' . $this->makeList( $conds, self::LIST_AND );
2242 }
2243 $sql .= ')';
2244
2245 $this->query( $sql, $fname );
2246 }
2247
2248 public function textFieldSize( $table, $field ) {
2249 $table = $this->tableName( $table );
2250 $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
2251 $res = $this->query( $sql, __METHOD__ );
2252 $row = $this->fetchObject( $res );
2253
2254 $m = [];
2255
2256 if ( preg_match( '/\‍((.*)\‍)/', $row->Type, $m ) ) {
2257 $size = $m[1];
2258 } else {
2259 $size = -1;
2260 }
2261
2262 return $size;
2263 }
2264
2265 public function delete( $table, $conds, $fname = __METHOD__ ) {
2266 if ( !$conds ) {
2267 throw new DBUnexpectedError( $this, __METHOD__ . ' called with no conditions' );
2268 }
2269
2270 $table = $this->tableName( $table );
2271 $sql = "DELETE FROM $table";
2272
2273 if ( $conds != '*' ) {
2274 if ( is_array( $conds ) ) {
2275 $conds = $this->makeList( $conds, self::LIST_AND );
2276 }
2277 $sql .= ' WHERE ' . $conds;
2278 }
2279
2280 return $this->query( $sql, $fname );
2281 }
2282
2283 public function insertSelect(
2284 $destTable, $srcTable, $varMap, $conds,
2285 $fname = __METHOD__, $insertOptions = [], $selectOptions = []
2286 ) {
2287 if ( $this->cliMode ) {
2288 // For massive migrations with downtime, we don't want to select everything
2289 // into memory and OOM, so do all this native on the server side if possible.
2290 return $this->nativeInsertSelect(
2291 $destTable,
2292 $srcTable,
2293 $varMap,
2294 $conds,
2295 $fname,
2296 $insertOptions,
2297 $selectOptions
2298 );
2299 }
2300
2301 // For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
2302 // on only the master (without needing row-based-replication). It also makes it easy to
2303 // know how big the INSERT is going to be.
2304 $fields = [];
2305 foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
2306 $fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
2307 }
2308 $selectOptions[] = 'FOR UPDATE';
2309 $res = $this->select( $srcTable, implode( ',', $fields ), $conds, $fname, $selectOptions );
2310 if ( !$res ) {
2311 return false;
2312 }
2313
2314 $rows = [];
2315 foreach ( $res as $row ) {
2316 $rows[] = (array)$row;
2317 }
2318
2319 return $this->insert( $destTable, $rows, $fname, $insertOptions );
2320 }
2321
2322 protected function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
2323 $fname = __METHOD__,
2324 $insertOptions = [], $selectOptions = []
2325 ) {
2326 $destTable = $this->tableName( $destTable );
2327
2328 if ( !is_array( $insertOptions ) ) {
2329 $insertOptions = [ $insertOptions ];
2330 }
2331
2332 $insertOptions = $this->makeInsertOptions( $insertOptions );
2333
2334 if ( !is_array( $selectOptions ) ) {
2335 $selectOptions = [ $selectOptions ];
2336 }
2337
2338 list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) = $this->makeSelectOptions(
2339 $selectOptions );
2340
2341 if ( is_array( $srcTable ) ) {
2342 $srcTable = implode( ',', array_map( [ $this, 'tableName' ], $srcTable ) );
2343 } else {
2344 $srcTable = $this->tableName( $srcTable );
2345 }
2346
2347 $sql = "INSERT $insertOptions" .
2348 " INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
2349 " SELECT $startOpts " . implode( ',', $varMap ) .
2350 " FROM $srcTable $useIndex $ignoreIndex ";
2351
2352 if ( $conds != '*' ) {
2353 if ( is_array( $conds ) ) {
2354 $conds = $this->makeList( $conds, self::LIST_AND );
2355 }
2356 $sql .= " WHERE $conds";
2357 }
2358
2359 $sql .= " $tailOpts";
2360
2361 return $this->query( $sql, $fname );
2362 }
2363
2383 public function limitResult( $sql, $limit, $offset = false ) {
2384 if ( !is_numeric( $limit ) ) {
2385 throw new DBUnexpectedError( $this,
2386 "Invalid non-numeric limit passed to limitResult()\n" );
2387 }
2388
2389 return "$sql LIMIT "
2390 . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
2391 . "{$limit} ";
2392 }
2393
2394 public function unionSupportsOrderAndLimit() {
2395 return true; // True for almost every DB supported
2396 }
2397
2398 public function unionQueries( $sqls, $all ) {
2399 $glue = $all ? ') UNION ALL (' : ') UNION (';
2400
2401 return '(' . implode( $glue, $sqls ) . ')';
2402 }
2403
2404 public function conditional( $cond, $trueVal, $falseVal ) {
2405 if ( is_array( $cond ) ) {
2406 $cond = $this->makeList( $cond, self::LIST_AND );
2407 }
2408
2409 return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
2410 }
2411
2412 public function strreplace( $orig, $old, $new ) {
2413 return "REPLACE({$orig}, {$old}, {$new})";
2414 }
2415
2416 public function getServerUptime() {
2417 return 0;
2418 }
2419
2420 public function wasDeadlock() {
2421 return false;
2422 }
2423
2424 public function wasLockTimeout() {
2425 return false;
2426 }
2427
2428 public function wasErrorReissuable() {
2429 return false;
2430 }
2431
2432 public function wasReadOnlyError() {
2433 return false;
2434 }
2435
2442 public function wasConnectionError( $errno ) {
2443 return false;
2444 }
2445
2446 public function deadlockLoop() {
2447 $args = func_get_args();
2448 $function = array_shift( $args );
2449 $tries = self::DEADLOCK_TRIES;
2450
2451 $this->begin( __METHOD__ );
2452
2453 $retVal = null;
2455 $e = null;
2456 do {
2457 try {
2458 $retVal = call_user_func_array( $function, $args );
2459 break;
2460 } catch ( DBQueryError $e ) {
2461 if ( $this->wasDeadlock() ) {
2462 // Retry after a randomized delay
2463 usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
2464 } else {
2465 // Throw the error back up
2466 throw $e;
2467 }
2468 }
2469 } while ( --$tries > 0 );
2470
2471 if ( $tries <= 0 ) {
2472 // Too many deadlocks; give up
2473 $this->rollback( __METHOD__ );
2474 throw $e;
2475 } else {
2476 $this->commit( __METHOD__ );
2477
2478 return $retVal;
2479 }
2480 }
2481
2482 public function masterPosWait( DBMasterPos $pos, $timeout ) {
2483 # Real waits are implemented in the subclass.
2484 return 0;
2485 }
2486
2487 public function getReplicaPos() {
2488 # Stub
2489 return false;
2490 }
2491
2492 public function getMasterPos() {
2493 # Stub
2494 return false;
2495 }
2496
2497 public function serverIsReadOnly() {
2498 return false;
2499 }
2500
2501 final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
2502 if ( !$this->mTrxLevel ) {
2503 throw new DBUnexpectedError( $this, "No transaction is active." );
2504 }
2505 $this->mTrxEndCallbacks[] = [ $callback, $fname ];
2506 }
2507
2508 final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
2509 $this->mTrxIdleCallbacks[] = [ $callback, $fname ];
2510 if ( !$this->mTrxLevel ) {
2511 $this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
2512 }
2513 }
2514
2515 final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
2516 if ( $this->mTrxLevel ) {
2517 $this->mTrxPreCommitCallbacks[] = [ $callback, $fname ];
2518 } else {
2519 // If no transaction is active, then make one for this callback
2520 $this->startAtomic( __METHOD__ );
2521 try {
2522 call_user_func( $callback );
2523 $this->endAtomic( __METHOD__ );
2524 } catch ( Exception $e ) {
2525 $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
2526 throw $e;
2527 }
2528 }
2529 }
2530
2531 final public function setTransactionListener( $name, callable $callback = null ) {
2532 if ( $callback ) {
2533 $this->mTrxRecurringCallbacks[$name] = $callback;
2534 } else {
2535 unset( $this->mTrxRecurringCallbacks[$name] );
2536 }
2537 }
2538
2547 final public function setTrxEndCallbackSuppression( $suppress ) {
2548 $this->mTrxEndCallbacksSuppressed = $suppress;
2549 }
2550
2560 public function runOnTransactionIdleCallbacks( $trigger ) {
2561 if ( $this->mTrxEndCallbacksSuppressed ) {
2562 return;
2563 }
2564
2565 $autoTrx = $this->getFlag( self::DBO_TRX ); // automatic begin() enabled?
2567 $e = null; // first exception
2568 do { // callbacks may add callbacks :)
2569 $callbacks = array_merge(
2570 $this->mTrxIdleCallbacks,
2571 $this->mTrxEndCallbacks // include "transaction resolution" callbacks
2572 );
2573 $this->mTrxIdleCallbacks = []; // consumed (and recursion guard)
2574 $this->mTrxEndCallbacks = []; // consumed (recursion guard)
2575 foreach ( $callbacks as $callback ) {
2576 try {
2577 list( $phpCallback ) = $callback;
2578 $this->clearFlag( self::DBO_TRX ); // make each query its own transaction
2579 call_user_func_array( $phpCallback, [ $trigger ] );
2580 if ( $autoTrx ) {
2581 $this->setFlag( self::DBO_TRX ); // restore automatic begin()
2582 } else {
2583 $this->clearFlag( self::DBO_TRX ); // restore auto-commit
2584 }
2585 } catch ( Exception $ex ) {
2586 call_user_func( $this->errorLogger, $ex );
2587 $e = $e ?: $ex;
2588 // Some callbacks may use startAtomic/endAtomic, so make sure
2589 // their transactions are ended so other callbacks don't fail
2590 if ( $this->trxLevel() ) {
2591 $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
2592 }
2593 }
2594 }
2595 } while ( count( $this->mTrxIdleCallbacks ) );
2596
2597 if ( $e instanceof Exception ) {
2598 throw $e; // re-throw any first exception
2599 }
2600 }
2601
2611 $e = null; // first exception
2612 do { // callbacks may add callbacks :)
2613 $callbacks = $this->mTrxPreCommitCallbacks;
2614 $this->mTrxPreCommitCallbacks = []; // consumed (and recursion guard)
2615 foreach ( $callbacks as $callback ) {
2616 try {
2617 list( $phpCallback ) = $callback;
2618 call_user_func( $phpCallback );
2619 } catch ( Exception $ex ) {
2620 call_user_func( $this->errorLogger, $ex );
2621 $e = $e ?: $ex;
2622 }
2623 }
2624 } while ( count( $this->mTrxPreCommitCallbacks ) );
2625
2626 if ( $e instanceof Exception ) {
2627 throw $e; // re-throw any first exception
2628 }
2629 }
2630
2640 public function runTransactionListenerCallbacks( $trigger ) {
2641 if ( $this->mTrxEndCallbacksSuppressed ) {
2642 return;
2643 }
2644
2646 $e = null; // first exception
2647
2648 foreach ( $this->mTrxRecurringCallbacks as $phpCallback ) {
2649 try {
2650 $phpCallback( $trigger, $this );
2651 } catch ( Exception $ex ) {
2652 call_user_func( $this->errorLogger, $ex );
2653 $e = $e ?: $ex;
2654 }
2655 }
2656
2657 if ( $e instanceof Exception ) {
2658 throw $e; // re-throw any first exception
2659 }
2660 }
2661
2662 final public function startAtomic( $fname = __METHOD__ ) {
2663 if ( !$this->mTrxLevel ) {
2664 $this->begin( $fname, self::TRANSACTION_INTERNAL );
2665 // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
2666 // in all changes being in one transaction to keep requests transactional.
2667 if ( !$this->getFlag( self::DBO_TRX ) ) {
2668 $this->mTrxAutomaticAtomic = true;
2669 }
2670 }
2671
2672 $this->mTrxAtomicLevels[] = $fname;
2673 }
2674
2675 final public function endAtomic( $fname = __METHOD__ ) {
2676 if ( !$this->mTrxLevel ) {
2677 throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." );
2678 }
2679 if ( !$this->mTrxAtomicLevels ||
2680 array_pop( $this->mTrxAtomicLevels ) !== $fname
2681 ) {
2682 throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." );
2683 }
2684
2685 if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
2686 $this->commit( $fname, self::FLUSHING_INTERNAL );
2687 }
2688 }
2689
2690 final public function doAtomicSection( $fname, callable $callback ) {
2691 $this->startAtomic( $fname );
2692 try {
2693 $res = call_user_func_array( $callback, [ $this, $fname ] );
2694 } catch ( Exception $e ) {
2695 $this->rollback( $fname, self::FLUSHING_INTERNAL );
2696 throw $e;
2697 }
2698 $this->endAtomic( $fname );
2699
2700 return $res;
2701 }
2702
2703 final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
2704 // Protect against mismatched atomic section, transaction nesting, and snapshot loss
2705 if ( $this->mTrxLevel ) {
2706 if ( $this->mTrxAtomicLevels ) {
2707 $levels = implode( ', ', $this->mTrxAtomicLevels );
2708 $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
2709 throw new DBUnexpectedError( $this, $msg );
2710 } elseif ( !$this->mTrxAutomatic ) {
2711 $msg = "$fname: Explicit transaction already active (from {$this->mTrxFname}).";
2712 throw new DBUnexpectedError( $this, $msg );
2713 } else {
2714 // @TODO: make this an exception at some point
2715 $msg = "$fname: Implicit transaction already active (from {$this->mTrxFname}).";
2716 $this->queryLogger->error( $msg );
2717 return; // join the main transaction set
2718 }
2719 } elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
2720 // @TODO: make this an exception at some point
2721 $msg = "$fname: Implicit transaction expected (DBO_TRX set).";
2722 $this->queryLogger->error( $msg );
2723 return; // let any writes be in the main transaction
2724 }
2725
2726 // Avoid fatals if close() was called
2727 $this->assertOpen();
2728
2729 $this->doBegin( $fname );
2730 $this->mTrxTimestamp = microtime( true );
2731 $this->mTrxFname = $fname;
2732 $this->mTrxDoneWrites = false;
2733 $this->mTrxAutomaticAtomic = false;
2734 $this->mTrxAtomicLevels = [];
2735 $this->mTrxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
2736 $this->mTrxWriteDuration = 0.0;
2737 $this->mTrxWriteQueryCount = 0;
2738 $this->mTrxWriteAdjDuration = 0.0;
2739 $this->mTrxWriteAdjQueryCount = 0;
2740 $this->mTrxWriteCallers = [];
2741 // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
2742 // Get an estimate of the replica DB lag before then, treating estimate staleness
2743 // as lag itself just to be safe
2745 $this->mTrxReplicaLag = $status['lag'] + ( microtime( true ) - $status['since'] );
2746 // T147697: make explicitTrxActive() return true until begin() finishes. This way, no
2747 // caller will think its OK to muck around with the transaction just because startAtomic()
2748 // has not yet completed (e.g. setting mTrxAtomicLevels).
2749 $this->mTrxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
2750 }
2751
2758 protected function doBegin( $fname ) {
2759 $this->query( 'BEGIN', $fname );
2760 $this->mTrxLevel = 1;
2761 }
2762
2763 final public function commit( $fname = __METHOD__, $flush = '' ) {
2764 if ( $this->mTrxLevel && $this->mTrxAtomicLevels ) {
2765 // There are still atomic sections open. This cannot be ignored
2766 $levels = implode( ', ', $this->mTrxAtomicLevels );
2767 throw new DBUnexpectedError(
2768 $this,
2769 "$fname: Got COMMIT while atomic sections $levels are still open."
2770 );
2771 }
2772
2773 if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
2774 if ( !$this->mTrxLevel ) {
2775 return; // nothing to do
2776 } elseif ( !$this->mTrxAutomatic ) {
2777 throw new DBUnexpectedError(
2778 $this,
2779 "$fname: Flushing an explicit transaction, getting out of sync."
2780 );
2781 }
2782 } else {
2783 if ( !$this->mTrxLevel ) {
2784 $this->queryLogger->error(
2785 "$fname: No transaction to commit, something got out of sync." );
2786 return; // nothing to do
2787 } elseif ( $this->mTrxAutomatic ) {
2788 // @TODO: make this an exception at some point
2789 $msg = "$fname: Explicit commit of implicit transaction.";
2790 $this->queryLogger->error( $msg );
2791 return; // wait for the main transaction set commit round
2792 }
2793 }
2794
2795 // Avoid fatals if close() was called
2796 $this->assertOpen();
2797
2799 $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
2800 $this->doCommit( $fname );
2801 if ( $this->mTrxDoneWrites ) {
2802 $this->mLastWriteTime = microtime( true );
2803 $this->trxProfiler->transactionWritingOut(
2804 $this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime );
2805 }
2806
2807 $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
2808 $this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
2809 }
2810
2817 protected function doCommit( $fname ) {
2818 if ( $this->mTrxLevel ) {
2819 $this->query( 'COMMIT', $fname );
2820 $this->mTrxLevel = 0;
2821 }
2822 }
2823
2824 final public function rollback( $fname = __METHOD__, $flush = '' ) {
2825 if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
2826 if ( !$this->mTrxLevel ) {
2827 return; // nothing to do
2828 }
2829 } else {
2830 if ( !$this->mTrxLevel ) {
2831 $this->queryLogger->error(
2832 "$fname: No transaction to rollback, something got out of sync." );
2833 return; // nothing to do
2834 } elseif ( $this->getFlag( self::DBO_TRX ) ) {
2835 throw new DBUnexpectedError(
2836 $this,
2837 "$fname: Expected mass rollback of all peer databases (DBO_TRX set)."
2838 );
2839 }
2840 }
2841
2842 // Avoid fatals if close() was called
2843 $this->assertOpen();
2844
2845 $this->doRollback( $fname );
2846 $this->mTrxAtomicLevels = [];
2847 if ( $this->mTrxDoneWrites ) {
2848 $this->trxProfiler->transactionWritingOut(
2849 $this->mServer, $this->mDBname, $this->mTrxShortId );
2850 }
2851
2852 $this->mTrxIdleCallbacks = []; // clear
2853 $this->mTrxPreCommitCallbacks = []; // clear
2854 $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
2855 $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
2856 }
2857
2864 protected function doRollback( $fname ) {
2865 if ( $this->mTrxLevel ) {
2866 # Disconnects cause rollback anyway, so ignore those errors
2867 $ignoreErrors = true;
2868 $this->query( 'ROLLBACK', $fname, $ignoreErrors );
2869 $this->mTrxLevel = 0;
2870 }
2871 }
2872
2873 public function flushSnapshot( $fname = __METHOD__ ) {
2874 if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
2875 // This only flushes transactions to clear snapshots, not to write data
2876 $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
2877 throw new DBUnexpectedError(
2878 $this,
2879 "$fname: Cannot flush snapshot because writes are pending ($fnames)."
2880 );
2881 }
2882
2883 $this->commit( $fname, self::FLUSHING_INTERNAL );
2884 }
2885
2886 public function explicitTrxActive() {
2887 return $this->mTrxLevel && ( $this->mTrxAtomicLevels || !$this->mTrxAutomatic );
2888 }
2889
2891 $oldName, $newName, $temporary = false, $fname = __METHOD__
2892 ) {
2893 throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
2894 }
2895
2896 public function listTables( $prefix = null, $fname = __METHOD__ ) {
2897 throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
2898 }
2899
2900 public function listViews( $prefix = null, $fname = __METHOD__ ) {
2901 throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
2902 }
2903
2904 public function timestamp( $ts = 0 ) {
2905 $t = new ConvertibleTimestamp( $ts );
2906 // Let errors bubble up to avoid putting garbage in the DB
2907 return $t->getTimestamp( TS_MW );
2908 }
2909
2910 public function timestampOrNull( $ts = null ) {
2911 if ( is_null( $ts ) ) {
2912 return null;
2913 } else {
2914 return $this->timestamp( $ts );
2915 }
2916 }
2917
2931 protected function resultObject( $result ) {
2932 if ( !$result ) {
2933 return false;
2934 } elseif ( $result instanceof ResultWrapper ) {
2935 return $result;
2936 } elseif ( $result === true ) {
2937 // Successful write query
2938 return $result;
2939 } else {
2940 return new ResultWrapper( $this, $result );
2941 }
2942 }
2943
2944 public function ping( &$rtt = null ) {
2945 // Avoid hitting the server if it was hit recently
2946 if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
2947 if ( !func_num_args() || $this->mRTTEstimate > 0 ) {
2948 $rtt = $this->mRTTEstimate;
2949 return true; // don't care about $rtt
2950 }
2951 }
2952
2953 // This will reconnect if possible or return false if not
2954 $this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
2955 $ok = ( $this->query( self::PING_QUERY, __METHOD__, true ) !== false );
2956 $this->restoreFlags( self::RESTORE_PRIOR );
2957
2958 if ( $ok ) {
2959 $rtt = $this->mRTTEstimate;
2960 }
2961
2962 return $ok;
2963 }
2964
2968 protected function reconnect() {
2969 $this->closeConnection();
2970 $this->mOpened = false;
2971 $this->mConn = false;
2972 try {
2973 $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
2974 $this->lastPing = microtime( true );
2975 $ok = true;
2976 } catch ( DBConnectionError $e ) {
2977 $ok = false;
2978 }
2979
2980 return $ok;
2981 }
2982
2983 public function getSessionLagStatus() {
2984 return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
2985 }
2986
2998 protected function getTransactionLagStatus() {
2999 return $this->mTrxLevel
3000 ? [ 'lag' => $this->mTrxReplicaLag, 'since' => $this->trxTimestamp() ]
3001 : null;
3002 }
3003
3010 protected function getApproximateLagStatus() {
3011 return [
3012 'lag' => $this->getLBInfo( 'replica' ) ? $this->getLag() : 0,
3013 'since' => microtime( true )
3014 ];
3015 }
3016
3035 public static function getCacheSetOptions( IDatabase $db1 ) {
3036 $res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
3037 foreach ( func_get_args() as $db ) {
3039 $status = $db->getSessionLagStatus();
3040 if ( $status['lag'] === false ) {
3041 $res['lag'] = false;
3042 } elseif ( $res['lag'] !== false ) {
3043 $res['lag'] = max( $res['lag'], $status['lag'] );
3044 }
3045 $res['since'] = min( $res['since'], $status['since'] );
3046 $res['pending'] = $res['pending'] ?: $db->writesPending();
3047 }
3048
3049 return $res;
3050 }
3051
3052 public function getLag() {
3053 return 0;
3054 }
3055
3056 public function maxListLen() {
3057 return 0;
3058 }
3059
3060 public function encodeBlob( $b ) {
3061 return $b;
3062 }
3063
3064 public function decodeBlob( $b ) {
3065 if ( $b instanceof Blob ) {
3066 $b = $b->fetch();
3067 }
3068 return $b;
3069 }
3070
3071 public function setSessionOptions( array $options ) {
3072 }
3073
3074 public function sourceFile(
3075 $filename,
3076 callable $lineCallback = null,
3077 callable $resultCallback = null,
3078 $fname = false,
3079 callable $inputCallback = null
3080 ) {
3081 MediaWiki\suppressWarnings();
3082 $fp = fopen( $filename, 'r' );
3083 MediaWiki\restoreWarnings();
3084
3085 if ( false === $fp ) {
3086 throw new RuntimeException( "Could not open \"{$filename}\".\n" );
3087 }
3088
3089 if ( !$fname ) {
3090 $fname = __METHOD__ . "( $filename )";
3091 }
3092
3093 try {
3094 $error = $this->sourceStream(
3095 $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
3096 } catch ( Exception $e ) {
3097 fclose( $fp );
3098 throw $e;
3099 }
3100
3101 fclose( $fp );
3102
3103 return $error;
3104 }
3105
3106 public function setSchemaVars( $vars ) {
3107 $this->mSchemaVars = $vars;
3108 }
3109
3110 public function sourceStream(
3111 $fp,
3112 callable $lineCallback = null,
3113 callable $resultCallback = null,
3114 $fname = __METHOD__,
3115 callable $inputCallback = null
3116 ) {
3117 $cmd = '';
3118
3119 while ( !feof( $fp ) ) {
3120 if ( $lineCallback ) {
3121 call_user_func( $lineCallback );
3122 }
3123
3124 $line = trim( fgets( $fp ) );
3125
3126 if ( $line == '' ) {
3127 continue;
3128 }
3129
3130 if ( '-' == $line[0] && '-' == $line[1] ) {
3131 continue;
3132 }
3133
3134 if ( $cmd != '' ) {
3135 $cmd .= ' ';
3136 }
3137
3138 $done = $this->streamStatementEnd( $cmd, $line );
3139
3140 $cmd .= "$line\n";
3141
3142 if ( $done || feof( $fp ) ) {
3143 $cmd = $this->replaceVars( $cmd );
3144
3145 if ( !$inputCallback || call_user_func( $inputCallback, $cmd ) ) {
3146 $res = $this->query( $cmd, $fname );
3147
3148 if ( $resultCallback ) {
3149 call_user_func( $resultCallback, $res, $this );
3150 }
3151
3152 if ( false === $res ) {
3153 $err = $this->lastError();
3154
3155 return "Query \"{$cmd}\" failed with error code \"$err\".\n";
3156 }
3157 }
3158 $cmd = '';
3159 }
3160 }
3161
3162 return true;
3163 }
3164
3172 public function streamStatementEnd( &$sql, &$newLine ) {
3173 if ( $this->delimiter ) {
3174 $prev = $newLine;
3175 $newLine = preg_replace(
3176 '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
3177 if ( $newLine != $prev ) {
3178 return true;
3179 }
3180 }
3181
3182 return false;
3183 }
3184
3205 protected function replaceVars( $ins ) {
3206 $vars = $this->getSchemaVars();
3207 return preg_replace_callback(
3208 '!
3209 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
3210 \'\{\$ (\w+) }\' | # 3. addQuotes
3211 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
3212 /\*\$ (\w+) \*/ # 5. leave unencoded
3213 !x',
3214 function ( $m ) use ( $vars ) {
3215 // Note: Because of <https://bugs.php.net/bug.php?id=51881>,
3216 // check for both nonexistent keys *and* the empty string.
3217 if ( isset( $m[1] ) && $m[1] !== '' ) {
3218 if ( $m[1] === 'i' ) {
3219 return $this->indexName( $m[2] );
3220 } else {
3221 return $this->tableName( $m[2] );
3222 }
3223 } elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
3224 return $this->addQuotes( $vars[$m[3]] );
3225 } elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
3226 return $this->addIdentifierQuotes( $vars[$m[4]] );
3227 } elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
3228 return $vars[$m[5]];
3229 } else {
3230 return $m[0];
3231 }
3232 },
3233 $ins
3234 );
3235 }
3236
3243 protected function getSchemaVars() {
3244 if ( $this->mSchemaVars ) {
3245 return $this->mSchemaVars;
3246 } else {
3247 return $this->getDefaultSchemaVars();
3248 }
3249 }
3250
3259 protected function getDefaultSchemaVars() {
3260 return [];
3261 }
3262
3263 public function lockIsFree( $lockName, $method ) {
3264 return true;
3265 }
3266
3267 public function lock( $lockName, $method, $timeout = 5 ) {
3268 $this->mNamedLocksHeld[$lockName] = 1;
3269
3270 return true;
3271 }
3272
3273 public function unlock( $lockName, $method ) {
3274 unset( $this->mNamedLocksHeld[$lockName] );
3275
3276 return true;
3277 }
3278
3279 public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
3280 if ( $this->writesOrCallbacksPending() ) {
3281 // This only flushes transactions to clear snapshots, not to write data
3282 $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
3283 throw new DBUnexpectedError(
3284 $this,
3285 "$fname: Cannot flush pre-lock snapshot because writes are pending ($fnames)."
3286 );
3287 }
3288
3289 if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
3290 return null;
3291 }
3292
3293 $unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
3294 if ( $this->trxLevel() ) {
3295 // There is a good chance an exception was thrown, causing any early return
3296 // from the caller. Let any error handler get a chance to issue rollback().
3297 // If there isn't one, let the error bubble up and trigger server-side rollback.
3299 function () use ( $lockKey, $fname ) {
3300 $this->unlock( $lockKey, $fname );
3301 },
3302 $fname
3303 );
3304 } else {
3305 $this->unlock( $lockKey, $fname );
3306 }
3307 } );
3308
3309 $this->commit( $fname, self::FLUSHING_INTERNAL );
3310
3311 return $unlocker;
3312 }
3313
3314 public function namedLocksEnqueue() {
3315 return false;
3316 }
3317
3319 return true;
3320 }
3321
3322 final public function lockTables( array $read, array $write, $method ) {
3323 if ( $this->writesOrCallbacksPending() ) {
3324 throw new DBUnexpectedError( $this, "Transaction writes or callbacks still pending." );
3325 }
3326
3327 if ( $this->tableLocksHaveTransactionScope() ) {
3328 $this->startAtomic( $method );
3329 }
3330
3331 return $this->doLockTables( $read, $write, $method );
3332 }
3333
3334 protected function doLockTables( array $read, array $write, $method ) {
3335 return true;
3336 }
3337
3338 final public function unlockTables( $method ) {
3339 if ( $this->tableLocksHaveTransactionScope() ) {
3340 $this->endAtomic( $method );
3341
3342 return true; // locks released on COMMIT/ROLLBACK
3343 }
3344
3345 return $this->doUnlockTables( $method );
3346 }
3347
3348 protected function doUnlockTables( $method ) {
3349 return true;
3350 }
3351
3359 public function dropTable( $tableName, $fName = __METHOD__ ) {
3360 if ( !$this->tableExists( $tableName, $fName ) ) {
3361 return false;
3362 }
3363 $sql = "DROP TABLE " . $this->tableName( $tableName ) . " CASCADE";
3364
3365 return $this->query( $sql, $fName );
3366 }
3367
3368 public function getInfinity() {
3369 return 'infinity';
3370 }
3371
3372 public function encodeExpiry( $expiry ) {
3373 return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3374 ? $this->getInfinity()
3375 : $this->timestamp( $expiry );
3376 }
3377
3378 public function decodeExpiry( $expiry, $format = TS_MW ) {
3379 if ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) {
3380 return 'infinity';
3381 }
3382
3383 return ConvertibleTimestamp::convert( $format, $expiry );
3384 }
3385
3386 public function setBigSelects( $value = true ) {
3387 // no-op
3388 }
3389
3390 public function isReadOnly() {
3391 return ( $this->getReadOnlyReason() !== false );
3392 }
3393
3397 protected function getReadOnlyReason() {
3398 $reason = $this->getLBInfo( 'readOnlyReason' );
3399
3400 return is_string( $reason ) ? $reason : false;
3401 }
3402
3403 public function setTableAliases( array $aliases ) {
3404 $this->tableAliases = $aliases;
3405 }
3406
3411 protected function requiresDatabaseUser() {
3412 return true;
3413 }
3414
3426 protected function getBindingHandle() {
3427 if ( !$this->mConn ) {
3428 throw new DBUnexpectedError(
3429 $this,
3430 'DB connection was already closed or the connection dropped.'
3431 );
3432 }
3433
3434 return $this->mConn;
3435 }
3436
3441 public function __toString() {
3442 return (string)$this->mConn;
3443 }
3444
3449 public function __clone() {
3450 $this->connLogger->warning(
3451 "Cloning " . static::class . " is not recomended; forking connection:\n" .
3452 ( new RuntimeException() )->getTraceAsString()
3453 );
3454
3455 if ( $this->isOpen() ) {
3456 // Open a new connection resource without messing with the old one
3457 $this->mOpened = false;
3458 $this->mConn = false;
3459 $this->mTrxEndCallbacks = []; // don't copy
3460 $this->handleSessionLoss(); // no trx or locks anymore
3461 $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
3462 $this->lastPing = microtime( true );
3463 }
3464 }
3465
3471 public function __sleep() {
3472 throw new RuntimeException( 'Database serialization may cause problems, since ' .
3473 'the connection is not restored on wakeup.' );
3474 }
3475
3479 public function __destruct() {
3480 if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
3481 trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
3482 }
3483
3484 $danglingWriters = $this->pendingWriteAndCallbackCallers();
3485 if ( $danglingWriters ) {
3486 $fnames = implode( ', ', $danglingWriters );
3487 trigger_error( "DB transaction writes or callbacks still pending ($fnames)." );
3488 }
3489
3490 if ( $this->mConn ) {
3491 // Avoid connection leaks for sanity. Normally, resources close at script completion.
3492 // The connection might already be closed in zend/hhvm by now, so suppress warnings.
3493 \MediaWiki\suppressWarnings();
3494 $this->closeConnection();
3495 \MediaWiki\restoreWarnings();
3496 $this->mConn = false;
3497 $this->mOpened = false;
3498 }
3499 }
3500}
3501
3502class_alias( Database::class, 'DatabaseBase' ); // b/c for old name
3503class_alias( Database::class, 'Database' ); // b/c global alias
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
if(!defined( 'MEDIAWIKI')) $fname
This file is not a valid entry point, perform no further processing unless MEDIAWIKI is defined.
Definition Setup.php:36
$line
Definition cdb.php:58
if( $line===false) $args
Definition cdb.php:63
interface is intended to be more or less compatible with the PHP memcached client.
Definition BagOStuff.php:47
Simple store for keeping values in an associative array for the current process.
Class to handle database/prefix specification for IDatabase domains.
Relational database abstraction object.
Definition Database.php:45
static factory( $dbType, $p=[])
Construct a Database subclass instance given a database type and parameters.
Definition Database.php:334
bool $cliMode
Whether this PHP instance is for a CLI script.
Definition Database.php:78
flushSnapshot( $fname=__METHOD__)
Commit any transaction but error out if writes or callbacks are pending.
getServerInfo()
A string describing the current software version, and possibly other details in a user-friendly way.
Definition Database.php:423
float $mTrxWriteDuration
Seconds spent in write queries for the current transaction.
Definition Database.php:198
lockTables(array $read, array $write, $method)
Lock specific tables.
encodeBlob( $b)
Some DBMSs have a special format for inserting into blob fields, they don't allow simple quoted strin...
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Single row SELECT wrapper.
object string $profiler
Class name or object With profileIn/profileOut methods.
Definition Database.php:231
runOnTransactionPreCommitCallbacks()
Actually run and consume any "on transaction pre-commit" callbacks.
float $mTrxReplicaLag
Lag estimate at the time of BEGIN.
Definition Database.php:154
begin( $fname=__METHOD__, $mode=self::TRANSACTION_EXPLICIT)
Begin a transaction.
makeUpdateOptions( $options)
Make UPDATE options for the Database::update function.
insertSelect( $destTable, $srcTable, $varMap, $conds, $fname=__METHOD__, $insertOptions=[], $selectOptions=[])
INSERT SELECT wrapper.
deadlockLoop()
Perform a deadlock-prone transaction.
static generalizeSQL( $sql)
Removes most variables from an SQL query and replaces them with X or N for numbers.
makeInsertOptions( $options)
Helper for Database::insert().
getApproximateLagStatus()
Get a replica DB lag estimate for this server.
insert( $table, $a, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
strencode( $s)
Wrapper for addslashes()
tableNamesWithIndexClauseOrJOIN( $tables, $use_index=[], $ignore_index=[], $join_conds=[])
Get the aliased table name clause for a FROM clause which might have a JOIN and/or USE INDEX or IGNOR...
const DEADLOCK_DELAY_MIN
Minimum time to wait before retry, in microseconds.
Definition Database.php:49
array[] $mTrxPreCommitCallbacks
List of (callable, method name)
Definition Database.php:99
int[] $priorFlags
Prior mFlags values.
Definition Database.php:228
conditional( $cond, $trueVal, $falseVal)
Returns an SQL expression for a simple conditional.
string $mTrxFname
Remembers the function name given for starting the most recent transaction via begin().
Definition Database.php:162
selectDB( $db)
Change the current database.
trxTimestamp()
Get the UNIX timestamp of the time that the transaction was established.
Definition Database.php:465
addQuotes( $s)
Adds quotes and backslashes.
fieldNameWithAlias( $name, $alias=false)
Get an aliased field name e.g.
tablePrefix( $prefix=null)
Get/set the table prefix.
Definition Database.php:469
setLogger(LoggerInterface $logger)
Definition Database.php:419
selectFieldValues( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
indexUnique( $table, $index)
Determines if a given index is unique.
string $mTrxShortId
Either a short hexidecimal string if a transaction is active or "".
Definition Database.php:143
fieldNamesWithAlias( $fields)
Gets an array of aliased field names.
getSessionLagStatus()
Get the replica DB lag when the current transaction started or a general lag estimate if not transact...
nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname=__METHOD__, $insertOptions=[], $selectOptions=[])
listTables( $prefix=null, $fname=__METHOD__)
List all tables on the database.
fieldExists( $table, $field, $fname=__METHOD__)
Determines whether a field exists in a table.
nextSequenceValue( $seqName)
Returns an appropriately quoted sequence value for inserting a new row.
startAtomic( $fname=__METHOD__)
Begin an atomic section of statements.
int $mTrxLevel
Either 1 if a transaction is active or 0 otherwise.
Definition Database.php:136
__destruct()
Run a few simple sanity checks and close dangling connections.
dbSchema( $schema=null)
Get/set the db schema.
Definition Database.php:481
wasLockTimeout()
Determines if the last failure was due to a lock timeout.
endAtomic( $fname=__METHOD__)
Ends an atomic section of SQL statements.
setSessionOptions(array $options)
Override database's default behavior.
string $agent
Agent name for query profiling.
Definition Database.php:80
indexName( $index)
Allows for index remapping in queries where this is not consistent across DBMS.
makeUpdateOptionsArray( $options)
Make UPDATE options array for Database::makeUpdateOptions.
listViews( $prefix=null, $fname=__METHOD__)
Lists all the VIEWs in the database.
assertOpen()
Make sure isOpen() returns true as a sanity check.
Definition Database.php:749
doCommit( $fname)
Issues the COMMIT command to the database server.
closeConnection()
Closes underlying database connection.
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
bool $mTrxAutomaticAtomic
Record if the current transaction was started implicitly by Database::startAtomic.
Definition Database.php:188
array $mSessionTempTables
Map of (table name => 1) for TEMPORARY tables.
Definition Database.php:219
getSchemaVars()
Get schema variables.
runTransactionListenerCallbacks( $trigger)
Actually run any "transaction listener" callbacks.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by wfTimestamp() to the format used for inserting ...
lastDoneWrites()
Returns the last time the connection may have been used for write queries.
Definition Database.php:539
isTransactableQuery( $sql)
Determine whether a SQL statement is sensitive to isolation level.
Definition Database.php:810
getServerUptime()
Determines how long the server has been up.
bitAnd( $fieldLeft, $fieldRight)
bool $mTrxAutomatic
Record if the current transaction was started implicitly due to DBO_TRX being set.
Definition Database.php:176
getDefaultSchemaVars()
Get schema variables to use if none have been set via setSchemaVars().
doBegin( $fname)
Issues the BEGIN command to the database server.
writesOrCallbacksPending()
Returns true if there is a transaction open with possible write queries or transaction pre-commit/idl...
Definition Database.php:547
buildGroupConcatField( $delim, $table, $field, $conds='', $join_conds=[])
Build a GROUP_CONCAT or equivalent statement for a query.
onTransactionResolution(callable $callback, $fname=__METHOD__)
Run a callback as soon as the current transaction commits or rolls back.
tableNames()
Fetch a number of table names into an array This is handy when you need to construct SQL for joins.
onTransactionIdle(callable $callback, $fname=__METHOD__)
Run a callback as soon as there is no transaction pending.
setBigSelects( $value=true)
Allow or deny "big selects" for this session only.
integer $mTrxWriteAdjQueryCount
Number of write queries counted in mTrxWriteAdjDuration.
Definition Database.php:210
lock( $lockName, $method, $timeout=5)
Acquire a named lock.
wasConnectionError( $errno)
Do not use this method outside of Database/DBError classes.
makeGroupByWithHaving( $options)
Returns an optional GROUP BY with an optional HAVING.
float $mRTTEstimate
RTT time estimate.
Definition Database.php:214
deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname=__METHOD__)
DELETE where the condition is a join.
unionSupportsOrderAndLimit()
Returns true if current database backend supports ORDER BY or LIMIT for separate subqueries within th...
duplicateTableStructure( $oldName, $newName, $temporary=false, $fname=__METHOD__)
Creates a new table with structure copied from existing table.
integer $mTrxWriteQueryCount
Number of write queries for the current transaction.
Definition Database.php:202
doneWrites()
Returns true if the connection may have been used for write queries.
Definition Database.php:535
anyChar()
Returns a token for buildLike() that denotes a '_' to be used in a LIKE query.
restoreFlags( $state=self::RESTORE_PRIOR)
Restore the flags to their prior state before the last setFlag/clearFlag call.
Definition Database.php:616
commit( $fname=__METHOD__, $flush='')
Commits a transaction previously started using begin().
resource null $mConn
Database connection.
Definition Database.php:92
static getCacheSetOptions(IDatabase $db1)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
setTrxEndCallbackSuppression( $suppress)
Whether to disable running of post-COMMIT/ROLLBACK callbacks.
getReplicaPos()
Get the replication position of this replica DB.
wasReadOnlyError()
Determines if the last failure was due to the database being read-only.
tableName( $name, $format='quoted')
Format a table name ready for use in constructing an SQL query.
replaceVars( $ins)
Database independent variable replacement.
freeResult( $res)
Free a result object returned by query() or select().
streamStatementEnd(&$sql, &$newLine)
Called by sourceStream() to check if we've reached a statement end.
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[])
A SELECT wrapper which returns a single field from a single result row.
makeList( $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.
wasDeadlock()
Determines if the last failure was due to a deadlock.
string[] $mTrxWriteCallers
Track the write query callers of the current transaction.
Definition Database.php:194
buildConcat( $stringList)
Build a concatenation list to feed into a SQL query.
ignoreIndexClause( $index)
IGNORE INDEX clause.
tableNamesWithAlias( $tables)
Gets an array of aliased table names.
getTransactionLagStatus()
Get the replica DB lag when the current transaction started.
bitOr( $fieldLeft, $fieldRight)
clearFlag( $flag, $remember=self::REMEMBER_NOTHING)
Clear a flag for this connection.
Definition Database.php:609
useIndexClause( $index)
USE INDEX clause.
DatabaseDomain $currentDomain
Definition Database.php:128
namedLocksEnqueue()
Check to see if a named lock used by lock() use blocking queues.
connectionErrorLogger( $errno, $errstr)
This method should not be used outside of Database classes.
Definition Database.php:705
maxListLen()
Return the maximum number of items allowed in a list, or 0 for unlimited.
LoggerInterface $queryLogger
Definition Database.php:87
sourceStream( $fp, callable $lineCallback=null, callable $resultCallback=null, $fname=__METHOD__, callable $inputCallback=null)
Read and execute commands from an open file handle.
tableNamesN()
Fetch a number of table names into an zero-indexed numerical array This is handy when you need to con...
callback $errorLogger
Error logging callback.
Definition Database.php:89
indexExists( $table, $index, $fname=__METHOD__)
Determines whether an index exists Usually throws a DBQueryError on failure If errors are explicitly ...
limitResult( $sql, $limit, $offset=false)
Construct a LIMIT query with optional offset.
makeSelectOptions( $options)
Returns an optional USE INDEX clause to go after the table, and a string to go at the end of the quer...
pendingWriteCallers()
Get the list of method names that did write queries for this transaction.
Definition Database.php:575
pendingWriteQueryDuration( $type=self::ESTIMATE_TOTAL)
Get the time spend running write queries for this transaction.
Definition Database.php:553
getWikiID()
Alias for getDomainID()
Definition Database.php:646
getLBInfo( $name=null)
Get properties passed down from the server info array of the load balancer.
Definition Database.php:490
aggregateValue( $valuedata, $valuename='value')
Return aggregated value alias.
getMasterPos()
Get the position of this master.
string $mLastQuery
SQL query.
Definition Database.php:62
__sleep()
Called by serialize.
addIdentifierQuotes( $s)
Quotes an identifier using backticks or "double quotes" depending on the database type.
dropTable( $tableName, $fName=__METHOD__)
Delete a table.
isWriteQuery( $sql)
Determine whether a query writes to the DB.
Definition Database.php:788
indexInfo( $table, $index, $fname=__METHOD__)
Get information about an index into an object.
getLogContext(array $extras=[])
Create a log context to pass to PSR-3 logger functions.
Definition Database.php:715
trxLevel()
Gets the current transaction level.
Definition Database.php:461
prependDatabaseOrSchema( $namespace, $relation, $format)
const DEADLOCK_DELAY_MAX
Maximum time to wait before retry.
Definition Database.php:51
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
runOnTransactionIdleCallbacks( $trigger)
Actually run and consume any "on transaction idle/resolution" callbacks.
const DEADLOCK_TRIES
Number of times to re-try an operation in case of deadlock.
Definition Database.php:47
unlockTables( $method)
Unlock all tables locked via lockTables()
setSchemaVars( $vars)
Set variables to be used in sourceFile/sourceStream, in preference to the ones in $GLOBALS.
reportConnectionError( $error='Unknown error')
Definition Database.php:762
nativeReplace( $table, $rows, $fname)
REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE statement.
decodeBlob( $b)
Some DBMSs return a special placeholder object representing blob fields in result objects.
setFlag( $flag, $remember=self::REMEMBER_NOTHING)
Set a flag for this connection.
Definition Database.php:602
selectRowCount( $tables, $vars=' *', $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Get the number of rows in dataset.
setTableAliases(array $aliases)
Make certain table names use their own database, schema, and table prefix when passed into SQL querie...
lastQuery()
Return the last query that went through IDatabase::query()
Definition Database.php:531
setLazyMasterHandle(IDatabase $conn)
Set a lazy-connecting DB handle to the master DB (for replication status purposes)
Definition Database.php:510
rollback( $fname=__METHOD__, $flush='')
Rollback a transaction previously started using begin().
array[] $mTrxEndCallbacks
List of (callable, method name)
Definition Database.php:101
array[] $mTrxIdleCallbacks
List of (callable, method name)
Definition Database.php:97
sourceFile( $filename, callable $lineCallback=null, callable $resultCallback=null, $fname=false, callable $inputCallback=null)
Read and execute SQL commands from a file.
makeWhereFrom2d( $data, $baseKey, $subKey)
Build a partial where clause from a 2-d array such as used for LinkBatch.
unlock( $lockName, $method)
Release a lock.
IDatabase null $lazyMasterHandle
Lazy handle to the master DB this server replicates from.
Definition Database.php:222
getServer()
Get the server hostname or IP address.
tableNameWithAlias( $name, $alias=false)
Get an aliased table name e.g.
getLag()
Get replica DB lag.
selectSQLText( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
The equivalent of IDatabase::select() except that the constructed SQL is returned,...
onTransactionPreCommitOrIdle(callable $callback, $fname=__METHOD__)
Run a callback before the current transaction commits or now if there is none.
unionQueries( $sqls, $all)
Construct a UNION query This is used for providing overload point for other DB abstractions not compa...
wasErrorReissuable()
Determines if the last query error was due to a dropped connection and should be dealt with by pingin...
float $mTrxWriteAdjDuration
Like mTrxWriteQueryCount but excludes lock-bound, easy to replicate, queries.
Definition Database.php:206
doAtomicSection( $fname, callable $callback)
Run a callback to do an atomic set of updates for this database.
float null $mTrxTimestamp
The UNIX time that the transaction started.
Definition Database.php:152
tableLocksHaveTransactionScope()
Checks if table locks acquired by lockTables() are transaction-bound in their scope.
setLBInfo( $name, $value=null)
Set the LB info array, or a member of it.
Definition Database.php:502
escapeLikeInternal( $s, $escapeChar='`')
array $mNamedLocksHeld
Map of (name => 1) for locks obtained via lock()
Definition Database.php:217
upsert( $table, array $rows, array $uniqueIndexes, array $set, $fname=__METHOD__)
INSERT ON DUPLICATE KEY UPDATE wrapper, upserts an array into a table.
ping(&$rtt=null)
Ping the server and try to reconnect if it there is no connection.
__clone()
Make sure that copies do not share the same client binding handle.
float bool $mLastWriteTime
UNIX timestamp of last write query.
Definition Database.php:64
string bool null $htmlErrors
Stashed value of html_errors INI setting.
Definition Database.php:124
implicitGroupby()
Returns true if this database does an implicit sort when doing GROUP BY.
Definition Database.php:523
anyString()
Returns a token for buildLike() that denotes a '' to be used in a LIKE query.
LoggerInterface $connLogger
Definition Database.php:85
bufferResults( $buffer=null)
Turns buffering of SQL result sets on (true) or off (false).
Definition Database.php:427
getInfinity()
Find out when 'infinity' is.
canRecoverFromDisconnect( $sql, $priorWritesPending)
resultObject( $result)
Take the result from a query, and wrap it in a ResultWrapper if necessary.
getScopedLockAndFlush( $lockKey, $fname, $timeout)
Acquire a named lock, flush any transaction, and return an RAII style unlocker object.
encodeExpiry( $expiry)
Encode an expiry time into the DBMS dependent format.
doRollback( $fname)
Issues the ROLLBACK command to the database server.
tableExists( $table, $fname=__METHOD__)
Query whether a given table exists.
bool $mTrxDoneWrites
Record if possible write queries were done in the last transaction started.
Definition Database.php:169
decodeExpiry( $expiry, $format=TS_MW)
Decode an expiry time into a DBMS independent format.
setTransactionListener( $name, callable $callback=null)
Run a callback each time any transaction commits or rolls back.
isQuotedIdentifier( $name)
Returns if the given identifier looks quoted or not according to the database convention for quoting ...
estimateRowCount( $table, $vars=' *', $conds='', $fname=__METHOD__, $options=[])
Estimate the number of rows in dataset.
updateTrxWriteQueryTime( $sql, $runtime)
Update the estimated run-time of a query, not counting large row lock times.
lockIsFree( $lockName, $method)
Check to see if a named lock is available (non-blocking)
isOpen()
Is a connection to the database open?
Definition Database.php:598
getFlag( $flag)
Returns a boolean whether the flag $flag is set for this connection.
Definition Database.php:629
query( $sql, $fname=__METHOD__, $tempIgnore=false)
Run an SQL query and return the result.
Definition Database.php:850
masterPosWait(DBMasterPos $pos, $timeout)
Wait for the replica DB to catch up to a given master position.
bool null $mDefaultBigSelects
Definition Database.php:116
doProfiledQuery( $sql, $commentedSql, $isWrite, $fname)
Definition Database.php:941
bool $mTrxEndCallbacksSuppressed
Whether to suppress triggering of transaction end callbacks.
Definition Database.php:105
const PING_TTL
How long before it is worth doing a dummy query to test the connection.
Definition Database.php:54
timestampOrNull( $ts=null)
Convert a timestamp in one of the formats accepted by wfTimestamp() to the format used for inserting ...
float $lastPing
UNIX timestamp.
Definition Database.php:225
array $mTrxAtomicLevels
Array of levels of atomicity within transactions.
Definition Database.php:182
TransactionProfiler $trxProfiler
Definition Database.php:233
doLockTables(array $read, array $write, $method)
callable[] $mTrxRecurringCallbacks
Map of (name => callable)
Definition Database.php:103
BagOStuff $srvCache
APC cache.
Definition Database.php:83
reportQueryError( $error, $errno, $sql, $fname, $tempIgnore=false)
Report a query error.
getBindingHandle()
Get the underlying binding handle, mConn.
replace( $table, $uniqueIndexes, $rows, $fname=__METHOD__)
REPLACE query wrapper.
buildLike()
LIKE statement wrapper, receives a variable-length argument list with parts of pattern to match conta...
doQuery( $sql)
The DBMS-dependent part of query()
implicitOrderby()
Returns true if this database does an implicit order by when the column has an index For example: SEL...
Definition Database.php:527
makeOrderBy( $options)
Returns an optional ORDER BY.
strreplace( $orig, $old, $new)
Returns a comand for str_replace function in SQL query.
__construct(array $params)
Constructor and database handle and attempt to connect to the DB server.
Definition Database.php:243
textFieldSize( $table, $field)
Returns the size of a text field, or -1 for "unlimited".
getDBname()
Get the current DB name.
close()
Closes a database connection.
Definition Database.php:726
ignoreErrors( $ignoreErrors=null)
Turns on (false) or off (true) the automatic generation and sending of a "we're sorry,...
Definition Database.php:450
Used by Database::buildLike() to represent characters that have special meaning in SQL LIKE clauses a...
Definition LikeMatch.php:10
Result wrapper for grabbing data queried from an IDatabase object.
Helper class that detects high-contention DB queries via profiling calls.
We use the convention $dbr for read and $dbw for write to help you keep track of whether the database object is a the world will explode Or to be a subsequent write query which succeeded on the master may fail when replicated to the slave due to a unique key collision Replication on the slave will stop and it may take hours to repair the database and get it back online Setting read_only in my cnf on the slave will avoid this but given the dire we prefer to have as many checks as possible We provide a but the wrapper functions like select() and insert() are usually more convenient. They take care of things like table prefixes and escaping for you. If you really need to make your own SQL
$res
Definition database.txt:21
We use the convention $dbr for read and $dbw for write to help you keep track of whether the database object is a the world will explode Or to be a subsequent write query which succeeded on the master may fail when replicated to the slave due to a unique key collision Replication on the slave will stop and it may take hours to repair the database and get it back online Setting read_only in my cnf on the slave will avoid this but given the dire we prefer to have as many checks as possible We provide a but the wrapper functions like please read the documentation for tableName() and addQuotes(). You will need both of them. ------------------------------------------------------------------------ Basic query optimisation ------------------------------------------------------------------------ MediaWiki developers who need to write DB queries should have some understanding of databases and the performance issues associated with them. Patches containing unacceptably slow features will not be accepted. Unindexed queries are generally not welcome in MediaWiki
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
the array() calling protocol came about after MediaWiki 1.4rc1.
static configuration should be added through ResourceLoaderGetConfigVars instead & $vars
Definition hooks.txt:2179
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition hooks.txt:249
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports. & $fullInterwikiPrefix:Interwiki prefix, may contain colons. & $pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable. Can be used to lazy-load the import sources list. & $importSources:The value of $wgImportSources. Modify as necessary. See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page. $context:IContextSource object & $pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect. & $title:Title object for the current page & $request:WebRequest & $ignoreRedirect:boolean to skip redirect check & $target:Title/string of redirect target & $article:Article object 'InternalParseBeforeLinks':during Parser 's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InternalParseBeforeSanitize':during Parser 's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings. Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not. Return true without providing an interwiki to continue interwiki search. $prefix:interwiki prefix we are looking for. & $iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user 's email has been invalidated successfully. $user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification. Callee may modify $url and $query, URL will be constructed as $url . $query & $url:URL to index.php & $query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) & $article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() & $ip:IP being check & $result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from & $allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn 't match your organization. $addr:The e-mail address entered by the user & $result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user & $result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we 're looking for a messages file for & $file:The messages file path, you can override this to change the location. 'LanguageGetMagic':DEPRECATED! Use $magicWords in a file listed in $wgExtensionMessagesFiles instead. Use this to define synonyms of magic words depending of the language & $magicExtensions:associative array of magic words synonyms $lang:language code(string) 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetSpecialPageAliases':DEPRECATED! Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead. Use to define aliases of special pages names depending of the language & $specialPageAliases:associative array of magic words synonyms $lang:language code(string) 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links:Array with elements of the form "language:title" in the order that they will be output. & $linkFlags:Associative array mapping prefixed links to arrays of flags. Currently unused, but planned to provide support for marking individual language links in the UI, e.g. for featured articles. 'LanguageSelector':Hook to change the language selector available on a page. $out:The output page. $cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED! Use HtmlPageLinkRendererBegin instead. Used when generating internal and interwiki links in Linker::link(), before processing starts. Return false to skip default processing and return $ret. See documentation for Linker::link() for details on the expected meanings of parameters. $skin:the Skin object $target:the Title that the link is pointing to & $html:the contents that the< a > tag should have(raw HTML) $result
Definition hooks.txt:1954
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account incomplete not yet checked for validity & $retval
Definition hooks.txt:268
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist & $tables
Definition hooks.txt:1018
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist and Watchlist you will want to construct new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects When constructing you specify which group they belong to You can reuse existing or create your you must register them with $special registerFilterGroup removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context the output can only depend on parameters provided to this hook not on global state indicating whether full HTML should be generated If generation of HTML may be but other information should still be present in the ParserOutput object to manipulate or replace but no entry for that model exists in $wgContentHandlers please use GetContentModels hook to make them known to core if desired whether it is OK to use $contentModel on $title Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok inclusive $limit
Definition hooks.txt:1143
presenting them properly to the user as errors is done by the caller return true use this to change the list i e rollback
Definition hooks.txt:1748
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist and Watchlist you will want to construct new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects When constructing you specify which group they belong to You can reuse existing or create your you must register them with $special registerFilterGroup removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context $options
Definition hooks.txt:1102
do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty its values are the globals values before the output is cached my talk my contributions etc etc otherwise the built in rate limiting checks are if enabled allows for interception of redirect as a string mapping parameter names to values & $type
Definition hooks.txt:2604
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition hooks.txt:1966
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:304
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and insert
Definition hooks.txt:2042
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist and Watchlist you will want to construct new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects When constructing you specify which group they belong to You can reuse existing or create your you must register them with $special registerFilterGroup removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set $status
Definition hooks.txt:1049
processing should stop and the error should be shown to the user * false
Definition hooks.txt:189
returning false will NOT prevent logging $e
Definition hooks.txt:2127
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition injection.txt:37
An object representing a master or replica DB position in a replicated setup.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:40
lastErrno()
Get the last error number.
fetchRow( $res)
Fetch the next row from the given result object, in associative array form.
affectedRows()
Get the number of rows affected by the last write query.
fieldInfo( $table, $field)
mysql_fetch_field() wrapper Returns false if the field doesn't exist
lastError()
Get a description of the last error.
fetchObject( $res)
Fetch the next row from the given result object, in object form.
numRows( $res)
Get the number of rows in a result object.
getServerVersion()
A string describing the current software version, like from mysql_get_server_info().
open( $server, $user, $password, $dbName)
Open a connection to the database.
Advanced database interface for IDatabase handles that include maintenance methods.
$buffer
A helper class for throttling authentication attempts.
const DBO_TRX
Definition defines.php:12
$params