MediaWiki REL1_30
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 = 0.010;
58 const SLOW_WRITE_SEC = 0.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;
218 private $mRTTEstimate = 0.0;
219
221 private $mNamedLocksHeld = [];
223 protected $mSessionTempTables = [];
224
227
229 protected $lastPing = 0.0;
230
232 private $priorFlags = [];
233
235 protected $profiler;
237 protected $trxProfiler;
238
248 $server = $params['host'];
249 $user = $params['user'];
250 $password = $params['password'];
251 $dbName = $params['dbname'];
252
253 $this->mSchema = $params['schema'];
254 $this->mTablePrefix = $params['tablePrefix'];
255
256 $this->cliMode = $params['cliMode'];
257 // Agent name is added to SQL queries in a comment, so make sure it can't break out
258 $this->agent = str_replace( '/', '-', $params['agent'] );
259
260 $this->mFlags = $params['flags'];
261 if ( $this->mFlags & self::DBO_DEFAULT ) {
262 if ( $this->cliMode ) {
263 $this->mFlags &= ~self::DBO_TRX;
264 } else {
265 $this->mFlags |= self::DBO_TRX;
266 }
267 }
268
269 $this->mSessionVars = $params['variables'];
270
271 $this->srvCache = isset( $params['srvCache'] )
272 ? $params['srvCache']
273 : new HashBagOStuff();
274
275 $this->profiler = $params['profiler'];
276 $this->trxProfiler = $params['trxProfiler'];
277 $this->connLogger = $params['connLogger'];
278 $this->queryLogger = $params['queryLogger'];
279 $this->errorLogger = $params['errorLogger'];
280
281 // Set initial dummy domain until open() sets the final DB/prefix
282 $this->currentDomain = DatabaseDomain::newUnspecified();
283
284 if ( $user ) {
285 $this->open( $server, $user, $password, $dbName );
286 } elseif ( $this->requiresDatabaseUser() ) {
287 throw new InvalidArgumentException( "No database user provided." );
288 }
289
290 // Set the domain object after open() sets the relevant fields
291 if ( $this->mDBname != '' ) {
292 // Domains with server scope but a table prefix are not used by IDatabase classes
293 $this->currentDomain = new DatabaseDomain( $this->mDBname, null, $this->mTablePrefix );
294 }
295 }
296
338 final public static function factory( $dbType, $p = [] ) {
339 static $canonicalDBTypes = [
340 'mysql' => [ 'mysqli', 'mysql' ],
341 'postgres' => [],
342 'sqlite' => [],
343 'oracle' => [],
344 'mssql' => [],
345 ];
346 static $classAliases = [
347 'DatabaseMssql' => DatabaseMssql::class,
348 'DatabaseMysql' => DatabaseMysql::class,
349 'DatabaseMysqli' => DatabaseMysqli::class,
350 'DatabaseSqlite' => DatabaseSqlite::class,
351 'DatabasePostgres' => DatabasePostgres::class
352 ];
353
354 $driver = false;
355 $dbType = strtolower( $dbType );
356 if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
357 $possibleDrivers = $canonicalDBTypes[$dbType];
358 if ( !empty( $p['driver'] ) ) {
359 if ( in_array( $p['driver'], $possibleDrivers ) ) {
360 $driver = $p['driver'];
361 } else {
362 throw new InvalidArgumentException( __METHOD__ .
363 " type '$dbType' does not support driver '{$p['driver']}'" );
364 }
365 } else {
366 foreach ( $possibleDrivers as $posDriver ) {
367 if ( extension_loaded( $posDriver ) ) {
368 $driver = $posDriver;
369 break;
370 }
371 }
372 }
373 } else {
374 $driver = $dbType;
375 }
376
377 if ( $driver === false || $driver === '' ) {
378 throw new InvalidArgumentException( __METHOD__ .
379 " no viable database extension found for type '$dbType'" );
380 }
381
382 $class = 'Database' . ucfirst( $driver );
383 if ( isset( $classAliases[$class] ) ) {
384 $class = $classAliases[$class];
385 }
386
387 if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
388 // Resolve some defaults for b/c
389 $p['host'] = isset( $p['host'] ) ? $p['host'] : false;
390 $p['user'] = isset( $p['user'] ) ? $p['user'] : false;
391 $p['password'] = isset( $p['password'] ) ? $p['password'] : false;
392 $p['dbname'] = isset( $p['dbname'] ) ? $p['dbname'] : false;
393 $p['flags'] = isset( $p['flags'] ) ? $p['flags'] : 0;
394 $p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
395 $p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : '';
396 $p['schema'] = isset( $p['schema'] ) ? $p['schema'] : '';
397 $p['cliMode'] = isset( $p['cliMode'] ) ? $p['cliMode'] : ( PHP_SAPI === 'cli' );
398 $p['agent'] = isset( $p['agent'] ) ? $p['agent'] : '';
399 if ( !isset( $p['connLogger'] ) ) {
400 $p['connLogger'] = new \Psr\Log\NullLogger();
401 }
402 if ( !isset( $p['queryLogger'] ) ) {
403 $p['queryLogger'] = new \Psr\Log\NullLogger();
404 }
405 $p['profiler'] = isset( $p['profiler'] ) ? $p['profiler'] : null;
406 if ( !isset( $p['trxProfiler'] ) ) {
407 $p['trxProfiler'] = new TransactionProfiler();
408 }
409 if ( !isset( $p['errorLogger'] ) ) {
410 $p['errorLogger'] = function ( Exception $e ) {
411 trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
412 };
413 }
414
415 $conn = new $class( $p );
416 } else {
417 $conn = null;
418 }
419
420 return $conn;
421 }
422
430 public function setLogger( LoggerInterface $logger ) {
431 $this->queryLogger = $logger;
432 }
433
434 public function getServerInfo() {
435 return $this->getServerVersion();
436 }
437
438 public function bufferResults( $buffer = null ) {
439 $res = !$this->getFlag( self::DBO_NOBUFFER );
440 if ( $buffer !== null ) {
441 $buffer
442 ? $this->clearFlag( self::DBO_NOBUFFER )
443 : $this->setFlag( self::DBO_NOBUFFER );
444 }
445
446 return $res;
447 }
448
461 protected function ignoreErrors( $ignoreErrors = null ) {
462 $res = $this->getFlag( self::DBO_IGNORE );
463 if ( $ignoreErrors !== null ) {
464 $ignoreErrors
465 ? $this->setFlag( self::DBO_IGNORE )
466 : $this->clearFlag( self::DBO_IGNORE );
467 }
468
469 return $res;
470 }
471
472 public function trxLevel() {
473 return $this->mTrxLevel;
474 }
475
476 public function trxTimestamp() {
477 return $this->mTrxLevel ? $this->mTrxTimestamp : null;
478 }
479
480 public function tablePrefix( $prefix = null ) {
481 $old = $this->mTablePrefix;
482 if ( $prefix !== null ) {
483 $this->mTablePrefix = $prefix;
484 $this->currentDomain = ( $this->mDBname != '' )
485 ? new DatabaseDomain( $this->mDBname, null, $this->mTablePrefix )
487 }
488
489 return $old;
490 }
491
492 public function dbSchema( $schema = null ) {
493 $old = $this->mSchema;
494 if ( $schema !== null ) {
495 $this->mSchema = $schema;
496 }
497
498 return $old;
499 }
500
501 public function getLBInfo( $name = null ) {
502 if ( is_null( $name ) ) {
503 return $this->mLBInfo;
504 } else {
505 if ( array_key_exists( $name, $this->mLBInfo ) ) {
506 return $this->mLBInfo[$name];
507 } else {
508 return null;
509 }
510 }
511 }
512
513 public function setLBInfo( $name, $value = null ) {
514 if ( is_null( $value ) ) {
515 $this->mLBInfo = $name;
516 } else {
517 $this->mLBInfo[$name] = $value;
518 }
519 }
520
521 public function setLazyMasterHandle( IDatabase $conn ) {
522 $this->lazyMasterHandle = $conn;
523 }
524
530 protected function getLazyMasterHandle() {
532 }
533
534 public function implicitGroupby() {
535 return true;
536 }
537
538 public function implicitOrderby() {
539 return true;
540 }
541
542 public function lastQuery() {
543 return $this->mLastQuery;
544 }
545
546 public function doneWrites() {
547 return (bool)$this->mLastWriteTime;
548 }
549
550 public function lastDoneWrites() {
551 return $this->mLastWriteTime ?: false;
552 }
553
554 public function writesPending() {
555 return $this->mTrxLevel && $this->mTrxDoneWrites;
556 }
557
558 public function writesOrCallbacksPending() {
559 return $this->mTrxLevel && (
560 $this->mTrxDoneWrites || $this->mTrxIdleCallbacks || $this->mTrxPreCommitCallbacks
561 );
562 }
563
564 public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
565 if ( !$this->mTrxLevel ) {
566 return false;
567 } elseif ( !$this->mTrxDoneWrites ) {
568 return 0.0;
569 }
570
571 switch ( $type ) {
572 case self::ESTIMATE_DB_APPLY:
573 $this->ping( $rtt );
574 $rttAdjTotal = $this->mTrxWriteAdjQueryCount * $rtt;
575 $applyTime = max( $this->mTrxWriteAdjDuration - $rttAdjTotal, 0 );
576 // For omitted queries, make them count as something at least
577 $omitted = $this->mTrxWriteQueryCount - $this->mTrxWriteAdjQueryCount;
578 $applyTime += self::TINY_WRITE_SEC * $omitted;
579
580 return $applyTime;
581 default: // everything
583 }
584 }
585
586 public function pendingWriteCallers() {
587 return $this->mTrxLevel ? $this->mTrxWriteCallers : [];
588 }
589
590 public function pendingWriteRowsAffected() {
592 }
593
600 protected function pendingWriteAndCallbackCallers() {
601 if ( !$this->mTrxLevel ) {
602 return [];
603 }
604
605 $fnames = $this->mTrxWriteCallers;
606 foreach ( [
607 $this->mTrxIdleCallbacks,
608 $this->mTrxPreCommitCallbacks,
609 $this->mTrxEndCallbacks
610 ] as $callbacks ) {
611 foreach ( $callbacks as $callback ) {
612 $fnames[] = $callback[1];
613 }
614 }
615
616 return $fnames;
617 }
618
619 public function isOpen() {
620 return $this->mOpened;
621 }
622
623 public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
624 if ( $remember === self::REMEMBER_PRIOR ) {
625 array_push( $this->priorFlags, $this->mFlags );
626 }
627 $this->mFlags |= $flag;
628 }
629
630 public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
631 if ( $remember === self::REMEMBER_PRIOR ) {
632 array_push( $this->priorFlags, $this->mFlags );
633 }
634 $this->mFlags &= ~$flag;
635 }
636
637 public function restoreFlags( $state = self::RESTORE_PRIOR ) {
638 if ( !$this->priorFlags ) {
639 return;
640 }
641
642 if ( $state === self::RESTORE_INITIAL ) {
643 $this->mFlags = reset( $this->priorFlags );
644 $this->priorFlags = [];
645 } else {
646 $this->mFlags = array_pop( $this->priorFlags );
647 }
648 }
649
650 public function getFlag( $flag ) {
651 return !!( $this->mFlags & $flag );
652 }
653
659 public function getProperty( $name ) {
660 return $this->$name;
661 }
662
663 public function getDomainID() {
664 return $this->currentDomain->getId();
665 }
666
667 final public function getWikiID() {
668 return $this->getDomainID();
669 }
670
678 abstract function indexInfo( $table, $index, $fname = __METHOD__ );
679
686 abstract function strencode( $s );
687
691 protected function installErrorHandler() {
692 $this->mPHPError = false;
693 $this->htmlErrors = ini_set( 'html_errors', '0' );
694 set_error_handler( [ $this, 'connectionErrorLogger' ] );
695 }
696
702 protected function restoreErrorHandler() {
703 restore_error_handler();
704 if ( $this->htmlErrors !== false ) {
705 ini_set( 'html_errors', $this->htmlErrors );
706 }
707
708 return $this->getLastPHPError();
709 }
710
714 protected function getLastPHPError() {
715 if ( $this->mPHPError ) {
716 $error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
717 $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
718
719 return $error;
720 }
721
722 return false;
723 }
724
732 public function connectionErrorLogger( $errno, $errstr ) {
733 $this->mPHPError = $errstr;
734 }
735
742 protected function getLogContext( array $extras = [] ) {
743 return array_merge(
744 [
745 'db_server' => $this->mServer,
746 'db_name' => $this->mDBname,
747 'db_user' => $this->mUser,
748 ],
749 $extras
750 );
751 }
752
753 public function close() {
754 if ( $this->mConn ) {
755 if ( $this->trxLevel() ) {
756 $this->commit( __METHOD__, self::FLUSHING_INTERNAL );
757 }
758
759 $closed = $this->closeConnection();
760 $this->mConn = false;
761 } elseif (
762 $this->mTrxIdleCallbacks ||
763 $this->mTrxPreCommitCallbacks ||
764 $this->mTrxEndCallbacks
765 ) { // sanity
766 throw new RuntimeException( "Transaction callbacks still pending." );
767 } else {
768 $closed = true;
769 }
770 $this->mOpened = false;
771
772 return $closed;
773 }
774
780 protected function assertOpen() {
781 if ( !$this->isOpen() ) {
782 throw new DBUnexpectedError( $this, "DB connection was already closed." );
783 }
784 }
785
791 abstract protected function closeConnection();
792
793 public function reportConnectionError( $error = 'Unknown error' ) {
794 $myError = $this->lastError();
795 if ( $myError ) {
796 $error = $myError;
797 }
798
799 # New method
800 throw new DBConnectionError( $this, $error );
801 }
802
810 abstract protected function doQuery( $sql );
811
819 protected function isWriteQuery( $sql ) {
820 return !preg_match(
821 '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\‍(SELECT)\b/i', $sql );
822 }
823
828 protected function getQueryVerb( $sql ) {
829 return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
830 }
831
841 protected function isTransactableQuery( $sql ) {
842 return !in_array(
843 $this->getQueryVerb( $sql ),
844 [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET', 'CREATE', 'ALTER' ],
845 true
846 );
847 }
848
853 protected function registerTempTableOperation( $sql ) {
854 if ( preg_match(
855 '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
856 $sql,
858 ) ) {
859 $this->mSessionTempTables[$matches[1]] = 1;
860
861 return true;
862 } elseif ( preg_match(
863 '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
864 $sql,
866 ) ) {
867 $isTemp = isset( $this->mSessionTempTables[$matches[1]] );
868 unset( $this->mSessionTempTables[$matches[1]] );
869
870 return $isTemp;
871 } elseif ( preg_match(
872 '/^TRUNCATE\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
873 $sql,
875 ) ) {
876 return isset( $this->mSessionTempTables[$matches[1]] );
877 } elseif ( preg_match(
878 '/^(?:INSERT\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+[`"\']?(\w+)[`"\']?/i',
879 $sql,
881 ) ) {
882 return isset( $this->mSessionTempTables[$matches[1]] );
883 }
884
885 return false;
886 }
887
888 public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
889 $priorWritesPending = $this->writesOrCallbacksPending();
890 $this->mLastQuery = $sql;
891
892 $isWrite = $this->isWriteQuery( $sql );
893 if ( $isWrite ) {
894 $isNonTempWrite = !$this->registerTempTableOperation( $sql );
895 } else {
896 $isNonTempWrite = false;
897 }
898
899 if ( $isWrite ) {
900 # In theory, non-persistent writes are allowed in read-only mode, but due to things
901 # like https://bugs.mysql.com/bug.php?id=33669 that might not work anyway...
902 $reason = $this->getReadOnlyReason();
903 if ( $reason !== false ) {
904 throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
905 }
906 # Set a flag indicating that writes have been done
907 $this->mLastWriteTime = microtime( true );
908 }
909
910 # Add trace comment to the begin of the sql string, right after the operator.
911 # Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
912 $commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
913
914 # Start implicit transactions that wrap the request if DBO_TRX is enabled
915 if ( !$this->mTrxLevel && $this->getFlag( self::DBO_TRX )
916 && $this->isTransactableQuery( $sql )
917 ) {
918 $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
919 $this->mTrxAutomatic = true;
920 }
921
922 # Keep track of whether the transaction has write queries pending
923 if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWrite ) {
924 $this->mTrxDoneWrites = true;
925 $this->trxProfiler->transactionWritingIn(
926 $this->mServer, $this->mDBname, $this->mTrxShortId );
927 }
928
929 if ( $this->getFlag( self::DBO_DEBUG ) ) {
930 $this->queryLogger->debug( "{$this->mDBname} {$commentedSql}" );
931 }
932
933 # Avoid fatals if close() was called
934 $this->assertOpen();
935
936 # Send the query to the server
937 $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
938
939 # Try reconnecting if the connection was lost
940 if ( false === $ret && $this->wasErrorReissuable() ) {
941 $recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
942 # Stash the last error values before anything might clear them
943 $lastError = $this->lastError();
944 $lastErrno = $this->lastErrno();
945 # Update state tracking to reflect transaction loss due to disconnection
946 $this->handleSessionLoss();
947 if ( $this->reconnect() ) {
948 $msg = __METHOD__ . ": lost connection to {$this->getServer()}; reconnected";
949 $this->connLogger->warning( $msg );
950 $this->queryLogger->warning(
951 "$msg:\n" . ( new RuntimeException() )->getTraceAsString() );
952
953 if ( !$recoverable ) {
954 # Callers may catch the exception and continue to use the DB
955 $this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
956 } else {
957 # Should be safe to silently retry the query
958 $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
959 }
960 } else {
961 $msg = __METHOD__ . ": lost connection to {$this->getServer()} permanently";
962 $this->connLogger->error( $msg );
963 }
964 }
965
966 if ( false === $ret ) {
967 # Deadlocks cause the entire transaction to abort, not just the statement.
968 # https://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
969 # https://www.postgresql.org/docs/9.1/static/explicit-locking.html
970 if ( $this->wasDeadlock() ) {
971 if ( $this->explicitTrxActive() || $priorWritesPending ) {
972 $tempIgnore = false; // not recoverable
973 }
974 # Update state tracking to reflect transaction loss
975 $this->handleSessionLoss();
976 }
977
978 $this->reportQueryError(
979 $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
980 }
981
982 $res = $this->resultObject( $ret );
983
984 return $res;
985 }
986
998 private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) {
999 $isMaster = !is_null( $this->getLBInfo( 'master' ) );
1000 # generalizeSQL() will probably cut down the query to reasonable
1001 # logging size most of the time. The substr is really just a sanity check.
1002 if ( $isMaster ) {
1003 $queryProf = 'query-m: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
1004 } else {
1005 $queryProf = 'query: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
1006 }
1007
1008 # Include query transaction state
1009 $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
1010
1011 $startTime = microtime( true );
1012 if ( $this->profiler ) {
1013 call_user_func( [ $this->profiler, 'profileIn' ], $queryProf );
1014 }
1015 $ret = $this->doQuery( $commentedSql );
1016 if ( $this->profiler ) {
1017 call_user_func( [ $this->profiler, 'profileOut' ], $queryProf );
1018 }
1019 $queryRuntime = max( microtime( true ) - $startTime, 0.0 );
1020
1021 unset( $queryProfSection ); // profile out (if set)
1022
1023 if ( $ret !== false ) {
1024 $this->lastPing = $startTime;
1025 if ( $isWrite && $this->mTrxLevel ) {
1026 $this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() );
1027 $this->mTrxWriteCallers[] = $fname;
1028 }
1029 }
1030
1031 if ( $sql === self::PING_QUERY ) {
1032 $this->mRTTEstimate = $queryRuntime;
1033 }
1034
1035 $this->trxProfiler->recordQueryCompletion(
1036 $queryProf, $startTime, $isWrite, $this->affectedRows()
1037 );
1038 $this->queryLogger->debug( $sql, [
1039 'method' => $fname,
1040 'master' => $isMaster,
1041 'runtime' => $queryRuntime,
1042 ] );
1043
1044 return $ret;
1045 }
1046
1059 private function updateTrxWriteQueryTime( $sql, $runtime, $affected ) {
1060 // Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
1061 $indicativeOfReplicaRuntime = true;
1062 if ( $runtime > self::SLOW_WRITE_SEC ) {
1063 $verb = $this->getQueryVerb( $sql );
1064 // insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
1065 if ( $verb === 'INSERT' ) {
1066 $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS;
1067 } elseif ( $verb === 'REPLACE' ) {
1068 $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS / 2;
1069 }
1070 }
1071
1072 $this->mTrxWriteDuration += $runtime;
1073 $this->mTrxWriteQueryCount += 1;
1074 $this->mTrxWriteAffectedRows += $affected;
1075 if ( $indicativeOfReplicaRuntime ) {
1076 $this->mTrxWriteAdjDuration += $runtime;
1077 $this->mTrxWriteAdjQueryCount += 1;
1078 }
1079 }
1080
1091 private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
1092 # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
1093 # Dropped connections also mean that named locks are automatically released.
1094 # Only allow error suppression in autocommit mode or when the lost transaction
1095 # didn't matter anyway (aside from DBO_TRX snapshot loss).
1096 if ( $this->mNamedLocksHeld ) {
1097 return false; // possible critical section violation
1098 } elseif ( $sql === 'COMMIT' ) {
1099 return !$priorWritesPending; // nothing written anyway? (T127428)
1100 } elseif ( $sql === 'ROLLBACK' ) {
1101 return true; // transaction lost...which is also what was requested :)
1102 } elseif ( $this->explicitTrxActive() ) {
1103 return false; // don't drop atomocity
1104 } elseif ( $priorWritesPending ) {
1105 return false; // prior writes lost from implicit transaction
1106 }
1107
1108 return true;
1109 }
1110
1116 private function handleSessionLoss() {
1117 $this->mTrxLevel = 0;
1118 $this->mTrxIdleCallbacks = []; // T67263
1119 $this->mTrxPreCommitCallbacks = []; // T67263
1120 $this->mSessionTempTables = [];
1121 $this->mNamedLocksHeld = [];
1122 try {
1123 // Handle callbacks in mTrxEndCallbacks
1124 $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
1125 $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
1126 return null;
1127 } catch ( Exception $e ) {
1128 // Already logged; move on...
1129 return $e;
1130 }
1131 }
1132
1133 public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
1134 if ( $this->ignoreErrors() || $tempIgnore ) {
1135 $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
1136 } else {
1137 $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
1138 $this->queryLogger->error(
1139 "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
1140 $this->getLogContext( [
1141 'method' => __METHOD__,
1142 'errno' => $errno,
1143 'error' => $error,
1144 'sql1line' => $sql1line,
1145 'fname' => $fname,
1146 ] )
1147 );
1148 $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
1149 throw new DBQueryError( $this, $error, $errno, $sql, $fname );
1150 }
1151 }
1152
1153 public function freeResult( $res ) {
1154 }
1155
1156 public function selectField(
1157 $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1158 ) {
1159 if ( $var === '*' ) { // sanity
1160 throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1161 }
1162
1163 if ( !is_array( $options ) ) {
1164 $options = [ $options ];
1165 }
1166
1167 $options['LIMIT'] = 1;
1168
1169 $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1170 if ( $res === false || !$this->numRows( $res ) ) {
1171 return false;
1172 }
1173
1174 $row = $this->fetchRow( $res );
1175
1176 if ( $row !== false ) {
1177 return reset( $row );
1178 } else {
1179 return false;
1180 }
1181 }
1182
1183 public function selectFieldValues(
1184 $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1185 ) {
1186 if ( $var === '*' ) { // sanity
1187 throw new DBUnexpectedError( $this, "Cannot use a * field" );
1188 } elseif ( !is_string( $var ) ) { // sanity
1189 throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1190 }
1191
1192 if ( !is_array( $options ) ) {
1193 $options = [ $options ];
1194 }
1195
1196 $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1197 if ( $res === false ) {
1198 return false;
1199 }
1200
1201 $values = [];
1202 foreach ( $res as $row ) {
1203 $values[] = $row->$var;
1204 }
1205
1206 return $values;
1207 }
1208
1218 protected function makeSelectOptions( $options ) {
1219 $preLimitTail = $postLimitTail = '';
1220 $startOpts = '';
1221
1222 $noKeyOptions = [];
1223
1224 foreach ( $options as $key => $option ) {
1225 if ( is_numeric( $key ) ) {
1226 $noKeyOptions[$option] = true;
1227 }
1228 }
1229
1230 $preLimitTail .= $this->makeGroupByWithHaving( $options );
1231
1232 $preLimitTail .= $this->makeOrderBy( $options );
1233
1234 if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1235 $postLimitTail .= ' FOR UPDATE';
1236 }
1237
1238 if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1239 $postLimitTail .= ' LOCK IN SHARE MODE';
1240 }
1241
1242 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1243 $startOpts .= 'DISTINCT';
1244 }
1245
1246 # Various MySQL extensions
1247 if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1248 $startOpts .= ' /*! STRAIGHT_JOIN */';
1249 }
1250
1251 if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
1252 $startOpts .= ' HIGH_PRIORITY';
1253 }
1254
1255 if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1256 $startOpts .= ' SQL_BIG_RESULT';
1257 }
1258
1259 if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1260 $startOpts .= ' SQL_BUFFER_RESULT';
1261 }
1262
1263 if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1264 $startOpts .= ' SQL_SMALL_RESULT';
1265 }
1266
1267 if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1268 $startOpts .= ' SQL_CALC_FOUND_ROWS';
1269 }
1270
1271 if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
1272 $startOpts .= ' SQL_CACHE';
1273 }
1274
1275 if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
1276 $startOpts .= ' SQL_NO_CACHE';
1277 }
1278
1279 if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1280 $useIndex = $this->useIndexClause( $options['USE INDEX'] );
1281 } else {
1282 $useIndex = '';
1283 }
1284 if ( isset( $options['IGNORE INDEX'] ) && is_string( $options['IGNORE INDEX'] ) ) {
1285 $ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
1286 } else {
1287 $ignoreIndex = '';
1288 }
1289
1290 return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1291 }
1292
1301 protected function makeGroupByWithHaving( $options ) {
1302 $sql = '';
1303 if ( isset( $options['GROUP BY'] ) ) {
1304 $gb = is_array( $options['GROUP BY'] )
1305 ? implode( ',', $options['GROUP BY'] )
1306 : $options['GROUP BY'];
1307 $sql .= ' GROUP BY ' . $gb;
1308 }
1309 if ( isset( $options['HAVING'] ) ) {
1310 $having = is_array( $options['HAVING'] )
1311 ? $this->makeList( $options['HAVING'], self::LIST_AND )
1312 : $options['HAVING'];
1313 $sql .= ' HAVING ' . $having;
1314 }
1315
1316 return $sql;
1317 }
1318
1327 protected function makeOrderBy( $options ) {
1328 if ( isset( $options['ORDER BY'] ) ) {
1329 $ob = is_array( $options['ORDER BY'] )
1330 ? implode( ',', $options['ORDER BY'] )
1331 : $options['ORDER BY'];
1332
1333 return ' ORDER BY ' . $ob;
1334 }
1335
1336 return '';
1337 }
1338
1339 public function select( $table, $vars, $conds = '', $fname = __METHOD__,
1340 $options = [], $join_conds = [] ) {
1341 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1342
1343 return $this->query( $sql, $fname );
1344 }
1345
1346 public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1347 $options = [], $join_conds = []
1348 ) {
1349 if ( is_array( $vars ) ) {
1350 $vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1351 }
1352
1353 $options = (array)$options;
1354 $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1355 ? $options['USE INDEX']
1356 : [];
1357 $ignoreIndexes = (
1358 isset( $options['IGNORE INDEX'] ) &&
1359 is_array( $options['IGNORE INDEX'] )
1360 )
1361 ? $options['IGNORE INDEX']
1362 : [];
1363
1364 if ( is_array( $table ) ) {
1365 $from = ' FROM ' .
1366 $this->tableNamesWithIndexClauseOrJOIN(
1367 $table, $useIndexes, $ignoreIndexes, $join_conds );
1368 } elseif ( $table != '' ) {
1369 if ( $table[0] == ' ' ) {
1370 $from = ' FROM ' . $table;
1371 } else {
1372 $from = ' FROM ' .
1373 $this->tableNamesWithIndexClauseOrJOIN(
1374 [ $table ], $useIndexes, $ignoreIndexes, [] );
1375 }
1376 } else {
1377 $from = '';
1378 }
1379
1380 list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
1381 $this->makeSelectOptions( $options );
1382
1383 if ( !empty( $conds ) ) {
1384 if ( is_array( $conds ) ) {
1385 $conds = $this->makeList( $conds, self::LIST_AND );
1386 }
1387 $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex " .
1388 "WHERE $conds $preLimitTail";
1389 } else {
1390 $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
1391 }
1392
1393 if ( isset( $options['LIMIT'] ) ) {
1394 $sql = $this->limitResult( $sql, $options['LIMIT'],
1395 isset( $options['OFFSET'] ) ? $options['OFFSET'] : false );
1396 }
1397 $sql = "$sql $postLimitTail";
1398
1399 if ( isset( $options['EXPLAIN'] ) ) {
1400 $sql = 'EXPLAIN ' . $sql;
1401 }
1402
1403 return $sql;
1404 }
1405
1406 public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1407 $options = [], $join_conds = []
1408 ) {
1409 $options = (array)$options;
1410 $options['LIMIT'] = 1;
1411 $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1412
1413 if ( $res === false ) {
1414 return false;
1415 }
1416
1417 if ( !$this->numRows( $res ) ) {
1418 return false;
1419 }
1420
1421 $obj = $this->fetchObject( $res );
1422
1423 return $obj;
1424 }
1425
1426 public function estimateRowCount(
1427 $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
1428 ) {
1429 $rows = 0;
1430 $res = $this->select( $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options );
1431
1432 if ( $res ) {
1433 $row = $this->fetchRow( $res );
1434 $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1435 }
1436
1437 return $rows;
1438 }
1439
1440 public function selectRowCount(
1441 $tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1442 ) {
1443 $rows = 0;
1444 $sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
1445 // The identifier quotes is primarily for MSSQL.
1446 $rowCountCol = $this->addIdentifierQuotes( "rowcount" );
1447 $tableName = $this->addIdentifierQuotes( "tmp_count" );
1448 $res = $this->query( "SELECT COUNT(*) AS $rowCountCol FROM ($sql) $tableName", $fname );
1449
1450 if ( $res ) {
1451 $row = $this->fetchRow( $res );
1452 $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1453 }
1454
1455 return $rows;
1456 }
1457
1466 protected static function generalizeSQL( $sql ) {
1467 # This does the same as the regexp below would do, but in such a way
1468 # as to avoid crashing php on some large strings.
1469 # $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
1470
1471 $sql = str_replace( "\\\\", '', $sql );
1472 $sql = str_replace( "\\'", '', $sql );
1473 $sql = str_replace( "\\\"", '', $sql );
1474 $sql = preg_replace( "/'.*'/s", "'X'", $sql );
1475 $sql = preg_replace( '/".*"/s', "'X'", $sql );
1476
1477 # All newlines, tabs, etc replaced by single space
1478 $sql = preg_replace( '/\s+/', ' ', $sql );
1479
1480 # All numbers => N,
1481 # except the ones surrounded by characters, e.g. l10n
1482 $sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
1483 $sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
1484
1485 return $sql;
1486 }
1487
1488 public function fieldExists( $table, $field, $fname = __METHOD__ ) {
1489 $info = $this->fieldInfo( $table, $field );
1490
1491 return (bool)$info;
1492 }
1493
1494 public function indexExists( $table, $index, $fname = __METHOD__ ) {
1495 if ( !$this->tableExists( $table ) ) {
1496 return null;
1497 }
1498
1499 $info = $this->indexInfo( $table, $index, $fname );
1500 if ( is_null( $info ) ) {
1501 return null;
1502 } else {
1503 return $info !== false;
1504 }
1505 }
1506
1507 public function tableExists( $table, $fname = __METHOD__ ) {
1508 $tableRaw = $this->tableName( $table, 'raw' );
1509 if ( isset( $this->mSessionTempTables[$tableRaw] ) ) {
1510 return true; // already known to exist
1511 }
1512
1513 $table = $this->tableName( $table );
1514 $ignoreErrors = true;
1515 $res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname, $ignoreErrors );
1516
1517 return (bool)$res;
1518 }
1519
1520 public function indexUnique( $table, $index ) {
1521 $indexInfo = $this->indexInfo( $table, $index );
1522
1523 if ( !$indexInfo ) {
1524 return null;
1525 }
1526
1527 return !$indexInfo[0]->Non_unique;
1528 }
1529
1536 protected function makeInsertOptions( $options ) {
1537 return implode( ' ', $options );
1538 }
1539
1540 public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
1541 # No rows to insert, easy just return now
1542 if ( !count( $a ) ) {
1543 return true;
1544 }
1545
1546 $table = $this->tableName( $table );
1547
1548 if ( !is_array( $options ) ) {
1549 $options = [ $options ];
1550 }
1551
1552 $fh = null;
1553 if ( isset( $options['fileHandle'] ) ) {
1554 $fh = $options['fileHandle'];
1555 }
1557
1558 if ( isset( $a[0] ) && is_array( $a[0] ) ) {
1559 $multi = true;
1560 $keys = array_keys( $a[0] );
1561 } else {
1562 $multi = false;
1563 $keys = array_keys( $a );
1564 }
1565
1566 $sql = 'INSERT ' . $options .
1567 " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
1568
1569 if ( $multi ) {
1570 $first = true;
1571 foreach ( $a as $row ) {
1572 if ( $first ) {
1573 $first = false;
1574 } else {
1575 $sql .= ',';
1576 }
1577 $sql .= '(' . $this->makeList( $row ) . ')';
1578 }
1579 } else {
1580 $sql .= '(' . $this->makeList( $a ) . ')';
1581 }
1582
1583 if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
1584 return false;
1585 } elseif ( $fh !== null ) {
1586 return true;
1587 }
1588
1589 return (bool)$this->query( $sql, $fname );
1590 }
1591
1598 protected function makeUpdateOptionsArray( $options ) {
1599 if ( !is_array( $options ) ) {
1600 $options = [ $options ];
1601 }
1602
1603 $opts = [];
1604
1605 if ( in_array( 'IGNORE', $options ) ) {
1606 $opts[] = 'IGNORE';
1607 }
1608
1609 return $opts;
1610 }
1611
1618 protected function makeUpdateOptions( $options ) {
1619 $opts = $this->makeUpdateOptionsArray( $options );
1620
1621 return implode( ' ', $opts );
1622 }
1623
1624 public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
1625 $table = $this->tableName( $table );
1626 $opts = $this->makeUpdateOptions( $options );
1627 $sql = "UPDATE $opts $table SET " . $this->makeList( $values, self::LIST_SET );
1628
1629 if ( $conds !== [] && $conds !== '*' ) {
1630 $sql .= " WHERE " . $this->makeList( $conds, self::LIST_AND );
1631 }
1632
1633 return (bool)$this->query( $sql, $fname );
1634 }
1635
1636 public function makeList( $a, $mode = self::LIST_COMMA ) {
1637 if ( !is_array( $a ) ) {
1638 throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
1639 }
1640
1641 $first = true;
1642 $list = '';
1643
1644 foreach ( $a as $field => $value ) {
1645 if ( !$first ) {
1646 if ( $mode == self::LIST_AND ) {
1647 $list .= ' AND ';
1648 } elseif ( $mode == self::LIST_OR ) {
1649 $list .= ' OR ';
1650 } else {
1651 $list .= ',';
1652 }
1653 } else {
1654 $first = false;
1655 }
1656
1657 if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
1658 $list .= "($value)";
1659 } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
1660 $list .= "$value";
1661 } elseif (
1662 ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
1663 ) {
1664 // Remove null from array to be handled separately if found
1665 $includeNull = false;
1666 foreach ( array_keys( $value, null, true ) as $nullKey ) {
1667 $includeNull = true;
1668 unset( $value[$nullKey] );
1669 }
1670 if ( count( $value ) == 0 && !$includeNull ) {
1671 throw new InvalidArgumentException(
1672 __METHOD__ . ": empty input for field $field" );
1673 } elseif ( count( $value ) == 0 ) {
1674 // only check if $field is null
1675 $list .= "$field IS NULL";
1676 } else {
1677 // IN clause contains at least one valid element
1678 if ( $includeNull ) {
1679 // Group subconditions to ensure correct precedence
1680 $list .= '(';
1681 }
1682 if ( count( $value ) == 1 ) {
1683 // Special-case single values, as IN isn't terribly efficient
1684 // Don't necessarily assume the single key is 0; we don't
1685 // enforce linear numeric ordering on other arrays here.
1686 $value = array_values( $value )[0];
1687 $list .= $field . " = " . $this->addQuotes( $value );
1688 } else {
1689 $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
1690 }
1691 // if null present in array, append IS NULL
1692 if ( $includeNull ) {
1693 $list .= " OR $field IS NULL)";
1694 }
1695 }
1696 } elseif ( $value === null ) {
1697 if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
1698 $list .= "$field IS ";
1699 } elseif ( $mode == self::LIST_SET ) {
1700 $list .= "$field = ";
1701 }
1702 $list .= 'NULL';
1703 } else {
1704 if (
1705 $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
1706 ) {
1707 $list .= "$field = ";
1708 }
1709 $list .= $mode == self::LIST_NAMES ? $value : $this->addQuotes( $value );
1710 }
1711 }
1712
1713 return $list;
1714 }
1715
1716 public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
1717 $conds = [];
1718
1719 foreach ( $data as $base => $sub ) {
1720 if ( count( $sub ) ) {
1721 $conds[] = $this->makeList(
1722 [ $baseKey => $base, $subKey => array_keys( $sub ) ],
1723 self::LIST_AND );
1724 }
1725 }
1726
1727 if ( $conds ) {
1728 return $this->makeList( $conds, self::LIST_OR );
1729 } else {
1730 // Nothing to search for...
1731 return false;
1732 }
1733 }
1734
1735 public function aggregateValue( $valuedata, $valuename = 'value' ) {
1736 return $valuename;
1737 }
1738
1739 public function bitNot( $field ) {
1740 return "(~$field)";
1741 }
1742
1743 public function bitAnd( $fieldLeft, $fieldRight ) {
1744 return "($fieldLeft & $fieldRight)";
1745 }
1746
1747 public function bitOr( $fieldLeft, $fieldRight ) {
1748 return "($fieldLeft | $fieldRight)";
1749 }
1750
1751 public function buildConcat( $stringList ) {
1752 return 'CONCAT(' . implode( ',', $stringList ) . ')';
1753 }
1754
1755 public function buildGroupConcatField(
1756 $delim, $table, $field, $conds = '', $join_conds = []
1757 ) {
1758 $fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
1759
1760 return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
1761 }
1762
1763 public function buildStringCast( $field ) {
1764 return $field;
1765 }
1766
1767 public function databasesAreIndependent() {
1768 return false;
1769 }
1770
1771 public function selectDB( $db ) {
1772 # Stub. Shouldn't cause serious problems if it's not overridden, but
1773 # if your database engine supports a concept similar to MySQL's
1774 # databases you may as well.
1775 $this->mDBname = $db;
1776
1777 return true;
1778 }
1779
1780 public function getDBname() {
1781 return $this->mDBname;
1782 }
1783
1784 public function getServer() {
1785 return $this->mServer;
1786 }
1787
1788 public function tableName( $name, $format = 'quoted' ) {
1789 # Skip the entire process when we have a string quoted on both ends.
1790 # Note that we check the end so that we will still quote any use of
1791 # use of `database`.table. But won't break things if someone wants
1792 # to query a database table with a dot in the name.
1793 if ( $this->isQuotedIdentifier( $name ) ) {
1794 return $name;
1795 }
1796
1797 # Lets test for any bits of text that should never show up in a table
1798 # name. Basically anything like JOIN or ON which are actually part of
1799 # SQL queries, but may end up inside of the table value to combine
1800 # sql. Such as how the API is doing.
1801 # Note that we use a whitespace test rather than a \b test to avoid
1802 # any remote case where a word like on may be inside of a table name
1803 # surrounded by symbols which may be considered word breaks.
1804 if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
1805 return $name;
1806 }
1807
1808 # Split database and table into proper variables.
1809 list( $database, $schema, $prefix, $table ) = $this->qualifiedTableComponents( $name );
1810
1811 # Quote $table and apply the prefix if not quoted.
1812 # $tableName might be empty if this is called from Database::replaceVars()
1813 $tableName = "{$prefix}{$table}";
1814 if ( $format === 'quoted'
1815 && !$this->isQuotedIdentifier( $tableName )
1816 && $tableName !== ''
1817 ) {
1818 $tableName = $this->addIdentifierQuotes( $tableName );
1819 }
1820
1821 # Quote $schema and $database and merge them with the table name if needed
1822 $tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
1823 $tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
1824
1825 return $tableName;
1826 }
1827
1834 protected function qualifiedTableComponents( $name ) {
1835 # We reverse the explode so that database.table and table both output the correct table.
1836 $dbDetails = explode( '.', $name, 3 );
1837 if ( count( $dbDetails ) == 3 ) {
1838 list( $database, $schema, $table ) = $dbDetails;
1839 # We don't want any prefix added in this case
1840 $prefix = '';
1841 } elseif ( count( $dbDetails ) == 2 ) {
1842 list( $database, $table ) = $dbDetails;
1843 # We don't want any prefix added in this case
1844 $prefix = '';
1845 # In dbs that support it, $database may actually be the schema
1846 # but that doesn't affect any of the functionality here
1847 $schema = '';
1848 } else {
1849 list( $table ) = $dbDetails;
1850 if ( isset( $this->tableAliases[$table] ) ) {
1851 $database = $this->tableAliases[$table]['dbname'];
1852 $schema = is_string( $this->tableAliases[$table]['schema'] )
1853 ? $this->tableAliases[$table]['schema']
1855 $prefix = is_string( $this->tableAliases[$table]['prefix'] )
1856 ? $this->tableAliases[$table]['prefix']
1858 } else {
1859 $database = '';
1860 $schema = $this->mSchema; # Default schema
1861 $prefix = $this->mTablePrefix; # Default prefix
1862 }
1863 }
1864
1865 return [ $database, $schema, $prefix, $table ];
1866 }
1867
1874 private function prependDatabaseOrSchema( $namespace, $relation, $format ) {
1875 if ( strlen( $namespace ) ) {
1876 if ( $format === 'quoted' && !$this->isQuotedIdentifier( $namespace ) ) {
1877 $namespace = $this->addIdentifierQuotes( $namespace );
1878 }
1879 $relation = $namespace . '.' . $relation;
1880 }
1881
1882 return $relation;
1883 }
1884
1885 public function tableNames() {
1886 $inArray = func_get_args();
1887 $retVal = [];
1888
1889 foreach ( $inArray as $name ) {
1890 $retVal[$name] = $this->tableName( $name );
1891 }
1892
1893 return $retVal;
1894 }
1895
1896 public function tableNamesN() {
1897 $inArray = func_get_args();
1898 $retVal = [];
1899
1900 foreach ( $inArray as $name ) {
1901 $retVal[] = $this->tableName( $name );
1902 }
1903
1904 return $retVal;
1905 }
1906
1915 protected function tableNameWithAlias( $name, $alias = false ) {
1916 if ( !$alias || $alias == $name ) {
1917 return $this->tableName( $name );
1918 } else {
1919 return $this->tableName( $name ) . ' ' . $this->addIdentifierQuotes( $alias );
1920 }
1921 }
1922
1929 protected function tableNamesWithAlias( $tables ) {
1930 $retval = [];
1931 foreach ( $tables as $alias => $table ) {
1932 if ( is_numeric( $alias ) ) {
1933 $alias = $table;
1934 }
1935 $retval[] = $this->tableNameWithAlias( $table, $alias );
1936 }
1937
1938 return $retval;
1939 }
1940
1949 protected function fieldNameWithAlias( $name, $alias = false ) {
1950 if ( !$alias || (string)$alias === (string)$name ) {
1951 return $name;
1952 } else {
1953 return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
1954 }
1955 }
1956
1963 protected function fieldNamesWithAlias( $fields ) {
1964 $retval = [];
1965 foreach ( $fields as $alias => $field ) {
1966 if ( is_numeric( $alias ) ) {
1967 $alias = $field;
1968 }
1969 $retval[] = $this->fieldNameWithAlias( $field, $alias );
1970 }
1971
1972 return $retval;
1973 }
1974
1986 $tables, $use_index = [], $ignore_index = [], $join_conds = []
1987 ) {
1988 $ret = [];
1989 $retJOIN = [];
1990 $use_index = (array)$use_index;
1991 $ignore_index = (array)$ignore_index;
1992 $join_conds = (array)$join_conds;
1993
1994 foreach ( $tables as $alias => $table ) {
1995 if ( !is_string( $alias ) ) {
1996 // No alias? Set it equal to the table name
1997 $alias = $table;
1998 }
1999 // Is there a JOIN clause for this table?
2000 if ( isset( $join_conds[$alias] ) ) {
2001 list( $joinType, $conds ) = $join_conds[$alias];
2002 $tableClause = $joinType;
2003 $tableClause .= ' ' . $this->tableNameWithAlias( $table, $alias );
2004 if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
2005 $use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
2006 if ( $use != '' ) {
2007 $tableClause .= ' ' . $use;
2008 }
2009 }
2010 if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
2011 $ignore = $this->ignoreIndexClause(
2012 implode( ',', (array)$ignore_index[$alias] ) );
2013 if ( $ignore != '' ) {
2014 $tableClause .= ' ' . $ignore;
2015 }
2016 }
2017 $on = $this->makeList( (array)$conds, self::LIST_AND );
2018 if ( $on != '' ) {
2019 $tableClause .= ' ON (' . $on . ')';
2020 }
2021
2022 $retJOIN[] = $tableClause;
2023 } elseif ( isset( $use_index[$alias] ) ) {
2024 // Is there an INDEX clause for this table?
2025 $tableClause = $this->tableNameWithAlias( $table, $alias );
2026 $tableClause .= ' ' . $this->useIndexClause(
2027 implode( ',', (array)$use_index[$alias] )
2028 );
2029
2030 $ret[] = $tableClause;
2031 } elseif ( isset( $ignore_index[$alias] ) ) {
2032 // Is there an INDEX clause for this table?
2033 $tableClause = $this->tableNameWithAlias( $table, $alias );
2034 $tableClause .= ' ' . $this->ignoreIndexClause(
2035 implode( ',', (array)$ignore_index[$alias] )
2036 );
2037
2038 $ret[] = $tableClause;
2039 } else {
2040 $tableClause = $this->tableNameWithAlias( $table, $alias );
2041
2042 $ret[] = $tableClause;
2043 }
2044 }
2045
2046 // We can't separate explicit JOIN clauses with ',', use ' ' for those
2047 $implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
2048 $explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
2049
2050 // Compile our final table clause
2051 return implode( ' ', [ $implicitJoins, $explicitJoins ] );
2052 }
2053
2060 protected function indexName( $index ) {
2061 return $index;
2062 }
2063
2064 public function addQuotes( $s ) {
2065 if ( $s instanceof Blob ) {
2066 $s = $s->fetch();
2067 }
2068 if ( $s === null ) {
2069 return 'NULL';
2070 } elseif ( is_bool( $s ) ) {
2071 return (int)$s;
2072 } else {
2073 # This will also quote numeric values. This should be harmless,
2074 # and protects against weird problems that occur when they really
2075 # _are_ strings such as article titles and string->number->string
2076 # conversion is not 1:1.
2077 return "'" . $this->strencode( $s ) . "'";
2078 }
2079 }
2080
2090 public function addIdentifierQuotes( $s ) {
2091 return '"' . str_replace( '"', '""', $s ) . '"';
2092 }
2093
2103 public function isQuotedIdentifier( $name ) {
2104 return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
2105 }
2106
2112 protected function escapeLikeInternal( $s, $escapeChar = '`' ) {
2113 return str_replace( [ $escapeChar, '%', '_' ],
2114 [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ],
2115 $s );
2116 }
2117
2118 public function buildLike() {
2119 $params = func_get_args();
2120
2121 if ( count( $params ) > 0 && is_array( $params[0] ) ) {
2122 $params = $params[0];
2123 }
2124
2125 $s = '';
2126
2127 // We use ` instead of \ as the default LIKE escape character, since addQuotes()
2128 // may escape backslashes, creating problems of double escaping. The `
2129 // character has good cross-DBMS compatibility, avoiding special operators
2130 // in MS SQL like ^ and %
2131 $escapeChar = '`';
2132
2133 foreach ( $params as $value ) {
2134 if ( $value instanceof LikeMatch ) {
2135 $s .= $value->toString();
2136 } else {
2137 $s .= $this->escapeLikeInternal( $value, $escapeChar );
2138 }
2139 }
2140
2141 return ' LIKE ' . $this->addQuotes( $s ) . ' ESCAPE ' . $this->addQuotes( $escapeChar ) . ' ';
2142 }
2143
2144 public function anyChar() {
2145 return new LikeMatch( '_' );
2146 }
2147
2148 public function anyString() {
2149 return new LikeMatch( '%' );
2150 }
2151
2152 public function nextSequenceValue( $seqName ) {
2153 return null;
2154 }
2155
2166 public function useIndexClause( $index ) {
2167 return '';
2168 }
2169
2180 public function ignoreIndexClause( $index ) {
2181 return '';
2182 }
2183
2184 public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2185 $quotedTable = $this->tableName( $table );
2186
2187 if ( count( $rows ) == 0 ) {
2188 return;
2189 }
2190
2191 # Single row case
2192 if ( !is_array( reset( $rows ) ) ) {
2193 $rows = [ $rows ];
2194 }
2195
2196 // @FXIME: this is not atomic, but a trx would break affectedRows()
2197 foreach ( $rows as $row ) {
2198 # Delete rows which collide
2199 if ( $uniqueIndexes ) {
2200 $sql = "DELETE FROM $quotedTable WHERE ";
2201 $first = true;
2202 foreach ( $uniqueIndexes as $index ) {
2203 if ( $first ) {
2204 $first = false;
2205 $sql .= '( ';
2206 } else {
2207 $sql .= ' ) OR ( ';
2208 }
2209 if ( is_array( $index ) ) {
2210 $first2 = true;
2211 foreach ( $index as $col ) {
2212 if ( $first2 ) {
2213 $first2 = false;
2214 } else {
2215 $sql .= ' AND ';
2216 }
2217 $sql .= $col . '=' . $this->addQuotes( $row[$col] );
2218 }
2219 } else {
2220 $sql .= $index . '=' . $this->addQuotes( $row[$index] );
2221 }
2222 }
2223 $sql .= ' )';
2224 $this->query( $sql, $fname );
2225 }
2226
2227 # Now insert the row
2228 $this->insert( $table, $row, $fname );
2229 }
2230 }
2231
2242 protected function nativeReplace( $table, $rows, $fname ) {
2243 $table = $this->tableName( $table );
2244
2245 # Single row case
2246 if ( !is_array( reset( $rows ) ) ) {
2247 $rows = [ $rows ];
2248 }
2249
2250 $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
2251 $first = true;
2252
2253 foreach ( $rows as $row ) {
2254 if ( $first ) {
2255 $first = false;
2256 } else {
2257 $sql .= ',';
2258 }
2259
2260 $sql .= '(' . $this->makeList( $row ) . ')';
2261 }
2262
2263 return $this->query( $sql, $fname );
2264 }
2265
2266 public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
2267 $fname = __METHOD__
2268 ) {
2269 if ( !count( $rows ) ) {
2270 return true; // nothing to do
2271 }
2272
2273 if ( !is_array( reset( $rows ) ) ) {
2274 $rows = [ $rows ];
2275 }
2276
2277 if ( count( $uniqueIndexes ) ) {
2278 $clauses = []; // list WHERE clauses that each identify a single row
2279 foreach ( $rows as $row ) {
2280 foreach ( $uniqueIndexes as $index ) {
2281 $index = is_array( $index ) ? $index : [ $index ]; // columns
2282 $rowKey = []; // unique key to this row
2283 foreach ( $index as $column ) {
2284 $rowKey[$column] = $row[$column];
2285 }
2286 $clauses[] = $this->makeList( $rowKey, self::LIST_AND );
2287 }
2288 }
2289 $where = [ $this->makeList( $clauses, self::LIST_OR ) ];
2290 } else {
2291 $where = false;
2292 }
2293
2294 $useTrx = !$this->mTrxLevel;
2295 if ( $useTrx ) {
2296 $this->begin( $fname, self::TRANSACTION_INTERNAL );
2297 }
2298 try {
2299 # Update any existing conflicting row(s)
2300 if ( $where !== false ) {
2301 $ok = $this->update( $table, $set, $where, $fname );
2302 } else {
2303 $ok = true;
2304 }
2305 # Now insert any non-conflicting row(s)
2306 $ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
2307 } catch ( Exception $e ) {
2308 if ( $useTrx ) {
2309 $this->rollback( $fname, self::FLUSHING_INTERNAL );
2310 }
2311 throw $e;
2312 }
2313 if ( $useTrx ) {
2314 $this->commit( $fname, self::FLUSHING_INTERNAL );
2315 }
2316
2317 return $ok;
2318 }
2319
2320 public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2321 $fname = __METHOD__
2322 ) {
2323 if ( !$conds ) {
2324 throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
2325 }
2326
2327 $delTable = $this->tableName( $delTable );
2328 $joinTable = $this->tableName( $joinTable );
2329 $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2330 if ( $conds != '*' ) {
2331 $sql .= 'WHERE ' . $this->makeList( $conds, self::LIST_AND );
2332 }
2333 $sql .= ')';
2334
2335 $this->query( $sql, $fname );
2336 }
2337
2338 public function textFieldSize( $table, $field ) {
2339 $table = $this->tableName( $table );
2340 $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
2341 $res = $this->query( $sql, __METHOD__ );
2342 $row = $this->fetchObject( $res );
2343
2344 $m = [];
2345
2346 if ( preg_match( '/\‍((.*)\‍)/', $row->Type, $m ) ) {
2347 $size = $m[1];
2348 } else {
2349 $size = -1;
2350 }
2351
2352 return $size;
2353 }
2354
2355 public function delete( $table, $conds, $fname = __METHOD__ ) {
2356 if ( !$conds ) {
2357 throw new DBUnexpectedError( $this, __METHOD__ . ' called with no conditions' );
2358 }
2359
2360 $table = $this->tableName( $table );
2361 $sql = "DELETE FROM $table";
2362
2363 if ( $conds != '*' ) {
2364 if ( is_array( $conds ) ) {
2365 $conds = $this->makeList( $conds, self::LIST_AND );
2366 }
2367 $sql .= ' WHERE ' . $conds;
2368 }
2369
2370 return $this->query( $sql, $fname );
2371 }
2372
2373 public function insertSelect(
2374 $destTable, $srcTable, $varMap, $conds,
2375 $fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = []
2376 ) {
2377 if ( $this->cliMode ) {
2378 // For massive migrations with downtime, we don't want to select everything
2379 // into memory and OOM, so do all this native on the server side if possible.
2380 return $this->nativeInsertSelect(
2381 $destTable,
2382 $srcTable,
2383 $varMap,
2384 $conds,
2385 $fname,
2386 $insertOptions,
2387 $selectOptions,
2388 $selectJoinConds
2389 );
2390 }
2391
2392 // For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
2393 // on only the master (without needing row-based-replication). It also makes it easy to
2394 // know how big the INSERT is going to be.
2395 $fields = [];
2396 foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
2397 $fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
2398 }
2399 $selectOptions[] = 'FOR UPDATE';
2400 $res = $this->select(
2401 $srcTable, implode( ',', $fields ), $conds, $fname, $selectOptions, $selectJoinConds
2402 );
2403 if ( !$res ) {
2404 return false;
2405 }
2406
2407 $rows = [];
2408 foreach ( $res as $row ) {
2409 $rows[] = (array)$row;
2410 }
2411
2412 return $this->insert( $destTable, $rows, $fname, $insertOptions );
2413 }
2414
2430 protected function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
2431 $fname = __METHOD__,
2432 $insertOptions = [], $selectOptions = [], $selectJoinConds = []
2433 ) {
2434 $destTable = $this->tableName( $destTable );
2435
2436 if ( !is_array( $insertOptions ) ) {
2437 $insertOptions = [ $insertOptions ];
2438 }
2439
2440 $insertOptions = $this->makeInsertOptions( $insertOptions );
2441
2442 $selectSql = $this->selectSQLText(
2443 $srcTable,
2444 array_values( $varMap ),
2445 $conds,
2446 $fname,
2447 $selectOptions,
2448 $selectJoinConds
2449 );
2450
2451 $sql = "INSERT $insertOptions" .
2452 " INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ') ' .
2453 $selectSql;
2454
2455 return $this->query( $sql, $fname );
2456 }
2457
2477 public function limitResult( $sql, $limit, $offset = false ) {
2478 if ( !is_numeric( $limit ) ) {
2479 throw new DBUnexpectedError( $this,
2480 "Invalid non-numeric limit passed to limitResult()\n" );
2481 }
2482
2483 return "$sql LIMIT "
2484 . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
2485 . "{$limit} ";
2486 }
2487
2488 public function unionSupportsOrderAndLimit() {
2489 return true; // True for almost every DB supported
2490 }
2491
2492 public function unionQueries( $sqls, $all ) {
2493 $glue = $all ? ') UNION ALL (' : ') UNION (';
2494
2495 return '(' . implode( $glue, $sqls ) . ')';
2496 }
2497
2499 $table, $vars, array $permute_conds, $extra_conds = '', $fname = __METHOD__,
2500 $options = [], $join_conds = []
2501 ) {
2502 // First, build the Cartesian product of $permute_conds
2503 $conds = [ [] ];
2504 foreach ( $permute_conds as $field => $values ) {
2505 if ( !$values ) {
2506 // Skip empty $values
2507 continue;
2508 }
2509 $values = array_unique( $values ); // For sanity
2510 $newConds = [];
2511 foreach ( $conds as $cond ) {
2512 foreach ( $values as $value ) {
2513 $cond[$field] = $value;
2514 $newConds[] = $cond; // Arrays are by-value, not by-reference, so this works
2515 }
2516 }
2517 $conds = $newConds;
2518 }
2519
2520 $extra_conds = $extra_conds === '' ? [] : (array)$extra_conds;
2521
2522 // If there's just one condition and no subordering, hand off to
2523 // selectSQLText directly.
2524 if ( count( $conds ) === 1 &&
2525 ( !isset( $options['INNER ORDER BY'] ) || !$this->unionSupportsOrderAndLimit() )
2526 ) {
2527 return $this->selectSQLText(
2528 $table, $vars, $conds[0] + $extra_conds, $fname, $options, $join_conds
2529 );
2530 }
2531
2532 // Otherwise, we need to pull out the order and limit to apply after
2533 // the union. Then build the SQL queries for each set of conditions in
2534 // $conds. Then union them together (using UNION ALL, because the
2535 // product *should* already be distinct).
2536 $orderBy = $this->makeOrderBy( $options );
2537 $limit = isset( $options['LIMIT'] ) ? $options['LIMIT'] : null;
2538 $offset = isset( $options['OFFSET'] ) ? $options['OFFSET'] : false;
2539 $all = empty( $options['NOTALL'] ) && !in_array( 'NOTALL', $options );
2540 if ( !$this->unionSupportsOrderAndLimit() ) {
2541 unset( $options['ORDER BY'], $options['LIMIT'], $options['OFFSET'] );
2542 } else {
2543 if ( array_key_exists( 'INNER ORDER BY', $options ) ) {
2544 $options['ORDER BY'] = $options['INNER ORDER BY'];
2545 }
2546 if ( $limit !== null && is_numeric( $offset ) && $offset != 0 ) {
2547 // We need to increase the limit by the offset rather than
2548 // using the offset directly, otherwise it'll skip incorrectly
2549 // in the subqueries.
2550 $options['LIMIT'] = $limit + $offset;
2551 unset( $options['OFFSET'] );
2552 }
2553 }
2554
2555 $sqls = [];
2556 foreach ( $conds as $cond ) {
2557 $sqls[] = $this->selectSQLText(
2558 $table, $vars, $cond + $extra_conds, $fname, $options, $join_conds
2559 );
2560 }
2561 $sql = $this->unionQueries( $sqls, $all ) . $orderBy;
2562 if ( $limit !== null ) {
2563 $sql = $this->limitResult( $sql, $limit, $offset );
2564 }
2565
2566 return $sql;
2567 }
2568
2569 public function conditional( $cond, $trueVal, $falseVal ) {
2570 if ( is_array( $cond ) ) {
2571 $cond = $this->makeList( $cond, self::LIST_AND );
2572 }
2573
2574 return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
2575 }
2576
2577 public function strreplace( $orig, $old, $new ) {
2578 return "REPLACE({$orig}, {$old}, {$new})";
2579 }
2580
2581 public function getServerUptime() {
2582 return 0;
2583 }
2584
2585 public function wasDeadlock() {
2586 return false;
2587 }
2588
2589 public function wasLockTimeout() {
2590 return false;
2591 }
2592
2593 public function wasErrorReissuable() {
2594 return false;
2595 }
2596
2597 public function wasReadOnlyError() {
2598 return false;
2599 }
2600
2607 public function wasConnectionError( $errno ) {
2608 return false;
2609 }
2610
2611 public function deadlockLoop() {
2612 $args = func_get_args();
2613 $function = array_shift( $args );
2614 $tries = self::DEADLOCK_TRIES;
2615
2616 $this->begin( __METHOD__ );
2617
2618 $retVal = null;
2620 $e = null;
2621 do {
2622 try {
2623 $retVal = call_user_func_array( $function, $args );
2624 break;
2625 } catch ( DBQueryError $e ) {
2626 if ( $this->wasDeadlock() ) {
2627 // Retry after a randomized delay
2628 usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
2629 } else {
2630 // Throw the error back up
2631 throw $e;
2632 }
2633 }
2634 } while ( --$tries > 0 );
2635
2636 if ( $tries <= 0 ) {
2637 // Too many deadlocks; give up
2638 $this->rollback( __METHOD__ );
2639 throw $e;
2640 } else {
2641 $this->commit( __METHOD__ );
2642
2643 return $retVal;
2644 }
2645 }
2646
2647 public function masterPosWait( DBMasterPos $pos, $timeout ) {
2648 # Real waits are implemented in the subclass.
2649 return 0;
2650 }
2651
2652 public function getReplicaPos() {
2653 # Stub
2654 return false;
2655 }
2656
2657 public function getMasterPos() {
2658 # Stub
2659 return false;
2660 }
2661
2662 public function serverIsReadOnly() {
2663 return false;
2664 }
2665
2666 final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
2667 if ( !$this->mTrxLevel ) {
2668 throw new DBUnexpectedError( $this, "No transaction is active." );
2669 }
2670 $this->mTrxEndCallbacks[] = [ $callback, $fname ];
2671 }
2672
2673 final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
2674 $this->mTrxIdleCallbacks[] = [ $callback, $fname ];
2675 if ( !$this->mTrxLevel ) {
2676 $this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
2677 }
2678 }
2679
2680 final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
2681 if ( $this->mTrxLevel || $this->getFlag( self::DBO_TRX ) ) {
2682 // As long as DBO_TRX is set, writes will accumulate until the load balancer issues
2683 // an implicit commit of all peer databases. This is true even if a transaction has
2684 // not yet been triggered by writes; make sure $callback runs *after* any such writes.
2685 $this->mTrxPreCommitCallbacks[] = [ $callback, $fname ];
2686 } else {
2687 // No transaction is active nor will start implicitly, so make one for this callback
2688 $this->startAtomic( __METHOD__ );
2689 try {
2690 call_user_func( $callback );
2691 $this->endAtomic( __METHOD__ );
2692 } catch ( Exception $e ) {
2693 $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
2694 throw $e;
2695 }
2696 }
2697 }
2698
2699 final public function setTransactionListener( $name, callable $callback = null ) {
2700 if ( $callback ) {
2701 $this->mTrxRecurringCallbacks[$name] = $callback;
2702 } else {
2703 unset( $this->mTrxRecurringCallbacks[$name] );
2704 }
2705 }
2706
2715 final public function setTrxEndCallbackSuppression( $suppress ) {
2716 $this->mTrxEndCallbacksSuppressed = $suppress;
2717 }
2718
2728 public function runOnTransactionIdleCallbacks( $trigger ) {
2729 if ( $this->mTrxEndCallbacksSuppressed ) {
2730 return;
2731 }
2732
2733 $autoTrx = $this->getFlag( self::DBO_TRX ); // automatic begin() enabled?
2735 $e = null; // first exception
2736 do { // callbacks may add callbacks :)
2737 $callbacks = array_merge(
2738 $this->mTrxIdleCallbacks,
2739 $this->mTrxEndCallbacks // include "transaction resolution" callbacks
2740 );
2741 $this->mTrxIdleCallbacks = []; // consumed (and recursion guard)
2742 $this->mTrxEndCallbacks = []; // consumed (recursion guard)
2743 foreach ( $callbacks as $callback ) {
2744 try {
2745 list( $phpCallback ) = $callback;
2746 $this->clearFlag( self::DBO_TRX ); // make each query its own transaction
2747 call_user_func_array( $phpCallback, [ $trigger ] );
2748 if ( $autoTrx ) {
2749 $this->setFlag( self::DBO_TRX ); // restore automatic begin()
2750 } else {
2751 $this->clearFlag( self::DBO_TRX ); // restore auto-commit
2752 }
2753 } catch ( Exception $ex ) {
2754 call_user_func( $this->errorLogger, $ex );
2755 $e = $e ?: $ex;
2756 // Some callbacks may use startAtomic/endAtomic, so make sure
2757 // their transactions are ended so other callbacks don't fail
2758 if ( $this->trxLevel() ) {
2759 $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
2760 }
2761 }
2762 }
2763 } while ( count( $this->mTrxIdleCallbacks ) );
2764
2765 if ( $e instanceof Exception ) {
2766 throw $e; // re-throw any first exception
2767 }
2768 }
2769
2779 $e = null; // first exception
2780 do { // callbacks may add callbacks :)
2781 $callbacks = $this->mTrxPreCommitCallbacks;
2782 $this->mTrxPreCommitCallbacks = []; // consumed (and recursion guard)
2783 foreach ( $callbacks as $callback ) {
2784 try {
2785 list( $phpCallback ) = $callback;
2786 call_user_func( $phpCallback );
2787 } catch ( Exception $ex ) {
2788 call_user_func( $this->errorLogger, $ex );
2789 $e = $e ?: $ex;
2790 }
2791 }
2792 } while ( count( $this->mTrxPreCommitCallbacks ) );
2793
2794 if ( $e instanceof Exception ) {
2795 throw $e; // re-throw any first exception
2796 }
2797 }
2798
2808 public function runTransactionListenerCallbacks( $trigger ) {
2809 if ( $this->mTrxEndCallbacksSuppressed ) {
2810 return;
2811 }
2812
2814 $e = null; // first exception
2815
2816 foreach ( $this->mTrxRecurringCallbacks as $phpCallback ) {
2817 try {
2818 $phpCallback( $trigger, $this );
2819 } catch ( Exception $ex ) {
2820 call_user_func( $this->errorLogger, $ex );
2821 $e = $e ?: $ex;
2822 }
2823 }
2824
2825 if ( $e instanceof Exception ) {
2826 throw $e; // re-throw any first exception
2827 }
2828 }
2829
2830 final public function startAtomic( $fname = __METHOD__ ) {
2831 if ( !$this->mTrxLevel ) {
2832 $this->begin( $fname, self::TRANSACTION_INTERNAL );
2833 // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
2834 // in all changes being in one transaction to keep requests transactional.
2835 if ( !$this->getFlag( self::DBO_TRX ) ) {
2836 $this->mTrxAutomaticAtomic = true;
2837 }
2838 }
2839
2840 $this->mTrxAtomicLevels[] = $fname;
2841 }
2842
2843 final public function endAtomic( $fname = __METHOD__ ) {
2844 if ( !$this->mTrxLevel ) {
2845 throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." );
2846 }
2847 if ( !$this->mTrxAtomicLevels ||
2848 array_pop( $this->mTrxAtomicLevels ) !== $fname
2849 ) {
2850 throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." );
2851 }
2852
2853 if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
2854 $this->commit( $fname, self::FLUSHING_INTERNAL );
2855 }
2856 }
2857
2858 final public function doAtomicSection( $fname, callable $callback ) {
2859 $this->startAtomic( $fname );
2860 try {
2861 $res = call_user_func_array( $callback, [ $this, $fname ] );
2862 } catch ( Exception $e ) {
2863 $this->rollback( $fname, self::FLUSHING_INTERNAL );
2864 throw $e;
2865 }
2866 $this->endAtomic( $fname );
2867
2868 return $res;
2869 }
2870
2871 final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
2872 // Protect against mismatched atomic section, transaction nesting, and snapshot loss
2873 if ( $this->mTrxLevel ) {
2874 if ( $this->mTrxAtomicLevels ) {
2875 $levels = implode( ', ', $this->mTrxAtomicLevels );
2876 $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
2877 throw new DBUnexpectedError( $this, $msg );
2878 } elseif ( !$this->mTrxAutomatic ) {
2879 $msg = "$fname: Explicit transaction already active (from {$this->mTrxFname}).";
2880 throw new DBUnexpectedError( $this, $msg );
2881 } else {
2882 // @TODO: make this an exception at some point
2883 $msg = "$fname: Implicit transaction already active (from {$this->mTrxFname}).";
2884 $this->queryLogger->error( $msg );
2885 return; // join the main transaction set
2886 }
2887 } elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
2888 // @TODO: make this an exception at some point
2889 $msg = "$fname: Implicit transaction expected (DBO_TRX set).";
2890 $this->queryLogger->error( $msg );
2891 return; // let any writes be in the main transaction
2892 }
2893
2894 // Avoid fatals if close() was called
2895 $this->assertOpen();
2896
2897 $this->doBegin( $fname );
2898 $this->mTrxTimestamp = microtime( true );
2899 $this->mTrxFname = $fname;
2900 $this->mTrxDoneWrites = false;
2901 $this->mTrxAutomaticAtomic = false;
2902 $this->mTrxAtomicLevels = [];
2903 $this->mTrxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
2904 $this->mTrxWriteDuration = 0.0;
2905 $this->mTrxWriteQueryCount = 0;
2906 $this->mTrxWriteAffectedRows = 0;
2907 $this->mTrxWriteAdjDuration = 0.0;
2908 $this->mTrxWriteAdjQueryCount = 0;
2909 $this->mTrxWriteCallers = [];
2910 // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
2911 // Get an estimate of the replica DB lag before then, treating estimate staleness
2912 // as lag itself just to be safe
2914 $this->mTrxReplicaLag = $status['lag'] + ( microtime( true ) - $status['since'] );
2915 // T147697: make explicitTrxActive() return true until begin() finishes. This way, no
2916 // caller will think its OK to muck around with the transaction just because startAtomic()
2917 // has not yet completed (e.g. setting mTrxAtomicLevels).
2918 $this->mTrxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
2919 }
2920
2927 protected function doBegin( $fname ) {
2928 $this->query( 'BEGIN', $fname );
2929 $this->mTrxLevel = 1;
2930 }
2931
2932 final public function commit( $fname = __METHOD__, $flush = '' ) {
2933 if ( $this->mTrxLevel && $this->mTrxAtomicLevels ) {
2934 // There are still atomic sections open. This cannot be ignored
2935 $levels = implode( ', ', $this->mTrxAtomicLevels );
2936 throw new DBUnexpectedError(
2937 $this,
2938 "$fname: Got COMMIT while atomic sections $levels are still open."
2939 );
2940 }
2941
2942 if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
2943 if ( !$this->mTrxLevel ) {
2944 return; // nothing to do
2945 } elseif ( !$this->mTrxAutomatic ) {
2946 throw new DBUnexpectedError(
2947 $this,
2948 "$fname: Flushing an explicit transaction, getting out of sync."
2949 );
2950 }
2951 } else {
2952 if ( !$this->mTrxLevel ) {
2953 $this->queryLogger->error(
2954 "$fname: No transaction to commit, something got out of sync." );
2955 return; // nothing to do
2956 } elseif ( $this->mTrxAutomatic ) {
2957 // @TODO: make this an exception at some point
2958 $msg = "$fname: Explicit commit of implicit transaction.";
2959 $this->queryLogger->error( $msg );
2960 return; // wait for the main transaction set commit round
2961 }
2962 }
2963
2964 // Avoid fatals if close() was called
2965 $this->assertOpen();
2966
2968 $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
2969 $this->doCommit( $fname );
2970 if ( $this->mTrxDoneWrites ) {
2971 $this->mLastWriteTime = microtime( true );
2972 $this->trxProfiler->transactionWritingOut(
2973 $this->mServer,
2974 $this->mDBname,
2975 $this->mTrxShortId,
2976 $writeTime,
2977 $this->mTrxWriteAffectedRows
2978 );
2979 }
2980
2981 $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
2982 $this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
2983 }
2984
2991 protected function doCommit( $fname ) {
2992 if ( $this->mTrxLevel ) {
2993 $this->query( 'COMMIT', $fname );
2994 $this->mTrxLevel = 0;
2995 }
2996 }
2997
2998 final public function rollback( $fname = __METHOD__, $flush = '' ) {
2999 if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
3000 if ( !$this->mTrxLevel ) {
3001 return; // nothing to do
3002 }
3003 } else {
3004 if ( !$this->mTrxLevel ) {
3005 $this->queryLogger->error(
3006 "$fname: No transaction to rollback, something got out of sync." );
3007 return; // nothing to do
3008 } elseif ( $this->getFlag( self::DBO_TRX ) ) {
3009 throw new DBUnexpectedError(
3010 $this,
3011 "$fname: Expected mass rollback of all peer databases (DBO_TRX set)."
3012 );
3013 }
3014 }
3015
3016 // Avoid fatals if close() was called
3017 $this->assertOpen();
3018
3019 $this->doRollback( $fname );
3020 $this->mTrxAtomicLevels = [];
3021 if ( $this->mTrxDoneWrites ) {
3022 $this->trxProfiler->transactionWritingOut(
3023 $this->mServer,
3024 $this->mDBname,
3025 $this->mTrxShortId
3026 );
3027 }
3028
3029 $this->mTrxIdleCallbacks = []; // clear
3030 $this->mTrxPreCommitCallbacks = []; // clear
3031 $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
3032 $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
3033 }
3034
3041 protected function doRollback( $fname ) {
3042 if ( $this->mTrxLevel ) {
3043 # Disconnects cause rollback anyway, so ignore those errors
3044 $ignoreErrors = true;
3045 $this->query( 'ROLLBACK', $fname, $ignoreErrors );
3046 $this->mTrxLevel = 0;
3047 }
3048 }
3049
3050 public function flushSnapshot( $fname = __METHOD__ ) {
3051 if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
3052 // This only flushes transactions to clear snapshots, not to write data
3053 $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
3054 throw new DBUnexpectedError(
3055 $this,
3056 "$fname: Cannot flush snapshot because writes are pending ($fnames)."
3057 );
3058 }
3059
3060 $this->commit( $fname, self::FLUSHING_INTERNAL );
3061 }
3062
3063 public function explicitTrxActive() {
3064 return $this->mTrxLevel && ( $this->mTrxAtomicLevels || !$this->mTrxAutomatic );
3065 }
3066
3068 $oldName, $newName, $temporary = false, $fname = __METHOD__
3069 ) {
3070 throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
3071 }
3072
3073 public function listTables( $prefix = null, $fname = __METHOD__ ) {
3074 throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
3075 }
3076
3077 public function listViews( $prefix = null, $fname = __METHOD__ ) {
3078 throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
3079 }
3080
3081 public function timestamp( $ts = 0 ) {
3082 $t = new ConvertibleTimestamp( $ts );
3083 // Let errors bubble up to avoid putting garbage in the DB
3084 return $t->getTimestamp( TS_MW );
3085 }
3086
3087 public function timestampOrNull( $ts = null ) {
3088 if ( is_null( $ts ) ) {
3089 return null;
3090 } else {
3091 return $this->timestamp( $ts );
3092 }
3093 }
3094
3108 protected function resultObject( $result ) {
3109 if ( !$result ) {
3110 return false;
3111 } elseif ( $result instanceof ResultWrapper ) {
3112 return $result;
3113 } elseif ( $result === true ) {
3114 // Successful write query
3115 return $result;
3116 } else {
3117 return new ResultWrapper( $this, $result );
3118 }
3119 }
3120
3121 public function ping( &$rtt = null ) {
3122 // Avoid hitting the server if it was hit recently
3123 if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
3124 if ( !func_num_args() || $this->mRTTEstimate > 0 ) {
3125 $rtt = $this->mRTTEstimate;
3126 return true; // don't care about $rtt
3127 }
3128 }
3129
3130 // This will reconnect if possible or return false if not
3131 $this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
3132 $ok = ( $this->query( self::PING_QUERY, __METHOD__, true ) !== false );
3133 $this->restoreFlags( self::RESTORE_PRIOR );
3134
3135 if ( $ok ) {
3136 $rtt = $this->mRTTEstimate;
3137 }
3138
3139 return $ok;
3140 }
3141
3147 protected function reconnect() {
3148 $this->closeConnection();
3149 $this->mOpened = false;
3150 $this->mConn = false;
3151 try {
3152 $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
3153 $this->lastPing = microtime( true );
3154 $ok = true;
3155 } catch ( DBConnectionError $e ) {
3156 $ok = false;
3157 }
3158
3159 return $ok;
3160 }
3161
3162 public function getSessionLagStatus() {
3163 return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
3164 }
3165
3177 protected function getTransactionLagStatus() {
3178 return $this->mTrxLevel
3179 ? [ 'lag' => $this->mTrxReplicaLag, 'since' => $this->trxTimestamp() ]
3180 : null;
3181 }
3182
3189 protected function getApproximateLagStatus() {
3190 return [
3191 'lag' => $this->getLBInfo( 'replica' ) ? $this->getLag() : 0,
3192 'since' => microtime( true )
3193 ];
3194 }
3195
3214 public static function getCacheSetOptions( IDatabase $db1 ) {
3215 $res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
3216 foreach ( func_get_args() as $db ) {
3218 $status = $db->getSessionLagStatus();
3219 if ( $status['lag'] === false ) {
3220 $res['lag'] = false;
3221 } elseif ( $res['lag'] !== false ) {
3222 $res['lag'] = max( $res['lag'], $status['lag'] );
3223 }
3224 $res['since'] = min( $res['since'], $status['since'] );
3225 $res['pending'] = $res['pending'] ?: $db->writesPending();
3226 }
3227
3228 return $res;
3229 }
3230
3231 public function getLag() {
3232 return 0;
3233 }
3234
3235 public function maxListLen() {
3236 return 0;
3237 }
3238
3239 public function encodeBlob( $b ) {
3240 return $b;
3241 }
3242
3243 public function decodeBlob( $b ) {
3244 if ( $b instanceof Blob ) {
3245 $b = $b->fetch();
3246 }
3247 return $b;
3248 }
3249
3250 public function setSessionOptions( array $options ) {
3251 }
3252
3253 public function sourceFile(
3254 $filename,
3255 callable $lineCallback = null,
3256 callable $resultCallback = null,
3257 $fname = false,
3258 callable $inputCallback = null
3259 ) {
3260 MediaWiki\suppressWarnings();
3261 $fp = fopen( $filename, 'r' );
3262 MediaWiki\restoreWarnings();
3263
3264 if ( false === $fp ) {
3265 throw new RuntimeException( "Could not open \"{$filename}\".\n" );
3266 }
3267
3268 if ( !$fname ) {
3269 $fname = __METHOD__ . "( $filename )";
3270 }
3271
3272 try {
3273 $error = $this->sourceStream(
3274 $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
3275 } catch ( Exception $e ) {
3276 fclose( $fp );
3277 throw $e;
3278 }
3279
3280 fclose( $fp );
3281
3282 return $error;
3283 }
3284
3285 public function setSchemaVars( $vars ) {
3286 $this->mSchemaVars = $vars;
3287 }
3288
3289 public function sourceStream(
3290 $fp,
3291 callable $lineCallback = null,
3292 callable $resultCallback = null,
3293 $fname = __METHOD__,
3294 callable $inputCallback = null
3295 ) {
3296 $cmd = '';
3297
3298 while ( !feof( $fp ) ) {
3299 if ( $lineCallback ) {
3300 call_user_func( $lineCallback );
3301 }
3302
3303 $line = trim( fgets( $fp ) );
3304
3305 if ( $line == '' ) {
3306 continue;
3307 }
3308
3309 if ( '-' == $line[0] && '-' == $line[1] ) {
3310 continue;
3311 }
3312
3313 if ( $cmd != '' ) {
3314 $cmd .= ' ';
3315 }
3316
3317 $done = $this->streamStatementEnd( $cmd, $line );
3318
3319 $cmd .= "$line\n";
3320
3321 if ( $done || feof( $fp ) ) {
3322 $cmd = $this->replaceVars( $cmd );
3323
3324 if ( !$inputCallback || call_user_func( $inputCallback, $cmd ) ) {
3325 $res = $this->query( $cmd, $fname );
3326
3327 if ( $resultCallback ) {
3328 call_user_func( $resultCallback, $res, $this );
3329 }
3330
3331 if ( false === $res ) {
3332 $err = $this->lastError();
3333
3334 return "Query \"{$cmd}\" failed with error code \"$err\".\n";
3335 }
3336 }
3337 $cmd = '';
3338 }
3339 }
3340
3341 return true;
3342 }
3343
3351 public function streamStatementEnd( &$sql, &$newLine ) {
3352 if ( $this->delimiter ) {
3353 $prev = $newLine;
3354 $newLine = preg_replace(
3355 '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
3356 if ( $newLine != $prev ) {
3357 return true;
3358 }
3359 }
3360
3361 return false;
3362 }
3363
3384 protected function replaceVars( $ins ) {
3385 $vars = $this->getSchemaVars();
3386 return preg_replace_callback(
3387 '!
3388 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
3389 \'\{\$ (\w+) }\' | # 3. addQuotes
3390 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
3391 /\*\$ (\w+) \*/ # 5. leave unencoded
3392 !x',
3393 function ( $m ) use ( $vars ) {
3394 // Note: Because of <https://bugs.php.net/bug.php?id=51881>,
3395 // check for both nonexistent keys *and* the empty string.
3396 if ( isset( $m[1] ) && $m[1] !== '' ) {
3397 if ( $m[1] === 'i' ) {
3398 return $this->indexName( $m[2] );
3399 } else {
3400 return $this->tableName( $m[2] );
3401 }
3402 } elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
3403 return $this->addQuotes( $vars[$m[3]] );
3404 } elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
3405 return $this->addIdentifierQuotes( $vars[$m[4]] );
3406 } elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
3407 return $vars[$m[5]];
3408 } else {
3409 return $m[0];
3410 }
3411 },
3412 $ins
3413 );
3414 }
3415
3422 protected function getSchemaVars() {
3423 if ( $this->mSchemaVars ) {
3424 return $this->mSchemaVars;
3425 } else {
3426 return $this->getDefaultSchemaVars();
3427 }
3428 }
3429
3438 protected function getDefaultSchemaVars() {
3439 return [];
3440 }
3441
3442 public function lockIsFree( $lockName, $method ) {
3443 return true;
3444 }
3445
3446 public function lock( $lockName, $method, $timeout = 5 ) {
3447 $this->mNamedLocksHeld[$lockName] = 1;
3448
3449 return true;
3450 }
3451
3452 public function unlock( $lockName, $method ) {
3453 unset( $this->mNamedLocksHeld[$lockName] );
3454
3455 return true;
3456 }
3457
3458 public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
3459 if ( $this->writesOrCallbacksPending() ) {
3460 // This only flushes transactions to clear snapshots, not to write data
3461 $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
3462 throw new DBUnexpectedError(
3463 $this,
3464 "$fname: Cannot flush pre-lock snapshot because writes are pending ($fnames)."
3465 );
3466 }
3467
3468 if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
3469 return null;
3470 }
3471
3472 $unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
3473 if ( $this->trxLevel() ) {
3474 // There is a good chance an exception was thrown, causing any early return
3475 // from the caller. Let any error handler get a chance to issue rollback().
3476 // If there isn't one, let the error bubble up and trigger server-side rollback.
3478 function () use ( $lockKey, $fname ) {
3479 $this->unlock( $lockKey, $fname );
3480 },
3481 $fname
3482 );
3483 } else {
3484 $this->unlock( $lockKey, $fname );
3485 }
3486 } );
3487
3488 $this->commit( $fname, self::FLUSHING_INTERNAL );
3489
3490 return $unlocker;
3491 }
3492
3493 public function namedLocksEnqueue() {
3494 return false;
3495 }
3496
3498 return true;
3499 }
3500
3501 final public function lockTables( array $read, array $write, $method ) {
3502 if ( $this->writesOrCallbacksPending() ) {
3503 throw new DBUnexpectedError( $this, "Transaction writes or callbacks still pending." );
3504 }
3505
3506 if ( $this->tableLocksHaveTransactionScope() ) {
3507 $this->startAtomic( $method );
3508 }
3509
3510 return $this->doLockTables( $read, $write, $method );
3511 }
3512
3521 protected function doLockTables( array $read, array $write, $method ) {
3522 return true;
3523 }
3524
3525 final public function unlockTables( $method ) {
3526 if ( $this->tableLocksHaveTransactionScope() ) {
3527 $this->endAtomic( $method );
3528
3529 return true; // locks released on COMMIT/ROLLBACK
3530 }
3531
3532 return $this->doUnlockTables( $method );
3533 }
3534
3541 protected function doUnlockTables( $method ) {
3542 return true;
3543 }
3544
3552 public function dropTable( $tableName, $fName = __METHOD__ ) {
3553 if ( !$this->tableExists( $tableName, $fName ) ) {
3554 return false;
3555 }
3556 $sql = "DROP TABLE " . $this->tableName( $tableName ) . " CASCADE";
3557
3558 return $this->query( $sql, $fName );
3559 }
3560
3561 public function getInfinity() {
3562 return 'infinity';
3563 }
3564
3565 public function encodeExpiry( $expiry ) {
3566 return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3567 ? $this->getInfinity()
3568 : $this->timestamp( $expiry );
3569 }
3570
3571 public function decodeExpiry( $expiry, $format = TS_MW ) {
3572 if ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) {
3573 return 'infinity';
3574 }
3575
3576 return ConvertibleTimestamp::convert( $format, $expiry );
3577 }
3578
3579 public function setBigSelects( $value = true ) {
3580 // no-op
3581 }
3582
3583 public function isReadOnly() {
3584 return ( $this->getReadOnlyReason() !== false );
3585 }
3586
3590 protected function getReadOnlyReason() {
3591 $reason = $this->getLBInfo( 'readOnlyReason' );
3592
3593 return is_string( $reason ) ? $reason : false;
3594 }
3595
3596 public function setTableAliases( array $aliases ) {
3597 $this->tableAliases = $aliases;
3598 }
3599
3604 protected function requiresDatabaseUser() {
3605 return true;
3606 }
3607
3619 protected function getBindingHandle() {
3620 if ( !$this->mConn ) {
3621 throw new DBUnexpectedError(
3622 $this,
3623 'DB connection was already closed or the connection dropped.'
3624 );
3625 }
3626
3627 return $this->mConn;
3628 }
3629
3634 public function __toString() {
3635 return (string)$this->mConn;
3636 }
3637
3642 public function __clone() {
3643 $this->connLogger->warning(
3644 "Cloning " . static::class . " is not recomended; forking connection:\n" .
3645 ( new RuntimeException() )->getTraceAsString()
3646 );
3647
3648 if ( $this->isOpen() ) {
3649 // Open a new connection resource without messing with the old one
3650 $this->mOpened = false;
3651 $this->mConn = false;
3652 $this->mTrxEndCallbacks = []; // don't copy
3653 $this->handleSessionLoss(); // no trx or locks anymore
3654 $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
3655 $this->lastPing = microtime( true );
3656 }
3657 }
3658
3664 public function __sleep() {
3665 throw new RuntimeException( 'Database serialization may cause problems, since ' .
3666 'the connection is not restored on wakeup.' );
3667 }
3668
3672 public function __destruct() {
3673 if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
3674 trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
3675 }
3676
3677 $danglingWriters = $this->pendingWriteAndCallbackCallers();
3678 if ( $danglingWriters ) {
3679 $fnames = implode( ', ', $danglingWriters );
3680 trigger_error( "DB transaction writes or callbacks still pending ($fnames)." );
3681 }
3682
3683 if ( $this->mConn ) {
3684 // Avoid connection leaks for sanity. Normally, resources close at script completion.
3685 // The connection might already be closed in zend/hhvm by now, so suppress warnings.
3686 \MediaWiki\suppressWarnings();
3687 $this->closeConnection();
3688 \MediaWiki\restoreWarnings();
3689 $this->mConn = false;
3690 $this->mOpened = false;
3691 }
3692 }
3693}
3694
3695class_alias( Database::class, 'DatabaseBase' ); // b/c for old name
3696class_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:338
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:434
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...
object string $profiler
Class name or object With profileIn/profileOut methods.
Definition Database.php:235
runOnTransactionPreCommitCallbacks()
Actually run and consume any "on transaction pre-commit" callbacks.
float $mTrxReplicaLag
Lag estimate at the time of BEGIN.
Definition Database.php:154
pendingWriteRowsAffected()
Get the number of affected rows from pending write queries.
Definition Database.php:590
begin( $fname=__METHOD__, $mode=self::TRANSACTION_EXPLICIT)
Begin a transaction.
makeUpdateOptions( $options)
Make UPDATE options for the Database::update function.
deadlockLoop()
Perform a deadlock-prone transaction.
makeInsertOptions( $options)
Helper for Database::insert().
restoreErrorHandler()
Restore the previous error handler and return the last PHP error for this DB.
Definition Database.php:702
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...
int $mTrxWriteAdjQueryCount
Number of write queries counted in mTrxWriteAdjDuration.
Definition Database.php:214
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:232
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:476
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:480
setLogger(LoggerInterface $logger)
Set the PSR-3 logger interface to use for query logging.
Definition Database.php:430
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...
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)
Deprecated method, calls should be removed.
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
nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname=__METHOD__, $insertOptions=[], $selectOptions=[], $selectJoinConds=[])
Native server-side implementation of insertSelect() for situations where we don't want to select ever...
__destruct()
Run a few simple sanity checks and close dangling connections.
dbSchema( $schema=null)
Get/set the db schema.
Definition Database.php:492
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:780
doCommit( $fname)
Issues the COMMIT command to the database server.
closeConnection()
Closes underlying database connection.
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:223
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:550
isTransactableQuery( $sql)
Determine whether a SQL statement is sensitive to isolation level.
Definition Database.php:841
getServerUptime()
Determines how long the server has been up.
reconnect()
Close existing database connection and open a new connection.
bitAnd( $fieldLeft, $fieldRight)
int $mTrxWriteQueryCount
Number of write queries for the current transaction.
Definition Database.php:202
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:558
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.
unionConditionPermutations( $table, $vars, array $permute_conds, $extra_conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Construct a UNION query for permutations of conditions.
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.
databasesAreIndependent()
Returns true if DBs are assumed to be on potentially different servers.
lock( $lockName, $method, $timeout=5)
Acquire a named lock.
wasConnectionError( $errno)
Do not use this method outside of Database/DBError classes.
float $mRTTEstimate
RTT time estimate.
Definition Database.php:218
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.
doneWrites()
Returns true if the connection may have been used for write queries.
Definition Database.php:546
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:637
commit( $fname=__METHOD__, $flush='')
Commits a transaction previously started using begin().
resource null $mConn
Database connection.
Definition Database.php:92
int $mTrxWriteAffectedRows
Number of rows affected by write queries for the current transaction.
Definition Database.php:206
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.
installErrorHandler()
Set a custom error handler for logging errors during database connection.
Definition Database.php:691
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.
streamStatementEnd(&$sql, &$newLine)
Called by sourceStream() to check if we've reached a statement end.
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:630
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)
Error handler for logging errors during database connection This method should not be used outside of...
Definition Database.php:732
maxListLen()
Return the maximum number of items allowed in a list, or 0 for unlimited.
LoggerInterface $queryLogger
Definition Database.php:87
pendingWriteAndCallbackCallers()
Get the list of method names that have pending write queries or callbacks for this transaction.
Definition Database.php:600
sourceStream( $fp, callable $lineCallback=null, callable $resultCallback=null, $fname=__METHOD__, callable $inputCallback=null)
Read and execute commands from an open file handle.
qualifiedTableComponents( $name)
Get the table components needed for a query given the currently selected database.
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.
pendingWriteCallers()
Get the list of method names that did write queries for this transaction.
Definition Database.php:586
pendingWriteQueryDuration( $type=self::ESTIMATE_TOTAL)
Get the time spend running write queries for this transaction.
Definition Database.php:564
getWikiID()
Alias for getDomainID()
Definition Database.php:667
getLBInfo( $name=null)
Get properties passed down from the server info array of the load balancer.
Definition Database.php:501
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:819
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:742
trxLevel()
Gets the current transaction level.
Definition Database.php:472
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:793
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:623
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:542
setLazyMasterHandle(IDatabase $conn)
Set a lazy-connecting DB handle to the master DB (for replication status purposes)
Definition Database.php:521
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:226
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:210
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:513
escapeLikeInternal( $s, $escapeChar='`')
array $mNamedLocksHeld
Map of (name => 1) for locks obtained via lock()
Definition Database.php:221
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:534
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:438
getInfinity()
Find out when 'infinity' is.
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 ...
insertSelect( $destTable, $srcTable, $varMap, $conds, $fname=__METHOD__, $insertOptions=[], $selectOptions=[], $selectJoinConds=[])
INSERT SELECT wrapper.
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:619
getFlag( $flag)
Returns a boolean whether the flag $flag is set for this connection.
Definition Database.php:650
query( $sql, $fname=__METHOD__, $tempIgnore=false)
Run an SQL query and return the result.
Definition Database.php:888
masterPosWait(DBMasterPos $pos, $timeout)
Wait for the replica DB to catch up to a given master position.
bool null $mDefaultBigSelects
Definition Database.php:116
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:229
array $mTrxAtomicLevels
Array of levels of atomicity within transactions.
Definition Database.php:182
TransactionProfiler $trxProfiler
Definition Database.php:237
doLockTables(array $read, array $write, $method)
Helper function for lockTables() that handles the actual table locking.
doUnlockTables( $method)
Helper function for unlockTables() that handles the actual table unlocking.
handleSessionLoss()
Clean things up after transaction loss due to disconnection.
callable[] $mTrxRecurringCallbacks
Map of (name => callable)
Definition Database.php:103
BagOStuff $srvCache
APC cache.
Definition Database.php:83
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:538
makeOrderBy( $options)
Returns an optional ORDER BY.
strreplace( $orig, $old, $new)
Returns a command 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:247
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:753
ignoreErrors( $ignoreErrors=null)
Turns on (false) or off (true) the automatic generation and sending of a "we're sorry,...
Definition Database.php:461
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
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action or null $user:User who performed the tagging when the tagging is subsequent to the action or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, 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. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy:boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1245
the array() calling protocol came about after MediaWiki 1.4rc1.
static configuration should be added through ResourceLoaderGetConfigVars instead & $vars
Definition hooks.txt:2198
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction $rows
Definition hooks.txt:2746
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:1963
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:266
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:1013
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:1757
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 & $options
Definition hooks.txt:1971
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:1975
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and insert
Definition hooks.txt:2061
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
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:247
returning false will NOT prevent logging $e
Definition hooks.txt:2146
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
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.
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