MediaWiki REL1_32
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;
41
48abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAwareInterface {
50 const DEADLOCK_TRIES = 4;
52 const DEADLOCK_DELAY_MIN = 500000;
54 const DEADLOCK_DELAY_MAX = 1500000;
55
57 const PING_TTL = 1.0;
58 const PING_QUERY = 'SELECT 1 AS ping';
59
60 const TINY_WRITE_SEC = 0.010;
61 const SLOW_WRITE_SEC = 0.500;
62 const SMALL_WRITE_ROWS = 100;
63
65 const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
66
68 const NEW_UNCONNECTED = 0;
70 const NEW_CONNECTED = 1;
71
73 protected $lastQuery = '';
75 protected $lastWriteTime = false;
77 protected $phpError = false;
79 protected $server;
81 protected $user;
83 protected $password;
85 protected $tableAliases = [];
87 protected $indexAliases = [];
89 protected $cliMode;
91 protected $agent;
93 protected $connectionParams = [];
95 protected $srvCache;
97 protected $connLogger;
99 protected $queryLogger;
101 protected $errorLogger;
104
106 protected $conn = null;
108 protected $opened = false;
109
111 protected $trxIdleCallbacks = [];
115 protected $trxEndCallbacks = [];
119 protected $trxEndCallbacksSuppressed = false;
120
122 protected $flags;
124 protected $lbInfo = [];
126 protected $schemaVars = false;
128 protected $sessionVars = [];
130 protected $preparedArgs;
132 protected $htmlErrors;
134 protected $delimiter = ';';
136 protected $currentDomain;
139
143 protected $trxStatus = self::STATUS_TRX_NONE;
159 protected $trxLevel = 0;
166 protected $trxShortId = '';
175 private $trxTimestamp = null;
177 private $trxReplicaLag = null;
185 private $trxFname = null;
192 private $trxDoneWrites = false;
199 private $trxAutomatic = false;
205 private $trxAtomicCounter = 0;
211 private $trxAtomicLevels = [];
217 private $trxAutomaticAtomic = false;
223 private $trxWriteCallers = [];
227 private $trxWriteDuration = 0.0;
239 private $trxWriteAdjDuration = 0.0;
247 private $rttEstimate = 0.0;
248
250 private $namedLocksHeld = [];
252 protected $sessionTempTables = [];
253
256
258 protected $lastPing = 0.0;
259
261 private $priorFlags = [];
262
264 protected $profiler;
266 protected $trxProfiler;
267
270
272 private static $NOT_APPLICABLE = 'n/a';
274 private static $SAVEPOINT_PREFIX = 'wikimedia_rdbms_atomic';
275
277 const STATUS_TRX_ERROR = 1;
279 const STATUS_TRX_OK = 2;
281 const STATUS_TRX_NONE = 3;
282
287 protected function __construct( array $params ) {
288 foreach ( [ 'host', 'user', 'password', 'dbname', 'schema', 'tablePrefix' ] as $name ) {
289 $this->connectionParams[$name] = $params[$name];
290 }
291
292 $this->cliMode = $params['cliMode'];
293 // Agent name is added to SQL queries in a comment, so make sure it can't break out
294 $this->agent = str_replace( '/', '-', $params['agent'] );
295
296 $this->flags = $params['flags'];
297 if ( $this->flags & self::DBO_DEFAULT ) {
298 if ( $this->cliMode ) {
299 $this->flags &= ~self::DBO_TRX;
300 } else {
301 $this->flags |= self::DBO_TRX;
302 }
303 }
304 // Disregard deprecated DBO_IGNORE flag (T189999)
305 $this->flags &= ~self::DBO_IGNORE;
306
307 $this->sessionVars = $params['variables'];
308
309 $this->srvCache = $params['srvCache'] ?? new HashBagOStuff();
310
311 $this->profiler = $params['profiler'];
312 $this->trxProfiler = $params['trxProfiler'];
313 $this->connLogger = $params['connLogger'];
314 $this->queryLogger = $params['queryLogger'];
315 $this->errorLogger = $params['errorLogger'];
316 $this->deprecationLogger = $params['deprecationLogger'];
317
318 if ( isset( $params['nonNativeInsertSelectBatchSize'] ) ) {
319 $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'];
320 }
321
322 // Set initial dummy domain until open() sets the final DB/prefix
323 $this->currentDomain = new DatabaseDomain(
324 $params['dbname'] != '' ? $params['dbname'] : null,
325 $params['schema'] != '' ? $params['schema'] : null,
326 $params['tablePrefix']
327 );
328 }
329
338 final public function initConnection() {
339 if ( $this->isOpen() ) {
340 throw new LogicException( __METHOD__ . ': already connected.' );
341 }
342 // Establish the connection
343 $this->doInitConnection();
344 }
345
353 protected function doInitConnection() {
354 if ( strlen( $this->connectionParams['user'] ) ) {
355 $this->open(
356 $this->connectionParams['host'],
357 $this->connectionParams['user'],
358 $this->connectionParams['password'],
359 $this->connectionParams['dbname'],
360 $this->connectionParams['schema'],
361 $this->connectionParams['tablePrefix']
362 );
363 } else {
364 throw new InvalidArgumentException( "No database user provided." );
365 }
366 }
367
380 abstract protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix );
381
426 final public static function factory( $dbType, $p = [], $connect = self::NEW_CONNECTED ) {
427 $class = self::getClass( $dbType, $p['driver'] ?? null );
428
429 if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
430 // Resolve some defaults for b/c
431 $p['host'] = $p['host'] ?? false;
432 $p['user'] = $p['user'] ?? false;
433 $p['password'] = $p['password'] ?? false;
434 $p['dbname'] = $p['dbname'] ?? false;
435 $p['flags'] = $p['flags'] ?? 0;
436 $p['variables'] = $p['variables'] ?? [];
437 $p['tablePrefix'] = $p['tablePrefix'] ?? '';
438 $p['schema'] = $p['schema'] ?? null;
439 $p['cliMode'] = $p['cliMode'] ?? ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' );
440 $p['agent'] = $p['agent'] ?? '';
441 if ( !isset( $p['connLogger'] ) ) {
442 $p['connLogger'] = new NullLogger();
443 }
444 if ( !isset( $p['queryLogger'] ) ) {
445 $p['queryLogger'] = new NullLogger();
446 }
447 $p['profiler'] = $p['profiler'] ?? null;
448 if ( !isset( $p['trxProfiler'] ) ) {
449 $p['trxProfiler'] = new TransactionProfiler();
450 }
451 if ( !isset( $p['errorLogger'] ) ) {
452 $p['errorLogger'] = function ( Exception $e ) {
453 trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
454 };
455 }
456 if ( !isset( $p['deprecationLogger'] ) ) {
457 $p['deprecationLogger'] = function ( $msg ) {
458 trigger_error( $msg, E_USER_DEPRECATED );
459 };
460 }
461
463 $conn = new $class( $p );
464 if ( $connect == self::NEW_CONNECTED ) {
465 $conn->initConnection();
466 }
467 } else {
468 $conn = null;
469 }
470
471 return $conn;
472 }
473
481 final public static function attributesFromType( $dbType, $driver = null ) {
482 static $defaults = [ self::ATTR_DB_LEVEL_LOCKING => false ];
483
484 $class = self::getClass( $dbType, $driver );
485
486 return call_user_func( [ $class, 'getAttributes' ] ) + $defaults;
487 }
488
495 private static function getClass( $dbType, $driver = null ) {
496 // For database types with built-in support, the below maps type to IDatabase
497 // implementations. For types with multipe driver implementations (PHP extensions),
498 // an array can be used, keyed by extension name. In case of an array, the
499 // optional 'driver' parameter can be used to force a specific driver. Otherwise,
500 // we auto-detect the first available driver. For types without built-in support,
501 // an class named "Database<Type>" us used, eg. DatabaseFoo for type 'foo'.
502 static $builtinTypes = [
503 'mssql' => DatabaseMssql::class,
504 'mysql' => [ 'mysqli' => DatabaseMysqli::class ],
505 'sqlite' => DatabaseSqlite::class,
506 'postgres' => DatabasePostgres::class,
507 ];
508
509 $dbType = strtolower( $dbType );
510 $class = false;
511
512 if ( isset( $builtinTypes[$dbType] ) ) {
513 $possibleDrivers = $builtinTypes[$dbType];
514 if ( is_string( $possibleDrivers ) ) {
515 $class = $possibleDrivers;
516 } else {
517 if ( (string)$driver !== '' ) {
518 if ( !isset( $possibleDrivers[$driver] ) ) {
519 throw new InvalidArgumentException( __METHOD__ .
520 " type '$dbType' does not support driver '{$driver}'" );
521 } else {
522 $class = $possibleDrivers[$driver];
523 }
524 } else {
525 foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
526 if ( extension_loaded( $posDriver ) ) {
527 $class = $possibleClass;
528 break;
529 }
530 }
531 }
532 }
533 } else {
534 $class = 'Database' . ucfirst( $dbType );
535 }
536
537 if ( $class === false ) {
538 throw new InvalidArgumentException( __METHOD__ .
539 " no viable database extension found for type '$dbType'" );
540 }
541
542 return $class;
543 }
544
549 protected static function getAttributes() {
550 return [];
551 }
552
560 public function setLogger( LoggerInterface $logger ) {
561 $this->queryLogger = $logger;
562 }
563
564 public function getServerInfo() {
565 return $this->getServerVersion();
566 }
567
568 public function bufferResults( $buffer = null ) {
569 $res = !$this->getFlag( self::DBO_NOBUFFER );
570 if ( $buffer !== null ) {
571 $buffer
572 ? $this->clearFlag( self::DBO_NOBUFFER )
573 : $this->setFlag( self::DBO_NOBUFFER );
574 }
575
576 return $res;
577 }
578
579 public function trxLevel() {
580 return $this->trxLevel;
581 }
582
583 public function trxTimestamp() {
584 return $this->trxLevel ? $this->trxTimestamp : null;
585 }
586
591 public function trxStatus() {
592 return $this->trxStatus;
593 }
594
595 public function tablePrefix( $prefix = null ) {
596 $old = $this->currentDomain->getTablePrefix();
597 if ( $prefix !== null ) {
598 $this->currentDomain = new DatabaseDomain(
599 $this->currentDomain->getDatabase(),
600 $this->currentDomain->getSchema(),
601 $prefix
602 );
603 }
604
605 return $old;
606 }
607
608 public function dbSchema( $schema = null ) {
609 $old = $this->currentDomain->getSchema();
610 if ( $schema !== null ) {
611 $this->currentDomain = new DatabaseDomain(
612 $this->currentDomain->getDatabase(),
613 // DatabaseDomain uses null for unspecified schemas
614 strlen( $schema ) ? $schema : null,
615 $this->currentDomain->getTablePrefix()
616 );
617 }
618
619 return (string)$old;
620 }
621
625 protected function relationSchemaQualifier() {
626 return $this->dbSchema();
627 }
628
629 public function getLBInfo( $name = null ) {
630 if ( is_null( $name ) ) {
631 return $this->lbInfo;
632 } else {
633 if ( array_key_exists( $name, $this->lbInfo ) ) {
634 return $this->lbInfo[$name];
635 } else {
636 return null;
637 }
638 }
639 }
640
641 public function setLBInfo( $name, $value = null ) {
642 if ( is_null( $value ) ) {
643 $this->lbInfo = $name;
644 } else {
645 $this->lbInfo[$name] = $value;
646 }
647 }
648
650 $this->lazyMasterHandle = $conn;
651 }
652
658 protected function getLazyMasterHandle() {
660 }
661
662 public function implicitGroupby() {
663 return true;
664 }
665
666 public function implicitOrderby() {
667 return true;
668 }
669
670 public function lastQuery() {
671 return $this->lastQuery;
672 }
673
674 public function doneWrites() {
675 return (bool)$this->lastWriteTime;
676 }
677
678 public function lastDoneWrites() {
679 return $this->lastWriteTime ?: false;
680 }
681
682 public function writesPending() {
683 return $this->trxLevel && $this->trxDoneWrites;
684 }
685
686 public function writesOrCallbacksPending() {
687 return $this->trxLevel && (
688 $this->trxDoneWrites ||
689 $this->trxIdleCallbacks ||
690 $this->trxPreCommitCallbacks ||
692 );
693 }
694
695 public function preCommitCallbacksPending() {
696 return $this->trxLevel && $this->trxPreCommitCallbacks;
697 }
698
702 final protected function getTransactionRoundId() {
703 // If transaction round participation is enabled, see if one is active
704 if ( $this->getFlag( self::DBO_TRX ) ) {
705 $id = $this->getLBInfo( 'trxRoundId' );
706
707 return is_string( $id ) ? $id : null;
708 }
709
710 return null;
711 }
712
713 public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
714 if ( !$this->trxLevel ) {
715 return false;
716 } elseif ( !$this->trxDoneWrites ) {
717 return 0.0;
718 }
719
720 switch ( $type ) {
721 case self::ESTIMATE_DB_APPLY:
722 return $this->pingAndCalculateLastTrxApplyTime();
723 default: // everything
725 }
726 }
727
732 $this->ping( $rtt );
733
734 $rttAdjTotal = $this->trxWriteAdjQueryCount * $rtt;
735 $applyTime = max( $this->trxWriteAdjDuration - $rttAdjTotal, 0 );
736 // For omitted queries, make them count as something at least
737 $omitted = $this->trxWriteQueryCount - $this->trxWriteAdjQueryCount;
738 $applyTime += self::TINY_WRITE_SEC * $omitted;
739
740 return $applyTime;
741 }
742
743 public function pendingWriteCallers() {
744 return $this->trxLevel ? $this->trxWriteCallers : [];
745 }
746
747 public function pendingWriteRowsAffected() {
749 }
750
760 $fnames = $this->pendingWriteCallers();
761 foreach ( [
762 $this->trxIdleCallbacks,
763 $this->trxPreCommitCallbacks,
764 $this->trxEndCallbacks
765 ] as $callbacks ) {
766 foreach ( $callbacks as $callback ) {
767 $fnames[] = $callback[1];
768 }
769 }
770
771 return $fnames;
772 }
773
777 private function flatAtomicSectionList() {
778 return array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) {
779 return $accum === null ? $v[0] : "$accum, " . $v[0];
780 } );
781 }
782
783 public function isOpen() {
784 return $this->opened;
785 }
786
787 public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
788 if ( ( $flag & self::DBO_IGNORE ) ) {
789 throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
790 }
791
792 if ( $remember === self::REMEMBER_PRIOR ) {
793 array_push( $this->priorFlags, $this->flags );
794 }
795 $this->flags |= $flag;
796 }
797
798 public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
799 if ( ( $flag & self::DBO_IGNORE ) ) {
800 throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
801 }
802
803 if ( $remember === self::REMEMBER_PRIOR ) {
804 array_push( $this->priorFlags, $this->flags );
805 }
806 $this->flags &= ~$flag;
807 }
808
809 public function restoreFlags( $state = self::RESTORE_PRIOR ) {
810 if ( !$this->priorFlags ) {
811 return;
812 }
813
814 if ( $state === self::RESTORE_INITIAL ) {
815 $this->flags = reset( $this->priorFlags );
816 $this->priorFlags = [];
817 } else {
818 $this->flags = array_pop( $this->priorFlags );
819 }
820 }
821
822 public function getFlag( $flag ) {
823 return !!( $this->flags & $flag );
824 }
825
831 public function getProperty( $name ) {
832 return $this->$name;
833 }
834
835 public function getDomainID() {
836 return $this->currentDomain->getId();
837 }
838
839 final public function getWikiID() {
840 return $this->getDomainID();
841 }
842
850 abstract function indexInfo( $table, $index, $fname = __METHOD__ );
851
858 abstract function strencode( $s );
859
863 protected function installErrorHandler() {
864 $this->phpError = false;
865 $this->htmlErrors = ini_set( 'html_errors', '0' );
866 set_error_handler( [ $this, 'connectionErrorLogger' ] );
867 }
868
874 protected function restoreErrorHandler() {
875 restore_error_handler();
876 if ( $this->htmlErrors !== false ) {
877 ini_set( 'html_errors', $this->htmlErrors );
878 }
879
880 return $this->getLastPHPError();
881 }
882
886 protected function getLastPHPError() {
887 if ( $this->phpError ) {
888 $error = preg_replace( '!\[<a.*</a>\]!', '', $this->phpError );
889 $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
890
891 return $error;
892 }
893
894 return false;
895 }
896
904 public function connectionErrorLogger( $errno, $errstr ) {
905 $this->phpError = $errstr;
906 }
907
914 protected function getLogContext( array $extras = [] ) {
915 return array_merge(
916 [
917 'db_server' => $this->server,
918 'db_name' => $this->getDBname(),
919 'db_user' => $this->user,
920 ],
921 $extras
922 );
923 }
924
925 final public function close() {
926 $exception = null; // error to throw after disconnecting
927
928 if ( $this->conn ) {
929 // Roll back any dangling transaction first
930 if ( $this->trxLevel ) {
931 if ( $this->trxAtomicLevels ) {
932 // Cannot let incomplete atomic sections be committed
933 $levels = $this->flatAtomicSectionList();
934 $exception = new DBUnexpectedError(
935 $this,
936 __METHOD__ . ": atomic sections $levels are still open."
937 );
938 } elseif ( $this->trxAutomatic ) {
939 // Only the connection manager can commit non-empty DBO_TRX transactions
940 // (empty ones we can silently roll back)
941 if ( $this->writesOrCallbacksPending() ) {
942 $exception = new DBUnexpectedError(
943 $this,
944 __METHOD__ .
945 ": mass commit/rollback of peer transaction required (DBO_TRX set)."
946 );
947 }
948 } else {
949 // Manual transactions should have been committed or rolled
950 // back, even if empty.
951 $exception = new DBUnexpectedError(
952 $this,
953 __METHOD__ . ": transaction is still open (from {$this->trxFname})."
954 );
955 }
956
957 if ( $this->trxEndCallbacksSuppressed ) {
958 $exception = $exception ?: new DBUnexpectedError(
959 $this,
960 __METHOD__ . ': callbacks are suppressed; cannot properly commit.'
961 );
962 }
963
964 // Rollback the changes and run any callbacks as needed
965 $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
966 }
967
968 // Close the actual connection in the binding handle
969 $closed = $this->closeConnection();
970 $this->conn = false;
971 } else {
972 $closed = true; // already closed; nothing to do
973 }
974
975 $this->opened = false;
976
977 // Throw any unexpected errors after having disconnected
978 if ( $exception instanceof Exception ) {
979 throw $exception;
980 }
981
982 // Sanity check that no callbacks are dangling
983 $fnames = $this->pendingWriteAndCallbackCallers();
984 if ( $fnames ) {
985 throw new RuntimeException(
986 "Transaction callbacks are still pending:\n" . implode( ', ', $fnames )
987 );
988 }
989
990 return $closed;
991 }
992
998 protected function assertOpen() {
999 if ( !$this->isOpen() ) {
1000 throw new DBUnexpectedError( $this, "DB connection was already closed." );
1001 }
1002 }
1003
1009 abstract protected function closeConnection();
1010
1016 public function reportConnectionError( $error = 'Unknown error' ) {
1017 call_user_func( $this->deprecationLogger, 'Use of ' . __METHOD__ . ' is deprecated.' );
1018 throw new DBConnectionError( $this, $this->lastError() ?: $error );
1019 }
1020
1030 abstract protected function doQuery( $sql );
1031
1048 protected function isWriteQuery( $sql ) {
1049 // BEGIN and COMMIT queries are considered read queries here.
1050 // Database backends and drivers (MySQL, MariaDB, php-mysqli) generally
1051 // treat these as write queries, in that their results have "affected rows"
1052 // as meta data as from writes, instead of "num rows" as from reads.
1053 // But, we treat them as read queries because when reading data (from
1054 // either replica or master) we use transactions to enable repeatable-read
1055 // snapshots, which ensures we get consistent results from the same snapshot
1056 // for all queries within a request. Use cases:
1057 // - Treating these as writes would trigger ChronologyProtector (see method doc).
1058 // - We use this method to reject writes to replicas, but we need to allow
1059 // use of transactions on replicas for read snapshots. This fine given
1060 // that transactions by themselves don't make changes, only actual writes
1061 // within the transaction matter, which we still detect.
1062 return !preg_match(
1063 '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SAVEPOINT|RELEASE|SET|SHOW|EXPLAIN|\‍(SELECT)\b/i',
1064 $sql
1065 );
1066 }
1067
1072 protected function getQueryVerb( $sql ) {
1073 return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
1074 }
1075
1089 protected function isTransactableQuery( $sql ) {
1090 return !in_array(
1091 $this->getQueryVerb( $sql ),
1092 [ 'BEGIN', 'ROLLBACK', 'COMMIT', 'SET', 'SHOW', 'CREATE', 'ALTER' ],
1093 true
1094 );
1095 }
1096
1101 protected function registerTempTableOperation( $sql ) {
1102 if ( preg_match(
1103 '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
1104 $sql,
1105 $matches
1106 ) ) {
1107 $this->sessionTempTables[$matches[1]] = 1;
1108
1109 return true;
1110 } elseif ( preg_match(
1111 '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
1112 $sql,
1113 $matches
1114 ) ) {
1115 $isTemp = isset( $this->sessionTempTables[$matches[1]] );
1116 unset( $this->sessionTempTables[$matches[1]] );
1117
1118 return $isTemp;
1119 } elseif ( preg_match(
1120 '/^TRUNCATE\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
1121 $sql,
1122 $matches
1123 ) ) {
1124 return isset( $this->sessionTempTables[$matches[1]] );
1125 } elseif ( preg_match(
1126 '/^(?:INSERT\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+[`"\']?(\w+)[`"\']?/i',
1127 $sql,
1128 $matches
1129 ) ) {
1130 return isset( $this->sessionTempTables[$matches[1]] );
1131 }
1132
1133 return false;
1134 }
1135
1136 public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
1137 $this->assertTransactionStatus( $sql, $fname );
1138
1139 # Avoid fatals if close() was called
1140 $this->assertOpen();
1141
1142 $priorWritesPending = $this->writesOrCallbacksPending();
1143 $this->lastQuery = $sql;
1144
1145 $isWrite = $this->isWriteQuery( $sql );
1146 if ( $isWrite ) {
1147 $isNonTempWrite = !$this->registerTempTableOperation( $sql );
1148 } else {
1149 $isNonTempWrite = false;
1150 }
1151
1152 if ( $isWrite ) {
1153 if ( $this->getLBInfo( 'replica' ) === true ) {
1154 throw new DBError(
1155 $this,
1156 'Write operations are not allowed on replica database connections.'
1157 );
1158 }
1159 # In theory, non-persistent writes are allowed in read-only mode, but due to things
1160 # like https://bugs.mysql.com/bug.php?id=33669 that might not work anyway...
1161 $reason = $this->getReadOnlyReason();
1162 if ( $reason !== false ) {
1163 throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
1164 }
1165 # Set a flag indicating that writes have been done
1166 $this->lastWriteTime = microtime( true );
1167 }
1168
1169 # Add trace comment to the begin of the sql string, right after the operator.
1170 # Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
1171 $commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
1172
1173 # Start implicit transactions that wrap the request if DBO_TRX is enabled
1174 if ( !$this->trxLevel && $this->getFlag( self::DBO_TRX )
1175 && $this->isTransactableQuery( $sql )
1176 ) {
1177 $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
1178 $this->trxAutomatic = true;
1179 }
1180
1181 # Keep track of whether the transaction has write queries pending
1182 if ( $this->trxLevel && !$this->trxDoneWrites && $isWrite ) {
1183 $this->trxDoneWrites = true;
1184 $this->trxProfiler->transactionWritingIn(
1185 $this->server, $this->getDomainID(), $this->trxShortId );
1186 }
1187
1188 if ( $this->getFlag( self::DBO_DEBUG ) ) {
1189 $this->queryLogger->debug( "{$this->getDomainID()} {$commentedSql}" );
1190 }
1191
1192 # Send the query to the server and fetch any corresponding errors
1193 $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
1194 $lastError = $this->lastError();
1195 $lastErrno = $this->lastErrno();
1196
1197 # Try reconnecting if the connection was lost
1198 if ( $ret === false && $this->wasConnectionLoss() ) {
1199 # Check if any meaningful session state was lost
1200 $recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
1201 # Update session state tracking and try to restore the connection
1202 $reconnected = $this->replaceLostConnection( __METHOD__ );
1203 # Silently resend the query to the server if it is safe and possible
1204 if ( $reconnected && $recoverable ) {
1205 $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
1206 $lastError = $this->lastError();
1207 $lastErrno = $this->lastErrno();
1208
1209 if ( $ret === false && $this->wasConnectionLoss() ) {
1210 # Query probably causes disconnects; reconnect and do not re-run it
1211 $this->replaceLostConnection( __METHOD__ );
1212 }
1213 }
1214 }
1215
1216 if ( $ret === false ) {
1217 if ( $this->trxLevel ) {
1218 if ( $this->wasKnownStatementRollbackError() ) {
1219 # We're ignoring an error that caused just the current query to be aborted.
1220 # But log the cause so we can log a deprecation notice if a caller actually
1221 # does ignore it.
1222 $this->trxStatusIgnoredCause = [ $lastError, $lastErrno, $fname ];
1223 } else {
1224 # Either the query was aborted or all queries after BEGIN where aborted.
1225 # In the first case, the only options going forward are (a) ROLLBACK, or
1226 # (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
1227 # option is ROLLBACK, since the snapshots would have been released.
1228 $this->trxStatus = self::STATUS_TRX_ERROR;
1229 $this->trxStatusCause =
1230 $this->makeQueryException( $lastError, $lastErrno, $sql, $fname );
1231 $tempIgnore = false; // cannot recover
1232 $this->trxStatusIgnoredCause = null;
1233 }
1234 }
1235
1236 $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $tempIgnore );
1237 }
1238
1239 return $this->resultObject( $ret );
1240 }
1241
1252 private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) {
1253 $isMaster = !is_null( $this->getLBInfo( 'master' ) );
1254 # generalizeSQL() will probably cut down the query to reasonable
1255 # logging size most of the time. The substr is really just a sanity check.
1256 if ( $isMaster ) {
1257 $queryProf = 'query-m: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
1258 } else {
1259 $queryProf = 'query: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
1260 }
1261
1262 # Include query transaction state
1263 $queryProf .= $this->trxShortId ? " [TRX#{$this->trxShortId}]" : "";
1264
1265 $startTime = microtime( true );
1266 if ( $this->profiler ) {
1267 $this->profiler->profileIn( $queryProf );
1268 }
1269 $this->affectedRowCount = null;
1270 $ret = $this->doQuery( $commentedSql );
1271 $this->affectedRowCount = $this->affectedRows();
1272 if ( $this->profiler ) {
1273 $this->profiler->profileOut( $queryProf );
1274 }
1275 $queryRuntime = max( microtime( true ) - $startTime, 0.0 );
1276
1277 unset( $queryProfSection ); // profile out (if set)
1278
1279 if ( $ret !== false ) {
1280 $this->lastPing = $startTime;
1281 if ( $isWrite && $this->trxLevel ) {
1282 $this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() );
1283 $this->trxWriteCallers[] = $fname;
1284 }
1285 }
1286
1287 if ( $sql === self::PING_QUERY ) {
1288 $this->rttEstimate = $queryRuntime;
1289 }
1290
1291 $this->trxProfiler->recordQueryCompletion(
1292 $queryProf,
1293 $startTime,
1294 $isWrite,
1295 $isWrite ? $this->affectedRows() : $this->numRows( $ret )
1296 );
1297 $this->queryLogger->debug( $sql, [
1298 'method' => $fname,
1299 'master' => $isMaster,
1300 'runtime' => $queryRuntime,
1301 ] );
1302
1303 return $ret;
1304 }
1305
1318 private function updateTrxWriteQueryTime( $sql, $runtime, $affected ) {
1319 // Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
1320 $indicativeOfReplicaRuntime = true;
1321 if ( $runtime > self::SLOW_WRITE_SEC ) {
1322 $verb = $this->getQueryVerb( $sql );
1323 // insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
1324 if ( $verb === 'INSERT' ) {
1325 $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS;
1326 } elseif ( $verb === 'REPLACE' ) {
1327 $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS / 2;
1328 }
1329 }
1330
1331 $this->trxWriteDuration += $runtime;
1332 $this->trxWriteQueryCount += 1;
1333 $this->trxWriteAffectedRows += $affected;
1334 if ( $indicativeOfReplicaRuntime ) {
1335 $this->trxWriteAdjDuration += $runtime;
1336 $this->trxWriteAdjQueryCount += 1;
1337 }
1338 }
1339
1345 private function assertTransactionStatus( $sql, $fname ) {
1346 if ( $this->getQueryVerb( $sql ) === 'ROLLBACK' ) { // transaction/savepoint
1347 return;
1348 }
1349
1350 if ( $this->trxStatus < self::STATUS_TRX_OK ) {
1351 throw new DBTransactionStateError(
1352 $this,
1353 "Cannot execute query from $fname while transaction status is ERROR.",
1354 [],
1355 $this->trxStatusCause
1356 );
1357 } elseif ( $this->trxStatus === self::STATUS_TRX_OK && $this->trxStatusIgnoredCause ) {
1358 list( $iLastError, $iLastErrno, $iFname ) = $this->trxStatusIgnoredCause;
1359 call_user_func( $this->deprecationLogger,
1360 "Caller from $fname ignored an error originally raised from $iFname: " .
1361 "[$iLastErrno] $iLastError"
1362 );
1363 $this->trxStatusIgnoredCause = null;
1364 }
1365 }
1366
1367 public function assertNoOpenTransactions() {
1368 if ( $this->explicitTrxActive() ) {
1369 throw new DBTransactionError(
1370 $this,
1371 "Explicit transaction still active. A caller may have caught an error. "
1372 . "Open transactions: " . $this->flatAtomicSectionList()
1373 );
1374 }
1375 }
1376
1387 private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
1388 # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
1389 # Dropped connections also mean that named locks are automatically released.
1390 # Only allow error suppression in autocommit mode or when the lost transaction
1391 # didn't matter anyway (aside from DBO_TRX snapshot loss).
1392 if ( $this->namedLocksHeld ) {
1393 return false; // possible critical section violation
1394 } elseif ( $this->sessionTempTables ) {
1395 return false; // tables might be queried latter
1396 } elseif ( $sql === 'COMMIT' ) {
1397 return !$priorWritesPending; // nothing written anyway? (T127428)
1398 } elseif ( $sql === 'ROLLBACK' ) {
1399 return true; // transaction lost...which is also what was requested :)
1400 } elseif ( $this->explicitTrxActive() ) {
1401 return false; // don't drop atomocity and explicit snapshots
1402 } elseif ( $priorWritesPending ) {
1403 return false; // prior writes lost from implicit transaction
1404 }
1405
1406 return true;
1407 }
1408
1412 private function handleSessionLoss() {
1413 // Clean up tracking of session-level things...
1414 // https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html
1415 // https://www.postgresql.org/docs/9.2/static/sql-createtable.html (ignoring ON COMMIT)
1416 $this->sessionTempTables = [];
1417 // https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
1418 // https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
1419 $this->namedLocksHeld = [];
1420 // Session loss implies transaction loss
1421 $this->handleTransactionLoss();
1422 }
1423
1427 private function handleTransactionLoss() {
1428 $this->trxLevel = 0;
1429 $this->trxAtomicCounter = 0;
1430 $this->trxIdleCallbacks = []; // T67263; transaction already lost
1431 $this->trxPreCommitCallbacks = []; // T67263; transaction already lost
1432 try {
1433 // Handle callbacks in trxEndCallbacks, e.g. onTransactionResolution().
1434 // If callback suppression is set then the array will remain unhandled.
1435 $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
1436 } catch ( Exception $ex ) {
1437 // Already logged; move on...
1438 }
1439 try {
1440 // Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener()
1441 $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
1442 } catch ( Exception $ex ) {
1443 // Already logged; move on...
1444 }
1445 }
1446
1457 protected function wasQueryTimeout( $error, $errno ) {
1458 return false;
1459 }
1460
1472 public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
1473 if ( $tempIgnore ) {
1474 $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
1475 } else {
1476 $exception = $this->makeQueryException( $error, $errno, $sql, $fname );
1477
1478 throw $exception;
1479 }
1480 }
1481
1489 private function makeQueryException( $error, $errno, $sql, $fname ) {
1490 $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
1491 $this->queryLogger->error(
1492 "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
1493 $this->getLogContext( [
1494 'method' => __METHOD__,
1495 'errno' => $errno,
1496 'error' => $error,
1497 'sql1line' => $sql1line,
1498 'fname' => $fname,
1499 ] )
1500 );
1501 $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
1502 $wasQueryTimeout = $this->wasQueryTimeout( $error, $errno );
1503 if ( $wasQueryTimeout ) {
1504 $e = new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
1505 } else {
1506 $e = new DBQueryError( $this, $error, $errno, $sql, $fname );
1507 }
1508
1509 return $e;
1510 }
1511
1512 public function freeResult( $res ) {
1513 }
1514
1515 public function selectField(
1516 $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1517 ) {
1518 if ( $var === '*' ) { // sanity
1519 throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1520 }
1521
1522 if ( !is_array( $options ) ) {
1523 $options = [ $options ];
1524 }
1525
1526 $options['LIMIT'] = 1;
1527
1528 $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1529 if ( $res === false || !$this->numRows( $res ) ) {
1530 return false;
1531 }
1532
1533 $row = $this->fetchRow( $res );
1534
1535 if ( $row !== false ) {
1536 return reset( $row );
1537 } else {
1538 return false;
1539 }
1540 }
1541
1542 public function selectFieldValues(
1543 $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1544 ) {
1545 if ( $var === '*' ) { // sanity
1546 throw new DBUnexpectedError( $this, "Cannot use a * field" );
1547 } elseif ( !is_string( $var ) ) { // sanity
1548 throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1549 }
1550
1551 if ( !is_array( $options ) ) {
1552 $options = [ $options ];
1553 }
1554
1555 $res = $this->select( $table, [ 'value' => $var ], $cond, $fname, $options, $join_conds );
1556 if ( $res === false ) {
1557 return false;
1558 }
1559
1560 $values = [];
1561 foreach ( $res as $row ) {
1562 $values[] = $row->value;
1563 }
1564
1565 return $values;
1566 }
1567
1577 protected function makeSelectOptions( $options ) {
1578 $preLimitTail = $postLimitTail = '';
1579 $startOpts = '';
1580
1581 $noKeyOptions = [];
1582
1583 foreach ( $options as $key => $option ) {
1584 if ( is_numeric( $key ) ) {
1585 $noKeyOptions[$option] = true;
1586 }
1587 }
1588
1589 $preLimitTail .= $this->makeGroupByWithHaving( $options );
1590
1591 $preLimitTail .= $this->makeOrderBy( $options );
1592
1593 if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1594 $postLimitTail .= ' FOR UPDATE';
1595 }
1596
1597 if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1598 $postLimitTail .= ' LOCK IN SHARE MODE';
1599 }
1600
1601 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1602 $startOpts .= 'DISTINCT';
1603 }
1604
1605 # Various MySQL extensions
1606 if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1607 $startOpts .= ' /*! STRAIGHT_JOIN */';
1608 }
1609
1610 if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
1611 $startOpts .= ' HIGH_PRIORITY';
1612 }
1613
1614 if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1615 $startOpts .= ' SQL_BIG_RESULT';
1616 }
1617
1618 if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1619 $startOpts .= ' SQL_BUFFER_RESULT';
1620 }
1621
1622 if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1623 $startOpts .= ' SQL_SMALL_RESULT';
1624 }
1625
1626 if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1627 $startOpts .= ' SQL_CALC_FOUND_ROWS';
1628 }
1629
1630 if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
1631 $startOpts .= ' SQL_CACHE';
1632 }
1633
1634 if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
1635 $startOpts .= ' SQL_NO_CACHE';
1636 }
1637
1638 if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1639 $useIndex = $this->useIndexClause( $options['USE INDEX'] );
1640 } else {
1641 $useIndex = '';
1642 }
1643 if ( isset( $options['IGNORE INDEX'] ) && is_string( $options['IGNORE INDEX'] ) ) {
1644 $ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
1645 } else {
1646 $ignoreIndex = '';
1647 }
1648
1649 return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1650 }
1651
1660 protected function makeGroupByWithHaving( $options ) {
1661 $sql = '';
1662 if ( isset( $options['GROUP BY'] ) ) {
1663 $gb = is_array( $options['GROUP BY'] )
1664 ? implode( ',', $options['GROUP BY'] )
1665 : $options['GROUP BY'];
1666 $sql .= ' GROUP BY ' . $gb;
1667 }
1668 if ( isset( $options['HAVING'] ) ) {
1669 $having = is_array( $options['HAVING'] )
1670 ? $this->makeList( $options['HAVING'], self::LIST_AND )
1671 : $options['HAVING'];
1672 $sql .= ' HAVING ' . $having;
1673 }
1674
1675 return $sql;
1676 }
1677
1686 protected function makeOrderBy( $options ) {
1687 if ( isset( $options['ORDER BY'] ) ) {
1688 $ob = is_array( $options['ORDER BY'] )
1689 ? implode( ',', $options['ORDER BY'] )
1690 : $options['ORDER BY'];
1691
1692 return ' ORDER BY ' . $ob;
1693 }
1694
1695 return '';
1696 }
1697
1698 public function select(
1699 $table, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1700 ) {
1701 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1702
1703 return $this->query( $sql, $fname );
1704 }
1705
1706 public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1707 $options = [], $join_conds = []
1708 ) {
1709 if ( is_array( $vars ) ) {
1710 $fields = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1711 } else {
1712 $fields = $vars;
1713 }
1714
1715 $options = (array)$options;
1716 $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1717 ? $options['USE INDEX']
1718 : [];
1719 $ignoreIndexes = (
1720 isset( $options['IGNORE INDEX'] ) &&
1721 is_array( $options['IGNORE INDEX'] )
1722 )
1723 ? $options['IGNORE INDEX']
1724 : [];
1725
1726 if (
1727 $this->selectOptionsIncludeLocking( $options ) &&
1728 $this->selectFieldsOrOptionsAggregate( $vars, $options )
1729 ) {
1730 // Some DB types (postgres/oracle) disallow FOR UPDATE with aggregate
1731 // functions. Discourage use of such queries to encourage compatibility.
1732 call_user_func(
1733 $this->deprecationLogger,
1734 __METHOD__ . ": aggregation used with a locking SELECT ($fname)."
1735 );
1736 }
1737
1738 if ( is_array( $table ) ) {
1739 $from = ' FROM ' .
1740 $this->tableNamesWithIndexClauseOrJOIN(
1741 $table, $useIndexes, $ignoreIndexes, $join_conds );
1742 } elseif ( $table != '' ) {
1743 $from = ' FROM ' .
1744 $this->tableNamesWithIndexClauseOrJOIN(
1745 [ $table ], $useIndexes, $ignoreIndexes, [] );
1746 } else {
1747 $from = '';
1748 }
1749
1750 list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
1751 $this->makeSelectOptions( $options );
1752
1753 if ( is_array( $conds ) ) {
1754 $conds = $this->makeList( $conds, self::LIST_AND );
1755 }
1756
1757 if ( $conds === null || $conds === false ) {
1758 $this->queryLogger->warning(
1759 __METHOD__
1760 . ' called from '
1761 . $fname
1762 . ' with incorrect parameters: $conds must be a string or an array'
1763 );
1764 $conds = '';
1765 }
1766
1767 if ( $conds === '' || $conds === '*' ) {
1768 $sql = "SELECT $startOpts $fields $from $useIndex $ignoreIndex $preLimitTail";
1769 } elseif ( is_string( $conds ) ) {
1770 $sql = "SELECT $startOpts $fields $from $useIndex $ignoreIndex " .
1771 "WHERE $conds $preLimitTail";
1772 } else {
1773 throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
1774 }
1775
1776 if ( isset( $options['LIMIT'] ) ) {
1777 $sql = $this->limitResult( $sql, $options['LIMIT'],
1778 $options['OFFSET'] ?? false );
1779 }
1780 $sql = "$sql $postLimitTail";
1781
1782 if ( isset( $options['EXPLAIN'] ) ) {
1783 $sql = 'EXPLAIN ' . $sql;
1784 }
1785
1786 return $sql;
1787 }
1788
1789 public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1790 $options = [], $join_conds = []
1791 ) {
1792 $options = (array)$options;
1793 $options['LIMIT'] = 1;
1794 $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1795
1796 if ( $res === false ) {
1797 return false;
1798 }
1799
1800 if ( !$this->numRows( $res ) ) {
1801 return false;
1802 }
1803
1804 $obj = $this->fetchObject( $res );
1805
1806 return $obj;
1807 }
1808
1809 public function estimateRowCount(
1810 $table, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1811 ) {
1812 $conds = $this->normalizeConditions( $conds, $fname );
1813 $column = $this->extractSingleFieldFromList( $var );
1814 if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
1815 $conds[] = "$column IS NOT NULL";
1816 }
1817
1818 $res = $this->select(
1819 $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options, $join_conds
1820 );
1821 $row = $res ? $this->fetchRow( $res ) : [];
1822
1823 return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
1824 }
1825
1826 public function selectRowCount(
1827 $tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1828 ) {
1829 $conds = $this->normalizeConditions( $conds, $fname );
1830 $column = $this->extractSingleFieldFromList( $var );
1831 if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
1832 $conds[] = "$column IS NOT NULL";
1833 }
1834
1835 $res = $this->select(
1836 [
1837 'tmp_count' => $this->buildSelectSubquery(
1838 $tables,
1839 '1',
1840 $conds,
1841 $fname,
1842 $options,
1843 $join_conds
1844 )
1845 ],
1846 [ 'rowcount' => 'COUNT(*)' ],
1847 [],
1848 $fname
1849 );
1850 $row = $res ? $this->fetchRow( $res ) : [];
1851
1852 return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
1853 }
1854
1859 private function selectOptionsIncludeLocking( $options ) {
1860 $options = (array)$options;
1861 foreach ( [ 'FOR UPDATE', 'LOCK IN SHARE MODE' ] as $lock ) {
1862 if ( in_array( $lock, $options, true ) ) {
1863 return true;
1864 }
1865 }
1866
1867 return false;
1868 }
1869
1875 private function selectFieldsOrOptionsAggregate( $fields, $options ) {
1876 foreach ( (array)$options as $key => $value ) {
1877 if ( is_string( $key ) ) {
1878 if ( preg_match( '/^(?:GROUP BY|HAVING)$/i', $key ) ) {
1879 return true;
1880 }
1881 } elseif ( is_string( $value ) ) {
1882 if ( preg_match( '/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
1883 return true;
1884 }
1885 }
1886 }
1887
1888 $regex = '/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
1889 foreach ( (array)$fields as $field ) {
1890 if ( is_string( $field ) && preg_match( $regex, $field ) ) {
1891 return true;
1892 }
1893 }
1894
1895 return false;
1896 }
1897
1903 final protected function normalizeConditions( $conds, $fname ) {
1904 if ( $conds === null || $conds === false ) {
1905 $this->queryLogger->warning(
1906 __METHOD__
1907 . ' called from '
1908 . $fname
1909 . ' with incorrect parameters: $conds must be a string or an array'
1910 );
1911 $conds = '';
1912 }
1913
1914 if ( !is_array( $conds ) ) {
1915 $conds = ( $conds === '' ) ? [] : [ $conds ];
1916 }
1917
1918 return $conds;
1919 }
1920
1926 final protected function extractSingleFieldFromList( $var ) {
1927 if ( is_array( $var ) ) {
1928 if ( !$var ) {
1929 $column = null;
1930 } elseif ( count( $var ) == 1 ) {
1931 $column = $var[0] ?? reset( $var );
1932 } else {
1933 throw new DBUnexpectedError( $this, __METHOD__ . ': got multiple columns.' );
1934 }
1935 } else {
1936 $column = $var;
1937 }
1938
1939 return $column;
1940 }
1941
1942 public function lockForUpdate(
1943 $table, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1944 ) {
1945 if ( !$this->trxLevel && !$this->getFlag( self::DBO_TRX ) ) {
1946 throw new DBUnexpectedError(
1947 $this,
1948 __METHOD__ . ': no transaction is active nor is DBO_TRX set'
1949 );
1950 }
1951
1952 $options = (array)$options;
1953 $options[] = 'FOR UPDATE';
1954
1955 return $this->selectRowCount( $table, '*', $conds, $fname, $options, $join_conds );
1956 }
1957
1966 protected static function generalizeSQL( $sql ) {
1967 # This does the same as the regexp below would do, but in such a way
1968 # as to avoid crashing php on some large strings.
1969 # $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
1970
1971 $sql = str_replace( "\\\\", '', $sql );
1972 $sql = str_replace( "\\'", '', $sql );
1973 $sql = str_replace( "\\\"", '', $sql );
1974 $sql = preg_replace( "/'.*'/s", "'X'", $sql );
1975 $sql = preg_replace( '/".*"/s', "'X'", $sql );
1976
1977 # All newlines, tabs, etc replaced by single space
1978 $sql = preg_replace( '/\s+/', ' ', $sql );
1979
1980 # All numbers => N,
1981 # except the ones surrounded by characters, e.g. l10n
1982 $sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
1983 $sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
1984
1985 return $sql;
1986 }
1987
1988 public function fieldExists( $table, $field, $fname = __METHOD__ ) {
1989 $info = $this->fieldInfo( $table, $field );
1990
1991 return (bool)$info;
1992 }
1993
1994 public function indexExists( $table, $index, $fname = __METHOD__ ) {
1995 if ( !$this->tableExists( $table ) ) {
1996 return null;
1997 }
1998
1999 $info = $this->indexInfo( $table, $index, $fname );
2000 if ( is_null( $info ) ) {
2001 return null;
2002 } else {
2003 return $info !== false;
2004 }
2005 }
2006
2007 abstract public function tableExists( $table, $fname = __METHOD__ );
2008
2009 public function indexUnique( $table, $index ) {
2010 $indexInfo = $this->indexInfo( $table, $index );
2011
2012 if ( !$indexInfo ) {
2013 return null;
2014 }
2015
2016 return !$indexInfo[0]->Non_unique;
2017 }
2018
2025 protected function makeInsertOptions( $options ) {
2026 return implode( ' ', $options );
2027 }
2028
2029 public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
2030 # No rows to insert, easy just return now
2031 if ( !count( $a ) ) {
2032 return true;
2033 }
2034
2035 $table = $this->tableName( $table );
2036
2037 if ( !is_array( $options ) ) {
2038 $options = [ $options ];
2039 }
2040
2041 $fh = null;
2042 if ( isset( $options['fileHandle'] ) ) {
2043 $fh = $options['fileHandle'];
2044 }
2046
2047 if ( isset( $a[0] ) && is_array( $a[0] ) ) {
2048 $multi = true;
2049 $keys = array_keys( $a[0] );
2050 } else {
2051 $multi = false;
2052 $keys = array_keys( $a );
2053 }
2054
2055 $sql = 'INSERT ' . $options .
2056 " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
2057
2058 if ( $multi ) {
2059 $first = true;
2060 foreach ( $a as $row ) {
2061 if ( $first ) {
2062 $first = false;
2063 } else {
2064 $sql .= ',';
2065 }
2066 $sql .= '(' . $this->makeList( $row ) . ')';
2067 }
2068 } else {
2069 $sql .= '(' . $this->makeList( $a ) . ')';
2070 }
2071
2072 if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
2073 return false;
2074 } elseif ( $fh !== null ) {
2075 return true;
2076 }
2077
2078 return (bool)$this->query( $sql, $fname );
2079 }
2080
2087 protected function makeUpdateOptionsArray( $options ) {
2088 if ( !is_array( $options ) ) {
2089 $options = [ $options ];
2090 }
2091
2092 $opts = [];
2093
2094 if ( in_array( 'IGNORE', $options ) ) {
2095 $opts[] = 'IGNORE';
2096 }
2097
2098 return $opts;
2099 }
2100
2107 protected function makeUpdateOptions( $options ) {
2108 $opts = $this->makeUpdateOptionsArray( $options );
2109
2110 return implode( ' ', $opts );
2111 }
2112
2113 public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
2114 $table = $this->tableName( $table );
2115 $opts = $this->makeUpdateOptions( $options );
2116 $sql = "UPDATE $opts $table SET " . $this->makeList( $values, self::LIST_SET );
2117
2118 if ( $conds !== [] && $conds !== '*' ) {
2119 $sql .= " WHERE " . $this->makeList( $conds, self::LIST_AND );
2120 }
2121
2122 return (bool)$this->query( $sql, $fname );
2123 }
2124
2125 public function makeList( $a, $mode = self::LIST_COMMA ) {
2126 if ( !is_array( $a ) ) {
2127 throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
2128 }
2129
2130 $first = true;
2131 $list = '';
2132
2133 foreach ( $a as $field => $value ) {
2134 if ( !$first ) {
2135 if ( $mode == self::LIST_AND ) {
2136 $list .= ' AND ';
2137 } elseif ( $mode == self::LIST_OR ) {
2138 $list .= ' OR ';
2139 } else {
2140 $list .= ',';
2141 }
2142 } else {
2143 $first = false;
2144 }
2145
2146 if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
2147 $list .= "($value)";
2148 } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
2149 $list .= "$value";
2150 } elseif (
2151 ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
2152 ) {
2153 // Remove null from array to be handled separately if found
2154 $includeNull = false;
2155 foreach ( array_keys( $value, null, true ) as $nullKey ) {
2156 $includeNull = true;
2157 unset( $value[$nullKey] );
2158 }
2159 if ( count( $value ) == 0 && !$includeNull ) {
2160 throw new InvalidArgumentException(
2161 __METHOD__ . ": empty input for field $field" );
2162 } elseif ( count( $value ) == 0 ) {
2163 // only check if $field is null
2164 $list .= "$field IS NULL";
2165 } else {
2166 // IN clause contains at least one valid element
2167 if ( $includeNull ) {
2168 // Group subconditions to ensure correct precedence
2169 $list .= '(';
2170 }
2171 if ( count( $value ) == 1 ) {
2172 // Special-case single values, as IN isn't terribly efficient
2173 // Don't necessarily assume the single key is 0; we don't
2174 // enforce linear numeric ordering on other arrays here.
2175 $value = array_values( $value )[0];
2176 $list .= $field . " = " . $this->addQuotes( $value );
2177 } else {
2178 $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
2179 }
2180 // if null present in array, append IS NULL
2181 if ( $includeNull ) {
2182 $list .= " OR $field IS NULL)";
2183 }
2184 }
2185 } elseif ( $value === null ) {
2186 if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
2187 $list .= "$field IS ";
2188 } elseif ( $mode == self::LIST_SET ) {
2189 $list .= "$field = ";
2190 }
2191 $list .= 'NULL';
2192 } else {
2193 if (
2194 $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
2195 ) {
2196 $list .= "$field = ";
2197 }
2198 $list .= $mode == self::LIST_NAMES ? $value : $this->addQuotes( $value );
2199 }
2200 }
2201
2202 return $list;
2203 }
2204
2205 public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
2206 $conds = [];
2207
2208 foreach ( $data as $base => $sub ) {
2209 if ( count( $sub ) ) {
2210 $conds[] = $this->makeList(
2211 [ $baseKey => $base, $subKey => array_keys( $sub ) ],
2212 self::LIST_AND );
2213 }
2214 }
2215
2216 if ( $conds ) {
2217 return $this->makeList( $conds, self::LIST_OR );
2218 } else {
2219 // Nothing to search for...
2220 return false;
2221 }
2222 }
2223
2224 public function aggregateValue( $valuedata, $valuename = 'value' ) {
2225 return $valuename;
2226 }
2227
2228 public function bitNot( $field ) {
2229 return "(~$field)";
2230 }
2231
2232 public function bitAnd( $fieldLeft, $fieldRight ) {
2233 return "($fieldLeft & $fieldRight)";
2234 }
2235
2236 public function bitOr( $fieldLeft, $fieldRight ) {
2237 return "($fieldLeft | $fieldRight)";
2238 }
2239
2240 public function buildConcat( $stringList ) {
2241 return 'CONCAT(' . implode( ',', $stringList ) . ')';
2242 }
2243
2244 public function buildGroupConcatField(
2245 $delim, $table, $field, $conds = '', $join_conds = []
2246 ) {
2247 $fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
2248
2249 return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
2250 }
2251
2252 public function buildSubstring( $input, $startPosition, $length = null ) {
2253 $this->assertBuildSubstringParams( $startPosition, $length );
2254 $functionBody = "$input FROM $startPosition";
2255 if ( $length !== null ) {
2256 $functionBody .= " FOR $length";
2257 }
2258 return 'SUBSTRING(' . $functionBody . ')';
2259 }
2260
2273 protected function assertBuildSubstringParams( $startPosition, $length ) {
2274 if ( !is_int( $startPosition ) || $startPosition <= 0 ) {
2275 throw new InvalidArgumentException(
2276 '$startPosition must be a positive integer'
2277 );
2278 }
2279 if ( !( is_int( $length ) && $length >= 0 || $length === null ) ) {
2280 throw new InvalidArgumentException(
2281 '$length must be null or an integer greater than or equal to 0'
2282 );
2283 }
2284 }
2285
2286 public function buildStringCast( $field ) {
2287 return $field;
2288 }
2289
2290 public function buildIntegerCast( $field ) {
2291 return 'CAST( ' . $field . ' AS INTEGER )';
2292 }
2293
2294 public function buildSelectSubquery(
2295 $table, $vars, $conds = '', $fname = __METHOD__,
2296 $options = [], $join_conds = []
2297 ) {
2298 return new Subquery(
2299 $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
2300 );
2301 }
2302
2303 public function databasesAreIndependent() {
2304 return false;
2305 }
2306
2307 final public function selectDB( $db ) {
2308 $this->selectDomain( new DatabaseDomain(
2309 $db,
2310 $this->currentDomain->getSchema(),
2311 $this->currentDomain->getTablePrefix()
2312 ) );
2313
2314 return true;
2315 }
2316
2317 final public function selectDomain( $domain ) {
2318 $this->doSelectDomain( DatabaseDomain::newFromId( $domain ) );
2319 }
2320
2321 protected function doSelectDomain( DatabaseDomain $domain ) {
2322 $this->currentDomain = $domain;
2323 }
2324
2325 public function getDBname() {
2326 return $this->currentDomain->getDatabase();
2327 }
2328
2329 public function getServer() {
2330 return $this->server;
2331 }
2332
2333 public function tableName( $name, $format = 'quoted' ) {
2334 if ( $name instanceof Subquery ) {
2335 throw new DBUnexpectedError(
2336 $this,
2337 __METHOD__ . ': got Subquery instance when expecting a string.'
2338 );
2339 }
2340
2341 # Skip the entire process when we have a string quoted on both ends.
2342 # Note that we check the end so that we will still quote any use of
2343 # use of `database`.table. But won't break things if someone wants
2344 # to query a database table with a dot in the name.
2345 if ( $this->isQuotedIdentifier( $name ) ) {
2346 return $name;
2347 }
2348
2349 # Lets test for any bits of text that should never show up in a table
2350 # name. Basically anything like JOIN or ON which are actually part of
2351 # SQL queries, but may end up inside of the table value to combine
2352 # sql. Such as how the API is doing.
2353 # Note that we use a whitespace test rather than a \b test to avoid
2354 # any remote case where a word like on may be inside of a table name
2355 # surrounded by symbols which may be considered word breaks.
2356 if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
2357 $this->queryLogger->warning(
2358 __METHOD__ . ": use of subqueries is not supported this way.",
2359 [ 'exception' => new RuntimeException() ]
2360 );
2361
2362 return $name;
2363 }
2364
2365 # Split database and table into proper variables.
2366 list( $database, $schema, $prefix, $table ) = $this->qualifiedTableComponents( $name );
2367
2368 # Quote $table and apply the prefix if not quoted.
2369 # $tableName might be empty if this is called from Database::replaceVars()
2370 $tableName = "{$prefix}{$table}";
2371 if ( $format === 'quoted'
2372 && !$this->isQuotedIdentifier( $tableName )
2373 && $tableName !== ''
2374 ) {
2375 $tableName = $this->addIdentifierQuotes( $tableName );
2376 }
2377
2378 # Quote $schema and $database and merge them with the table name if needed
2379 $tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
2380 $tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
2381
2382 return $tableName;
2383 }
2384
2391 protected function qualifiedTableComponents( $name ) {
2392 # We reverse the explode so that database.table and table both output the correct table.
2393 $dbDetails = explode( '.', $name, 3 );
2394 if ( count( $dbDetails ) == 3 ) {
2395 list( $database, $schema, $table ) = $dbDetails;
2396 # We don't want any prefix added in this case
2397 $prefix = '';
2398 } elseif ( count( $dbDetails ) == 2 ) {
2399 list( $database, $table ) = $dbDetails;
2400 # We don't want any prefix added in this case
2401 $prefix = '';
2402 # In dbs that support it, $database may actually be the schema
2403 # but that doesn't affect any of the functionality here
2404 $schema = '';
2405 } else {
2406 list( $table ) = $dbDetails;
2407 if ( isset( $this->tableAliases[$table] ) ) {
2408 $database = $this->tableAliases[$table]['dbname'];
2409 $schema = is_string( $this->tableAliases[$table]['schema'] )
2410 ? $this->tableAliases[$table]['schema']
2411 : $this->relationSchemaQualifier();
2412 $prefix = is_string( $this->tableAliases[$table]['prefix'] )
2413 ? $this->tableAliases[$table]['prefix']
2414 : $this->tablePrefix();
2415 } else {
2416 $database = '';
2417 $schema = $this->relationSchemaQualifier(); # Default schema
2418 $prefix = $this->tablePrefix(); # Default prefix
2419 }
2420 }
2421
2422 return [ $database, $schema, $prefix, $table ];
2423 }
2424
2431 private function prependDatabaseOrSchema( $namespace, $relation, $format ) {
2432 if ( strlen( $namespace ) ) {
2433 if ( $format === 'quoted' && !$this->isQuotedIdentifier( $namespace ) ) {
2434 $namespace = $this->addIdentifierQuotes( $namespace );
2435 }
2436 $relation = $namespace . '.' . $relation;
2437 }
2438
2439 return $relation;
2440 }
2441
2442 public function tableNames() {
2443 $inArray = func_get_args();
2444 $retVal = [];
2445
2446 foreach ( $inArray as $name ) {
2447 $retVal[$name] = $this->tableName( $name );
2448 }
2449
2450 return $retVal;
2451 }
2452
2453 public function tableNamesN() {
2454 $inArray = func_get_args();
2455 $retVal = [];
2456
2457 foreach ( $inArray as $name ) {
2458 $retVal[] = $this->tableName( $name );
2459 }
2460
2461 return $retVal;
2462 }
2463
2475 protected function tableNameWithAlias( $table, $alias = false ) {
2476 if ( is_string( $table ) ) {
2477 $quotedTable = $this->tableName( $table );
2478 } elseif ( $table instanceof Subquery ) {
2479 $quotedTable = (string)$table;
2480 } else {
2481 throw new InvalidArgumentException( "Table must be a string or Subquery." );
2482 }
2483
2484 if ( !strlen( $alias ) || $alias === $table ) {
2485 if ( $table instanceof Subquery ) {
2486 throw new InvalidArgumentException( "Subquery table missing alias." );
2487 }
2488
2489 return $quotedTable;
2490 } else {
2491 return $quotedTable . ' ' . $this->addIdentifierQuotes( $alias );
2492 }
2493 }
2494
2501 protected function tableNamesWithAlias( $tables ) {
2502 $retval = [];
2503 foreach ( $tables as $alias => $table ) {
2504 if ( is_numeric( $alias ) ) {
2505 $alias = $table;
2506 }
2507 $retval[] = $this->tableNameWithAlias( $table, $alias );
2508 }
2509
2510 return $retval;
2511 }
2512
2521 protected function fieldNameWithAlias( $name, $alias = false ) {
2522 if ( !$alias || (string)$alias === (string)$name ) {
2523 return $name;
2524 } else {
2525 return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
2526 }
2527 }
2528
2535 protected function fieldNamesWithAlias( $fields ) {
2536 $retval = [];
2537 foreach ( $fields as $alias => $field ) {
2538 if ( is_numeric( $alias ) ) {
2539 $alias = $field;
2540 }
2541 $retval[] = $this->fieldNameWithAlias( $field, $alias );
2542 }
2543
2544 return $retval;
2545 }
2546
2558 $tables, $use_index = [], $ignore_index = [], $join_conds = []
2559 ) {
2560 $ret = [];
2561 $retJOIN = [];
2562 $use_index = (array)$use_index;
2563 $ignore_index = (array)$ignore_index;
2564 $join_conds = (array)$join_conds;
2565
2566 foreach ( $tables as $alias => $table ) {
2567 if ( !is_string( $alias ) ) {
2568 // No alias? Set it equal to the table name
2569 $alias = $table;
2570 }
2571
2572 if ( is_array( $table ) ) {
2573 // A parenthesized group
2574 if ( count( $table ) > 1 ) {
2575 $joinedTable = '(' .
2577 $table, $use_index, $ignore_index, $join_conds ) . ')';
2578 } else {
2579 // Degenerate case
2580 $innerTable = reset( $table );
2581 $innerAlias = key( $table );
2582 $joinedTable = $this->tableNameWithAlias(
2583 $innerTable,
2584 is_string( $innerAlias ) ? $innerAlias : $innerTable
2585 );
2586 }
2587 } else {
2588 $joinedTable = $this->tableNameWithAlias( $table, $alias );
2589 }
2590
2591 // Is there a JOIN clause for this table?
2592 if ( isset( $join_conds[$alias] ) ) {
2593 list( $joinType, $conds ) = $join_conds[$alias];
2594 $tableClause = $joinType;
2595 $tableClause .= ' ' . $joinedTable;
2596 if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
2597 $use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
2598 if ( $use != '' ) {
2599 $tableClause .= ' ' . $use;
2600 }
2601 }
2602 if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
2603 $ignore = $this->ignoreIndexClause(
2604 implode( ',', (array)$ignore_index[$alias] ) );
2605 if ( $ignore != '' ) {
2606 $tableClause .= ' ' . $ignore;
2607 }
2608 }
2609 $on = $this->makeList( (array)$conds, self::LIST_AND );
2610 if ( $on != '' ) {
2611 $tableClause .= ' ON (' . $on . ')';
2612 }
2613
2614 $retJOIN[] = $tableClause;
2615 } elseif ( isset( $use_index[$alias] ) ) {
2616 // Is there an INDEX clause for this table?
2617 $tableClause = $joinedTable;
2618 $tableClause .= ' ' . $this->useIndexClause(
2619 implode( ',', (array)$use_index[$alias] )
2620 );
2621
2622 $ret[] = $tableClause;
2623 } elseif ( isset( $ignore_index[$alias] ) ) {
2624 // Is there an INDEX clause for this table?
2625 $tableClause = $joinedTable;
2626 $tableClause .= ' ' . $this->ignoreIndexClause(
2627 implode( ',', (array)$ignore_index[$alias] )
2628 );
2629
2630 $ret[] = $tableClause;
2631 } else {
2632 $tableClause = $joinedTable;
2633
2634 $ret[] = $tableClause;
2635 }
2636 }
2637
2638 // We can't separate explicit JOIN clauses with ',', use ' ' for those
2639 $implicitJoins = $ret ? implode( ',', $ret ) : "";
2640 $explicitJoins = $retJOIN ? implode( ' ', $retJOIN ) : "";
2641
2642 // Compile our final table clause
2643 return implode( ' ', [ $implicitJoins, $explicitJoins ] );
2644 }
2645
2652 protected function indexName( $index ) {
2653 return $this->indexAliases[$index] ?? $index;
2654 }
2655
2656 public function addQuotes( $s ) {
2657 if ( $s instanceof Blob ) {
2658 $s = $s->fetch();
2659 }
2660 if ( $s === null ) {
2661 return 'NULL';
2662 } elseif ( is_bool( $s ) ) {
2663 return (int)$s;
2664 } else {
2665 # This will also quote numeric values. This should be harmless,
2666 # and protects against weird problems that occur when they really
2667 # _are_ strings such as article titles and string->number->string
2668 # conversion is not 1:1.
2669 return "'" . $this->strencode( $s ) . "'";
2670 }
2671 }
2672
2682 public function addIdentifierQuotes( $s ) {
2683 return '"' . str_replace( '"', '""', $s ) . '"';
2684 }
2685
2695 public function isQuotedIdentifier( $name ) {
2696 return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
2697 }
2698
2704 protected function escapeLikeInternal( $s, $escapeChar = '`' ) {
2705 return str_replace( [ $escapeChar, '%', '_' ],
2706 [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ],
2707 $s );
2708 }
2709
2710 public function buildLike() {
2711 $params = func_get_args();
2712
2713 if ( count( $params ) > 0 && is_array( $params[0] ) ) {
2714 $params = $params[0];
2715 }
2716
2717 $s = '';
2718
2719 // We use ` instead of \ as the default LIKE escape character, since addQuotes()
2720 // may escape backslashes, creating problems of double escaping. The `
2721 // character has good cross-DBMS compatibility, avoiding special operators
2722 // in MS SQL like ^ and %
2723 $escapeChar = '`';
2724
2725 foreach ( $params as $value ) {
2726 if ( $value instanceof LikeMatch ) {
2727 $s .= $value->toString();
2728 } else {
2729 $s .= $this->escapeLikeInternal( $value, $escapeChar );
2730 }
2731 }
2732
2733 return ' LIKE ' .
2734 $this->addQuotes( $s ) . ' ESCAPE ' . $this->addQuotes( $escapeChar ) . ' ';
2735 }
2736
2737 public function anyChar() {
2738 return new LikeMatch( '_' );
2739 }
2740
2741 public function anyString() {
2742 return new LikeMatch( '%' );
2743 }
2744
2745 public function nextSequenceValue( $seqName ) {
2746 return null;
2747 }
2748
2759 public function useIndexClause( $index ) {
2760 return '';
2761 }
2762
2773 public function ignoreIndexClause( $index ) {
2774 return '';
2775 }
2776
2777 public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2778 if ( count( $rows ) == 0 ) {
2779 return;
2780 }
2781
2782 // Single row case
2783 if ( !is_array( reset( $rows ) ) ) {
2784 $rows = [ $rows ];
2785 }
2786
2787 try {
2788 $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
2790 foreach ( $rows as $row ) {
2791 // Delete rows which collide with this one
2792 $indexWhereClauses = [];
2793 foreach ( $uniqueIndexes as $index ) {
2794 $indexColumns = (array)$index;
2795 $indexRowValues = array_intersect_key( $row, array_flip( $indexColumns ) );
2796 if ( count( $indexRowValues ) != count( $indexColumns ) ) {
2797 throw new DBUnexpectedError(
2798 $this,
2799 'New record does not provide all values for unique key (' .
2800 implode( ', ', $indexColumns ) . ')'
2801 );
2802 } elseif ( in_array( null, $indexRowValues, true ) ) {
2803 throw new DBUnexpectedError(
2804 $this,
2805 'New record has a null value for unique key (' .
2806 implode( ', ', $indexColumns ) . ')'
2807 );
2808 }
2809 $indexWhereClauses[] = $this->makeList( $indexRowValues, LIST_AND );
2810 }
2811
2812 if ( $indexWhereClauses ) {
2813 $this->delete( $table, $this->makeList( $indexWhereClauses, LIST_OR ), $fname );
2814 $affectedRowCount += $this->affectedRows();
2815 }
2816
2817 // Now insert the row
2818 $this->insert( $table, $row, $fname );
2819 $affectedRowCount += $this->affectedRows();
2820 }
2821 $this->endAtomic( $fname );
2822 $this->affectedRowCount = $affectedRowCount;
2823 } catch ( Exception $e ) {
2824 $this->cancelAtomic( $fname );
2825 throw $e;
2826 }
2827 }
2828
2839 protected function nativeReplace( $table, $rows, $fname ) {
2840 $table = $this->tableName( $table );
2841
2842 # Single row case
2843 if ( !is_array( reset( $rows ) ) ) {
2844 $rows = [ $rows ];
2845 }
2846
2847 $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
2848 $first = true;
2849
2850 foreach ( $rows as $row ) {
2851 if ( $first ) {
2852 $first = false;
2853 } else {
2854 $sql .= ',';
2855 }
2856
2857 $sql .= '(' . $this->makeList( $row ) . ')';
2858 }
2859
2860 return $this->query( $sql, $fname );
2861 }
2862
2863 public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
2864 $fname = __METHOD__
2865 ) {
2866 if ( !count( $rows ) ) {
2867 return true; // nothing to do
2868 }
2869
2870 if ( !is_array( reset( $rows ) ) ) {
2871 $rows = [ $rows ];
2872 }
2873
2874 if ( count( $uniqueIndexes ) ) {
2875 $clauses = []; // list WHERE clauses that each identify a single row
2876 foreach ( $rows as $row ) {
2877 foreach ( $uniqueIndexes as $index ) {
2878 $index = is_array( $index ) ? $index : [ $index ]; // columns
2879 $rowKey = []; // unique key to this row
2880 foreach ( $index as $column ) {
2881 $rowKey[$column] = $row[$column];
2882 }
2883 $clauses[] = $this->makeList( $rowKey, self::LIST_AND );
2884 }
2885 }
2886 $where = [ $this->makeList( $clauses, self::LIST_OR ) ];
2887 } else {
2888 $where = false;
2889 }
2890
2892 try {
2893 $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
2894 # Update any existing conflicting row(s)
2895 if ( $where !== false ) {
2896 $ok = $this->update( $table, $set, $where, $fname );
2897 $affectedRowCount += $this->affectedRows();
2898 } else {
2899 $ok = true;
2900 }
2901 # Now insert any non-conflicting row(s)
2902 $ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
2903 $affectedRowCount += $this->affectedRows();
2904 $this->endAtomic( $fname );
2905 $this->affectedRowCount = $affectedRowCount;
2906 } catch ( Exception $e ) {
2907 $this->cancelAtomic( $fname );
2908 throw $e;
2909 }
2910
2911 return $ok;
2912 }
2913
2914 public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2915 $fname = __METHOD__
2916 ) {
2917 if ( !$conds ) {
2918 throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
2919 }
2920
2921 $delTable = $this->tableName( $delTable );
2922 $joinTable = $this->tableName( $joinTable );
2923 $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2924 if ( $conds != '*' ) {
2925 $sql .= 'WHERE ' . $this->makeList( $conds, self::LIST_AND );
2926 }
2927 $sql .= ')';
2928
2929 $this->query( $sql, $fname );
2930 }
2931
2932 public function textFieldSize( $table, $field ) {
2933 $table = $this->tableName( $table );
2934 $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
2935 $res = $this->query( $sql, __METHOD__ );
2936 $row = $this->fetchObject( $res );
2937
2938 $m = [];
2939
2940 if ( preg_match( '/\‍((.*)\‍)/', $row->Type, $m ) ) {
2941 $size = $m[1];
2942 } else {
2943 $size = -1;
2944 }
2945
2946 return $size;
2947 }
2948
2949 public function delete( $table, $conds, $fname = __METHOD__ ) {
2950 if ( !$conds ) {
2951 throw new DBUnexpectedError( $this, __METHOD__ . ' called with no conditions' );
2952 }
2953
2954 $table = $this->tableName( $table );
2955 $sql = "DELETE FROM $table";
2956
2957 if ( $conds != '*' ) {
2958 if ( is_array( $conds ) ) {
2959 $conds = $this->makeList( $conds, self::LIST_AND );
2960 }
2961 $sql .= ' WHERE ' . $conds;
2962 }
2963
2964 return $this->query( $sql, $fname );
2965 }
2966
2967 final public function insertSelect(
2968 $destTable, $srcTable, $varMap, $conds,
2969 $fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = []
2970 ) {
2971 static $hints = [ 'NO_AUTO_COLUMNS' ];
2972
2973 $insertOptions = (array)$insertOptions;
2974 $selectOptions = (array)$selectOptions;
2975
2976 if ( $this->cliMode && $this->isInsertSelectSafe( $insertOptions, $selectOptions ) ) {
2977 // For massive migrations with downtime, we don't want to select everything
2978 // into memory and OOM, so do all this native on the server side if possible.
2979 return $this->nativeInsertSelect(
2980 $destTable,
2981 $srcTable,
2982 $varMap,
2983 $conds,
2984 $fname,
2985 array_diff( $insertOptions, $hints ),
2986 $selectOptions,
2987 $selectJoinConds
2988 );
2989 }
2990
2991 return $this->nonNativeInsertSelect(
2992 $destTable,
2993 $srcTable,
2994 $varMap,
2995 $conds,
2996 $fname,
2997 array_diff( $insertOptions, $hints ),
2998 $selectOptions,
2999 $selectJoinConds
3000 );
3001 }
3002
3009 protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
3010 return true;
3011 }
3012
3028 protected function nonNativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
3029 $fname = __METHOD__,
3030 $insertOptions = [], $selectOptions = [], $selectJoinConds = []
3031 ) {
3032 // For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
3033 // on only the master (without needing row-based-replication). It also makes it easy to
3034 // know how big the INSERT is going to be.
3035 $fields = [];
3036 foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
3037 $fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
3038 }
3039 $selectOptions[] = 'FOR UPDATE';
3040 $res = $this->select(
3041 $srcTable, implode( ',', $fields ), $conds, $fname, $selectOptions, $selectJoinConds
3042 );
3043 if ( !$res ) {
3044 return false;
3045 }
3046
3047 try {
3049 $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
3050 $rows = [];
3051 $ok = true;
3052 foreach ( $res as $row ) {
3053 $rows[] = (array)$row;
3054
3055 // Avoid inserts that are too huge
3056 if ( count( $rows ) >= $this->nonNativeInsertSelectBatchSize ) {
3057 $ok = $this->insert( $destTable, $rows, $fname, $insertOptions );
3058 if ( !$ok ) {
3059 break;
3060 }
3061 $affectedRowCount += $this->affectedRows();
3062 $rows = [];
3063 }
3064 }
3065 if ( $rows && $ok ) {
3066 $ok = $this->insert( $destTable, $rows, $fname, $insertOptions );
3067 if ( $ok ) {
3068 $affectedRowCount += $this->affectedRows();
3069 }
3070 }
3071 if ( $ok ) {
3072 $this->endAtomic( $fname );
3073 $this->affectedRowCount = $affectedRowCount;
3074 } else {
3075 $this->cancelAtomic( $fname );
3076 }
3077 return $ok;
3078 } catch ( Exception $e ) {
3079 $this->cancelAtomic( $fname );
3080 throw $e;
3081 }
3082 }
3083
3099 protected function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
3100 $fname = __METHOD__,
3101 $insertOptions = [], $selectOptions = [], $selectJoinConds = []
3102 ) {
3103 $destTable = $this->tableName( $destTable );
3104
3105 if ( !is_array( $insertOptions ) ) {
3106 $insertOptions = [ $insertOptions ];
3107 }
3108
3109 $insertOptions = $this->makeInsertOptions( $insertOptions );
3110
3111 $selectSql = $this->selectSQLText(
3112 $srcTable,
3113 array_values( $varMap ),
3114 $conds,
3115 $fname,
3116 $selectOptions,
3117 $selectJoinConds
3118 );
3119
3120 $sql = "INSERT $insertOptions" .
3121 " INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ') ' .
3122 $selectSql;
3123
3124 return $this->query( $sql, $fname );
3125 }
3126
3146 public function limitResult( $sql, $limit, $offset = false ) {
3147 if ( !is_numeric( $limit ) ) {
3148 throw new DBUnexpectedError( $this,
3149 "Invalid non-numeric limit passed to limitResult()\n" );
3150 }
3151
3152 return "$sql LIMIT "
3153 . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
3154 . "{$limit} ";
3155 }
3156
3157 public function unionSupportsOrderAndLimit() {
3158 return true; // True for almost every DB supported
3159 }
3160
3161 public function unionQueries( $sqls, $all ) {
3162 $glue = $all ? ') UNION ALL (' : ') UNION (';
3163
3164 return '(' . implode( $glue, $sqls ) . ')';
3165 }
3166
3168 $table, $vars, array $permute_conds, $extra_conds = '', $fname = __METHOD__,
3169 $options = [], $join_conds = []
3170 ) {
3171 // First, build the Cartesian product of $permute_conds
3172 $conds = [ [] ];
3173 foreach ( $permute_conds as $field => $values ) {
3174 if ( !$values ) {
3175 // Skip empty $values
3176 continue;
3177 }
3178 $values = array_unique( $values ); // For sanity
3179 $newConds = [];
3180 foreach ( $conds as $cond ) {
3181 foreach ( $values as $value ) {
3182 $cond[$field] = $value;
3183 $newConds[] = $cond; // Arrays are by-value, not by-reference, so this works
3184 }
3185 }
3186 $conds = $newConds;
3187 }
3188
3189 $extra_conds = $extra_conds === '' ? [] : (array)$extra_conds;
3190
3191 // If there's just one condition and no subordering, hand off to
3192 // selectSQLText directly.
3193 if ( count( $conds ) === 1 &&
3194 ( !isset( $options['INNER ORDER BY'] ) || !$this->unionSupportsOrderAndLimit() )
3195 ) {
3196 return $this->selectSQLText(
3197 $table, $vars, $conds[0] + $extra_conds, $fname, $options, $join_conds
3198 );
3199 }
3200
3201 // Otherwise, we need to pull out the order and limit to apply after
3202 // the union. Then build the SQL queries for each set of conditions in
3203 // $conds. Then union them together (using UNION ALL, because the
3204 // product *should* already be distinct).
3205 $orderBy = $this->makeOrderBy( $options );
3206 $limit = $options['LIMIT'] ?? null;
3207 $offset = $options['OFFSET'] ?? false;
3208 $all = empty( $options['NOTALL'] ) && !in_array( 'NOTALL', $options );
3209 if ( !$this->unionSupportsOrderAndLimit() ) {
3210 unset( $options['ORDER BY'], $options['LIMIT'], $options['OFFSET'] );
3211 } else {
3212 if ( array_key_exists( 'INNER ORDER BY', $options ) ) {
3213 $options['ORDER BY'] = $options['INNER ORDER BY'];
3214 }
3215 if ( $limit !== null && is_numeric( $offset ) && $offset != 0 ) {
3216 // We need to increase the limit by the offset rather than
3217 // using the offset directly, otherwise it'll skip incorrectly
3218 // in the subqueries.
3219 $options['LIMIT'] = $limit + $offset;
3220 unset( $options['OFFSET'] );
3221 }
3222 }
3223
3224 $sqls = [];
3225 foreach ( $conds as $cond ) {
3226 $sqls[] = $this->selectSQLText(
3227 $table, $vars, $cond + $extra_conds, $fname, $options, $join_conds
3228 );
3229 }
3230 $sql = $this->unionQueries( $sqls, $all ) . $orderBy;
3231 if ( $limit !== null ) {
3232 $sql = $this->limitResult( $sql, $limit, $offset );
3233 }
3234
3235 return $sql;
3236 }
3237
3238 public function conditional( $cond, $trueVal, $falseVal ) {
3239 if ( is_array( $cond ) ) {
3240 $cond = $this->makeList( $cond, self::LIST_AND );
3241 }
3242
3243 return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
3244 }
3245
3246 public function strreplace( $orig, $old, $new ) {
3247 return "REPLACE({$orig}, {$old}, {$new})";
3248 }
3249
3250 public function getServerUptime() {
3251 return 0;
3252 }
3253
3254 public function wasDeadlock() {
3255 return false;
3256 }
3257
3258 public function wasLockTimeout() {
3259 return false;
3260 }
3261
3262 public function wasConnectionLoss() {
3263 return $this->wasConnectionError( $this->lastErrno() );
3264 }
3265
3266 public function wasReadOnlyError() {
3267 return false;
3268 }
3269
3270 public function wasErrorReissuable() {
3271 return (
3272 $this->wasDeadlock() ||
3273 $this->wasLockTimeout() ||
3274 $this->wasConnectionLoss()
3275 );
3276 }
3277
3284 public function wasConnectionError( $errno ) {
3285 return false;
3286 }
3287
3294 protected function wasKnownStatementRollbackError() {
3295 return false; // don't know; it could have caused a transaction rollback
3296 }
3297
3298 public function deadlockLoop() {
3299 $args = func_get_args();
3300 $function = array_shift( $args );
3301 $tries = self::DEADLOCK_TRIES;
3302
3303 $this->begin( __METHOD__ );
3304
3305 $retVal = null;
3307 $e = null;
3308 do {
3309 try {
3310 $retVal = $function( ...$args );
3311 break;
3312 } catch ( DBQueryError $e ) {
3313 if ( $this->wasDeadlock() ) {
3314 // Retry after a randomized delay
3315 usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
3316 } else {
3317 // Throw the error back up
3318 throw $e;
3319 }
3320 }
3321 } while ( --$tries > 0 );
3322
3323 if ( $tries <= 0 ) {
3324 // Too many deadlocks; give up
3325 $this->rollback( __METHOD__ );
3326 throw $e;
3327 } else {
3328 $this->commit( __METHOD__ );
3329
3330 return $retVal;
3331 }
3332 }
3333
3334 public function masterPosWait( DBMasterPos $pos, $timeout ) {
3335 # Real waits are implemented in the subclass.
3336 return 0;
3337 }
3338
3339 public function getReplicaPos() {
3340 # Stub
3341 return false;
3342 }
3343
3344 public function getMasterPos() {
3345 # Stub
3346 return false;
3347 }
3348
3349 public function serverIsReadOnly() {
3350 return false;
3351 }
3352
3353 final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
3354 if ( !$this->trxLevel ) {
3355 throw new DBUnexpectedError( $this, "No transaction is active." );
3356 }
3357 $this->trxEndCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3358 }
3359
3360 final public function onTransactionCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
3361 if ( !$this->trxLevel && $this->getTransactionRoundId() ) {
3362 // Start an implicit transaction similar to how query() does
3363 $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
3364 $this->trxAutomatic = true;
3365 }
3366
3367 $this->trxIdleCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3368 if ( !$this->trxLevel ) {
3369 $this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
3370 }
3371 }
3372
3373 final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
3374 $this->onTransactionCommitOrIdle( $callback, $fname );
3375 }
3376
3377 final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
3378 if ( !$this->trxLevel && $this->getTransactionRoundId() ) {
3379 // Start an implicit transaction similar to how query() does
3380 $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
3381 $this->trxAutomatic = true;
3382 }
3383
3384 if ( $this->trxLevel ) {
3385 $this->trxPreCommitCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3386 } else {
3387 // No transaction is active nor will start implicitly, so make one for this callback
3388 $this->startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
3389 try {
3390 $callback( $this );
3391 $this->endAtomic( __METHOD__ );
3392 } catch ( Exception $e ) {
3393 $this->cancelAtomic( __METHOD__ );
3394 throw $e;
3395 }
3396 }
3397 }
3398
3402 private function currentAtomicSectionId() {
3403 if ( $this->trxLevel && $this->trxAtomicLevels ) {
3404 $levelInfo = end( $this->trxAtomicLevels );
3405
3406 return $levelInfo[1];
3407 }
3408
3409 return null;
3410 }
3411
3418 ) {
3419 foreach ( $this->trxPreCommitCallbacks as $key => $info ) {
3420 if ( $info[2] === $old ) {
3421 $this->trxPreCommitCallbacks[$key][2] = $new;
3422 }
3423 }
3424 foreach ( $this->trxIdleCallbacks as $key => $info ) {
3425 if ( $info[2] === $old ) {
3426 $this->trxIdleCallbacks[$key][2] = $new;
3427 }
3428 }
3429 foreach ( $this->trxEndCallbacks as $key => $info ) {
3430 if ( $info[2] === $old ) {
3431 $this->trxEndCallbacks[$key][2] = $new;
3432 }
3433 }
3434 }
3435
3440 private function modifyCallbacksForCancel( array $sectionIds ) {
3441 // Cancel the "on commit" callbacks owned by this savepoint
3442 $this->trxIdleCallbacks = array_filter(
3443 $this->trxIdleCallbacks,
3444 function ( $entry ) use ( $sectionIds ) {
3445 return !in_array( $entry[2], $sectionIds, true );
3446 }
3447 );
3448 $this->trxPreCommitCallbacks = array_filter(
3449 $this->trxPreCommitCallbacks,
3450 function ( $entry ) use ( $sectionIds ) {
3451 return !in_array( $entry[2], $sectionIds, true );
3452 }
3453 );
3454 // Make "on resolution" callbacks owned by this savepoint to perceive a rollback
3455 foreach ( $this->trxEndCallbacks as $key => $entry ) {
3456 if ( in_array( $entry[2], $sectionIds, true ) ) {
3457 $callback = $entry[0];
3458 $this->trxEndCallbacks[$key][0] = function () use ( $callback ) {
3459 return $callback( self::TRIGGER_ROLLBACK, $this );
3460 };
3461 }
3462 }
3463 }
3464
3465 final public function setTransactionListener( $name, callable $callback = null ) {
3466 if ( $callback ) {
3467 $this->trxRecurringCallbacks[$name] = $callback;
3468 } else {
3469 unset( $this->trxRecurringCallbacks[$name] );
3470 }
3471 }
3472
3481 final public function setTrxEndCallbackSuppression( $suppress ) {
3482 $this->trxEndCallbacksSuppressed = $suppress;
3483 }
3484
3495 public function runOnTransactionIdleCallbacks( $trigger ) {
3496 if ( $this->trxLevel ) { // sanity
3497 throw new DBUnexpectedError( $this, __METHOD__ . ': a transaction is still open.' );
3498 }
3499
3500 if ( $this->trxEndCallbacksSuppressed ) {
3501 return 0;
3502 }
3503
3504 $count = 0;
3505 $autoTrx = $this->getFlag( self::DBO_TRX ); // automatic begin() enabled?
3507 $e = null; // first exception
3508 do { // callbacks may add callbacks :)
3509 $callbacks = array_merge(
3510 $this->trxIdleCallbacks,
3511 $this->trxEndCallbacks // include "transaction resolution" callbacks
3512 );
3513 $this->trxIdleCallbacks = []; // consumed (and recursion guard)
3514 $this->trxEndCallbacks = []; // consumed (recursion guard)
3515 foreach ( $callbacks as $callback ) {
3516 ++$count;
3517 list( $phpCallback ) = $callback;
3518 $this->clearFlag( self::DBO_TRX ); // make each query its own transaction
3519 try {
3520 call_user_func( $phpCallback, $trigger, $this );
3521 } catch ( Exception $ex ) {
3522 call_user_func( $this->errorLogger, $ex );
3523 $e = $e ?: $ex;
3524 // Some callbacks may use startAtomic/endAtomic, so make sure
3525 // their transactions are ended so other callbacks don't fail
3526 if ( $this->trxLevel() ) {
3527 $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
3528 }
3529 } finally {
3530 if ( $autoTrx ) {
3531 $this->setFlag( self::DBO_TRX ); // restore automatic begin()
3532 } else {
3533 $this->clearFlag( self::DBO_TRX ); // restore auto-commit
3534 }
3535 }
3536 }
3537 } while ( count( $this->trxIdleCallbacks ) );
3538
3539 if ( $e instanceof Exception ) {
3540 throw $e; // re-throw any first exception
3541 }
3542
3543 return $count;
3544 }
3545
3556 $count = 0;
3557
3558 $e = null; // first exception
3559 do { // callbacks may add callbacks :)
3560 $callbacks = $this->trxPreCommitCallbacks;
3561 $this->trxPreCommitCallbacks = []; // consumed (and recursion guard)
3562 foreach ( $callbacks as $callback ) {
3563 try {
3564 ++$count;
3565 list( $phpCallback ) = $callback;
3566 $phpCallback( $this );
3567 } catch ( Exception $ex ) {
3568 ( $this->errorLogger )( $ex );
3569 $e = $e ?: $ex;
3570 }
3571 }
3572 } while ( count( $this->trxPreCommitCallbacks ) );
3573
3574 if ( $e instanceof Exception ) {
3575 throw $e; // re-throw any first exception
3576 }
3577
3578 return $count;
3579 }
3580
3590 public function runTransactionListenerCallbacks( $trigger ) {
3591 if ( $this->trxEndCallbacksSuppressed ) {
3592 return;
3593 }
3594
3596 $e = null; // first exception
3597
3598 foreach ( $this->trxRecurringCallbacks as $phpCallback ) {
3599 try {
3600 $phpCallback( $trigger, $this );
3601 } catch ( Exception $ex ) {
3602 ( $this->errorLogger )( $ex );
3603 $e = $e ?: $ex;
3604 }
3605 }
3606
3607 if ( $e instanceof Exception ) {
3608 throw $e; // re-throw any first exception
3609 }
3610 }
3611
3622 protected function doSavepoint( $identifier, $fname ) {
3623 $this->query( 'SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
3624 }
3625
3636 protected function doReleaseSavepoint( $identifier, $fname ) {
3637 $this->query( 'RELEASE SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
3638 }
3639
3650 protected function doRollbackToSavepoint( $identifier, $fname ) {
3651 $this->query( 'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
3652 }
3653
3658 private function nextSavepointId( $fname ) {
3659 $savepointId = self::$SAVEPOINT_PREFIX . ++$this->trxAtomicCounter;
3660 if ( strlen( $savepointId ) > 30 ) {
3661 // 30 == Oracle's identifier length limit (pre 12c)
3662 // With a 22 character prefix, that puts the highest number at 99999999.
3663 throw new DBUnexpectedError(
3664 $this,
3665 'There have been an excessively large number of atomic sections in a transaction'
3666 . " started by $this->trxFname (at $fname)"
3667 );
3668 }
3669
3670 return $savepointId;
3671 }
3672
3673 final public function startAtomic(
3674 $fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE
3675 ) {
3676 $savepointId = $cancelable === self::ATOMIC_CANCELABLE ? self::$NOT_APPLICABLE : null;
3677
3678 if ( !$this->trxLevel ) {
3679 $this->begin( $fname, self::TRANSACTION_INTERNAL ); // sets trxAutomatic
3680 // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
3681 // in all changes being in one transaction to keep requests transactional.
3682 if ( $this->getFlag( self::DBO_TRX ) ) {
3683 // Since writes could happen in between the topmost atomic sections as part
3684 // of the transaction, those sections will need savepoints.
3685 $savepointId = $this->nextSavepointId( $fname );
3686 $this->doSavepoint( $savepointId, $fname );
3687 } else {
3688 $this->trxAutomaticAtomic = true;
3689 }
3690 } elseif ( $cancelable === self::ATOMIC_CANCELABLE ) {
3691 $savepointId = $this->nextSavepointId( $fname );
3692 $this->doSavepoint( $savepointId, $fname );
3693 }
3694
3695 $sectionId = new AtomicSectionIdentifier;
3696 $this->trxAtomicLevels[] = [ $fname, $sectionId, $savepointId ];
3697 $this->queryLogger->debug( 'startAtomic: entering level ' .
3698 ( count( $this->trxAtomicLevels ) - 1 ) . " ($fname)" );
3699
3700 return $sectionId;
3701 }
3702
3703 final public function endAtomic( $fname = __METHOD__ ) {
3704 if ( !$this->trxLevel || !$this->trxAtomicLevels ) {
3705 throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
3706 }
3707
3708 // Check if the current section matches $fname
3709 $pos = count( $this->trxAtomicLevels ) - 1;
3710 list( $savedFname, $sectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
3711 $this->queryLogger->debug( "endAtomic: leaving level $pos ($fname)" );
3712
3713 if ( $savedFname !== $fname ) {
3714 throw new DBUnexpectedError(
3715 $this,
3716 "Invalid atomic section ended (got $fname but expected $savedFname)."
3717 );
3718 }
3719
3720 // Remove the last section (no need to re-index the array)
3721 array_pop( $this->trxAtomicLevels );
3722
3723 if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
3724 $this->commit( $fname, self::FLUSHING_INTERNAL );
3725 } elseif ( $savepointId !== null && $savepointId !== self::$NOT_APPLICABLE ) {
3726 $this->doReleaseSavepoint( $savepointId, $fname );
3727 }
3728
3729 // Hoist callback ownership for callbacks in the section that just ended;
3730 // all callbacks should have an owner that is present in trxAtomicLevels.
3731 $currentSectionId = $this->currentAtomicSectionId();
3732 if ( $currentSectionId ) {
3733 $this->reassignCallbacksForSection( $sectionId, $currentSectionId );
3734 }
3735 }
3736
3737 final public function cancelAtomic(
3738 $fname = __METHOD__, AtomicSectionIdentifier $sectionId = null
3739 ) {
3740 if ( !$this->trxLevel || !$this->trxAtomicLevels ) {
3741 throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
3742 }
3743
3744 $excisedFnames = [];
3745 if ( $sectionId !== null ) {
3746 // Find the (last) section with the given $sectionId
3747 $pos = -1;
3748 foreach ( $this->trxAtomicLevels as $i => list( $asFname, $asId, $spId ) ) {
3749 if ( $asId === $sectionId ) {
3750 $pos = $i;
3751 }
3752 }
3753 if ( $pos < 0 ) {
3754 throw new DBUnexpectedError( $this, "Atomic section not found (for $fname)" );
3755 }
3756 // Remove all descendant sections and re-index the array
3757 $excisedIds = [];
3758 $len = count( $this->trxAtomicLevels );
3759 for ( $i = $pos + 1; $i < $len; ++$i ) {
3760 $excisedFnames[] = $this->trxAtomicLevels[$i][0];
3761 $excisedIds[] = $this->trxAtomicLevels[$i][1];
3762 }
3763 $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 );
3764 $this->modifyCallbacksForCancel( $excisedIds );
3765 }
3766
3767 // Check if the current section matches $fname
3768 $pos = count( $this->trxAtomicLevels ) - 1;
3769 list( $savedFname, $savedSectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
3770
3771 if ( $excisedFnames ) {
3772 $this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname) " .
3773 "and descendants " . implode( ', ', $excisedFnames ) );
3774 } else {
3775 $this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname)" );
3776 }
3777
3778 if ( $savedFname !== $fname ) {
3779 throw new DBUnexpectedError(
3780 $this,
3781 "Invalid atomic section ended (got $fname but expected $savedFname)."
3782 );
3783 }
3784
3785 // Remove the last section (no need to re-index the array)
3786 array_pop( $this->trxAtomicLevels );
3787 $this->modifyCallbacksForCancel( [ $savedSectionId ] );
3788
3789 if ( $savepointId !== null ) {
3790 // Rollback the transaction to the state just before this atomic section
3791 if ( $savepointId === self::$NOT_APPLICABLE ) {
3792 $this->rollback( $fname, self::FLUSHING_INTERNAL );
3793 } else {
3794 $this->doRollbackToSavepoint( $savepointId, $fname );
3795 $this->trxStatus = self::STATUS_TRX_OK; // no exception; recovered
3796 $this->trxStatusIgnoredCause = null;
3797 }
3798 } elseif ( $this->trxStatus > self::STATUS_TRX_ERROR ) {
3799 // Put the transaction into an error state if it's not already in one
3800 $this->trxStatus = self::STATUS_TRX_ERROR;
3801 $this->trxStatusCause = new DBUnexpectedError(
3802 $this,
3803 "Uncancelable atomic section canceled (got $fname)."
3804 );
3805 }
3806
3807 $this->affectedRowCount = 0; // for the sake of consistency
3808 }
3809
3810 final public function doAtomicSection(
3811 $fname, callable $callback, $cancelable = self::ATOMIC_NOT_CANCELABLE
3812 ) {
3813 $sectionId = $this->startAtomic( $fname, $cancelable );
3814 try {
3815 $res = $callback( $this, $fname );
3816 } catch ( Exception $e ) {
3817 $this->cancelAtomic( $fname, $sectionId );
3818
3819 throw $e;
3820 }
3821 $this->endAtomic( $fname );
3822
3823 return $res;
3824 }
3825
3826 final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
3827 static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
3828 if ( !in_array( $mode, $modes, true ) ) {
3829 throw new DBUnexpectedError( $this, "$fname: invalid mode parameter '$mode'." );
3830 }
3831
3832 // Protect against mismatched atomic section, transaction nesting, and snapshot loss
3833 if ( $this->trxLevel ) {
3834 if ( $this->trxAtomicLevels ) {
3835 $levels = $this->flatAtomicSectionList();
3836 $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
3837 throw new DBUnexpectedError( $this, $msg );
3838 } elseif ( !$this->trxAutomatic ) {
3839 $msg = "$fname: Explicit transaction already active (from {$this->trxFname}).";
3840 throw new DBUnexpectedError( $this, $msg );
3841 } else {
3842 $msg = "$fname: Implicit transaction already active (from {$this->trxFname}).";
3843 throw new DBUnexpectedError( $this, $msg );
3844 }
3845 } elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
3846 $msg = "$fname: Implicit transaction expected (DBO_TRX set).";
3847 throw new DBUnexpectedError( $this, $msg );
3848 }
3849
3850 // Avoid fatals if close() was called
3851 $this->assertOpen();
3852
3853 $this->doBegin( $fname );
3854 $this->trxStatus = self::STATUS_TRX_OK;
3855 $this->trxStatusIgnoredCause = null;
3856 $this->trxAtomicCounter = 0;
3857 $this->trxTimestamp = microtime( true );
3858 $this->trxFname = $fname;
3859 $this->trxDoneWrites = false;
3860 $this->trxAutomaticAtomic = false;
3861 $this->trxAtomicLevels = [];
3862 $this->trxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
3863 $this->trxWriteDuration = 0.0;
3864 $this->trxWriteQueryCount = 0;
3865 $this->trxWriteAffectedRows = 0;
3866 $this->trxWriteAdjDuration = 0.0;
3867 $this->trxWriteAdjQueryCount = 0;
3868 $this->trxWriteCallers = [];
3869 // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
3870 // Get an estimate of the replication lag before any such queries.
3871 $this->trxReplicaLag = null; // clear cached value first
3872 $this->trxReplicaLag = $this->getApproximateLagStatus()['lag'];
3873 // T147697: make explicitTrxActive() return true until begin() finishes. This way, no
3874 // caller will think its OK to muck around with the transaction just because startAtomic()
3875 // has not yet completed (e.g. setting trxAtomicLevels).
3876 $this->trxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
3877 }
3878
3885 protected function doBegin( $fname ) {
3886 $this->query( 'BEGIN', $fname );
3887 $this->trxLevel = 1;
3888 }
3889
3890 final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
3891 static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
3892 if ( !in_array( $flush, $modes, true ) ) {
3893 throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'." );
3894 }
3895
3896 if ( $this->trxLevel && $this->trxAtomicLevels ) {
3897 // There are still atomic sections open; this cannot be ignored
3898 $levels = $this->flatAtomicSectionList();
3899 throw new DBUnexpectedError(
3900 $this,
3901 "$fname: Got COMMIT while atomic sections $levels are still open."
3902 );
3903 }
3904
3905 if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
3906 if ( !$this->trxLevel ) {
3907 return; // nothing to do
3908 } elseif ( !$this->trxAutomatic ) {
3909 throw new DBUnexpectedError(
3910 $this,
3911 "$fname: Flushing an explicit transaction, getting out of sync."
3912 );
3913 }
3914 } else {
3915 if ( !$this->trxLevel ) {
3916 $this->queryLogger->error(
3917 "$fname: No transaction to commit, something got out of sync." );
3918 return; // nothing to do
3919 } elseif ( $this->trxAutomatic ) {
3920 throw new DBUnexpectedError(
3921 $this,
3922 "$fname: Expected mass commit of all peer transactions (DBO_TRX set)."
3923 );
3924 }
3925 }
3926
3927 // Avoid fatals if close() was called
3928 $this->assertOpen();
3929
3931
3932 $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
3933 $this->doCommit( $fname );
3934 $this->trxStatus = self::STATUS_TRX_NONE;
3935
3936 if ( $this->trxDoneWrites ) {
3937 $this->lastWriteTime = microtime( true );
3938 $this->trxProfiler->transactionWritingOut(
3939 $this->server,
3940 $this->getDomainID(),
3941 $this->trxShortId,
3942 $writeTime,
3943 $this->trxWriteAffectedRows
3944 );
3945 }
3946
3947 // With FLUSHING_ALL_PEERS, callbacks will be explicitly run later
3948 if ( $flush !== self::FLUSHING_ALL_PEERS ) {
3949 $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
3950 $this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
3951 }
3952 }
3953
3960 protected function doCommit( $fname ) {
3961 if ( $this->trxLevel ) {
3962 $this->query( 'COMMIT', $fname );
3963 $this->trxLevel = 0;
3964 }
3965 }
3966
3967 final public function rollback( $fname = __METHOD__, $flush = '' ) {
3968 $trxActive = $this->trxLevel;
3969
3970 if ( $flush !== self::FLUSHING_INTERNAL && $flush !== self::FLUSHING_ALL_PEERS ) {
3971 if ( $this->getFlag( self::DBO_TRX ) ) {
3972 throw new DBUnexpectedError(
3973 $this,
3974 "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)."
3975 );
3976 }
3977 }
3978
3979 if ( $trxActive ) {
3980 // Avoid fatals if close() was called
3981 $this->assertOpen();
3982
3983 $this->doRollback( $fname );
3984 $this->trxStatus = self::STATUS_TRX_NONE;
3985 $this->trxAtomicLevels = [];
3986 // Estimate the RTT via a query now that trxStatus is OK
3987 $writeTime = $this->pingAndCalculateLastTrxApplyTime();
3988
3989 if ( $this->trxDoneWrites ) {
3990 $this->trxProfiler->transactionWritingOut(
3991 $this->server,
3992 $this->getDomainID(),
3993 $this->trxShortId,
3994 $writeTime,
3995 $this->trxWriteAffectedRows
3996 );
3997 }
3998 }
3999
4000 // Clear any commit-dependant callbacks. They might even be present
4001 // only due to transaction rounds, with no SQL transaction being active
4002 $this->trxIdleCallbacks = [];
4003 $this->trxPreCommitCallbacks = [];
4004
4005 // With FLUSHING_ALL_PEERS, callbacks will be explicitly run later
4006 if ( $trxActive && $flush !== self::FLUSHING_ALL_PEERS ) {
4007 try {
4008 $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
4009 } catch ( Exception $e ) {
4010 // already logged; finish and let LoadBalancer move on during mass-rollback
4011 }
4012 try {
4013 $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
4014 } catch ( Exception $e ) {
4015 // already logged; let LoadBalancer move on during mass-rollback
4016 }
4017
4018 $this->affectedRowCount = 0; // for the sake of consistency
4019 }
4020 }
4021
4028 protected function doRollback( $fname ) {
4029 if ( $this->trxLevel ) {
4030 # Disconnects cause rollback anyway, so ignore those errors
4031 $ignoreErrors = true;
4032 $this->query( 'ROLLBACK', $fname, $ignoreErrors );
4033 $this->trxLevel = 0;
4034 }
4035 }
4036
4037 public function flushSnapshot( $fname = __METHOD__ ) {
4038 if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
4039 // This only flushes transactions to clear snapshots, not to write data
4040 $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
4041 throw new DBUnexpectedError(
4042 $this,
4043 "$fname: Cannot flush snapshot because writes are pending ($fnames)."
4044 );
4045 }
4046
4047 $this->commit( $fname, self::FLUSHING_INTERNAL );
4048 }
4049
4050 public function explicitTrxActive() {
4051 return $this->trxLevel && ( $this->trxAtomicLevels || !$this->trxAutomatic );
4052 }
4053
4055 $oldName, $newName, $temporary = false, $fname = __METHOD__
4056 ) {
4057 throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
4058 }
4059
4060 public function listTables( $prefix = null, $fname = __METHOD__ ) {
4061 throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
4062 }
4063
4064 public function listViews( $prefix = null, $fname = __METHOD__ ) {
4065 throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
4066 }
4067
4068 public function timestamp( $ts = 0 ) {
4069 $t = new ConvertibleTimestamp( $ts );
4070 // Let errors bubble up to avoid putting garbage in the DB
4071 return $t->getTimestamp( TS_MW );
4072 }
4073
4074 public function timestampOrNull( $ts = null ) {
4075 if ( is_null( $ts ) ) {
4076 return null;
4077 } else {
4078 return $this->timestamp( $ts );
4079 }
4080 }
4081
4082 public function affectedRows() {
4083 return ( $this->affectedRowCount === null )
4084 ? $this->fetchAffectedRowCount() // default to driver value
4086 }
4087
4091 abstract protected function fetchAffectedRowCount();
4092
4106 protected function resultObject( $result ) {
4107 if ( !$result ) {
4108 return false;
4109 } elseif ( $result instanceof ResultWrapper ) {
4110 return $result;
4111 } elseif ( $result === true ) {
4112 // Successful write query
4113 return $result;
4114 } else {
4115 return new ResultWrapper( $this, $result );
4116 }
4117 }
4118
4119 public function ping( &$rtt = null ) {
4120 // Avoid hitting the server if it was hit recently
4121 if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
4122 if ( !func_num_args() || $this->rttEstimate > 0 ) {
4123 $rtt = $this->rttEstimate;
4124 return true; // don't care about $rtt
4125 }
4126 }
4127
4128 // This will reconnect if possible or return false if not
4129 $this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
4130 $ok = ( $this->query( self::PING_QUERY, __METHOD__, true ) !== false );
4131 $this->restoreFlags( self::RESTORE_PRIOR );
4132
4133 if ( $ok ) {
4134 $rtt = $this->rttEstimate;
4135 }
4136
4137 return $ok;
4138 }
4139
4146 protected function replaceLostConnection( $fname ) {
4147 $this->closeConnection();
4148 $this->opened = false;
4149 $this->conn = false;
4150 try {
4151 $this->open(
4152 $this->server,
4153 $this->user,
4154 $this->password,
4155 $this->getDBname(),
4156 $this->dbSchema(),
4157 $this->tablePrefix()
4158 );
4159 $this->lastPing = microtime( true );
4160 $ok = true;
4161
4162 $this->connLogger->warning(
4163 $fname . ': lost connection to {dbserver}; reconnected',
4164 [
4165 'dbserver' => $this->getServer(),
4166 'exception' => new RuntimeException()
4167 ]
4168 );
4169 } catch ( DBConnectionError $e ) {
4170 $ok = false;
4171
4172 $this->connLogger->error(
4173 $fname . ': lost connection to {dbserver} permanently',
4174 [ 'dbserver' => $this->getServer() ]
4175 );
4176 }
4177
4178 $this->handleSessionLoss();
4179
4180 return $ok;
4181 }
4182
4183 public function getSessionLagStatus() {
4184 return $this->getRecordedTransactionLagStatus() ?: $this->getApproximateLagStatus();
4185 }
4186
4200 final protected function getRecordedTransactionLagStatus() {
4201 return ( $this->trxLevel && $this->trxReplicaLag !== null )
4202 ? [ 'lag' => $this->trxReplicaLag, 'since' => $this->trxTimestamp() ]
4203 : null;
4204 }
4205
4212 protected function getApproximateLagStatus() {
4213 return [
4214 'lag' => $this->getLBInfo( 'replica' ) ? $this->getLag() : 0,
4215 'since' => microtime( true )
4216 ];
4217 }
4218
4238 public static function getCacheSetOptions( IDatabase $db1, IDatabase $db2 = null ) {
4239 $res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
4240 foreach ( func_get_args() as $db ) {
4242 $status = $db->getSessionLagStatus();
4243 if ( $status['lag'] === false ) {
4244 $res['lag'] = false;
4245 } elseif ( $res['lag'] !== false ) {
4246 $res['lag'] = max( $res['lag'], $status['lag'] );
4247 }
4248 $res['since'] = min( $res['since'], $status['since'] );
4249 $res['pending'] = $res['pending'] ?: $db->writesPending();
4250 }
4251
4252 return $res;
4253 }
4254
4255 public function getLag() {
4256 return 0;
4257 }
4258
4259 public function maxListLen() {
4260 return 0;
4261 }
4262
4263 public function encodeBlob( $b ) {
4264 return $b;
4265 }
4266
4267 public function decodeBlob( $b ) {
4268 if ( $b instanceof Blob ) {
4269 $b = $b->fetch();
4270 }
4271 return $b;
4272 }
4273
4274 public function setSessionOptions( array $options ) {
4275 }
4276
4277 public function sourceFile(
4278 $filename,
4279 callable $lineCallback = null,
4280 callable $resultCallback = null,
4281 $fname = false,
4282 callable $inputCallback = null
4283 ) {
4284 Wikimedia\suppressWarnings();
4285 $fp = fopen( $filename, 'r' );
4286 Wikimedia\restoreWarnings();
4287
4288 if ( false === $fp ) {
4289 throw new RuntimeException( "Could not open \"{$filename}\".\n" );
4290 }
4291
4292 if ( !$fname ) {
4293 $fname = __METHOD__ . "( $filename )";
4294 }
4295
4296 try {
4297 $error = $this->sourceStream(
4298 $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
4299 } catch ( Exception $e ) {
4300 fclose( $fp );
4301 throw $e;
4302 }
4303
4304 fclose( $fp );
4305
4306 return $error;
4307 }
4308
4309 public function setSchemaVars( $vars ) {
4310 $this->schemaVars = $vars;
4311 }
4312
4313 public function sourceStream(
4314 $fp,
4315 callable $lineCallback = null,
4316 callable $resultCallback = null,
4317 $fname = __METHOD__,
4318 callable $inputCallback = null
4319 ) {
4320 $delimiterReset = new ScopedCallback(
4321 function ( $delimiter ) {
4322 $this->delimiter = $delimiter;
4323 },
4325 );
4326 $cmd = '';
4327
4328 while ( !feof( $fp ) ) {
4329 if ( $lineCallback ) {
4330 call_user_func( $lineCallback );
4331 }
4332
4333 $line = trim( fgets( $fp ) );
4334
4335 if ( $line == '' ) {
4336 continue;
4337 }
4338
4339 if ( '-' == $line[0] && '-' == $line[1] ) {
4340 continue;
4341 }
4342
4343 if ( $cmd != '' ) {
4344 $cmd .= ' ';
4345 }
4346
4347 $done = $this->streamStatementEnd( $cmd, $line );
4348
4349 $cmd .= "$line\n";
4350
4351 if ( $done || feof( $fp ) ) {
4352 $cmd = $this->replaceVars( $cmd );
4353
4354 if ( $inputCallback ) {
4355 $callbackResult = $inputCallback( $cmd );
4356
4357 if ( is_string( $callbackResult ) || !$callbackResult ) {
4358 $cmd = $callbackResult;
4359 }
4360 }
4361
4362 if ( $cmd ) {
4363 $res = $this->query( $cmd, $fname );
4364
4365 if ( $resultCallback ) {
4366 $resultCallback( $res, $this );
4367 }
4368
4369 if ( false === $res ) {
4370 $err = $this->lastError();
4371
4372 return "Query \"{$cmd}\" failed with error code \"$err\".\n";
4373 }
4374 }
4375 $cmd = '';
4376 }
4377 }
4378
4379 ScopedCallback::consume( $delimiterReset );
4380 return true;
4381 }
4382
4390 public function streamStatementEnd( &$sql, &$newLine ) {
4391 if ( $this->delimiter ) {
4392 $prev = $newLine;
4393 $newLine = preg_replace(
4394 '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
4395 if ( $newLine != $prev ) {
4396 return true;
4397 }
4398 }
4399
4400 return false;
4401 }
4402
4423 protected function replaceVars( $ins ) {
4424 $vars = $this->getSchemaVars();
4425 return preg_replace_callback(
4426 '!
4427 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
4428 \'\{\$ (\w+) }\' | # 3. addQuotes
4429 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
4430 /\*\$ (\w+) \*/ # 5. leave unencoded
4431 !x',
4432 function ( $m ) use ( $vars ) {
4433 // Note: Because of <https://bugs.php.net/bug.php?id=51881>,
4434 // check for both nonexistent keys *and* the empty string.
4435 if ( isset( $m[1] ) && $m[1] !== '' ) {
4436 if ( $m[1] === 'i' ) {
4437 return $this->indexName( $m[2] );
4438 } else {
4439 return $this->tableName( $m[2] );
4440 }
4441 } elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
4442 return $this->addQuotes( $vars[$m[3]] );
4443 } elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
4444 return $this->addIdentifierQuotes( $vars[$m[4]] );
4445 } elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
4446 return $vars[$m[5]];
4447 } else {
4448 return $m[0];
4449 }
4450 },
4451 $ins
4452 );
4453 }
4454
4461 protected function getSchemaVars() {
4462 if ( $this->schemaVars ) {
4463 return $this->schemaVars;
4464 } else {
4465 return $this->getDefaultSchemaVars();
4466 }
4467 }
4468
4477 protected function getDefaultSchemaVars() {
4478 return [];
4479 }
4480
4481 public function lockIsFree( $lockName, $method ) {
4482 // RDBMs methods for checking named locks may or may not count this thread itself.
4483 // In MySQL, IS_FREE_LOCK() returns 0 if the thread already has the lock. This is
4484 // the behavior choosen by the interface for this method.
4485 return !isset( $this->namedLocksHeld[$lockName] );
4486 }
4487
4488 public function lock( $lockName, $method, $timeout = 5 ) {
4489 $this->namedLocksHeld[$lockName] = 1;
4490
4491 return true;
4492 }
4493
4494 public function unlock( $lockName, $method ) {
4495 unset( $this->namedLocksHeld[$lockName] );
4496
4497 return true;
4498 }
4499
4500 public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
4501 if ( $this->writesOrCallbacksPending() ) {
4502 // This only flushes transactions to clear snapshots, not to write data
4503 $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
4504 throw new DBUnexpectedError(
4505 $this,
4506 "$fname: Cannot flush pre-lock snapshot because writes are pending ($fnames)."
4507 );
4508 }
4509
4510 if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
4511 return null;
4512 }
4513
4514 $unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
4515 if ( $this->trxLevel() ) {
4516 // There is a good chance an exception was thrown, causing any early return
4517 // from the caller. Let any error handler get a chance to issue rollback().
4518 // If there isn't one, let the error bubble up and trigger server-side rollback.
4520 function () use ( $lockKey, $fname ) {
4521 $this->unlock( $lockKey, $fname );
4522 },
4523 $fname
4524 );
4525 } else {
4526 $this->unlock( $lockKey, $fname );
4527 }
4528 } );
4529
4530 $this->commit( $fname, self::FLUSHING_INTERNAL );
4531
4532 return $unlocker;
4533 }
4534
4535 public function namedLocksEnqueue() {
4536 return false;
4537 }
4538
4540 return true;
4541 }
4542
4543 final public function lockTables( array $read, array $write, $method ) {
4544 if ( $this->writesOrCallbacksPending() ) {
4545 throw new DBUnexpectedError( $this, "Transaction writes or callbacks still pending." );
4546 }
4547
4548 if ( $this->tableLocksHaveTransactionScope() ) {
4549 $this->startAtomic( $method );
4550 }
4551
4552 return $this->doLockTables( $read, $write, $method );
4553 }
4554
4563 protected function doLockTables( array $read, array $write, $method ) {
4564 return true;
4565 }
4566
4567 final public function unlockTables( $method ) {
4568 if ( $this->tableLocksHaveTransactionScope() ) {
4569 $this->endAtomic( $method );
4570
4571 return true; // locks released on COMMIT/ROLLBACK
4572 }
4573
4574 return $this->doUnlockTables( $method );
4575 }
4576
4583 protected function doUnlockTables( $method ) {
4584 return true;
4585 }
4586
4594 public function dropTable( $tableName, $fName = __METHOD__ ) {
4595 if ( !$this->tableExists( $tableName, $fName ) ) {
4596 return false;
4597 }
4598 $sql = "DROP TABLE " . $this->tableName( $tableName ) . " CASCADE";
4599
4600 return $this->query( $sql, $fName );
4601 }
4602
4603 public function getInfinity() {
4604 return 'infinity';
4605 }
4606
4607 public function encodeExpiry( $expiry ) {
4608 return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
4609 ? $this->getInfinity()
4610 : $this->timestamp( $expiry );
4611 }
4612
4613 public function decodeExpiry( $expiry, $format = TS_MW ) {
4614 if ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) {
4615 return 'infinity';
4616 }
4617
4618 return ConvertibleTimestamp::convert( $format, $expiry );
4619 }
4620
4621 public function setBigSelects( $value = true ) {
4622 // no-op
4623 }
4624
4625 public function isReadOnly() {
4626 return ( $this->getReadOnlyReason() !== false );
4627 }
4628
4632 protected function getReadOnlyReason() {
4633 $reason = $this->getLBInfo( 'readOnlyReason' );
4634
4635 return is_string( $reason ) ? $reason : false;
4636 }
4637
4638 public function setTableAliases( array $aliases ) {
4639 $this->tableAliases = $aliases;
4640 }
4641
4642 public function setIndexAliases( array $aliases ) {
4643 $this->indexAliases = $aliases;
4644 }
4645
4657 protected function getBindingHandle() {
4658 if ( !$this->conn ) {
4659 throw new DBUnexpectedError(
4660 $this,
4661 'DB connection was already closed or the connection dropped.'
4662 );
4663 }
4664
4665 return $this->conn;
4666 }
4667
4672 public function __toString() {
4673 return (string)$this->conn;
4674 }
4675
4680 public function __clone() {
4681 $this->connLogger->warning(
4682 "Cloning " . static::class . " is not recommended; forking connection",
4683 [ 'exception' => new RuntimeException() ]
4684 );
4685
4686 if ( $this->isOpen() ) {
4687 // Open a new connection resource without messing with the old one
4688 $this->opened = false;
4689 $this->conn = false;
4690 $this->trxEndCallbacks = []; // don't copy
4691 $this->handleSessionLoss(); // no trx or locks anymore
4692 $this->open(
4693 $this->server,
4694 $this->user,
4695 $this->password,
4696 $this->getDBname(),
4697 $this->dbSchema(),
4698 $this->tablePrefix()
4699 );
4700 $this->lastPing = microtime( true );
4701 }
4702 }
4703
4709 public function __sleep() {
4710 throw new RuntimeException( 'Database serialization may cause problems, since ' .
4711 'the connection is not restored on wakeup.' );
4712 }
4713
4717 public function __destruct() {
4718 if ( $this->trxLevel && $this->trxDoneWrites ) {
4719 trigger_error( "Uncommitted DB writes (transaction from {$this->trxFname})." );
4720 }
4721
4722 $danglingWriters = $this->pendingWriteAndCallbackCallers();
4723 if ( $danglingWriters ) {
4724 $fnames = implode( ', ', $danglingWriters );
4725 trigger_error( "DB transaction writes or callbacks still pending ($fnames)." );
4726 }
4727
4728 if ( $this->conn ) {
4729 // Avoid connection leaks for sanity. Normally, resources close at script completion.
4730 // The connection might already be closed in zend/hhvm by now, so suppress warnings.
4731 Wikimedia\suppressWarnings();
4732 $this->closeConnection();
4733 Wikimedia\restoreWarnings();
4734 $this->conn = false;
4735 $this->opened = false;
4736 }
4737 }
4738}
4739
4743class_alias( Database::class, 'DatabaseBase' );
4744
4748class_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:121
$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.
Database error base class.
Definition DBError.php:30
Class to handle database/prefix specification for IDatabase domains.
Relational database abstraction object.
Definition Database.php:48
bool $cliMode
Whether this PHP instance is for a CLI script.
Definition Database.php:89
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:564
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:747
integer null $affectedRowCount
Rows affected by the last query to query() or its CRUD wrappers.
Definition Database.php:138
mixed $profiler
Class name or object With profileIn/profileOut methods.
Definition Database.php:264
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:111
static string $SAVEPOINT_PREFIX
Prefix to the atomic section counter used to make savepoint IDs.
Definition Database.php:274
restoreErrorHandler()
Restore the previous error handler and return the last PHP error for this DB.
Definition Database.php:874
getApproximateLagStatus()
Get a replica DB lag estimate for this server.
bool $trxDoneWrites
Record if possible write queries were done in the last transaction started.
Definition Database.php:192
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:52
int[] $priorFlags
Prior flags member variable values.
Definition Database.php:261
doInitConnection()
Actually connect to the database over the wire (or to local files)
Definition Database.php:353
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:583
reassignCallbacksForSection(AtomicSectionIdentifier $old, AtomicSectionIdentifier $new)
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:595
setLogger(LoggerInterface $logger)
Set the PSR-3 logger interface to use for query logging.
Definition Database.php:560
int $trxWriteQueryCount
Number of write queries for the current transaction.
Definition Database.php:231
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.
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:608
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:91
indexName( $index)
Allows for index remapping in queries where this is not consistent across DBMS.
callable[] $trxRecurringCallbacks
Map of (name => callable)
Definition Database.php:117
getDomainID()
Return the currently selected domain ID.
Definition Database.php:835
makeUpdateOptionsArray( $options)
Make UPDATE options array for Database::makeUpdateOptions.
listViews( $prefix=null, $fname=__METHOD__)
Lists all the VIEWs in the database.
assertOpen()
Make sure isOpen() returns true as a sanity check.
Definition Database.php:998
array[] $trxPreCommitCallbacks
List of (callable, method name, atomic section id)
Definition Database.php:113
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:81
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:252
lastDoneWrites()
Returns the last time the connection may have been used for write queries.
Definition Database.php:678
isTransactableQuery( $sql)
Determine whether a SQL statement is sensitive to isolation level.
int $trxStatus
Transaction status.
Definition Database.php:143
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:686
buildGroupConcatField( $delim, $table, $field, $conds='', $join_conds=[])
Build a GROUP_CONCAT or equivalent statement for a query.
initConnection()
Initialize the connection to the database over the wire (or to local files)
Definition Database.php:338
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:223
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:87
array null $trxStatusIgnoredCause
If wasKnownStatementRollbackError() prevented trxStatus from being set, the relevant details are stor...
Definition Database.php:152
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.
lock( $lockName, $method, $timeout=5)
Acquire a named lock.
wasConnectionError( $errno)
Do not use this method outside of Database/DBError classes.
int $trxWriteAffectedRows
Number of rows affected by write queries for the current transaction.
Definition Database.php:235
deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname=__METHOD__)
DELETE where the condition is a join.
unionSupportsOrderAndLimit()
Returns true if current database backend supports ORDER BY or LIMIT for separate subqueries within th...
duplicateTableStructure( $oldName, $newName, $temporary=false, $fname=__METHOD__)
Creates a new table with structure copied from existing table.
doRollbackToSavepoint( $identifier, $fname)
Rollback to a savepoint.
doneWrites()
Returns true if the connection may have been used for write queries.
Definition Database.php:674
anyChar()
Returns a token for buildLike() that denotes a '_' to be used in a LIKE query.
restoreFlags( $state=self::RESTORE_PRIOR)
Restore the flags to their prior state before the last setFlag/clearFlag call.
Definition Database.php:809
doAtomicSection( $fname, callable $callback, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Perform an atomic section of reversable SQL statements from a callback.
modifyCallbacksForCancel(array $sectionIds)
setTrxEndCallbackSuppression( $suppress)
Whether to disable running of post-COMMIT/ROLLBACK callbacks.
float $rttEstimate
RTT time estimate.
Definition Database.php:247
installErrorHandler()
Set a custom error handler for logging errors during database connection.
Definition Database.php:863
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:175
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:426
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:185
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:798
useIndexClause( $index)
USE INDEX clause.
DatabaseDomain $currentDomain
Definition Database.php:136
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:904
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:93
LoggerInterface $queryLogger
Definition Database.php:99
int $trxAtomicCounter
Counter for atomic savepoint identifiers.
Definition Database.php:205
pendingWriteAndCallbackCallers()
List the methods that have write queries or callbacks for the current transaction.
Definition Database.php:759
bool $trxEndCallbacksSuppressed
Whether to suppress triggering of transaction end callbacks.
Definition Database.php:119
Exception null $trxStatusCause
The last error that caused the status to become STATUS_TRX_ERROR.
Definition Database.php:147
sourceStream( $fp, callable $lineCallback=null, callable $resultCallback=null, $fname=__METHOD__, callable $inputCallback=null)
Read and execute commands from an open file handle.
qualifiedTableComponents( $name)
Get the table components needed for a query given the currently selected database.
tableNamesN()
Fetch a number of table names into an zero-indexed numerical array This is handy when you need to con...
callback $errorLogger
Error logging callback.
Definition Database.php:101
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:743
pendingWriteQueryDuration( $type=self::ESTIMATE_TOTAL)
Get the time spend running write queries for this transaction.
Definition Database.php:713
startAtomic( $fname=__METHOD__, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Begin an atomic section of SQL statements.
getWikiID()
Alias for getDomainID()
Definition Database.php:839
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:629
aggregateValue( $valuedata, $valuename='value')
Return aggregated value alias.
array[] $tableAliases
Map of (table => (dbname, schema, prefix) map)
Definition Database.php:85
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 using backticks or "double quotes" depending on the database type.
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)
getLogContext(array $extras=[])
Create a log context to pass to PSR-3 logger functions.
Definition Database.php:914
resource null $conn
Database connection.
Definition Database.php:106
trxLevel()
Gets the current transaction level.
Definition Database.php:579
prependDatabaseOrSchema( $namespace, $relation, $format)
const DEADLOCK_DELAY_MAX
Maximum time to wait before retry.
Definition Database.php:54
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:50
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:787
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:670
static getClass( $dbType, $driver=null)
Definition Database.php:495
setLazyMasterHandle(IDatabase $conn)
Set a lazy-connecting DB handle to the master DB (for replication status purposes)
Definition Database.php:649
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:481
IDatabase null $lazyMasterHandle
Lazy handle to the master DB this server replicates from.
Definition Database.php:255
float bool $lastWriteTime
UNIX timestamp of last write query.
Definition Database.php:75
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:73
onTransactionPreCommitOrIdle(callable $callback, $fname=__METHOD__)
Run a callback before the current transaction commits or now if there is none.
unionQueries( $sqls, $all)
Construct a UNION query This is used for providing overload point for other DB abstractions not compa...
wasErrorReissuable()
Determines if the last query error was due to something outside of the query itself.
doReleaseSavepoint( $identifier, $fname)
Release a savepoint.
string $password
Password used to establish the current connection.
Definition Database.php:83
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:641
escapeLikeInternal( $s, $escapeChar='`')
bool $trxAutomatic
Record if the current transaction was started implicitly due to DBO_TRX being set.
Definition Database.php:199
upsert( $table, array $rows, array $uniqueIndexes, array $set, $fname=__METHOD__)
INSERT ON DUPLICATE KEY UPDATE wrapper, upserts an array into a table.
ping(&$rtt=null)
Ping the server and try to reconnect if it there is no connection.
int $trxLevel
Either 1 if a transaction is active or 0 otherwise.
Definition Database.php:159
__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:132
implicitGroupby()
Returns true if this database does an implicit sort when doing GROUP BY.
Definition Database.php:662
anyString()
Returns a token for buildLike() that denotes a '' to be used in a LIKE query.
LoggerInterface $connLogger
Definition Database.php:97
string $trxShortId
Either a short hexidecimal string if a transaction is active or "".
Definition Database.php:166
bool $trxAutomaticAtomic
Record if the current transaction was started implicitly by Database::startAtomic.
Definition Database.php:217
bufferResults( $buffer=null)
Turns buffering of SQL result sets on (true) or off (false).
Definition Database.php:568
getInfinity()
Find out when 'infinity' is.
assertTransactionStatus( $sql, $fname)
string $server
Server that this instance is currently connected to.
Definition Database.php:79
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:783
getFlag( $flag)
Returns a boolean whether the flag $flag is set for this connection.
Definition Database.php:822
query( $sql, $fname=__METHOD__, $tempIgnore=false)
Run an SQL query and return the result.
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:211
float $trxWriteAdjDuration
Like trxWriteQueryCount but excludes lock-bound, easy to replicate, queries.
Definition Database.php:239
float $trxReplicaLag
Lag estimate at the time of BEGIN.
Definition Database.php:177
const PING_TTL
How long before it is worth doing a dummy query to test the connection.
Definition Database.php:57
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:250
float $lastPing
UNIX timestamp.
Definition Database.php:258
array[] $trxEndCallbacks
List of (callable, method name, atomic section id)
Definition Database.php:115
int $trxWriteAdjQueryCount
Number of write queries counted in trxWriteAdjDuration.
Definition Database.php:243
TransactionProfiler $trxProfiler
Definition Database.php:266
doLockTables(array $read, array $write, $method)
Helper function for lockTables() that handles the actual table locking.
doUnlockTables( $method)
Helper function for unlockTables() that handles the actual table unlocking.
handleSessionLoss()
Clean things up after session (and thus transaction) loss.
callback $deprecationLogger
Deprecation logging callback.
Definition Database.php:103
BagOStuff $srvCache
APC cache.
Definition Database.php:95
float $trxWriteDuration
Seconds spent in write queries for the current transaction.
Definition Database.php:227
getBindingHandle()
Get the underlying binding connection handle.
static string $NOT_APPLICABLE
Idiom used when a cancelable atomic section started the transaction.
Definition Database.php:272
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 (that has all IResultWrapper methods)
implicitOrderby()
Returns true if this database does an implicit order by when the column has an index For example: SEL...
Definition Database.php:666
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:287
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:925
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.
const LIST_OR
Definition Defines.php:46
const LIST_AND
Definition Defines.php:43
static configuration should be added through ResourceLoaderGetConfigVars instead & $vars
Definition hooks.txt:2278
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:2857
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. 'LanguageGetMagic':DEPRECATED since 1.16! Use $magicWords in a file listed in $wgExtensionMessagesFiles instead. Use this to define synonyms of magic words depending of the language & $magicExtensions:associative array of magic words synonyms $lang:language code(string) 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetSpecialPageAliases':DEPRECATED! Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead. Use to define aliases of special pages names depending of the language & $specialPageAliases:associative array of magic words synonyms $lang:language code(string) 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links:Array with elements of the form "language:title" in the order that they will be output. & $linkFlags:Associative array mapping prefixed links to arrays of flags. Currently unused, but planned to provide support for marking individual language links in the UI, e.g. for featured articles. 'LanguageSelector':Hook to change the language selector available on a page. $out:The output page. $cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED 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:2042
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account incomplete not yet checked for validity & $retval
Definition hooks.txt:266
This 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:1815
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:1305
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:2050
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:2214
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:1035
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:2054
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and insert
Definition hooks.txt:2140
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:2226
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