MediaWiki REL1_33
Database.php
Go to the documentation of this file.
1<?php
26namespace Wikimedia\Rdbms;
27
28use Psr\Log\LoggerAwareInterface;
29use Psr\Log\LoggerInterface;
30use Psr\Log\NullLogger;
31use Wikimedia\ScopedCallback;
32use Wikimedia\Timestamp\ConvertibleTimestamp;
36use LogicException;
37use InvalidArgumentException;
38use UnexpectedValueException;
39use Exception;
40use RuntimeException;
41use Throwable;
42
49abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAwareInterface {
51 const DEADLOCK_TRIES = 4;
53 const DEADLOCK_DELAY_MIN = 500000;
55 const DEADLOCK_DELAY_MAX = 1500000;
56
58 const PING_TTL = 1.0;
59 const PING_QUERY = 'SELECT 1 AS ping';
60
61 const TINY_WRITE_SEC = 0.010;
62 const SLOW_WRITE_SEC = 0.500;
63 const SMALL_WRITE_ROWS = 100;
64
66 const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
68 const ATTR_SCHEMAS_AS_TABLE_GROUPS = 'supports-schemas';
69
71 const NEW_UNCONNECTED = 0;
73 const NEW_CONNECTED = 1;
74
76 protected $lastQuery = '';
78 protected $lastWriteTime = false;
80 protected $phpError = false;
82 protected $server;
84 protected $user;
86 protected $password;
88 protected $tableAliases = [];
90 protected $indexAliases = [];
92 protected $cliMode;
94 protected $agent;
96 protected $connectionParams = [];
98 protected $srvCache;
100 protected $connLogger;
102 protected $queryLogger;
104 protected $errorLogger;
107
109 protected $conn = null;
111 protected $opened = false;
112
114 protected $trxIdleCallbacks = [];
118 protected $trxEndCallbacks = [];
124 protected $trxEndCallbacksSuppressed = false;
125
127 protected $flags;
129 protected $lbInfo = [];
131 protected $schemaVars = false;
133 protected $sessionVars = [];
135 protected $preparedArgs;
137 protected $htmlErrors;
139 protected $delimiter = ';';
141 protected $currentDomain;
144
148 protected $trxStatus = self::STATUS_TRX_NONE;
164 protected $trxLevel = 0;
171 protected $trxShortId = '';
180 private $trxTimestamp = null;
182 private $trxReplicaLag = null;
190 private $trxFname = null;
197 private $trxDoneWrites = false;
204 private $trxAutomatic = false;
210 private $trxAtomicCounter = 0;
216 private $trxAtomicLevels = [];
222 private $trxAutomaticAtomic = false;
228 private $trxWriteCallers = [];
232 private $trxWriteDuration = 0.0;
244 private $trxWriteAdjDuration = 0.0;
252 private $rttEstimate = 0.0;
253
255 private $namedLocksHeld = [];
257 protected $sessionTempTables = [];
258
261
263 protected $lastPing = 0.0;
264
266 private $priorFlags = [];
267
269 protected $profiler;
271 protected $trxProfiler;
272
275
277 private static $NOT_APPLICABLE = 'n/a';
279 private static $SAVEPOINT_PREFIX = 'wikimedia_rdbms_atomic';
280
282 const STATUS_TRX_ERROR = 1;
284 const STATUS_TRX_OK = 2;
286 const STATUS_TRX_NONE = 3;
287
289 const TEMP_NORMAL = 1;
291 const TEMP_PSEUDO_PERMANENT = 2;
292
297 protected function __construct( array $params ) {
298 foreach ( [ 'host', 'user', 'password', 'dbname', 'schema', 'tablePrefix' ] as $name ) {
299 $this->connectionParams[$name] = $params[$name];
300 }
301
302 $this->cliMode = $params['cliMode'];
303 // Agent name is added to SQL queries in a comment, so make sure it can't break out
304 $this->agent = str_replace( '/', '-', $params['agent'] );
305
306 $this->flags = $params['flags'];
307 if ( $this->flags & self::DBO_DEFAULT ) {
308 if ( $this->cliMode ) {
309 $this->flags &= ~self::DBO_TRX;
310 } else {
311 $this->flags |= self::DBO_TRX;
312 }
313 }
314 // Disregard deprecated DBO_IGNORE flag (T189999)
315 $this->flags &= ~self::DBO_IGNORE;
316
317 $this->sessionVars = $params['variables'];
318
319 $this->srvCache = $params['srvCache'] ?? new HashBagOStuff();
320
321 $this->profiler = is_callable( $params['profiler'] ) ? $params['profiler'] : null;
322 $this->trxProfiler = $params['trxProfiler'];
323 $this->connLogger = $params['connLogger'];
324 $this->queryLogger = $params['queryLogger'];
325 $this->errorLogger = $params['errorLogger'];
326 $this->deprecationLogger = $params['deprecationLogger'];
327
328 if ( isset( $params['nonNativeInsertSelectBatchSize'] ) ) {
329 $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'];
330 }
331
332 // Set initial dummy domain until open() sets the final DB/prefix
333 $this->currentDomain = new DatabaseDomain(
334 $params['dbname'] != '' ? $params['dbname'] : null,
335 $params['schema'] != '' ? $params['schema'] : null,
336 $params['tablePrefix']
337 );
338 }
339
348 final public function initConnection() {
349 if ( $this->isOpen() ) {
350 throw new LogicException( __METHOD__ . ': already connected.' );
351 }
352 // Establish the connection
353 $this->doInitConnection();
354 }
355
363 protected function doInitConnection() {
364 if ( strlen( $this->connectionParams['user'] ) ) {
365 $this->open(
366 $this->connectionParams['host'],
367 $this->connectionParams['user'],
368 $this->connectionParams['password'],
369 $this->connectionParams['dbname'],
370 $this->connectionParams['schema'],
371 $this->connectionParams['tablePrefix']
372 );
373 } else {
374 throw new InvalidArgumentException( "No database user provided." );
375 }
376 }
377
390 abstract protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix );
391
437 final public static function factory( $dbType, $p = [], $connect = self::NEW_CONNECTED ) {
438 $class = self::getClass( $dbType, $p['driver'] ?? null );
439
440 if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
441 // Resolve some defaults for b/c
442 $p['host'] = $p['host'] ?? false;
443 $p['user'] = $p['user'] ?? false;
444 $p['password'] = $p['password'] ?? false;
445 $p['dbname'] = $p['dbname'] ?? false;
446 $p['flags'] = $p['flags'] ?? 0;
447 $p['variables'] = $p['variables'] ?? [];
448 $p['tablePrefix'] = $p['tablePrefix'] ?? '';
449 $p['schema'] = $p['schema'] ?? null;
450 $p['cliMode'] = $p['cliMode'] ?? ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' );
451 $p['agent'] = $p['agent'] ?? '';
452 if ( !isset( $p['connLogger'] ) ) {
453 $p['connLogger'] = new NullLogger();
454 }
455 if ( !isset( $p['queryLogger'] ) ) {
456 $p['queryLogger'] = new NullLogger();
457 }
458 $p['profiler'] = $p['profiler'] ?? null;
459 if ( !isset( $p['trxProfiler'] ) ) {
460 $p['trxProfiler'] = new TransactionProfiler();
461 }
462 if ( !isset( $p['errorLogger'] ) ) {
463 $p['errorLogger'] = function ( Exception $e ) {
464 trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
465 };
466 }
467 if ( !isset( $p['deprecationLogger'] ) ) {
468 $p['deprecationLogger'] = function ( $msg ) {
469 trigger_error( $msg, E_USER_DEPRECATED );
470 };
471 }
472
474 $conn = new $class( $p );
475 if ( $connect == self::NEW_CONNECTED ) {
476 $conn->initConnection();
477 }
478 } else {
479 $conn = null;
480 }
481
482 return $conn;
483 }
484
492 final public static function attributesFromType( $dbType, $driver = null ) {
493 static $defaults = [
494 self::ATTR_DB_LEVEL_LOCKING => false,
495 self::ATTR_SCHEMAS_AS_TABLE_GROUPS => false
496 ];
497
498 $class = self::getClass( $dbType, $driver );
499
500 return call_user_func( [ $class, 'getAttributes' ] ) + $defaults;
501 }
502
509 private static function getClass( $dbType, $driver = null ) {
510 // For database types with built-in support, the below maps type to IDatabase
511 // implementations. For types with multipe driver implementations (PHP extensions),
512 // an array can be used, keyed by extension name. In case of an array, the
513 // optional 'driver' parameter can be used to force a specific driver. Otherwise,
514 // we auto-detect the first available driver. For types without built-in support,
515 // an class named "Database<Type>" us used, eg. DatabaseFoo for type 'foo'.
516 static $builtinTypes = [
517 'mssql' => DatabaseMssql::class,
518 'mysql' => [ 'mysqli' => DatabaseMysqli::class ],
519 'sqlite' => DatabaseSqlite::class,
520 'postgres' => DatabasePostgres::class,
521 ];
522
523 $dbType = strtolower( $dbType );
524 $class = false;
525
526 if ( isset( $builtinTypes[$dbType] ) ) {
527 $possibleDrivers = $builtinTypes[$dbType];
528 if ( is_string( $possibleDrivers ) ) {
529 $class = $possibleDrivers;
530 } elseif ( (string)$driver !== '' ) {
531 if ( !isset( $possibleDrivers[$driver] ) ) {
532 throw new InvalidArgumentException( __METHOD__ .
533 " type '$dbType' does not support driver '{$driver}'" );
534 }
535
536 $class = $possibleDrivers[$driver];
537 } else {
538 foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
539 if ( extension_loaded( $posDriver ) ) {
540 $class = $possibleClass;
541 break;
542 }
543 }
544 }
545 } else {
546 $class = 'Database' . ucfirst( $dbType );
547 }
548
549 if ( $class === false ) {
550 throw new InvalidArgumentException( __METHOD__ .
551 " no viable database extension found for type '$dbType'" );
552 }
553
554 return $class;
555 }
556
561 protected static function getAttributes() {
562 return [];
563 }
564
572 public function setLogger( LoggerInterface $logger ) {
573 $this->queryLogger = $logger;
574 }
575
576 public function getServerInfo() {
577 return $this->getServerVersion();
578 }
579
580 public function bufferResults( $buffer = null ) {
581 $res = !$this->getFlag( self::DBO_NOBUFFER );
582 if ( $buffer !== null ) {
583 $buffer
584 ? $this->clearFlag( self::DBO_NOBUFFER )
585 : $this->setFlag( self::DBO_NOBUFFER );
586 }
587
588 return $res;
589 }
590
591 public function trxLevel() {
592 return $this->trxLevel;
593 }
594
595 public function trxTimestamp() {
596 return $this->trxLevel ? $this->trxTimestamp : null;
597 }
598
603 public function trxStatus() {
604 return $this->trxStatus;
605 }
606
607 public function tablePrefix( $prefix = null ) {
608 $old = $this->currentDomain->getTablePrefix();
609 if ( $prefix !== null ) {
610 $this->currentDomain = new DatabaseDomain(
611 $this->currentDomain->getDatabase(),
612 $this->currentDomain->getSchema(),
613 $prefix
614 );
615 }
616
617 return $old;
618 }
619
620 public function dbSchema( $schema = null ) {
621 if ( strlen( $schema ) && $this->getDBname() === null ) {
622 throw new DBUnexpectedError( $this, "Cannot set schema to '$schema'; no database set." );
623 }
624
625 $old = $this->currentDomain->getSchema();
626 if ( $schema !== null ) {
627 $this->currentDomain = new DatabaseDomain(
628 $this->currentDomain->getDatabase(),
629 // DatabaseDomain uses null for unspecified schemas
630 strlen( $schema ) ? $schema : null,
631 $this->currentDomain->getTablePrefix()
632 );
633 }
634
635 return (string)$old;
636 }
637
641 protected function relationSchemaQualifier() {
642 return $this->dbSchema();
643 }
644
645 public function getLBInfo( $name = null ) {
646 if ( is_null( $name ) ) {
647 return $this->lbInfo;
648 }
649
650 if ( array_key_exists( $name, $this->lbInfo ) ) {
651 return $this->lbInfo[$name];
652 }
653
654 return null;
655 }
656
657 public function setLBInfo( $name, $value = null ) {
658 if ( is_null( $value ) ) {
659 $this->lbInfo = $name;
660 } else {
661 $this->lbInfo[$name] = $value;
662 }
663 }
664
666 $this->lazyMasterHandle = $conn;
667 }
668
674 protected function getLazyMasterHandle() {
676 }
677
678 public function implicitGroupby() {
679 return true;
680 }
681
682 public function implicitOrderby() {
683 return true;
684 }
685
686 public function lastQuery() {
687 return $this->lastQuery;
688 }
689
690 public function doneWrites() {
691 return (bool)$this->lastWriteTime;
692 }
693
694 public function lastDoneWrites() {
695 return $this->lastWriteTime ?: false;
696 }
697
698 public function writesPending() {
699 return $this->trxLevel && $this->trxDoneWrites;
700 }
701
702 public function writesOrCallbacksPending() {
703 return $this->trxLevel && (
704 $this->trxDoneWrites ||
705 $this->trxIdleCallbacks ||
706 $this->trxPreCommitCallbacks ||
707 $this->trxEndCallbacks ||
709 );
710 }
711
712 public function preCommitCallbacksPending() {
713 return $this->trxLevel && $this->trxPreCommitCallbacks;
714 }
715
719 final protected function getTransactionRoundId() {
720 // If transaction round participation is enabled, see if one is active
721 if ( $this->getFlag( self::DBO_TRX ) ) {
722 $id = $this->getLBInfo( 'trxRoundId' );
723
724 return is_string( $id ) ? $id : null;
725 }
726
727 return null;
728 }
729
730 public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
731 if ( !$this->trxLevel ) {
732 return false;
733 } elseif ( !$this->trxDoneWrites ) {
734 return 0.0;
735 }
736
737 switch ( $type ) {
738 case self::ESTIMATE_DB_APPLY:
739 return $this->pingAndCalculateLastTrxApplyTime();
740 default: // everything
742 }
743 }
744
749 $this->ping( $rtt );
750
751 $rttAdjTotal = $this->trxWriteAdjQueryCount * $rtt;
752 $applyTime = max( $this->trxWriteAdjDuration - $rttAdjTotal, 0 );
753 // For omitted queries, make them count as something at least
754 $omitted = $this->trxWriteQueryCount - $this->trxWriteAdjQueryCount;
755 $applyTime += self::TINY_WRITE_SEC * $omitted;
756
757 return $applyTime;
758 }
759
760 public function pendingWriteCallers() {
761 return $this->trxLevel ? $this->trxWriteCallers : [];
762 }
763
764 public function pendingWriteRowsAffected() {
766 }
767
777 $fnames = $this->pendingWriteCallers();
778 foreach ( [
779 $this->trxIdleCallbacks,
780 $this->trxPreCommitCallbacks,
781 $this->trxEndCallbacks,
782 $this->trxSectionCancelCallbacks
783 ] as $callbacks ) {
784 foreach ( $callbacks as $callback ) {
785 $fnames[] = $callback[1];
786 }
787 }
788
789 return $fnames;
790 }
791
795 private function flatAtomicSectionList() {
796 return array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) {
797 return $accum === null ? $v[0] : "$accum, " . $v[0];
798 } );
799 }
800
801 public function isOpen() {
802 return $this->opened;
803 }
804
805 public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
806 if ( ( $flag & self::DBO_IGNORE ) ) {
807 throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
808 }
809
810 if ( $remember === self::REMEMBER_PRIOR ) {
811 array_push( $this->priorFlags, $this->flags );
812 }
813 $this->flags |= $flag;
814 }
815
816 public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
817 if ( ( $flag & self::DBO_IGNORE ) ) {
818 throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
819 }
820
821 if ( $remember === self::REMEMBER_PRIOR ) {
822 array_push( $this->priorFlags, $this->flags );
823 }
824 $this->flags &= ~$flag;
825 }
826
827 public function restoreFlags( $state = self::RESTORE_PRIOR ) {
828 if ( !$this->priorFlags ) {
829 return;
830 }
831
832 if ( $state === self::RESTORE_INITIAL ) {
833 $this->flags = reset( $this->priorFlags );
834 $this->priorFlags = [];
835 } else {
836 $this->flags = array_pop( $this->priorFlags );
837 }
838 }
839
840 public function getFlag( $flag ) {
841 return (bool)( $this->flags & $flag );
842 }
843
849 public function getProperty( $name ) {
850 return $this->$name;
851 }
852
853 public function getDomainID() {
854 return $this->currentDomain->getId();
855 }
856
857 final public function getWikiID() {
858 return $this->getDomainID();
859 }
860
868 abstract function indexInfo( $table, $index, $fname = __METHOD__ );
869
876 abstract function strencode( $s );
877
881 protected function installErrorHandler() {
882 $this->phpError = false;
883 $this->htmlErrors = ini_set( 'html_errors', '0' );
884 set_error_handler( [ $this, 'connectionErrorLogger' ] );
885 }
886
892 protected function restoreErrorHandler() {
893 restore_error_handler();
894 if ( $this->htmlErrors !== false ) {
895 ini_set( 'html_errors', $this->htmlErrors );
896 }
897
898 return $this->getLastPHPError();
899 }
900
904 protected function getLastPHPError() {
905 if ( $this->phpError ) {
906 $error = preg_replace( '!\[<a.*</a>\]!', '', $this->phpError );
907 $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
908
909 return $error;
910 }
911
912 return false;
913 }
914
922 public function connectionErrorLogger( $errno, $errstr ) {
923 $this->phpError = $errstr;
924 }
925
932 protected function getLogContext( array $extras = [] ) {
933 return array_merge(
934 [
935 'db_server' => $this->server,
936 'db_name' => $this->getDBname(),
937 'db_user' => $this->user,
938 ],
939 $extras
940 );
941 }
942
943 final public function close() {
944 $exception = null; // error to throw after disconnecting
945
946 $wasOpen = $this->opened;
947 // This should mostly do nothing if the connection is already closed
948 if ( $this->conn ) {
949 // Roll back any dangling transaction first
950 if ( $this->trxLevel ) {
951 if ( $this->trxAtomicLevels ) {
952 // Cannot let incomplete atomic sections be committed
953 $levels = $this->flatAtomicSectionList();
954 $exception = new DBUnexpectedError(
955 $this,
956 __METHOD__ . ": atomic sections $levels are still open."
957 );
958 } elseif ( $this->trxAutomatic ) {
959 // Only the connection manager can commit non-empty DBO_TRX transactions
960 // (empty ones we can silently roll back)
961 if ( $this->writesOrCallbacksPending() ) {
962 $exception = new DBUnexpectedError(
963 $this,
964 __METHOD__ .
965 ": mass commit/rollback of peer transaction required (DBO_TRX set)."
966 );
967 }
968 } else {
969 // Manual transactions should have been committed or rolled
970 // back, even if empty.
971 $exception = new DBUnexpectedError(
972 $this,
973 __METHOD__ . ": transaction is still open (from {$this->trxFname})."
974 );
975 }
976
977 if ( $this->trxEndCallbacksSuppressed ) {
978 $exception = $exception ?: new DBUnexpectedError(
979 $this,
980 __METHOD__ . ': callbacks are suppressed; cannot properly commit.'
981 );
982 }
983
984 // Rollback the changes and run any callbacks as needed
985 $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
986 }
987
988 // Close the actual connection in the binding handle
989 $closed = $this->closeConnection();
990 } else {
991 $closed = true; // already closed; nothing to do
992 }
993
994 $this->conn = false;
995 $this->opened = false;
996
997 // Throw any unexpected errors after having disconnected
998 if ( $exception instanceof Exception ) {
999 throw $exception;
1000 }
1001
1002 // Note that various subclasses call close() at the start of open(), which itself is
1003 // called by replaceLostConnection(). In that case, just because onTransactionResolution()
1004 // callbacks are pending does not mean that an exception should be thrown. Rather, they
1005 // will be executed after the reconnection step.
1006 if ( $wasOpen ) {
1007 // Sanity check that no callbacks are dangling
1008 $fnames = $this->pendingWriteAndCallbackCallers();
1009 if ( $fnames ) {
1010 throw new RuntimeException(
1011 "Transaction callbacks are still pending:\n" . implode( ', ', $fnames )
1012 );
1013 }
1014 }
1015
1016 return $closed;
1017 }
1018
1027 protected function assertHasConnectionHandle() {
1028 if ( !$this->isOpen() ) {
1029 throw new DBUnexpectedError( $this, "DB connection was already closed." );
1030 }
1031 }
1032
1038 protected function assertIsWritableMaster() {
1039 if ( $this->getLBInfo( 'replica' ) === true ) {
1040 throw new DBReadOnlyRoleError(
1041 $this,
1042 'Write operations are not allowed on replica database connections.'
1043 );
1044 }
1045 $reason = $this->getReadOnlyReason();
1046 if ( $reason !== false ) {
1047 throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
1048 }
1049 }
1050
1056 abstract protected function closeConnection();
1057
1063 public function reportConnectionError( $error = 'Unknown error' ) {
1064 call_user_func( $this->deprecationLogger, 'Use of ' . __METHOD__ . ' is deprecated.' );
1065 throw new DBConnectionError( $this, $this->lastError() ?: $error );
1066 }
1067
1086 abstract protected function doQuery( $sql );
1087
1104 protected function isWriteQuery( $sql ) {
1105 // BEGIN and COMMIT queries are considered read queries here.
1106 // Database backends and drivers (MySQL, MariaDB, php-mysqli) generally
1107 // treat these as write queries, in that their results have "affected rows"
1108 // as meta data as from writes, instead of "num rows" as from reads.
1109 // But, we treat them as read queries because when reading data (from
1110 // either replica or master) we use transactions to enable repeatable-read
1111 // snapshots, which ensures we get consistent results from the same snapshot
1112 // for all queries within a request. Use cases:
1113 // - Treating these as writes would trigger ChronologyProtector (see method doc).
1114 // - We use this method to reject writes to replicas, but we need to allow
1115 // use of transactions on replicas for read snapshots. This fine given
1116 // that transactions by themselves don't make changes, only actual writes
1117 // within the transaction matter, which we still detect.
1118 return !preg_match(
1119 '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SAVEPOINT|RELEASE|SET|SHOW|EXPLAIN|\‍(SELECT)\b/i',
1120 $sql
1121 );
1122 }
1123
1128 protected function getQueryVerb( $sql ) {
1129 return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
1130 }
1131
1145 protected function isTransactableQuery( $sql ) {
1146 return !in_array(
1147 $this->getQueryVerb( $sql ),
1148 [ 'BEGIN', 'ROLLBACK', 'COMMIT', 'SET', 'SHOW', 'CREATE', 'ALTER' ],
1149 true
1150 );
1151 }
1152
1158 protected function registerTempTableWrite( $sql, $pseudoPermanent ) {
1159 static $qt = '[`"\']?(\w+)[`"\']?'; // quoted table
1160
1161 if ( preg_match(
1162 '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?' . $qt . '/i',
1163 $sql,
1164 $matches
1165 ) ) {
1166 $type = $pseudoPermanent ? self::TEMP_PSEUDO_PERMANENT : self::TEMP_NORMAL;
1167 $this->sessionTempTables[$matches[1]] = $type;
1168
1169 return $type;
1170 } elseif ( preg_match(
1171 '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?' . $qt . '/i',
1172 $sql,
1173 $matches
1174 ) ) {
1175 $type = $this->sessionTempTables[$matches[1]] ?? null;
1176 unset( $this->sessionTempTables[$matches[1]] );
1177
1178 return $type;
1179 } elseif ( preg_match(
1180 '/^TRUNCATE\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?' . $qt . '/i',
1181 $sql,
1182 $matches
1183 ) ) {
1184 return $this->sessionTempTables[$matches[1]] ?? null;
1185 } elseif ( preg_match(
1186 '/^(?:(?:INSERT|REPLACE)\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+' . $qt . '/i',
1187 $sql,
1188 $matches
1189 ) ) {
1190 return $this->sessionTempTables[$matches[1]] ?? null;
1191 }
1192
1193 return null;
1194 }
1195
1196 public function query( $sql, $fname = __METHOD__, $flags = 0 ) {
1197 $this->assertTransactionStatus( $sql, $fname );
1199
1200 $flags = (int)$flags; // b/c; this field used to be a bool
1201 $ignoreErrors = $this->hasFlags( $flags, self::QUERY_SILENCE_ERRORS );
1202
1203 $priorTransaction = $this->trxLevel;
1204 $priorWritesPending = $this->writesOrCallbacksPending();
1205 $this->lastQuery = $sql;
1206
1207 if ( $this->isWriteQuery( $sql ) ) {
1208 # In theory, non-persistent writes are allowed in read-only mode, but due to things
1209 # like https://bugs.mysql.com/bug.php?id=33669 that might not work anyway...
1210 $this->assertIsWritableMaster();
1211 # Do not treat temporary table writes as "meaningful writes" that need committing.
1212 # Profile them as reads. Integration tests can override this behavior via $flags.
1213 $pseudoPermanent = $this->hasFlags( $flags, self::QUERY_PSEUDO_PERMANENT );
1214 $tableType = $this->registerTempTableWrite( $sql, $pseudoPermanent );
1215 $isEffectiveWrite = ( $tableType !== self::TEMP_NORMAL );
1216 # DBConnRef uses QUERY_REPLICA_ROLE to enforce the replica role for raw SQL queries
1217 if ( $isEffectiveWrite && $this->hasFlags( $flags, self::QUERY_REPLICA_ROLE ) ) {
1218 throw new DBReadOnlyRoleError( $this, "Cannot write; target role is DB_REPLICA" );
1219 }
1220 } else {
1221 $isEffectiveWrite = false;
1222 }
1223
1224 # Add trace comment to the begin of the sql string, right after the operator.
1225 # Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
1226 $commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
1227
1228 # Send the query to the server and fetch any corresponding errors
1229 $ret = $this->attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname );
1230 $lastError = $this->lastError();
1231 $lastErrno = $this->lastErrno();
1232
1233 $recoverableSR = false; // recoverable statement rollback?
1234 $recoverableCL = false; // recoverable connection loss?
1235
1236 if ( $ret === false && $this->wasConnectionLoss() ) {
1237 # Check if no meaningful session state was lost
1238 $recoverableCL = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
1239 # Update session state tracking and try to restore the connection
1240 $reconnected = $this->replaceLostConnection( __METHOD__ );
1241 # Silently resend the query to the server if it is safe and possible
1242 if ( $recoverableCL && $reconnected ) {
1243 $ret = $this->attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname );
1244 $lastError = $this->lastError();
1245 $lastErrno = $this->lastErrno();
1246
1247 if ( $ret === false && $this->wasConnectionLoss() ) {
1248 # Query probably causes disconnects; reconnect and do not re-run it
1249 $this->replaceLostConnection( __METHOD__ );
1250 } else {
1251 $recoverableCL = false; // connection does not need recovering
1252 $recoverableSR = $this->wasKnownStatementRollbackError();
1253 }
1254 }
1255 } else {
1256 $recoverableSR = $this->wasKnownStatementRollbackError();
1257 }
1258
1259 if ( $ret === false ) {
1260 if ( $priorTransaction ) {
1261 if ( $recoverableSR ) {
1262 # We're ignoring an error that caused just the current query to be aborted.
1263 # But log the cause so we can log a deprecation notice if a caller actually
1264 # does ignore it.
1265 $this->trxStatusIgnoredCause = [ $lastError, $lastErrno, $fname ];
1266 } elseif ( !$recoverableCL ) {
1267 # Either the query was aborted or all queries after BEGIN where aborted.
1268 # In the first case, the only options going forward are (a) ROLLBACK, or
1269 # (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
1270 # option is ROLLBACK, since the snapshots would have been released.
1271 $this->trxStatus = self::STATUS_TRX_ERROR;
1272 $this->trxStatusCause =
1273 $this->getQueryExceptionAndLog( $lastError, $lastErrno, $sql, $fname );
1274 $ignoreErrors = false; // cannot recover
1275 $this->trxStatusIgnoredCause = null;
1276 }
1277 }
1278
1279 $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $ignoreErrors );
1280 }
1281
1282 return $this->resultObject( $ret );
1283 }
1284
1295 private function attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname ) {
1296 $this->beginIfImplied( $sql, $fname );
1297
1298 # Keep track of whether the transaction has write queries pending
1299 if ( $isEffectiveWrite ) {
1300 $this->lastWriteTime = microtime( true );
1301 if ( $this->trxLevel && !$this->trxDoneWrites ) {
1302 $this->trxDoneWrites = true;
1303 $this->trxProfiler->transactionWritingIn(
1304 $this->server, $this->getDomainID(), $this->trxShortId );
1305 }
1306 }
1307
1308 if ( $this->getFlag( self::DBO_DEBUG ) ) {
1309 $this->queryLogger->debug( "{$this->getDomainID()} {$commentedSql}" );
1310 }
1311
1312 $isMaster = !is_null( $this->getLBInfo( 'master' ) );
1313 # generalizeSQL() will probably cut down the query to reasonable
1314 # logging size most of the time. The substr is really just a sanity check.
1315 if ( $isMaster ) {
1316 $queryProf = 'query-m: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
1317 } else {
1318 $queryProf = 'query: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
1319 }
1320
1321 # Include query transaction state
1322 $queryProf .= $this->trxShortId ? " [TRX#{$this->trxShortId}]" : "";
1323
1324 $startTime = microtime( true );
1325 $ps = $this->profiler ? ( $this->profiler )( $queryProf ) : null;
1326 $this->affectedRowCount = null;
1327 $ret = $this->doQuery( $commentedSql );
1328 $this->affectedRowCount = $this->affectedRows();
1329 unset( $ps ); // profile out (if set)
1330 $queryRuntime = max( microtime( true ) - $startTime, 0.0 );
1331
1332 if ( $ret !== false ) {
1333 $this->lastPing = $startTime;
1334 if ( $isEffectiveWrite && $this->trxLevel ) {
1335 $this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() );
1336 $this->trxWriteCallers[] = $fname;
1337 }
1338 }
1339
1340 if ( $sql === self::PING_QUERY ) {
1341 $this->rttEstimate = $queryRuntime;
1342 }
1343
1344 $this->trxProfiler->recordQueryCompletion(
1345 $queryProf,
1346 $startTime,
1347 $isEffectiveWrite,
1348 $isEffectiveWrite ? $this->affectedRows() : $this->numRows( $ret )
1349 );
1350 $this->queryLogger->debug( $sql, [
1351 'method' => $fname,
1352 'master' => $isMaster,
1353 'runtime' => $queryRuntime,
1354 ] );
1355
1356 return $ret;
1357 }
1358
1365 private function beginIfImplied( $sql, $fname ) {
1366 if (
1367 !$this->trxLevel &&
1368 $this->getFlag( self::DBO_TRX ) &&
1369 $this->isTransactableQuery( $sql )
1370 ) {
1371 $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
1372 $this->trxAutomatic = true;
1373 }
1374 }
1375
1388 private function updateTrxWriteQueryTime( $sql, $runtime, $affected ) {
1389 // Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
1390 $indicativeOfReplicaRuntime = true;
1391 if ( $runtime > self::SLOW_WRITE_SEC ) {
1392 $verb = $this->getQueryVerb( $sql );
1393 // insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
1394 if ( $verb === 'INSERT' ) {
1395 $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS;
1396 } elseif ( $verb === 'REPLACE' ) {
1397 $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS / 2;
1398 }
1399 }
1400
1401 $this->trxWriteDuration += $runtime;
1402 $this->trxWriteQueryCount += 1;
1403 $this->trxWriteAffectedRows += $affected;
1404 if ( $indicativeOfReplicaRuntime ) {
1405 $this->trxWriteAdjDuration += $runtime;
1406 $this->trxWriteAdjQueryCount += 1;
1407 }
1408 }
1409
1417 private function assertTransactionStatus( $sql, $fname ) {
1418 $verb = $this->getQueryVerb( $sql );
1419 if ( $verb === 'USE' ) {
1420 throw new DBUnexpectedError( $this, "Got USE query; use selectDomain() instead." );
1421 }
1422
1423 if ( $verb === 'ROLLBACK' ) { // transaction/savepoint
1424 return;
1425 }
1426
1427 if ( $this->trxStatus < self::STATUS_TRX_OK ) {
1428 throw new DBTransactionStateError(
1429 $this,
1430 "Cannot execute query from $fname while transaction status is ERROR.",
1431 [],
1432 $this->trxStatusCause
1433 );
1434 } elseif ( $this->trxStatus === self::STATUS_TRX_OK && $this->trxStatusIgnoredCause ) {
1435 list( $iLastError, $iLastErrno, $iFname ) = $this->trxStatusIgnoredCause;
1436 call_user_func( $this->deprecationLogger,
1437 "Caller from $fname ignored an error originally raised from $iFname: " .
1438 "[$iLastErrno] $iLastError"
1439 );
1440 $this->trxStatusIgnoredCause = null;
1441 }
1442 }
1443
1444 public function assertNoOpenTransactions() {
1445 if ( $this->explicitTrxActive() ) {
1446 throw new DBTransactionError(
1447 $this,
1448 "Explicit transaction still active. A caller may have caught an error. "
1449 . "Open transactions: " . $this->flatAtomicSectionList()
1450 );
1451 }
1452 }
1453
1463 private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
1464 # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
1465 # Dropped connections also mean that named locks are automatically released.
1466 # Only allow error suppression in autocommit mode or when the lost transaction
1467 # didn't matter anyway (aside from DBO_TRX snapshot loss).
1468 if ( $this->namedLocksHeld ) {
1469 return false; // possible critical section violation
1470 } elseif ( $this->sessionTempTables ) {
1471 return false; // tables might be queried latter
1472 } elseif ( $sql === 'COMMIT' ) {
1473 return !$priorWritesPending; // nothing written anyway? (T127428)
1474 } elseif ( $sql === 'ROLLBACK' ) {
1475 return true; // transaction lost...which is also what was requested :)
1476 } elseif ( $this->explicitTrxActive() ) {
1477 return false; // don't drop atomocity and explicit snapshots
1478 } elseif ( $priorWritesPending ) {
1479 return false; // prior writes lost from implicit transaction
1480 }
1481
1482 return true;
1483 }
1484
1488 private function handleSessionLossPreconnect() {
1489 // Clean up tracking of session-level things...
1490 // https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html
1491 // https://www.postgresql.org/docs/9.2/static/sql-createtable.html (ignoring ON COMMIT)
1492 $this->sessionTempTables = [];
1493 // https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
1494 // https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
1495 $this->namedLocksHeld = [];
1496 // Session loss implies transaction loss
1497 $this->trxLevel = 0;
1498 $this->trxAtomicCounter = 0;
1499 $this->trxIdleCallbacks = []; // T67263; transaction already lost
1500 $this->trxPreCommitCallbacks = []; // T67263; transaction already lost
1501 // @note: leave trxRecurringCallbacks in place
1502 if ( $this->trxDoneWrites ) {
1503 $this->trxProfiler->transactionWritingOut(
1504 $this->server,
1505 $this->getDomainID(),
1506 $this->trxShortId,
1507 $this->pendingWriteQueryDuration( self::ESTIMATE_TOTAL ),
1508 $this->trxWriteAffectedRows
1509 );
1510 }
1511 }
1512
1516 private function handleSessionLossPostconnect() {
1517 try {
1518 // Handle callbacks in trxEndCallbacks, e.g. onTransactionResolution().
1519 // If callback suppression is set then the array will remain unhandled.
1520 $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
1521 } catch ( Exception $ex ) {
1522 // Already logged; move on...
1523 }
1524 try {
1525 // Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener()
1526 $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
1527 } catch ( Exception $ex ) {
1528 // Already logged; move on...
1529 }
1530 }
1531
1542 protected function wasQueryTimeout( $error, $errno ) {
1543 return false;
1544 }
1545
1557 public function reportQueryError( $error, $errno, $sql, $fname, $ignoreErrors = false ) {
1558 if ( $ignoreErrors ) {
1559 $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
1560 } else {
1561 $exception = $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
1562
1563 throw $exception;
1564 }
1565 }
1566
1574 private function getQueryExceptionAndLog( $error, $errno, $sql, $fname ) {
1575 $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
1576 $this->queryLogger->error(
1577 "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
1578 $this->getLogContext( [
1579 'method' => __METHOD__,
1580 'errno' => $errno,
1581 'error' => $error,
1582 'sql1line' => $sql1line,
1583 'fname' => $fname,
1584 'exception' => new RuntimeException()
1585 ] )
1586 );
1587 $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
1588 $wasQueryTimeout = $this->wasQueryTimeout( $error, $errno );
1589 if ( $wasQueryTimeout ) {
1590 $e = new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
1591 } else {
1592 $e = new DBQueryError( $this, $error, $errno, $sql, $fname );
1593 }
1594
1595 return $e;
1596 }
1597
1598 public function freeResult( $res ) {
1599 }
1600
1601 public function selectField(
1602 $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1603 ) {
1604 if ( $var === '*' ) { // sanity
1605 throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1606 }
1607
1608 if ( !is_array( $options ) ) {
1609 $options = [ $options ];
1610 }
1611
1612 $options['LIMIT'] = 1;
1613
1614 $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1615 if ( $res === false || !$this->numRows( $res ) ) {
1616 return false;
1617 }
1618
1619 $row = $this->fetchRow( $res );
1620
1621 if ( $row !== false ) {
1622 return reset( $row );
1623 } else {
1624 return false;
1625 }
1626 }
1627
1628 public function selectFieldValues(
1629 $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1630 ) {
1631 if ( $var === '*' ) { // sanity
1632 throw new DBUnexpectedError( $this, "Cannot use a * field" );
1633 } elseif ( !is_string( $var ) ) { // sanity
1634 throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1635 }
1636
1637 if ( !is_array( $options ) ) {
1638 $options = [ $options ];
1639 }
1640
1641 $res = $this->select( $table, [ 'value' => $var ], $cond, $fname, $options, $join_conds );
1642 if ( $res === false ) {
1643 return false;
1644 }
1645
1646 $values = [];
1647 foreach ( $res as $row ) {
1648 $values[] = $row->value;
1649 }
1650
1651 return $values;
1652 }
1653
1663 protected function makeSelectOptions( $options ) {
1664 $preLimitTail = $postLimitTail = '';
1665 $startOpts = '';
1666
1667 $noKeyOptions = [];
1668
1669 foreach ( $options as $key => $option ) {
1670 if ( is_numeric( $key ) ) {
1671 $noKeyOptions[$option] = true;
1672 }
1673 }
1674
1675 $preLimitTail .= $this->makeGroupByWithHaving( $options );
1676
1677 $preLimitTail .= $this->makeOrderBy( $options );
1678
1679 if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1680 $postLimitTail .= ' FOR UPDATE';
1681 }
1682
1683 if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1684 $postLimitTail .= ' LOCK IN SHARE MODE';
1685 }
1686
1687 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1688 $startOpts .= 'DISTINCT';
1689 }
1690
1691 # Various MySQL extensions
1692 if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1693 $startOpts .= ' /*! STRAIGHT_JOIN */';
1694 }
1695
1696 if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
1697 $startOpts .= ' HIGH_PRIORITY';
1698 }
1699
1700 if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1701 $startOpts .= ' SQL_BIG_RESULT';
1702 }
1703
1704 if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1705 $startOpts .= ' SQL_BUFFER_RESULT';
1706 }
1707
1708 if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1709 $startOpts .= ' SQL_SMALL_RESULT';
1710 }
1711
1712 if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1713 $startOpts .= ' SQL_CALC_FOUND_ROWS';
1714 }
1715
1716 if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
1717 $startOpts .= ' SQL_CACHE';
1718 }
1719
1720 if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
1721 $startOpts .= ' SQL_NO_CACHE';
1722 }
1723
1724 if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1725 $useIndex = $this->useIndexClause( $options['USE INDEX'] );
1726 } else {
1727 $useIndex = '';
1728 }
1729 if ( isset( $options['IGNORE INDEX'] ) && is_string( $options['IGNORE INDEX'] ) ) {
1730 $ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
1731 } else {
1732 $ignoreIndex = '';
1733 }
1734
1735 return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1736 }
1737
1746 protected function makeGroupByWithHaving( $options ) {
1747 $sql = '';
1748 if ( isset( $options['GROUP BY'] ) ) {
1749 $gb = is_array( $options['GROUP BY'] )
1750 ? implode( ',', $options['GROUP BY'] )
1751 : $options['GROUP BY'];
1752 $sql .= ' GROUP BY ' . $gb;
1753 }
1754 if ( isset( $options['HAVING'] ) ) {
1755 $having = is_array( $options['HAVING'] )
1756 ? $this->makeList( $options['HAVING'], self::LIST_AND )
1757 : $options['HAVING'];
1758 $sql .= ' HAVING ' . $having;
1759 }
1760
1761 return $sql;
1762 }
1763
1772 protected function makeOrderBy( $options ) {
1773 if ( isset( $options['ORDER BY'] ) ) {
1774 $ob = is_array( $options['ORDER BY'] )
1775 ? implode( ',', $options['ORDER BY'] )
1776 : $options['ORDER BY'];
1777
1778 return ' ORDER BY ' . $ob;
1779 }
1780
1781 return '';
1782 }
1783
1784 public function select(
1785 $table, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1786 ) {
1787 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1788
1789 return $this->query( $sql, $fname );
1790 }
1791
1792 public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1793 $options = [], $join_conds = []
1794 ) {
1795 if ( is_array( $vars ) ) {
1796 $fields = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1797 } else {
1798 $fields = $vars;
1799 }
1800
1801 $options = (array)$options;
1802 $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1803 ? $options['USE INDEX']
1804 : [];
1805 $ignoreIndexes = (
1806 isset( $options['IGNORE INDEX'] ) &&
1807 is_array( $options['IGNORE INDEX'] )
1808 )
1809 ? $options['IGNORE INDEX']
1810 : [];
1811
1812 if (
1813 $this->selectOptionsIncludeLocking( $options ) &&
1814 $this->selectFieldsOrOptionsAggregate( $vars, $options )
1815 ) {
1816 // Some DB types (postgres/oracle) disallow FOR UPDATE with aggregate
1817 // functions. Discourage use of such queries to encourage compatibility.
1818 call_user_func(
1819 $this->deprecationLogger,
1820 __METHOD__ . ": aggregation used with a locking SELECT ($fname)."
1821 );
1822 }
1823
1824 if ( is_array( $table ) ) {
1825 $from = ' FROM ' .
1826 $this->tableNamesWithIndexClauseOrJOIN(
1827 $table, $useIndexes, $ignoreIndexes, $join_conds );
1828 } elseif ( $table != '' ) {
1829 $from = ' FROM ' .
1830 $this->tableNamesWithIndexClauseOrJOIN(
1831 [ $table ], $useIndexes, $ignoreIndexes, [] );
1832 } else {
1833 $from = '';
1834 }
1835
1836 list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
1837 $this->makeSelectOptions( $options );
1838
1839 if ( is_array( $conds ) ) {
1840 $conds = $this->makeList( $conds, self::LIST_AND );
1841 }
1842
1843 if ( $conds === null || $conds === false ) {
1844 $this->queryLogger->warning(
1845 __METHOD__
1846 . ' called from '
1847 . $fname
1848 . ' with incorrect parameters: $conds must be a string or an array'
1849 );
1850 $conds = '';
1851 }
1852
1853 if ( $conds === '' || $conds === '*' ) {
1854 $sql = "SELECT $startOpts $fields $from $useIndex $ignoreIndex $preLimitTail";
1855 } elseif ( is_string( $conds ) ) {
1856 $sql = "SELECT $startOpts $fields $from $useIndex $ignoreIndex " .
1857 "WHERE $conds $preLimitTail";
1858 } else {
1859 throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
1860 }
1861
1862 if ( isset( $options['LIMIT'] ) ) {
1863 $sql = $this->limitResult( $sql, $options['LIMIT'],
1864 $options['OFFSET'] ?? false );
1865 }
1866 $sql = "$sql $postLimitTail";
1867
1868 if ( isset( $options['EXPLAIN'] ) ) {
1869 $sql = 'EXPLAIN ' . $sql;
1870 }
1871
1872 return $sql;
1873 }
1874
1875 public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1876 $options = [], $join_conds = []
1877 ) {
1878 $options = (array)$options;
1879 $options['LIMIT'] = 1;
1880 $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1881
1882 if ( $res === false ) {
1883 return false;
1884 }
1885
1886 if ( !$this->numRows( $res ) ) {
1887 return false;
1888 }
1889
1890 $obj = $this->fetchObject( $res );
1891
1892 return $obj;
1893 }
1894
1895 public function estimateRowCount(
1896 $table, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1897 ) {
1898 $conds = $this->normalizeConditions( $conds, $fname );
1899 $column = $this->extractSingleFieldFromList( $var );
1900 if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
1901 $conds[] = "$column IS NOT NULL";
1902 }
1903
1904 $res = $this->select(
1905 $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options, $join_conds
1906 );
1907 $row = $res ? $this->fetchRow( $res ) : [];
1908
1909 return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
1910 }
1911
1912 public function selectRowCount(
1913 $tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1914 ) {
1915 $conds = $this->normalizeConditions( $conds, $fname );
1916 $column = $this->extractSingleFieldFromList( $var );
1917 if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
1918 $conds[] = "$column IS NOT NULL";
1919 }
1920
1921 $res = $this->select(
1922 [
1923 'tmp_count' => $this->buildSelectSubquery(
1924 $tables,
1925 '1',
1926 $conds,
1927 $fname,
1928 $options,
1929 $join_conds
1930 )
1931 ],
1932 [ 'rowcount' => 'COUNT(*)' ],
1933 [],
1934 $fname
1935 );
1936 $row = $res ? $this->fetchRow( $res ) : [];
1937
1938 return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
1939 }
1940
1945 private function selectOptionsIncludeLocking( $options ) {
1946 $options = (array)$options;
1947 foreach ( [ 'FOR UPDATE', 'LOCK IN SHARE MODE' ] as $lock ) {
1948 if ( in_array( $lock, $options, true ) ) {
1949 return true;
1950 }
1951 }
1952
1953 return false;
1954 }
1955
1961 private function selectFieldsOrOptionsAggregate( $fields, $options ) {
1962 foreach ( (array)$options as $key => $value ) {
1963 if ( is_string( $key ) ) {
1964 if ( preg_match( '/^(?:GROUP BY|HAVING)$/i', $key ) ) {
1965 return true;
1966 }
1967 } elseif ( is_string( $value ) ) {
1968 if ( preg_match( '/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
1969 return true;
1970 }
1971 }
1972 }
1973
1974 $regex = '/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
1975 foreach ( (array)$fields as $field ) {
1976 if ( is_string( $field ) && preg_match( $regex, $field ) ) {
1977 return true;
1978 }
1979 }
1980
1981 return false;
1982 }
1983
1989 final protected function normalizeConditions( $conds, $fname ) {
1990 if ( $conds === null || $conds === false ) {
1991 $this->queryLogger->warning(
1992 __METHOD__
1993 . ' called from '
1994 . $fname
1995 . ' with incorrect parameters: $conds must be a string or an array'
1996 );
1997 $conds = '';
1998 }
1999
2000 if ( !is_array( $conds ) ) {
2001 $conds = ( $conds === '' ) ? [] : [ $conds ];
2002 }
2003
2004 return $conds;
2005 }
2006
2012 final protected function extractSingleFieldFromList( $var ) {
2013 if ( is_array( $var ) ) {
2014 if ( !$var ) {
2015 $column = null;
2016 } elseif ( count( $var ) == 1 ) {
2017 $column = $var[0] ?? reset( $var );
2018 } else {
2019 throw new DBUnexpectedError( $this, __METHOD__ . ': got multiple columns.' );
2020 }
2021 } else {
2022 $column = $var;
2023 }
2024
2025 return $column;
2026 }
2027
2028 public function lockForUpdate(
2029 $table, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
2030 ) {
2031 if ( !$this->trxLevel && !$this->getFlag( self::DBO_TRX ) ) {
2032 throw new DBUnexpectedError(
2033 $this,
2034 __METHOD__ . ': no transaction is active nor is DBO_TRX set'
2035 );
2036 }
2037
2038 $options = (array)$options;
2039 $options[] = 'FOR UPDATE';
2040
2041 return $this->selectRowCount( $table, '*', $conds, $fname, $options, $join_conds );
2042 }
2043
2052 protected static function generalizeSQL( $sql ) {
2053 # This does the same as the regexp below would do, but in such a way
2054 # as to avoid crashing php on some large strings.
2055 # $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
2056
2057 $sql = str_replace( "\\\\", '', $sql );
2058 $sql = str_replace( "\\'", '', $sql );
2059 $sql = str_replace( "\\\"", '', $sql );
2060 $sql = preg_replace( "/'.*'/s", "'X'", $sql );
2061 $sql = preg_replace( '/".*"/s', "'X'", $sql );
2062
2063 # All newlines, tabs, etc replaced by single space
2064 $sql = preg_replace( '/\s+/', ' ', $sql );
2065
2066 # All numbers => N,
2067 # except the ones surrounded by characters, e.g. l10n
2068 $sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
2069 $sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
2070
2071 return $sql;
2072 }
2073
2074 public function fieldExists( $table, $field, $fname = __METHOD__ ) {
2075 $info = $this->fieldInfo( $table, $field );
2076
2077 return (bool)$info;
2078 }
2079
2080 public function indexExists( $table, $index, $fname = __METHOD__ ) {
2081 if ( !$this->tableExists( $table ) ) {
2082 return null;
2083 }
2084
2085 $info = $this->indexInfo( $table, $index, $fname );
2086 if ( is_null( $info ) ) {
2087 return null;
2088 } else {
2089 return $info !== false;
2090 }
2091 }
2092
2093 abstract public function tableExists( $table, $fname = __METHOD__ );
2094
2095 public function indexUnique( $table, $index ) {
2096 $indexInfo = $this->indexInfo( $table, $index );
2097
2098 if ( !$indexInfo ) {
2099 return null;
2100 }
2101
2102 return !$indexInfo[0]->Non_unique;
2103 }
2104
2111 protected function makeInsertOptions( $options ) {
2112 return implode( ' ', $options );
2113 }
2114
2115 public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
2116 # No rows to insert, easy just return now
2117 if ( !count( $a ) ) {
2118 return true;
2119 }
2120
2121 $table = $this->tableName( $table );
2122
2123 if ( !is_array( $options ) ) {
2124 $options = [ $options ];
2125 }
2126
2128
2129 if ( isset( $a[0] ) && is_array( $a[0] ) ) {
2130 $multi = true;
2131 $keys = array_keys( $a[0] );
2132 } else {
2133 $multi = false;
2134 $keys = array_keys( $a );
2135 }
2136
2137 $sql = 'INSERT ' . $options .
2138 " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
2139
2140 if ( $multi ) {
2141 $first = true;
2142 foreach ( $a as $row ) {
2143 if ( $first ) {
2144 $first = false;
2145 } else {
2146 $sql .= ',';
2147 }
2148 $sql .= '(' . $this->makeList( $row ) . ')';
2149 }
2150 } else {
2151 $sql .= '(' . $this->makeList( $a ) . ')';
2152 }
2153
2154 $this->query( $sql, $fname );
2155
2156 return true;
2157 }
2158
2165 protected function makeUpdateOptionsArray( $options ) {
2166 if ( !is_array( $options ) ) {
2167 $options = [ $options ];
2168 }
2169
2170 $opts = [];
2171
2172 if ( in_array( 'IGNORE', $options ) ) {
2173 $opts[] = 'IGNORE';
2174 }
2175
2176 return $opts;
2177 }
2178
2185 protected function makeUpdateOptions( $options ) {
2186 $opts = $this->makeUpdateOptionsArray( $options );
2187
2188 return implode( ' ', $opts );
2189 }
2190
2191 public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
2192 $table = $this->tableName( $table );
2193 $opts = $this->makeUpdateOptions( $options );
2194 $sql = "UPDATE $opts $table SET " . $this->makeList( $values, self::LIST_SET );
2195
2196 if ( $conds !== [] && $conds !== '*' ) {
2197 $sql .= " WHERE " . $this->makeList( $conds, self::LIST_AND );
2198 }
2199
2200 $this->query( $sql, $fname );
2201
2202 return true;
2203 }
2204
2205 public function makeList( $a, $mode = self::LIST_COMMA ) {
2206 if ( !is_array( $a ) ) {
2207 throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
2208 }
2209
2210 $first = true;
2211 $list = '';
2212
2213 foreach ( $a as $field => $value ) {
2214 if ( !$first ) {
2215 if ( $mode == self::LIST_AND ) {
2216 $list .= ' AND ';
2217 } elseif ( $mode == self::LIST_OR ) {
2218 $list .= ' OR ';
2219 } else {
2220 $list .= ',';
2221 }
2222 } else {
2223 $first = false;
2224 }
2225
2226 if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
2227 $list .= "($value)";
2228 } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
2229 $list .= "$value";
2230 } elseif (
2231 ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
2232 ) {
2233 // Remove null from array to be handled separately if found
2234 $includeNull = false;
2235 foreach ( array_keys( $value, null, true ) as $nullKey ) {
2236 $includeNull = true;
2237 unset( $value[$nullKey] );
2238 }
2239 if ( count( $value ) == 0 && !$includeNull ) {
2240 throw new InvalidArgumentException(
2241 __METHOD__ . ": empty input for field $field" );
2242 } elseif ( count( $value ) == 0 ) {
2243 // only check if $field is null
2244 $list .= "$field IS NULL";
2245 } else {
2246 // IN clause contains at least one valid element
2247 if ( $includeNull ) {
2248 // Group subconditions to ensure correct precedence
2249 $list .= '(';
2250 }
2251 if ( count( $value ) == 1 ) {
2252 // Special-case single values, as IN isn't terribly efficient
2253 // Don't necessarily assume the single key is 0; we don't
2254 // enforce linear numeric ordering on other arrays here.
2255 $value = array_values( $value )[0];
2256 $list .= $field . " = " . $this->addQuotes( $value );
2257 } else {
2258 $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
2259 }
2260 // if null present in array, append IS NULL
2261 if ( $includeNull ) {
2262 $list .= " OR $field IS NULL)";
2263 }
2264 }
2265 } elseif ( $value === null ) {
2266 if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
2267 $list .= "$field IS ";
2268 } elseif ( $mode == self::LIST_SET ) {
2269 $list .= "$field = ";
2270 }
2271 $list .= 'NULL';
2272 } else {
2273 if (
2274 $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
2275 ) {
2276 $list .= "$field = ";
2277 }
2278 $list .= $mode == self::LIST_NAMES ? $value : $this->addQuotes( $value );
2279 }
2280 }
2281
2282 return $list;
2283 }
2284
2285 public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
2286 $conds = [];
2287
2288 foreach ( $data as $base => $sub ) {
2289 if ( count( $sub ) ) {
2290 $conds[] = $this->makeList(
2291 [ $baseKey => $base, $subKey => array_keys( $sub ) ],
2292 self::LIST_AND );
2293 }
2294 }
2295
2296 if ( $conds ) {
2297 return $this->makeList( $conds, self::LIST_OR );
2298 } else {
2299 // Nothing to search for...
2300 return false;
2301 }
2302 }
2303
2304 public function aggregateValue( $valuedata, $valuename = 'value' ) {
2305 return $valuename;
2306 }
2307
2308 public function bitNot( $field ) {
2309 return "(~$field)";
2310 }
2311
2312 public function bitAnd( $fieldLeft, $fieldRight ) {
2313 return "($fieldLeft & $fieldRight)";
2314 }
2315
2316 public function bitOr( $fieldLeft, $fieldRight ) {
2317 return "($fieldLeft | $fieldRight)";
2318 }
2319
2320 public function buildConcat( $stringList ) {
2321 return 'CONCAT(' . implode( ',', $stringList ) . ')';
2322 }
2323
2324 public function buildGroupConcatField(
2325 $delim, $table, $field, $conds = '', $join_conds = []
2326 ) {
2327 $fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
2328
2329 return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
2330 }
2331
2332 public function buildSubstring( $input, $startPosition, $length = null ) {
2333 $this->assertBuildSubstringParams( $startPosition, $length );
2334 $functionBody = "$input FROM $startPosition";
2335 if ( $length !== null ) {
2336 $functionBody .= " FOR $length";
2337 }
2338 return 'SUBSTRING(' . $functionBody . ')';
2339 }
2340
2353 protected function assertBuildSubstringParams( $startPosition, $length ) {
2354 if ( !is_int( $startPosition ) || $startPosition <= 0 ) {
2355 throw new InvalidArgumentException(
2356 '$startPosition must be a positive integer'
2357 );
2358 }
2359 if ( !( is_int( $length ) && $length >= 0 || $length === null ) ) {
2360 throw new InvalidArgumentException(
2361 '$length must be null or an integer greater than or equal to 0'
2362 );
2363 }
2364 }
2365
2366 public function buildStringCast( $field ) {
2367 // In theory this should work for any standards-compliant
2368 // SQL implementation, although it may not be the best way to do it.
2369 return "CAST( $field AS CHARACTER )";
2370 }
2371
2372 public function buildIntegerCast( $field ) {
2373 return 'CAST( ' . $field . ' AS INTEGER )';
2374 }
2375
2376 public function buildSelectSubquery(
2377 $table, $vars, $conds = '', $fname = __METHOD__,
2378 $options = [], $join_conds = []
2379 ) {
2380 return new Subquery(
2381 $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
2382 );
2383 }
2384
2385 public function databasesAreIndependent() {
2386 return false;
2387 }
2388
2389 final public function selectDB( $db ) {
2390 $this->selectDomain( new DatabaseDomain(
2391 $db,
2392 $this->currentDomain->getSchema(),
2393 $this->currentDomain->getTablePrefix()
2394 ) );
2395
2396 return true;
2397 }
2398
2399 final public function selectDomain( $domain ) {
2400 $this->doSelectDomain( DatabaseDomain::newFromId( $domain ) );
2401 }
2402
2403 protected function doSelectDomain( DatabaseDomain $domain ) {
2404 $this->currentDomain = $domain;
2405 }
2406
2407 public function getDBname() {
2408 return $this->currentDomain->getDatabase();
2409 }
2410
2411 public function getServer() {
2412 return $this->server;
2413 }
2414
2415 public function tableName( $name, $format = 'quoted' ) {
2416 if ( $name instanceof Subquery ) {
2417 throw new DBUnexpectedError(
2418 $this,
2419 __METHOD__ . ': got Subquery instance when expecting a string.'
2420 );
2421 }
2422
2423 # Skip the entire process when we have a string quoted on both ends.
2424 # Note that we check the end so that we will still quote any use of
2425 # use of `database`.table. But won't break things if someone wants
2426 # to query a database table with a dot in the name.
2427 if ( $this->isQuotedIdentifier( $name ) ) {
2428 return $name;
2429 }
2430
2431 # Lets test for any bits of text that should never show up in a table
2432 # name. Basically anything like JOIN or ON which are actually part of
2433 # SQL queries, but may end up inside of the table value to combine
2434 # sql. Such as how the API is doing.
2435 # Note that we use a whitespace test rather than a \b test to avoid
2436 # any remote case where a word like on may be inside of a table name
2437 # surrounded by symbols which may be considered word breaks.
2438 if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
2439 $this->queryLogger->warning(
2440 __METHOD__ . ": use of subqueries is not supported this way.",
2441 [ 'exception' => new RuntimeException() ]
2442 );
2443
2444 return $name;
2445 }
2446
2447 # Split database and table into proper variables.
2448 list( $database, $schema, $prefix, $table ) = $this->qualifiedTableComponents( $name );
2449
2450 # Quote $table and apply the prefix if not quoted.
2451 # $tableName might be empty if this is called from Database::replaceVars()
2452 $tableName = "{$prefix}{$table}";
2453 if ( $format === 'quoted'
2454 && !$this->isQuotedIdentifier( $tableName )
2455 && $tableName !== ''
2456 ) {
2457 $tableName = $this->addIdentifierQuotes( $tableName );
2458 }
2459
2460 # Quote $schema and $database and merge them with the table name if needed
2461 $tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
2462 $tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
2463
2464 return $tableName;
2465 }
2466
2473 protected function qualifiedTableComponents( $name ) {
2474 # We reverse the explode so that database.table and table both output the correct table.
2475 $dbDetails = explode( '.', $name, 3 );
2476 if ( count( $dbDetails ) == 3 ) {
2477 list( $database, $schema, $table ) = $dbDetails;
2478 # We don't want any prefix added in this case
2479 $prefix = '';
2480 } elseif ( count( $dbDetails ) == 2 ) {
2481 list( $database, $table ) = $dbDetails;
2482 # We don't want any prefix added in this case
2483 $prefix = '';
2484 # In dbs that support it, $database may actually be the schema
2485 # but that doesn't affect any of the functionality here
2486 $schema = '';
2487 } else {
2488 list( $table ) = $dbDetails;
2489 if ( isset( $this->tableAliases[$table] ) ) {
2490 $database = $this->tableAliases[$table]['dbname'];
2491 $schema = is_string( $this->tableAliases[$table]['schema'] )
2492 ? $this->tableAliases[$table]['schema']
2493 : $this->relationSchemaQualifier();
2494 $prefix = is_string( $this->tableAliases[$table]['prefix'] )
2495 ? $this->tableAliases[$table]['prefix']
2496 : $this->tablePrefix();
2497 } else {
2498 $database = '';
2499 $schema = $this->relationSchemaQualifier(); # Default schema
2500 $prefix = $this->tablePrefix(); # Default prefix
2501 }
2502 }
2503
2504 return [ $database, $schema, $prefix, $table ];
2505 }
2506
2513 private function prependDatabaseOrSchema( $namespace, $relation, $format ) {
2514 if ( strlen( $namespace ) ) {
2515 if ( $format === 'quoted' && !$this->isQuotedIdentifier( $namespace ) ) {
2516 $namespace = $this->addIdentifierQuotes( $namespace );
2517 }
2518 $relation = $namespace . '.' . $relation;
2519 }
2520
2521 return $relation;
2522 }
2523
2524 public function tableNames() {
2525 $inArray = func_get_args();
2526 $retVal = [];
2527
2528 foreach ( $inArray as $name ) {
2529 $retVal[$name] = $this->tableName( $name );
2530 }
2531
2532 return $retVal;
2533 }
2534
2535 public function tableNamesN() {
2536 $inArray = func_get_args();
2537 $retVal = [];
2538
2539 foreach ( $inArray as $name ) {
2540 $retVal[] = $this->tableName( $name );
2541 }
2542
2543 return $retVal;
2544 }
2545
2557 protected function tableNameWithAlias( $table, $alias = false ) {
2558 if ( is_string( $table ) ) {
2559 $quotedTable = $this->tableName( $table );
2560 } elseif ( $table instanceof Subquery ) {
2561 $quotedTable = (string)$table;
2562 } else {
2563 throw new InvalidArgumentException( "Table must be a string or Subquery." );
2564 }
2565
2566 if ( $alias === false || $alias === $table ) {
2567 if ( $table instanceof Subquery ) {
2568 throw new InvalidArgumentException( "Subquery table missing alias." );
2569 }
2570
2571 return $quotedTable;
2572 } else {
2573 return $quotedTable . ' ' . $this->addIdentifierQuotes( $alias );
2574 }
2575 }
2576
2583 protected function tableNamesWithAlias( $tables ) {
2584 $retval = [];
2585 foreach ( $tables as $alias => $table ) {
2586 if ( is_numeric( $alias ) ) {
2587 $alias = $table;
2588 }
2589 $retval[] = $this->tableNameWithAlias( $table, $alias );
2590 }
2591
2592 return $retval;
2593 }
2594
2603 protected function fieldNameWithAlias( $name, $alias = false ) {
2604 if ( !$alias || (string)$alias === (string)$name ) {
2605 return $name;
2606 } else {
2607 return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
2608 }
2609 }
2610
2617 protected function fieldNamesWithAlias( $fields ) {
2618 $retval = [];
2619 foreach ( $fields as $alias => $field ) {
2620 if ( is_numeric( $alias ) ) {
2621 $alias = $field;
2622 }
2623 $retval[] = $this->fieldNameWithAlias( $field, $alias );
2624 }
2625
2626 return $retval;
2627 }
2628
2640 $tables, $use_index = [], $ignore_index = [], $join_conds = []
2641 ) {
2642 $ret = [];
2643 $retJOIN = [];
2644 $use_index = (array)$use_index;
2645 $ignore_index = (array)$ignore_index;
2646 $join_conds = (array)$join_conds;
2647
2648 foreach ( $tables as $alias => $table ) {
2649 if ( !is_string( $alias ) ) {
2650 // No alias? Set it equal to the table name
2651 $alias = $table;
2652 }
2653
2654 if ( is_array( $table ) ) {
2655 // A parenthesized group
2656 if ( count( $table ) > 1 ) {
2657 $joinedTable = '(' .
2659 $table, $use_index, $ignore_index, $join_conds ) . ')';
2660 } else {
2661 // Degenerate case
2662 $innerTable = reset( $table );
2663 $innerAlias = key( $table );
2664 $joinedTable = $this->tableNameWithAlias(
2665 $innerTable,
2666 is_string( $innerAlias ) ? $innerAlias : $innerTable
2667 );
2668 }
2669 } else {
2670 $joinedTable = $this->tableNameWithAlias( $table, $alias );
2671 }
2672
2673 // Is there a JOIN clause for this table?
2674 if ( isset( $join_conds[$alias] ) ) {
2675 list( $joinType, $conds ) = $join_conds[$alias];
2676 $tableClause = $joinType;
2677 $tableClause .= ' ' . $joinedTable;
2678 if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
2679 $use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
2680 if ( $use != '' ) {
2681 $tableClause .= ' ' . $use;
2682 }
2683 }
2684 if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
2685 $ignore = $this->ignoreIndexClause(
2686 implode( ',', (array)$ignore_index[$alias] ) );
2687 if ( $ignore != '' ) {
2688 $tableClause .= ' ' . $ignore;
2689 }
2690 }
2691 $on = $this->makeList( (array)$conds, self::LIST_AND );
2692 if ( $on != '' ) {
2693 $tableClause .= ' ON (' . $on . ')';
2694 }
2695
2696 $retJOIN[] = $tableClause;
2697 } elseif ( isset( $use_index[$alias] ) ) {
2698 // Is there an INDEX clause for this table?
2699 $tableClause = $joinedTable;
2700 $tableClause .= ' ' . $this->useIndexClause(
2701 implode( ',', (array)$use_index[$alias] )
2702 );
2703
2704 $ret[] = $tableClause;
2705 } elseif ( isset( $ignore_index[$alias] ) ) {
2706 // Is there an INDEX clause for this table?
2707 $tableClause = $joinedTable;
2708 $tableClause .= ' ' . $this->ignoreIndexClause(
2709 implode( ',', (array)$ignore_index[$alias] )
2710 );
2711
2712 $ret[] = $tableClause;
2713 } else {
2714 $tableClause = $joinedTable;
2715
2716 $ret[] = $tableClause;
2717 }
2718 }
2719
2720 // We can't separate explicit JOIN clauses with ',', use ' ' for those
2721 $implicitJoins = $ret ? implode( ',', $ret ) : "";
2722 $explicitJoins = $retJOIN ? implode( ' ', $retJOIN ) : "";
2723
2724 // Compile our final table clause
2725 return implode( ' ', [ $implicitJoins, $explicitJoins ] );
2726 }
2727
2734 protected function indexName( $index ) {
2735 return $this->indexAliases[$index] ?? $index;
2736 }
2737
2738 public function addQuotes( $s ) {
2739 if ( $s instanceof Blob ) {
2740 $s = $s->fetch();
2741 }
2742 if ( $s === null ) {
2743 return 'NULL';
2744 } elseif ( is_bool( $s ) ) {
2745 return (int)$s;
2746 } else {
2747 # This will also quote numeric values. This should be harmless,
2748 # and protects against weird problems that occur when they really
2749 # _are_ strings such as article titles and string->number->string
2750 # conversion is not 1:1.
2751 return "'" . $this->strencode( $s ) . "'";
2752 }
2753 }
2754
2755 public function addIdentifierQuotes( $s ) {
2756 return '"' . str_replace( '"', '""', $s ) . '"';
2757 }
2758
2768 public function isQuotedIdentifier( $name ) {
2769 return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
2770 }
2771
2777 protected function escapeLikeInternal( $s, $escapeChar = '`' ) {
2778 return str_replace( [ $escapeChar, '%', '_' ],
2779 [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ],
2780 $s );
2781 }
2782
2783 public function buildLike() {
2784 $params = func_get_args();
2785
2786 if ( count( $params ) > 0 && is_array( $params[0] ) ) {
2787 $params = $params[0];
2788 }
2789
2790 $s = '';
2791
2792 // We use ` instead of \ as the default LIKE escape character, since addQuotes()
2793 // may escape backslashes, creating problems of double escaping. The `
2794 // character has good cross-DBMS compatibility, avoiding special operators
2795 // in MS SQL like ^ and %
2796 $escapeChar = '`';
2797
2798 foreach ( $params as $value ) {
2799 if ( $value instanceof LikeMatch ) {
2800 $s .= $value->toString();
2801 } else {
2802 $s .= $this->escapeLikeInternal( $value, $escapeChar );
2803 }
2804 }
2805
2806 return ' LIKE ' .
2807 $this->addQuotes( $s ) . ' ESCAPE ' . $this->addQuotes( $escapeChar ) . ' ';
2808 }
2809
2810 public function anyChar() {
2811 return new LikeMatch( '_' );
2812 }
2813
2814 public function anyString() {
2815 return new LikeMatch( '%' );
2816 }
2817
2818 public function nextSequenceValue( $seqName ) {
2819 return null;
2820 }
2821
2832 public function useIndexClause( $index ) {
2833 return '';
2834 }
2835
2846 public function ignoreIndexClause( $index ) {
2847 return '';
2848 }
2849
2850 public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2851 if ( count( $rows ) == 0 ) {
2852 return;
2853 }
2854
2855 $uniqueIndexes = (array)$uniqueIndexes;
2856 // Single row case
2857 if ( !is_array( reset( $rows ) ) ) {
2858 $rows = [ $rows ];
2859 }
2860
2861 try {
2862 $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
2864 foreach ( $rows as $row ) {
2865 // Delete rows which collide with this one
2866 $indexWhereClauses = [];
2867 foreach ( $uniqueIndexes as $index ) {
2868 $indexColumns = (array)$index;
2869 $indexRowValues = array_intersect_key( $row, array_flip( $indexColumns ) );
2870 if ( count( $indexRowValues ) != count( $indexColumns ) ) {
2871 throw new DBUnexpectedError(
2872 $this,
2873 'New record does not provide all values for unique key (' .
2874 implode( ', ', $indexColumns ) . ')'
2875 );
2876 } elseif ( in_array( null, $indexRowValues, true ) ) {
2877 throw new DBUnexpectedError(
2878 $this,
2879 'New record has a null value for unique key (' .
2880 implode( ', ', $indexColumns ) . ')'
2881 );
2882 }
2883 $indexWhereClauses[] = $this->makeList( $indexRowValues, LIST_AND );
2884 }
2885
2886 if ( $indexWhereClauses ) {
2887 $this->delete( $table, $this->makeList( $indexWhereClauses, LIST_OR ), $fname );
2888 $affectedRowCount += $this->affectedRows();
2889 }
2890
2891 // Now insert the row
2892 $this->insert( $table, $row, $fname );
2893 $affectedRowCount += $this->affectedRows();
2894 }
2895 $this->endAtomic( $fname );
2896 $this->affectedRowCount = $affectedRowCount;
2897 } catch ( Exception $e ) {
2898 $this->cancelAtomic( $fname );
2899 throw $e;
2900 }
2901 }
2902
2911 protected function nativeReplace( $table, $rows, $fname ) {
2912 $table = $this->tableName( $table );
2913
2914 # Single row case
2915 if ( !is_array( reset( $rows ) ) ) {
2916 $rows = [ $rows ];
2917 }
2918
2919 $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
2920 $first = true;
2921
2922 foreach ( $rows as $row ) {
2923 if ( $first ) {
2924 $first = false;
2925 } else {
2926 $sql .= ',';
2927 }
2928
2929 $sql .= '(' . $this->makeList( $row ) . ')';
2930 }
2931
2932 $this->query( $sql, $fname );
2933 }
2934
2935 public function upsert( $table, array $rows, $uniqueIndexes, array $set,
2936 $fname = __METHOD__
2937 ) {
2938 if ( $rows === [] ) {
2939 return true; // nothing to do
2940 }
2941
2942 $uniqueIndexes = (array)$uniqueIndexes;
2943 if ( !is_array( reset( $rows ) ) ) {
2944 $rows = [ $rows ];
2945 }
2946
2947 if ( count( $uniqueIndexes ) ) {
2948 $clauses = []; // list WHERE clauses that each identify a single row
2949 foreach ( $rows as $row ) {
2950 foreach ( $uniqueIndexes as $index ) {
2951 $index = is_array( $index ) ? $index : [ $index ]; // columns
2952 $rowKey = []; // unique key to this row
2953 foreach ( $index as $column ) {
2954 $rowKey[$column] = $row[$column];
2955 }
2956 $clauses[] = $this->makeList( $rowKey, self::LIST_AND );
2957 }
2958 }
2959 $where = [ $this->makeList( $clauses, self::LIST_OR ) ];
2960 } else {
2961 $where = false;
2962 }
2963
2965 try {
2966 $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
2967 # Update any existing conflicting row(s)
2968 if ( $where !== false ) {
2969 $this->update( $table, $set, $where, $fname );
2970 $affectedRowCount += $this->affectedRows();
2971 }
2972 # Now insert any non-conflicting row(s)
2973 $this->insert( $table, $rows, $fname, [ 'IGNORE' ] );
2974 $affectedRowCount += $this->affectedRows();
2975 $this->endAtomic( $fname );
2976 $this->affectedRowCount = $affectedRowCount;
2977 } catch ( Exception $e ) {
2978 $this->cancelAtomic( $fname );
2979 throw $e;
2980 }
2981
2982 return true;
2983 }
2984
2985 public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2986 $fname = __METHOD__
2987 ) {
2988 if ( !$conds ) {
2989 throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
2990 }
2991
2992 $delTable = $this->tableName( $delTable );
2993 $joinTable = $this->tableName( $joinTable );
2994 $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2995 if ( $conds != '*' ) {
2996 $sql .= 'WHERE ' . $this->makeList( $conds, self::LIST_AND );
2997 }
2998 $sql .= ')';
2999
3000 $this->query( $sql, $fname );
3001 }
3002
3003 public function textFieldSize( $table, $field ) {
3004 $table = $this->tableName( $table );
3005 $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
3006 $res = $this->query( $sql, __METHOD__ );
3007 $row = $this->fetchObject( $res );
3008
3009 $m = [];
3010
3011 if ( preg_match( '/\‍((.*)\‍)/', $row->Type, $m ) ) {
3012 $size = $m[1];
3013 } else {
3014 $size = -1;
3015 }
3016
3017 return $size;
3018 }
3019
3020 public function delete( $table, $conds, $fname = __METHOD__ ) {
3021 if ( !$conds ) {
3022 throw new DBUnexpectedError( $this, __METHOD__ . ' called with no conditions' );
3023 }
3024
3025 $table = $this->tableName( $table );
3026 $sql = "DELETE FROM $table";
3027
3028 if ( $conds != '*' ) {
3029 if ( is_array( $conds ) ) {
3030 $conds = $this->makeList( $conds, self::LIST_AND );
3031 }
3032 $sql .= ' WHERE ' . $conds;
3033 }
3034
3035 $this->query( $sql, $fname );
3036
3037 return true;
3038 }
3039
3040 final public function insertSelect(
3041 $destTable, $srcTable, $varMap, $conds,
3042 $fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = []
3043 ) {
3044 static $hints = [ 'NO_AUTO_COLUMNS' ];
3045
3046 $insertOptions = (array)$insertOptions;
3047 $selectOptions = (array)$selectOptions;
3048
3049 if ( $this->cliMode && $this->isInsertSelectSafe( $insertOptions, $selectOptions ) ) {
3050 // For massive migrations with downtime, we don't want to select everything
3051 // into memory and OOM, so do all this native on the server side if possible.
3052 $this->nativeInsertSelect(
3053 $destTable,
3054 $srcTable,
3055 $varMap,
3056 $conds,
3057 $fname,
3058 array_diff( $insertOptions, $hints ),
3059 $selectOptions,
3060 $selectJoinConds
3061 );
3062 } else {
3063 $this->nonNativeInsertSelect(
3064 $destTable,
3065 $srcTable,
3066 $varMap,
3067 $conds,
3068 $fname,
3069 array_diff( $insertOptions, $hints ),
3070 $selectOptions,
3071 $selectJoinConds
3072 );
3073 }
3074
3075 return true;
3076 }
3077
3084 protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
3085 return true;
3086 }
3087
3102 protected function nonNativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
3103 $fname = __METHOD__,
3104 $insertOptions = [], $selectOptions = [], $selectJoinConds = []
3105 ) {
3106 // For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
3107 // on only the master (without needing row-based-replication). It also makes it easy to
3108 // know how big the INSERT is going to be.
3109 $fields = [];
3110 foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
3111 $fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
3112 }
3113 $selectOptions[] = 'FOR UPDATE';
3114 $res = $this->select(
3115 $srcTable, implode( ',', $fields ), $conds, $fname, $selectOptions, $selectJoinConds
3116 );
3117 if ( !$res ) {
3118 return;
3119 }
3120
3121 try {
3123 $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
3124 $rows = [];
3125 $ok = true;
3126 foreach ( $res as $row ) {
3127 $rows[] = (array)$row;
3128
3129 // Avoid inserts that are too huge
3130 if ( count( $rows ) >= $this->nonNativeInsertSelectBatchSize ) {
3131 $ok = $this->insert( $destTable, $rows, $fname, $insertOptions );
3132 if ( !$ok ) {
3133 break;
3134 }
3135 $affectedRowCount += $this->affectedRows();
3136 $rows = [];
3137 }
3138 }
3139 if ( $rows && $ok ) {
3140 $ok = $this->insert( $destTable, $rows, $fname, $insertOptions );
3141 if ( $ok ) {
3142 $affectedRowCount += $this->affectedRows();
3143 }
3144 }
3145 if ( $ok ) {
3146 $this->endAtomic( $fname );
3147 $this->affectedRowCount = $affectedRowCount;
3148 } else {
3149 $this->cancelAtomic( $fname );
3150 }
3151 } catch ( Exception $e ) {
3152 $this->cancelAtomic( $fname );
3153 throw $e;
3154 }
3155 }
3156
3171 protected function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
3172 $fname = __METHOD__,
3173 $insertOptions = [], $selectOptions = [], $selectJoinConds = []
3174 ) {
3175 $destTable = $this->tableName( $destTable );
3176
3177 if ( !is_array( $insertOptions ) ) {
3178 $insertOptions = [ $insertOptions ];
3179 }
3180
3181 $insertOptions = $this->makeInsertOptions( $insertOptions );
3182
3183 $selectSql = $this->selectSQLText(
3184 $srcTable,
3185 array_values( $varMap ),
3186 $conds,
3187 $fname,
3188 $selectOptions,
3189 $selectJoinConds
3190 );
3191
3192 $sql = "INSERT $insertOptions" .
3193 " INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ') ' .
3194 $selectSql;
3195
3196 $this->query( $sql, $fname );
3197 }
3198
3218 public function limitResult( $sql, $limit, $offset = false ) {
3219 if ( !is_numeric( $limit ) ) {
3220 throw new DBUnexpectedError( $this,
3221 "Invalid non-numeric limit passed to limitResult()\n" );
3222 }
3223
3224 return "$sql LIMIT "
3225 . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
3226 . "{$limit} ";
3227 }
3228
3229 public function unionSupportsOrderAndLimit() {
3230 return true; // True for almost every DB supported
3231 }
3232
3233 public function unionQueries( $sqls, $all ) {
3234 $glue = $all ? ') UNION ALL (' : ') UNION (';
3235
3236 return '(' . implode( $glue, $sqls ) . ')';
3237 }
3238
3240 $table, $vars, array $permute_conds, $extra_conds = '', $fname = __METHOD__,
3241 $options = [], $join_conds = []
3242 ) {
3243 // First, build the Cartesian product of $permute_conds
3244 $conds = [ [] ];
3245 foreach ( $permute_conds as $field => $values ) {
3246 if ( !$values ) {
3247 // Skip empty $values
3248 continue;
3249 }
3250 $values = array_unique( $values ); // For sanity
3251 $newConds = [];
3252 foreach ( $conds as $cond ) {
3253 foreach ( $values as $value ) {
3254 $cond[$field] = $value;
3255 $newConds[] = $cond; // Arrays are by-value, not by-reference, so this works
3256 }
3257 }
3258 $conds = $newConds;
3259 }
3260
3261 $extra_conds = $extra_conds === '' ? [] : (array)$extra_conds;
3262
3263 // If there's just one condition and no subordering, hand off to
3264 // selectSQLText directly.
3265 if ( count( $conds ) === 1 &&
3266 ( !isset( $options['INNER ORDER BY'] ) || !$this->unionSupportsOrderAndLimit() )
3267 ) {
3268 return $this->selectSQLText(
3269 $table, $vars, $conds[0] + $extra_conds, $fname, $options, $join_conds
3270 );
3271 }
3272
3273 // Otherwise, we need to pull out the order and limit to apply after
3274 // the union. Then build the SQL queries for each set of conditions in
3275 // $conds. Then union them together (using UNION ALL, because the
3276 // product *should* already be distinct).
3277 $orderBy = $this->makeOrderBy( $options );
3278 $limit = $options['LIMIT'] ?? null;
3279 $offset = $options['OFFSET'] ?? false;
3280 $all = empty( $options['NOTALL'] ) && !in_array( 'NOTALL', $options );
3281 if ( !$this->unionSupportsOrderAndLimit() ) {
3282 unset( $options['ORDER BY'], $options['LIMIT'], $options['OFFSET'] );
3283 } else {
3284 if ( array_key_exists( 'INNER ORDER BY', $options ) ) {
3285 $options['ORDER BY'] = $options['INNER ORDER BY'];
3286 }
3287 if ( $limit !== null && is_numeric( $offset ) && $offset != 0 ) {
3288 // We need to increase the limit by the offset rather than
3289 // using the offset directly, otherwise it'll skip incorrectly
3290 // in the subqueries.
3291 $options['LIMIT'] = $limit + $offset;
3292 unset( $options['OFFSET'] );
3293 }
3294 }
3295
3296 $sqls = [];
3297 foreach ( $conds as $cond ) {
3298 $sqls[] = $this->selectSQLText(
3299 $table, $vars, $cond + $extra_conds, $fname, $options, $join_conds
3300 );
3301 }
3302 $sql = $this->unionQueries( $sqls, $all ) . $orderBy;
3303 if ( $limit !== null ) {
3304 $sql = $this->limitResult( $sql, $limit, $offset );
3305 }
3306
3307 return $sql;
3308 }
3309
3310 public function conditional( $cond, $trueVal, $falseVal ) {
3311 if ( is_array( $cond ) ) {
3312 $cond = $this->makeList( $cond, self::LIST_AND );
3313 }
3314
3315 return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
3316 }
3317
3318 public function strreplace( $orig, $old, $new ) {
3319 return "REPLACE({$orig}, {$old}, {$new})";
3320 }
3321
3322 public function getServerUptime() {
3323 return 0;
3324 }
3325
3326 public function wasDeadlock() {
3327 return false;
3328 }
3329
3330 public function wasLockTimeout() {
3331 return false;
3332 }
3333
3334 public function wasConnectionLoss() {
3335 return $this->wasConnectionError( $this->lastErrno() );
3336 }
3337
3338 public function wasReadOnlyError() {
3339 return false;
3340 }
3341
3342 public function wasErrorReissuable() {
3343 return (
3344 $this->wasDeadlock() ||
3345 $this->wasLockTimeout() ||
3346 $this->wasConnectionLoss()
3347 );
3348 }
3349
3356 public function wasConnectionError( $errno ) {
3357 return false;
3358 }
3359
3366 protected function wasKnownStatementRollbackError() {
3367 return false; // don't know; it could have caused a transaction rollback
3368 }
3369
3370 public function deadlockLoop() {
3371 $args = func_get_args();
3372 $function = array_shift( $args );
3373 $tries = self::DEADLOCK_TRIES;
3374
3375 $this->begin( __METHOD__ );
3376
3377 $retVal = null;
3379 $e = null;
3380 do {
3381 try {
3382 $retVal = $function( ...$args );
3383 break;
3384 } catch ( DBQueryError $e ) {
3385 if ( $this->wasDeadlock() ) {
3386 // Retry after a randomized delay
3387 usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
3388 } else {
3389 // Throw the error back up
3390 throw $e;
3391 }
3392 }
3393 } while ( --$tries > 0 );
3394
3395 if ( $tries <= 0 ) {
3396 // Too many deadlocks; give up
3397 $this->rollback( __METHOD__ );
3398 throw $e;
3399 } else {
3400 $this->commit( __METHOD__ );
3401
3402 return $retVal;
3403 }
3404 }
3405
3406 public function masterPosWait( DBMasterPos $pos, $timeout ) {
3407 # Real waits are implemented in the subclass.
3408 return 0;
3409 }
3410
3411 public function getReplicaPos() {
3412 # Stub
3413 return false;
3414 }
3415
3416 public function getMasterPos() {
3417 # Stub
3418 return false;
3419 }
3420
3421 public function serverIsReadOnly() {
3422 return false;
3423 }
3424
3425 final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
3426 if ( !$this->trxLevel ) {
3427 throw new DBUnexpectedError( $this, "No transaction is active." );
3428 }
3429 $this->trxEndCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3430 }
3431
3432 final public function onTransactionCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
3433 if ( !$this->trxLevel && $this->getTransactionRoundId() ) {
3434 // Start an implicit transaction similar to how query() does
3435 $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
3436 $this->trxAutomatic = true;
3437 }
3438
3439 $this->trxIdleCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3440 if ( !$this->trxLevel ) {
3441 $this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
3442 }
3443 }
3444
3445 final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
3446 $this->onTransactionCommitOrIdle( $callback, $fname );
3447 }
3448
3449 final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
3450 if ( !$this->trxLevel && $this->getTransactionRoundId() ) {
3451 // Start an implicit transaction similar to how query() does
3452 $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
3453 $this->trxAutomatic = true;
3454 }
3455
3456 if ( $this->trxLevel ) {
3457 $this->trxPreCommitCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3458 } else {
3459 // No transaction is active nor will start implicitly, so make one for this callback
3460 $this->startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
3461 try {
3462 $callback( $this );
3463 $this->endAtomic( __METHOD__ );
3464 } catch ( Exception $e ) {
3465 $this->cancelAtomic( __METHOD__ );
3466 throw $e;
3467 }
3468 }
3469 }
3470
3471 final public function onAtomicSectionCancel( callable $callback, $fname = __METHOD__ ) {
3472 if ( !$this->trxLevel || !$this->trxAtomicLevels ) {
3473 throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
3474 }
3475 $this->trxSectionCancelCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3476 }
3477
3481 private function currentAtomicSectionId() {
3482 if ( $this->trxLevel && $this->trxAtomicLevels ) {
3483 $levelInfo = end( $this->trxAtomicLevels );
3484
3485 return $levelInfo[1];
3486 }
3487
3488 return null;
3489 }
3490
3499 ) {
3500 foreach ( $this->trxPreCommitCallbacks as $key => $info ) {
3501 if ( $info[2] === $old ) {
3502 $this->trxPreCommitCallbacks[$key][2] = $new;
3503 }
3504 }
3505 foreach ( $this->trxIdleCallbacks as $key => $info ) {
3506 if ( $info[2] === $old ) {
3507 $this->trxIdleCallbacks[$key][2] = $new;
3508 }
3509 }
3510 foreach ( $this->trxEndCallbacks as $key => $info ) {
3511 if ( $info[2] === $old ) {
3512 $this->trxEndCallbacks[$key][2] = $new;
3513 }
3514 }
3515 foreach ( $this->trxSectionCancelCallbacks as $key => $info ) {
3516 if ( $info[2] === $old ) {
3517 $this->trxSectionCancelCallbacks[$key][2] = $new;
3518 }
3519 }
3520 }
3521
3542 array $sectionIds, AtomicSectionIdentifier $newSectionId = null
3543 ) {
3544 // Cancel the "on commit" callbacks owned by this savepoint
3545 $this->trxIdleCallbacks = array_filter(
3546 $this->trxIdleCallbacks,
3547 function ( $entry ) use ( $sectionIds ) {
3548 return !in_array( $entry[2], $sectionIds, true );
3549 }
3550 );
3551 $this->trxPreCommitCallbacks = array_filter(
3552 $this->trxPreCommitCallbacks,
3553 function ( $entry ) use ( $sectionIds ) {
3554 return !in_array( $entry[2], $sectionIds, true );
3555 }
3556 );
3557 // Make "on resolution" callbacks owned by this savepoint to perceive a rollback
3558 foreach ( $this->trxEndCallbacks as $key => $entry ) {
3559 if ( in_array( $entry[2], $sectionIds, true ) ) {
3560 $callback = $entry[0];
3561 $this->trxEndCallbacks[$key][0] = function () use ( $callback ) {
3562 // @phan-suppress-next-line PhanInfiniteRecursion No recursion at all here, phan is confused
3563 return $callback( self::TRIGGER_ROLLBACK, $this );
3564 };
3565 // This "on resolution" callback no longer belongs to a section.
3566 $this->trxEndCallbacks[$key][2] = null;
3567 }
3568 }
3569 // Hoist callback ownership for section cancel callbacks to the new top section
3570 foreach ( $this->trxSectionCancelCallbacks as $key => $entry ) {
3571 if ( in_array( $entry[2], $sectionIds, true ) ) {
3572 $this->trxSectionCancelCallbacks[$key][2] = $newSectionId;
3573 }
3574 }
3575 }
3576
3577 final public function setTransactionListener( $name, callable $callback = null ) {
3578 if ( $callback ) {
3579 $this->trxRecurringCallbacks[$name] = $callback;
3580 } else {
3581 unset( $this->trxRecurringCallbacks[$name] );
3582 }
3583 }
3584
3593 final public function setTrxEndCallbackSuppression( $suppress ) {
3594 $this->trxEndCallbacksSuppressed = $suppress;
3595 }
3596
3607 public function runOnTransactionIdleCallbacks( $trigger ) {
3608 if ( $this->trxLevel ) { // sanity
3609 throw new DBUnexpectedError( $this, __METHOD__ . ': a transaction is still open.' );
3610 }
3611
3612 if ( $this->trxEndCallbacksSuppressed ) {
3613 return 0;
3614 }
3615
3616 $count = 0;
3617 $autoTrx = $this->getFlag( self::DBO_TRX ); // automatic begin() enabled?
3619 $e = null; // first exception
3620 do { // callbacks may add callbacks :)
3621 $callbacks = array_merge(
3622 $this->trxIdleCallbacks,
3623 $this->trxEndCallbacks // include "transaction resolution" callbacks
3624 );
3625 $this->trxIdleCallbacks = []; // consumed (and recursion guard)
3626 $this->trxEndCallbacks = []; // consumed (recursion guard)
3627
3628 // Only run trxSectionCancelCallbacks on rollback, not commit.
3629 // But always consume them.
3630 if ( $trigger === self::TRIGGER_ROLLBACK ) {
3631 $callbacks = array_merge( $callbacks, $this->trxSectionCancelCallbacks );
3632 }
3633 $this->trxSectionCancelCallbacks = []; // consumed (recursion guard)
3634
3635 foreach ( $callbacks as $callback ) {
3636 ++$count;
3637 list( $phpCallback ) = $callback;
3638 $this->clearFlag( self::DBO_TRX ); // make each query its own transaction
3639 try {
3640 // @phan-suppress-next-line PhanParamTooManyCallable
3641 call_user_func( $phpCallback, $trigger, $this );
3642 } catch ( Exception $ex ) {
3643 call_user_func( $this->errorLogger, $ex );
3644 $e = $e ?: $ex;
3645 // Some callbacks may use startAtomic/endAtomic, so make sure
3646 // their transactions are ended so other callbacks don't fail
3647 if ( $this->trxLevel() ) {
3648 $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
3649 }
3650 } finally {
3651 if ( $autoTrx ) {
3652 $this->setFlag( self::DBO_TRX ); // restore automatic begin()
3653 } else {
3654 $this->clearFlag( self::DBO_TRX ); // restore auto-commit
3655 }
3656 }
3657 }
3658 } while ( count( $this->trxIdleCallbacks ) );
3659
3660 if ( $e instanceof Exception ) {
3661 throw $e; // re-throw any first exception
3662 }
3663
3664 return $count;
3665 }
3666
3677 $count = 0;
3678
3679 $e = null; // first exception
3680 do { // callbacks may add callbacks :)
3681 $callbacks = $this->trxPreCommitCallbacks;
3682 $this->trxPreCommitCallbacks = []; // consumed (and recursion guard)
3683 foreach ( $callbacks as $callback ) {
3684 try {
3685 ++$count;
3686 list( $phpCallback ) = $callback;
3687 $phpCallback( $this );
3688 } catch ( Exception $ex ) {
3689 ( $this->errorLogger )( $ex );
3690 $e = $e ?: $ex;
3691 }
3692 }
3693 } while ( count( $this->trxPreCommitCallbacks ) );
3694
3695 if ( $e instanceof Exception ) {
3696 throw $e; // re-throw any first exception
3697 }
3698
3699 return $count;
3700 }
3701
3710 $trigger, array $sectionIds = null
3711 ) {
3713 $e = null; // first exception
3714
3715 $notCancelled = [];
3716 do {
3718 $this->trxSectionCancelCallbacks = []; // consumed (recursion guard)
3719 foreach ( $callbacks as $entry ) {
3720 if ( $sectionIds === null || in_array( $entry[2], $sectionIds, true ) ) {
3721 try {
3722 $entry[0]( $trigger, $this );
3723 } catch ( Exception $ex ) {
3724 ( $this->errorLogger )( $ex );
3725 $e = $e ?: $ex;
3726 } catch ( Throwable $ex ) {
3727 // @todo: Log?
3728 $e = $e ?: $ex;
3729 }
3730 } else {
3731 $notCancelled[] = $entry;
3732 }
3733 }
3734 } while ( count( $this->trxSectionCancelCallbacks ) );
3735 $this->trxSectionCancelCallbacks = $notCancelled;
3736
3737 if ( $e !== null ) {
3738 throw $e; // re-throw any first Exception/Throwable
3739 }
3740 }
3741
3751 public function runTransactionListenerCallbacks( $trigger ) {
3752 if ( $this->trxEndCallbacksSuppressed ) {
3753 return;
3754 }
3755
3757 $e = null; // first exception
3758
3759 foreach ( $this->trxRecurringCallbacks as $phpCallback ) {
3760 try {
3761 $phpCallback( $trigger, $this );
3762 } catch ( Exception $ex ) {
3763 ( $this->errorLogger )( $ex );
3764 $e = $e ?: $ex;
3765 }
3766 }
3767
3768 if ( $e instanceof Exception ) {
3769 throw $e; // re-throw any first exception
3770 }
3771 }
3772
3783 protected function doSavepoint( $identifier, $fname ) {
3784 $this->query( 'SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
3785 }
3786
3797 protected function doReleaseSavepoint( $identifier, $fname ) {
3798 $this->query( 'RELEASE SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
3799 }
3800
3811 protected function doRollbackToSavepoint( $identifier, $fname ) {
3812 $this->query( 'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
3813 }
3814
3819 private function nextSavepointId( $fname ) {
3820 $savepointId = self::$SAVEPOINT_PREFIX . ++$this->trxAtomicCounter;
3821 if ( strlen( $savepointId ) > 30 ) {
3822 // 30 == Oracle's identifier length limit (pre 12c)
3823 // With a 22 character prefix, that puts the highest number at 99999999.
3824 throw new DBUnexpectedError(
3825 $this,
3826 'There have been an excessively large number of atomic sections in a transaction'
3827 . " started by $this->trxFname (at $fname)"
3828 );
3829 }
3830
3831 return $savepointId;
3832 }
3833
3834 final public function startAtomic(
3835 $fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE
3836 ) {
3837 $savepointId = $cancelable === self::ATOMIC_CANCELABLE ? self::$NOT_APPLICABLE : null;
3838
3839 if ( !$this->trxLevel ) {
3840 $this->begin( $fname, self::TRANSACTION_INTERNAL ); // sets trxAutomatic
3841 // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
3842 // in all changes being in one transaction to keep requests transactional.
3843 if ( $this->getFlag( self::DBO_TRX ) ) {
3844 // Since writes could happen in between the topmost atomic sections as part
3845 // of the transaction, those sections will need savepoints.
3846 $savepointId = $this->nextSavepointId( $fname );
3847 $this->doSavepoint( $savepointId, $fname );
3848 } else {
3849 $this->trxAutomaticAtomic = true;
3850 }
3851 } elseif ( $cancelable === self::ATOMIC_CANCELABLE ) {
3852 $savepointId = $this->nextSavepointId( $fname );
3853 $this->doSavepoint( $savepointId, $fname );
3854 }
3855
3856 $sectionId = new AtomicSectionIdentifier;
3857 $this->trxAtomicLevels[] = [ $fname, $sectionId, $savepointId ];
3858 $this->queryLogger->debug( 'startAtomic: entering level ' .
3859 ( count( $this->trxAtomicLevels ) - 1 ) . " ($fname)" );
3860
3861 return $sectionId;
3862 }
3863
3864 final public function endAtomic( $fname = __METHOD__ ) {
3865 if ( !$this->trxLevel || !$this->trxAtomicLevels ) {
3866 throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
3867 }
3868
3869 // Check if the current section matches $fname
3870 $pos = count( $this->trxAtomicLevels ) - 1;
3871 list( $savedFname, $sectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
3872 $this->queryLogger->debug( "endAtomic: leaving level $pos ($fname)" );
3873
3874 if ( $savedFname !== $fname ) {
3875 throw new DBUnexpectedError(
3876 $this,
3877 "Invalid atomic section ended (got $fname but expected $savedFname)."
3878 );
3879 }
3880
3881 // Remove the last section (no need to re-index the array)
3882 array_pop( $this->trxAtomicLevels );
3883
3884 if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
3885 $this->commit( $fname, self::FLUSHING_INTERNAL );
3886 } elseif ( $savepointId !== null && $savepointId !== self::$NOT_APPLICABLE ) {
3887 $this->doReleaseSavepoint( $savepointId, $fname );
3888 }
3889
3890 // Hoist callback ownership for callbacks in the section that just ended;
3891 // all callbacks should have an owner that is present in trxAtomicLevels.
3892 $currentSectionId = $this->currentAtomicSectionId();
3893 if ( $currentSectionId ) {
3894 $this->reassignCallbacksForSection( $sectionId, $currentSectionId );
3895 }
3896 }
3897
3898 final public function cancelAtomic(
3899 $fname = __METHOD__, AtomicSectionIdentifier $sectionId = null
3900 ) {
3901 if ( !$this->trxLevel || !$this->trxAtomicLevels ) {
3902 throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
3903 }
3904
3905 $excisedIds = [];
3906 $newTopSection = $this->currentAtomicSectionId();
3907 try {
3908 $excisedFnames = [];
3909 if ( $sectionId !== null ) {
3910 // Find the (last) section with the given $sectionId
3911 $pos = -1;
3912 foreach ( $this->trxAtomicLevels as $i => list( $asFname, $asId, $spId ) ) {
3913 if ( $asId === $sectionId ) {
3914 $pos = $i;
3915 }
3916 }
3917 if ( $pos < 0 ) {
3918 throw new DBUnexpectedError( $this, "Atomic section not found (for $fname)" );
3919 }
3920 // Remove all descendant sections and re-index the array
3921 $len = count( $this->trxAtomicLevels );
3922 for ( $i = $pos + 1; $i < $len; ++$i ) {
3923 $excisedFnames[] = $this->trxAtomicLevels[$i][0];
3924 $excisedIds[] = $this->trxAtomicLevels[$i][1];
3925 }
3926 $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 );
3927 $newTopSection = $this->currentAtomicSectionId();
3928 }
3929
3930 // Check if the current section matches $fname
3931 $pos = count( $this->trxAtomicLevels ) - 1;
3932 list( $savedFname, $savedSectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
3933
3934 if ( $excisedFnames ) {
3935 $this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname) " .
3936 "and descendants " . implode( ', ', $excisedFnames ) );
3937 } else {
3938 $this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname)" );
3939 }
3940
3941 if ( $savedFname !== $fname ) {
3942 throw new DBUnexpectedError(
3943 $this,
3944 "Invalid atomic section ended (got $fname but expected $savedFname)."
3945 );
3946 }
3947
3948 // Remove the last section (no need to re-index the array)
3949 array_pop( $this->trxAtomicLevels );
3950 $excisedIds[] = $savedSectionId;
3951 $newTopSection = $this->currentAtomicSectionId();
3952
3953 if ( $savepointId !== null ) {
3954 // Rollback the transaction to the state just before this atomic section
3955 if ( $savepointId === self::$NOT_APPLICABLE ) {
3956 $this->rollback( $fname, self::FLUSHING_INTERNAL );
3957 // Note: rollback() will run trxSectionCancelCallbacks
3958 } else {
3959 $this->doRollbackToSavepoint( $savepointId, $fname );
3960 $this->trxStatus = self::STATUS_TRX_OK; // no exception; recovered
3961 $this->trxStatusIgnoredCause = null;
3962
3963 // Run trxSectionCancelCallbacks now.
3964 $this->runOnAtomicSectionCancelCallbacks( self::TRIGGER_CANCEL, $excisedIds );
3965 }
3966 } elseif ( $this->trxStatus > self::STATUS_TRX_ERROR ) {
3967 // Put the transaction into an error state if it's not already in one
3968 $this->trxStatus = self::STATUS_TRX_ERROR;
3969 $this->trxStatusCause = new DBUnexpectedError(
3970 $this,
3971 "Uncancelable atomic section canceled (got $fname)."
3972 );
3973 }
3974 } finally {
3975 // Fix up callbacks owned by the sections that were just cancelled.
3976 // All callbacks should have an owner that is present in trxAtomicLevels.
3977 $this->modifyCallbacksForCancel( $excisedIds, $newTopSection );
3978 }
3979
3980 $this->affectedRowCount = 0; // for the sake of consistency
3981 }
3982
3983 final public function doAtomicSection(
3984 $fname, callable $callback, $cancelable = self::ATOMIC_NOT_CANCELABLE
3985 ) {
3986 $sectionId = $this->startAtomic( $fname, $cancelable );
3987 try {
3988 $res = $callback( $this, $fname );
3989 } catch ( Exception $e ) {
3990 $this->cancelAtomic( $fname, $sectionId );
3991
3992 throw $e;
3993 }
3994 $this->endAtomic( $fname );
3995
3996 return $res;
3997 }
3998
3999 final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
4000 static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
4001 if ( !in_array( $mode, $modes, true ) ) {
4002 throw new DBUnexpectedError( $this, "$fname: invalid mode parameter '$mode'." );
4003 }
4004
4005 // Protect against mismatched atomic section, transaction nesting, and snapshot loss
4006 if ( $this->trxLevel ) {
4007 if ( $this->trxAtomicLevels ) {
4008 $levels = $this->flatAtomicSectionList();
4009 $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
4010 throw new DBUnexpectedError( $this, $msg );
4011 } elseif ( !$this->trxAutomatic ) {
4012 $msg = "$fname: Explicit transaction already active (from {$this->trxFname}).";
4013 throw new DBUnexpectedError( $this, $msg );
4014 } else {
4015 $msg = "$fname: Implicit transaction already active (from {$this->trxFname}).";
4016 throw new DBUnexpectedError( $this, $msg );
4017 }
4018 } elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
4019 $msg = "$fname: Implicit transaction expected (DBO_TRX set).";
4020 throw new DBUnexpectedError( $this, $msg );
4021 }
4022
4024
4025 $this->doBegin( $fname );
4026 $this->trxStatus = self::STATUS_TRX_OK;
4027 $this->trxStatusIgnoredCause = null;
4028 $this->trxAtomicCounter = 0;
4029 $this->trxTimestamp = microtime( true );
4030 $this->trxFname = $fname;
4031 $this->trxDoneWrites = false;
4032 $this->trxAutomaticAtomic = false;
4033 $this->trxAtomicLevels = [];
4034 $this->trxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
4035 $this->trxWriteDuration = 0.0;
4036 $this->trxWriteQueryCount = 0;
4037 $this->trxWriteAffectedRows = 0;
4038 $this->trxWriteAdjDuration = 0.0;
4039 $this->trxWriteAdjQueryCount = 0;
4040 $this->trxWriteCallers = [];
4041 // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
4042 // Get an estimate of the replication lag before any such queries.
4043 $this->trxReplicaLag = null; // clear cached value first
4044 $this->trxReplicaLag = $this->getApproximateLagStatus()['lag'];
4045 // T147697: make explicitTrxActive() return true until begin() finishes. This way, no
4046 // caller will think its OK to muck around with the transaction just because startAtomic()
4047 // has not yet completed (e.g. setting trxAtomicLevels).
4048 $this->trxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
4049 }
4050
4057 protected function doBegin( $fname ) {
4058 $this->query( 'BEGIN', $fname );
4059 $this->trxLevel = 1;
4060 }
4061
4062 final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4063 static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
4064 if ( !in_array( $flush, $modes, true ) ) {
4065 throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'." );
4066 }
4067
4068 if ( $this->trxLevel && $this->trxAtomicLevels ) {
4069 // There are still atomic sections open; this cannot be ignored
4070 $levels = $this->flatAtomicSectionList();
4071 throw new DBUnexpectedError(
4072 $this,
4073 "$fname: Got COMMIT while atomic sections $levels are still open."
4074 );
4075 }
4076
4077 if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
4078 if ( !$this->trxLevel ) {
4079 return; // nothing to do
4080 } elseif ( !$this->trxAutomatic ) {
4081 throw new DBUnexpectedError(
4082 $this,
4083 "$fname: Flushing an explicit transaction, getting out of sync."
4084 );
4085 }
4086 } elseif ( !$this->trxLevel ) {
4087 $this->queryLogger->error(
4088 "$fname: No transaction to commit, something got out of sync." );
4089 return; // nothing to do
4090 } elseif ( $this->trxAutomatic ) {
4091 throw new DBUnexpectedError(
4092 $this,
4093 "$fname: Expected mass commit of all peer transactions (DBO_TRX set)."
4094 );
4095 }
4096
4098
4100
4101 $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
4102 $this->doCommit( $fname );
4103 $this->trxStatus = self::STATUS_TRX_NONE;
4104
4105 if ( $this->trxDoneWrites ) {
4106 $this->lastWriteTime = microtime( true );
4107 $this->trxProfiler->transactionWritingOut(
4108 $this->server,
4109 $this->getDomainID(),
4110 $this->trxShortId,
4111 $writeTime,
4112 $this->trxWriteAffectedRows
4113 );
4114 }
4115
4116 // With FLUSHING_ALL_PEERS, callbacks will be explicitly run later
4117 if ( $flush !== self::FLUSHING_ALL_PEERS ) {
4118 $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
4119 $this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
4120 }
4121 }
4122
4129 protected function doCommit( $fname ) {
4130 if ( $this->trxLevel ) {
4131 $this->query( 'COMMIT', $fname );
4132 $this->trxLevel = 0;
4133 }
4134 }
4135
4136 final public function rollback( $fname = __METHOD__, $flush = '' ) {
4137 $trxActive = $this->trxLevel;
4138
4139 if ( $flush !== self::FLUSHING_INTERNAL
4140 && $flush !== self::FLUSHING_ALL_PEERS
4141 && $this->getFlag( self::DBO_TRX )
4142 ) {
4143 throw new DBUnexpectedError(
4144 $this,
4145 "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)."
4146 );
4147 }
4148
4149 if ( $trxActive ) {
4151
4152 $this->doRollback( $fname );
4153 $this->trxStatus = self::STATUS_TRX_NONE;
4154 $this->trxAtomicLevels = [];
4155 // Estimate the RTT via a query now that trxStatus is OK
4156 $writeTime = $this->pingAndCalculateLastTrxApplyTime();
4157
4158 if ( $this->trxDoneWrites ) {
4159 $this->trxProfiler->transactionWritingOut(
4160 $this->server,
4161 $this->getDomainID(),
4162 $this->trxShortId,
4163 $writeTime,
4164 $this->trxWriteAffectedRows
4165 );
4166 }
4167 }
4168
4169 // Clear any commit-dependant callbacks. They might even be present
4170 // only due to transaction rounds, with no SQL transaction being active
4171 $this->trxIdleCallbacks = [];
4172 $this->trxPreCommitCallbacks = [];
4173
4174 // With FLUSHING_ALL_PEERS, callbacks will be explicitly run later
4175 if ( $trxActive && $flush !== self::FLUSHING_ALL_PEERS ) {
4176 try {
4177 $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
4178 } catch ( Exception $e ) {
4179 // already logged; finish and let LoadBalancer move on during mass-rollback
4180 }
4181 try {
4182 $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
4183 } catch ( Exception $e ) {
4184 // already logged; let LoadBalancer move on during mass-rollback
4185 }
4186
4187 $this->affectedRowCount = 0; // for the sake of consistency
4188 }
4189 }
4190
4197 protected function doRollback( $fname ) {
4198 if ( $this->trxLevel ) {
4199 # Disconnects cause rollback anyway, so ignore those errors
4200 $ignoreErrors = true;
4201 $this->query( 'ROLLBACK', $fname, $ignoreErrors );
4202 $this->trxLevel = 0;
4203 }
4204 }
4205
4206 public function flushSnapshot( $fname = __METHOD__ ) {
4207 if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
4208 // This only flushes transactions to clear snapshots, not to write data
4209 $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
4210 throw new DBUnexpectedError(
4211 $this,
4212 "$fname: Cannot flush snapshot because writes are pending ($fnames)."
4213 );
4214 }
4215
4216 $this->commit( $fname, self::FLUSHING_INTERNAL );
4217 }
4218
4219 public function explicitTrxActive() {
4220 return $this->trxLevel && ( $this->trxAtomicLevels || !$this->trxAutomatic );
4221 }
4222
4224 $oldName, $newName, $temporary = false, $fname = __METHOD__
4225 ) {
4226 throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
4227 }
4228
4229 public function listTables( $prefix = null, $fname = __METHOD__ ) {
4230 throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
4231 }
4232
4233 public function listViews( $prefix = null, $fname = __METHOD__ ) {
4234 throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
4235 }
4236
4237 public function timestamp( $ts = 0 ) {
4238 $t = new ConvertibleTimestamp( $ts );
4239 // Let errors bubble up to avoid putting garbage in the DB
4240 return $t->getTimestamp( TS_MW );
4241 }
4242
4243 public function timestampOrNull( $ts = null ) {
4244 if ( is_null( $ts ) ) {
4245 return null;
4246 } else {
4247 return $this->timestamp( $ts );
4248 }
4249 }
4250
4251 public function affectedRows() {
4252 return ( $this->affectedRowCount === null )
4253 ? $this->fetchAffectedRowCount() // default to driver value
4255 }
4256
4260 abstract protected function fetchAffectedRowCount();
4261
4275 protected function resultObject( $result ) {
4276 if ( !$result ) {
4277 return false;
4278 } elseif ( $result instanceof ResultWrapper ) {
4279 return $result;
4280 } elseif ( $result === true ) {
4281 // Successful write query
4282 return $result;
4283 } else {
4284 return new ResultWrapper( $this, $result );
4285 }
4286 }
4287
4288 public function ping( &$rtt = null ) {
4289 // Avoid hitting the server if it was hit recently
4290 if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
4291 if ( !func_num_args() || $this->rttEstimate > 0 ) {
4292 $rtt = $this->rttEstimate;
4293 return true; // don't care about $rtt
4294 }
4295 }
4296
4297 // This will reconnect if possible or return false if not
4298 $this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
4299 $ok = ( $this->query( self::PING_QUERY, __METHOD__, true ) !== false );
4300 $this->restoreFlags( self::RESTORE_PRIOR );
4301
4302 if ( $ok ) {
4303 $rtt = $this->rttEstimate;
4304 }
4305
4306 return $ok;
4307 }
4308
4315 protected function replaceLostConnection( $fname ) {
4316 $this->closeConnection();
4317 $this->opened = false;
4318 $this->conn = false;
4319
4321
4322 try {
4323 $this->open(
4324 $this->server,
4325 $this->user,
4326 $this->password,
4327 $this->getDBname(),
4328 $this->dbSchema(),
4329 $this->tablePrefix()
4330 );
4331 $this->lastPing = microtime( true );
4332 $ok = true;
4333
4334 $this->connLogger->warning(
4335 $fname . ': lost connection to {dbserver}; reconnected',
4336 [
4337 'dbserver' => $this->getServer(),
4338 'exception' => new RuntimeException()
4339 ]
4340 );
4341 } catch ( DBConnectionError $e ) {
4342 $ok = false;
4343
4344 $this->connLogger->error(
4345 $fname . ': lost connection to {dbserver} permanently',
4346 [ 'dbserver' => $this->getServer() ]
4347 );
4348 }
4349
4351
4352 return $ok;
4353 }
4354
4355 public function getSessionLagStatus() {
4356 return $this->getRecordedTransactionLagStatus() ?: $this->getApproximateLagStatus();
4357 }
4358
4372 final protected function getRecordedTransactionLagStatus() {
4373 return ( $this->trxLevel && $this->trxReplicaLag !== null )
4374 ? [ 'lag' => $this->trxReplicaLag, 'since' => $this->trxTimestamp() ]
4375 : null;
4376 }
4377
4384 protected function getApproximateLagStatus() {
4385 return [
4386 'lag' => $this->getLBInfo( 'replica' ) ? $this->getLag() : 0,
4387 'since' => microtime( true )
4388 ];
4389 }
4390
4410 public static function getCacheSetOptions( IDatabase $db1, IDatabase $db2 = null ) {
4411 $res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
4412 foreach ( func_get_args() as $db ) {
4414 $status = $db->getSessionLagStatus();
4415 if ( $status['lag'] === false ) {
4416 $res['lag'] = false;
4417 } elseif ( $res['lag'] !== false ) {
4418 $res['lag'] = max( $res['lag'], $status['lag'] );
4419 }
4420 $res['since'] = min( $res['since'], $status['since'] );
4421 $res['pending'] = $res['pending'] ?: $db->writesPending();
4422 }
4423
4424 return $res;
4425 }
4426
4427 public function getLag() {
4428 return 0;
4429 }
4430
4431 public function maxListLen() {
4432 return 0;
4433 }
4434
4435 public function encodeBlob( $b ) {
4436 return $b;
4437 }
4438
4439 public function decodeBlob( $b ) {
4440 if ( $b instanceof Blob ) {
4441 $b = $b->fetch();
4442 }
4443 return $b;
4444 }
4445
4446 public function setSessionOptions( array $options ) {
4447 }
4448
4449 public function sourceFile(
4450 $filename,
4451 callable $lineCallback = null,
4452 callable $resultCallback = null,
4453 $fname = false,
4454 callable $inputCallback = null
4455 ) {
4456 Wikimedia\suppressWarnings();
4457 $fp = fopen( $filename, 'r' );
4458 Wikimedia\restoreWarnings();
4459
4460 if ( $fp === false ) {
4461 throw new RuntimeException( "Could not open \"{$filename}\".\n" );
4462 }
4463
4464 if ( !$fname ) {
4465 $fname = __METHOD__ . "( $filename )";
4466 }
4467
4468 try {
4469 $error = $this->sourceStream(
4470 $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
4471 } catch ( Exception $e ) {
4472 fclose( $fp );
4473 throw $e;
4474 }
4475
4476 fclose( $fp );
4477
4478 return $error;
4479 }
4480
4481 public function setSchemaVars( $vars ) {
4482 $this->schemaVars = $vars;
4483 }
4484
4485 public function sourceStream(
4486 $fp,
4487 callable $lineCallback = null,
4488 callable $resultCallback = null,
4489 $fname = __METHOD__,
4490 callable $inputCallback = null
4491 ) {
4492 $delimiterReset = new ScopedCallback(
4493 function ( $delimiter ) {
4494 $this->delimiter = $delimiter;
4495 },
4497 );
4498 $cmd = '';
4499
4500 while ( !feof( $fp ) ) {
4501 if ( $lineCallback ) {
4502 call_user_func( $lineCallback );
4503 }
4504
4505 $line = trim( fgets( $fp ) );
4506
4507 if ( $line == '' ) {
4508 continue;
4509 }
4510
4511 if ( $line[0] == '-' && $line[1] == '-' ) {
4512 continue;
4513 }
4514
4515 if ( $cmd != '' ) {
4516 $cmd .= ' ';
4517 }
4518
4519 $done = $this->streamStatementEnd( $cmd, $line );
4520
4521 $cmd .= "$line\n";
4522
4523 if ( $done || feof( $fp ) ) {
4524 $cmd = $this->replaceVars( $cmd );
4525
4526 if ( $inputCallback ) {
4527 $callbackResult = $inputCallback( $cmd );
4528
4529 if ( is_string( $callbackResult ) || !$callbackResult ) {
4530 $cmd = $callbackResult;
4531 }
4532 }
4533
4534 if ( $cmd ) {
4535 $res = $this->query( $cmd, $fname );
4536
4537 if ( $resultCallback ) {
4538 $resultCallback( $res, $this );
4539 }
4540
4541 if ( $res === false ) {
4542 $err = $this->lastError();
4543
4544 return "Query \"{$cmd}\" failed with error code \"$err\".\n";
4545 }
4546 }
4547 $cmd = '';
4548 }
4549 }
4550
4551 ScopedCallback::consume( $delimiterReset );
4552 return true;
4553 }
4554
4562 public function streamStatementEnd( &$sql, &$newLine ) {
4563 if ( $this->delimiter ) {
4564 $prev = $newLine;
4565 $newLine = preg_replace(
4566 '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
4567 if ( $newLine != $prev ) {
4568 return true;
4569 }
4570 }
4571
4572 return false;
4573 }
4574
4595 protected function replaceVars( $ins ) {
4596 $vars = $this->getSchemaVars();
4597 return preg_replace_callback(
4598 '!
4599 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
4600 \'\{\$ (\w+) }\' | # 3. addQuotes
4601 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
4602 /\*\$ (\w+) \*/ # 5. leave unencoded
4603 !x',
4604 function ( $m ) use ( $vars ) {
4605 // Note: Because of <https://bugs.php.net/bug.php?id=51881>,
4606 // check for both nonexistent keys *and* the empty string.
4607 if ( isset( $m[1] ) && $m[1] !== '' ) {
4608 if ( $m[1] === 'i' ) {
4609 return $this->indexName( $m[2] );
4610 } else {
4611 return $this->tableName( $m[2] );
4612 }
4613 } elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
4614 return $this->addQuotes( $vars[$m[3]] );
4615 } elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
4616 return $this->addIdentifierQuotes( $vars[$m[4]] );
4617 } elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
4618 return $vars[$m[5]];
4619 } else {
4620 return $m[0];
4621 }
4622 },
4623 $ins
4624 );
4625 }
4626
4633 protected function getSchemaVars() {
4634 if ( $this->schemaVars ) {
4635 return $this->schemaVars;
4636 } else {
4637 return $this->getDefaultSchemaVars();
4638 }
4639 }
4640
4649 protected function getDefaultSchemaVars() {
4650 return [];
4651 }
4652
4653 public function lockIsFree( $lockName, $method ) {
4654 // RDBMs methods for checking named locks may or may not count this thread itself.
4655 // In MySQL, IS_FREE_LOCK() returns 0 if the thread already has the lock. This is
4656 // the behavior choosen by the interface for this method.
4657 return !isset( $this->namedLocksHeld[$lockName] );
4658 }
4659
4660 public function lock( $lockName, $method, $timeout = 5 ) {
4661 $this->namedLocksHeld[$lockName] = 1;
4662
4663 return true;
4664 }
4665
4666 public function unlock( $lockName, $method ) {
4667 unset( $this->namedLocksHeld[$lockName] );
4668
4669 return true;
4670 }
4671
4672 public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
4673 if ( $this->writesOrCallbacksPending() ) {
4674 // This only flushes transactions to clear snapshots, not to write data
4675 $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
4676 throw new DBUnexpectedError(
4677 $this,
4678 "$fname: Cannot flush pre-lock snapshot because writes are pending ($fnames)."
4679 );
4680 }
4681
4682 if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
4683 return null;
4684 }
4685
4686 $unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
4687 if ( $this->trxLevel() ) {
4688 // There is a good chance an exception was thrown, causing any early return
4689 // from the caller. Let any error handler get a chance to issue rollback().
4690 // If there isn't one, let the error bubble up and trigger server-side rollback.
4692 function () use ( $lockKey, $fname ) {
4693 $this->unlock( $lockKey, $fname );
4694 },
4695 $fname
4696 );
4697 } else {
4698 $this->unlock( $lockKey, $fname );
4699 }
4700 } );
4701
4702 $this->commit( $fname, self::FLUSHING_INTERNAL );
4703
4704 return $unlocker;
4705 }
4706
4707 public function namedLocksEnqueue() {
4708 return false;
4709 }
4710
4712 return true;
4713 }
4714
4715 final public function lockTables( array $read, array $write, $method ) {
4716 if ( $this->writesOrCallbacksPending() ) {
4717 throw new DBUnexpectedError( $this, "Transaction writes or callbacks still pending." );
4718 }
4719
4720 if ( $this->tableLocksHaveTransactionScope() ) {
4721 $this->startAtomic( $method );
4722 }
4723
4724 return $this->doLockTables( $read, $write, $method );
4725 }
4726
4735 protected function doLockTables( array $read, array $write, $method ) {
4736 return true;
4737 }
4738
4739 final public function unlockTables( $method ) {
4740 if ( $this->tableLocksHaveTransactionScope() ) {
4741 $this->endAtomic( $method );
4742
4743 return true; // locks released on COMMIT/ROLLBACK
4744 }
4745
4746 return $this->doUnlockTables( $method );
4747 }
4748
4755 protected function doUnlockTables( $method ) {
4756 return true;
4757 }
4758
4766 public function dropTable( $tableName, $fName = __METHOD__ ) {
4767 if ( !$this->tableExists( $tableName, $fName ) ) {
4768 return false;
4769 }
4770 $sql = "DROP TABLE " . $this->tableName( $tableName ) . " CASCADE";
4771
4772 return $this->query( $sql, $fName );
4773 }
4774
4775 public function getInfinity() {
4776 return 'infinity';
4777 }
4778
4779 public function encodeExpiry( $expiry ) {
4780 return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
4781 ? $this->getInfinity()
4782 : $this->timestamp( $expiry );
4783 }
4784
4785 public function decodeExpiry( $expiry, $format = TS_MW ) {
4786 if ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) {
4787 return 'infinity';
4788 }
4789
4790 return ConvertibleTimestamp::convert( $format, $expiry );
4791 }
4792
4793 public function setBigSelects( $value = true ) {
4794 // no-op
4795 }
4796
4797 public function isReadOnly() {
4798 return ( $this->getReadOnlyReason() !== false );
4799 }
4800
4804 protected function getReadOnlyReason() {
4805 $reason = $this->getLBInfo( 'readOnlyReason' );
4806
4807 return is_string( $reason ) ? $reason : false;
4808 }
4809
4810 public function setTableAliases( array $aliases ) {
4811 $this->tableAliases = $aliases;
4812 }
4813
4814 public function setIndexAliases( array $aliases ) {
4815 $this->indexAliases = $aliases;
4816 }
4817
4823 protected function hasFlags( $field, $flags ) {
4824 return ( ( $field & $flags ) === $flags );
4825 }
4826
4838 protected function getBindingHandle() {
4839 if ( !$this->conn ) {
4840 throw new DBUnexpectedError(
4841 $this,
4842 'DB connection was already closed or the connection dropped.'
4843 );
4844 }
4845
4846 return $this->conn;
4847 }
4848
4853 public function __toString() {
4854 return (string)$this->conn;
4855 }
4856
4861 public function __clone() {
4862 $this->connLogger->warning(
4863 "Cloning " . static::class . " is not recommended; forking connection",
4864 [ 'exception' => new RuntimeException() ]
4865 );
4866
4867 if ( $this->isOpen() ) {
4868 // Open a new connection resource without messing with the old one
4869 $this->opened = false;
4870 $this->conn = false;
4871 $this->trxEndCallbacks = []; // don't copy
4872 $this->trxSectionCancelCallbacks = []; // don't copy
4873 $this->handleSessionLossPreconnect(); // no trx or locks anymore
4874 $this->open(
4875 $this->server,
4876 $this->user,
4877 $this->password,
4878 $this->getDBname(),
4879 $this->dbSchema(),
4880 $this->tablePrefix()
4881 );
4882 $this->lastPing = microtime( true );
4883 }
4884 }
4885
4891 public function __sleep() {
4892 throw new RuntimeException( 'Database serialization may cause problems, since ' .
4893 'the connection is not restored on wakeup.' );
4894 }
4895
4899 public function __destruct() {
4900 if ( $this->trxLevel && $this->trxDoneWrites ) {
4901 trigger_error( "Uncommitted DB writes (transaction from {$this->trxFname})." );
4902 }
4903
4904 $danglingWriters = $this->pendingWriteAndCallbackCallers();
4905 if ( $danglingWriters ) {
4906 $fnames = implode( ', ', $danglingWriters );
4907 trigger_error( "DB transaction writes or callbacks still pending ($fnames)." );
4908 }
4909
4910 if ( $this->conn ) {
4911 // Avoid connection leaks for sanity. Normally, resources close at script completion.
4912 // The connection might already be closed in zend/hhvm by now, so suppress warnings.
4913 Wikimedia\suppressWarnings();
4914 $this->closeConnection();
4915 Wikimedia\restoreWarnings();
4916 $this->conn = false;
4917 $this->opened = false;
4918 }
4919 }
4920}
4921
4925class_alias( Database::class, 'DatabaseBase' );
4926
4930class_alias( Database::class, 'Database' );
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings,...
Definition Setup.php:123
$line
Definition cdb.php:59
if( $line===false) $args
Definition cdb.php:64
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:58
Simple store for keeping values in an associative array for the current process.
Class used for token representing identifiers for atomic sections from IDatabase instances.
Exception class for attempted DB write access to a DBConnRef with the DB_REPLICA role.
Class to handle database/prefix specification for IDatabase domains.
Relational database abstraction object.
Definition Database.php:49
bool $cliMode
Whether this PHP instance is for a CLI script.
Definition Database.php:92
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:576
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...
runOnTransactionPreCommitCallbacks()
Actually consume and run any "on transaction pre-commit" callbacks.
pendingWriteRowsAffected()
Get the number of affected rows from pending write queries.
Definition Database.php:764
integer null $affectedRowCount
Rows affected by the last query to query() or its CRUD wrappers.
Definition Database.php:143
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().
array[] $trxIdleCallbacks
List of (callable, method name, atomic section id)
Definition Database.php:114
static string $SAVEPOINT_PREFIX
Prefix to the atomic section counter used to make savepoint IDs.
Definition Database.php:279
restoreErrorHandler()
Restore the previous error handler and return the last PHP error for this DB.
Definition Database.php:892
getApproximateLagStatus()
Get a replica DB lag estimate for this server.
callable $errorLogger
Error logging callback.
Definition Database.php:104
bool $trxDoneWrites
Record if possible write queries were done in the last transaction started.
Definition Database.php:197
insert( $table, $a, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
strencode( $s)
Wrapper for addslashes()
tableNamesWithIndexClauseOrJOIN( $tables, $use_index=[], $ignore_index=[], $join_conds=[])
Get the aliased table name clause for a FROM clause which might have a JOIN and/or USE INDEX or IGNOR...
const DEADLOCK_DELAY_MIN
Minimum time to wait before retry, in microseconds.
Definition Database.php:53
int[] $priorFlags
Prior flags member variable values.
Definition Database.php:266
object resource null $conn
Database connection.
Definition Database.php:109
doInitConnection()
Actually connect to the database over the wire (or to local files)
Definition Database.php:363
upsert( $table, array $rows, $uniqueIndexes, array $set, $fname=__METHOD__)
INSERT ON DUPLICATE KEY UPDATE wrapper, upserts an array into a table.
conditional( $cond, $trueVal, $falseVal)
Returns an SQL expression for a simple conditional.
selectDB( $db)
Change the current database.
trxTimestamp()
Get the UNIX timestamp of the time that the transaction was established.
Definition Database.php:595
reassignCallbacksForSection(AtomicSectionIdentifier $old, AtomicSectionIdentifier $new)
Hoist callback ownership for callbacks in a section to a parent section.
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:607
setLogger(LoggerInterface $logger)
Set the PSR-3 logger interface to use for query logging.
Definition Database.php:572
int $trxWriteQueryCount
Number of write queries for the current transaction.
Definition Database.php:236
isInsertSelectSafe(array $insertOptions, array $selectOptions)
indexUnique( $table, $index)
Determines if a given index is unique.
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.
handleSessionLossPostconnect()
Clean things up after session (and thus transaction) loss after reconnect.
fieldExists( $table, $field, $fname=__METHOD__)
Determines whether a field exists in a table.
open( $server, $user, $password, $dbName, $schema, $tablePrefix)
Open a new connection to the database (closing any existing one)
nextSequenceValue( $seqName)
Deprecated method, calls should be removed.
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:620
wasLockTimeout()
Determines if the last failure was due to a lock timeout.
endAtomic( $fname=__METHOD__)
Ends an atomic section of SQL statements.
cancelAtomic( $fname=__METHOD__, AtomicSectionIdentifier $sectionId=null)
Cancel an atomic section of SQL statements.
setSessionOptions(array $options)
Override database's default behavior.
string $agent
Agent name for query profiling.
Definition Database.php:94
indexName( $index)
Allows for index remapping in queries where this is not consistent across DBMS.
callable[] $trxRecurringCallbacks
Map of (name => callable)
Definition Database.php:120
getDomainID()
Return the currently selected domain ID.
Definition Database.php:853
makeUpdateOptionsArray( $options)
Make UPDATE options array for Database::makeUpdateOptions.
listViews( $prefix=null, $fname=__METHOD__)
Lists all the VIEWs in the database.
array[] $trxPreCommitCallbacks
List of (callable, method name, atomic section id)
Definition Database.php:116
doCommit( $fname)
Issues the COMMIT command to the database server.
closeConnection()
Closes underlying database connection.
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 ...
string $user
User that this instance is currently connected under the name of.
Definition Database.php:84
static getCacheSetOptions(IDatabase $db1, IDatabase $db2=null)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
getRecordedTransactionLagStatus()
Get the replica DB lag when the current transaction started.
array $sessionTempTables
Map of (table name => 1) for TEMPORARY tables.
Definition Database.php:257
lastDoneWrites()
Returns the last time the connection may have been used for write queries.
Definition Database.php:694
isTransactableQuery( $sql)
Determine whether a SQL statement is sensitive to isolation level.
int $trxStatus
Transaction status.
Definition Database.php:148
replaceLostConnection( $fname)
Close any existing (dead) database connection and open a new connection.
getServerUptime()
Determines how long the server has been up.
bitAnd( $fieldLeft, $fieldRight)
setIndexAliases(array $aliases)
Convert certain index names to alternative names before querying the DB.
getDefaultSchemaVars()
Get schema variables to use if none have been set via setSchemaVars().
doBegin( $fname)
Issues the BEGIN command to the database server.
writesOrCallbacksPending()
Whether there is a transaction open with either possible write queries or unresolved pre-commit/commi...
Definition Database.php:702
buildGroupConcatField( $delim, $table, $field, $conds='', $join_conds=[])
Build a GROUP_CONCAT or equivalent statement for a query.
handleSessionLossPreconnect()
Clean things up after session (and thus transaction) loss before reconnect.
initConnection()
Initialize the connection to the database over the wire (or to local files)
Definition Database.php:348
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.
string[] $trxWriteCallers
Track the write query callers of the current transaction.
Definition Database.php:228
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__)
Alias for onTransactionCommitOrIdle() for backwards-compatibility.
setBigSelects( $value=true)
Allow or deny "big selects" for this session only.
string[] $indexAliases
Map of (index alias => index)
Definition Database.php:90
array null $trxStatusIgnoredCause
If wasKnownStatementRollbackError() prevented trxStatus from being set, the relevant details are stor...
Definition Database.php:157
wasConnectionLoss()
Determines if the last query error was due to a dropped connection.
databasesAreIndependent()
Returns true if DBs are assumed to be on potentially different servers.
assertHasConnectionHandle()
Make sure there is an open connection handle (alive or not) as a sanity check.
lock( $lockName, $method, $timeout=5)
Acquire a named lock.
wasConnectionError( $errno)
Do not use this method outside of Database/DBError classes.
callable $deprecationLogger
Deprecation logging callback.
Definition Database.php:106
int $trxWriteAffectedRows
Number of rows affected by write queries for the current transaction.
Definition Database.php:240
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...
assertIsWritableMaster()
Make sure that this server is not marked as a replica nor read-only as a sanity check.
duplicateTableStructure( $oldName, $newName, $temporary=false, $fname=__METHOD__)
Creates a new table with structure copied from existing table.
doRollbackToSavepoint( $identifier, $fname)
Rollback to a savepoint.
doneWrites()
Returns true if the connection may have been used for write queries.
Definition Database.php:690
anyChar()
Returns a token for buildLike() that denotes a '_' to be used in a LIKE query.
array[] $trxSectionCancelCallbacks
List of (callable, method name, atomic section id)
Definition Database.php:122
restoreFlags( $state=self::RESTORE_PRIOR)
Restore the flags to their prior state before the last setFlag/clearFlag call.
Definition Database.php:827
doAtomicSection( $fname, callable $callback, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Perform an atomic section of reversable SQL statements from a callback.
setTrxEndCallbackSuppression( $suppress)
Whether to disable running of post-COMMIT/ROLLBACK callbacks.
float $rttEstimate
RTT time estimate.
Definition Database.php:252
installErrorHandler()
Set a custom error handler for logging errors during database connection.
Definition Database.php:881
affectedRows()
Get the number of rows affected by the last write query.
buildSubstring( $input, $startPosition, $length=null)
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.
selectDomain( $domain)
Set the current domain (database, schema, and table prefix)
tableNameWithAlias( $table, $alias=false)
Get an aliased table name.
streamStatementEnd(&$sql, &$newLine)
Called by sourceStream() to check if we've reached a statement end.
float null $trxTimestamp
The UNIX time that the transaction started.
Definition Database.php:180
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.
static factory( $dbType, $p=[], $connect=self::NEW_CONNECTED)
Construct a Database subclass instance given a database type and parameters.
Definition Database.php:437
buildConcat( $stringList)
Build a concatenation list to feed into a SQL query.
ignoreIndexClause( $index)
IGNORE INDEX clause.
string $trxFname
Remembers the function name given for starting the most recent transaction via begin().
Definition Database.php:190
tableNamesWithAlias( $tables)
Gets an array of aliased table names.
bitOr( $fieldLeft, $fieldRight)
clearFlag( $flag, $remember=self::REMEMBER_NOTHING)
Clear a flag for this connection.
Definition Database.php:816
useIndexClause( $index)
USE INDEX clause.
DatabaseDomain $currentDomain
Definition Database.php:141
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:922
maxListLen()
Return the maximum number of items allowed in a list, or 0 for unlimited.
array $connectionParams
Parameters used by initConnection() to establish a connection.
Definition Database.php:96
LoggerInterface $queryLogger
Definition Database.php:102
int $trxAtomicCounter
Counter for atomic savepoint identifiers.
Definition Database.php:210
pendingWriteAndCallbackCallers()
List the methods that have write queries or callbacks for the current transaction.
Definition Database.php:776
bool $trxEndCallbacksSuppressed
Whether to suppress triggering of transaction end callbacks.
Definition Database.php:124
Exception null $trxStatusCause
The last error that caused the status to become STATUS_TRX_ERROR.
Definition Database.php:152
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...
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:760
pendingWriteQueryDuration( $type=self::ESTIMATE_TOTAL)
Get the time spend running write queries for this transaction.
Definition Database.php:730
startAtomic( $fname=__METHOD__, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Begin an atomic section of SQL statements.
getWikiID()
Alias for getDomainID()
Definition Database.php:857
nonNativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname=__METHOD__, $insertOptions=[], $selectOptions=[], $selectJoinConds=[])
Implementation of insertSelect() based on select() and insert()
commit( $fname=__METHOD__, $flush=self::FLUSHING_ONE)
Commits a transaction previously started using begin().
getLBInfo( $name=null)
Get properties passed down from the server info array of the load balancer.
Definition Database.php:645
aggregateValue( $valuedata, $valuename='value')
Return aggregated value alias.
array[] $tableAliases
Map of (table => (dbname, schema, prefix) map)
Definition Database.php:88
getMasterPos()
Get the position of this master.
buildSelectSubquery( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Equivalent to IDatabase::selectSQLText() except wraps the result in Subqyery.
__sleep()
Called by serialize.
addIdentifierQuotes( $s)
Quotes an identifier, in order to make user controlled input safe.
callable null $profiler
Definition Database.php:269
doSavepoint( $identifier, $fname)
Create a savepoint.
dropTable( $tableName, $fName=__METHOD__)
Delete a table.
isWriteQuery( $sql)
Determine whether a query writes to the DB.
indexInfo( $table, $index, $fname=__METHOD__)
Get information about an index into an object.
doSelectDomain(DatabaseDomain $domain)
hasFlags( $field, $flags)
getLogContext(array $extras=[])
Create a log context to pass to PSR-3 logger functions.
Definition Database.php:932
modifyCallbacksForCancel(array $sectionIds, AtomicSectionIdentifier $newSectionId=null)
Update callbacks that were owned by cancelled atomic sections.
trxLevel()
Gets the current transaction level.
Definition Database.php:591
prependDatabaseOrSchema( $namespace, $relation, $format)
const DEADLOCK_DELAY_MAX
Maximum time to wait before retry.
Definition Database.php:55
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
runOnTransactionIdleCallbacks( $trigger)
Actually consume and run any "on transaction idle/resolution" callbacks.
const DEADLOCK_TRIES
Number of times to re-try an operation in case of deadlock.
Definition Database.php:51
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')
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:805
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:686
static getClass( $dbType, $driver=null)
Definition Database.php:509
setLazyMasterHandle(IDatabase $conn)
Set a lazy-connecting DB handle to the master DB (for replication status purposes)
Definition Database.php:665
rollback( $fname=__METHOD__, $flush='')
Rollback a transaction previously started using begin().
sourceFile( $filename, callable $lineCallback=null, callable $resultCallback=null, $fname=false, callable $inputCallback=null)
Read and execute SQL commands from a file.
assertBuildSubstringParams( $startPosition, $length)
Check type and bounds for parameters to self::buildSubstring()
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.
static attributesFromType( $dbType, $driver=null)
Definition Database.php:492
IDatabase null $lazyMasterHandle
Lazy handle to the master DB this server replicates from.
Definition Database.php:260
float bool $lastWriteTime
UNIX timestamp of last write query.
Definition Database.php:78
getServer()
Get the server hostname or IP address.
getLag()
Get the amount of replication lag for this database server.
selectSQLText( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
The equivalent of IDatabase::select() except that the constructed SQL is returned,...
string $lastQuery
SQL query.
Definition Database.php:76
onTransactionPreCommitOrIdle(callable $callback, $fname=__METHOD__)
Run a callback before the current transaction commits or now if there is none.
runOnAtomicSectionCancelCallbacks( $trigger, array $sectionIds=null)
Actually run any "atomic section cancel" callbacks.
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 something outside of the query itself.
doReleaseSavepoint( $identifier, $fname)
Release a savepoint.
string $password
Password used to establish the current connection.
Definition Database.php:86
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:657
escapeLikeInternal( $s, $escapeChar='`')
bool $trxAutomatic
Record if the current transaction was started implicitly due to DBO_TRX being set.
Definition Database.php:204
ping(&$rtt=null)
Ping the server and try to reconnect if it there is no connection.
int $trxLevel
Either 1 if a transaction is active or 0 otherwise.
Definition Database.php:164
registerTempTableWrite( $sql, $pseudoPermanent)
__clone()
Make sure that copies do not share the same client binding handle.
string bool null $htmlErrors
Stashed value of html_errors INI setting.
Definition Database.php:137
implicitGroupby()
Returns true if this database does an implicit sort when doing GROUP BY.
Definition Database.php:678
anyString()
Returns a token for buildLike() that denotes a '' to be used in a LIKE query.
LoggerInterface $connLogger
Definition Database.php:100
string $trxShortId
Either a short hexidecimal string if a transaction is active or "".
Definition Database.php:171
bool $trxAutomaticAtomic
Record if the current transaction was started implicitly by Database::startAtomic.
Definition Database.php:222
bufferResults( $buffer=null)
Turns buffering of SQL result sets on (true) or off (false).
Definition Database.php:580
getInfinity()
Find out when 'infinity' is.
assertTransactionStatus( $sql, $fname)
Error out if the DB is not in a valid state for a query via query()
string $server
Server that this instance is currently connected to.
Definition Database.php:82
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.
decodeExpiry( $expiry, $format=TS_MW)
Decode an expiry time into a DBMS independent format.
setTransactionListener( $name, callable $callback=null)
Run a callback after 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 not locked by any thread (non-blocking)
isOpen()
Is a connection to the database open?
Definition Database.php:801
getFlag( $flag)
Returns a boolean whether the flag $flag is set for this connection.
Definition Database.php:840
masterPosWait(DBMasterPos $pos, $timeout)
Wait for the replica DB to catch up to a given master position.
array $trxAtomicLevels
Array of levels of atomicity within transactions.
Definition Database.php:216
float $trxWriteAdjDuration
Like trxWriteQueryCount but excludes lock-bound, easy to replicate, queries.
Definition Database.php:244
float $trxReplicaLag
Lag estimate at the time of BEGIN.
Definition Database.php:182
const PING_TTL
How long before it is worth doing a dummy query to test the connection.
Definition Database.php:58
timestampOrNull( $ts=null)
Convert a timestamp in one of the formats accepted by wfTimestamp() to the format used for inserting ...
array $namedLocksHeld
Map of (name => 1) for locks obtained via lock()
Definition Database.php:255
float $lastPing
UNIX timestamp.
Definition Database.php:263
array[] $trxEndCallbacks
List of (callable, method name, atomic section id)
Definition Database.php:118
int $trxWriteAdjQueryCount
Number of write queries counted in trxWriteAdjDuration.
Definition Database.php:248
TransactionProfiler $trxProfiler
Definition Database.php:271
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.
onAtomicSectionCancel(callable $callback, $fname=__METHOD__)
Run a callback when the atomic section is cancelled.
BagOStuff $srvCache
APC cache.
Definition Database.php:98
query( $sql, $fname=__METHOD__, $flags=0)
Run an SQL query and return the result.
float $trxWriteDuration
Seconds spent in write queries for the current transaction.
Definition Database.php:232
getBindingHandle()
Get the underlying binding connection handle.
static string $NOT_APPLICABLE
Idiom used when a cancelable atomic section started the transaction.
Definition Database.php:277
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)
Run a query and return a DBMS-dependent wrapper or boolean.
implicitOrderby()
Returns true if this database does an implicit order by when the column has an index For example: SEL...
Definition Database.php:682
makeOrderBy( $options)
Returns an optional ORDER BY.
strreplace( $orig, $old, $new)
Returns a command for str_replace function in SQL query.
__construct(array $params)
Definition Database.php:297
textFieldSize( $table, $field)
Returns the size of a text field, or -1 for "unlimited".
getDBname()
Get the current DB name.
close()
Close the database connection.
Definition Database.php:943
onTransactionCommitOrIdle(callable $callback, $fname=__METHOD__)
Run a callback as soon as there is no transaction pending.
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
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 and we might be restricted by PHP settings such as safe mode or open_basedir We cannot assume that the software even has read access anywhere useful Many shared hosts run all users web applications under the same so they can t rely on Unix and must forbid reads to even standard directories like tmp lest users read each others files We cannot assume that the user has the ability to install or run any programs not written as web accessible PHP scripts Since anything that works on cheap shared hosting will work if you have shell or root access MediaWiki s design is based around catering to the lowest common denominator Although we support higher end setups as the way many things work by default is tailored toward shared hosting These defaults are unconventional from the point of view of and they certainly aren t ideal for someone who s installing MediaWiki as MediaWiki does not conform to normal Unix filesystem layout Hopefully we ll offer direct support for standard layouts in the but for now *any change to the location of files is unsupported *Moving things and leaving symlinks will *probably *not break but it is *strongly *advised not to try any more intrusive changes to get MediaWiki to conform more closely to your filesystem hierarchy Any such attempt will almost certainly result in unnecessary bugs The standard recommended location to install relative to the web is it should be possible to enable the appropriate rewrite rules by if you can reconfigure the web server
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 and we might be restricted by PHP settings such as safe mode or open_basedir We cannot assume that the software even has read access anywhere useful Many shared hosts run all users web applications under the same user
Wikitext formatted, in the key only.
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
const LIST_OR
Definition Defines.php:55
const LIST_AND
Definition Defines.php:52
static configuration should be added through ResourceLoaderGetConfigVars instead & $vars
Definition hooks.txt:2228
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:2818
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. 'ImgAuthModifyHeaders':Executed just before a file is streamed to a user via img_auth.php, allowing headers to be modified beforehand. $title:LinkTarget object & $headers:HTTP headers(name=> value, names are case insensitive). Two headers get special handling:If-Modified-Since(value must be a valid HTTP date) and Range(must be of the form "bytes=(\d*-\d*)") will be honored when streaming the file. '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 'ImportHandleUnknownUser':When a user doesn 't exist locally, this hook is called to give extensions an opportunity to auto-create it. If the auto-creation is successful, return false. $name:User name '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. '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 '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 since 1.28! 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:1991
This code would result in ircNotify being run twice when an article is and once for brion Hooks can return three possible true was required This is the default since MediaWiki *some string
Definition hooks.txt:181
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:1776
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. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header '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). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. '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:1266
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:1999
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message key
Definition hooks.txt:2163
this hook is for auditing only 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:996
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:2003
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:271
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and insert
Definition hooks.txt:2089
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
returning false will NOT prevent logging $e
Definition hooks.txt:2175
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:38
lastErrno()
Get the last error number.
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().
Advanced database interface for IDatabase handles that include maintenance methods.
fieldInfo( $table, $field)
mysql_fetch_field() wrapper Returns false if the field doesn't exist
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
$buffer
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...
if(is_array($mode)) switch( $mode) $input
const DBO_IGNORE
Definition defines.php:11
const DBO_TRX
Definition defines.php:12
$params