MediaWiki  master
Database.php
Go to the documentation of this file.
1 <?php
26 namespace Wikimedia\Rdbms;
27 
42 
49 abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAwareInterface {
51  protected $server;
53  protected $user;
55  protected $password;
57  protected $tableAliases = [];
59  protected $indexAliases = [];
61  protected $cliMode;
63  protected $agent;
65  protected $flags;
67  protected $lbInfo = [];
69  protected $schemaVars = false;
71  protected $connectionParams = [];
73  protected $connectionVariables = [];
75  protected $delimiter = ';';
77  protected $htmlErrors;
80 
82  protected $srvCache;
84  protected $connLogger;
86  protected $queryLogger;
88  protected $errorLogger;
90  protected $deprecationLogger;
92  protected $profiler;
94  protected $trxProfiler;
96  protected $currentDomain;
98  protected $conn;
99 
102 
104  protected $sessionNamedLocks = [];
106  protected $sessionTempTables = [];
107 
109  protected $trxShortId = '';
111  protected $trxStatus = self::STATUS_TRX_NONE;
113  protected $trxStatusCause;
117  private $trxTimestamp = null;
119  private $trxReplicaLag = null;
121  private $trxFname = null;
123  private $trxDoneWrites = false;
125  private $trxAutomatic = false;
127  private $trxAtomicCounter = 0;
129  private $trxAtomicLevels = [];
131  private $trxAutomaticAtomic = false;
133  private $trxWriteCallers = [];
135  private $trxWriteDuration = 0.0;
137  private $trxWriteQueryCount = 0;
141  private $trxWriteAdjDuration = 0.0;
145  private $trxIdleCallbacks = [];
149  private $trxEndCallbacks = [];
155  private $trxEndCallbacksSuppressed = false;
156 
158  private $priorFlags = [];
159 
161  protected $affectedRowCount;
162 
164  private $lastPing = 0.0;
166  private $lastQuery = '';
168  private $lastWriteTime = false;
170  private $lastPhpError = false;
172  private $lastRoundTripEstimate = 0.0;
173 
175  const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
177  const ATTR_SCHEMAS_AS_TABLE_GROUPS = 'supports-schemas';
178 
180  const NEW_UNCONNECTED = 0;
182  const NEW_CONNECTED = 1;
183 
185  const STATUS_TRX_ERROR = 1;
187  const STATUS_TRX_OK = 2;
189  const STATUS_TRX_NONE = 3;
190 
192  private static $NOT_APPLICABLE = 'n/a';
194  private static $SAVEPOINT_PREFIX = 'wikimedia_rdbms_atomic';
195 
197  private static $TEMP_NORMAL = 1;
199  private static $TEMP_PSEUDO_PERMANENT = 2;
200 
202  private static $DEADLOCK_TRIES = 4;
204  private static $DEADLOCK_DELAY_MIN = 500000;
206  private static $DEADLOCK_DELAY_MAX = 1500000;
207 
209  private static $PING_TTL = 1.0;
211  private static $PING_QUERY = 'SELECT 1 AS ping';
212 
214  private static $TINY_WRITE_SEC = 0.010;
216  private static $SLOW_WRITE_SEC = 0.500;
218  private static $SMALL_WRITE_ROWS = 100;
219 
221  protected static $MUTABLE_FLAGS = [
222  'DBO_DEBUG',
223  'DBO_NOBUFFER',
224  'DBO_TRX',
225  'DBO_DDLMODE',
226  ];
228  protected static $DBO_MUTABLE = (
230  );
231 
236  protected function __construct( array $params ) {
237  foreach ( [ 'host', 'user', 'password', 'dbname', 'schema', 'tablePrefix' ] as $name ) {
238  $this->connectionParams[$name] = $params[$name];
239  }
240 
241  $this->cliMode = $params['cliMode'];
242  // Agent name is added to SQL queries in a comment, so make sure it can't break out
243  $this->agent = str_replace( '/', '-', $params['agent'] );
244 
245  $this->flags = $params['flags'];
246  if ( $this->flags & self::DBO_DEFAULT ) {
247  if ( $this->cliMode ) {
248  $this->flags &= ~self::DBO_TRX;
249  } else {
250  $this->flags |= self::DBO_TRX;
251  }
252  }
253 
254  $this->connectionVariables = $params['variables'];
255 
256  $this->srvCache = $params['srvCache'] ?? new HashBagOStuff();
257 
258  $this->profiler = is_callable( $params['profiler'] ) ? $params['profiler'] : null;
259  $this->trxProfiler = $params['trxProfiler'];
260  $this->connLogger = $params['connLogger'];
261  $this->queryLogger = $params['queryLogger'];
262  $this->errorLogger = $params['errorLogger'];
263  $this->deprecationLogger = $params['deprecationLogger'];
264 
265  if ( isset( $params['nonNativeInsertSelectBatchSize'] ) ) {
266  $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'];
267  }
268 
269  // Set initial dummy domain until open() sets the final DB/prefix
270  $this->currentDomain = new DatabaseDomain(
271  $params['dbname'] != '' ? $params['dbname'] : null,
272  $params['schema'] != '' ? $params['schema'] : null,
273  $params['tablePrefix']
274  );
275  }
276 
285  final public function initConnection() {
286  if ( $this->isOpen() ) {
287  throw new LogicException( __METHOD__ . ': already connected' );
288  }
289  // Establish the connection
290  $this->doInitConnection();
291  }
292 
299  protected function doInitConnection() {
300  $this->open(
301  $this->connectionParams['host'],
302  $this->connectionParams['user'],
303  $this->connectionParams['password'],
304  $this->connectionParams['dbname'],
305  $this->connectionParams['schema'],
306  $this->connectionParams['tablePrefix']
307  );
308  }
309 
321  abstract protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix );
322 
368  final public static function factory( $type, $params = [], $connect = self::NEW_CONNECTED ) {
369  $class = self::getClass( $type, $params['driver'] ?? null );
370 
371  if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
372  $params += [
373  'host' => null,
374  'user' => null,
375  'password' => null,
376  'dbname' => null,
377  'schema' => null,
378  'tablePrefix' => '',
379  'flags' => 0,
380  'variables' => [],
381  'cliMode' => ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ),
382  'agent' => basename( $_SERVER['SCRIPT_NAME'] ) . '@' . gethostname()
383  ];
384 
385  $normalizedParams = [
386  // Configuration
387  'host' => strlen( $params['host'] ) ? $params['host'] : null,
388  'user' => strlen( $params['user'] ) ? $params['user'] : null,
389  'password' => is_string( $params['password'] ) ? $params['password'] : null,
390  'dbname' => strlen( $params['dbname'] ) ? $params['dbname'] : null,
391  'schema' => strlen( $params['schema'] ) ? $params['schema'] : null,
392  'tablePrefix' => (string)$params['tablePrefix'],
393  'flags' => (int)$params['flags'],
394  'variables' => $params['variables'],
395  'cliMode' => (bool)$params['cliMode'],
396  'agent' => (string)$params['agent'],
397  // Objects and callbacks
398  'profiler' => $params['profiler'] ?? null,
399  'trxProfiler' => $params['trxProfiler'] ?? new TransactionProfiler(),
400  'connLogger' => $params['connLogger'] ?? new NullLogger(),
401  'queryLogger' => $params['queryLogger'] ?? new NullLogger(),
402  'errorLogger' => $params['errorLogger'] ?? function ( Exception $e ) {
403  trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
404  },
405  'deprecationLogger' => $params['deprecationLogger'] ?? function ( $msg ) {
406  trigger_error( $msg, E_USER_DEPRECATED );
407  }
408  ] + $params;
409 
411  $conn = new $class( $normalizedParams );
412  if ( $connect === self::NEW_CONNECTED ) {
413  $conn->initConnection();
414  }
415  } else {
416  $conn = null;
417  }
418 
419  return $conn;
420  }
421 
429  final public static function attributesFromType( $dbType, $driver = null ) {
430  static $defaults = [
431  self::ATTR_DB_LEVEL_LOCKING => false,
432  self::ATTR_SCHEMAS_AS_TABLE_GROUPS => false
433  ];
434 
435  $class = self::getClass( $dbType, $driver );
436 
437  return call_user_func( [ $class, 'getAttributes' ] ) + $defaults;
438  }
439 
446  private static function getClass( $dbType, $driver = null ) {
447  // For database types with built-in support, the below maps type to IDatabase
448  // implementations. For types with multipe driver implementations (PHP extensions),
449  // an array can be used, keyed by extension name. In case of an array, the
450  // optional 'driver' parameter can be used to force a specific driver. Otherwise,
451  // we auto-detect the first available driver. For types without built-in support,
452  // an class named "Database<Type>" us used, eg. DatabaseFoo for type 'foo'.
453  static $builtinTypes = [
454  'mysql' => [ 'mysqli' => DatabaseMysqli::class ],
455  'sqlite' => DatabaseSqlite::class,
456  'postgres' => DatabasePostgres::class,
457  ];
458 
459  $dbType = strtolower( $dbType );
460  $class = false;
461 
462  if ( isset( $builtinTypes[$dbType] ) ) {
463  $possibleDrivers = $builtinTypes[$dbType];
464  if ( is_string( $possibleDrivers ) ) {
465  $class = $possibleDrivers;
466  } elseif ( (string)$driver !== '' ) {
467  if ( !isset( $possibleDrivers[$driver] ) ) {
468  throw new InvalidArgumentException( __METHOD__ .
469  " type '$dbType' does not support driver '{$driver}'" );
470  }
471 
472  $class = $possibleDrivers[$driver];
473  } else {
474  foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
475  if ( extension_loaded( $posDriver ) ) {
476  $class = $possibleClass;
477  break;
478  }
479  }
480  }
481  } else {
482  $class = 'Database' . ucfirst( $dbType );
483  }
484 
485  if ( $class === false ) {
486  throw new InvalidArgumentException( __METHOD__ .
487  " no viable database extension found for type '$dbType'" );
488  }
489 
490  return $class;
491  }
492 
497  protected static function getAttributes() {
498  return [];
499  }
500 
508  public function setLogger( LoggerInterface $logger ) {
509  $this->queryLogger = $logger;
510  }
511 
512  public function getServerInfo() {
513  return $this->getServerVersion();
514  }
515 
523  public function bufferResults( $buffer = null ) {
524  return true;
525  }
526 
527  final public function trxLevel() {
528  return ( $this->trxShortId != '' ) ? 1 : 0;
529  }
530 
531  public function trxTimestamp() {
532  return $this->trxLevel() ? $this->trxTimestamp : null;
533  }
534 
539  public function trxStatus() {
540  return $this->trxStatus;
541  }
542 
543  public function tablePrefix( $prefix = null ) {
544  $old = $this->currentDomain->getTablePrefix();
545  if ( $prefix !== null ) {
546  $this->currentDomain = new DatabaseDomain(
547  $this->currentDomain->getDatabase(),
548  $this->currentDomain->getSchema(),
549  $prefix
550  );
551  }
552 
553  return $old;
554  }
555 
556  public function dbSchema( $schema = null ) {
557  if ( strlen( $schema ) && $this->getDBname() === null ) {
558  throw new DBUnexpectedError( $this, "Cannot set schema to '$schema'; no database set" );
559  }
560 
561  $old = $this->currentDomain->getSchema();
562  if ( $schema !== null ) {
563  $this->currentDomain = new DatabaseDomain(
564  $this->currentDomain->getDatabase(),
565  // DatabaseDomain uses null for unspecified schemas
566  strlen( $schema ) ? $schema : null,
567  $this->currentDomain->getTablePrefix()
568  );
569  }
570 
571  return (string)$old;
572  }
573 
577  protected function relationSchemaQualifier() {
578  return $this->dbSchema();
579  }
580 
581  public function getLBInfo( $name = null ) {
582  if ( is_null( $name ) ) {
583  return $this->lbInfo;
584  }
585 
586  if ( array_key_exists( $name, $this->lbInfo ) ) {
587  return $this->lbInfo[$name];
588  }
589 
590  return null;
591  }
592 
593  public function setLBInfo( $nameOrArray, $value = null ) {
594  if ( is_array( $nameOrArray ) ) {
595  $this->lbInfo = $nameOrArray;
596  } elseif ( is_string( $nameOrArray ) ) {
597  if ( $value !== null ) {
598  $this->lbInfo[$nameOrArray] = $value;
599  } else {
600  unset( $this->lbInfo[$nameOrArray] );
601  }
602  } else {
603  throw new InvalidArgumentException( "Got non-string key" );
604  }
605  }
606 
607  public function setLazyMasterHandle( IDatabase $conn ) {
608  $this->lazyMasterHandle = $conn;
609  }
610 
616  protected function getLazyMasterHandle() {
618  }
619 
620  public function implicitOrderby() {
621  return true;
622  }
623 
624  public function lastQuery() {
625  return $this->lastQuery;
626  }
627 
628  public function lastDoneWrites() {
629  return $this->lastWriteTime ?: false;
630  }
631 
632  public function writesPending() {
633  return $this->trxLevel() && $this->trxDoneWrites;
634  }
635 
636  public function writesOrCallbacksPending() {
637  return $this->trxLevel() && (
638  $this->trxDoneWrites ||
639  $this->trxIdleCallbacks ||
640  $this->trxPreCommitCallbacks ||
641  $this->trxEndCallbacks ||
643  );
644  }
645 
646  public function preCommitCallbacksPending() {
647  return $this->trxLevel() && $this->trxPreCommitCallbacks;
648  }
649 
653  final protected function getTransactionRoundId() {
654  // If transaction round participation is enabled, see if one is active
655  if ( $this->getFlag( self::DBO_TRX ) ) {
656  $id = $this->getLBInfo( 'trxRoundId' );
657 
658  return is_string( $id ) ? $id : null;
659  }
660 
661  return null;
662  }
663 
664  public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
665  if ( !$this->trxLevel() ) {
666  return false;
667  } elseif ( !$this->trxDoneWrites ) {
668  return 0.0;
669  }
670 
671  switch ( $type ) {
672  case self::ESTIMATE_DB_APPLY:
673  return $this->pingAndCalculateLastTrxApplyTime();
674  default: // everything
676  }
677  }
678 
682  private function pingAndCalculateLastTrxApplyTime() {
683  $this->ping( $rtt );
684 
685  $rttAdjTotal = $this->trxWriteAdjQueryCount * $rtt;
686  $applyTime = max( $this->trxWriteAdjDuration - $rttAdjTotal, 0 );
687  // For omitted queries, make them count as something at least
688  $omitted = $this->trxWriteQueryCount - $this->trxWriteAdjQueryCount;
689  $applyTime += self::$TINY_WRITE_SEC * $omitted;
690 
691  return $applyTime;
692  }
693 
694  public function pendingWriteCallers() {
695  return $this->trxLevel() ? $this->trxWriteCallers : [];
696  }
697 
698  public function pendingWriteRowsAffected() {
700  }
701 
710  public function pendingWriteAndCallbackCallers() {
711  $fnames = $this->pendingWriteCallers();
712  foreach ( [
713  $this->trxIdleCallbacks,
714  $this->trxPreCommitCallbacks,
715  $this->trxEndCallbacks,
716  $this->trxSectionCancelCallbacks
717  ] as $callbacks ) {
718  foreach ( $callbacks as $callback ) {
719  $fnames[] = $callback[1];
720  }
721  }
722 
723  return $fnames;
724  }
725 
729  private function flatAtomicSectionList() {
730  return array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) {
731  return $accum === null ? $v[0] : "$accum, " . $v[0];
732  } );
733  }
734 
735  public function isOpen() {
736  return (bool)$this->conn;
737  }
738 
739  public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
740  if ( $flag & ~static::$DBO_MUTABLE ) {
741  throw new DBUnexpectedError(
742  $this,
743  "Got $flag (allowed: " . implode( ', ', static::$MUTABLE_FLAGS ) . ')'
744  );
745  }
746 
747  if ( $remember === self::REMEMBER_PRIOR ) {
748  array_push( $this->priorFlags, $this->flags );
749  }
750 
751  $this->flags |= $flag;
752  }
753 
754  public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
755  if ( $flag & ~static::$DBO_MUTABLE ) {
756  throw new DBUnexpectedError(
757  $this,
758  "Got $flag (allowed: " . implode( ', ', static::$MUTABLE_FLAGS ) . ')'
759  );
760  }
761 
762  if ( $remember === self::REMEMBER_PRIOR ) {
763  array_push( $this->priorFlags, $this->flags );
764  }
765 
766  $this->flags &= ~$flag;
767  }
768 
769  public function restoreFlags( $state = self::RESTORE_PRIOR ) {
770  if ( !$this->priorFlags ) {
771  return;
772  }
773 
774  if ( $state === self::RESTORE_INITIAL ) {
775  $this->flags = reset( $this->priorFlags );
776  $this->priorFlags = [];
777  } else {
778  $this->flags = array_pop( $this->priorFlags );
779  }
780  }
781 
782  public function getFlag( $flag ) {
783  return ( ( $this->flags & $flag ) === $flag );
784  }
785 
786  public function getDomainID() {
787  return $this->currentDomain->getId();
788  }
789 
797  abstract function indexInfo( $table, $index, $fname = __METHOD__ );
798 
805  abstract function strencode( $s );
806 
810  protected function installErrorHandler() {
811  $this->lastPhpError = false;
812  $this->htmlErrors = ini_set( 'html_errors', '0' );
813  set_error_handler( [ $this, 'connectionErrorLogger' ] );
814  }
815 
821  protected function restoreErrorHandler() {
822  restore_error_handler();
823  if ( $this->htmlErrors !== false ) {
824  ini_set( 'html_errors', $this->htmlErrors );
825  }
826 
827  return $this->getLastPHPError();
828  }
829 
833  protected function getLastPHPError() {
834  if ( $this->lastPhpError ) {
835  $error = preg_replace( '!\[<a.*</a>\]!', '', $this->lastPhpError );
836  $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
837 
838  return $error;
839  }
840 
841  return false;
842  }
843 
851  public function connectionErrorLogger( $errno, $errstr ) {
852  $this->lastPhpError = $errstr;
853  }
854 
861  protected function getLogContext( array $extras = [] ) {
862  return array_merge(
863  [
864  'db_server' => $this->server,
865  'db_name' => $this->getDBname(),
866  'db_user' => $this->user,
867  ],
868  $extras
869  );
870  }
871 
872  final public function close() {
873  $exception = null; // error to throw after disconnecting
874 
875  $wasOpen = (bool)$this->conn;
876  // This should mostly do nothing if the connection is already closed
877  if ( $this->conn ) {
878  // Roll back any dangling transaction first
879  if ( $this->trxLevel() ) {
880  if ( $this->trxAtomicLevels ) {
881  // Cannot let incomplete atomic sections be committed
882  $levels = $this->flatAtomicSectionList();
883  $exception = new DBUnexpectedError(
884  $this,
885  __METHOD__ . ": atomic sections $levels are still open"
886  );
887  } elseif ( $this->trxAutomatic ) {
888  // Only the connection manager can commit non-empty DBO_TRX transactions
889  // (empty ones we can silently roll back)
890  if ( $this->writesOrCallbacksPending() ) {
891  $exception = new DBUnexpectedError(
892  $this,
893  __METHOD__ .
894  ": mass commit/rollback of peer transaction required (DBO_TRX set)"
895  );
896  }
897  } else {
898  // Manual transactions should have been committed or rolled
899  // back, even if empty.
900  $exception = new DBUnexpectedError(
901  $this,
902  __METHOD__ . ": transaction is still open (from {$this->trxFname})"
903  );
904  }
905 
906  if ( $this->trxEndCallbacksSuppressed ) {
907  $exception = $exception ?: new DBUnexpectedError(
908  $this,
909  __METHOD__ . ': callbacks are suppressed; cannot properly commit'
910  );
911  }
912 
913  // Rollback the changes and run any callbacks as needed
914  $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
915  }
916 
917  // Close the actual connection in the binding handle
918  $closed = $this->closeConnection();
919  } else {
920  $closed = true; // already closed; nothing to do
921  }
922 
923  $this->conn = null;
924 
925  // Throw any unexpected errors after having disconnected
926  if ( $exception instanceof Exception ) {
927  throw $exception;
928  }
929 
930  // Note that various subclasses call close() at the start of open(), which itself is
931  // called by replaceLostConnection(). In that case, just because onTransactionResolution()
932  // callbacks are pending does not mean that an exception should be thrown. Rather, they
933  // will be executed after the reconnection step.
934  if ( $wasOpen ) {
935  // Sanity check that no callbacks are dangling
936  $fnames = $this->pendingWriteAndCallbackCallers();
937  if ( $fnames ) {
938  throw new RuntimeException(
939  "Transaction callbacks are still pending: " . implode( ', ', $fnames )
940  );
941  }
942  }
943 
944  return $closed;
945  }
946 
955  final protected function assertHasConnectionHandle() {
956  if ( !$this->isOpen() ) {
957  throw new DBUnexpectedError( $this, "DB connection was already closed" );
958  }
959  }
960 
967  protected function assertIsWritableMaster() {
968  if ( $this->getLBInfo( 'replica' ) === true ) {
969  throw new DBReadOnlyRoleError(
970  $this,
971  'Write operations are not allowed on replica database connections'
972  );
973  }
974  $reason = $this->getReadOnlyReason();
975  if ( $reason !== false ) {
976  throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
977  }
978  }
979 
985  abstract protected function closeConnection();
986 
1016  abstract protected function doQuery( $sql );
1017 
1034  protected function isWriteQuery( $sql ) {
1035  // BEGIN and COMMIT queries are considered read queries here.
1036  // Database backends and drivers (MySQL, MariaDB, php-mysqli) generally
1037  // treat these as write queries, in that their results have "affected rows"
1038  // as meta data as from writes, instead of "num rows" as from reads.
1039  // But, we treat them as read queries because when reading data (from
1040  // either replica or master) we use transactions to enable repeatable-read
1041  // snapshots, which ensures we get consistent results from the same snapshot
1042  // for all queries within a request. Use cases:
1043  // - Treating these as writes would trigger ChronologyProtector (see method doc).
1044  // - We use this method to reject writes to replicas, but we need to allow
1045  // use of transactions on replicas for read snapshots. This is fine given
1046  // that transactions by themselves don't make changes, only actual writes
1047  // within the transaction matter, which we still detect.
1048  return !preg_match(
1049  '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SAVEPOINT|RELEASE|SET|SHOW|EXPLAIN|USE|\(SELECT)\b/i',
1050  $sql
1051  );
1052  }
1053 
1058  protected function getQueryVerb( $sql ) {
1059  return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
1060  }
1061 
1075  protected function isTransactableQuery( $sql ) {
1076  return !in_array(
1077  $this->getQueryVerb( $sql ),
1078  [ 'BEGIN', 'ROLLBACK', 'COMMIT', 'SET', 'SHOW', 'CREATE', 'ALTER', 'USE', 'SHOW' ],
1079  true
1080  );
1081  }
1082 
1091  protected function getTempWrites( $sql, $pseudoPermanent ) {
1092  static $qt = '[`"\']?(\w+)[`"\']?'; // quoted table
1093 
1094  if ( preg_match(
1095  '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?' . $qt . '/i',
1096  $sql,
1097  $matches
1098  ) ) {
1099  $type = $pseudoPermanent ? self::$TEMP_PSEUDO_PERMANENT : self::$TEMP_NORMAL;
1100 
1101  return [ $type, $matches[1], null ];
1102  } elseif ( preg_match(
1103  '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?' . $qt . '/i',
1104  $sql,
1105  $matches
1106  ) ) {
1107  return [ $this->sessionTempTables[$matches[1]] ?? null, null, $matches[1] ];
1108  } elseif ( preg_match(
1109  '/^TRUNCATE\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?' . $qt . '/i',
1110  $sql,
1111  $matches
1112  ) ) {
1113  return [ $this->sessionTempTables[$matches[1]] ?? null, null, null ];
1114  } elseif ( preg_match(
1115  '/^(?:(?:INSERT|REPLACE)\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+' . $qt . '/i',
1116  $sql,
1117  $matches
1118  ) ) {
1119  return [ $this->sessionTempTables[$matches[1]] ?? null, null, null ];
1120  }
1121 
1122  return [ null, null, null ];
1123  }
1124 
1131  protected function registerTempWrites( $ret, $tmpType, $tmpNew, $tmpDel ) {
1132  if ( $ret !== false ) {
1133  if ( $tmpNew !== null ) {
1134  $this->sessionTempTables[$tmpNew] = $tmpType;
1135  }
1136  if ( $tmpDel !== null ) {
1137  unset( $this->sessionTempTables[$tmpDel] );
1138  }
1139  }
1140  }
1141 
1142  public function query( $sql, $fname = __METHOD__, $flags = 0 ) {
1143  $flags = (int)$flags; // b/c; this field used to be a bool
1144  // Sanity check that the SQL query is appropriate in the current context and is
1145  // allowed for an outside caller (e.g. does not break transaction/session tracking).
1146  $this->assertQueryIsCurrentlyAllowed( $sql, $fname );
1147 
1148  // Send the query to the server and fetch any corresponding errors
1149  list( $ret, $err, $errno, $unignorable ) = $this->executeQuery( $sql, $fname, $flags );
1150  if ( $ret === false ) {
1151  $ignoreErrors = $this->hasFlags( $flags, self::QUERY_SILENCE_ERRORS );
1152  // Throw an error unless both the ignore flag was set and a rollback is not needed
1153  $this->reportQueryError( $err, $errno, $sql, $fname, $ignoreErrors && !$unignorable );
1154  }
1155 
1156  return $this->resultObject( $ret );
1157  }
1158 
1179  final protected function executeQuery( $sql, $fname, $flags ) {
1180  $this->assertHasConnectionHandle();
1181 
1182  $priorTransaction = $this->trxLevel();
1183 
1184  if ( $this->isWriteQuery( $sql ) ) {
1185  // In theory, non-persistent writes are allowed in read-only mode, but due to things
1186  // like https://bugs.mysql.com/bug.php?id=33669 that might not work anyway...
1187  $this->assertIsWritableMaster();
1188  // Do not treat temporary table writes as "meaningful writes" since they are only
1189  // visible to one session and are not permanent. Profile them as reads. Integration
1190  // tests can override this behavior via $flags.
1191  $pseudoPermanent = $this->hasFlags( $flags, self::QUERY_PSEUDO_PERMANENT );
1192  list( $tmpType, $tmpNew, $tmpDel ) = $this->getTempWrites( $sql, $pseudoPermanent );
1193  $isPermWrite = ( $tmpType !== self::$TEMP_NORMAL );
1194  // DBConnRef uses QUERY_REPLICA_ROLE to enforce the replica role for raw SQL queries
1195  if ( $isPermWrite && $this->hasFlags( $flags, self::QUERY_REPLICA_ROLE ) ) {
1196  throw new DBReadOnlyRoleError( $this, "Cannot write; target role is DB_REPLICA" );
1197  }
1198  } else {
1199  // No permanent writes in this query
1200  $isPermWrite = false;
1201  // No temporary tables written to either
1202  list( $tmpType, $tmpNew, $tmpDel ) = [ null, null, null ];
1203  }
1204 
1205  // Add trace comment to the begin of the sql string, right after the operator.
1206  // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
1207  $commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
1208 
1209  // Send the query to the server and fetch any corresponding errors.
1210  // This also doubles as a "ping" to see if the connection was dropped.
1211  list( $ret, $err, $errno, $recoverableSR, $recoverableCL, $reconnected ) =
1212  $this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags );
1213 
1214  // Check if the query failed due to a recoverable connection loss
1215  $allowRetry = !$this->hasFlags( $flags, self::QUERY_NO_RETRY );
1216  if ( $ret === false && $recoverableCL && $reconnected && $allowRetry ) {
1217  // Silently resend the query to the server since it is safe and possible
1218  list( $ret, $err, $errno, $recoverableSR, $recoverableCL ) =
1219  $this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags );
1220  }
1221 
1222  // Register creation and dropping of temporary tables
1223  $this->registerTempWrites( $ret, $tmpType, $tmpNew, $tmpDel );
1224 
1225  $corruptedTrx = false;
1226 
1227  if ( $ret === false ) {
1228  if ( $priorTransaction ) {
1229  if ( $recoverableSR ) {
1230  # We're ignoring an error that caused just the current query to be aborted.
1231  # But log the cause so we can log a deprecation notice if a caller actually
1232  # does ignore it.
1233  $this->trxStatusIgnoredCause = [ $err, $errno, $fname ];
1234  } elseif ( !$recoverableCL ) {
1235  # Either the query was aborted or all queries after BEGIN where aborted.
1236  # In the first case, the only options going forward are (a) ROLLBACK, or
1237  # (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
1238  # option is ROLLBACK, since the snapshots would have been released.
1239  $corruptedTrx = true; // cannot recover
1240  $this->trxStatus = self::STATUS_TRX_ERROR;
1241  $this->trxStatusCause =
1242  $this->getQueryExceptionAndLog( $err, $errno, $sql, $fname );
1243  $this->trxStatusIgnoredCause = null;
1244  }
1245  }
1246  }
1247 
1248  return [ $ret, $err, $errno, $corruptedTrx ];
1249  }
1250 
1269  private function executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags ) {
1270  $priorWritesPending = $this->writesOrCallbacksPending();
1271 
1272  if ( ( $flags & self::QUERY_IGNORE_DBO_TRX ) == 0 ) {
1273  $this->beginIfImplied( $sql, $fname );
1274  }
1275 
1276  // Keep track of whether the transaction has write queries pending
1277  if ( $isPermWrite ) {
1278  $this->lastWriteTime = microtime( true );
1279  if ( $this->trxLevel() && !$this->trxDoneWrites ) {
1280  $this->trxDoneWrites = true;
1281  $this->trxProfiler->transactionWritingIn(
1282  $this->server, $this->getDomainID(), $this->trxShortId );
1283  }
1284  }
1285 
1286  $prefix = !is_null( $this->getLBInfo( 'master' ) ) ? 'query-m: ' : 'query: ';
1287  $generalizedSql = new GeneralizedSql( $sql, $this->trxShortId, $prefix );
1288 
1289  $startTime = microtime( true );
1290  $ps = $this->profiler
1291  ? ( $this->profiler )( $generalizedSql->stringify() )
1292  : null;
1293  $this->affectedRowCount = null;
1294  $this->lastQuery = $sql;
1295  $ret = $this->doQuery( $commentedSql );
1296  $lastError = $this->lastError();
1297  $lastErrno = $this->lastErrno();
1298 
1299  $this->affectedRowCount = $this->affectedRows();
1300  unset( $ps ); // profile out (if set)
1301  $queryRuntime = max( microtime( true ) - $startTime, 0.0 );
1302 
1303  $recoverableSR = false; // recoverable statement rollback?
1304  $recoverableCL = false; // recoverable connection loss?
1305  $reconnected = false; // reconnection both attempted and succeeded?
1306 
1307  if ( $ret !== false ) {
1308  $this->lastPing = $startTime;
1309  if ( $isPermWrite && $this->trxLevel() ) {
1310  $this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() );
1311  $this->trxWriteCallers[] = $fname;
1312  }
1313  } elseif ( $this->wasConnectionError( $lastErrno ) ) {
1314  # Check if no meaningful session state was lost
1315  $recoverableCL = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
1316  # Update session state tracking and try to restore the connection
1317  $reconnected = $this->replaceLostConnection( __METHOD__ );
1318  } else {
1319  # Check if only the last query was rolled back
1320  $recoverableSR = $this->wasKnownStatementRollbackError();
1321  }
1322 
1323  if ( $sql === self::$PING_QUERY ) {
1324  $this->lastRoundTripEstimate = $queryRuntime;
1325  }
1326 
1327  $this->trxProfiler->recordQueryCompletion(
1328  $generalizedSql,
1329  $startTime,
1330  $isPermWrite,
1331  $isPermWrite ? $this->affectedRows() : $this->numRows( $ret )
1332  );
1333 
1334  // Avoid the overhead of logging calls unless debug mode is enabled
1335  if ( $this->getFlag( self::DBO_DEBUG ) ) {
1336  $this->queryLogger->debug(
1337  "{method} [{runtime}s] {db_host}: $sql",
1338  [
1339  'method' => $fname,
1340  'db_host' => $this->getServer(),
1341  'domain' => $this->getDomainID(),
1342  'runtime' => round( $queryRuntime, 3 )
1343  ]
1344  );
1345  }
1346 
1347  return [ $ret, $lastError, $lastErrno, $recoverableSR, $recoverableCL, $reconnected ];
1348  }
1349 
1356  private function beginIfImplied( $sql, $fname ) {
1357  if (
1358  !$this->trxLevel() &&
1359  $this->getFlag( self::DBO_TRX ) &&
1360  $this->isTransactableQuery( $sql )
1361  ) {
1362  $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
1363  $this->trxAutomatic = true;
1364  }
1365  }
1366 
1379  private function updateTrxWriteQueryTime( $sql, $runtime, $affected ) {
1380  // Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
1381  $indicativeOfReplicaRuntime = true;
1382  if ( $runtime > self::$SLOW_WRITE_SEC ) {
1383  $verb = $this->getQueryVerb( $sql );
1384  // insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
1385  if ( $verb === 'INSERT' ) {
1386  $indicativeOfReplicaRuntime = $this->affectedRows() > self::$SMALL_WRITE_ROWS;
1387  } elseif ( $verb === 'REPLACE' ) {
1388  $indicativeOfReplicaRuntime = $this->affectedRows() > self::$SMALL_WRITE_ROWS / 2;
1389  }
1390  }
1391 
1392  $this->trxWriteDuration += $runtime;
1393  $this->trxWriteQueryCount += 1;
1394  $this->trxWriteAffectedRows += $affected;
1395  if ( $indicativeOfReplicaRuntime ) {
1396  $this->trxWriteAdjDuration += $runtime;
1397  $this->trxWriteAdjQueryCount += 1;
1398  }
1399  }
1400 
1408  private function assertQueryIsCurrentlyAllowed( $sql, $fname ) {
1409  $verb = $this->getQueryVerb( $sql );
1410  if ( $verb === 'USE' ) {
1411  throw new DBUnexpectedError( $this, "Got USE query; use selectDomain() instead" );
1412  }
1413 
1414  if ( $verb === 'ROLLBACK' ) { // transaction/savepoint
1415  return;
1416  }
1417 
1418  if ( $this->trxStatus < self::STATUS_TRX_OK ) {
1419  throw new DBTransactionStateError(
1420  $this,
1421  "Cannot execute query from $fname while transaction status is ERROR",
1422  [],
1423  $this->trxStatusCause
1424  );
1425  } elseif ( $this->trxStatus === self::STATUS_TRX_OK && $this->trxStatusIgnoredCause ) {
1426  list( $iLastError, $iLastErrno, $iFname ) = $this->trxStatusIgnoredCause;
1427  call_user_func( $this->deprecationLogger,
1428  "Caller from $fname ignored an error originally raised from $iFname: " .
1429  "[$iLastErrno] $iLastError"
1430  );
1431  $this->trxStatusIgnoredCause = null;
1432  }
1433  }
1434 
1435  public function assertNoOpenTransactions() {
1436  if ( $this->explicitTrxActive() ) {
1437  throw new DBTransactionError(
1438  $this,
1439  "Explicit transaction still active. A caller may have caught an error. "
1440  . "Open transactions: " . $this->flatAtomicSectionList()
1441  );
1442  }
1443  }
1444 
1454  private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
1455  # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
1456  # Dropped connections also mean that named locks are automatically released.
1457  # Only allow error suppression in autocommit mode or when the lost transaction
1458  # didn't matter anyway (aside from DBO_TRX snapshot loss).
1459  if ( $this->sessionNamedLocks ) {
1460  return false; // possible critical section violation
1461  } elseif ( $this->sessionTempTables ) {
1462  return false; // tables might be queried latter
1463  } elseif ( $sql === 'COMMIT' ) {
1464  return !$priorWritesPending; // nothing written anyway? (T127428)
1465  } elseif ( $sql === 'ROLLBACK' ) {
1466  return true; // transaction lost...which is also what was requested :)
1467  } elseif ( $this->explicitTrxActive() ) {
1468  return false; // don't drop atomocity and explicit snapshots
1469  } elseif ( $priorWritesPending ) {
1470  return false; // prior writes lost from implicit transaction
1471  }
1472 
1473  return true;
1474  }
1475 
1479  private function handleSessionLossPreconnect() {
1480  // Clean up tracking of session-level things...
1481  // https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html
1482  // https://www.postgresql.org/docs/9.2/static/sql-createtable.html (ignoring ON COMMIT)
1483  $this->sessionTempTables = [];
1484  // https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
1485  // https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
1486  $this->sessionNamedLocks = [];
1487  // Session loss implies transaction loss
1488  $oldTrxShortId = $this->consumeTrxShortId();
1489  $this->trxAtomicCounter = 0;
1490  $this->trxIdleCallbacks = []; // T67263; transaction already lost
1491  $this->trxPreCommitCallbacks = []; // T67263; transaction already lost
1492  // @note: leave trxRecurringCallbacks in place
1493  if ( $this->trxDoneWrites ) {
1494  $this->trxProfiler->transactionWritingOut(
1495  $this->server,
1496  $this->getDomainID(),
1497  $oldTrxShortId,
1498  $this->pendingWriteQueryDuration( self::ESTIMATE_TOTAL ),
1499  $this->trxWriteAffectedRows
1500  );
1501  }
1502  }
1503 
1507  private function handleSessionLossPostconnect() {
1508  try {
1509  // Handle callbacks in trxEndCallbacks, e.g. onTransactionResolution().
1510  // If callback suppression is set then the array will remain unhandled.
1511  $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
1512  } catch ( Exception $ex ) {
1513  // Already logged; move on...
1514  }
1515  try {
1516  // Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener()
1517  $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
1518  } catch ( Exception $ex ) {
1519  // Already logged; move on...
1520  }
1521  }
1522 
1528  private function consumeTrxShortId() {
1529  $old = $this->trxShortId;
1530  $this->trxShortId = '';
1531 
1532  return $old;
1533  }
1534 
1545  protected function wasQueryTimeout( $error, $errno ) {
1546  return false;
1547  }
1548 
1560  public function reportQueryError( $error, $errno, $sql, $fname, $ignore = false ) {
1561  if ( $ignore ) {
1562  $this->queryLogger->debug( "SQL ERROR (ignored): $error" );
1563  } else {
1564  throw $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
1565  }
1566  }
1567 
1575  private function getQueryExceptionAndLog( $error, $errno, $sql, $fname ) {
1576  $this->queryLogger->error(
1577  "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
1578  $this->getLogContext( [
1579  'method' => __METHOD__,
1580  'errno' => $errno,
1581  'error' => $error,
1582  'sql1line' => mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 ),
1583  'fname' => $fname,
1584  'trace' => ( new RuntimeException() )->getTraceAsString()
1585  ] )
1586  );
1587 
1588  if ( $this->wasQueryTimeout( $error, $errno ) ) {
1589  $e = new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
1590  } elseif ( $this->wasConnectionError( $errno ) ) {
1591  $e = new DBQueryDisconnectedError( $this, $error, $errno, $sql, $fname );
1592  } else {
1593  $e = new DBQueryError( $this, $error, $errno, $sql, $fname );
1594  }
1595 
1596  return $e;
1597  }
1598 
1603  final protected function newExceptionAfterConnectError( $error ) {
1604  // Connection was not fully initialized and is not safe for use
1605  $this->conn = null;
1606 
1607  $this->connLogger->error(
1608  "Error connecting to {db_server} as user {db_user}: {error}",
1609  $this->getLogContext( [
1610  'error' => $error,
1611  'trace' => ( new RuntimeException() )->getTraceAsString()
1612  ] )
1613  );
1614 
1615  return new DBConnectionError( $this, $error );
1616  }
1617 
1618  public function freeResult( $res ) {
1619  }
1620 
1621  public function selectField(
1622  $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1623  ) {
1624  if ( $var === '*' ) { // sanity
1625  throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1626  }
1627 
1628  if ( !is_array( $options ) ) {
1629  $options = [ $options ];
1630  }
1631 
1632  $options['LIMIT'] = 1;
1633 
1634  $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1635  if ( $res === false ) {
1636  throw new DBUnexpectedError( $this, "Got false from select()" );
1637  }
1638 
1639  $row = $this->fetchRow( $res );
1640  if ( $row === false ) {
1641  return false;
1642  }
1643 
1644  return reset( $row );
1645  }
1646 
1647  public function selectFieldValues(
1648  $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1649  ) {
1650  if ( $var === '*' ) { // sanity
1651  throw new DBUnexpectedError( $this, "Cannot use a * field" );
1652  } elseif ( !is_string( $var ) ) { // sanity
1653  throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1654  }
1655 
1656  if ( !is_array( $options ) ) {
1657  $options = [ $options ];
1658  }
1659 
1660  $res = $this->select( $table, [ 'value' => $var ], $cond, $fname, $options, $join_conds );
1661  if ( $res === false ) {
1662  throw new DBUnexpectedError( $this, "Got false from select()" );
1663  }
1664 
1665  $values = [];
1666  foreach ( $res as $row ) {
1667  $values[] = $row->value;
1668  }
1669 
1670  return $values;
1671  }
1672 
1682  protected function makeSelectOptions( $options ) {
1683  $preLimitTail = $postLimitTail = '';
1684  $startOpts = '';
1685 
1686  $noKeyOptions = [];
1687 
1688  foreach ( $options as $key => $option ) {
1689  if ( is_numeric( $key ) ) {
1690  $noKeyOptions[$option] = true;
1691  }
1692  }
1693 
1694  $preLimitTail .= $this->makeGroupByWithHaving( $options );
1695 
1696  $preLimitTail .= $this->makeOrderBy( $options );
1697 
1698  if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1699  $postLimitTail .= ' FOR UPDATE';
1700  }
1701 
1702  if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1703  $postLimitTail .= ' LOCK IN SHARE MODE';
1704  }
1705 
1706  if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1707  $startOpts .= 'DISTINCT';
1708  }
1709 
1710  # Various MySQL extensions
1711  if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1712  $startOpts .= ' /*! STRAIGHT_JOIN */';
1713  }
1714 
1715  if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1716  $startOpts .= ' SQL_BIG_RESULT';
1717  }
1718 
1719  if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1720  $startOpts .= ' SQL_BUFFER_RESULT';
1721  }
1722 
1723  if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1724  $startOpts .= ' SQL_SMALL_RESULT';
1725  }
1726 
1727  if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1728  $startOpts .= ' SQL_CALC_FOUND_ROWS';
1729  }
1730 
1731  if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1732  $useIndex = $this->useIndexClause( $options['USE INDEX'] );
1733  } else {
1734  $useIndex = '';
1735  }
1736  if ( isset( $options['IGNORE INDEX'] ) && is_string( $options['IGNORE INDEX'] ) ) {
1737  $ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
1738  } else {
1739  $ignoreIndex = '';
1740  }
1741 
1742  return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1743  }
1744 
1753  protected function makeGroupByWithHaving( $options ) {
1754  $sql = '';
1755  if ( isset( $options['GROUP BY'] ) ) {
1756  $gb = is_array( $options['GROUP BY'] )
1757  ? implode( ',', $options['GROUP BY'] )
1758  : $options['GROUP BY'];
1759  $sql .= ' GROUP BY ' . $gb;
1760  }
1761  if ( isset( $options['HAVING'] ) ) {
1762  $having = is_array( $options['HAVING'] )
1763  ? $this->makeList( $options['HAVING'], self::LIST_AND )
1764  : $options['HAVING'];
1765  $sql .= ' HAVING ' . $having;
1766  }
1767 
1768  return $sql;
1769  }
1770 
1779  protected function makeOrderBy( $options ) {
1780  if ( isset( $options['ORDER BY'] ) ) {
1781  $ob = is_array( $options['ORDER BY'] )
1782  ? implode( ',', $options['ORDER BY'] )
1783  : $options['ORDER BY'];
1784 
1785  return ' ORDER BY ' . $ob;
1786  }
1787 
1788  return '';
1789  }
1790 
1791  public function select(
1792  $table, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1793  ) {
1794  $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1795 
1796  return $this->query( $sql, $fname );
1797  }
1798 
1799  public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1800  $options = [], $join_conds = []
1801  ) {
1802  if ( is_array( $vars ) ) {
1803  $fields = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1804  } else {
1805  $fields = $vars;
1806  }
1807 
1808  $options = (array)$options;
1809  $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1810  ? $options['USE INDEX']
1811  : [];
1812  $ignoreIndexes = (
1813  isset( $options['IGNORE INDEX'] ) &&
1814  is_array( $options['IGNORE INDEX'] )
1815  )
1816  ? $options['IGNORE INDEX']
1817  : [];
1818 
1819  if (
1822  ) {
1823  // Some DB types (postgres/oracle) disallow FOR UPDATE with aggregate
1824  // functions. Discourage use of such queries to encourage compatibility.
1825  call_user_func(
1826  $this->deprecationLogger,
1827  __METHOD__ . ": aggregation used with a locking SELECT ($fname)"
1828  );
1829  }
1830 
1831  if ( is_array( $table ) ) {
1832  $from = ' FROM ' .
1834  $table, $useIndexes, $ignoreIndexes, $join_conds );
1835  } elseif ( $table != '' ) {
1836  $from = ' FROM ' .
1838  [ $table ], $useIndexes, $ignoreIndexes, [] );
1839  } else {
1840  $from = '';
1841  }
1842 
1843  list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
1844  $this->makeSelectOptions( $options );
1845 
1846  if ( is_array( $conds ) ) {
1847  $conds = $this->makeList( $conds, self::LIST_AND );
1848  }
1849 
1850  if ( $conds === null || $conds === false ) {
1851  $this->queryLogger->warning(
1852  __METHOD__
1853  . ' called from '
1854  . $fname
1855  . ' with incorrect parameters: $conds must be a string or an array'
1856  );
1857  $conds = '';
1858  }
1859 
1860  if ( $conds === '' || $conds === '*' ) {
1861  $sql = "SELECT $startOpts $fields $from $useIndex $ignoreIndex $preLimitTail";
1862  } elseif ( is_string( $conds ) ) {
1863  $sql = "SELECT $startOpts $fields $from $useIndex $ignoreIndex " .
1864  "WHERE $conds $preLimitTail";
1865  } else {
1866  throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
1867  }
1868 
1869  if ( isset( $options['LIMIT'] ) ) {
1870  $sql = $this->limitResult( $sql, $options['LIMIT'],
1871  $options['OFFSET'] ?? false );
1872  }
1873  $sql = "$sql $postLimitTail";
1874 
1875  if ( isset( $options['EXPLAIN'] ) ) {
1876  $sql = 'EXPLAIN ' . $sql;
1877  }
1878 
1879  return $sql;
1880  }
1881 
1882  public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1883  $options = [], $join_conds = []
1884  ) {
1885  $options = (array)$options;
1886  $options['LIMIT'] = 1;
1887 
1888  $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1889  if ( $res === false ) {
1890  throw new DBUnexpectedError( $this, "Got false from select()" );
1891  }
1892 
1893  if ( !$this->numRows( $res ) ) {
1894  return false;
1895  }
1896 
1897  return $this->fetchObject( $res );
1898  }
1899 
1900  public function estimateRowCount(
1901  $table, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1902  ) {
1903  $conds = $this->normalizeConditions( $conds, $fname );
1904  $column = $this->extractSingleFieldFromList( $var );
1905  if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
1906  $conds[] = "$column IS NOT NULL";
1907  }
1908 
1909  $res = $this->select(
1910  $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options, $join_conds
1911  );
1912  $row = $res ? $this->fetchRow( $res ) : [];
1913 
1914  return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
1915  }
1916 
1917  public function selectRowCount(
1918  $tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1919  ) {
1920  $conds = $this->normalizeConditions( $conds, $fname );
1921  $column = $this->extractSingleFieldFromList( $var );
1922  if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
1923  $conds[] = "$column IS NOT NULL";
1924  }
1925 
1926  $res = $this->select(
1927  [
1928  'tmp_count' => $this->buildSelectSubquery(
1929  $tables,
1930  '1',
1931  $conds,
1932  $fname,
1933  $options,
1934  $join_conds
1935  )
1936  ],
1937  [ 'rowcount' => 'COUNT(*)' ],
1938  [],
1939  $fname
1940  );
1941  $row = $res ? $this->fetchRow( $res ) : [];
1942 
1943  return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
1944  }
1945 
1951  $options = (array)$options;
1952  foreach ( [ 'FOR UPDATE', 'LOCK IN SHARE MODE' ] as $lock ) {
1953  if ( in_array( $lock, $options, true ) ) {
1954  return true;
1955  }
1956  }
1957 
1958  return false;
1959  }
1960 
1966  private function selectFieldsOrOptionsAggregate( $fields, $options ) {
1967  foreach ( (array)$options as $key => $value ) {
1968  if ( is_string( $key ) ) {
1969  if ( preg_match( '/^(?:GROUP BY|HAVING)$/i', $key ) ) {
1970  return true;
1971  }
1972  } elseif ( is_string( $value ) ) {
1973  if ( preg_match( '/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
1974  return true;
1975  }
1976  }
1977  }
1978 
1979  $regex = '/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
1980  foreach ( (array)$fields as $field ) {
1981  if ( is_string( $field ) && preg_match( $regex, $field ) ) {
1982  return true;
1983  }
1984  }
1985 
1986  return false;
1987  }
1988 
1994  final protected function normalizeConditions( $conds, $fname ) {
1995  if ( $conds === null || $conds === false ) {
1996  $this->queryLogger->warning(
1997  __METHOD__
1998  . ' called from '
1999  . $fname
2000  . ' with incorrect parameters: $conds must be a string or an array'
2001  );
2002  $conds = '';
2003  }
2004 
2005  if ( !is_array( $conds ) ) {
2006  $conds = ( $conds === '' ) ? [] : [ $conds ];
2007  }
2008 
2009  return $conds;
2010  }
2011 
2017  final protected function extractSingleFieldFromList( $var ) {
2018  if ( is_array( $var ) ) {
2019  if ( !$var ) {
2020  $column = null;
2021  } elseif ( count( $var ) == 1 ) {
2022  $column = $var[0] ?? reset( $var );
2023  } else {
2024  throw new DBUnexpectedError( $this, __METHOD__ . ': got multiple columns' );
2025  }
2026  } else {
2027  $column = $var;
2028  }
2029 
2030  return $column;
2031  }
2032 
2033  public function lockForUpdate(
2034  $table, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
2035  ) {
2036  if ( !$this->trxLevel() && !$this->getFlag( self::DBO_TRX ) ) {
2037  throw new DBUnexpectedError(
2038  $this,
2039  __METHOD__ . ': no transaction is active nor is DBO_TRX set'
2040  );
2041  }
2042 
2043  $options = (array)$options;
2044  $options[] = 'FOR UPDATE';
2045 
2046  return $this->selectRowCount( $table, '*', $conds, $fname, $options, $join_conds );
2047  }
2048 
2049  public function fieldExists( $table, $field, $fname = __METHOD__ ) {
2050  $info = $this->fieldInfo( $table, $field );
2051 
2052  return (bool)$info;
2053  }
2054 
2055  public function indexExists( $table, $index, $fname = __METHOD__ ) {
2056  if ( !$this->tableExists( $table ) ) {
2057  return null;
2058  }
2059 
2060  $info = $this->indexInfo( $table, $index, $fname );
2061  if ( is_null( $info ) ) {
2062  return null;
2063  } else {
2064  return $info !== false;
2065  }
2066  }
2067 
2068  abstract public function tableExists( $table, $fname = __METHOD__ );
2069 
2070  public function indexUnique( $table, $index ) {
2071  $indexInfo = $this->indexInfo( $table, $index );
2072 
2073  if ( !$indexInfo ) {
2074  return null;
2075  }
2076 
2077  return !$indexInfo[0]->Non_unique;
2078  }
2079 
2086  protected function makeInsertOptions( $options ) {
2087  return implode( ' ', $options );
2088  }
2089 
2090  public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
2091  # No rows to insert, easy just return now
2092  if ( !count( $a ) ) {
2093  return true;
2094  }
2095 
2096  $table = $this->tableName( $table );
2097 
2098  if ( !is_array( $options ) ) {
2099  $options = [ $options ];
2100  }
2101 
2102  $options = $this->makeInsertOptions( $options );
2103 
2104  if ( isset( $a[0] ) && is_array( $a[0] ) ) {
2105  $multi = true;
2106  $keys = array_keys( $a[0] );
2107  } else {
2108  $multi = false;
2109  $keys = array_keys( $a );
2110  }
2111 
2112  $sql = 'INSERT ' . $options .
2113  " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
2114 
2115  if ( $multi ) {
2116  $first = true;
2117  foreach ( $a as $row ) {
2118  if ( $first ) {
2119  $first = false;
2120  } else {
2121  $sql .= ',';
2122  }
2123  $sql .= '(' . $this->makeList( $row ) . ')';
2124  }
2125  } else {
2126  $sql .= '(' . $this->makeList( $a ) . ')';
2127  }
2128 
2129  $this->query( $sql, $fname );
2130 
2131  return true;
2132  }
2133 
2140  protected function makeUpdateOptionsArray( $options ) {
2141  if ( !is_array( $options ) ) {
2142  $options = [ $options ];
2143  }
2144 
2145  $opts = [];
2146 
2147  if ( in_array( 'IGNORE', $options ) ) {
2148  $opts[] = 'IGNORE';
2149  }
2150 
2151  return $opts;
2152  }
2153 
2160  protected function makeUpdateOptions( $options ) {
2161  $opts = $this->makeUpdateOptionsArray( $options );
2162 
2163  return implode( ' ', $opts );
2164  }
2165 
2166  public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
2167  $table = $this->tableName( $table );
2168  $opts = $this->makeUpdateOptions( $options );
2169  $sql = "UPDATE $opts $table SET " . $this->makeList( $values, self::LIST_SET );
2170 
2171  if ( $conds !== [] && $conds !== '*' ) {
2172  $sql .= " WHERE " . $this->makeList( $conds, self::LIST_AND );
2173  }
2174 
2175  $this->query( $sql, $fname );
2176 
2177  return true;
2178  }
2179 
2180  public function makeList( $a, $mode = self::LIST_COMMA ) {
2181  if ( !is_array( $a ) ) {
2182  throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
2183  }
2184 
2185  $first = true;
2186  $list = '';
2187 
2188  foreach ( $a as $field => $value ) {
2189  if ( !$first ) {
2190  if ( $mode == self::LIST_AND ) {
2191  $list .= ' AND ';
2192  } elseif ( $mode == self::LIST_OR ) {
2193  $list .= ' OR ';
2194  } else {
2195  $list .= ',';
2196  }
2197  } else {
2198  $first = false;
2199  }
2200 
2201  if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
2202  $list .= "($value)";
2203  } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
2204  $list .= "$value";
2205  } elseif (
2206  ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
2207  ) {
2208  // Remove null from array to be handled separately if found
2209  $includeNull = false;
2210  foreach ( array_keys( $value, null, true ) as $nullKey ) {
2211  $includeNull = true;
2212  unset( $value[$nullKey] );
2213  }
2214  if ( count( $value ) == 0 && !$includeNull ) {
2215  throw new InvalidArgumentException(
2216  __METHOD__ . ": empty input for field $field" );
2217  } elseif ( count( $value ) == 0 ) {
2218  // only check if $field is null
2219  $list .= "$field IS NULL";
2220  } else {
2221  // IN clause contains at least one valid element
2222  if ( $includeNull ) {
2223  // Group subconditions to ensure correct precedence
2224  $list .= '(';
2225  }
2226  if ( count( $value ) == 1 ) {
2227  // Special-case single values, as IN isn't terribly efficient
2228  // Don't necessarily assume the single key is 0; we don't
2229  // enforce linear numeric ordering on other arrays here.
2230  $value = array_values( $value )[0];
2231  $list .= $field . " = " . $this->addQuotes( $value );
2232  } else {
2233  $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
2234  }
2235  // if null present in array, append IS NULL
2236  if ( $includeNull ) {
2237  $list .= " OR $field IS NULL)";
2238  }
2239  }
2240  } elseif ( $value === null ) {
2241  if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
2242  $list .= "$field IS ";
2243  } elseif ( $mode == self::LIST_SET ) {
2244  $list .= "$field = ";
2245  }
2246  $list .= 'NULL';
2247  } else {
2248  if (
2249  $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
2250  ) {
2251  $list .= "$field = ";
2252  }
2253  $list .= $mode == self::LIST_NAMES ? $value : $this->addQuotes( $value );
2254  }
2255  }
2256 
2257  return $list;
2258  }
2259 
2260  public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
2261  $conds = [];
2262 
2263  foreach ( $data as $base => $sub ) {
2264  if ( count( $sub ) ) {
2265  $conds[] = $this->makeList(
2266  [ $baseKey => $base, $subKey => array_keys( $sub ) ],
2267  self::LIST_AND );
2268  }
2269  }
2270 
2271  if ( $conds ) {
2272  return $this->makeList( $conds, self::LIST_OR );
2273  } else {
2274  // Nothing to search for...
2275  return false;
2276  }
2277  }
2278 
2279  public function aggregateValue( $valuedata, $valuename = 'value' ) {
2280  return $valuename;
2281  }
2282 
2283  public function bitNot( $field ) {
2284  return "(~$field)";
2285  }
2286 
2287  public function bitAnd( $fieldLeft, $fieldRight ) {
2288  return "($fieldLeft & $fieldRight)";
2289  }
2290 
2291  public function bitOr( $fieldLeft, $fieldRight ) {
2292  return "($fieldLeft | $fieldRight)";
2293  }
2294 
2295  public function buildConcat( $stringList ) {
2296  return 'CONCAT(' . implode( ',', $stringList ) . ')';
2297  }
2298 
2299  public function buildGroupConcatField(
2300  $delim, $table, $field, $conds = '', $join_conds = []
2301  ) {
2302  $fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
2303 
2304  return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
2305  }
2306 
2307  public function buildSubstring( $input, $startPosition, $length = null ) {
2308  $this->assertBuildSubstringParams( $startPosition, $length );
2309  $functionBody = "$input FROM $startPosition";
2310  if ( $length !== null ) {
2311  $functionBody .= " FOR $length";
2312  }
2313  return 'SUBSTRING(' . $functionBody . ')';
2314  }
2315 
2328  protected function assertBuildSubstringParams( $startPosition, $length ) {
2329  if ( !is_int( $startPosition ) || $startPosition <= 0 ) {
2330  throw new InvalidArgumentException(
2331  '$startPosition must be a positive integer'
2332  );
2333  }
2334  if ( !( is_int( $length ) && $length >= 0 || $length === null ) ) {
2335  throw new InvalidArgumentException(
2336  '$length must be null or an integer greater than or equal to 0'
2337  );
2338  }
2339  }
2340 
2341  public function buildStringCast( $field ) {
2342  // In theory this should work for any standards-compliant
2343  // SQL implementation, although it may not be the best way to do it.
2344  return "CAST( $field AS CHARACTER )";
2345  }
2346 
2347  public function buildIntegerCast( $field ) {
2348  return 'CAST( ' . $field . ' AS INTEGER )';
2349  }
2350 
2351  public function buildSelectSubquery(
2352  $table, $vars, $conds = '', $fname = __METHOD__,
2353  $options = [], $join_conds = []
2354  ) {
2355  return new Subquery(
2356  $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
2357  );
2358  }
2359 
2360  public function databasesAreIndependent() {
2361  return false;
2362  }
2363 
2364  final public function selectDB( $db ) {
2365  $this->selectDomain( new DatabaseDomain(
2366  $db,
2367  $this->currentDomain->getSchema(),
2368  $this->currentDomain->getTablePrefix()
2369  ) );
2370 
2371  return true;
2372  }
2373 
2374  final public function selectDomain( $domain ) {
2375  $this->doSelectDomain( DatabaseDomain::newFromId( $domain ) );
2376  }
2377 
2384  protected function doSelectDomain( DatabaseDomain $domain ) {
2385  $this->currentDomain = $domain;
2386  }
2387 
2388  public function getDBname() {
2389  return $this->currentDomain->getDatabase();
2390  }
2391 
2392  public function getServer() {
2393  return $this->server;
2394  }
2395 
2396  public function tableName( $name, $format = 'quoted' ) {
2397  if ( $name instanceof Subquery ) {
2398  throw new DBUnexpectedError(
2399  $this,
2400  __METHOD__ . ': got Subquery instance when expecting a string'
2401  );
2402  }
2403 
2404  # Skip the entire process when we have a string quoted on both ends.
2405  # Note that we check the end so that we will still quote any use of
2406  # use of `database`.table. But won't break things if someone wants
2407  # to query a database table with a dot in the name.
2408  if ( $this->isQuotedIdentifier( $name ) ) {
2409  return $name;
2410  }
2411 
2412  # Lets test for any bits of text that should never show up in a table
2413  # name. Basically anything like JOIN or ON which are actually part of
2414  # SQL queries, but may end up inside of the table value to combine
2415  # sql. Such as how the API is doing.
2416  # Note that we use a whitespace test rather than a \b test to avoid
2417  # any remote case where a word like on may be inside of a table name
2418  # surrounded by symbols which may be considered word breaks.
2419  if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
2420  $this->queryLogger->warning(
2421  __METHOD__ . ": use of subqueries is not supported this way",
2422  [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
2423  );
2424 
2425  return $name;
2426  }
2427 
2428  # Split database and table into proper variables.
2429  list( $database, $schema, $prefix, $table ) = $this->qualifiedTableComponents( $name );
2430 
2431  # Quote $table and apply the prefix if not quoted.
2432  # $tableName might be empty if this is called from Database::replaceVars()
2433  $tableName = "{$prefix}{$table}";
2434  if ( $format === 'quoted'
2435  && !$this->isQuotedIdentifier( $tableName )
2436  && $tableName !== ''
2437  ) {
2438  $tableName = $this->addIdentifierQuotes( $tableName );
2439  }
2440 
2441  # Quote $schema and $database and merge them with the table name if needed
2442  $tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
2443  $tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
2444 
2445  return $tableName;
2446  }
2447 
2454  protected function qualifiedTableComponents( $name ) {
2455  # We reverse the explode so that database.table and table both output the correct table.
2456  $dbDetails = explode( '.', $name, 3 );
2457  if ( count( $dbDetails ) == 3 ) {
2458  list( $database, $schema, $table ) = $dbDetails;
2459  # We don't want any prefix added in this case
2460  $prefix = '';
2461  } elseif ( count( $dbDetails ) == 2 ) {
2462  list( $database, $table ) = $dbDetails;
2463  # We don't want any prefix added in this case
2464  $prefix = '';
2465  # In dbs that support it, $database may actually be the schema
2466  # but that doesn't affect any of the functionality here
2467  $schema = '';
2468  } else {
2469  list( $table ) = $dbDetails;
2470  if ( isset( $this->tableAliases[$table] ) ) {
2471  $database = $this->tableAliases[$table]['dbname'];
2472  $schema = is_string( $this->tableAliases[$table]['schema'] )
2473  ? $this->tableAliases[$table]['schema']
2474  : $this->relationSchemaQualifier();
2475  $prefix = is_string( $this->tableAliases[$table]['prefix'] )
2476  ? $this->tableAliases[$table]['prefix']
2477  : $this->tablePrefix();
2478  } else {
2479  $database = '';
2480  $schema = $this->relationSchemaQualifier(); # Default schema
2481  $prefix = $this->tablePrefix(); # Default prefix
2482  }
2483  }
2484 
2485  return [ $database, $schema, $prefix, $table ];
2486  }
2487 
2494  private function prependDatabaseOrSchema( $namespace, $relation, $format ) {
2495  if ( strlen( $namespace ) ) {
2496  if ( $format === 'quoted' && !$this->isQuotedIdentifier( $namespace ) ) {
2497  $namespace = $this->addIdentifierQuotes( $namespace );
2498  }
2499  $relation = $namespace . '.' . $relation;
2500  }
2501 
2502  return $relation;
2503  }
2504 
2505  public function tableNames() {
2506  $inArray = func_get_args();
2507  $retVal = [];
2508 
2509  foreach ( $inArray as $name ) {
2510  $retVal[$name] = $this->tableName( $name );
2511  }
2512 
2513  return $retVal;
2514  }
2515 
2516  public function tableNamesN() {
2517  $inArray = func_get_args();
2518  $retVal = [];
2519 
2520  foreach ( $inArray as $name ) {
2521  $retVal[] = $this->tableName( $name );
2522  }
2523 
2524  return $retVal;
2525  }
2526 
2538  protected function tableNameWithAlias( $table, $alias = false ) {
2539  if ( is_string( $table ) ) {
2540  $quotedTable = $this->tableName( $table );
2541  } elseif ( $table instanceof Subquery ) {
2542  $quotedTable = (string)$table;
2543  } else {
2544  throw new InvalidArgumentException( "Table must be a string or Subquery" );
2545  }
2546 
2547  if ( $alias === false || $alias === $table ) {
2548  if ( $table instanceof Subquery ) {
2549  throw new InvalidArgumentException( "Subquery table missing alias" );
2550  }
2551 
2552  return $quotedTable;
2553  } else {
2554  return $quotedTable . ' ' . $this->addIdentifierQuotes( $alias );
2555  }
2556  }
2557 
2564  protected function tableNamesWithAlias( $tables ) {
2565  $retval = [];
2566  foreach ( $tables as $alias => $table ) {
2567  if ( is_numeric( $alias ) ) {
2568  $alias = $table;
2569  }
2570  $retval[] = $this->tableNameWithAlias( $table, $alias );
2571  }
2572 
2573  return $retval;
2574  }
2575 
2584  protected function fieldNameWithAlias( $name, $alias = false ) {
2585  if ( !$alias || (string)$alias === (string)$name ) {
2586  return $name;
2587  } else {
2588  return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
2589  }
2590  }
2591 
2598  protected function fieldNamesWithAlias( $fields ) {
2599  $retval = [];
2600  foreach ( $fields as $alias => $field ) {
2601  if ( is_numeric( $alias ) ) {
2602  $alias = $field;
2603  }
2604  $retval[] = $this->fieldNameWithAlias( $field, $alias );
2605  }
2606 
2607  return $retval;
2608  }
2609 
2621  $tables, $use_index = [], $ignore_index = [], $join_conds = []
2622  ) {
2623  $ret = [];
2624  $retJOIN = [];
2625  $use_index = (array)$use_index;
2626  $ignore_index = (array)$ignore_index;
2627  $join_conds = (array)$join_conds;
2628 
2629  foreach ( $tables as $alias => $table ) {
2630  if ( !is_string( $alias ) ) {
2631  // No alias? Set it equal to the table name
2632  $alias = $table;
2633  }
2634 
2635  if ( is_array( $table ) ) {
2636  // A parenthesized group
2637  if ( count( $table ) > 1 ) {
2638  $joinedTable = '(' .
2640  $table, $use_index, $ignore_index, $join_conds ) . ')';
2641  } else {
2642  // Degenerate case
2643  $innerTable = reset( $table );
2644  $innerAlias = key( $table );
2645  $joinedTable = $this->tableNameWithAlias(
2646  $innerTable,
2647  is_string( $innerAlias ) ? $innerAlias : $innerTable
2648  );
2649  }
2650  } else {
2651  $joinedTable = $this->tableNameWithAlias( $table, $alias );
2652  }
2653 
2654  // Is there a JOIN clause for this table?
2655  if ( isset( $join_conds[$alias] ) ) {
2656  list( $joinType, $conds ) = $join_conds[$alias];
2657  $tableClause = $joinType;
2658  $tableClause .= ' ' . $joinedTable;
2659  if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
2660  $use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
2661  if ( $use != '' ) {
2662  $tableClause .= ' ' . $use;
2663  }
2664  }
2665  if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
2666  $ignore = $this->ignoreIndexClause(
2667  implode( ',', (array)$ignore_index[$alias] ) );
2668  if ( $ignore != '' ) {
2669  $tableClause .= ' ' . $ignore;
2670  }
2671  }
2672  $on = $this->makeList( (array)$conds, self::LIST_AND );
2673  if ( $on != '' ) {
2674  $tableClause .= ' ON (' . $on . ')';
2675  }
2676 
2677  $retJOIN[] = $tableClause;
2678  } elseif ( isset( $use_index[$alias] ) ) {
2679  // Is there an INDEX clause for this table?
2680  $tableClause = $joinedTable;
2681  $tableClause .= ' ' . $this->useIndexClause(
2682  implode( ',', (array)$use_index[$alias] )
2683  );
2684 
2685  $ret[] = $tableClause;
2686  } elseif ( isset( $ignore_index[$alias] ) ) {
2687  // Is there an INDEX clause for this table?
2688  $tableClause = $joinedTable;
2689  $tableClause .= ' ' . $this->ignoreIndexClause(
2690  implode( ',', (array)$ignore_index[$alias] )
2691  );
2692 
2693  $ret[] = $tableClause;
2694  } else {
2695  $tableClause = $joinedTable;
2696 
2697  $ret[] = $tableClause;
2698  }
2699  }
2700 
2701  // We can't separate explicit JOIN clauses with ',', use ' ' for those
2702  $implicitJoins = implode( ',', $ret );
2703  $explicitJoins = implode( ' ', $retJOIN );
2704 
2705  // Compile our final table clause
2706  return implode( ' ', [ $implicitJoins, $explicitJoins ] );
2707  }
2708 
2715  protected function indexName( $index ) {
2716  return $this->indexAliases[$index] ?? $index;
2717  }
2718 
2719  public function addQuotes( $s ) {
2720  if ( $s instanceof Blob ) {
2721  $s = $s->fetch();
2722  }
2723  if ( $s === null ) {
2724  return 'NULL';
2725  } elseif ( is_bool( $s ) ) {
2726  return (int)$s;
2727  } else {
2728  # This will also quote numeric values. This should be harmless,
2729  # and protects against weird problems that occur when they really
2730  # _are_ strings such as article titles and string->number->string
2731  # conversion is not 1:1.
2732  return "'" . $this->strencode( $s ) . "'";
2733  }
2734  }
2735 
2736  public function addIdentifierQuotes( $s ) {
2737  return '"' . str_replace( '"', '""', $s ) . '"';
2738  }
2739 
2749  public function isQuotedIdentifier( $name ) {
2750  return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
2751  }
2752 
2758  protected function escapeLikeInternal( $s, $escapeChar = '`' ) {
2759  return str_replace( [ $escapeChar, '%', '_' ],
2760  [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ],
2761  $s );
2762  }
2763 
2764  public function buildLike( $param, ...$params ) {
2765  if ( is_array( $param ) ) {
2766  $params = $param;
2767  } else {
2768  $params = func_get_args();
2769  }
2770 
2771  $s = '';
2772 
2773  // We use ` instead of \ as the default LIKE escape character, since addQuotes()
2774  // may escape backslashes, creating problems of double escaping. The `
2775  // character has good cross-DBMS compatibility, avoiding special operators
2776  // in MS SQL like ^ and %
2777  $escapeChar = '`';
2778 
2779  foreach ( $params as $value ) {
2780  if ( $value instanceof LikeMatch ) {
2781  $s .= $value->toString();
2782  } else {
2783  $s .= $this->escapeLikeInternal( $value, $escapeChar );
2784  }
2785  }
2786 
2787  return ' LIKE ' .
2788  $this->addQuotes( $s ) . ' ESCAPE ' . $this->addQuotes( $escapeChar ) . ' ';
2789  }
2790 
2791  public function anyChar() {
2792  return new LikeMatch( '_' );
2793  }
2794 
2795  public function anyString() {
2796  return new LikeMatch( '%' );
2797  }
2798 
2799  public function nextSequenceValue( $seqName ) {
2800  return null;
2801  }
2802 
2813  public function useIndexClause( $index ) {
2814  return '';
2815  }
2816 
2827  public function ignoreIndexClause( $index ) {
2828  return '';
2829  }
2830 
2831  public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2832  if ( count( $rows ) == 0 ) {
2833  return;
2834  }
2835 
2836  $uniqueIndexes = (array)$uniqueIndexes;
2837  // Single row case
2838  if ( !is_array( reset( $rows ) ) ) {
2839  $rows = [ $rows ];
2840  }
2841 
2842  try {
2843  $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
2844  $affectedRowCount = 0;
2845  foreach ( $rows as $row ) {
2846  // Delete rows which collide with this one
2847  $indexWhereClauses = [];
2848  foreach ( $uniqueIndexes as $index ) {
2849  $indexColumns = (array)$index;
2850  $indexRowValues = array_intersect_key( $row, array_flip( $indexColumns ) );
2851  if ( count( $indexRowValues ) != count( $indexColumns ) ) {
2852  throw new DBUnexpectedError(
2853  $this,
2854  'New record does not provide all values for unique key (' .
2855  implode( ', ', $indexColumns ) . ')'
2856  );
2857  } elseif ( in_array( null, $indexRowValues, true ) ) {
2858  throw new DBUnexpectedError(
2859  $this,
2860  'New record has a null value for unique key (' .
2861  implode( ', ', $indexColumns ) . ')'
2862  );
2863  }
2864  $indexWhereClauses[] = $this->makeList( $indexRowValues, LIST_AND );
2865  }
2866 
2867  if ( $indexWhereClauses ) {
2868  $this->delete( $table, $this->makeList( $indexWhereClauses, LIST_OR ), $fname );
2869  $affectedRowCount += $this->affectedRows();
2870  }
2871 
2872  // Now insert the row
2873  $this->insert( $table, $row, $fname );
2874  $affectedRowCount += $this->affectedRows();
2875  }
2876  $this->endAtomic( $fname );
2877  $this->affectedRowCount = $affectedRowCount;
2878  } catch ( Exception $e ) {
2879  $this->cancelAtomic( $fname );
2880  throw $e;
2881  }
2882  }
2883 
2892  protected function nativeReplace( $table, $rows, $fname ) {
2893  $table = $this->tableName( $table );
2894 
2895  # Single row case
2896  if ( !is_array( reset( $rows ) ) ) {
2897  $rows = [ $rows ];
2898  }
2899 
2900  $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
2901  $first = true;
2902 
2903  foreach ( $rows as $row ) {
2904  if ( $first ) {
2905  $first = false;
2906  } else {
2907  $sql .= ',';
2908  }
2909 
2910  $sql .= '(' . $this->makeList( $row ) . ')';
2911  }
2912 
2913  $this->query( $sql, $fname );
2914  }
2915 
2916  public function upsert( $table, array $rows, $uniqueIndexes, array $set,
2917  $fname = __METHOD__
2918  ) {
2919  if ( $rows === [] ) {
2920  return true; // nothing to do
2921  }
2922 
2923  $uniqueIndexes = (array)$uniqueIndexes;
2924  if ( !is_array( reset( $rows ) ) ) {
2925  $rows = [ $rows ];
2926  }
2927 
2928  if ( count( $uniqueIndexes ) ) {
2929  $clauses = []; // list WHERE clauses that each identify a single row
2930  foreach ( $rows as $row ) {
2931  foreach ( $uniqueIndexes as $index ) {
2932  $index = is_array( $index ) ? $index : [ $index ]; // columns
2933  $rowKey = []; // unique key to this row
2934  foreach ( $index as $column ) {
2935  $rowKey[$column] = $row[$column];
2936  }
2937  $clauses[] = $this->makeList( $rowKey, self::LIST_AND );
2938  }
2939  }
2940  $where = [ $this->makeList( $clauses, self::LIST_OR ) ];
2941  } else {
2942  $where = false;
2943  }
2944 
2945  $affectedRowCount = 0;
2946  try {
2947  $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
2948  # Update any existing conflicting row(s)
2949  if ( $where !== false ) {
2950  $this->update( $table, $set, $where, $fname );
2951  $affectedRowCount += $this->affectedRows();
2952  }
2953  # Now insert any non-conflicting row(s)
2954  $this->insert( $table, $rows, $fname, [ 'IGNORE' ] );
2955  $affectedRowCount += $this->affectedRows();
2956  $this->endAtomic( $fname );
2957  $this->affectedRowCount = $affectedRowCount;
2958  } catch ( Exception $e ) {
2959  $this->cancelAtomic( $fname );
2960  throw $e;
2961  }
2962 
2963  return true;
2964  }
2965 
2966  public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2967  $fname = __METHOD__
2968  ) {
2969  if ( !$conds ) {
2970  throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
2971  }
2972 
2973  $delTable = $this->tableName( $delTable );
2974  $joinTable = $this->tableName( $joinTable );
2975  $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2976  if ( $conds != '*' ) {
2977  $sql .= 'WHERE ' . $this->makeList( $conds, self::LIST_AND );
2978  }
2979  $sql .= ')';
2980 
2981  $this->query( $sql, $fname );
2982  }
2983 
2984  public function textFieldSize( $table, $field ) {
2985  $table = $this->tableName( $table );
2986  $sql = "SHOW COLUMNS FROM $table LIKE \"$field\"";
2987  $res = $this->query( $sql, __METHOD__ );
2988  $row = $this->fetchObject( $res );
2989 
2990  $m = [];
2991 
2992  if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
2993  $size = $m[1];
2994  } else {
2995  $size = -1;
2996  }
2997 
2998  return $size;
2999  }
3000 
3001  public function delete( $table, $conds, $fname = __METHOD__ ) {
3002  if ( !$conds ) {
3003  throw new DBUnexpectedError( $this, __METHOD__ . ' called with no conditions' );
3004  }
3005 
3006  $table = $this->tableName( $table );
3007  $sql = "DELETE FROM $table";
3008 
3009  if ( $conds != '*' ) {
3010  if ( is_array( $conds ) ) {
3011  $conds = $this->makeList( $conds, self::LIST_AND );
3012  }
3013  $sql .= ' WHERE ' . $conds;
3014  }
3015 
3016  $this->query( $sql, $fname );
3017 
3018  return true;
3019  }
3020 
3021  final public function insertSelect(
3022  $destTable, $srcTable, $varMap, $conds,
3023  $fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = []
3024  ) {
3025  static $hints = [ 'NO_AUTO_COLUMNS' ];
3026 
3027  $insertOptions = (array)$insertOptions;
3028  $selectOptions = (array)$selectOptions;
3029 
3030  if ( $this->cliMode && $this->isInsertSelectSafe( $insertOptions, $selectOptions ) ) {
3031  // For massive migrations with downtime, we don't want to select everything
3032  // into memory and OOM, so do all this native on the server side if possible.
3033  $this->nativeInsertSelect(
3034  $destTable,
3035  $srcTable,
3036  $varMap,
3037  $conds,
3038  $fname,
3039  array_diff( $insertOptions, $hints ),
3040  $selectOptions,
3041  $selectJoinConds
3042  );
3043  } else {
3044  $this->nonNativeInsertSelect(
3045  $destTable,
3046  $srcTable,
3047  $varMap,
3048  $conds,
3049  $fname,
3050  array_diff( $insertOptions, $hints ),
3051  $selectOptions,
3052  $selectJoinConds
3053  );
3054  }
3055 
3056  return true;
3057  }
3058 
3065  protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
3066  return true;
3067  }
3068 
3083  protected function nonNativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
3084  $fname = __METHOD__,
3085  $insertOptions = [], $selectOptions = [], $selectJoinConds = []
3086  ) {
3087  // For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
3088  // on only the master (without needing row-based-replication). It also makes it easy to
3089  // know how big the INSERT is going to be.
3090  $fields = [];
3091  foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
3092  $fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
3093  }
3094  $selectOptions[] = 'FOR UPDATE';
3095  $res = $this->select(
3096  $srcTable, implode( ',', $fields ), $conds, $fname, $selectOptions, $selectJoinConds
3097  );
3098  if ( !$res ) {
3099  return;
3100  }
3101 
3102  try {
3103  $affectedRowCount = 0;
3104  $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
3105  $rows = [];
3106  $ok = true;
3107  foreach ( $res as $row ) {
3108  $rows[] = (array)$row;
3109 
3110  // Avoid inserts that are too huge
3111  if ( count( $rows ) >= $this->nonNativeInsertSelectBatchSize ) {
3112  $ok = $this->insert( $destTable, $rows, $fname, $insertOptions );
3113  if ( !$ok ) {
3114  break;
3115  }
3116  $affectedRowCount += $this->affectedRows();
3117  $rows = [];
3118  }
3119  }
3120  if ( $rows && $ok ) {
3121  $ok = $this->insert( $destTable, $rows, $fname, $insertOptions );
3122  if ( $ok ) {
3123  $affectedRowCount += $this->affectedRows();
3124  }
3125  }
3126  if ( $ok ) {
3127  $this->endAtomic( $fname );
3128  $this->affectedRowCount = $affectedRowCount;
3129  } else {
3130  $this->cancelAtomic( $fname );
3131  }
3132  } catch ( Exception $e ) {
3133  $this->cancelAtomic( $fname );
3134  throw $e;
3135  }
3136  }
3137 
3152  protected function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
3153  $fname = __METHOD__,
3154  $insertOptions = [], $selectOptions = [], $selectJoinConds = []
3155  ) {
3156  $destTable = $this->tableName( $destTable );
3157 
3158  if ( !is_array( $insertOptions ) ) {
3159  $insertOptions = [ $insertOptions ];
3160  }
3161 
3162  $insertOptions = $this->makeInsertOptions( $insertOptions );
3163 
3164  $selectSql = $this->selectSQLText(
3165  $srcTable,
3166  array_values( $varMap ),
3167  $conds,
3168  $fname,
3169  $selectOptions,
3170  $selectJoinConds
3171  );
3172 
3173  $sql = "INSERT $insertOptions" .
3174  " INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ') ' .
3175  $selectSql;
3176 
3177  $this->query( $sql, $fname );
3178  }
3179 
3180  public function limitResult( $sql, $limit, $offset = false ) {
3181  if ( !is_numeric( $limit ) ) {
3182  throw new DBUnexpectedError(
3183  $this,
3184  "Invalid non-numeric limit passed to " . __METHOD__
3185  );
3186  }
3187  // This version works in MySQL and SQLite. It will very likely need to be
3188  // overridden for most other RDBMS subclasses.
3189  return "$sql LIMIT "
3190  . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
3191  . "{$limit} ";
3192  }
3193 
3194  public function unionSupportsOrderAndLimit() {
3195  return true; // True for almost every DB supported
3196  }
3197 
3198  public function unionQueries( $sqls, $all ) {
3199  $glue = $all ? ') UNION ALL (' : ') UNION (';
3200 
3201  return '(' . implode( $glue, $sqls ) . ')';
3202  }
3203 
3205  $table, $vars, array $permute_conds, $extra_conds = '', $fname = __METHOD__,
3206  $options = [], $join_conds = []
3207  ) {
3208  // First, build the Cartesian product of $permute_conds
3209  $conds = [ [] ];
3210  foreach ( $permute_conds as $field => $values ) {
3211  if ( !$values ) {
3212  // Skip empty $values
3213  continue;
3214  }
3215  $values = array_unique( $values ); // For sanity
3216  $newConds = [];
3217  foreach ( $conds as $cond ) {
3218  foreach ( $values as $value ) {
3219  $cond[$field] = $value;
3220  $newConds[] = $cond; // Arrays are by-value, not by-reference, so this works
3221  }
3222  }
3223  $conds = $newConds;
3224  }
3225 
3226  $extra_conds = $extra_conds === '' ? [] : (array)$extra_conds;
3227 
3228  // If there's just one condition and no subordering, hand off to
3229  // selectSQLText directly.
3230  if ( count( $conds ) === 1 &&
3231  ( !isset( $options['INNER ORDER BY'] ) || !$this->unionSupportsOrderAndLimit() )
3232  ) {
3233  return $this->selectSQLText(
3234  $table, $vars, $conds[0] + $extra_conds, $fname, $options, $join_conds
3235  );
3236  }
3237 
3238  // Otherwise, we need to pull out the order and limit to apply after
3239  // the union. Then build the SQL queries for each set of conditions in
3240  // $conds. Then union them together (using UNION ALL, because the
3241  // product *should* already be distinct).
3242  $orderBy = $this->makeOrderBy( $options );
3243  $limit = $options['LIMIT'] ?? null;
3244  $offset = $options['OFFSET'] ?? false;
3245  $all = empty( $options['NOTALL'] ) && !in_array( 'NOTALL', $options );
3246  if ( !$this->unionSupportsOrderAndLimit() ) {
3247  unset( $options['ORDER BY'], $options['LIMIT'], $options['OFFSET'] );
3248  } else {
3249  if ( array_key_exists( 'INNER ORDER BY', $options ) ) {
3250  $options['ORDER BY'] = $options['INNER ORDER BY'];
3251  }
3252  if ( $limit !== null && is_numeric( $offset ) && $offset != 0 ) {
3253  // We need to increase the limit by the offset rather than
3254  // using the offset directly, otherwise it'll skip incorrectly
3255  // in the subqueries.
3256  $options['LIMIT'] = $limit + $offset;
3257  unset( $options['OFFSET'] );
3258  }
3259  }
3260 
3261  $sqls = [];
3262  foreach ( $conds as $cond ) {
3263  $sqls[] = $this->selectSQLText(
3264  $table, $vars, $cond + $extra_conds, $fname, $options, $join_conds
3265  );
3266  }
3267  $sql = $this->unionQueries( $sqls, $all ) . $orderBy;
3268  if ( $limit !== null ) {
3269  $sql = $this->limitResult( $sql, $limit, $offset );
3270  }
3271 
3272  return $sql;
3273  }
3274 
3275  public function conditional( $cond, $trueVal, $falseVal ) {
3276  if ( is_array( $cond ) ) {
3277  $cond = $this->makeList( $cond, self::LIST_AND );
3278  }
3279 
3280  return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
3281  }
3282 
3283  public function strreplace( $orig, $old, $new ) {
3284  return "REPLACE({$orig}, {$old}, {$new})";
3285  }
3286 
3287  public function getServerUptime() {
3288  return 0;
3289  }
3290 
3291  public function wasDeadlock() {
3292  return false;
3293  }
3294 
3295  public function wasLockTimeout() {
3296  return false;
3297  }
3298 
3299  public function wasConnectionLoss() {
3300  return $this->wasConnectionError( $this->lastErrno() );
3301  }
3302 
3303  public function wasReadOnlyError() {
3304  return false;
3305  }
3306 
3307  public function wasErrorReissuable() {
3308  return (
3309  $this->wasDeadlock() ||
3310  $this->wasLockTimeout() ||
3311  $this->wasConnectionLoss()
3312  );
3313  }
3314 
3321  public function wasConnectionError( $errno ) {
3322  return false;
3323  }
3324 
3331  protected function wasKnownStatementRollbackError() {
3332  return false; // don't know; it could have caused a transaction rollback
3333  }
3334 
3335  public function deadlockLoop() {
3336  $args = func_get_args();
3337  $function = array_shift( $args );
3338  $tries = self::$DEADLOCK_TRIES;
3339 
3340  $this->begin( __METHOD__ );
3341 
3342  $retVal = null;
3344  $e = null;
3345  do {
3346  try {
3347  $retVal = $function( ...$args );
3348  break;
3349  } catch ( DBQueryError $e ) {
3350  if ( $this->wasDeadlock() ) {
3351  // Retry after a randomized delay
3352  usleep( mt_rand( self::$DEADLOCK_DELAY_MIN, self::$DEADLOCK_DELAY_MAX ) );
3353  } else {
3354  // Throw the error back up
3355  throw $e;
3356  }
3357  }
3358  } while ( --$tries > 0 );
3359 
3360  if ( $tries <= 0 ) {
3361  // Too many deadlocks; give up
3362  $this->rollback( __METHOD__ );
3363  throw $e;
3364  } else {
3365  $this->commit( __METHOD__ );
3366 
3367  return $retVal;
3368  }
3369  }
3370 
3371  public function masterPosWait( DBMasterPos $pos, $timeout ) {
3372  # Real waits are implemented in the subclass.
3373  return 0;
3374  }
3375 
3376  public function getReplicaPos() {
3377  # Stub
3378  return false;
3379  }
3380 
3381  public function getMasterPos() {
3382  # Stub
3383  return false;
3384  }
3385 
3386  public function serverIsReadOnly() {
3387  return false;
3388  }
3389 
3390  final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
3391  if ( !$this->trxLevel() ) {
3392  throw new DBUnexpectedError( $this, "No transaction is active" );
3393  }
3394  $this->trxEndCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3395  }
3396 
3397  final public function onTransactionCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
3398  if ( !$this->trxLevel() && $this->getTransactionRoundId() ) {
3399  // Start an implicit transaction similar to how query() does
3400  $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
3401  $this->trxAutomatic = true;
3402  }
3403 
3404  $this->trxIdleCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3405  if ( !$this->trxLevel() ) {
3406  $this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
3407  }
3408  }
3409 
3410  final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
3411  $this->onTransactionCommitOrIdle( $callback, $fname );
3412  }
3413 
3414  final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
3415  if ( !$this->trxLevel() && $this->getTransactionRoundId() ) {
3416  // Start an implicit transaction similar to how query() does
3417  $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
3418  $this->trxAutomatic = true;
3419  }
3420 
3421  if ( $this->trxLevel() ) {
3422  $this->trxPreCommitCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3423  } else {
3424  // No transaction is active nor will start implicitly, so make one for this callback
3425  $this->startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
3426  try {
3427  $callback( $this );
3428  $this->endAtomic( __METHOD__ );
3429  } catch ( Exception $e ) {
3430  $this->cancelAtomic( __METHOD__ );
3431  throw $e;
3432  }
3433  }
3434  }
3435 
3436  final public function onAtomicSectionCancel( callable $callback, $fname = __METHOD__ ) {
3437  if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
3438  throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)" );
3439  }
3440  $this->trxSectionCancelCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3441  }
3442 
3446  private function currentAtomicSectionId() {
3447  if ( $this->trxLevel() && $this->trxAtomicLevels ) {
3448  $levelInfo = end( $this->trxAtomicLevels );
3449 
3450  return $levelInfo[1];
3451  }
3452 
3453  return null;
3454  }
3455 
3464  ) {
3465  foreach ( $this->trxPreCommitCallbacks as $key => $info ) {
3466  if ( $info[2] === $old ) {
3467  $this->trxPreCommitCallbacks[$key][2] = $new;
3468  }
3469  }
3470  foreach ( $this->trxIdleCallbacks as $key => $info ) {
3471  if ( $info[2] === $old ) {
3472  $this->trxIdleCallbacks[$key][2] = $new;
3473  }
3474  }
3475  foreach ( $this->trxEndCallbacks as $key => $info ) {
3476  if ( $info[2] === $old ) {
3477  $this->trxEndCallbacks[$key][2] = $new;
3478  }
3479  }
3480  foreach ( $this->trxSectionCancelCallbacks as $key => $info ) {
3481  if ( $info[2] === $old ) {
3482  $this->trxSectionCancelCallbacks[$key][2] = $new;
3483  }
3484  }
3485  }
3486 
3506  private function modifyCallbacksForCancel(
3507  array $sectionIds, AtomicSectionIdentifier $newSectionId = null
3508  ) {
3509  // Cancel the "on commit" callbacks owned by this savepoint
3510  $this->trxIdleCallbacks = array_filter(
3511  $this->trxIdleCallbacks,
3512  function ( $entry ) use ( $sectionIds ) {
3513  return !in_array( $entry[2], $sectionIds, true );
3514  }
3515  );
3516  $this->trxPreCommitCallbacks = array_filter(
3517  $this->trxPreCommitCallbacks,
3518  function ( $entry ) use ( $sectionIds ) {
3519  return !in_array( $entry[2], $sectionIds, true );
3520  }
3521  );
3522  // Make "on resolution" callbacks owned by this savepoint to perceive a rollback
3523  foreach ( $this->trxEndCallbacks as $key => $entry ) {
3524  if ( in_array( $entry[2], $sectionIds, true ) ) {
3525  $callback = $entry[0];
3526  $this->trxEndCallbacks[$key][0] = function () use ( $callback ) {
3527  // @phan-suppress-next-line PhanInfiniteRecursion No recursion at all here, phan is confused
3528  return $callback( self::TRIGGER_ROLLBACK, $this );
3529  };
3530  // This "on resolution" callback no longer belongs to a section.
3531  $this->trxEndCallbacks[$key][2] = null;
3532  }
3533  }
3534  // Hoist callback ownership for section cancel callbacks to the new top section
3535  foreach ( $this->trxSectionCancelCallbacks as $key => $entry ) {
3536  if ( in_array( $entry[2], $sectionIds, true ) ) {
3537  $this->trxSectionCancelCallbacks[$key][2] = $newSectionId;
3538  }
3539  }
3540  }
3541 
3542  final public function setTransactionListener( $name, callable $callback = null ) {
3543  if ( $callback ) {
3544  $this->trxRecurringCallbacks[$name] = $callback;
3545  } else {
3546  unset( $this->trxRecurringCallbacks[$name] );
3547  }
3548  }
3549 
3558  final public function setTrxEndCallbackSuppression( $suppress ) {
3559  $this->trxEndCallbacksSuppressed = $suppress;
3560  }
3561 
3572  public function runOnTransactionIdleCallbacks( $trigger ) {
3573  if ( $this->trxLevel() ) { // sanity
3574  throw new DBUnexpectedError( $this, __METHOD__ . ': a transaction is still open' );
3575  }
3576 
3577  if ( $this->trxEndCallbacksSuppressed ) {
3578  return 0;
3579  }
3580 
3581  $count = 0;
3582  $autoTrx = $this->getFlag( self::DBO_TRX ); // automatic begin() enabled?
3584  $e = null; // first exception
3585  do { // callbacks may add callbacks :)
3586  $callbacks = array_merge(
3587  $this->trxIdleCallbacks,
3588  $this->trxEndCallbacks // include "transaction resolution" callbacks
3589  );
3590  $this->trxIdleCallbacks = []; // consumed (and recursion guard)
3591  $this->trxEndCallbacks = []; // consumed (recursion guard)
3592 
3593  // Only run trxSectionCancelCallbacks on rollback, not commit.
3594  // But always consume them.
3595  if ( $trigger === self::TRIGGER_ROLLBACK ) {
3596  $callbacks = array_merge( $callbacks, $this->trxSectionCancelCallbacks );
3597  }
3598  $this->trxSectionCancelCallbacks = []; // consumed (recursion guard)
3599 
3600  foreach ( $callbacks as $callback ) {
3601  ++$count;
3602  list( $phpCallback ) = $callback;
3603  $this->clearFlag( self::DBO_TRX ); // make each query its own transaction
3604  try {
3605  // @phan-suppress-next-line PhanParamTooManyCallable
3606  call_user_func( $phpCallback, $trigger, $this );
3607  } catch ( Exception $ex ) {
3608  call_user_func( $this->errorLogger, $ex );
3609  $e = $e ?: $ex;
3610  // Some callbacks may use startAtomic/endAtomic, so make sure
3611  // their transactions are ended so other callbacks don't fail
3612  if ( $this->trxLevel() ) {
3613  $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
3614  }
3615  } finally {
3616  if ( $autoTrx ) {
3617  $this->setFlag( self::DBO_TRX ); // restore automatic begin()
3618  } else {
3619  $this->clearFlag( self::DBO_TRX ); // restore auto-commit
3620  }
3621  }
3622  }
3623  } while ( count( $this->trxIdleCallbacks ) );
3624 
3625  if ( $e instanceof Exception ) {
3626  throw $e; // re-throw any first exception
3627  }
3628 
3629  return $count;
3630  }
3631 
3642  $count = 0;
3643 
3644  $e = null; // first exception
3645  do { // callbacks may add callbacks :)
3646  $callbacks = $this->trxPreCommitCallbacks;
3647  $this->trxPreCommitCallbacks = []; // consumed (and recursion guard)
3648  foreach ( $callbacks as $callback ) {
3649  try {
3650  ++$count;
3651  list( $phpCallback ) = $callback;
3652  $phpCallback( $this );
3653  } catch ( Exception $ex ) {
3654  ( $this->errorLogger )( $ex );
3655  $e = $e ?: $ex;
3656  }
3657  }
3658  } while ( count( $this->trxPreCommitCallbacks ) );
3659 
3660  if ( $e instanceof Exception ) {
3661  throw $e; // re-throw any first exception
3662  }
3663 
3664  return $count;
3665  }
3666 
3675  $trigger, array $sectionIds = null
3676  ) {
3678  $e = null; // first exception
3679 
3680  $notCancelled = [];
3681  do {
3682  $callbacks = $this->trxSectionCancelCallbacks;
3683  $this->trxSectionCancelCallbacks = []; // consumed (recursion guard)
3684  foreach ( $callbacks as $entry ) {
3685  if ( $sectionIds === null || in_array( $entry[2], $sectionIds, true ) ) {
3686  try {
3687  $entry[0]( $trigger, $this );
3688  } catch ( Exception $ex ) {
3689  ( $this->errorLogger )( $ex );
3690  $e = $e ?: $ex;
3691  } catch ( Throwable $ex ) {
3692  // @todo: Log?
3693  $e = $e ?: $ex;
3694  }
3695  } else {
3696  $notCancelled[] = $entry;
3697  }
3698  }
3699  } while ( count( $this->trxSectionCancelCallbacks ) );
3700  $this->trxSectionCancelCallbacks = $notCancelled;
3701 
3702  if ( $e !== null ) {
3703  throw $e; // re-throw any first Exception/Throwable
3704  }
3705  }
3706 
3716  public function runTransactionListenerCallbacks( $trigger ) {
3717  if ( $this->trxEndCallbacksSuppressed ) {
3718  return;
3719  }
3720 
3722  $e = null; // first exception
3723 
3724  foreach ( $this->trxRecurringCallbacks as $phpCallback ) {
3725  try {
3726  $phpCallback( $trigger, $this );
3727  } catch ( Exception $ex ) {
3728  ( $this->errorLogger )( $ex );
3729  $e = $e ?: $ex;
3730  }
3731  }
3732 
3733  if ( $e instanceof Exception ) {
3734  throw $e; // re-throw any first exception
3735  }
3736  }
3737 
3748  protected function doSavepoint( $identifier, $fname ) {
3749  $this->query( 'SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
3750  }
3751 
3762  protected function doReleaseSavepoint( $identifier, $fname ) {
3763  $this->query( 'RELEASE SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
3764  }
3765 
3776  protected function doRollbackToSavepoint( $identifier, $fname ) {
3777  $this->query( 'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
3778  }
3779 
3784  private function nextSavepointId( $fname ) {
3785  $savepointId = self::$SAVEPOINT_PREFIX . ++$this->trxAtomicCounter;
3786  if ( strlen( $savepointId ) > 30 ) {
3787  // 30 == Oracle's identifier length limit (pre 12c)
3788  // With a 22 character prefix, that puts the highest number at 99999999.
3789  throw new DBUnexpectedError(
3790  $this,
3791  'There have been an excessively large number of atomic sections in a transaction'
3792  . " started by $this->trxFname (at $fname)"
3793  );
3794  }
3795 
3796  return $savepointId;
3797  }
3798 
3799  final public function startAtomic(
3800  $fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE
3801  ) {
3802  $savepointId = $cancelable === self::ATOMIC_CANCELABLE ? self::$NOT_APPLICABLE : null;
3803 
3804  if ( !$this->trxLevel() ) {
3805  $this->begin( $fname, self::TRANSACTION_INTERNAL ); // sets trxAutomatic
3806  // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
3807  // in all changes being in one transaction to keep requests transactional.
3808  if ( $this->getFlag( self::DBO_TRX ) ) {
3809  // Since writes could happen in between the topmost atomic sections as part
3810  // of the transaction, those sections will need savepoints.
3811  $savepointId = $this->nextSavepointId( $fname );
3812  $this->doSavepoint( $savepointId, $fname );
3813  } else {
3814  $this->trxAutomaticAtomic = true;
3815  }
3816  } elseif ( $cancelable === self::ATOMIC_CANCELABLE ) {
3817  $savepointId = $this->nextSavepointId( $fname );
3818  $this->doSavepoint( $savepointId, $fname );
3819  }
3820 
3821  $sectionId = new AtomicSectionIdentifier;
3822  $this->trxAtomicLevels[] = [ $fname, $sectionId, $savepointId ];
3823  $this->queryLogger->debug( 'startAtomic: entering level ' .
3824  ( count( $this->trxAtomicLevels ) - 1 ) . " ($fname)" );
3825 
3826  return $sectionId;
3827  }
3828 
3829  final public function endAtomic( $fname = __METHOD__ ) {
3830  if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
3831  throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)" );
3832  }
3833 
3834  // Check if the current section matches $fname
3835  $pos = count( $this->trxAtomicLevels ) - 1;
3836  list( $savedFname, $sectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
3837  $this->queryLogger->debug( "endAtomic: leaving level $pos ($fname)" );
3838 
3839  if ( $savedFname !== $fname ) {
3840  throw new DBUnexpectedError(
3841  $this,
3842  "Invalid atomic section ended (got $fname but expected $savedFname)"
3843  );
3844  }
3845 
3846  // Remove the last section (no need to re-index the array)
3847  array_pop( $this->trxAtomicLevels );
3848 
3849  if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
3850  $this->commit( $fname, self::FLUSHING_INTERNAL );
3851  } elseif ( $savepointId !== null && $savepointId !== self::$NOT_APPLICABLE ) {
3852  $this->doReleaseSavepoint( $savepointId, $fname );
3853  }
3854 
3855  // Hoist callback ownership for callbacks in the section that just ended;
3856  // all callbacks should have an owner that is present in trxAtomicLevels.
3857  $currentSectionId = $this->currentAtomicSectionId();
3858  if ( $currentSectionId ) {
3859  $this->reassignCallbacksForSection( $sectionId, $currentSectionId );
3860  }
3861  }
3862 
3863  final public function cancelAtomic(
3864  $fname = __METHOD__, AtomicSectionIdentifier $sectionId = null
3865  ) {
3866  if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
3867  throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)" );
3868  }
3869 
3870  $excisedIds = [];
3871  $newTopSection = $this->currentAtomicSectionId();
3872  try {
3873  $excisedFnames = [];
3874  if ( $sectionId !== null ) {
3875  // Find the (last) section with the given $sectionId
3876  $pos = -1;
3877  foreach ( $this->trxAtomicLevels as $i => list( $asFname, $asId, $spId ) ) {
3878  if ( $asId === $sectionId ) {
3879  $pos = $i;
3880  }
3881  }
3882  if ( $pos < 0 ) {
3883  throw new DBUnexpectedError( $this, "Atomic section not found (for $fname)" );
3884  }
3885  // Remove all descendant sections and re-index the array
3886  $len = count( $this->trxAtomicLevels );
3887  for ( $i = $pos + 1; $i < $len; ++$i ) {
3888  $excisedFnames[] = $this->trxAtomicLevels[$i][0];
3889  $excisedIds[] = $this->trxAtomicLevels[$i][1];
3890  }
3891  $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 );
3892  $newTopSection = $this->currentAtomicSectionId();
3893  }
3894 
3895  // Check if the current section matches $fname
3896  $pos = count( $this->trxAtomicLevels ) - 1;
3897  list( $savedFname, $savedSectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
3898 
3899  if ( $excisedFnames ) {
3900  $this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname) " .
3901  "and descendants " . implode( ', ', $excisedFnames ) );
3902  } else {
3903  $this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname)" );
3904  }
3905 
3906  if ( $savedFname !== $fname ) {
3907  throw new DBUnexpectedError(
3908  $this,
3909  "Invalid atomic section ended (got $fname but expected $savedFname)"
3910  );
3911  }
3912 
3913  // Remove the last section (no need to re-index the array)
3914  array_pop( $this->trxAtomicLevels );
3915  $excisedIds[] = $savedSectionId;
3916  $newTopSection = $this->currentAtomicSectionId();
3917 
3918  if ( $savepointId !== null ) {
3919  // Rollback the transaction to the state just before this atomic section
3920  if ( $savepointId === self::$NOT_APPLICABLE ) {
3921  $this->rollback( $fname, self::FLUSHING_INTERNAL );
3922  // Note: rollback() will run trxSectionCancelCallbacks
3923  } else {
3924  $this->doRollbackToSavepoint( $savepointId, $fname );
3925  $this->trxStatus = self::STATUS_TRX_OK; // no exception; recovered
3926  $this->trxStatusIgnoredCause = null;
3927 
3928  // Run trxSectionCancelCallbacks now.
3929  $this->runOnAtomicSectionCancelCallbacks( self::TRIGGER_CANCEL, $excisedIds );
3930  }
3931  } elseif ( $this->trxStatus > self::STATUS_TRX_ERROR ) {
3932  // Put the transaction into an error state if it's not already in one
3933  $this->trxStatus = self::STATUS_TRX_ERROR;
3934  $this->trxStatusCause = new DBUnexpectedError(
3935  $this,
3936  "Uncancelable atomic section canceled (got $fname)"
3937  );
3938  }
3939  } finally {
3940  // Fix up callbacks owned by the sections that were just cancelled.
3941  // All callbacks should have an owner that is present in trxAtomicLevels.
3942  $this->modifyCallbacksForCancel( $excisedIds, $newTopSection );
3943  }
3944 
3945  $this->affectedRowCount = 0; // for the sake of consistency
3946  }
3947 
3948  final public function doAtomicSection(
3949  $fname, callable $callback, $cancelable = self::ATOMIC_NOT_CANCELABLE
3950  ) {
3951  $sectionId = $this->startAtomic( $fname, $cancelable );
3952  try {
3953  $res = $callback( $this, $fname );
3954  } catch ( Exception $e ) {
3955  $this->cancelAtomic( $fname, $sectionId );
3956 
3957  throw $e;
3958  }
3959  $this->endAtomic( $fname );
3960 
3961  return $res;
3962  }
3963 
3964  final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
3965  static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
3966  if ( !in_array( $mode, $modes, true ) ) {
3967  throw new DBUnexpectedError( $this, "$fname: invalid mode parameter '$mode'" );
3968  }
3969 
3970  // Protect against mismatched atomic section, transaction nesting, and snapshot loss
3971  if ( $this->trxLevel() ) {
3972  if ( $this->trxAtomicLevels ) {
3973  $levels = $this->flatAtomicSectionList();
3974  $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open";
3975  throw new DBUnexpectedError( $this, $msg );
3976  } elseif ( !$this->trxAutomatic ) {
3977  $msg = "$fname: Explicit transaction already active (from {$this->trxFname})";
3978  throw new DBUnexpectedError( $this, $msg );
3979  } else {
3980  $msg = "$fname: Implicit transaction already active (from {$this->trxFname})";
3981  throw new DBUnexpectedError( $this, $msg );
3982  }
3983  } elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
3984  $msg = "$fname: Implicit transaction expected (DBO_TRX set)";
3985  throw new DBUnexpectedError( $this, $msg );
3986  }
3987 
3988  $this->assertHasConnectionHandle();
3989 
3990  $this->doBegin( $fname );
3991  $this->trxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
3992  $this->trxStatus = self::STATUS_TRX_OK;
3993  $this->trxStatusIgnoredCause = null;
3994  $this->trxAtomicCounter = 0;
3995  $this->trxTimestamp = microtime( true );
3996  $this->trxFname = $fname;
3997  $this->trxDoneWrites = false;
3998  $this->trxAutomaticAtomic = false;
3999  $this->trxAtomicLevels = [];
4000  $this->trxWriteDuration = 0.0;
4001  $this->trxWriteQueryCount = 0;
4002  $this->trxWriteAffectedRows = 0;
4003  $this->trxWriteAdjDuration = 0.0;
4004  $this->trxWriteAdjQueryCount = 0;
4005  $this->trxWriteCallers = [];
4006  // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
4007  // Get an estimate of the replication lag before any such queries.
4008  $this->trxReplicaLag = null; // clear cached value first
4009  $this->trxReplicaLag = $this->getApproximateLagStatus()['lag'];
4010  // T147697: make explicitTrxActive() return true until begin() finishes. This way, no
4011  // caller will think its OK to muck around with the transaction just because startAtomic()
4012  // has not yet completed (e.g. setting trxAtomicLevels).
4013  $this->trxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
4014  }
4015 
4023  protected function doBegin( $fname ) {
4024  $this->query( 'BEGIN', $fname );
4025  }
4026 
4027  final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4028  static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
4029  if ( !in_array( $flush, $modes, true ) ) {
4030  throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'" );
4031  }
4032 
4033  if ( $this->trxLevel() && $this->trxAtomicLevels ) {
4034  // There are still atomic sections open; this cannot be ignored
4035  $levels = $this->flatAtomicSectionList();
4036  throw new DBUnexpectedError(
4037  $this,
4038  "$fname: Got COMMIT while atomic sections $levels are still open"
4039  );
4040  }
4041 
4042  if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
4043  if ( !$this->trxLevel() ) {
4044  return; // nothing to do
4045  } elseif ( !$this->trxAutomatic ) {
4046  throw new DBUnexpectedError(
4047  $this,
4048  "$fname: Flushing an explicit transaction, getting out of sync"
4049  );
4050  }
4051  } elseif ( !$this->trxLevel() ) {
4052  $this->queryLogger->error(
4053  "$fname: No transaction to commit, something got out of sync" );
4054  return; // nothing to do
4055  } elseif ( $this->trxAutomatic ) {
4056  throw new DBUnexpectedError(
4057  $this,
4058  "$fname: Expected mass commit of all peer transactions (DBO_TRX set)"
4059  );
4060  }
4061 
4062  $this->assertHasConnectionHandle();
4063 
4065 
4066  $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
4067  $this->doCommit( $fname );
4068  $oldTrxShortId = $this->consumeTrxShortId();
4069  $this->trxStatus = self::STATUS_TRX_NONE;
4070 
4071  if ( $this->trxDoneWrites ) {
4072  $this->lastWriteTime = microtime( true );
4073  $this->trxProfiler->transactionWritingOut(
4074  $this->server,
4075  $this->getDomainID(),
4076  $oldTrxShortId,
4077  $writeTime,
4078  $this->trxWriteAffectedRows
4079  );
4080  }
4081 
4082  // With FLUSHING_ALL_PEERS, callbacks will be explicitly run later
4083  if ( $flush !== self::FLUSHING_ALL_PEERS ) {
4084  $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
4085  $this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
4086  }
4087  }
4088 
4096  protected function doCommit( $fname ) {
4097  if ( $this->trxLevel() ) {
4098  $this->query( 'COMMIT', $fname );
4099  }
4100  }
4101 
4102  final public function rollback( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4103  $trxActive = $this->trxLevel();
4104 
4105  if ( $flush !== self::FLUSHING_INTERNAL
4106  && $flush !== self::FLUSHING_ALL_PEERS
4107  && $this->getFlag( self::DBO_TRX )
4108  ) {
4109  throw new DBUnexpectedError(
4110  $this,
4111  "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)"
4112  );
4113  }
4114 
4115  if ( $trxActive ) {
4116  $this->assertHasConnectionHandle();
4117 
4118  $this->doRollback( $fname );
4119  $oldTrxShortId = $this->consumeTrxShortId();
4120  $this->trxStatus = self::STATUS_TRX_NONE;
4121  $this->trxAtomicLevels = [];
4122  // Estimate the RTT via a query now that trxStatus is OK
4123  $writeTime = $this->pingAndCalculateLastTrxApplyTime();
4124 
4125  if ( $this->trxDoneWrites ) {
4126  $this->trxProfiler->transactionWritingOut(
4127  $this->server,
4128  $this->getDomainID(),
4129  $oldTrxShortId,
4130  $writeTime,
4131  $this->trxWriteAffectedRows
4132  );
4133  }
4134  }
4135 
4136  // Clear any commit-dependant callbacks. They might even be present
4137  // only due to transaction rounds, with no SQL transaction being active
4138  $this->trxIdleCallbacks = [];
4139  $this->trxPreCommitCallbacks = [];
4140 
4141  // With FLUSHING_ALL_PEERS, callbacks will be explicitly run later
4142  if ( $trxActive && $flush !== self::FLUSHING_ALL_PEERS ) {
4143  try {
4144  $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
4145  } catch ( Exception $e ) {
4146  // already logged; finish and let LoadBalancer move on during mass-rollback
4147  }
4148  try {
4149  $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
4150  } catch ( Exception $e ) {
4151  // already logged; let LoadBalancer move on during mass-rollback
4152  }
4153 
4154  $this->affectedRowCount = 0; // for the sake of consistency
4155  }
4156  }
4157 
4165  protected function doRollback( $fname ) {
4166  if ( $this->trxLevel() ) {
4167  # Disconnects cause rollback anyway, so ignore those errors
4168  $ignoreErrors = true;
4169  $this->query( 'ROLLBACK', $fname, $ignoreErrors );
4170  }
4171  }
4172 
4173  public function flushSnapshot( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4174  if ( $this->explicitTrxActive() ) {
4175  // Committing this transaction would break callers that assume it is still open
4176  throw new DBUnexpectedError(
4177  $this,
4178  "$fname: Cannot flush snapshot; " .
4179  "explicit transaction '{$this->trxFname}' is still open"
4180  );
4181  } elseif ( $this->writesOrCallbacksPending() ) {
4182  // This only flushes transactions to clear snapshots, not to write data
4183  $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
4184  throw new DBUnexpectedError(
4185  $this,
4186  "$fname: Cannot flush snapshot; " .
4187  "writes from transaction {$this->trxFname} are still pending ($fnames)"
4188  );
4189  } elseif (
4190  $this->trxLevel() &&
4191  $this->getTransactionRoundId() &&
4192  $flush !== self::FLUSHING_INTERNAL &&
4193  $flush !== self::FLUSHING_ALL_PEERS
4194  ) {
4195  $this->queryLogger->warning(
4196  "$fname: Expected mass snapshot flush of all peer transactions " .
4197  "in the explicit transactions round '{$this->getTransactionRoundId()}'",
4198  [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
4199  );
4200  }
4201 
4202  $this->commit( $fname, self::FLUSHING_INTERNAL );
4203  }
4204 
4205  public function explicitTrxActive() {
4206  return $this->trxLevel() && ( $this->trxAtomicLevels || !$this->trxAutomatic );
4207  }
4208 
4209  public function duplicateTableStructure(
4210  $oldName, $newName, $temporary = false, $fname = __METHOD__
4211  ) {
4212  throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
4213  }
4214 
4215  public function listTables( $prefix = null, $fname = __METHOD__ ) {
4216  throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
4217  }
4218 
4219  public function listViews( $prefix = null, $fname = __METHOD__ ) {
4220  throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
4221  }
4222 
4223  public function timestamp( $ts = 0 ) {
4224  $t = new ConvertibleTimestamp( $ts );
4225  // Let errors bubble up to avoid putting garbage in the DB
4226  return $t->getTimestamp( TS_MW );
4227  }
4228 
4229  public function timestampOrNull( $ts = null ) {
4230  if ( is_null( $ts ) ) {
4231  return null;
4232  } else {
4233  return $this->timestamp( $ts );
4234  }
4235  }
4236 
4237  public function affectedRows() {
4238  return ( $this->affectedRowCount === null )
4239  ? $this->fetchAffectedRowCount() // default to driver value
4241  }
4242 
4246  abstract protected function fetchAffectedRowCount();
4247 
4260  protected function resultObject( $result ) {
4261  if ( !$result ) {
4262  return false; // failed query
4263  } elseif ( $result instanceof IResultWrapper ) {
4264  return $result;
4265  } elseif ( $result === true ) {
4266  return $result; // succesful write query
4267  } else {
4268  return new ResultWrapper( $this, $result );
4269  }
4270  }
4271 
4272  public function ping( &$rtt = null ) {
4273  // Avoid hitting the server if it was hit recently
4274  if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::$PING_TTL ) {
4275  if ( !func_num_args() || $this->lastRoundTripEstimate > 0 ) {
4277  return true; // don't care about $rtt
4278  }
4279  }
4280 
4281  // This will reconnect if possible or return false if not
4282  $this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
4283  $ok = ( $this->query( self::$PING_QUERY, __METHOD__, true ) !== false );
4284  $this->restoreFlags( self::RESTORE_PRIOR );
4285 
4286  if ( $ok ) {
4288  }
4289 
4290  return $ok;
4291  }
4292 
4299  protected function replaceLostConnection( $fname ) {
4300  $this->closeConnection();
4301  $this->conn = null;
4302 
4303  $this->handleSessionLossPreconnect();
4304 
4305  try {
4306  $this->open(
4307  $this->server,
4308  $this->user,
4309  $this->password,
4310  $this->currentDomain->getDatabase(),
4311  $this->currentDomain->getSchema(),
4312  $this->tablePrefix()
4313  );
4314  $this->lastPing = microtime( true );
4315  $ok = true;
4316 
4317  $this->connLogger->warning(
4318  $fname . ': lost connection to {dbserver}; reconnected',
4319  [
4320  'dbserver' => $this->getServer(),
4321  'trace' => ( new RuntimeException() )->getTraceAsString()
4322  ]
4323  );
4324  } catch ( DBConnectionError $e ) {
4325  $ok = false;
4326 
4327  $this->connLogger->error(
4328  $fname . ': lost connection to {dbserver} permanently',
4329  [ 'dbserver' => $this->getServer() ]
4330  );
4331  }
4332 
4334 
4335  return $ok;
4336  }
4337 
4338  public function getSessionLagStatus() {
4339  return $this->getRecordedTransactionLagStatus() ?: $this->getApproximateLagStatus();
4340  }
4341 
4355  final protected function getRecordedTransactionLagStatus() {
4356  return ( $this->trxLevel() && $this->trxReplicaLag !== null )
4357  ? [ 'lag' => $this->trxReplicaLag, 'since' => $this->trxTimestamp() ]
4358  : null;
4359  }
4360 
4367  protected function getApproximateLagStatus() {
4368  return [
4369  'lag' => $this->getLBInfo( 'replica' ) ? $this->getLag() : 0,
4370  'since' => microtime( true )
4371  ];
4372  }
4373 
4393  public static function getCacheSetOptions( IDatabase $db1, IDatabase $db2 = null ) {
4394  $res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
4395  foreach ( func_get_args() as $db ) {
4397  $status = $db->getSessionLagStatus();
4398  if ( $status['lag'] === false ) {
4399  $res['lag'] = false;
4400  } elseif ( $res['lag'] !== false ) {
4401  $res['lag'] = max( $res['lag'], $status['lag'] );
4402  }
4403  $res['since'] = min( $res['since'], $status['since'] );
4404  $res['pending'] = $res['pending'] ?: $db->writesPending();
4405  }
4406 
4407  return $res;
4408  }
4409 
4410  public function getLag() {
4411  if ( $this->getLBInfo( 'master' ) ) {
4412  return 0; // this is the master
4413  } elseif ( $this->getLBInfo( 'is static' ) ) {
4414  return 0; // static dataset
4415  }
4416 
4417  return $this->doGetLag();
4418  }
4419 
4420  protected function doGetLag() {
4421  return 0;
4422  }
4423 
4424  public function maxListLen() {
4425  return 0;
4426  }
4427 
4428  public function encodeBlob( $b ) {
4429  return $b;
4430  }
4431 
4432  public function decodeBlob( $b ) {
4433  if ( $b instanceof Blob ) {
4434  $b = $b->fetch();
4435  }
4436  return $b;
4437  }
4438 
4439  public function setSessionOptions( array $options ) {
4440  }
4441 
4442  public function sourceFile(
4443  $filename,
4444  callable $lineCallback = null,
4445  callable $resultCallback = null,
4446  $fname = false,
4447  callable $inputCallback = null
4448  ) {
4449  AtEase::suppressWarnings();
4450  $fp = fopen( $filename, 'r' );
4451  AtEase::restoreWarnings();
4452 
4453  if ( $fp === false ) {
4454  throw new RuntimeException( "Could not open \"{$filename}\"" );
4455  }
4456 
4457  if ( !$fname ) {
4458  $fname = __METHOD__ . "( $filename )";
4459  }
4460 
4461  try {
4462  $error = $this->sourceStream(
4463  $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
4464  } catch ( Exception $e ) {
4465  fclose( $fp );
4466  throw $e;
4467  }
4468 
4469  fclose( $fp );
4470 
4471  return $error;
4472  }
4473 
4474  public function setSchemaVars( $vars ) {
4475  $this->schemaVars = $vars;
4476  }
4477 
4478  public function sourceStream(
4479  $fp,
4480  callable $lineCallback = null,
4481  callable $resultCallback = null,
4482  $fname = __METHOD__,
4483  callable $inputCallback = null
4484  ) {
4485  $delimiterReset = new ScopedCallback(
4486  function ( $delimiter ) {
4487  $this->delimiter = $delimiter;
4488  },
4489  [ $this->delimiter ]
4490  );
4491  $cmd = '';
4492 
4493  while ( !feof( $fp ) ) {
4494  if ( $lineCallback ) {
4495  call_user_func( $lineCallback );
4496  }
4497 
4498  $line = trim( fgets( $fp ) );
4499 
4500  if ( $line == '' ) {
4501  continue;
4502  }
4503 
4504  if ( $line[0] == '-' && $line[1] == '-' ) {
4505  continue;
4506  }
4507 
4508  if ( $cmd != '' ) {
4509  $cmd .= ' ';
4510  }
4511 
4512  $done = $this->streamStatementEnd( $cmd, $line );
4513 
4514  $cmd .= "$line\n";
4515 
4516  if ( $done || feof( $fp ) ) {
4517  $cmd = $this->replaceVars( $cmd );
4518 
4519  if ( $inputCallback ) {
4520  $callbackResult = $inputCallback( $cmd );
4521 
4522  if ( is_string( $callbackResult ) || !$callbackResult ) {
4523  $cmd = $callbackResult;
4524  }
4525  }
4526 
4527  if ( $cmd ) {
4528  $res = $this->query( $cmd, $fname );
4529 
4530  if ( $resultCallback ) {
4531  $resultCallback( $res, $this );
4532  }
4533 
4534  if ( $res === false ) {
4535  $err = $this->lastError();
4536 
4537  return "Query \"{$cmd}\" failed with error code \"$err\".\n";
4538  }
4539  }
4540  $cmd = '';
4541  }
4542  }
4543 
4544  ScopedCallback::consume( $delimiterReset );
4545  return true;
4546  }
4547 
4555  public function streamStatementEnd( &$sql, &$newLine ) {
4556  if ( $this->delimiter ) {
4557  $prev = $newLine;
4558  $newLine = preg_replace(
4559  '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
4560  if ( $newLine != $prev ) {
4561  return true;
4562  }
4563  }
4564 
4565  return false;
4566  }
4567 
4588  protected function replaceVars( $ins ) {
4589  $vars = $this->getSchemaVars();
4590  return preg_replace_callback(
4591  '!
4592  /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
4593  \'\{\$ (\w+) }\' | # 3. addQuotes
4594  `\{\$ (\w+) }` | # 4. addIdentifierQuotes
4595  /\*\$ (\w+) \*/ # 5. leave unencoded
4596  !x',
4597  function ( $m ) use ( $vars ) {
4598  // Note: Because of <https://bugs.php.net/bug.php?id=51881>,
4599  // check for both nonexistent keys *and* the empty string.
4600  if ( isset( $m[1] ) && $m[1] !== '' ) {
4601  if ( $m[1] === 'i' ) {
4602  return $this->indexName( $m[2] );
4603  } else {
4604  return $this->tableName( $m[2] );
4605  }
4606  } elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
4607  return $this->addQuotes( $vars[$m[3]] );
4608  } elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
4609  return $this->addIdentifierQuotes( $vars[$m[4]] );
4610  } elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
4611  return $vars[$m[5]];
4612  } else {
4613  return $m[0];
4614  }
4615  },
4616  $ins
4617  );
4618  }
4619 
4626  protected function getSchemaVars() {
4627  if ( $this->schemaVars ) {
4628  return $this->schemaVars;
4629  } else {
4630  return $this->getDefaultSchemaVars();
4631  }
4632  }
4633 
4642  protected function getDefaultSchemaVars() {
4643  return [];
4644  }
4645 
4646  public function lockIsFree( $lockName, $method ) {
4647  // RDBMs methods for checking named locks may or may not count this thread itself.
4648  // In MySQL, IS_FREE_LOCK() returns 0 if the thread already has the lock. This is
4649  // the behavior choosen by the interface for this method.
4650  return !isset( $this->sessionNamedLocks[$lockName] );
4651  }
4652 
4653  public function lock( $lockName, $method, $timeout = 5 ) {
4654  $this->sessionNamedLocks[$lockName] = 1;
4655 
4656  return true;
4657  }
4658 
4659  public function unlock( $lockName, $method ) {
4660  unset( $this->sessionNamedLocks[$lockName] );
4661 
4662  return true;
4663  }
4664 
4665  public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
4666  if ( $this->writesOrCallbacksPending() ) {
4667  // This only flushes transactions to clear snapshots, not to write data
4668  $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
4669  throw new DBUnexpectedError(
4670  $this,
4671  "$fname: Cannot flush pre-lock snapshot; " .
4672  "writes from transaction {$this->trxFname} are still pending ($fnames)"
4673  );
4674  }
4675 
4676  if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
4677  return null;
4678  }
4679 
4680  $unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
4681  if ( $this->trxLevel() ) {
4682  // There is a good chance an exception was thrown, causing any early return
4683  // from the caller. Let any error handler get a chance to issue rollback().
4684  // If there isn't one, let the error bubble up and trigger server-side rollback.
4685  $this->onTransactionResolution(
4686  function () use ( $lockKey, $fname ) {
4687  $this->unlock( $lockKey, $fname );
4688  },
4689  $fname
4690  );
4691  } else {
4692  $this->unlock( $lockKey, $fname );
4693  }
4694  } );
4695 
4696  $this->commit( $fname, self::FLUSHING_INTERNAL );
4697 
4698  return $unlocker;
4699  }
4700 
4701  public function namedLocksEnqueue() {
4702  return false;
4703  }
4704 
4706  return true;
4707  }
4708 
4709  final public function lockTables( array $read, array $write, $method ) {
4710  if ( $this->writesOrCallbacksPending() ) {
4711  throw new DBUnexpectedError( $this, "Transaction writes or callbacks still pending" );
4712  }
4713 
4714  if ( $this->tableLocksHaveTransactionScope() ) {
4715  $this->startAtomic( $method );
4716  }
4717 
4718  return $this->doLockTables( $read, $write, $method );
4719  }
4720 
4729  protected function doLockTables( array $read, array $write, $method ) {
4730  return true;
4731  }
4732 
4733  final public function unlockTables( $method ) {
4734  if ( $this->tableLocksHaveTransactionScope() ) {
4735  $this->endAtomic( $method );
4736 
4737  return true; // locks released on COMMIT/ROLLBACK
4738  }
4739 
4740  return $this->doUnlockTables( $method );
4741  }
4742 
4749  protected function doUnlockTables( $method ) {
4750  return true;
4751  }
4752 
4760  public function dropTable( $tableName, $fName = __METHOD__ ) {
4761  if ( !$this->tableExists( $tableName, $fName ) ) {
4762  return false;
4763  }
4764  $sql = "DROP TABLE " . $this->tableName( $tableName ) . " CASCADE";
4765 
4766  return $this->query( $sql, $fName );
4767  }
4768 
4769  public function getInfinity() {
4770  return 'infinity';
4771  }
4772 
4773  public function encodeExpiry( $expiry ) {
4774  return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
4775  ? $this->getInfinity()
4776  : $this->timestamp( $expiry );
4777  }
4778 
4779  public function decodeExpiry( $expiry, $format = TS_MW ) {
4780  if ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) {
4781  return 'infinity';
4782  }
4783 
4784  return ConvertibleTimestamp::convert( $format, $expiry );
4785  }
4786 
4787  public function setBigSelects( $value = true ) {
4788  // no-op
4789  }
4790 
4791  public function isReadOnly() {
4792  return ( $this->getReadOnlyReason() !== false );
4793  }
4794 
4798  protected function getReadOnlyReason() {
4799  $reason = $this->getLBInfo( 'readOnlyReason' );
4800  if ( is_string( $reason ) ) {
4801  return $reason;
4802  } elseif ( $this->getLBInfo( 'replica' ) ) {
4803  return "Server is configured in the role of a read-only replica database.";
4804  }
4805 
4806  return false;
4807  }
4808 
4809  public function setTableAliases( array $aliases ) {
4810  $this->tableAliases = $aliases;
4811  }
4812 
4813  public function setIndexAliases( array $aliases ) {
4814  $this->indexAliases = $aliases;
4815  }
4816 
4822  protected function hasFlags( $field, $flags ) {
4823  return ( ( $field & $flags ) === $flags );
4824  }
4825 
4837  protected function getBindingHandle() {
4838  if ( !$this->conn ) {
4839  throw new DBUnexpectedError(
4840  $this,
4841  'DB connection was already closed or the connection dropped'
4842  );
4843  }
4844 
4845  return $this->conn;
4846  }
4847 
4848  public function __toString() {
4849  // spl_object_id is PHP >= 7.2
4850  $id = function_exists( 'spl_object_id' )
4851  ? spl_object_id( $this )
4852  : spl_object_hash( $this );
4853 
4854  $description = $this->getType() . ' object #' . $id;
4855  if ( is_resource( $this->conn ) ) {
4856  $description .= ' (' . (string)$this->conn . ')'; // "resource id #<ID>"
4857  } elseif ( is_object( $this->conn ) ) {
4858  // spl_object_id is PHP >= 7.2
4859  $handleId = function_exists( 'spl_object_id' )
4860  ? spl_object_id( $this->conn )
4861  : spl_object_hash( $this->conn );
4862  $description .= " (handle id #$handleId)";
4863  }
4864 
4865  return $description;
4866  }
4867 
4872  public function __clone() {
4873  $this->connLogger->warning(
4874  "Cloning " . static::class . " is not recommended; forking connection",
4875  [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
4876  );
4877 
4878  if ( $this->isOpen() ) {
4879  // Open a new connection resource without messing with the old one
4880  $this->conn = null;
4881  $this->trxEndCallbacks = []; // don't copy
4882  $this->trxSectionCancelCallbacks = []; // don't copy
4883  $this->handleSessionLossPreconnect(); // no trx or locks anymore
4884  $this->open(
4885  $this->server,
4886  $this->user,
4887  $this->password,
4888  $this->currentDomain->getDatabase(),
4889  $this->currentDomain->getSchema(),
4890  $this->tablePrefix()
4891  );
4892  $this->lastPing = microtime( true );
4893  }
4894  }
4895 
4901  public function __sleep() {
4902  throw new RuntimeException( 'Database serialization may cause problems, since ' .
4903  'the connection is not restored on wakeup' );
4904  }
4905 
4909  public function __destruct() {
4910  if ( $this->trxLevel() && $this->trxDoneWrites ) {
4911  trigger_error( "Uncommitted DB writes (transaction from {$this->trxFname})" );
4912  }
4913 
4914  $danglingWriters = $this->pendingWriteAndCallbackCallers();
4915  if ( $danglingWriters ) {
4916  $fnames = implode( ', ', $danglingWriters );
4917  trigger_error( "DB transaction writes or callbacks still pending ($fnames)" );
4918  }
4919 
4920  if ( $this->conn ) {
4921  // Avoid connection leaks for sanity. Normally, resources close at script completion.
4922  // The connection might already be closed in zend/hhvm by now, so suppress warnings.
4923  AtEase::suppressWarnings();
4924  $this->closeConnection();
4925  AtEase::restoreWarnings();
4926  $this->conn = null;
4927  }
4928  }
4929 }
4930 
4934 class_alias( Database::class, 'DatabaseBase' );
4935 
4939 class_alias( Database::class, 'Database' );
Helper class that detects high-contention DB queries via profiling calls.
int $nonNativeInsertSelectBatchSize
Row batch size to use for emulated INSERT SELECT queries.
Definition: Database.php:79
normalizeConditions( $conds, $fname)
Definition: Database.php:1994
setIndexAliases(array $aliases)
Convert certain index names to alternative names before querying the DB.
Definition: Database.php:4813
rollback( $fname=__METHOD__, $flush=self::FLUSHING_ONE)
Rollback a transaction previously started using begin().
Definition: Database.php:4102
canRecoverFromDisconnect( $sql, $priorWritesPending)
Determine whether it is safe to retry queries after a database connection is lost.
Definition: Database.php:1454
conditional( $cond, $trueVal, $falseVal)
Returns an SQL expression for a simple conditional.
Definition: Database.php:3275
clearFlag( $flag, $remember=self::REMEMBER_NOTHING)
Clear a flag for this connection.
Definition: Database.php:754
executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags)
Wrapper for doQuery() that handles DBO_TRX, profiling, logging, affected row count tracking...
Definition: Database.php:1269
static float $SMALL_WRITE_ROWS
Assume an insert of this many rows or less should be fast to replicate.
Definition: Database.php:218
decodeExpiry( $expiry, $format=TS_MW)
Decode an expiry time into a DBMS independent format.
Definition: Database.php:4779
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:2627
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
array $connectionVariables
SQL variables values to use for all new connections.
Definition: Database.php:73
isTransactableQuery( $sql)
Determine whether a SQL statement is sensitive to isolation level.
Definition: Database.php:1075
setSessionOptions(array $options)
Override database&#39;s default behavior.
Definition: Database.php:4439
namedLocksEnqueue()
Check to see if a named lock used by lock() use blocking queues.
Definition: Database.php:4701
getFlag( $flag)
Returns a boolean whether the flag $flag is set for this connection.
Definition: Database.php:782
freeResult( $res)
Free a result object returned by query() or select().
Definition: Database.php:1618
databasesAreIndependent()
Returns true if DBs are assumed to be on potentially different servers.
Definition: Database.php:2360
bool $trxAutomatic
Whether the current transaction was started implicitly due to DBO_TRX.
Definition: Database.php:125
unlockTables( $method)
Unlock all tables locked via lockTables()
Definition: Database.php:4733
flushSnapshot( $fname=__METHOD__, $flush=self::FLUSHING_ONE)
Commit any transaction but error out if writes or callbacks are pending.
Definition: Database.php:4173
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by wfTimestamp() to the format used for inserting ...
Definition: Database.php:4223
affectedRows()
Get the number of rows affected by the last write query.
Definition: Database.php:4237
encodeBlob( $b)
Some DBMSs have a special format for inserting into blob fields, they don&#39;t allow simple quoted strin...
Definition: Database.php:4428
fieldInfo( $table, $field)
mysql_fetch_field() wrapper Returns false if the field doesn&#39;t exist
fetchObject( $res)
Fetch the next row from the given result object, in object form.
int $trxWriteAdjQueryCount
Number of write queries counted in trxWriteAdjDuration.
Definition: Database.php:143
if(is_array( $mode)) switch( $mode) $input
array $sessionTempTables
Map of (table name => 1) for TEMPORARY tables.
Definition: Database.php:106
doSelectDomain(DatabaseDomain $domain)
Definition: Database.php:2384
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:193
lastErrno()
Get the last error number.
getTempWrites( $sql, $pseudoPermanent)
Definition: Database.php:1091
escapeLikeInternal( $s, $escapeChar='`')
Definition: Database.php:2758
buildConcat( $stringList)
Build a concatenation list to feed into a SQL query.
Definition: Database.php:2295
setLBInfo( $nameOrArray, $value=null)
Set the entire array or a particular key of the managing load balancer info array.
Definition: Database.php:593
selectDomain( $domain)
Set the current domain (database, schema, and table prefix)
Definition: Database.php:2374
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:1978
pendingWriteQueryDuration( $type=self::ESTIMATE_TOTAL)
Get the time spend running write queries for this transaction.
Definition: Database.php:664
begin( $fname=__METHOD__, $mode=self::TRANSACTION_EXPLICIT)
Begin a transaction.
Definition: Database.php:3964
encodeExpiry( $expiry)
Encode an expiry time into the DBMS dependent format.
Definition: Database.php:4773
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
Result wrapper for grabbing data queried from an IDatabase object.
pendingWriteCallers()
Get the list of method names that did write queries for this transaction.
Definition: Database.php:694
static int $TEMP_PSEUDO_PERMANENT
Writes to this temporary table effect lastDoneWrites()
Definition: Database.php:199
array null $trxStatusIgnoredCause
Error details of the last statement-only rollback.
Definition: Database.php:115
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging $e
Definition: hooks.txt:2153
const LIST_NAMES
Definition: Defines.php:41
ignoreIndexClause( $index)
IGNORE INDEX clause.
Definition: Database.php:2827
static string $SAVEPOINT_PREFIX
Prefix to the atomic section counter used to make savepoint IDs.
Definition: Database.php:194
insert( $table, $a, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
Definition: Database.php:2090
getScopedLockAndFlush( $lockKey, $fname, $timeout)
Acquire a named lock, flush any transaction, and return an RAII style unlocker object.
Definition: Database.php:4665
numRows( $res)
Get the number of rows in a query result.
anyChar()
Returns a token for buildLike() that denotes a &#39;_&#39; to be used in a LIKE query.
Definition: Database.php:2791
executeQuery( $sql, $fname, $flags)
Execute a query, retrying it if there is a recoverable connection loss.
Definition: Database.php:1179
__sleep()
Called by serialize.
Definition: Database.php:4901
static int $DEADLOCK_DELAY_MIN
Minimum time to wait before retry, in microseconds.
Definition: Database.php:204
getServer()
Get the server hostname or IP address.
Definition: Database.php:2392
assertBuildSubstringParams( $startPosition, $length)
Check type and bounds for parameters to self::buildSubstring()
Definition: Database.php:2328
string $trxShortId
ID of the active transaction or the empty string otherwise.
Definition: Database.php:109
Exception null $trxStatusCause
The last error that caused the status to become STATUS_TRX_ERROR.
Definition: Database.php:113
setTrxEndCallbackSuppression( $suppress)
Whether to disable running of post-COMMIT/ROLLBACK callbacks.
Definition: Database.php:3558
selectFieldValues( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a list of single field values from result rows.
Definition: Database.php:1647
getSessionLagStatus()
Get the replica DB lag when the current transaction started or a general lag estimate if not transact...
Definition: Database.php:4338
__clone()
Make sure that copies do not share the same client binding handle.
Definition: Database.php:4872
array $connectionParams
Parameters used by initConnection() to establish a connection.
Definition: Database.php:71
selectDB( $db)
Change the current database.
Definition: Database.php:2364
reassignCallbacksForSection(AtomicSectionIdentifier $old, AtomicSectionIdentifier $new)
Hoist callback ownership for callbacks in a section to a parent section.
Definition: Database.php:3462
writesOrCallbacksPending()
Whether there is a transaction open with either possible write queries or unresolved pre-commit/commi...
Definition: Database.php:636
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a single field from a single result row.
Definition: Database.php:1621
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
DatabaseDomain $currentDomain
Definition: Database.php:96
$value
int $flags
Bit field of class DBO_* constants.
Definition: Database.php:65
makeWhereFrom2d( $data, $baseKey, $subKey)
Build a partial where clause from a 2-d array such as used for LinkBatch.
Definition: Database.php:2260
onTransactionIdle(callable $callback, $fname=__METHOD__)
Alias for onTransactionCommitOrIdle() for backwards-compatibility.
Definition: Database.php:3410
getDomainID()
Return the currently selected domain ID.
Definition: Database.php:786
buildSelectSubquery( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Equivalent to IDatabase::selectSQLText() except wraps the result in Subqyery.
Definition: Database.php:2351
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:2145
limitResult( $sql, $limit, $offset=false)
Construct a LIMIT query with optional offset.
Definition: Database.php:3180
nonNativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname=__METHOD__, $insertOptions=[], $selectOptions=[], $selectJoinConds=[])
Implementation of insertSelect() based on select() and insert()
Definition: Database.php:3083
trxLevel()
Gets the current transaction level.
Definition: Database.php:527
callable [] $trxRecurringCallbacks
Map of (name => callable)
Definition: Database.php:153
startAtomic( $fname=__METHOD__, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Begin an atomic section of SQL statements.
Definition: Database.php:3799
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
Definition: Database.php:2166
replaceVars( $ins)
Database independent variable replacement.
Definition: Database.php:4588
string bool null $htmlErrors
Stashed value of html_errors INI setting.
Definition: Database.php:77
fieldNameWithAlias( $name, $alias=false)
Get an aliased field name e.g.
Definition: Database.php:2584
nativeReplace( $table, $rows, $fname)
REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE statement.
Definition: Database.php:2892
implicitOrderby()
Returns true if this database does an implicit order by when the column has an index For example: SEL...
Definition: Database.php:620
endAtomic( $fname=__METHOD__)
Ends an atomic section of SQL statements.
Definition: Database.php:3829
doCommit( $fname)
Issues the COMMIT command to the database server.
Definition: Database.php:4096
query( $sql, $fname=__METHOD__, $flags=0)
Run an SQL query and return the result.
Definition: Database.php:1142
string $server
Server that this instance is currently connected to.
Definition: Database.php:51
getLBInfo( $name=null)
Get properties passed down from the server info array of the load balancer.
Definition: Database.php:581
nextSequenceValue( $seqName)
Deprecated method, calls should be removed.
Definition: Database.php:2799
handleSessionLossPreconnect()
Clean things up after session (and thus transaction) loss before reconnect.
Definition: Database.php:1479
buildGroupConcatField( $delim, $table, $field, $conds='', $join_conds=[])
Build a GROUP_CONCAT or equivalent statement for a query.
Definition: Database.php:2299
doLockTables(array $read, array $write, $method)
Helper function for lockTables() that handles the actual table locking.
Definition: Database.php:4729
textFieldSize( $table, $field)
Returns the size of a text field, or -1 for "unlimited".
Definition: Database.php:2984
bufferResults( $buffer=null)
Backwards-compatibility no-op method for disabling query buffering.
Definition: Database.php:523
fieldExists( $table, $field, $fname=__METHOD__)
Determines whether a field exists in a table.
Definition: Database.php:2049
float $lastPing
UNIX timestamp.
Definition: Database.php:164
callable $errorLogger
Error logging callback.
Definition: Database.php:88
const DBO_DDLMODE
Definition: defines.php:16
bitAnd( $fieldLeft, $fieldRight)
Definition: Database.php:2287
callable null $profiler
Definition: Database.php:92
Lazy-loaded wrapper for simplification and scrubbing of SQL queries for profiling.
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:966
getServerVersion()
A string describing the current software version, like from mysql_get_server_info().
maxListLen()
Return the maximum number of items allowed in a list, or 0 for unlimited.
Definition: Database.php:4424
doSavepoint( $identifier, $fname)
Create a savepoint.
Definition: Database.php:3748
newExceptionAfterConnectError( $error)
Definition: Database.php:1603
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=(\*-\*)") will be honored when streaming the file. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUnknownUser':When a user doesn 't exist locally, this hook is called to give extensions an opportunity to auto-create it. If the auto-creation is successful, return false. $name:User name 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports. & $fullInterwikiPrefix:Interwiki prefix, may contain colons. & $pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable. Can be used to lazy-load the import sources list. & $importSources:The value of $wgImportSources. Modify as necessary. See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page. $context:IContextSource object & $pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect. & $title:Title object for the current page & $request:WebRequest & $ignoreRedirect:boolean to skip redirect check & $target:Title/string of redirect target & $article:Article object 'InternalParseBeforeLinks':during Parser 's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InternalParseBeforeSanitize':during Parser 's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings. Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not. Return true without providing an interwiki to continue interwiki search. $prefix:interwiki prefix we are looking for. & $iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user 's email has been invalidated successfully. $user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification. Callee may modify $url and $query, URL will be constructed as $url . $query & $url:URL to index.php & $query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) & $article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() & $ip:IP being check & $result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from & $allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn 't match your organization. $addr:The e-mail address entered by the user & $result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user & $result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we 're looking for a messages file for & $file:The messages file path, you can override this to change the location. 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links:Array with elements of the form "language:title" in the order that they will be output. & $linkFlags:Associative array mapping prefixed links to arrays of flags. Currently unused, but planned to provide support for marking individual language links in the UI, e.g. for featured articles. 'LanguageSelector':Hook to change the language selector available on a page. $out:The output page. $cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED since 1.28! Use HtmlPageLinkRendererBegin instead. Used when generating internal and interwiki links in Linker::link(), before processing starts. Return false to skip default processing and return $ret. See documentation for Linker::link() for details on the expected meanings of parameters. $skin:the Skin object $target:the Title that the link is pointing to & $html:the contents that the< a > tag should have(raw HTML) $result
Definition: hooks.txt:1976
makeSelectOptions( $options)
Returns an optional USE INDEX clause to go after the table, and a string to go at the end of the quer...
Definition: Database.php:1682
if( $line===false) $args
Definition: cdb.php:64
isWriteQuery( $sql)
Determine whether a query writes to the DB.
Definition: Database.php:1034
tableNames()
Fetch a number of table names into an array This is handy when you need to construct SQL for joins...
Definition: Database.php:2505
close()
Close the database connection.
Definition: Database.php:872
listViews( $prefix=null, $fname=__METHOD__)
Lists all the VIEWs in the database.
Definition: Database.php:4219
strreplace( $orig, $old, $new)
Returns a command for str_replace function in SQL query.
Definition: Database.php:3283
getMasterPos()
Get the position of this master.
Definition: Database.php:3381
__destruct()
Run a few simple sanity checks and close dangling connections.
Definition: Database.php:4909
isOpen()
Is a connection to the database open?
Definition: Database.php:735
installErrorHandler()
Set a custom error handler for logging errors during database connection.
Definition: Database.php:810
getDBname()
Get the current DB name.
Definition: Database.php:2388
static int $DBO_MUTABLE
Bit field of all DBO_* flags that can be changed after connection.
Definition: Database.php:228
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 '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:1250
LoggerInterface $queryLogger
Definition: Database.php:86
setLogger(LoggerInterface $logger)
Set the PSR-3 logger interface to use for query logging.
Definition: Database.php:508
array $sessionNamedLocks
Map of (name => 1) for locks obtained via lock()
Definition: Database.php:104
static int $DEADLOCK_DELAY_MAX
Maximum time to wait before retry.
Definition: Database.php:206
setBigSelects( $value=true)
Allow or deny "big selects" for this session only.
Definition: Database.php:4787
replaceLostConnection( $fname)
Close any existing (dead) database connection and open a new connection.
Definition: Database.php:4299
float $trxReplicaLag
Replication lag estimate at the time of BEGIN for the last transaction.
Definition: Database.php:119
decodeBlob( $b)
Some DBMSs return a special placeholder object representing blob fields in result objects...
Definition: Database.php:4432
buildLike( $param,... $params)
Definition: Database.php:2764
aggregateValue( $valuedata, $valuename='value')
Return aggregated value alias.
Definition: Database.php:2279
array $lbInfo
LoadBalancer tracking information.
Definition: Database.php:67
timestampOrNull( $ts=null)
Convert a timestamp in one of the formats accepted by wfTimestamp() to the format used for inserting ...
Definition: Database.php:4229
addQuotes( $s)
Adds quotes and backslashes.
Definition: Database.php:2719
int $trxWriteAffectedRows
Number of rows affected by write queries for the current transaction.
Definition: Database.php:139
TransactionProfiler $trxProfiler
Definition: Database.php:94
const DBO_DEBUG
Definition: defines.php:9
const LIST_AND
Definition: Defines.php:39
makeInsertOptions( $options)
Helper for Database::insert().
Definition: Database.php:2086
static getClass( $dbType, $driver=null)
Definition: Database.php:446
assertHasConnectionHandle()
Make sure there is an open connection handle (alive or not) as a sanity check.
Definition: Database.php:955
selectOptionsIncludeLocking( $options)
Definition: Database.php:1950
sourceFile( $filename, callable $lineCallback=null, callable $resultCallback=null, $fname=false, callable $inputCallback=null)
Read and execute SQL commands from a file.
Definition: Database.php:4442
doRollback( $fname)
Issues the ROLLBACK command to the database server.
Definition: Database.php:4165
onAtomicSectionCancel(callable $callback, $fname=__METHOD__)
Run a callback when the atomic section is cancelled.
Definition: Database.php:3436
static float $TINY_WRITE_SEC
Guess of how many seconds it takes to replicate a small insert.
Definition: Database.php:214
beginIfImplied( $sql, $fname)
Start an implicit transaction if DBO_TRX is enabled and no transaction is active. ...
Definition: Database.php:1356
reportQueryError( $error, $errno, $sql, $fname, $ignore=false)
Report a query error.
Definition: Database.php:1560
An object representing a master or replica DB position in a replicated setup.
Definition: DBMasterPos.php:12
indexExists( $table, $index, $fname=__METHOD__)
Determines whether an index exists Usually throws a DBQueryError on failure If errors are explicitly ...
Definition: Database.php:2055
assertQueryIsCurrentlyAllowed( $sql, $fname)
Error out if the DB is not in a valid state for a query via query()
Definition: Database.php:1408
open( $server, $user, $password, $dbName, $schema, $tablePrefix)
Open a new connection to the database (closing any existing one)
static string $PING_QUERY
Dummy SQL query.
Definition: Database.php:211
indexName( $index)
Allows for index remapping in queries where this is not consistent across DBMS.
Definition: Database.php:2715
static string [] $MUTABLE_FLAGS
List of DBO_* flags that can be changed after connection.
Definition: Database.php:221
handleSessionLossPostconnect()
Clean things up after session (and thus transaction) loss after reconnect.
Definition: Database.php:1507
string $trxFname
Name of the function that start the last transaction.
Definition: Database.php:121
const LIST_COMMA
Definition: Defines.php:38
$res
Definition: database.txt:21
fieldNamesWithAlias( $fields)
Gets an array of aliased field names.
Definition: Database.php:2598
insertSelect( $destTable, $srcTable, $varMap, $conds, $fname=__METHOD__, $insertOptions=[], $selectOptions=[], $selectJoinConds=[])
INSERT SELECT wrapper.
Definition: Database.php:3021
doRollbackToSavepoint( $identifier, $fname)
Rollback to a savepoint.
Definition: Database.php:3776
tableName( $name, $format='quoted')
Format a table name ready for use in constructing an SQL query.
Definition: Database.php:2396
runOnTransactionIdleCallbacks( $trigger)
Actually consume and run any "on transaction idle/resolution" callbacks.
Definition: Database.php:3572
getServerUptime()
Determines how long the server has been up.
Definition: Database.php:3287
unionConditionPermutations( $table, $vars, array $permute_conds, $extra_conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Construct a UNION query for permutations of conditions.
Definition: Database.php:3204
string $delimiter
Current SQL query delimiter.
Definition: Database.php:75
setSchemaVars( $vars)
Set variables to be used in sourceFile/sourceStream, in preference to the ones in $GLOBALS...
Definition: Database.php:4474
bool $trxAutomaticAtomic
Whether the current transaction was started implicitly by startAtomic()
Definition: Database.php:131
int $trxAtomicCounter
Counter for atomic savepoint identifiers (reset with each transaction)
Definition: Database.php:127
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.
Definition: distributors.txt:9
doQuery( $sql)
Run a query and return a DBMS-dependent wrapper or boolean.
onTransactionCommitOrIdle(callable $callback, $fname=__METHOD__)
Run a callback as soon as there is no transaction pending.
Definition: Database.php:3397
$params
Result wrapper for grabbing data queried from an IDatabase object.
indexUnique( $table, $index)
Determines if a given index is unique.
Definition: Database.php:2070
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:1978
tableNameWithAlias( $table, $alias=false)
Get an aliased table name.
Definition: Database.php:2538
getInfinity()
Find out when &#39;infinity&#39; is.
Definition: Database.php:4769
static string $NOT_APPLICABLE
Idiom used when a cancelable atomic section started the transaction.
Definition: Database.php:192
string $lastQuery
The last SQL query attempted.
Definition: Database.php:166
static int $PING_TTL
How long before it is worth doing a dummy query to test the connection.
Definition: Database.php:209
array $trxAtomicLevels
List of (name, unique ID, savepoint ID) for each active atomic section level.
Definition: Database.php:129
static int $DEADLOCK_TRIES
Number of times to re-try an operation in case of deadlock.
Definition: Database.php:202
string [] $trxWriteCallers
Write query callers of the current transaction.
Definition: Database.php:133
runTransactionListenerCallbacks( $trigger)
Actually run any "transaction listener" callbacks.
Definition: Database.php:3716
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:773
getApproximateLagStatus()
Get a replica DB lag estimate for this server.
Definition: Database.php:4367
lastError()
Get a description of the last error.
setFlag( $flag, $remember=self::REMEMBER_NOTHING)
Set a flag for this connection.
Definition: Database.php:739
makeUpdateOptions( $options)
Make UPDATE options for the Database::update function.
Definition: Database.php:2160
$buffer
IDatabase null $lazyMasterHandle
Lazy handle to the master DB this server replicates from.
Definition: Database.php:101
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...
Definition: Database.php:2620
prependDatabaseOrSchema( $namespace, $relation, $format)
Definition: Database.php:2494
integer null $affectedRowCount
Rows affected by the last query to query() or its CRUD wrappers.
Definition: Database.php:161
doReleaseSavepoint( $identifier, $fname)
Release a savepoint.
Definition: Database.php:3762
isInsertSelectSafe(array $insertOptions, array $selectOptions)
Definition: Database.php:3065
float $trxWriteAdjDuration
Like trxWriteQueryCount but excludes lock-bound, easy to replicate, queries.
Definition: Database.php:141
initConnection()
Initialize the connection to the database over the wire (or to local files)
Definition: Database.php:285
lockForUpdate( $table, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Lock all rows meeting the given conditions/options FOR UPDATE.
Definition: Database.php:2033
restoreFlags( $state=self::RESTORE_PRIOR)
Restore the flags to their prior state before the last setFlag/clearFlag call.
Definition: Database.php:769
selectSQLText( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
The equivalent of IDatabase::select() except that the constructed SQL is returned, instead of being immediately executed.
Definition: Database.php:1799
makeList( $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.
Definition: Database.php:2180
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
Definition: distributors.txt:9
__construct(array $params)
Definition: Database.php:236
makeUpdateOptionsArray( $options)
Make UPDATE options array for Database::makeUpdateOptions.
Definition: Database.php:2140
connectionErrorLogger( $errno, $errstr)
Error handler for logging errors during database connection This method should not be used outside of...
Definition: Database.php:851
nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname=__METHOD__, $insertOptions=[], $selectOptions=[], $selectJoinConds=[])
Native server-side implementation of insertSelect() for situations where we don&#39;t want to select ever...
Definition: Database.php:3152
qualifiedTableComponents( $name)
Get the table components needed for a query given the currently selected database.
Definition: Database.php:2454
tableExists( $table, $fname=__METHOD__)
Query whether a given table exists.
getRecordedTransactionLagStatus()
Get the replica DB lag when the current transaction started.
Definition: Database.php:4355
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings, LocalSettings).
Definition: Setup.php:131
runOnTransactionPreCommitCallbacks()
Actually consume and run any "on transaction pre-commit" callbacks.
Definition: Database.php:3641
const LIST_SET
Definition: Defines.php:40
setTableAliases(array $aliases)
Make certain table names use their own database, schema, and table prefix when passed into SQL querie...
Definition: Database.php:4809
trxTimestamp()
Get the UNIX timestamp of the time that the transaction was established.
Definition: Database.php:531
tablePrefix( $prefix=null)
Get/set the table prefix.
Definition: Database.php:543
const LIST_OR
Definition: Defines.php:42
const DBO_TRX
Definition: defines.php:12
isQuotedIdentifier( $name)
Returns if the given identifier looks quoted or not according to the database convention for quoting ...
Definition: Database.php:2749
deadlockLoop()
Perform a deadlock-prone transaction.
Definition: Database.php:3335
BagOStuff $srvCache
APC cache.
Definition: Database.php:82
registerTempWrites( $ret, $tmpType, $tmpNew, $tmpDel)
Definition: Database.php:1131
cancelAtomic( $fname=__METHOD__, AtomicSectionIdentifier $sectionId=null)
Cancel an atomic section of SQL statements.
Definition: Database.php:3863
getServerInfo()
A string describing the current software version, and possibly other details in a user-friendly way...
Definition: Database.php:512
float bool $lastWriteTime
UNIX timestamp of last write query.
Definition: Database.php:168
const DBO_NOBUFFER
Definition: defines.php:10
string $agent
Agent name for query profiling.
Definition: Database.php:63
unlock( $lockName, $method)
Release a lock.
Definition: Database.php:4659
int $trxStatus
Transaction status.
Definition: Database.php:111
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:35
const DBO_DEFAULT
Definition: defines.php:13
doUnlockTables( $method)
Helper function for unlockTables() that handles the actual table unlocking.
Definition: Database.php:4749
ping(&$rtt=null)
Ping the server and try to reconnect if it there is no connection.
Definition: Database.php:4272
getType()
Get the type of the DBMS, as it appears in $wgDBtype.
lastDoneWrites()
Returns the last time the connection may have been used for write queries.
Definition: Database.php:628
LoggerInterface $connLogger
Definition: Database.php:84
setLazyMasterHandle(IDatabase $conn)
Set a lazy-connecting DB handle to the master DB (for replication status purposes) ...
Definition: Database.php:607
tableNamesWithAlias( $tables)
Gets an array of aliased table names.
Definition: Database.php:2564
bitOr( $fieldLeft, $fieldRight)
Definition: Database.php:2291
Class to handle database/prefix specification for IDatabase domains.
array [] $trxIdleCallbacks
List of (callable, method name, atomic section id)
Definition: Database.php:145
bool $trxDoneWrites
Whether possible write queries were done in the last transaction started.
Definition: Database.php:123
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
float null $trxTimestamp
UNIX timestamp at the time of BEGIN for the last transaction.
Definition: Database.php:117
getLogContext(array $extras=[])
Create a log context to pass to PSR-3 logger functions.
Definition: Database.php:861
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Single row SELECT wrapper.
Definition: Database.php:1882
assertNoOpenTransactions()
Assert that all explicit transactions or atomic sections have been closed.
Definition: Database.php:1435
upsert( $table, array $rows, $uniqueIndexes, array $set, $fname=__METHOD__)
INSERT ON DUPLICATE KEY UPDATE wrapper, upserts an array into a table.
Definition: Database.php:2916
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
tableNamesN()
Fetch a number of table names into an zero-indexed numerical array This is handy when you need to con...
Definition: Database.php:2516
Relational database abstraction object.
Definition: Database.php:49
array [] $trxEndCallbacks
List of (callable, method name, atomic section id)
Definition: Database.php:149
unionQueries( $sqls, $all)
Construct a UNION query This is used for providing overload point for other DB abstractions not compa...
Definition: Database.php:3198
static attributesFromType( $dbType, $driver=null)
Definition: Database.php:429
wasErrorReissuable()
Determines if the last query error was due to something outside of the query itself.
Definition: Database.php:3307
indexInfo( $table, $index, $fname=__METHOD__)
Get information about an index into an object.
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
Definition: Database.php:1791
Advanced database interface for IDatabase handles that include maintenance methods.
buildSubstring( $input, $startPosition, $length=null)
Definition: Database.php:2307
wasConnectionError( $errno)
Do not use this method outside of Database/DBError classes.
Definition: Database.php:3321
bool $trxEndCallbacksSuppressed
Whether to suppress triggering of transaction end callbacks.
Definition: Database.php:155
lastQuery()
Return the last query that sent on account of IDatabase::query()
Definition: Database.php:624
pendingWriteAndCallbackCallers()
List the methods that have write queries or callbacks for the current transaction.
Definition: Database.php:710
commit( $fname=__METHOD__, $flush=self::FLUSHING_ONE)
Commits a transaction previously started using begin().
Definition: Database.php:4027
onTransactionResolution(callable $callback, $fname=__METHOD__)
Run a callback as soon as the current transaction commits or rolls back.
Definition: Database.php:3390
listTables( $prefix=null, $fname=__METHOD__)
List all tables on the database.
Definition: Database.php:4215
$line
Definition: cdb.php:59
estimateRowCount( $table, $var=' *', $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Estimate the number of rows in dataset.
Definition: Database.php:1900
doAtomicSection( $fname, callable $callback, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Perform an atomic section of reversable SQL statements from a callback.
Definition: Database.php:3948
restoreErrorHandler()
Restore the previous error handler and return the last PHP error for this DB.
Definition: Database.php:821
array [] $trxPreCommitCallbacks
List of (callable, method name, atomic section id)
Definition: Database.php:147
object resource null $conn
Database connection.
Definition: Database.php:98
assertIsWritableMaster()
Make sure that this server is not marked as a replica nor read-only as a sanity check.
Definition: Database.php:967
setTransactionListener( $name, callable $callback=null)
Run a callback after each time any transaction commits or rolls back.
Definition: Database.php:3542
Used by Database::buildLike() to represent characters that have special meaning in SQL LIKE clauses a...
Definition: LikeMatch.php:10
dbSchema( $schema=null)
Get/set the db schema.
Definition: Database.php:556
static int $TEMP_NORMAL
Writes to this temporary table do not affect lastDoneWrites()
Definition: Database.php:197
dropTable( $tableName, $fName=__METHOD__)
Delete a table.
Definition: Database.php:4760
hasFlags( $field, $flags)
Definition: Database.php:4822
doInitConnection()
Actually connect to the database over the wire (or to local files)
Definition: Database.php:299
deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname=__METHOD__)
DELETE where the condition is a join.
Definition: Database.php:2966
masterPosWait(DBMasterPos $pos, $timeout)
Wait for the replica DB to catch up to a given master position.
Definition: Database.php:3371
string $user
User that this instance is currently connected under the name of.
Definition: Database.php:53
lock( $lockName, $method, $timeout=5)
Acquire a named lock.
Definition: Database.php:4653
static float $SLOW_WRITE_SEC
Consider a write slow if it took more than this many seconds.
Definition: Database.php:216
callable $deprecationLogger
Deprecation logging callback.
Definition: Database.php:90
addIdentifierQuotes( $s)
Quotes an identifier, in order to make user controlled input safe.
Definition: Database.php:2736
Error thrown when a query times out.
strencode( $s)
Wrapper for addslashes()
selectFieldsOrOptionsAggregate( $fields, $options)
Definition: Database.php:1966
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
static getCacheSetOptions(IDatabase $db1, IDatabase $db2=null)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
Definition: Database.php:4393
lockTables(array $read, array $write, $method)
Lock specific tables.
Definition: Database.php:4709
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:277
makeOrderBy( $options)
Returns an optional ORDER BY.
Definition: Database.php:1779
wasReadOnlyError()
Determines if the last failure was due to the database being read-only.
Definition: Database.php:3303
selectRowCount( $tables, $var=' *', $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Get the number of rows in dataset.
Definition: Database.php:1917
wasConnectionLoss()
Determines if the last query error was due to a dropped connection.
Definition: Database.php:3299
int $trxWriteQueryCount
Number of write queries for the current transaction.
Definition: Database.php:137
tableLocksHaveTransactionScope()
Checks if table locks acquired by lockTables() are transaction-bound in their scope.
Definition: Database.php:4705
getSchemaVars()
Get schema variables.
Definition: Database.php:4626
Exception class for attempted DB write access to a DBConnRef with the DB_REPLICA role.
int [] $priorFlags
Prior flags member variable values.
Definition: Database.php:158
closeConnection()
Closes underlying database connection.
string bool $lastPhpError
Definition: Database.php:170
lockIsFree( $lockName, $method)
Check to see if a named lock is not locked by any thread (non-blocking)
Definition: Database.php:4646
makeGroupByWithHaving( $options)
Returns an optional GROUP BY with an optional HAVING.
Definition: Database.php:1753
bool $cliMode
Whether this PHP instance is for a CLI script.
Definition: Database.php:61
runOnAtomicSectionCancelCallbacks( $trigger, array $sectionIds=null)
Actually run any "atomic section cancel" callbacks.
Definition: Database.php:3674
doBegin( $fname)
Issues the BEGIN command to the database server.
Definition: Database.php:4023
wasDeadlock()
Determines if the last failure was due to a deadlock.
Definition: Database.php:3291
replace( $table, $uniqueIndexes, $rows, $fname=__METHOD__)
REPLACE query wrapper.
Definition: Database.php:2831
streamStatementEnd(&$sql, &$newLine)
Called by sourceStream() to check if we&#39;ve reached a statement end.
Definition: Database.php:4555
Class used for token representing identifiers for atomic sections from IDatabase instances.
getLag()
Get the amount of replication lag for this database server.
Definition: Database.php:4410
fetchRow( $res)
Fetch the next row from the given result object, in associative array form.
duplicateTableStructure( $oldName, $newName, $temporary=false, $fname=__METHOD__)
Creates a new table with structure copied from existing table.
Definition: Database.php:4209
anyString()
Returns a token for buildLike() that denotes a &#39;&#39; to be used in a LIKE query.
Definition: Database.php:2795
array [] $tableAliases
Map of (table => (dbname, schema, prefix) map)
Definition: Database.php:57
wasLockTimeout()
Determines if the last failure was due to a lock timeout.
Definition: Database.php:3295
static factory( $type, $params=[], $connect=self::NEW_CONNECTED)
Construct a Database subclass instance given a database type and parameters.
Definition: Database.php:368
useIndexClause( $index)
USE INDEX clause.
Definition: Database.php:2813
unionSupportsOrderAndLimit()
Returns true if current database backend supports ORDER BY or LIMIT for separate subqueries within th...
Definition: Database.php:3194
updateTrxWriteQueryTime( $sql, $runtime, $affected)
Update the estimated run-time of a query, not counting large row lock times.
Definition: Database.php:1379
wasQueryTimeout( $error, $errno)
Checks whether the cause of the error is detected to be a timeout.
Definition: Database.php:1545
getBindingHandle()
Get the underlying binding connection handle.
Definition: Database.php:4837
static configuration should be added through ResourceLoaderGetConfigVars instead & $vars
Definition: hooks.txt:2211
pendingWriteRowsAffected()
Get the number of affected rows from pending write queries.
Definition: Database.php:698
array bool $schemaVars
Variables use for schema element placeholders.
Definition: Database.php:69
__toString()
Get a debugging string that mentions the database type, the ID of this instance, and the ID of any un...
Definition: Database.php:4848
string [] $indexAliases
Map of (index alias => index)
Definition: Database.php:59
array [] $trxSectionCancelCallbacks
List of (callable, method name, atomic section id)
Definition: Database.php:151
resultObject( $result)
Take a query result and wrap it in an iterable result wrapper if necessary.
Definition: Database.php:4260
modifyCallbacksForCancel(array $sectionIds, AtomicSectionIdentifier $newSectionId=null)
Update callbacks that were owned by cancelled atomic sections.
Definition: Database.php:3506
string $password
Password used to establish the current connection.
Definition: Database.php:55
float $trxWriteDuration
Seconds spent in write queries for the current transaction.
Definition: Database.php:135
getQueryExceptionAndLog( $error, $errno, $sql, $fname)
Definition: Database.php:1575
consumeTrxShortId()
Reset the transaction ID and return the old one.
Definition: Database.php:1528
onTransactionPreCommitOrIdle(callable $callback, $fname=__METHOD__)
Run a callback before the current transaction commits or now if there is none.
Definition: Database.php:3414
sourceStream( $fp, callable $lineCallback=null, callable $resultCallback=null, $fname=__METHOD__, callable $inputCallback=null)
Read and execute commands from an open file handle.
Definition: Database.php:4478
$matches
float $lastRoundTripEstimate
Query rount trip time estimate.
Definition: Database.php:172
getReplicaPos()
Get the replication position of this replica DB.
Definition: Database.php:3376
getDefaultSchemaVars()
Get schema variables to use if none have been set via setSchemaVars().
Definition: Database.php:4642