MediaWiki  master
Database.php
Go to the documentation of this file.
1 <?php
26 namespace Wikimedia\Rdbms;
27 
41 
48 abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAwareInterface {
50  const DEADLOCK_TRIES = 4;
52  const DEADLOCK_DELAY_MIN = 500000;
54  const DEADLOCK_DELAY_MAX = 1500000;
55 
57  const PING_TTL = 1.0;
58  const PING_QUERY = 'SELECT 1 AS ping';
59 
60  const TINY_WRITE_SEC = 0.010;
61  const SLOW_WRITE_SEC = 0.500;
62  const SMALL_WRITE_ROWS = 100;
63 
65  const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
67  const ATTR_SCHEMAS_AS_TABLE_GROUPS = 'supports-schemas';
68 
70  const NEW_UNCONNECTED = 0;
72  const NEW_CONNECTED = 1;
73 
75  protected $lastQuery = '';
77  protected $lastWriteTime = false;
79  protected $phpError = false;
81  protected $server;
83  protected $user;
85  protected $password;
87  protected $tableAliases = [];
89  protected $indexAliases = [];
91  protected $cliMode;
93  protected $agent;
95  protected $connectionParams = [];
97  protected $srvCache;
99  protected $connLogger;
101  protected $queryLogger;
103  protected $errorLogger;
106 
108  protected $conn = null;
110  protected $opened = false;
111 
113  protected $trxIdleCallbacks = [];
115  protected $trxPreCommitCallbacks = [];
117  protected $trxEndCallbacks = [];
119  protected $trxRecurringCallbacks = [];
121  protected $trxEndCallbacksSuppressed = false;
122 
124  protected $flags;
126  protected $lbInfo = [];
128  protected $schemaVars = false;
130  protected $sessionVars = [];
132  protected $preparedArgs;
134  protected $htmlErrors;
136  protected $delimiter = ';';
138  protected $currentDomain;
140  protected $affectedRowCount;
141 
145  protected $trxStatus = self::STATUS_TRX_NONE;
149  protected $trxStatusCause;
161  protected $trxLevel = 0;
168  protected $trxShortId = '';
177  private $trxTimestamp = null;
179  private $trxReplicaLag = null;
187  private $trxFname = null;
194  private $trxDoneWrites = false;
201  private $trxAutomatic = false;
207  private $trxAtomicCounter = 0;
213  private $trxAtomicLevels = [];
219  private $trxAutomaticAtomic = false;
225  private $trxWriteCallers = [];
229  private $trxWriteDuration = 0.0;
233  private $trxWriteQueryCount = 0;
241  private $trxWriteAdjDuration = 0.0;
249  private $rttEstimate = 0.0;
250 
252  private $namedLocksHeld = [];
254  protected $sessionTempTables = [];
255 
258 
260  protected $lastPing = 0.0;
261 
263  private $priorFlags = [];
264 
266  protected $profiler;
268  protected $trxProfiler;
269 
272 
274  private static $NOT_APPLICABLE = 'n/a';
276  private static $SAVEPOINT_PREFIX = 'wikimedia_rdbms_atomic';
277 
279  const STATUS_TRX_ERROR = 1;
281  const STATUS_TRX_OK = 2;
283  const STATUS_TRX_NONE = 3;
284 
286  const TEMP_NORMAL = 1;
288  const TEMP_PSEUDO_PERMANENT = 2;
289 
294  protected function __construct( array $params ) {
295  foreach ( [ 'host', 'user', 'password', 'dbname', 'schema', 'tablePrefix' ] as $name ) {
296  $this->connectionParams[$name] = $params[$name];
297  }
298 
299  $this->cliMode = $params['cliMode'];
300  // Agent name is added to SQL queries in a comment, so make sure it can't break out
301  $this->agent = str_replace( '/', '-', $params['agent'] );
302 
303  $this->flags = $params['flags'];
304  if ( $this->flags & self::DBO_DEFAULT ) {
305  if ( $this->cliMode ) {
306  $this->flags &= ~self::DBO_TRX;
307  } else {
308  $this->flags |= self::DBO_TRX;
309  }
310  }
311  // Disregard deprecated DBO_IGNORE flag (T189999)
312  $this->flags &= ~self::DBO_IGNORE;
313 
314  $this->sessionVars = $params['variables'];
315 
316  $this->srvCache = $params['srvCache'] ?? new HashBagOStuff();
317 
318  $this->profiler = is_callable( $params['profiler'] ) ? $params['profiler'] : null;
319  $this->trxProfiler = $params['trxProfiler'];
320  $this->connLogger = $params['connLogger'];
321  $this->queryLogger = $params['queryLogger'];
322  $this->errorLogger = $params['errorLogger'];
323  $this->deprecationLogger = $params['deprecationLogger'];
324 
325  if ( isset( $params['nonNativeInsertSelectBatchSize'] ) ) {
326  $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'];
327  }
328 
329  // Set initial dummy domain until open() sets the final DB/prefix
330  $this->currentDomain = new DatabaseDomain(
331  $params['dbname'] != '' ? $params['dbname'] : null,
332  $params['schema'] != '' ? $params['schema'] : null,
333  $params['tablePrefix']
334  );
335  }
336 
345  final public function initConnection() {
346  if ( $this->isOpen() ) {
347  throw new LogicException( __METHOD__ . ': already connected.' );
348  }
349  // Establish the connection
350  $this->doInitConnection();
351  }
352 
360  protected function doInitConnection() {
361  if ( strlen( $this->connectionParams['user'] ) ) {
362  $this->open(
363  $this->connectionParams['host'],
364  $this->connectionParams['user'],
365  $this->connectionParams['password'],
366  $this->connectionParams['dbname'],
367  $this->connectionParams['schema'],
368  $this->connectionParams['tablePrefix']
369  );
370  } else {
371  throw new InvalidArgumentException( "No database user provided." );
372  }
373  }
374 
387  abstract protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix );
388 
434  final public static function factory( $dbType, $p = [], $connect = self::NEW_CONNECTED ) {
435  $class = self::getClass( $dbType, $p['driver'] ?? null );
436 
437  if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
438  // Resolve some defaults for b/c
439  $p['host'] = $p['host'] ?? false;
440  $p['user'] = $p['user'] ?? false;
441  $p['password'] = $p['password'] ?? false;
442  $p['dbname'] = $p['dbname'] ?? false;
443  $p['flags'] = $p['flags'] ?? 0;
444  $p['variables'] = $p['variables'] ?? [];
445  $p['tablePrefix'] = $p['tablePrefix'] ?? '';
446  $p['schema'] = $p['schema'] ?? null;
447  $p['cliMode'] = $p['cliMode'] ?? ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' );
448  $p['agent'] = $p['agent'] ?? '';
449  if ( !isset( $p['connLogger'] ) ) {
450  $p['connLogger'] = new NullLogger();
451  }
452  if ( !isset( $p['queryLogger'] ) ) {
453  $p['queryLogger'] = new NullLogger();
454  }
455  $p['profiler'] = $p['profiler'] ?? null;
456  if ( !isset( $p['trxProfiler'] ) ) {
457  $p['trxProfiler'] = new TransactionProfiler();
458  }
459  if ( !isset( $p['errorLogger'] ) ) {
460  $p['errorLogger'] = function ( Exception $e ) {
461  trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
462  };
463  }
464  if ( !isset( $p['deprecationLogger'] ) ) {
465  $p['deprecationLogger'] = function ( $msg ) {
466  trigger_error( $msg, E_USER_DEPRECATED );
467  };
468  }
469 
471  $conn = new $class( $p );
472  if ( $connect == self::NEW_CONNECTED ) {
473  $conn->initConnection();
474  }
475  } else {
476  $conn = null;
477  }
478 
479  return $conn;
480  }
481 
489  final public static function attributesFromType( $dbType, $driver = null ) {
490  static $defaults = [
491  self::ATTR_DB_LEVEL_LOCKING => false,
492  self::ATTR_SCHEMAS_AS_TABLE_GROUPS => false
493  ];
494 
495  $class = self::getClass( $dbType, $driver );
496 
497  return call_user_func( [ $class, 'getAttributes' ] ) + $defaults;
498  }
499 
506  private static function getClass( $dbType, $driver = null ) {
507  // For database types with built-in support, the below maps type to IDatabase
508  // implementations. For types with multipe driver implementations (PHP extensions),
509  // an array can be used, keyed by extension name. In case of an array, the
510  // optional 'driver' parameter can be used to force a specific driver. Otherwise,
511  // we auto-detect the first available driver. For types without built-in support,
512  // an class named "Database<Type>" us used, eg. DatabaseFoo for type 'foo'.
513  static $builtinTypes = [
514  'mssql' => DatabaseMssql::class,
515  'mysql' => [ 'mysqli' => DatabaseMysqli::class ],
516  'sqlite' => DatabaseSqlite::class,
517  'postgres' => DatabasePostgres::class,
518  ];
519 
520  $dbType = strtolower( $dbType );
521  $class = false;
522 
523  if ( isset( $builtinTypes[$dbType] ) ) {
524  $possibleDrivers = $builtinTypes[$dbType];
525  if ( is_string( $possibleDrivers ) ) {
526  $class = $possibleDrivers;
527  } elseif ( (string)$driver !== '' ) {
528  if ( !isset( $possibleDrivers[$driver] ) ) {
529  throw new InvalidArgumentException( __METHOD__ .
530  " type '$dbType' does not support driver '{$driver}'" );
531  }
532 
533  $class = $possibleDrivers[$driver];
534  } else {
535  foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
536  if ( extension_loaded( $posDriver ) ) {
537  $class = $possibleClass;
538  break;
539  }
540  }
541  }
542  } else {
543  $class = 'Database' . ucfirst( $dbType );
544  }
545 
546  if ( $class === false ) {
547  throw new InvalidArgumentException( __METHOD__ .
548  " no viable database extension found for type '$dbType'" );
549  }
550 
551  return $class;
552  }
553 
558  protected static function getAttributes() {
559  return [];
560  }
561 
569  public function setLogger( LoggerInterface $logger ) {
570  $this->queryLogger = $logger;
571  }
572 
573  public function getServerInfo() {
574  return $this->getServerVersion();
575  }
576 
577  public function bufferResults( $buffer = null ) {
578  $res = !$this->getFlag( self::DBO_NOBUFFER );
579  if ( $buffer !== null ) {
580  $buffer
581  ? $this->clearFlag( self::DBO_NOBUFFER )
582  : $this->setFlag( self::DBO_NOBUFFER );
583  }
584 
585  return $res;
586  }
587 
588  public function trxLevel() {
589  return $this->trxLevel;
590  }
591 
592  public function trxTimestamp() {
593  return $this->trxLevel ? $this->trxTimestamp : null;
594  }
595 
600  public function trxStatus() {
601  return $this->trxStatus;
602  }
603 
604  public function tablePrefix( $prefix = null ) {
605  $old = $this->currentDomain->getTablePrefix();
606  if ( $prefix !== null ) {
607  $this->currentDomain = new DatabaseDomain(
608  $this->currentDomain->getDatabase(),
609  $this->currentDomain->getSchema(),
610  $prefix
611  );
612  }
613 
614  return $old;
615  }
616 
617  public function dbSchema( $schema = null ) {
618  if ( strlen( $schema ) && $this->getDBname() === null ) {
619  throw new DBUnexpectedError( $this, "Cannot set schema to '$schema'; no database set." );
620  }
621 
622  $old = $this->currentDomain->getSchema();
623  if ( $schema !== null ) {
624  $this->currentDomain = new DatabaseDomain(
625  $this->currentDomain->getDatabase(),
626  // DatabaseDomain uses null for unspecified schemas
627  strlen( $schema ) ? $schema : null,
628  $this->currentDomain->getTablePrefix()
629  );
630  }
631 
632  return (string)$old;
633  }
634 
638  protected function relationSchemaQualifier() {
639  return $this->dbSchema();
640  }
641 
642  public function getLBInfo( $name = null ) {
643  if ( is_null( $name ) ) {
644  return $this->lbInfo;
645  }
646 
647  if ( array_key_exists( $name, $this->lbInfo ) ) {
648  return $this->lbInfo[$name];
649  }
650 
651  return null;
652  }
653 
654  public function setLBInfo( $name, $value = null ) {
655  if ( is_null( $value ) ) {
656  $this->lbInfo = $name;
657  } else {
658  $this->lbInfo[$name] = $value;
659  }
660  }
661 
662  public function setLazyMasterHandle( IDatabase $conn ) {
663  $this->lazyMasterHandle = $conn;
664  }
665 
671  protected function getLazyMasterHandle() {
673  }
674 
675  public function implicitGroupby() {
676  return true;
677  }
678 
679  public function implicitOrderby() {
680  return true;
681  }
682 
683  public function lastQuery() {
684  return $this->lastQuery;
685  }
686 
687  public function doneWrites() {
688  return (bool)$this->lastWriteTime;
689  }
690 
691  public function lastDoneWrites() {
692  return $this->lastWriteTime ?: false;
693  }
694 
695  public function writesPending() {
696  return $this->trxLevel && $this->trxDoneWrites;
697  }
698 
699  public function writesOrCallbacksPending() {
700  return $this->trxLevel && (
701  $this->trxDoneWrites ||
702  $this->trxIdleCallbacks ||
703  $this->trxPreCommitCallbacks ||
705  );
706  }
707 
708  public function preCommitCallbacksPending() {
709  return $this->trxLevel && $this->trxPreCommitCallbacks;
710  }
711 
715  final protected function getTransactionRoundId() {
716  // If transaction round participation is enabled, see if one is active
717  if ( $this->getFlag( self::DBO_TRX ) ) {
718  $id = $this->getLBInfo( 'trxRoundId' );
719 
720  return is_string( $id ) ? $id : null;
721  }
722 
723  return null;
724  }
725 
726  public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
727  if ( !$this->trxLevel ) {
728  return false;
729  } elseif ( !$this->trxDoneWrites ) {
730  return 0.0;
731  }
732 
733  switch ( $type ) {
734  case self::ESTIMATE_DB_APPLY:
735  return $this->pingAndCalculateLastTrxApplyTime();
736  default: // everything
738  }
739  }
740 
744  private function pingAndCalculateLastTrxApplyTime() {
745  $this->ping( $rtt );
746 
747  $rttAdjTotal = $this->trxWriteAdjQueryCount * $rtt;
748  $applyTime = max( $this->trxWriteAdjDuration - $rttAdjTotal, 0 );
749  // For omitted queries, make them count as something at least
750  $omitted = $this->trxWriteQueryCount - $this->trxWriteAdjQueryCount;
751  $applyTime += self::TINY_WRITE_SEC * $omitted;
752 
753  return $applyTime;
754  }
755 
756  public function pendingWriteCallers() {
757  return $this->trxLevel ? $this->trxWriteCallers : [];
758  }
759 
760  public function pendingWriteRowsAffected() {
762  }
763 
772  public function pendingWriteAndCallbackCallers() {
773  $fnames = $this->pendingWriteCallers();
774  foreach ( [
775  $this->trxIdleCallbacks,
776  $this->trxPreCommitCallbacks,
777  $this->trxEndCallbacks
778  ] as $callbacks ) {
779  foreach ( $callbacks as $callback ) {
780  $fnames[] = $callback[1];
781  }
782  }
783 
784  return $fnames;
785  }
786 
790  private function flatAtomicSectionList() {
791  return array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) {
792  return $accum === null ? $v[0] : "$accum, " . $v[0];
793  } );
794  }
795 
796  public function isOpen() {
797  return $this->opened;
798  }
799 
800  public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
801  if ( ( $flag & self::DBO_IGNORE ) ) {
802  throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
803  }
804 
805  if ( $remember === self::REMEMBER_PRIOR ) {
806  array_push( $this->priorFlags, $this->flags );
807  }
808  $this->flags |= $flag;
809  }
810 
811  public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
812  if ( ( $flag & self::DBO_IGNORE ) ) {
813  throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
814  }
815 
816  if ( $remember === self::REMEMBER_PRIOR ) {
817  array_push( $this->priorFlags, $this->flags );
818  }
819  $this->flags &= ~$flag;
820  }
821 
822  public function restoreFlags( $state = self::RESTORE_PRIOR ) {
823  if ( !$this->priorFlags ) {
824  return;
825  }
826 
827  if ( $state === self::RESTORE_INITIAL ) {
828  $this->flags = reset( $this->priorFlags );
829  $this->priorFlags = [];
830  } else {
831  $this->flags = array_pop( $this->priorFlags );
832  }
833  }
834 
835  public function getFlag( $flag ) {
836  return (bool)( $this->flags & $flag );
837  }
838 
844  public function getProperty( $name ) {
845  return $this->$name;
846  }
847 
848  public function getDomainID() {
849  return $this->currentDomain->getId();
850  }
851 
852  final public function getWikiID() {
853  return $this->getDomainID();
854  }
855 
863  abstract function indexInfo( $table, $index, $fname = __METHOD__ );
864 
871  abstract function strencode( $s );
872 
876  protected function installErrorHandler() {
877  $this->phpError = false;
878  $this->htmlErrors = ini_set( 'html_errors', '0' );
879  set_error_handler( [ $this, 'connectionErrorLogger' ] );
880  }
881 
887  protected function restoreErrorHandler() {
888  restore_error_handler();
889  if ( $this->htmlErrors !== false ) {
890  ini_set( 'html_errors', $this->htmlErrors );
891  }
892 
893  return $this->getLastPHPError();
894  }
895 
899  protected function getLastPHPError() {
900  if ( $this->phpError ) {
901  $error = preg_replace( '!\[<a.*</a>\]!', '', $this->phpError );
902  $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
903 
904  return $error;
905  }
906 
907  return false;
908  }
909 
917  public function connectionErrorLogger( $errno, $errstr ) {
918  $this->phpError = $errstr;
919  }
920 
927  protected function getLogContext( array $extras = [] ) {
928  return array_merge(
929  [
930  'db_server' => $this->server,
931  'db_name' => $this->getDBname(),
932  'db_user' => $this->user,
933  ],
934  $extras
935  );
936  }
937 
938  final public function close() {
939  $exception = null; // error to throw after disconnecting
940 
941  $wasOpen = $this->opened;
942  // This should mostly do nothing if the connection is already closed
943  if ( $this->conn ) {
944  // Roll back any dangling transaction first
945  if ( $this->trxLevel ) {
946  if ( $this->trxAtomicLevels ) {
947  // Cannot let incomplete atomic sections be committed
948  $levels = $this->flatAtomicSectionList();
949  $exception = new DBUnexpectedError(
950  $this,
951  __METHOD__ . ": atomic sections $levels are still open."
952  );
953  } elseif ( $this->trxAutomatic ) {
954  // Only the connection manager can commit non-empty DBO_TRX transactions
955  // (empty ones we can silently roll back)
956  if ( $this->writesOrCallbacksPending() ) {
957  $exception = new DBUnexpectedError(
958  $this,
959  __METHOD__ .
960  ": mass commit/rollback of peer transaction required (DBO_TRX set)."
961  );
962  }
963  } else {
964  // Manual transactions should have been committed or rolled
965  // back, even if empty.
966  $exception = new DBUnexpectedError(
967  $this,
968  __METHOD__ . ": transaction is still open (from {$this->trxFname})."
969  );
970  }
971 
972  if ( $this->trxEndCallbacksSuppressed ) {
973  $exception = $exception ?: new DBUnexpectedError(
974  $this,
975  __METHOD__ . ': callbacks are suppressed; cannot properly commit.'
976  );
977  }
978 
979  // Rollback the changes and run any callbacks as needed
980  $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
981  }
982 
983  // Close the actual connection in the binding handle
984  $closed = $this->closeConnection();
985  } else {
986  $closed = true; // already closed; nothing to do
987  }
988 
989  $this->conn = false;
990  $this->opened = false;
991 
992  // Throw any unexpected errors after having disconnected
993  if ( $exception instanceof Exception ) {
994  throw $exception;
995  }
996 
997  // Note that various subclasses call close() at the start of open(), which itself is
998  // called by replaceLostConnection(). In that case, just because onTransactionResolution()
999  // callbacks are pending does not mean that an exception should be thrown. Rather, they
1000  // will be executed after the reconnection step.
1001  if ( $wasOpen ) {
1002  // Sanity check that no callbacks are dangling
1003  $fnames = $this->pendingWriteAndCallbackCallers();
1004  if ( $fnames ) {
1005  throw new RuntimeException(
1006  "Transaction callbacks are still pending:\n" . implode( ', ', $fnames )
1007  );
1008  }
1009  }
1010 
1011  return $closed;
1012  }
1013 
1022  protected function assertHasConnectionHandle() {
1023  if ( !$this->isOpen() ) {
1024  throw new DBUnexpectedError( $this, "DB connection was already closed." );
1025  }
1026  }
1027 
1033  protected function assertIsWritableMaster() {
1034  if ( $this->getLBInfo( 'replica' ) === true ) {
1035  throw new DBReadOnlyRoleError(
1036  $this,
1037  'Write operations are not allowed on replica database connections.'
1038  );
1039  }
1040  $reason = $this->getReadOnlyReason();
1041  if ( $reason !== false ) {
1042  throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
1043  }
1044  }
1045 
1051  abstract protected function closeConnection();
1052 
1058  public function reportConnectionError( $error = 'Unknown error' ) {
1059  call_user_func( $this->deprecationLogger, 'Use of ' . __METHOD__ . ' is deprecated.' );
1060  throw new DBConnectionError( $this, $this->lastError() ?: $error );
1061  }
1062 
1081  abstract protected function doQuery( $sql );
1082 
1099  protected function isWriteQuery( $sql ) {
1100  // BEGIN and COMMIT queries are considered read queries here.
1101  // Database backends and drivers (MySQL, MariaDB, php-mysqli) generally
1102  // treat these as write queries, in that their results have "affected rows"
1103  // as meta data as from writes, instead of "num rows" as from reads.
1104  // But, we treat them as read queries because when reading data (from
1105  // either replica or master) we use transactions to enable repeatable-read
1106  // snapshots, which ensures we get consistent results from the same snapshot
1107  // for all queries within a request. Use cases:
1108  // - Treating these as writes would trigger ChronologyProtector (see method doc).
1109  // - We use this method to reject writes to replicas, but we need to allow
1110  // use of transactions on replicas for read snapshots. This fine given
1111  // that transactions by themselves don't make changes, only actual writes
1112  // within the transaction matter, which we still detect.
1113  return !preg_match(
1114  '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SAVEPOINT|RELEASE|SET|SHOW|EXPLAIN|\(SELECT)\b/i',
1115  $sql
1116  );
1117  }
1118 
1123  protected function getQueryVerb( $sql ) {
1124  return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
1125  }
1126 
1140  protected function isTransactableQuery( $sql ) {
1141  return !in_array(
1142  $this->getQueryVerb( $sql ),
1143  [ 'BEGIN', 'ROLLBACK', 'COMMIT', 'SET', 'SHOW', 'CREATE', 'ALTER' ],
1144  true
1145  );
1146  }
1147 
1153  protected function registerTempTableWrite( $sql, $pseudoPermanent ) {
1154  static $qt = '[`"\']?(\w+)[`"\']?'; // quoted table
1155 
1156  if ( preg_match(
1157  '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?' . $qt . '/i',
1158  $sql,
1159  $matches
1160  ) ) {
1161  $type = $pseudoPermanent ? self::TEMP_PSEUDO_PERMANENT : self::TEMP_NORMAL;
1162  $this->sessionTempTables[$matches[1]] = $type;
1163 
1164  return $type;
1165  } elseif ( preg_match(
1166  '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?' . $qt . '/i',
1167  $sql,
1168  $matches
1169  ) ) {
1170  $type = $this->sessionTempTables[$matches[1]] ?? null;
1171  unset( $this->sessionTempTables[$matches[1]] );
1172 
1173  return $type;
1174  } elseif ( preg_match(
1175  '/^TRUNCATE\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?' . $qt . '/i',
1176  $sql,
1177  $matches
1178  ) ) {
1179  return $this->sessionTempTables[$matches[1]] ?? null;
1180  } elseif ( preg_match(
1181  '/^(?:(?:INSERT|REPLACE)\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+' . $qt . '/i',
1182  $sql,
1183  $matches
1184  ) ) {
1185  return $this->sessionTempTables[$matches[1]] ?? null;
1186  }
1187 
1188  return null;
1189  }
1190 
1191  public function query( $sql, $fname = __METHOD__, $flags = 0 ) {
1192  $this->assertTransactionStatus( $sql, $fname );
1193  $this->assertHasConnectionHandle();
1194 
1195  $flags = (int)$flags; // b/c; this field used to be a bool
1196  $ignoreErrors = $this->hasFlags( $flags, self::QUERY_SILENCE_ERRORS );
1197 
1198  $priorTransaction = $this->trxLevel;
1199  $priorWritesPending = $this->writesOrCallbacksPending();
1200  $this->lastQuery = $sql;
1201 
1202  if ( $this->isWriteQuery( $sql ) ) {
1203  # In theory, non-persistent writes are allowed in read-only mode, but due to things
1204  # like https://bugs.mysql.com/bug.php?id=33669 that might not work anyway...
1205  $this->assertIsWritableMaster();
1206  # Do not treat temporary table writes as "meaningful writes" that need committing.
1207  # Profile them as reads. Integration tests can override this behavior via $flags.
1208  $pseudoPermanent = $this->hasFlags( $flags, self::QUERY_PSEUDO_PERMANENT );
1209  $tableType = $this->registerTempTableWrite( $sql, $pseudoPermanent );
1210  $isEffectiveWrite = ( $tableType !== self::TEMP_NORMAL );
1211  # DBConnRef uses QUERY_REPLICA_ROLE to enforce the replica role for raw SQL queries
1212  if ( $isEffectiveWrite && $this->hasFlags( $flags, self::QUERY_REPLICA_ROLE ) ) {
1213  throw new DBReadOnlyRoleError( $this, "Cannot write; target role is DB_REPLICA" );
1214  }
1215  } else {
1216  $isEffectiveWrite = false;
1217  }
1218 
1219  # Add trace comment to the begin of the sql string, right after the operator.
1220  # Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
1221  $commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
1222 
1223  # Send the query to the server and fetch any corresponding errors
1224  $ret = $this->attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname );
1225  $lastError = $this->lastError();
1226  $lastErrno = $this->lastErrno();
1227 
1228  $recoverableSR = false; // recoverable statement rollback?
1229  $recoverableCL = false; // recoverable connection loss?
1230 
1231  if ( $ret === false && $this->wasConnectionLoss() ) {
1232  # Check if no meaningful session state was lost
1233  $recoverableCL = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
1234  # Update session state tracking and try to restore the connection
1235  $reconnected = $this->replaceLostConnection( __METHOD__ );
1236  # Silently resend the query to the server if it is safe and possible
1237  if ( $recoverableCL && $reconnected ) {
1238  $ret = $this->attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname );
1239  $lastError = $this->lastError();
1240  $lastErrno = $this->lastErrno();
1241 
1242  if ( $ret === false && $this->wasConnectionLoss() ) {
1243  # Query probably causes disconnects; reconnect and do not re-run it
1244  $this->replaceLostConnection( __METHOD__ );
1245  } else {
1246  $recoverableCL = false; // connection does not need recovering
1247  $recoverableSR = $this->wasKnownStatementRollbackError();
1248  }
1249  }
1250  } else {
1251  $recoverableSR = $this->wasKnownStatementRollbackError();
1252  }
1253 
1254  if ( $ret === false ) {
1255  if ( $priorTransaction ) {
1256  if ( $recoverableSR ) {
1257  # We're ignoring an error that caused just the current query to be aborted.
1258  # But log the cause so we can log a deprecation notice if a caller actually
1259  # does ignore it.
1260  $this->trxStatusIgnoredCause = [ $lastError, $lastErrno, $fname ];
1261  } elseif ( !$recoverableCL ) {
1262  # Either the query was aborted or all queries after BEGIN where aborted.
1263  # In the first case, the only options going forward are (a) ROLLBACK, or
1264  # (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
1265  # option is ROLLBACK, since the snapshots would have been released.
1266  $this->trxStatus = self::STATUS_TRX_ERROR;
1267  $this->trxStatusCause =
1268  $this->getQueryExceptionAndLog( $lastError, $lastErrno, $sql, $fname );
1269  $ignoreErrors = false; // cannot recover
1270  $this->trxStatusIgnoredCause = null;
1271  }
1272  }
1273 
1274  $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $ignoreErrors );
1275  }
1276 
1277  return $this->resultObject( $ret );
1278  }
1279 
1290  private function attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname ) {
1291  $this->beginIfImplied( $sql, $fname );
1292 
1293  # Keep track of whether the transaction has write queries pending
1294  if ( $isEffectiveWrite ) {
1295  $this->lastWriteTime = microtime( true );
1296  if ( $this->trxLevel && !$this->trxDoneWrites ) {
1297  $this->trxDoneWrites = true;
1298  $this->trxProfiler->transactionWritingIn(
1299  $this->server, $this->getDomainID(), $this->trxShortId );
1300  }
1301  }
1302 
1303  if ( $this->getFlag( self::DBO_DEBUG ) ) {
1304  $this->queryLogger->debug( "{$this->getDomainID()} {$commentedSql}" );
1305  }
1306 
1307  $isMaster = !is_null( $this->getLBInfo( 'master' ) );
1308  # generalizeSQL() will probably cut down the query to reasonable
1309  # logging size most of the time. The substr is really just a sanity check.
1310  if ( $isMaster ) {
1311  $queryProf = 'query-m: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
1312  } else {
1313  $queryProf = 'query: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
1314  }
1315 
1316  # Include query transaction state
1317  $queryProf .= $this->trxShortId ? " [TRX#{$this->trxShortId}]" : "";
1318 
1319  $startTime = microtime( true );
1320  $ps = $this->profiler ? ( $this->profiler )( $queryProf ) : null;
1321  $this->affectedRowCount = null;
1322  $ret = $this->doQuery( $commentedSql );
1323  $this->affectedRowCount = $this->affectedRows();
1324  unset( $ps ); // profile out (if set)
1325  $queryRuntime = max( microtime( true ) - $startTime, 0.0 );
1326 
1327  if ( $ret !== false ) {
1328  $this->lastPing = $startTime;
1329  if ( $isEffectiveWrite && $this->trxLevel ) {
1330  $this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() );
1331  $this->trxWriteCallers[] = $fname;
1332  }
1333  }
1334 
1335  if ( $sql === self::PING_QUERY ) {
1336  $this->rttEstimate = $queryRuntime;
1337  }
1338 
1339  $this->trxProfiler->recordQueryCompletion(
1340  $queryProf,
1341  $startTime,
1342  $isEffectiveWrite,
1343  $isEffectiveWrite ? $this->affectedRows() : $this->numRows( $ret )
1344  );
1345  $this->queryLogger->debug( $sql, [
1346  'method' => $fname,
1347  'master' => $isMaster,
1348  'runtime' => $queryRuntime,
1349  ] );
1350 
1351  return $ret;
1352  }
1353 
1360  private function beginIfImplied( $sql, $fname ) {
1361  if (
1362  !$this->trxLevel &&
1363  $this->getFlag( self::DBO_TRX ) &&
1364  $this->isTransactableQuery( $sql )
1365  ) {
1366  $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
1367  $this->trxAutomatic = true;
1368  }
1369  }
1370 
1383  private function updateTrxWriteQueryTime( $sql, $runtime, $affected ) {
1384  // Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
1385  $indicativeOfReplicaRuntime = true;
1386  if ( $runtime > self::SLOW_WRITE_SEC ) {
1387  $verb = $this->getQueryVerb( $sql );
1388  // insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
1389  if ( $verb === 'INSERT' ) {
1390  $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS;
1391  } elseif ( $verb === 'REPLACE' ) {
1392  $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS / 2;
1393  }
1394  }
1395 
1396  $this->trxWriteDuration += $runtime;
1397  $this->trxWriteQueryCount += 1;
1398  $this->trxWriteAffectedRows += $affected;
1399  if ( $indicativeOfReplicaRuntime ) {
1400  $this->trxWriteAdjDuration += $runtime;
1401  $this->trxWriteAdjQueryCount += 1;
1402  }
1403  }
1404 
1412  private function assertTransactionStatus( $sql, $fname ) {
1413  $verb = $this->getQueryVerb( $sql );
1414  if ( $verb === 'USE' ) {
1415  throw new DBUnexpectedError( $this, "Got USE query; use selectDomain() instead." );
1416  }
1417 
1418  if ( $verb === 'ROLLBACK' ) { // transaction/savepoint
1419  return;
1420  }
1421 
1422  if ( $this->trxStatus < self::STATUS_TRX_OK ) {
1423  throw new DBTransactionStateError(
1424  $this,
1425  "Cannot execute query from $fname while transaction status is ERROR.",
1426  [],
1427  $this->trxStatusCause
1428  );
1429  } elseif ( $this->trxStatus === self::STATUS_TRX_OK && $this->trxStatusIgnoredCause ) {
1430  list( $iLastError, $iLastErrno, $iFname ) = $this->trxStatusIgnoredCause;
1431  call_user_func( $this->deprecationLogger,
1432  "Caller from $fname ignored an error originally raised from $iFname: " .
1433  "[$iLastErrno] $iLastError"
1434  );
1435  $this->trxStatusIgnoredCause = null;
1436  }
1437  }
1438 
1439  public function assertNoOpenTransactions() {
1440  if ( $this->explicitTrxActive() ) {
1441  throw new DBTransactionError(
1442  $this,
1443  "Explicit transaction still active. A caller may have caught an error. "
1444  . "Open transactions: " . $this->flatAtomicSectionList()
1445  );
1446  }
1447  }
1448 
1458  private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
1459  # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
1460  # Dropped connections also mean that named locks are automatically released.
1461  # Only allow error suppression in autocommit mode or when the lost transaction
1462  # didn't matter anyway (aside from DBO_TRX snapshot loss).
1463  if ( $this->namedLocksHeld ) {
1464  return false; // possible critical section violation
1465  } elseif ( $this->sessionTempTables ) {
1466  return false; // tables might be queried latter
1467  } elseif ( $sql === 'COMMIT' ) {
1468  return !$priorWritesPending; // nothing written anyway? (T127428)
1469  } elseif ( $sql === 'ROLLBACK' ) {
1470  return true; // transaction lost...which is also what was requested :)
1471  } elseif ( $this->explicitTrxActive() ) {
1472  return false; // don't drop atomocity and explicit snapshots
1473  } elseif ( $priorWritesPending ) {
1474  return false; // prior writes lost from implicit transaction
1475  }
1476 
1477  return true;
1478  }
1479 
1483  private function handleSessionLossPreconnect() {
1484  // Clean up tracking of session-level things...
1485  // https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html
1486  // https://www.postgresql.org/docs/9.2/static/sql-createtable.html (ignoring ON COMMIT)
1487  $this->sessionTempTables = [];
1488  // https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
1489  // https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
1490  $this->namedLocksHeld = [];
1491  // Session loss implies transaction loss
1492  $this->trxLevel = 0;
1493  $this->trxAtomicCounter = 0;
1494  $this->trxIdleCallbacks = []; // T67263; transaction already lost
1495  $this->trxPreCommitCallbacks = []; // T67263; transaction already lost
1496  // @note: leave trxRecurringCallbacks in place
1497  if ( $this->trxDoneWrites ) {
1498  $this->trxProfiler->transactionWritingOut(
1499  $this->server,
1500  $this->getDomainID(),
1501  $this->trxShortId,
1502  $this->pendingWriteQueryDuration( self::ESTIMATE_TOTAL ),
1503  $this->trxWriteAffectedRows
1504  );
1505  }
1506  }
1507 
1511  private function handleSessionLossPostconnect() {
1512  try {
1513  // Handle callbacks in trxEndCallbacks, e.g. onTransactionResolution().
1514  // If callback suppression is set then the array will remain unhandled.
1515  $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
1516  } catch ( Exception $ex ) {
1517  // Already logged; move on...
1518  }
1519  try {
1520  // Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener()
1521  $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
1522  } catch ( Exception $ex ) {
1523  // Already logged; move on...
1524  }
1525  }
1526 
1537  protected function wasQueryTimeout( $error, $errno ) {
1538  return false;
1539  }
1540 
1552  public function reportQueryError( $error, $errno, $sql, $fname, $ignoreErrors = false ) {
1553  if ( $ignoreErrors ) {
1554  $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
1555  } else {
1556  $exception = $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
1557 
1558  throw $exception;
1559  }
1560  }
1561 
1569  private function getQueryExceptionAndLog( $error, $errno, $sql, $fname ) {
1570  $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
1571  $this->queryLogger->error(
1572  "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
1573  $this->getLogContext( [
1574  'method' => __METHOD__,
1575  'errno' => $errno,
1576  'error' => $error,
1577  'sql1line' => $sql1line,
1578  'fname' => $fname,
1579  'trace' => ( new RuntimeException() )->getTraceAsString()
1580  ] )
1581  );
1582  $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
1583  $wasQueryTimeout = $this->wasQueryTimeout( $error, $errno );
1584  if ( $wasQueryTimeout ) {
1585  $e = new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
1586  } else {
1587  $e = new DBQueryError( $this, $error, $errno, $sql, $fname );
1588  }
1589 
1590  return $e;
1591  }
1592 
1593  public function freeResult( $res ) {
1594  }
1595 
1596  public function selectField(
1597  $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1598  ) {
1599  if ( $var === '*' ) { // sanity
1600  throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1601  }
1602 
1603  if ( !is_array( $options ) ) {
1604  $options = [ $options ];
1605  }
1606 
1607  $options['LIMIT'] = 1;
1608 
1609  $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1610  if ( $res === false || !$this->numRows( $res ) ) {
1611  return false;
1612  }
1613 
1614  $row = $this->fetchRow( $res );
1615 
1616  if ( $row !== false ) {
1617  return reset( $row );
1618  } else {
1619  return false;
1620  }
1621  }
1622 
1623  public function selectFieldValues(
1624  $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1625  ) {
1626  if ( $var === '*' ) { // sanity
1627  throw new DBUnexpectedError( $this, "Cannot use a * field" );
1628  } elseif ( !is_string( $var ) ) { // sanity
1629  throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1630  }
1631 
1632  if ( !is_array( $options ) ) {
1633  $options = [ $options ];
1634  }
1635 
1636  $res = $this->select( $table, [ 'value' => $var ], $cond, $fname, $options, $join_conds );
1637  if ( $res === false ) {
1638  return false;
1639  }
1640 
1641  $values = [];
1642  foreach ( $res as $row ) {
1643  $values[] = $row->value;
1644  }
1645 
1646  return $values;
1647  }
1648 
1658  protected function makeSelectOptions( $options ) {
1659  $preLimitTail = $postLimitTail = '';
1660  $startOpts = '';
1661 
1662  $noKeyOptions = [];
1663 
1664  foreach ( $options as $key => $option ) {
1665  if ( is_numeric( $key ) ) {
1666  $noKeyOptions[$option] = true;
1667  }
1668  }
1669 
1670  $preLimitTail .= $this->makeGroupByWithHaving( $options );
1671 
1672  $preLimitTail .= $this->makeOrderBy( $options );
1673 
1674  if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1675  $postLimitTail .= ' FOR UPDATE';
1676  }
1677 
1678  if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1679  $postLimitTail .= ' LOCK IN SHARE MODE';
1680  }
1681 
1682  if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1683  $startOpts .= 'DISTINCT';
1684  }
1685 
1686  # Various MySQL extensions
1687  if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1688  $startOpts .= ' /*! STRAIGHT_JOIN */';
1689  }
1690 
1691  if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
1692  $startOpts .= ' HIGH_PRIORITY';
1693  }
1694 
1695  if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1696  $startOpts .= ' SQL_BIG_RESULT';
1697  }
1698 
1699  if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1700  $startOpts .= ' SQL_BUFFER_RESULT';
1701  }
1702 
1703  if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1704  $startOpts .= ' SQL_SMALL_RESULT';
1705  }
1706 
1707  if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1708  $startOpts .= ' SQL_CALC_FOUND_ROWS';
1709  }
1710 
1711  if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
1712  $startOpts .= ' SQL_CACHE';
1713  }
1714 
1715  if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
1716  $startOpts .= ' SQL_NO_CACHE';
1717  }
1718 
1719  if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1720  $useIndex = $this->useIndexClause( $options['USE INDEX'] );
1721  } else {
1722  $useIndex = '';
1723  }
1724  if ( isset( $options['IGNORE INDEX'] ) && is_string( $options['IGNORE INDEX'] ) ) {
1725  $ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
1726  } else {
1727  $ignoreIndex = '';
1728  }
1729 
1730  return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1731  }
1732 
1741  protected function makeGroupByWithHaving( $options ) {
1742  $sql = '';
1743  if ( isset( $options['GROUP BY'] ) ) {
1744  $gb = is_array( $options['GROUP BY'] )
1745  ? implode( ',', $options['GROUP BY'] )
1746  : $options['GROUP BY'];
1747  $sql .= ' GROUP BY ' . $gb;
1748  }
1749  if ( isset( $options['HAVING'] ) ) {
1750  $having = is_array( $options['HAVING'] )
1751  ? $this->makeList( $options['HAVING'], self::LIST_AND )
1752  : $options['HAVING'];
1753  $sql .= ' HAVING ' . $having;
1754  }
1755 
1756  return $sql;
1757  }
1758 
1767  protected function makeOrderBy( $options ) {
1768  if ( isset( $options['ORDER BY'] ) ) {
1769  $ob = is_array( $options['ORDER BY'] )
1770  ? implode( ',', $options['ORDER BY'] )
1771  : $options['ORDER BY'];
1772 
1773  return ' ORDER BY ' . $ob;
1774  }
1775 
1776  return '';
1777  }
1778 
1779  public function select(
1780  $table, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1781  ) {
1782  $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1783 
1784  return $this->query( $sql, $fname );
1785  }
1786 
1787  public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1788  $options = [], $join_conds = []
1789  ) {
1790  if ( is_array( $vars ) ) {
1791  $fields = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1792  } else {
1793  $fields = $vars;
1794  }
1795 
1796  $options = (array)$options;
1797  $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1798  ? $options['USE INDEX']
1799  : [];
1800  $ignoreIndexes = (
1801  isset( $options['IGNORE INDEX'] ) &&
1802  is_array( $options['IGNORE INDEX'] )
1803  )
1804  ? $options['IGNORE INDEX']
1805  : [];
1806 
1807  if (
1810  ) {
1811  // Some DB types (postgres/oracle) disallow FOR UPDATE with aggregate
1812  // functions. Discourage use of such queries to encourage compatibility.
1813  call_user_func(
1814  $this->deprecationLogger,
1815  __METHOD__ . ": aggregation used with a locking SELECT ($fname)."
1816  );
1817  }
1818 
1819  if ( is_array( $table ) ) {
1820  $from = ' FROM ' .
1822  $table, $useIndexes, $ignoreIndexes, $join_conds );
1823  } elseif ( $table != '' ) {
1824  $from = ' FROM ' .
1826  [ $table ], $useIndexes, $ignoreIndexes, [] );
1827  } else {
1828  $from = '';
1829  }
1830 
1831  list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
1832  $this->makeSelectOptions( $options );
1833 
1834  if ( is_array( $conds ) ) {
1835  $conds = $this->makeList( $conds, self::LIST_AND );
1836  }
1837 
1838  if ( $conds === null || $conds === false ) {
1839  $this->queryLogger->warning(
1840  __METHOD__
1841  . ' called from '
1842  . $fname
1843  . ' with incorrect parameters: $conds must be a string or an array'
1844  );
1845  $conds = '';
1846  }
1847 
1848  if ( $conds === '' || $conds === '*' ) {
1849  $sql = "SELECT $startOpts $fields $from $useIndex $ignoreIndex $preLimitTail";
1850  } elseif ( is_string( $conds ) ) {
1851  $sql = "SELECT $startOpts $fields $from $useIndex $ignoreIndex " .
1852  "WHERE $conds $preLimitTail";
1853  } else {
1854  throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
1855  }
1856 
1857  if ( isset( $options['LIMIT'] ) ) {
1858  $sql = $this->limitResult( $sql, $options['LIMIT'],
1859  $options['OFFSET'] ?? false );
1860  }
1861  $sql = "$sql $postLimitTail";
1862 
1863  if ( isset( $options['EXPLAIN'] ) ) {
1864  $sql = 'EXPLAIN ' . $sql;
1865  }
1866 
1867  return $sql;
1868  }
1869 
1870  public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1871  $options = [], $join_conds = []
1872  ) {
1873  $options = (array)$options;
1874  $options['LIMIT'] = 1;
1875  $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1876 
1877  if ( $res === false ) {
1878  return false;
1879  }
1880 
1881  if ( !$this->numRows( $res ) ) {
1882  return false;
1883  }
1884 
1885  $obj = $this->fetchObject( $res );
1886 
1887  return $obj;
1888  }
1889 
1890  public function estimateRowCount(
1891  $table, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1892  ) {
1893  $conds = $this->normalizeConditions( $conds, $fname );
1894  $column = $this->extractSingleFieldFromList( $var );
1895  if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
1896  $conds[] = "$column IS NOT NULL";
1897  }
1898 
1899  $res = $this->select(
1900  $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options, $join_conds
1901  );
1902  $row = $res ? $this->fetchRow( $res ) : [];
1903 
1904  return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
1905  }
1906 
1907  public function selectRowCount(
1908  $tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1909  ) {
1910  $conds = $this->normalizeConditions( $conds, $fname );
1911  $column = $this->extractSingleFieldFromList( $var );
1912  if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
1913  $conds[] = "$column IS NOT NULL";
1914  }
1915 
1916  $res = $this->select(
1917  [
1918  'tmp_count' => $this->buildSelectSubquery(
1919  $tables,
1920  '1',
1921  $conds,
1922  $fname,
1923  $options,
1924  $join_conds
1925  )
1926  ],
1927  [ 'rowcount' => 'COUNT(*)' ],
1928  [],
1929  $fname
1930  );
1931  $row = $res ? $this->fetchRow( $res ) : [];
1932 
1933  return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
1934  }
1935 
1941  $options = (array)$options;
1942  foreach ( [ 'FOR UPDATE', 'LOCK IN SHARE MODE' ] as $lock ) {
1943  if ( in_array( $lock, $options, true ) ) {
1944  return true;
1945  }
1946  }
1947 
1948  return false;
1949  }
1950 
1956  private function selectFieldsOrOptionsAggregate( $fields, $options ) {
1957  foreach ( (array)$options as $key => $value ) {
1958  if ( is_string( $key ) ) {
1959  if ( preg_match( '/^(?:GROUP BY|HAVING)$/i', $key ) ) {
1960  return true;
1961  }
1962  } elseif ( is_string( $value ) ) {
1963  if ( preg_match( '/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
1964  return true;
1965  }
1966  }
1967  }
1968 
1969  $regex = '/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
1970  foreach ( (array)$fields as $field ) {
1971  if ( is_string( $field ) && preg_match( $regex, $field ) ) {
1972  return true;
1973  }
1974  }
1975 
1976  return false;
1977  }
1978 
1984  final protected function normalizeConditions( $conds, $fname ) {
1985  if ( $conds === null || $conds === false ) {
1986  $this->queryLogger->warning(
1987  __METHOD__
1988  . ' called from '
1989  . $fname
1990  . ' with incorrect parameters: $conds must be a string or an array'
1991  );
1992  $conds = '';
1993  }
1994 
1995  if ( !is_array( $conds ) ) {
1996  $conds = ( $conds === '' ) ? [] : [ $conds ];
1997  }
1998 
1999  return $conds;
2000  }
2001 
2007  final protected function extractSingleFieldFromList( $var ) {
2008  if ( is_array( $var ) ) {
2009  if ( !$var ) {
2010  $column = null;
2011  } elseif ( count( $var ) == 1 ) {
2012  $column = $var[0] ?? reset( $var );
2013  } else {
2014  throw new DBUnexpectedError( $this, __METHOD__ . ': got multiple columns.' );
2015  }
2016  } else {
2017  $column = $var;
2018  }
2019 
2020  return $column;
2021  }
2022 
2023  public function lockForUpdate(
2024  $table, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
2025  ) {
2026  if ( !$this->trxLevel && !$this->getFlag( self::DBO_TRX ) ) {
2027  throw new DBUnexpectedError(
2028  $this,
2029  __METHOD__ . ': no transaction is active nor is DBO_TRX set'
2030  );
2031  }
2032 
2033  $options = (array)$options;
2034  $options[] = 'FOR UPDATE';
2035 
2036  return $this->selectRowCount( $table, '*', $conds, $fname, $options, $join_conds );
2037  }
2038 
2047  protected static function generalizeSQL( $sql ) {
2048  # This does the same as the regexp below would do, but in such a way
2049  # as to avoid crashing php on some large strings.
2050  # $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
2051 
2052  $sql = str_replace( "\\\\", '', $sql );
2053  $sql = str_replace( "\\'", '', $sql );
2054  $sql = str_replace( "\\\"", '', $sql );
2055  $sql = preg_replace( "/'.*'/s", "'X'", $sql );
2056  $sql = preg_replace( '/".*"/s', "'X'", $sql );
2057 
2058  # All newlines, tabs, etc replaced by single space
2059  $sql = preg_replace( '/\s+/', ' ', $sql );
2060 
2061  # All numbers => N,
2062  # except the ones surrounded by characters, e.g. l10n
2063  $sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
2064  $sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
2065 
2066  return $sql;
2067  }
2068 
2069  public function fieldExists( $table, $field, $fname = __METHOD__ ) {
2070  $info = $this->fieldInfo( $table, $field );
2071 
2072  return (bool)$info;
2073  }
2074 
2075  public function indexExists( $table, $index, $fname = __METHOD__ ) {
2076  if ( !$this->tableExists( $table ) ) {
2077  return null;
2078  }
2079 
2080  $info = $this->indexInfo( $table, $index, $fname );
2081  if ( is_null( $info ) ) {
2082  return null;
2083  } else {
2084  return $info !== false;
2085  }
2086  }
2087 
2088  abstract public function tableExists( $table, $fname = __METHOD__ );
2089 
2090  public function indexUnique( $table, $index ) {
2091  $indexInfo = $this->indexInfo( $table, $index );
2092 
2093  if ( !$indexInfo ) {
2094  return null;
2095  }
2096 
2097  return !$indexInfo[0]->Non_unique;
2098  }
2099 
2106  protected function makeInsertOptions( $options ) {
2107  return implode( ' ', $options );
2108  }
2109 
2110  public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
2111  # No rows to insert, easy just return now
2112  if ( !count( $a ) ) {
2113  return true;
2114  }
2115 
2116  $table = $this->tableName( $table );
2117 
2118  if ( !is_array( $options ) ) {
2119  $options = [ $options ];
2120  }
2121 
2122  $options = $this->makeInsertOptions( $options );
2123 
2124  if ( isset( $a[0] ) && is_array( $a[0] ) ) {
2125  $multi = true;
2126  $keys = array_keys( $a[0] );
2127  } else {
2128  $multi = false;
2129  $keys = array_keys( $a );
2130  }
2131 
2132  $sql = 'INSERT ' . $options .
2133  " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
2134 
2135  if ( $multi ) {
2136  $first = true;
2137  foreach ( $a as $row ) {
2138  if ( $first ) {
2139  $first = false;
2140  } else {
2141  $sql .= ',';
2142  }
2143  $sql .= '(' . $this->makeList( $row ) . ')';
2144  }
2145  } else {
2146  $sql .= '(' . $this->makeList( $a ) . ')';
2147  }
2148 
2149  $this->query( $sql, $fname );
2150 
2151  return true;
2152  }
2153 
2160  protected function makeUpdateOptionsArray( $options ) {
2161  if ( !is_array( $options ) ) {
2162  $options = [ $options ];
2163  }
2164 
2165  $opts = [];
2166 
2167  if ( in_array( 'IGNORE', $options ) ) {
2168  $opts[] = 'IGNORE';
2169  }
2170 
2171  return $opts;
2172  }
2173 
2180  protected function makeUpdateOptions( $options ) {
2181  $opts = $this->makeUpdateOptionsArray( $options );
2182 
2183  return implode( ' ', $opts );
2184  }
2185 
2186  public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
2187  $table = $this->tableName( $table );
2188  $opts = $this->makeUpdateOptions( $options );
2189  $sql = "UPDATE $opts $table SET " . $this->makeList( $values, self::LIST_SET );
2190 
2191  if ( $conds !== [] && $conds !== '*' ) {
2192  $sql .= " WHERE " . $this->makeList( $conds, self::LIST_AND );
2193  }
2194 
2195  $this->query( $sql, $fname );
2196 
2197  return true;
2198  }
2199 
2200  public function makeList( $a, $mode = self::LIST_COMMA ) {
2201  if ( !is_array( $a ) ) {
2202  throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
2203  }
2204 
2205  $first = true;
2206  $list = '';
2207 
2208  foreach ( $a as $field => $value ) {
2209  if ( !$first ) {
2210  if ( $mode == self::LIST_AND ) {
2211  $list .= ' AND ';
2212  } elseif ( $mode == self::LIST_OR ) {
2213  $list .= ' OR ';
2214  } else {
2215  $list .= ',';
2216  }
2217  } else {
2218  $first = false;
2219  }
2220 
2221  if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
2222  $list .= "($value)";
2223  } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
2224  $list .= "$value";
2225  } elseif (
2226  ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
2227  ) {
2228  // Remove null from array to be handled separately if found
2229  $includeNull = false;
2230  foreach ( array_keys( $value, null, true ) as $nullKey ) {
2231  $includeNull = true;
2232  unset( $value[$nullKey] );
2233  }
2234  if ( count( $value ) == 0 && !$includeNull ) {
2235  throw new InvalidArgumentException(
2236  __METHOD__ . ": empty input for field $field" );
2237  } elseif ( count( $value ) == 0 ) {
2238  // only check if $field is null
2239  $list .= "$field IS NULL";
2240  } else {
2241  // IN clause contains at least one valid element
2242  if ( $includeNull ) {
2243  // Group subconditions to ensure correct precedence
2244  $list .= '(';
2245  }
2246  if ( count( $value ) == 1 ) {
2247  // Special-case single values, as IN isn't terribly efficient
2248  // Don't necessarily assume the single key is 0; we don't
2249  // enforce linear numeric ordering on other arrays here.
2250  $value = array_values( $value )[0];
2251  $list .= $field . " = " . $this->addQuotes( $value );
2252  } else {
2253  $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
2254  }
2255  // if null present in array, append IS NULL
2256  if ( $includeNull ) {
2257  $list .= " OR $field IS NULL)";
2258  }
2259  }
2260  } elseif ( $value === null ) {
2261  if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
2262  $list .= "$field IS ";
2263  } elseif ( $mode == self::LIST_SET ) {
2264  $list .= "$field = ";
2265  }
2266  $list .= 'NULL';
2267  } else {
2268  if (
2269  $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
2270  ) {
2271  $list .= "$field = ";
2272  }
2273  $list .= $mode == self::LIST_NAMES ? $value : $this->addQuotes( $value );
2274  }
2275  }
2276 
2277  return $list;
2278  }
2279 
2280  public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
2281  $conds = [];
2282 
2283  foreach ( $data as $base => $sub ) {
2284  if ( count( $sub ) ) {
2285  $conds[] = $this->makeList(
2286  [ $baseKey => $base, $subKey => array_keys( $sub ) ],
2287  self::LIST_AND );
2288  }
2289  }
2290 
2291  if ( $conds ) {
2292  return $this->makeList( $conds, self::LIST_OR );
2293  } else {
2294  // Nothing to search for...
2295  return false;
2296  }
2297  }
2298 
2299  public function aggregateValue( $valuedata, $valuename = 'value' ) {
2300  return $valuename;
2301  }
2302 
2303  public function bitNot( $field ) {
2304  return "(~$field)";
2305  }
2306 
2307  public function bitAnd( $fieldLeft, $fieldRight ) {
2308  return "($fieldLeft & $fieldRight)";
2309  }
2310 
2311  public function bitOr( $fieldLeft, $fieldRight ) {
2312  return "($fieldLeft | $fieldRight)";
2313  }
2314 
2315  public function buildConcat( $stringList ) {
2316  return 'CONCAT(' . implode( ',', $stringList ) . ')';
2317  }
2318 
2319  public function buildGroupConcatField(
2320  $delim, $table, $field, $conds = '', $join_conds = []
2321  ) {
2322  $fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
2323 
2324  return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
2325  }
2326 
2327  public function buildSubstring( $input, $startPosition, $length = null ) {
2328  $this->assertBuildSubstringParams( $startPosition, $length );
2329  $functionBody = "$input FROM $startPosition";
2330  if ( $length !== null ) {
2331  $functionBody .= " FOR $length";
2332  }
2333  return 'SUBSTRING(' . $functionBody . ')';
2334  }
2335 
2348  protected function assertBuildSubstringParams( $startPosition, $length ) {
2349  if ( !is_int( $startPosition ) || $startPosition <= 0 ) {
2350  throw new InvalidArgumentException(
2351  '$startPosition must be a positive integer'
2352  );
2353  }
2354  if ( !( is_int( $length ) && $length >= 0 || $length === null ) ) {
2355  throw new InvalidArgumentException(
2356  '$length must be null or an integer greater than or equal to 0'
2357  );
2358  }
2359  }
2360 
2361  public function buildStringCast( $field ) {
2362  // In theory this should work for any standards-compliant
2363  // SQL implementation, although it may not be the best way to do it.
2364  return "CAST( $field AS CHARACTER )";
2365  }
2366 
2367  public function buildIntegerCast( $field ) {
2368  return 'CAST( ' . $field . ' AS INTEGER )';
2369  }
2370 
2371  public function buildSelectSubquery(
2372  $table, $vars, $conds = '', $fname = __METHOD__,
2373  $options = [], $join_conds = []
2374  ) {
2375  return new Subquery(
2376  $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
2377  );
2378  }
2379 
2380  public function databasesAreIndependent() {
2381  return false;
2382  }
2383 
2384  final public function selectDB( $db ) {
2385  $this->selectDomain( new DatabaseDomain(
2386  $db,
2387  $this->currentDomain->getSchema(),
2388  $this->currentDomain->getTablePrefix()
2389  ) );
2390 
2391  return true;
2392  }
2393 
2394  final public function selectDomain( $domain ) {
2395  $this->doSelectDomain( DatabaseDomain::newFromId( $domain ) );
2396  }
2397 
2398  protected function doSelectDomain( DatabaseDomain $domain ) {
2399  $this->currentDomain = $domain;
2400  }
2401 
2402  public function getDBname() {
2403  return $this->currentDomain->getDatabase();
2404  }
2405 
2406  public function getServer() {
2407  return $this->server;
2408  }
2409 
2410  public function tableName( $name, $format = 'quoted' ) {
2411  if ( $name instanceof Subquery ) {
2412  throw new DBUnexpectedError(
2413  $this,
2414  __METHOD__ . ': got Subquery instance when expecting a string.'
2415  );
2416  }
2417 
2418  # Skip the entire process when we have a string quoted on both ends.
2419  # Note that we check the end so that we will still quote any use of
2420  # use of `database`.table. But won't break things if someone wants
2421  # to query a database table with a dot in the name.
2422  if ( $this->isQuotedIdentifier( $name ) ) {
2423  return $name;
2424  }
2425 
2426  # Lets test for any bits of text that should never show up in a table
2427  # name. Basically anything like JOIN or ON which are actually part of
2428  # SQL queries, but may end up inside of the table value to combine
2429  # sql. Such as how the API is doing.
2430  # Note that we use a whitespace test rather than a \b test to avoid
2431  # any remote case where a word like on may be inside of a table name
2432  # surrounded by symbols which may be considered word breaks.
2433  if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
2434  $this->queryLogger->warning(
2435  __METHOD__ . ": use of subqueries is not supported this way.",
2436  [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
2437  );
2438 
2439  return $name;
2440  }
2441 
2442  # Split database and table into proper variables.
2443  list( $database, $schema, $prefix, $table ) = $this->qualifiedTableComponents( $name );
2444 
2445  # Quote $table and apply the prefix if not quoted.
2446  # $tableName might be empty if this is called from Database::replaceVars()
2447  $tableName = "{$prefix}{$table}";
2448  if ( $format === 'quoted'
2449  && !$this->isQuotedIdentifier( $tableName )
2450  && $tableName !== ''
2451  ) {
2452  $tableName = $this->addIdentifierQuotes( $tableName );
2453  }
2454 
2455  # Quote $schema and $database and merge them with the table name if needed
2456  $tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
2457  $tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
2458 
2459  return $tableName;
2460  }
2461 
2468  protected function qualifiedTableComponents( $name ) {
2469  # We reverse the explode so that database.table and table both output the correct table.
2470  $dbDetails = explode( '.', $name, 3 );
2471  if ( count( $dbDetails ) == 3 ) {
2472  list( $database, $schema, $table ) = $dbDetails;
2473  # We don't want any prefix added in this case
2474  $prefix = '';
2475  } elseif ( count( $dbDetails ) == 2 ) {
2476  list( $database, $table ) = $dbDetails;
2477  # We don't want any prefix added in this case
2478  $prefix = '';
2479  # In dbs that support it, $database may actually be the schema
2480  # but that doesn't affect any of the functionality here
2481  $schema = '';
2482  } else {
2483  list( $table ) = $dbDetails;
2484  if ( isset( $this->tableAliases[$table] ) ) {
2485  $database = $this->tableAliases[$table]['dbname'];
2486  $schema = is_string( $this->tableAliases[$table]['schema'] )
2487  ? $this->tableAliases[$table]['schema']
2488  : $this->relationSchemaQualifier();
2489  $prefix = is_string( $this->tableAliases[$table]['prefix'] )
2490  ? $this->tableAliases[$table]['prefix']
2491  : $this->tablePrefix();
2492  } else {
2493  $database = '';
2494  $schema = $this->relationSchemaQualifier(); # Default schema
2495  $prefix = $this->tablePrefix(); # Default prefix
2496  }
2497  }
2498 
2499  return [ $database, $schema, $prefix, $table ];
2500  }
2501 
2508  private function prependDatabaseOrSchema( $namespace, $relation, $format ) {
2509  if ( strlen( $namespace ) ) {
2510  if ( $format === 'quoted' && !$this->isQuotedIdentifier( $namespace ) ) {
2511  $namespace = $this->addIdentifierQuotes( $namespace );
2512  }
2513  $relation = $namespace . '.' . $relation;
2514  }
2515 
2516  return $relation;
2517  }
2518 
2519  public function tableNames() {
2520  $inArray = func_get_args();
2521  $retVal = [];
2522 
2523  foreach ( $inArray as $name ) {
2524  $retVal[$name] = $this->tableName( $name );
2525  }
2526 
2527  return $retVal;
2528  }
2529 
2530  public function tableNamesN() {
2531  $inArray = func_get_args();
2532  $retVal = [];
2533 
2534  foreach ( $inArray as $name ) {
2535  $retVal[] = $this->tableName( $name );
2536  }
2537 
2538  return $retVal;
2539  }
2540 
2552  protected function tableNameWithAlias( $table, $alias = false ) {
2553  if ( is_string( $table ) ) {
2554  $quotedTable = $this->tableName( $table );
2555  } elseif ( $table instanceof Subquery ) {
2556  $quotedTable = (string)$table;
2557  } else {
2558  throw new InvalidArgumentException( "Table must be a string or Subquery." );
2559  }
2560 
2561  if ( $alias === false || $alias === $table ) {
2562  if ( $table instanceof Subquery ) {
2563  throw new InvalidArgumentException( "Subquery table missing alias." );
2564  }
2565 
2566  return $quotedTable;
2567  } else {
2568  return $quotedTable . ' ' . $this->addIdentifierQuotes( $alias );
2569  }
2570  }
2571 
2578  protected function tableNamesWithAlias( $tables ) {
2579  $retval = [];
2580  foreach ( $tables as $alias => $table ) {
2581  if ( is_numeric( $alias ) ) {
2582  $alias = $table;
2583  }
2584  $retval[] = $this->tableNameWithAlias( $table, $alias );
2585  }
2586 
2587  return $retval;
2588  }
2589 
2598  protected function fieldNameWithAlias( $name, $alias = false ) {
2599  if ( !$alias || (string)$alias === (string)$name ) {
2600  return $name;
2601  } else {
2602  return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
2603  }
2604  }
2605 
2612  protected function fieldNamesWithAlias( $fields ) {
2613  $retval = [];
2614  foreach ( $fields as $alias => $field ) {
2615  if ( is_numeric( $alias ) ) {
2616  $alias = $field;
2617  }
2618  $retval[] = $this->fieldNameWithAlias( $field, $alias );
2619  }
2620 
2621  return $retval;
2622  }
2623 
2635  $tables, $use_index = [], $ignore_index = [], $join_conds = []
2636  ) {
2637  $ret = [];
2638  $retJOIN = [];
2639  $use_index = (array)$use_index;
2640  $ignore_index = (array)$ignore_index;
2641  $join_conds = (array)$join_conds;
2642 
2643  foreach ( $tables as $alias => $table ) {
2644  if ( !is_string( $alias ) ) {
2645  // No alias? Set it equal to the table name
2646  $alias = $table;
2647  }
2648 
2649  if ( is_array( $table ) ) {
2650  // A parenthesized group
2651  if ( count( $table ) > 1 ) {
2652  $joinedTable = '(' .
2654  $table, $use_index, $ignore_index, $join_conds ) . ')';
2655  } else {
2656  // Degenerate case
2657  $innerTable = reset( $table );
2658  $innerAlias = key( $table );
2659  $joinedTable = $this->tableNameWithAlias(
2660  $innerTable,
2661  is_string( $innerAlias ) ? $innerAlias : $innerTable
2662  );
2663  }
2664  } else {
2665  $joinedTable = $this->tableNameWithAlias( $table, $alias );
2666  }
2667 
2668  // Is there a JOIN clause for this table?
2669  if ( isset( $join_conds[$alias] ) ) {
2670  list( $joinType, $conds ) = $join_conds[$alias];
2671  $tableClause = $joinType;
2672  $tableClause .= ' ' . $joinedTable;
2673  if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
2674  $use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
2675  if ( $use != '' ) {
2676  $tableClause .= ' ' . $use;
2677  }
2678  }
2679  if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
2680  $ignore = $this->ignoreIndexClause(
2681  implode( ',', (array)$ignore_index[$alias] ) );
2682  if ( $ignore != '' ) {
2683  $tableClause .= ' ' . $ignore;
2684  }
2685  }
2686  $on = $this->makeList( (array)$conds, self::LIST_AND );
2687  if ( $on != '' ) {
2688  $tableClause .= ' ON (' . $on . ')';
2689  }
2690 
2691  $retJOIN[] = $tableClause;
2692  } elseif ( isset( $use_index[$alias] ) ) {
2693  // Is there an INDEX clause for this table?
2694  $tableClause = $joinedTable;
2695  $tableClause .= ' ' . $this->useIndexClause(
2696  implode( ',', (array)$use_index[$alias] )
2697  );
2698 
2699  $ret[] = $tableClause;
2700  } elseif ( isset( $ignore_index[$alias] ) ) {
2701  // Is there an INDEX clause for this table?
2702  $tableClause = $joinedTable;
2703  $tableClause .= ' ' . $this->ignoreIndexClause(
2704  implode( ',', (array)$ignore_index[$alias] )
2705  );
2706 
2707  $ret[] = $tableClause;
2708  } else {
2709  $tableClause = $joinedTable;
2710 
2711  $ret[] = $tableClause;
2712  }
2713  }
2714 
2715  // We can't separate explicit JOIN clauses with ',', use ' ' for those
2716  $implicitJoins = implode( ',', $ret );
2717  $explicitJoins = implode( ' ', $retJOIN );
2718 
2719  // Compile our final table clause
2720  return implode( ' ', [ $implicitJoins, $explicitJoins ] );
2721  }
2722 
2729  protected function indexName( $index ) {
2730  return $this->indexAliases[$index] ?? $index;
2731  }
2732 
2733  public function addQuotes( $s ) {
2734  if ( $s instanceof Blob ) {
2735  $s = $s->fetch();
2736  }
2737  if ( $s === null ) {
2738  return 'NULL';
2739  } elseif ( is_bool( $s ) ) {
2740  return (int)$s;
2741  } else {
2742  # This will also quote numeric values. This should be harmless,
2743  # and protects against weird problems that occur when they really
2744  # _are_ strings such as article titles and string->number->string
2745  # conversion is not 1:1.
2746  return "'" . $this->strencode( $s ) . "'";
2747  }
2748  }
2749 
2750  public function addIdentifierQuotes( $s ) {
2751  return '"' . str_replace( '"', '""', $s ) . '"';
2752  }
2753 
2763  public function isQuotedIdentifier( $name ) {
2764  return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
2765  }
2766 
2772  protected function escapeLikeInternal( $s, $escapeChar = '`' ) {
2773  return str_replace( [ $escapeChar, '%', '_' ],
2774  [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ],
2775  $s );
2776  }
2777 
2778  public function buildLike() {
2779  $params = func_get_args();
2780 
2781  if ( count( $params ) > 0 && is_array( $params[0] ) ) {
2782  $params = $params[0];
2783  }
2784 
2785  $s = '';
2786 
2787  // We use ` instead of \ as the default LIKE escape character, since addQuotes()
2788  // may escape backslashes, creating problems of double escaping. The `
2789  // character has good cross-DBMS compatibility, avoiding special operators
2790  // in MS SQL like ^ and %
2791  $escapeChar = '`';
2792 
2793  foreach ( $params as $value ) {
2794  if ( $value instanceof LikeMatch ) {
2795  $s .= $value->toString();
2796  } else {
2797  $s .= $this->escapeLikeInternal( $value, $escapeChar );
2798  }
2799  }
2800 
2801  return ' LIKE ' .
2802  $this->addQuotes( $s ) . ' ESCAPE ' . $this->addQuotes( $escapeChar ) . ' ';
2803  }
2804 
2805  public function anyChar() {
2806  return new LikeMatch( '_' );
2807  }
2808 
2809  public function anyString() {
2810  return new LikeMatch( '%' );
2811  }
2812 
2813  public function nextSequenceValue( $seqName ) {
2814  return null;
2815  }
2816 
2827  public function useIndexClause( $index ) {
2828  return '';
2829  }
2830 
2841  public function ignoreIndexClause( $index ) {
2842  return '';
2843  }
2844 
2845  public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2846  if ( count( $rows ) == 0 ) {
2847  return;
2848  }
2849 
2850  $uniqueIndexes = (array)$uniqueIndexes;
2851  // Single row case
2852  if ( !is_array( reset( $rows ) ) ) {
2853  $rows = [ $rows ];
2854  }
2855 
2856  try {
2857  $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
2858  $affectedRowCount = 0;
2859  foreach ( $rows as $row ) {
2860  // Delete rows which collide with this one
2861  $indexWhereClauses = [];
2862  foreach ( $uniqueIndexes as $index ) {
2863  $indexColumns = (array)$index;
2864  $indexRowValues = array_intersect_key( $row, array_flip( $indexColumns ) );
2865  if ( count( $indexRowValues ) != count( $indexColumns ) ) {
2866  throw new DBUnexpectedError(
2867  $this,
2868  'New record does not provide all values for unique key (' .
2869  implode( ', ', $indexColumns ) . ')'
2870  );
2871  } elseif ( in_array( null, $indexRowValues, true ) ) {
2872  throw new DBUnexpectedError(
2873  $this,
2874  'New record has a null value for unique key (' .
2875  implode( ', ', $indexColumns ) . ')'
2876  );
2877  }
2878  $indexWhereClauses[] = $this->makeList( $indexRowValues, LIST_AND );
2879  }
2880 
2881  if ( $indexWhereClauses ) {
2882  $this->delete( $table, $this->makeList( $indexWhereClauses, LIST_OR ), $fname );
2883  $affectedRowCount += $this->affectedRows();
2884  }
2885 
2886  // Now insert the row
2887  $this->insert( $table, $row, $fname );
2888  $affectedRowCount += $this->affectedRows();
2889  }
2890  $this->endAtomic( $fname );
2891  $this->affectedRowCount = $affectedRowCount;
2892  } catch ( Exception $e ) {
2893  $this->cancelAtomic( $fname );
2894  throw $e;
2895  }
2896  }
2897 
2906  protected function nativeReplace( $table, $rows, $fname ) {
2907  $table = $this->tableName( $table );
2908 
2909  # Single row case
2910  if ( !is_array( reset( $rows ) ) ) {
2911  $rows = [ $rows ];
2912  }
2913 
2914  $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
2915  $first = true;
2916 
2917  foreach ( $rows as $row ) {
2918  if ( $first ) {
2919  $first = false;
2920  } else {
2921  $sql .= ',';
2922  }
2923 
2924  $sql .= '(' . $this->makeList( $row ) . ')';
2925  }
2926 
2927  $this->query( $sql, $fname );
2928  }
2929 
2930  public function upsert( $table, array $rows, $uniqueIndexes, array $set,
2931  $fname = __METHOD__
2932  ) {
2933  if ( $rows === [] ) {
2934  return true; // nothing to do
2935  }
2936 
2937  $uniqueIndexes = (array)$uniqueIndexes;
2938  if ( !is_array( reset( $rows ) ) ) {
2939  $rows = [ $rows ];
2940  }
2941 
2942  if ( count( $uniqueIndexes ) ) {
2943  $clauses = []; // list WHERE clauses that each identify a single row
2944  foreach ( $rows as $row ) {
2945  foreach ( $uniqueIndexes as $index ) {
2946  $index = is_array( $index ) ? $index : [ $index ]; // columns
2947  $rowKey = []; // unique key to this row
2948  foreach ( $index as $column ) {
2949  $rowKey[$column] = $row[$column];
2950  }
2951  $clauses[] = $this->makeList( $rowKey, self::LIST_AND );
2952  }
2953  }
2954  $where = [ $this->makeList( $clauses, self::LIST_OR ) ];
2955  } else {
2956  $where = false;
2957  }
2958 
2959  $affectedRowCount = 0;
2960  try {
2961  $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
2962  # Update any existing conflicting row(s)
2963  if ( $where !== false ) {
2964  $this->update( $table, $set, $where, $fname );
2965  $affectedRowCount += $this->affectedRows();
2966  }
2967  # Now insert any non-conflicting row(s)
2968  $this->insert( $table, $rows, $fname, [ 'IGNORE' ] );
2969  $affectedRowCount += $this->affectedRows();
2970  $this->endAtomic( $fname );
2971  $this->affectedRowCount = $affectedRowCount;
2972  } catch ( Exception $e ) {
2973  $this->cancelAtomic( $fname );
2974  throw $e;
2975  }
2976 
2977  return true;
2978  }
2979 
2980  public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2981  $fname = __METHOD__
2982  ) {
2983  if ( !$conds ) {
2984  throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
2985  }
2986 
2987  $delTable = $this->tableName( $delTable );
2988  $joinTable = $this->tableName( $joinTable );
2989  $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2990  if ( $conds != '*' ) {
2991  $sql .= 'WHERE ' . $this->makeList( $conds, self::LIST_AND );
2992  }
2993  $sql .= ')';
2994 
2995  $this->query( $sql, $fname );
2996  }
2997 
2998  public function textFieldSize( $table, $field ) {
2999  $table = $this->tableName( $table );
3000  $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
3001  $res = $this->query( $sql, __METHOD__ );
3002  $row = $this->fetchObject( $res );
3003 
3004  $m = [];
3005 
3006  if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
3007  $size = $m[1];
3008  } else {
3009  $size = -1;
3010  }
3011 
3012  return $size;
3013  }
3014 
3015  public function delete( $table, $conds, $fname = __METHOD__ ) {
3016  if ( !$conds ) {
3017  throw new DBUnexpectedError( $this, __METHOD__ . ' called with no conditions' );
3018  }
3019 
3020  $table = $this->tableName( $table );
3021  $sql = "DELETE FROM $table";
3022 
3023  if ( $conds != '*' ) {
3024  if ( is_array( $conds ) ) {
3025  $conds = $this->makeList( $conds, self::LIST_AND );
3026  }
3027  $sql .= ' WHERE ' . $conds;
3028  }
3029 
3030  $this->query( $sql, $fname );
3031 
3032  return true;
3033  }
3034 
3035  final public function insertSelect(
3036  $destTable, $srcTable, $varMap, $conds,
3037  $fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = []
3038  ) {
3039  static $hints = [ 'NO_AUTO_COLUMNS' ];
3040 
3041  $insertOptions = (array)$insertOptions;
3042  $selectOptions = (array)$selectOptions;
3043 
3044  if ( $this->cliMode && $this->isInsertSelectSafe( $insertOptions, $selectOptions ) ) {
3045  // For massive migrations with downtime, we don't want to select everything
3046  // into memory and OOM, so do all this native on the server side if possible.
3047  $this->nativeInsertSelect(
3048  $destTable,
3049  $srcTable,
3050  $varMap,
3051  $conds,
3052  $fname,
3053  array_diff( $insertOptions, $hints ),
3054  $selectOptions,
3055  $selectJoinConds
3056  );
3057  } else {
3058  $this->nonNativeInsertSelect(
3059  $destTable,
3060  $srcTable,
3061  $varMap,
3062  $conds,
3063  $fname,
3064  array_diff( $insertOptions, $hints ),
3065  $selectOptions,
3066  $selectJoinConds
3067  );
3068  }
3069 
3070  return true;
3071  }
3072 
3079  protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
3080  return true;
3081  }
3082 
3097  protected function nonNativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
3098  $fname = __METHOD__,
3099  $insertOptions = [], $selectOptions = [], $selectJoinConds = []
3100  ) {
3101  // For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
3102  // on only the master (without needing row-based-replication). It also makes it easy to
3103  // know how big the INSERT is going to be.
3104  $fields = [];
3105  foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
3106  $fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
3107  }
3108  $selectOptions[] = 'FOR UPDATE';
3109  $res = $this->select(
3110  $srcTable, implode( ',', $fields ), $conds, $fname, $selectOptions, $selectJoinConds
3111  );
3112  if ( !$res ) {
3113  return;
3114  }
3115 
3116  try {
3117  $affectedRowCount = 0;
3118  $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
3119  $rows = [];
3120  $ok = true;
3121  foreach ( $res as $row ) {
3122  $rows[] = (array)$row;
3123 
3124  // Avoid inserts that are too huge
3125  if ( count( $rows ) >= $this->nonNativeInsertSelectBatchSize ) {
3126  $ok = $this->insert( $destTable, $rows, $fname, $insertOptions );
3127  if ( !$ok ) {
3128  break;
3129  }
3130  $affectedRowCount += $this->affectedRows();
3131  $rows = [];
3132  }
3133  }
3134  if ( $rows && $ok ) {
3135  $ok = $this->insert( $destTable, $rows, $fname, $insertOptions );
3136  if ( $ok ) {
3137  $affectedRowCount += $this->affectedRows();
3138  }
3139  }
3140  if ( $ok ) {
3141  $this->endAtomic( $fname );
3142  $this->affectedRowCount = $affectedRowCount;
3143  } else {
3144  $this->cancelAtomic( $fname );
3145  }
3146  } catch ( Exception $e ) {
3147  $this->cancelAtomic( $fname );
3148  throw $e;
3149  }
3150  }
3151 
3166  protected function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
3167  $fname = __METHOD__,
3168  $insertOptions = [], $selectOptions = [], $selectJoinConds = []
3169  ) {
3170  $destTable = $this->tableName( $destTable );
3171 
3172  if ( !is_array( $insertOptions ) ) {
3173  $insertOptions = [ $insertOptions ];
3174  }
3175 
3176  $insertOptions = $this->makeInsertOptions( $insertOptions );
3177 
3178  $selectSql = $this->selectSQLText(
3179  $srcTable,
3180  array_values( $varMap ),
3181  $conds,
3182  $fname,
3183  $selectOptions,
3184  $selectJoinConds
3185  );
3186 
3187  $sql = "INSERT $insertOptions" .
3188  " INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ') ' .
3189  $selectSql;
3190 
3191  $this->query( $sql, $fname );
3192  }
3193 
3194  public function limitResult( $sql, $limit, $offset = false ) {
3195  if ( !is_numeric( $limit ) ) {
3196  throw new DBUnexpectedError( $this,
3197  "Invalid non-numeric limit passed to limitResult()\n" );
3198  }
3199  // This version works in MySQL and SQLite. It will very likely need to be
3200  // overridden for most other RDBMS subclasses.
3201  return "$sql LIMIT "
3202  . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
3203  . "{$limit} ";
3204  }
3205 
3206  public function unionSupportsOrderAndLimit() {
3207  return true; // True for almost every DB supported
3208  }
3209 
3210  public function unionQueries( $sqls, $all ) {
3211  $glue = $all ? ') UNION ALL (' : ') UNION (';
3212 
3213  return '(' . implode( $glue, $sqls ) . ')';
3214  }
3215 
3217  $table, $vars, array $permute_conds, $extra_conds = '', $fname = __METHOD__,
3218  $options = [], $join_conds = []
3219  ) {
3220  // First, build the Cartesian product of $permute_conds
3221  $conds = [ [] ];
3222  foreach ( $permute_conds as $field => $values ) {
3223  if ( !$values ) {
3224  // Skip empty $values
3225  continue;
3226  }
3227  $values = array_unique( $values ); // For sanity
3228  $newConds = [];
3229  foreach ( $conds as $cond ) {
3230  foreach ( $values as $value ) {
3231  $cond[$field] = $value;
3232  $newConds[] = $cond; // Arrays are by-value, not by-reference, so this works
3233  }
3234  }
3235  $conds = $newConds;
3236  }
3237 
3238  $extra_conds = $extra_conds === '' ? [] : (array)$extra_conds;
3239 
3240  // If there's just one condition and no subordering, hand off to
3241  // selectSQLText directly.
3242  if ( count( $conds ) === 1 &&
3243  ( !isset( $options['INNER ORDER BY'] ) || !$this->unionSupportsOrderAndLimit() )
3244  ) {
3245  return $this->selectSQLText(
3246  $table, $vars, $conds[0] + $extra_conds, $fname, $options, $join_conds
3247  );
3248  }
3249 
3250  // Otherwise, we need to pull out the order and limit to apply after
3251  // the union. Then build the SQL queries for each set of conditions in
3252  // $conds. Then union them together (using UNION ALL, because the
3253  // product *should* already be distinct).
3254  $orderBy = $this->makeOrderBy( $options );
3255  $limit = $options['LIMIT'] ?? null;
3256  $offset = $options['OFFSET'] ?? false;
3257  $all = empty( $options['NOTALL'] ) && !in_array( 'NOTALL', $options );
3258  if ( !$this->unionSupportsOrderAndLimit() ) {
3259  unset( $options['ORDER BY'], $options['LIMIT'], $options['OFFSET'] );
3260  } else {
3261  if ( array_key_exists( 'INNER ORDER BY', $options ) ) {
3262  $options['ORDER BY'] = $options['INNER ORDER BY'];
3263  }
3264  if ( $limit !== null && is_numeric( $offset ) && $offset != 0 ) {
3265  // We need to increase the limit by the offset rather than
3266  // using the offset directly, otherwise it'll skip incorrectly
3267  // in the subqueries.
3268  $options['LIMIT'] = $limit + $offset;
3269  unset( $options['OFFSET'] );
3270  }
3271  }
3272 
3273  $sqls = [];
3274  foreach ( $conds as $cond ) {
3275  $sqls[] = $this->selectSQLText(
3276  $table, $vars, $cond + $extra_conds, $fname, $options, $join_conds
3277  );
3278  }
3279  $sql = $this->unionQueries( $sqls, $all ) . $orderBy;
3280  if ( $limit !== null ) {
3281  $sql = $this->limitResult( $sql, $limit, $offset );
3282  }
3283 
3284  return $sql;
3285  }
3286 
3287  public function conditional( $cond, $trueVal, $falseVal ) {
3288  if ( is_array( $cond ) ) {
3289  $cond = $this->makeList( $cond, self::LIST_AND );
3290  }
3291 
3292  return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
3293  }
3294 
3295  public function strreplace( $orig, $old, $new ) {
3296  return "REPLACE({$orig}, {$old}, {$new})";
3297  }
3298 
3299  public function getServerUptime() {
3300  return 0;
3301  }
3302 
3303  public function wasDeadlock() {
3304  return false;
3305  }
3306 
3307  public function wasLockTimeout() {
3308  return false;
3309  }
3310 
3311  public function wasConnectionLoss() {
3312  return $this->wasConnectionError( $this->lastErrno() );
3313  }
3314 
3315  public function wasReadOnlyError() {
3316  return false;
3317  }
3318 
3319  public function wasErrorReissuable() {
3320  return (
3321  $this->wasDeadlock() ||
3322  $this->wasLockTimeout() ||
3323  $this->wasConnectionLoss()
3324  );
3325  }
3326 
3333  public function wasConnectionError( $errno ) {
3334  return false;
3335  }
3336 
3343  protected function wasKnownStatementRollbackError() {
3344  return false; // don't know; it could have caused a transaction rollback
3345  }
3346 
3347  public function deadlockLoop() {
3348  $args = func_get_args();
3349  $function = array_shift( $args );
3350  $tries = self::DEADLOCK_TRIES;
3351 
3352  $this->begin( __METHOD__ );
3353 
3354  $retVal = null;
3356  $e = null;
3357  do {
3358  try {
3359  $retVal = $function( ...$args );
3360  break;
3361  } catch ( DBQueryError $e ) {
3362  if ( $this->wasDeadlock() ) {
3363  // Retry after a randomized delay
3364  usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
3365  } else {
3366  // Throw the error back up
3367  throw $e;
3368  }
3369  }
3370  } while ( --$tries > 0 );
3371 
3372  if ( $tries <= 0 ) {
3373  // Too many deadlocks; give up
3374  $this->rollback( __METHOD__ );
3375  throw $e;
3376  } else {
3377  $this->commit( __METHOD__ );
3378 
3379  return $retVal;
3380  }
3381  }
3382 
3383  public function masterPosWait( DBMasterPos $pos, $timeout ) {
3384  # Real waits are implemented in the subclass.
3385  return 0;
3386  }
3387 
3388  public function getReplicaPos() {
3389  # Stub
3390  return false;
3391  }
3392 
3393  public function getMasterPos() {
3394  # Stub
3395  return false;
3396  }
3397 
3398  public function serverIsReadOnly() {
3399  return false;
3400  }
3401 
3402  final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
3403  if ( !$this->trxLevel ) {
3404  throw new DBUnexpectedError( $this, "No transaction is active." );
3405  }
3406  $this->trxEndCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3407  }
3408 
3409  final public function onTransactionCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
3410  if ( !$this->trxLevel && $this->getTransactionRoundId() ) {
3411  // Start an implicit transaction similar to how query() does
3412  $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
3413  $this->trxAutomatic = true;
3414  }
3415 
3416  $this->trxIdleCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3417  if ( !$this->trxLevel ) {
3418  $this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
3419  }
3420  }
3421 
3422  final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
3423  $this->onTransactionCommitOrIdle( $callback, $fname );
3424  }
3425 
3426  final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
3427  if ( !$this->trxLevel && $this->getTransactionRoundId() ) {
3428  // Start an implicit transaction similar to how query() does
3429  $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
3430  $this->trxAutomatic = true;
3431  }
3432 
3433  if ( $this->trxLevel ) {
3434  $this->trxPreCommitCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3435  } else {
3436  // No transaction is active nor will start implicitly, so make one for this callback
3437  $this->startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
3438  try {
3439  $callback( $this );
3440  $this->endAtomic( __METHOD__ );
3441  } catch ( Exception $e ) {
3442  $this->cancelAtomic( __METHOD__ );
3443  throw $e;
3444  }
3445  }
3446  }
3447 
3451  private function currentAtomicSectionId() {
3452  if ( $this->trxLevel && $this->trxAtomicLevels ) {
3453  $levelInfo = end( $this->trxAtomicLevels );
3454 
3455  return $levelInfo[1];
3456  }
3457 
3458  return null;
3459  }
3460 
3467  ) {
3468  foreach ( $this->trxPreCommitCallbacks as $key => $info ) {
3469  if ( $info[2] === $old ) {
3470  $this->trxPreCommitCallbacks[$key][2] = $new;
3471  }
3472  }
3473  foreach ( $this->trxIdleCallbacks as $key => $info ) {
3474  if ( $info[2] === $old ) {
3475  $this->trxIdleCallbacks[$key][2] = $new;
3476  }
3477  }
3478  foreach ( $this->trxEndCallbacks as $key => $info ) {
3479  if ( $info[2] === $old ) {
3480  $this->trxEndCallbacks[$key][2] = $new;
3481  }
3482  }
3483  }
3484 
3489  private function modifyCallbacksForCancel( array $sectionIds ) {
3490  // Cancel the "on commit" callbacks owned by this savepoint
3491  $this->trxIdleCallbacks = array_filter(
3492  $this->trxIdleCallbacks,
3493  function ( $entry ) use ( $sectionIds ) {
3494  return !in_array( $entry[2], $sectionIds, true );
3495  }
3496  );
3497  $this->trxPreCommitCallbacks = array_filter(
3498  $this->trxPreCommitCallbacks,
3499  function ( $entry ) use ( $sectionIds ) {
3500  return !in_array( $entry[2], $sectionIds, true );
3501  }
3502  );
3503  // Make "on resolution" callbacks owned by this savepoint to perceive a rollback
3504  foreach ( $this->trxEndCallbacks as $key => $entry ) {
3505  if ( in_array( $entry[2], $sectionIds, true ) ) {
3506  $callback = $entry[0];
3507  $this->trxEndCallbacks[$key][0] = function () use ( $callback ) {
3508  return $callback( self::TRIGGER_ROLLBACK, $this );
3509  };
3510  }
3511  }
3512  }
3513 
3514  final public function setTransactionListener( $name, callable $callback = null ) {
3515  if ( $callback ) {
3516  $this->trxRecurringCallbacks[$name] = $callback;
3517  } else {
3518  unset( $this->trxRecurringCallbacks[$name] );
3519  }
3520  }
3521 
3530  final public function setTrxEndCallbackSuppression( $suppress ) {
3531  $this->trxEndCallbacksSuppressed = $suppress;
3532  }
3533 
3544  public function runOnTransactionIdleCallbacks( $trigger ) {
3545  if ( $this->trxLevel ) { // sanity
3546  throw new DBUnexpectedError( $this, __METHOD__ . ': a transaction is still open.' );
3547  }
3548 
3549  if ( $this->trxEndCallbacksSuppressed ) {
3550  return 0;
3551  }
3552 
3553  $count = 0;
3554  $autoTrx = $this->getFlag( self::DBO_TRX ); // automatic begin() enabled?
3556  $e = null; // first exception
3557  do { // callbacks may add callbacks :)
3558  $callbacks = array_merge(
3559  $this->trxIdleCallbacks,
3560  $this->trxEndCallbacks // include "transaction resolution" callbacks
3561  );
3562  $this->trxIdleCallbacks = []; // consumed (and recursion guard)
3563  $this->trxEndCallbacks = []; // consumed (recursion guard)
3564  foreach ( $callbacks as $callback ) {
3565  ++$count;
3566  list( $phpCallback ) = $callback;
3567  $this->clearFlag( self::DBO_TRX ); // make each query its own transaction
3568  try {
3569  // @phan-suppress-next-line PhanParamTooManyCallable
3570  call_user_func( $phpCallback, $trigger, $this );
3571  } catch ( Exception $ex ) {
3572  call_user_func( $this->errorLogger, $ex );
3573  $e = $e ?: $ex;
3574  // Some callbacks may use startAtomic/endAtomic, so make sure
3575  // their transactions are ended so other callbacks don't fail
3576  if ( $this->trxLevel() ) {
3577  $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
3578  }
3579  } finally {
3580  if ( $autoTrx ) {
3581  $this->setFlag( self::DBO_TRX ); // restore automatic begin()
3582  } else {
3583  $this->clearFlag( self::DBO_TRX ); // restore auto-commit
3584  }
3585  }
3586  }
3587  } while ( count( $this->trxIdleCallbacks ) );
3588 
3589  if ( $e instanceof Exception ) {
3590  throw $e; // re-throw any first exception
3591  }
3592 
3593  return $count;
3594  }
3595 
3606  $count = 0;
3607 
3608  $e = null; // first exception
3609  do { // callbacks may add callbacks :)
3610  $callbacks = $this->trxPreCommitCallbacks;
3611  $this->trxPreCommitCallbacks = []; // consumed (and recursion guard)
3612  foreach ( $callbacks as $callback ) {
3613  try {
3614  ++$count;
3615  list( $phpCallback ) = $callback;
3616  $phpCallback( $this );
3617  } catch ( Exception $ex ) {
3618  ( $this->errorLogger )( $ex );
3619  $e = $e ?: $ex;
3620  }
3621  }
3622  } while ( count( $this->trxPreCommitCallbacks ) );
3623 
3624  if ( $e instanceof Exception ) {
3625  throw $e; // re-throw any first exception
3626  }
3627 
3628  return $count;
3629  }
3630 
3640  public function runTransactionListenerCallbacks( $trigger ) {
3641  if ( $this->trxEndCallbacksSuppressed ) {
3642  return;
3643  }
3644 
3646  $e = null; // first exception
3647 
3648  foreach ( $this->trxRecurringCallbacks as $phpCallback ) {
3649  try {
3650  $phpCallback( $trigger, $this );
3651  } catch ( Exception $ex ) {
3652  ( $this->errorLogger )( $ex );
3653  $e = $e ?: $ex;
3654  }
3655  }
3656 
3657  if ( $e instanceof Exception ) {
3658  throw $e; // re-throw any first exception
3659  }
3660  }
3661 
3672  protected function doSavepoint( $identifier, $fname ) {
3673  $this->query( 'SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
3674  }
3675 
3686  protected function doReleaseSavepoint( $identifier, $fname ) {
3687  $this->query( 'RELEASE SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
3688  }
3689 
3700  protected function doRollbackToSavepoint( $identifier, $fname ) {
3701  $this->query( 'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
3702  }
3703 
3708  private function nextSavepointId( $fname ) {
3709  $savepointId = self::$SAVEPOINT_PREFIX . ++$this->trxAtomicCounter;
3710  if ( strlen( $savepointId ) > 30 ) {
3711  // 30 == Oracle's identifier length limit (pre 12c)
3712  // With a 22 character prefix, that puts the highest number at 99999999.
3713  throw new DBUnexpectedError(
3714  $this,
3715  'There have been an excessively large number of atomic sections in a transaction'
3716  . " started by $this->trxFname (at $fname)"
3717  );
3718  }
3719 
3720  return $savepointId;
3721  }
3722 
3723  final public function startAtomic(
3724  $fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE
3725  ) {
3726  $savepointId = $cancelable === self::ATOMIC_CANCELABLE ? self::$NOT_APPLICABLE : null;
3727 
3728  if ( !$this->trxLevel ) {
3729  $this->begin( $fname, self::TRANSACTION_INTERNAL ); // sets trxAutomatic
3730  // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
3731  // in all changes being in one transaction to keep requests transactional.
3732  if ( $this->getFlag( self::DBO_TRX ) ) {
3733  // Since writes could happen in between the topmost atomic sections as part
3734  // of the transaction, those sections will need savepoints.
3735  $savepointId = $this->nextSavepointId( $fname );
3736  $this->doSavepoint( $savepointId, $fname );
3737  } else {
3738  $this->trxAutomaticAtomic = true;
3739  }
3740  } elseif ( $cancelable === self::ATOMIC_CANCELABLE ) {
3741  $savepointId = $this->nextSavepointId( $fname );
3742  $this->doSavepoint( $savepointId, $fname );
3743  }
3744 
3745  $sectionId = new AtomicSectionIdentifier;
3746  $this->trxAtomicLevels[] = [ $fname, $sectionId, $savepointId ];
3747  $this->queryLogger->debug( 'startAtomic: entering level ' .
3748  ( count( $this->trxAtomicLevels ) - 1 ) . " ($fname)" );
3749 
3750  return $sectionId;
3751  }
3752 
3753  final public function endAtomic( $fname = __METHOD__ ) {
3754  if ( !$this->trxLevel || !$this->trxAtomicLevels ) {
3755  throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
3756  }
3757 
3758  // Check if the current section matches $fname
3759  $pos = count( $this->trxAtomicLevels ) - 1;
3760  list( $savedFname, $sectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
3761  $this->queryLogger->debug( "endAtomic: leaving level $pos ($fname)" );
3762 
3763  if ( $savedFname !== $fname ) {
3764  throw new DBUnexpectedError(
3765  $this,
3766  "Invalid atomic section ended (got $fname but expected $savedFname)."
3767  );
3768  }
3769 
3770  // Remove the last section (no need to re-index the array)
3771  array_pop( $this->trxAtomicLevels );
3772 
3773  if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
3774  $this->commit( $fname, self::FLUSHING_INTERNAL );
3775  } elseif ( $savepointId !== null && $savepointId !== self::$NOT_APPLICABLE ) {
3776  $this->doReleaseSavepoint( $savepointId, $fname );
3777  }
3778 
3779  // Hoist callback ownership for callbacks in the section that just ended;
3780  // all callbacks should have an owner that is present in trxAtomicLevels.
3781  $currentSectionId = $this->currentAtomicSectionId();
3782  if ( $currentSectionId ) {
3783  $this->reassignCallbacksForSection( $sectionId, $currentSectionId );
3784  }
3785  }
3786 
3787  final public function cancelAtomic(
3788  $fname = __METHOD__, AtomicSectionIdentifier $sectionId = null
3789  ) {
3790  if ( !$this->trxLevel || !$this->trxAtomicLevels ) {
3791  throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
3792  }
3793 
3794  $excisedFnames = [];
3795  if ( $sectionId !== null ) {
3796  // Find the (last) section with the given $sectionId
3797  $pos = -1;
3798  foreach ( $this->trxAtomicLevels as $i => list( $asFname, $asId, $spId ) ) {
3799  if ( $asId === $sectionId ) {
3800  $pos = $i;
3801  }
3802  }
3803  if ( $pos < 0 ) {
3804  throw new DBUnexpectedError( $this, "Atomic section not found (for $fname)" );
3805  }
3806  // Remove all descendant sections and re-index the array
3807  $excisedIds = [];
3808  $len = count( $this->trxAtomicLevels );
3809  for ( $i = $pos + 1; $i < $len; ++$i ) {
3810  $excisedFnames[] = $this->trxAtomicLevels[$i][0];
3811  $excisedIds[] = $this->trxAtomicLevels[$i][1];
3812  }
3813  $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 );
3814  $this->modifyCallbacksForCancel( $excisedIds );
3815  }
3816 
3817  // Check if the current section matches $fname
3818  $pos = count( $this->trxAtomicLevels ) - 1;
3819  list( $savedFname, $savedSectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
3820 
3821  if ( $excisedFnames ) {
3822  $this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname) " .
3823  "and descendants " . implode( ', ', $excisedFnames ) );
3824  } else {
3825  $this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname)" );
3826  }
3827 
3828  if ( $savedFname !== $fname ) {
3829  throw new DBUnexpectedError(
3830  $this,
3831  "Invalid atomic section ended (got $fname but expected $savedFname)."
3832  );
3833  }
3834 
3835  // Remove the last section (no need to re-index the array)
3836  array_pop( $this->trxAtomicLevels );
3837  $this->modifyCallbacksForCancel( [ $savedSectionId ] );
3838 
3839  if ( $savepointId !== null ) {
3840  // Rollback the transaction to the state just before this atomic section
3841  if ( $savepointId === self::$NOT_APPLICABLE ) {
3842  $this->rollback( $fname, self::FLUSHING_INTERNAL );
3843  } else {
3844  $this->doRollbackToSavepoint( $savepointId, $fname );
3845  $this->trxStatus = self::STATUS_TRX_OK; // no exception; recovered
3846  $this->trxStatusIgnoredCause = null;
3847  }
3848  } elseif ( $this->trxStatus > self::STATUS_TRX_ERROR ) {
3849  // Put the transaction into an error state if it's not already in one
3850  $this->trxStatus = self::STATUS_TRX_ERROR;
3851  $this->trxStatusCause = new DBUnexpectedError(
3852  $this,
3853  "Uncancelable atomic section canceled (got $fname)."
3854  );
3855  }
3856 
3857  $this->affectedRowCount = 0; // for the sake of consistency
3858  }
3859 
3860  final public function doAtomicSection(
3861  $fname, callable $callback, $cancelable = self::ATOMIC_NOT_CANCELABLE
3862  ) {
3863  $sectionId = $this->startAtomic( $fname, $cancelable );
3864  try {
3865  $res = $callback( $this, $fname );
3866  } catch ( Exception $e ) {
3867  $this->cancelAtomic( $fname, $sectionId );
3868 
3869  throw $e;
3870  }
3871  $this->endAtomic( $fname );
3872 
3873  return $res;
3874  }
3875 
3876  final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
3877  static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
3878  if ( !in_array( $mode, $modes, true ) ) {
3879  throw new DBUnexpectedError( $this, "$fname: invalid mode parameter '$mode'." );
3880  }
3881 
3882  // Protect against mismatched atomic section, transaction nesting, and snapshot loss
3883  if ( $this->trxLevel ) {
3884  if ( $this->trxAtomicLevels ) {
3885  $levels = $this->flatAtomicSectionList();
3886  $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
3887  throw new DBUnexpectedError( $this, $msg );
3888  } elseif ( !$this->trxAutomatic ) {
3889  $msg = "$fname: Explicit transaction already active (from {$this->trxFname}).";
3890  throw new DBUnexpectedError( $this, $msg );
3891  } else {
3892  $msg = "$fname: Implicit transaction already active (from {$this->trxFname}).";
3893  throw new DBUnexpectedError( $this, $msg );
3894  }
3895  } elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
3896  $msg = "$fname: Implicit transaction expected (DBO_TRX set).";
3897  throw new DBUnexpectedError( $this, $msg );
3898  }
3899 
3900  $this->assertHasConnectionHandle();
3901 
3902  $this->doBegin( $fname );
3903  $this->trxStatus = self::STATUS_TRX_OK;
3904  $this->trxStatusIgnoredCause = null;
3905  $this->trxAtomicCounter = 0;
3906  $this->trxTimestamp = microtime( true );
3907  $this->trxFname = $fname;
3908  $this->trxDoneWrites = false;
3909  $this->trxAutomaticAtomic = false;
3910  $this->trxAtomicLevels = [];
3911  $this->trxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
3912  $this->trxWriteDuration = 0.0;
3913  $this->trxWriteQueryCount = 0;
3914  $this->trxWriteAffectedRows = 0;
3915  $this->trxWriteAdjDuration = 0.0;
3916  $this->trxWriteAdjQueryCount = 0;
3917  $this->trxWriteCallers = [];
3918  // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
3919  // Get an estimate of the replication lag before any such queries.
3920  $this->trxReplicaLag = null; // clear cached value first
3921  $this->trxReplicaLag = $this->getApproximateLagStatus()['lag'];
3922  // T147697: make explicitTrxActive() return true until begin() finishes. This way, no
3923  // caller will think its OK to muck around with the transaction just because startAtomic()
3924  // has not yet completed (e.g. setting trxAtomicLevels).
3925  $this->trxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
3926  }
3927 
3934  protected function doBegin( $fname ) {
3935  $this->query( 'BEGIN', $fname );
3936  $this->trxLevel = 1;
3937  }
3938 
3939  final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
3940  static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
3941  if ( !in_array( $flush, $modes, true ) ) {
3942  throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'." );
3943  }
3944 
3945  if ( $this->trxLevel && $this->trxAtomicLevels ) {
3946  // There are still atomic sections open; this cannot be ignored
3947  $levels = $this->flatAtomicSectionList();
3948  throw new DBUnexpectedError(
3949  $this,
3950  "$fname: Got COMMIT while atomic sections $levels are still open."
3951  );
3952  }
3953 
3954  if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
3955  if ( !$this->trxLevel ) {
3956  return; // nothing to do
3957  } elseif ( !$this->trxAutomatic ) {
3958  throw new DBUnexpectedError(
3959  $this,
3960  "$fname: Flushing an explicit transaction, getting out of sync."
3961  );
3962  }
3963  } elseif ( !$this->trxLevel ) {
3964  $this->queryLogger->error(
3965  "$fname: No transaction to commit, something got out of sync." );
3966  return; // nothing to do
3967  } elseif ( $this->trxAutomatic ) {
3968  throw new DBUnexpectedError(
3969  $this,
3970  "$fname: Expected mass commit of all peer transactions (DBO_TRX set)."
3971  );
3972  }
3973 
3974  $this->assertHasConnectionHandle();
3975 
3977 
3978  $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
3979  $this->doCommit( $fname );
3980  $this->trxStatus = self::STATUS_TRX_NONE;
3981 
3982  if ( $this->trxDoneWrites ) {
3983  $this->lastWriteTime = microtime( true );
3984  $this->trxProfiler->transactionWritingOut(
3985  $this->server,
3986  $this->getDomainID(),
3987  $this->trxShortId,
3988  $writeTime,
3989  $this->trxWriteAffectedRows
3990  );
3991  }
3992 
3993  // With FLUSHING_ALL_PEERS, callbacks will be explicitly run later
3994  if ( $flush !== self::FLUSHING_ALL_PEERS ) {
3995  $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
3996  $this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
3997  }
3998  }
3999 
4006  protected function doCommit( $fname ) {
4007  if ( $this->trxLevel ) {
4008  $this->query( 'COMMIT', $fname );
4009  $this->trxLevel = 0;
4010  }
4011  }
4012 
4013  final public function rollback( $fname = __METHOD__, $flush = '' ) {
4014  $trxActive = $this->trxLevel;
4015 
4016  if ( $flush !== self::FLUSHING_INTERNAL
4017  && $flush !== self::FLUSHING_ALL_PEERS
4018  && $this->getFlag( self::DBO_TRX )
4019  ) {
4020  throw new DBUnexpectedError(
4021  $this,
4022  "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)."
4023  );
4024  }
4025 
4026  if ( $trxActive ) {
4027  $this->assertHasConnectionHandle();
4028 
4029  $this->doRollback( $fname );
4030  $this->trxStatus = self::STATUS_TRX_NONE;
4031  $this->trxAtomicLevels = [];
4032  // Estimate the RTT via a query now that trxStatus is OK
4033  $writeTime = $this->pingAndCalculateLastTrxApplyTime();
4034 
4035  if ( $this->trxDoneWrites ) {
4036  $this->trxProfiler->transactionWritingOut(
4037  $this->server,
4038  $this->getDomainID(),
4039  $this->trxShortId,
4040  $writeTime,
4041  $this->trxWriteAffectedRows
4042  );
4043  }
4044  }
4045 
4046  // Clear any commit-dependant callbacks. They might even be present
4047  // only due to transaction rounds, with no SQL transaction being active
4048  $this->trxIdleCallbacks = [];
4049  $this->trxPreCommitCallbacks = [];
4050 
4051  // With FLUSHING_ALL_PEERS, callbacks will be explicitly run later
4052  if ( $trxActive && $flush !== self::FLUSHING_ALL_PEERS ) {
4053  try {
4054  $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
4055  } catch ( Exception $e ) {
4056  // already logged; finish and let LoadBalancer move on during mass-rollback
4057  }
4058  try {
4059  $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
4060  } catch ( Exception $e ) {
4061  // already logged; let LoadBalancer move on during mass-rollback
4062  }
4063 
4064  $this->affectedRowCount = 0; // for the sake of consistency
4065  }
4066  }
4067 
4074  protected function doRollback( $fname ) {
4075  if ( $this->trxLevel ) {
4076  # Disconnects cause rollback anyway, so ignore those errors
4077  $ignoreErrors = true;
4078  $this->query( 'ROLLBACK', $fname, $ignoreErrors );
4079  $this->trxLevel = 0;
4080  }
4081  }
4082 
4083  public function flushSnapshot( $fname = __METHOD__ ) {
4084  if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
4085  // This only flushes transactions to clear snapshots, not to write data
4086  $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
4087  throw new DBUnexpectedError(
4088  $this,
4089  "$fname: Cannot flush snapshot because writes are pending ($fnames)."
4090  );
4091  }
4092 
4093  $this->commit( $fname, self::FLUSHING_INTERNAL );
4094  }
4095 
4096  public function explicitTrxActive() {
4097  return $this->trxLevel && ( $this->trxAtomicLevels || !$this->trxAutomatic );
4098  }
4099 
4100  public function duplicateTableStructure(
4101  $oldName, $newName, $temporary = false, $fname = __METHOD__
4102  ) {
4103  throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
4104  }
4105 
4106  public function listTables( $prefix = null, $fname = __METHOD__ ) {
4107  throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
4108  }
4109 
4110  public function listViews( $prefix = null, $fname = __METHOD__ ) {
4111  throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
4112  }
4113 
4114  public function timestamp( $ts = 0 ) {
4115  $t = new ConvertibleTimestamp( $ts );
4116  // Let errors bubble up to avoid putting garbage in the DB
4117  return $t->getTimestamp( TS_MW );
4118  }
4119 
4120  public function timestampOrNull( $ts = null ) {
4121  if ( is_null( $ts ) ) {
4122  return null;
4123  } else {
4124  return $this->timestamp( $ts );
4125  }
4126  }
4127 
4128  public function affectedRows() {
4129  return ( $this->affectedRowCount === null )
4130  ? $this->fetchAffectedRowCount() // default to driver value
4132  }
4133 
4137  abstract protected function fetchAffectedRowCount();
4138 
4152  protected function resultObject( $result ) {
4153  if ( !$result ) {
4154  return false;
4155  } elseif ( $result instanceof ResultWrapper ) {
4156  return $result;
4157  } elseif ( $result === true ) {
4158  // Successful write query
4159  return $result;
4160  } else {
4161  return new ResultWrapper( $this, $result );
4162  }
4163  }
4164 
4165  public function ping( &$rtt = null ) {
4166  // Avoid hitting the server if it was hit recently
4167  if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
4168  if ( !func_num_args() || $this->rttEstimate > 0 ) {
4169  $rtt = $this->rttEstimate;
4170  return true; // don't care about $rtt
4171  }
4172  }
4173 
4174  // This will reconnect if possible or return false if not
4175  $this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
4176  $ok = ( $this->query( self::PING_QUERY, __METHOD__, true ) !== false );
4177  $this->restoreFlags( self::RESTORE_PRIOR );
4178 
4179  if ( $ok ) {
4180  $rtt = $this->rttEstimate;
4181  }
4182 
4183  return $ok;
4184  }
4185 
4192  protected function replaceLostConnection( $fname ) {
4193  $this->closeConnection();
4194  $this->opened = false;
4195  $this->conn = false;
4196 
4197  $this->handleSessionLossPreconnect();
4198 
4199  try {
4200  $this->open(
4201  $this->server,
4202  $this->user,
4203  $this->password,
4204  $this->getDBname(),
4205  $this->dbSchema(),
4206  $this->tablePrefix()
4207  );
4208  $this->lastPing = microtime( true );
4209  $ok = true;
4210 
4211  $this->connLogger->warning(
4212  $fname . ': lost connection to {dbserver}; reconnected',
4213  [
4214  'dbserver' => $this->getServer(),
4215  'trace' => ( new RuntimeException() )->getTraceAsString()
4216  ]
4217  );
4218  } catch ( DBConnectionError $e ) {
4219  $ok = false;
4220 
4221  $this->connLogger->error(
4222  $fname . ': lost connection to {dbserver} permanently',
4223  [ 'dbserver' => $this->getServer() ]
4224  );
4225  }
4226 
4228 
4229  return $ok;
4230  }
4231 
4232  public function getSessionLagStatus() {
4233  return $this->getRecordedTransactionLagStatus() ?: $this->getApproximateLagStatus();
4234  }
4235 
4249  final protected function getRecordedTransactionLagStatus() {
4250  return ( $this->trxLevel && $this->trxReplicaLag !== null )
4251  ? [ 'lag' => $this->trxReplicaLag, 'since' => $this->trxTimestamp() ]
4252  : null;
4253  }
4254 
4261  protected function getApproximateLagStatus() {
4262  return [
4263  'lag' => $this->getLBInfo( 'replica' ) ? $this->getLag() : 0,
4264  'since' => microtime( true )
4265  ];
4266  }
4267 
4287  public static function getCacheSetOptions( IDatabase $db1, IDatabase $db2 = null ) {
4288  $res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
4289  foreach ( func_get_args() as $db ) {
4291  $status = $db->getSessionLagStatus();
4292  if ( $status['lag'] === false ) {
4293  $res['lag'] = false;
4294  } elseif ( $res['lag'] !== false ) {
4295  $res['lag'] = max( $res['lag'], $status['lag'] );
4296  }
4297  $res['since'] = min( $res['since'], $status['since'] );
4298  $res['pending'] = $res['pending'] ?: $db->writesPending();
4299  }
4300 
4301  return $res;
4302  }
4303 
4304  public function getLag() {
4305  return 0;
4306  }
4307 
4308  public function maxListLen() {
4309  return 0;
4310  }
4311 
4312  public function encodeBlob( $b ) {
4313  return $b;
4314  }
4315 
4316  public function decodeBlob( $b ) {
4317  if ( $b instanceof Blob ) {
4318  $b = $b->fetch();
4319  }
4320  return $b;
4321  }
4322 
4323  public function setSessionOptions( array $options ) {
4324  }
4325 
4326  public function sourceFile(
4327  $filename,
4328  callable $lineCallback = null,
4329  callable $resultCallback = null,
4330  $fname = false,
4331  callable $inputCallback = null
4332  ) {
4333  Wikimedia\suppressWarnings();
4334  $fp = fopen( $filename, 'r' );
4335  Wikimedia\restoreWarnings();
4336 
4337  if ( $fp === false ) {
4338  throw new RuntimeException( "Could not open \"{$filename}\".\n" );
4339  }
4340 
4341  if ( !$fname ) {
4342  $fname = __METHOD__ . "( $filename )";
4343  }
4344 
4345  try {
4346  $error = $this->sourceStream(
4347  $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
4348  } catch ( Exception $e ) {
4349  fclose( $fp );
4350  throw $e;
4351  }
4352 
4353  fclose( $fp );
4354 
4355  return $error;
4356  }
4357 
4358  public function setSchemaVars( $vars ) {
4359  $this->schemaVars = $vars;
4360  }
4361 
4362  public function sourceStream(
4363  $fp,
4364  callable $lineCallback = null,
4365  callable $resultCallback = null,
4366  $fname = __METHOD__,
4367  callable $inputCallback = null
4368  ) {
4369  $delimiterReset = new ScopedCallback(
4370  function ( $delimiter ) {
4371  $this->delimiter = $delimiter;
4372  },
4373  [ $this->delimiter ]
4374  );
4375  $cmd = '';
4376 
4377  while ( !feof( $fp ) ) {
4378  if ( $lineCallback ) {
4379  call_user_func( $lineCallback );
4380  }
4381 
4382  $line = trim( fgets( $fp ) );
4383 
4384  if ( $line == '' ) {
4385  continue;
4386  }
4387 
4388  if ( $line[0] == '-' && $line[1] == '-' ) {
4389  continue;
4390  }
4391 
4392  if ( $cmd != '' ) {
4393  $cmd .= ' ';
4394  }
4395 
4396  $done = $this->streamStatementEnd( $cmd, $line );
4397 
4398  $cmd .= "$line\n";
4399 
4400  if ( $done || feof( $fp ) ) {
4401  $cmd = $this->replaceVars( $cmd );
4402 
4403  if ( $inputCallback ) {
4404  $callbackResult = $inputCallback( $cmd );
4405 
4406  if ( is_string( $callbackResult ) || !$callbackResult ) {
4407  $cmd = $callbackResult;
4408  }
4409  }
4410 
4411  if ( $cmd ) {
4412  $res = $this->query( $cmd, $fname );
4413 
4414  if ( $resultCallback ) {
4415  $resultCallback( $res, $this );
4416  }
4417 
4418  if ( $res === false ) {
4419  $err = $this->lastError();
4420 
4421  return "Query \"{$cmd}\" failed with error code \"$err\".\n";
4422  }
4423  }
4424  $cmd = '';
4425  }
4426  }
4427 
4428  ScopedCallback::consume( $delimiterReset );
4429  return true;
4430  }
4431 
4439  public function streamStatementEnd( &$sql, &$newLine ) {
4440  if ( $this->delimiter ) {
4441  $prev = $newLine;
4442  $newLine = preg_replace(
4443  '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
4444  if ( $newLine != $prev ) {
4445  return true;
4446  }
4447  }
4448 
4449  return false;
4450  }
4451 
4472  protected function replaceVars( $ins ) {
4473  $vars = $this->getSchemaVars();
4474  return preg_replace_callback(
4475  '!
4476  /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
4477  \'\{\$ (\w+) }\' | # 3. addQuotes
4478  `\{\$ (\w+) }` | # 4. addIdentifierQuotes
4479  /\*\$ (\w+) \*/ # 5. leave unencoded
4480  !x',
4481  function ( $m ) use ( $vars ) {
4482  // Note: Because of <https://bugs.php.net/bug.php?id=51881>,
4483  // check for both nonexistent keys *and* the empty string.
4484  if ( isset( $m[1] ) && $m[1] !== '' ) {
4485  if ( $m[1] === 'i' ) {
4486  return $this->indexName( $m[2] );
4487  } else {
4488  return $this->tableName( $m[2] );
4489  }
4490  } elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
4491  return $this->addQuotes( $vars[$m[3]] );
4492  } elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
4493  return $this->addIdentifierQuotes( $vars[$m[4]] );
4494  } elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
4495  return $vars[$m[5]];
4496  } else {
4497  return $m[0];
4498  }
4499  },
4500  $ins
4501  );
4502  }
4503 
4510  protected function getSchemaVars() {
4511  if ( $this->schemaVars ) {
4512  return $this->schemaVars;
4513  } else {
4514  return $this->getDefaultSchemaVars();
4515  }
4516  }
4517 
4526  protected function getDefaultSchemaVars() {
4527  return [];
4528  }
4529 
4530  public function lockIsFree( $lockName, $method ) {
4531  // RDBMs methods for checking named locks may or may not count this thread itself.
4532  // In MySQL, IS_FREE_LOCK() returns 0 if the thread already has the lock. This is
4533  // the behavior choosen by the interface for this method.
4534  return !isset( $this->namedLocksHeld[$lockName] );
4535  }
4536 
4537  public function lock( $lockName, $method, $timeout = 5 ) {
4538  $this->namedLocksHeld[$lockName] = 1;
4539 
4540  return true;
4541  }
4542 
4543  public function unlock( $lockName, $method ) {
4544  unset( $this->namedLocksHeld[$lockName] );
4545 
4546  return true;
4547  }
4548 
4549  public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
4550  if ( $this->writesOrCallbacksPending() ) {
4551  // This only flushes transactions to clear snapshots, not to write data
4552  $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
4553  throw new DBUnexpectedError(
4554  $this,
4555  "$fname: Cannot flush pre-lock snapshot because writes are pending ($fnames)."
4556  );
4557  }
4558 
4559  if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
4560  return null;
4561  }
4562 
4563  $unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
4564  if ( $this->trxLevel() ) {
4565  // There is a good chance an exception was thrown, causing any early return
4566  // from the caller. Let any error handler get a chance to issue rollback().
4567  // If there isn't one, let the error bubble up and trigger server-side rollback.
4568  $this->onTransactionResolution(
4569  function () use ( $lockKey, $fname ) {
4570  $this->unlock( $lockKey, $fname );
4571  },
4572  $fname
4573  );
4574  } else {
4575  $this->unlock( $lockKey, $fname );
4576  }
4577  } );
4578 
4579  $this->commit( $fname, self::FLUSHING_INTERNAL );
4580 
4581  return $unlocker;
4582  }
4583 
4584  public function namedLocksEnqueue() {
4585  return false;
4586  }
4587 
4589  return true;
4590  }
4591 
4592  final public function lockTables( array $read, array $write, $method ) {
4593  if ( $this->writesOrCallbacksPending() ) {
4594  throw new DBUnexpectedError( $this, "Transaction writes or callbacks still pending." );
4595  }
4596 
4597  if ( $this->tableLocksHaveTransactionScope() ) {
4598  $this->startAtomic( $method );
4599  }
4600 
4601  return $this->doLockTables( $read, $write, $method );
4602  }
4603 
4612  protected function doLockTables( array $read, array $write, $method ) {
4613  return true;
4614  }
4615 
4616  final public function unlockTables( $method ) {
4617  if ( $this->tableLocksHaveTransactionScope() ) {
4618  $this->endAtomic( $method );
4619 
4620  return true; // locks released on COMMIT/ROLLBACK
4621  }
4622 
4623  return $this->doUnlockTables( $method );
4624  }
4625 
4632  protected function doUnlockTables( $method ) {
4633  return true;
4634  }
4635 
4643  public function dropTable( $tableName, $fName = __METHOD__ ) {
4644  if ( !$this->tableExists( $tableName, $fName ) ) {
4645  return false;
4646  }
4647  $sql = "DROP TABLE " . $this->tableName( $tableName ) . " CASCADE";
4648 
4649  return $this->query( $sql, $fName );
4650  }
4651 
4652  public function getInfinity() {
4653  return 'infinity';
4654  }
4655 
4656  public function encodeExpiry( $expiry ) {
4657  return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
4658  ? $this->getInfinity()
4659  : $this->timestamp( $expiry );
4660  }
4661 
4662  public function decodeExpiry( $expiry, $format = TS_MW ) {
4663  if ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) {
4664  return 'infinity';
4665  }
4666 
4667  return ConvertibleTimestamp::convert( $format, $expiry );
4668  }
4669 
4670  public function setBigSelects( $value = true ) {
4671  // no-op
4672  }
4673 
4674  public function isReadOnly() {
4675  return ( $this->getReadOnlyReason() !== false );
4676  }
4677 
4681  protected function getReadOnlyReason() {
4682  $reason = $this->getLBInfo( 'readOnlyReason' );
4683 
4684  return is_string( $reason ) ? $reason : false;
4685  }
4686 
4687  public function setTableAliases( array $aliases ) {
4688  $this->tableAliases = $aliases;
4689  }
4690 
4691  public function setIndexAliases( array $aliases ) {
4692  $this->indexAliases = $aliases;
4693  }
4694 
4700  protected function hasFlags( $field, $flags ) {
4701  return ( ( $field & $flags ) === $flags );
4702  }
4703 
4715  protected function getBindingHandle() {
4716  if ( !$this->conn ) {
4717  throw new DBUnexpectedError(
4718  $this,
4719  'DB connection was already closed or the connection dropped.'
4720  );
4721  }
4722 
4723  return $this->conn;
4724  }
4725 
4730  public function __toString() {
4731  return (string)$this->conn;
4732  }
4733 
4738  public function __clone() {
4739  $this->connLogger->warning(
4740  "Cloning " . static::class . " is not recommended; forking connection:\n" .
4741  ( new RuntimeException() )->getTraceAsString()
4742  );
4743 
4744  if ( $this->isOpen() ) {
4745  // Open a new connection resource without messing with the old one
4746  $this->opened = false;
4747  $this->conn = false;
4748  $this->trxEndCallbacks = []; // don't copy
4749  $this->handleSessionLossPreconnect(); // no trx or locks anymore
4750  $this->open(
4751  $this->server,
4752  $this->user,
4753  $this->password,
4754  $this->getDBname(),
4755  $this->dbSchema(),
4756  $this->tablePrefix()
4757  );
4758  $this->lastPing = microtime( true );
4759  }
4760  }
4761 
4767  public function __sleep() {
4768  throw new RuntimeException( 'Database serialization may cause problems, since ' .
4769  'the connection is not restored on wakeup.' );
4770  }
4771 
4775  public function __destruct() {
4776  if ( $this->trxLevel && $this->trxDoneWrites ) {
4777  trigger_error( "Uncommitted DB writes (transaction from {$this->trxFname})." );
4778  }
4779 
4780  $danglingWriters = $this->pendingWriteAndCallbackCallers();
4781  if ( $danglingWriters ) {
4782  $fnames = implode( ', ', $danglingWriters );
4783  trigger_error( "DB transaction writes or callbacks still pending ($fnames)." );
4784  }
4785 
4786  if ( $this->conn ) {
4787  // Avoid connection leaks for sanity. Normally, resources close at script completion.
4788  // The connection might already be closed in zend/hhvm by now, so suppress warnings.
4789  Wikimedia\suppressWarnings();
4790  $this->closeConnection();
4791  Wikimedia\restoreWarnings();
4792  $this->conn = false;
4793  $this->opened = false;
4794  }
4795  }
4796 }
4797 
4801 class_alias( Database::class, 'DatabaseBase' );
4802 
4806 class_alias( Database::class, 'Database' );
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
Helper class that detects high-contention DB queries via profiling calls.
const DEADLOCK_DELAY_MAX
Maximum time to wait before retry.
Definition: Database.php:54
normalizeConditions( $conds, $fname)
Definition: Database.php:1984
setIndexAliases(array $aliases)
Convert certain index names to alternative names before querying the DB.
Definition: Database.php:4691
canRecoverFromDisconnect( $sql, $priorWritesPending)
Determine whether it is safe to retry queries after a database connection is lost.
Definition: Database.php:1458
const DBO_IGNORE
Definition: defines.php:11
conditional( $cond, $trueVal, $falseVal)
Returns an SQL expression for a simple conditional.
Definition: Database.php:3287
clearFlag( $flag, $remember=self::REMEMBER_NOTHING)
Clear a flag for this connection.
Definition: Database.php:811
doneWrites()
Returns true if the connection may have been used for write queries.
Definition: Database.php:687
decodeExpiry( $expiry, $format=TS_MW)
Decode an expiry time into a DBMS independent format.
Definition: Database.php:4662
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:2633
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
isTransactableQuery( $sql)
Determine whether a SQL statement is sensitive to isolation level.
Definition: Database.php:1140
setSessionOptions(array $options)
Override database&#39;s default behavior.
Definition: Database.php:4323
namedLocksEnqueue()
Check to see if a named lock used by lock() use blocking queues.
Definition: Database.php:4584
reportQueryError( $error, $errno, $sql, $fname, $ignoreErrors=false)
Report a query error.
Definition: Database.php:1552
getFlag( $flag)
Returns a boolean whether the flag $flag is set for this connection.
Definition: Database.php:835
freeResult( $res)
Free a result object returned by query() or select().
Definition: Database.php:1593
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
databasesAreIndependent()
Returns true if DBs are assumed to be on potentially different servers.
Definition: Database.php:2380
assertTransactionStatus( $sql, $fname)
Error out if the DB is not in a valid state for a query via query()
Definition: Database.php:1412
bool $trxAutomatic
Record if the current transaction was started implicitly due to DBO_TRX being set.
Definition: Database.php:201
unlockTables( $method)
Unlock all tables locked via lockTables()
Definition: Database.php:4616
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by wfTimestamp() to the format used for inserting ...
Definition: Database.php:4114
affectedRows()
Get the number of rows affected by the last write query.
Definition: Database.php:4128
encodeBlob( $b)
Some DBMSs have a special format for inserting into blob fields, they don&#39;t allow simple quoted strin...
Definition: Database.php:4312
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:245
if(is_array( $mode)) switch( $mode) $input
array $sessionTempTables
Map of (table name => 1) for TEMPORARY tables.
Definition: Database.php:254
doSelectDomain(DatabaseDomain $domain)
Definition: Database.php:2398
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
const DEADLOCK_DELAY_MIN
Minimum time to wait before retry, in microseconds.
Definition: Database.php:52
lastErrno()
Get the last error number.
escapeLikeInternal( $s, $escapeChar='`')
Definition: Database.php:2772
buildConcat( $stringList)
Build a concatenation list to feed into a SQL query.
Definition: Database.php:2315
selectDomain( $domain)
Set the current domain (database, schema, and table prefix)
Definition: Database.php:2394
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:1982
pendingWriteQueryDuration( $type=self::ESTIMATE_TOTAL)
Get the time spend running write queries for this transaction.
Definition: Database.php:726
begin( $fname=__METHOD__, $mode=self::TRANSACTION_EXPLICIT)
Begin a transaction.
Definition: Database.php:3876
encodeExpiry( $expiry)
Encode an expiry time into the DBMS dependent format.
Definition: Database.php:4656
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:756
array null $trxStatusIgnoredCause
If wasKnownStatementRollbackError() prevented trxStatus from being set, the relevant details are stor...
Definition: Database.php:154
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging $e
Definition: hooks.txt:2159
const LIST_NAMES
Definition: Defines.php:45
ignoreIndexClause( $index)
IGNORE INDEX clause.
Definition: Database.php:2841
static string $SAVEPOINT_PREFIX
Prefix to the atomic section counter used to make savepoint IDs.
Definition: Database.php:276
insert( $table, $a, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
Definition: Database.php:2110
getScopedLockAndFlush( $lockKey, $fname, $timeout)
Acquire a named lock, flush any transaction, and return an RAII style unlocker object.
Definition: Database.php:4549
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:2805
__sleep()
Called by serialize.
Definition: Database.php:4767
getServer()
Get the server hostname or IP address.
Definition: Database.php:2406
assertBuildSubstringParams( $startPosition, $length)
Check type and bounds for parameters to self::buildSubstring()
Definition: Database.php:2348
string $trxShortId
Either a short hexidecimal string if a transaction is active or "".
Definition: Database.php:168
Exception null $trxStatusCause
The last error that caused the status to become STATUS_TRX_ERROR.
Definition: Database.php:149
int $trxLevel
Either 1 if a transaction is active or 0 otherwise.
Definition: Database.php:161
setTrxEndCallbackSuppression( $suppress)
Whether to disable running of post-COMMIT/ROLLBACK callbacks.
Definition: Database.php:3530
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:1623
getSessionLagStatus()
Get the replica DB lag when the current transaction started or a general lag estimate if not transact...
Definition: Database.php:4232
__clone()
Make sure that copies do not share the same client binding handle.
Definition: Database.php:4738
array $connectionParams
Parameters used by initConnection() to establish a connection.
Definition: Database.php:95
selectDB( $db)
Change the current database.
Definition: Database.php:2384
reassignCallbacksForSection(AtomicSectionIdentifier $old, AtomicSectionIdentifier $new)
Definition: Database.php:3465
writesOrCallbacksPending()
Whether there is a transaction open with either possible write queries or unresolved pre-commit/commi...
Definition: Database.php:699
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:1596
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:175
DatabaseDomain $currentDomain
Definition: Database.php:138
$value
makeWhereFrom2d( $data, $baseKey, $subKey)
Build a partial where clause from a 2-d array such as used for LinkBatch.
Definition: Database.php:2280
onTransactionIdle(callable $callback, $fname=__METHOD__)
Alias for onTransactionCommitOrIdle() for backwards-compatibility.
Definition: Database.php:3422
getDomainID()
Return the currently selected domain ID.
Definition: Database.php:848
buildSelectSubquery( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Equivalent to IDatabase::selectSQLText() except wraps the result in Subqyery.
Definition: Database.php:2371
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:2151
limitResult( $sql, $limit, $offset=false)
Construct a LIMIT query with optional offset.
Definition: Database.php:3194
nonNativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname=__METHOD__, $insertOptions=[], $selectOptions=[], $selectJoinConds=[])
Implementation of insertSelect() based on select() and insert()
Definition: Database.php:3097
trxLevel()
Gets the current transaction level.
Definition: Database.php:588
callable [] $trxRecurringCallbacks
Map of (name => callable)
Definition: Database.php:119
startAtomic( $fname=__METHOD__, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Begin an atomic section of SQL statements.
Definition: Database.php:3723
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
Definition: Database.php:2186
replaceVars( $ins)
Database independent variable replacement.
Definition: Database.php:4472
string bool null $htmlErrors
Stashed value of html_errors INI setting.
Definition: Database.php:134
fieldNameWithAlias( $name, $alias=false)
Get an aliased field name e.g.
Definition: Database.php:2598
nativeReplace( $table, $rows, $fname)
REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE statement.
Definition: Database.php:2906
implicitOrderby()
Returns true if this database does an implicit order by when the column has an index For example: SEL...
Definition: Database.php:679
static factory( $dbType, $p=[], $connect=self::NEW_CONNECTED)
Construct a Database subclass instance given a database type and parameters.
Definition: Database.php:434
endAtomic( $fname=__METHOD__)
Ends an atomic section of SQL statements.
Definition: Database.php:3753
doCommit( $fname)
Issues the COMMIT command to the database server.
Definition: Database.php:4006
query( $sql, $fname=__METHOD__, $flags=0)
Run an SQL query and return the result.
Definition: Database.php:1191
string $server
Server that this instance is currently connected to.
Definition: Database.php:81
getLBInfo( $name=null)
Get properties passed down from the server info array of the load balancer.
Definition: Database.php:642
nextSequenceValue( $seqName)
Deprecated method, calls should be removed.
Definition: Database.php:2813
handleSessionLossPreconnect()
Clean things up after session (and thus transaction) loss before reconnect.
Definition: Database.php:1483
buildGroupConcatField( $delim, $table, $field, $conds='', $join_conds=[])
Build a GROUP_CONCAT or equivalent statement for a query.
Definition: Database.php:2319
doLockTables(array $read, array $write, $method)
Helper function for lockTables() that handles the actual table locking.
Definition: Database.php:4612
textFieldSize( $table, $field)
Returns the size of a text field, or -1 for "unlimited".
Definition: Database.php:2998
reportConnectionError( $error='Unknown error')
Definition: Database.php:1058
bufferResults( $buffer=null)
Turns buffering of SQL result sets on (true) or off (false).
Definition: Database.php:577
fieldExists( $table, $field, $fname=__METHOD__)
Determines whether a field exists in a table.
Definition: Database.php:2069
float $lastPing
UNIX timestamp.
Definition: Database.php:260
callable $errorLogger
Error logging callback.
Definition: Database.php:103
bitAnd( $fieldLeft, $fieldRight)
Definition: Database.php:2307
callable null $profiler
Definition: Database.php:266
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:979
getServerVersion()
A string describing the current software version, like from mysql_get_server_info().
getWikiID()
Alias for getDomainID()
Definition: Database.php:852
maxListLen()
Return the maximum number of items allowed in a list, or 0 for unlimited.
Definition: Database.php:4308
doSavepoint( $identifier, $fname)
Create a savepoint.
Definition: Database.php:3672
modifyCallbacksForCancel(array $sectionIds)
Definition: Database.php:3489
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object '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:1980
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:1658
if( $line===false) $args
Definition: cdb.php:64
isWriteQuery( $sql)
Determine whether a query writes to the DB.
Definition: Database.php:1099
tableNames()
Fetch a number of table names into an array This is handy when you need to construct SQL for joins...
Definition: Database.php:2519
close()
Close the database connection.
Definition: Database.php:938
listViews( $prefix=null, $fname=__METHOD__)
Lists all the VIEWs in the database.
Definition: Database.php:4110
strreplace( $orig, $old, $new)
Returns a command for str_replace function in SQL query.
Definition: Database.php:3295
getMasterPos()
Get the position of this master.
Definition: Database.php:3393
__destruct()
Run a few simple sanity checks and close dangling connections.
Definition: Database.php:4775
isOpen()
Is a connection to the database open?
Definition: Database.php:796
installErrorHandler()
Set a custom error handler for logging errors during database connection.
Definition: Database.php:876
getDBname()
Get the current DB name.
Definition: Database.php:2402
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:1263
static generalizeSQL( $sql)
Removes most variables from an SQL query and replaces them with X or N for numbers.
Definition: Database.php:2047
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...
LoggerInterface $queryLogger
Definition: Database.php:101
implicitGroupby()
Returns true if this database does an implicit sort when doing GROUP BY.
Definition: Database.php:675
setLogger(LoggerInterface $logger)
Set the PSR-3 logger interface to use for query logging.
Definition: Database.php:569
setBigSelects( $value=true)
Allow or deny "big selects" for this session only.
Definition: Database.php:4670
replaceLostConnection( $fname)
Close any existing (dead) database connection and open a new connection.
Definition: Database.php:4192
float $trxReplicaLag
Lag estimate at the time of BEGIN.
Definition: Database.php:179
decodeBlob( $b)
Some DBMSs return a special placeholder object representing blob fields in result objects...
Definition: Database.php:4316
aggregateValue( $valuedata, $valuename='value')
Return aggregated value alias.
Definition: Database.php:2299
timestampOrNull( $ts=null)
Convert a timestamp in one of the formats accepted by wfTimestamp() to the format used for inserting ...
Definition: Database.php:4120
addQuotes( $s)
Adds quotes and backslashes.
Definition: Database.php:2733
int $trxWriteAffectedRows
Number of rows affected by write queries for the current transaction.
Definition: Database.php:237
attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname)
Wrapper for query() that also handles profiling, logging, and affected row count updates.
Definition: Database.php:1290
TransactionProfiler $trxProfiler
Definition: Database.php:268
const DBO_DEBUG
Definition: defines.php:9
const LIST_AND
Definition: Defines.php:43
makeInsertOptions( $options)
Helper for Database::insert().
Definition: Database.php:2106
static getClass( $dbType, $driver=null)
Definition: Database.php:506
assertHasConnectionHandle()
Make sure there is an open connection handle (alive or not) as a sanity check.
Definition: Database.php:1022
selectOptionsIncludeLocking( $options)
Definition: Database.php:1940
sourceFile( $filename, callable $lineCallback=null, callable $resultCallback=null, $fname=false, callable $inputCallback=null)
Read and execute SQL commands from a file.
Definition: Database.php:4326
doRollback( $fname)
Issues the ROLLBACK command to the database server.
Definition: Database.php:4074
beginIfImplied( $sql, $fname)
Start an implicit transaction if DBO_TRX is enabled and no transaction is active. ...
Definition: Database.php:1360
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:2075
open( $server, $user, $password, $dbName, $schema, $tablePrefix)
Open a new connection to the database (closing any existing one)
indexName( $index)
Allows for index remapping in queries where this is not consistent across DBMS.
Definition: Database.php:2729
handleSessionLossPostconnect()
Clean things up after session (and thus transaction) loss after reconnect.
Definition: Database.php:1511
string $trxFname
Remembers the function name given for starting the most recent transaction via begin().
Definition: Database.php:187
const LIST_COMMA
Definition: Defines.php:42
$res
Definition: database.txt:21
fieldNamesWithAlias( $fields)
Gets an array of aliased field names.
Definition: Database.php:2612
insertSelect( $destTable, $srcTable, $varMap, $conds, $fname=__METHOD__, $insertOptions=[], $selectOptions=[], $selectJoinConds=[])
INSERT SELECT wrapper.
Definition: Database.php:3035
doRollbackToSavepoint( $identifier, $fname)
Rollback to a savepoint.
Definition: Database.php:3700
tableName( $name, $format='quoted')
Format a table name ready for use in constructing an SQL query.
Definition: Database.php:2410
runOnTransactionIdleCallbacks( $trigger)
Actually consume and run any "on transaction idle/resolution" callbacks.
Definition: Database.php:3544
getServerUptime()
Determines how long the server has been up.
Definition: Database.php:3299
unionConditionPermutations( $table, $vars, array $permute_conds, $extra_conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Construct a UNION query for permutations of conditions.
Definition: Database.php:3216
setSchemaVars( $vars)
Set variables to be used in sourceFile/sourceStream, in preference to the ones in $GLOBALS...
Definition: Database.php:4358
bool $trxAutomaticAtomic
Record if the current transaction was started implicitly by Database::startAtomic.
Definition: Database.php:219
int $trxAtomicCounter
Counter for atomic savepoint identifiers.
Definition: Database.php:207
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:3409
flushSnapshot( $fname=__METHOD__)
Commit any transaction but error out if writes or callbacks are pending.
Definition: Database.php:4083
$params
indexUnique( $table, $index)
Determines if a given index is unique.
Definition: Database.php:2090
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:1982
tableNameWithAlias( $table, $alias=false)
Get an aliased table name.
Definition: Database.php:2552
getInfinity()
Find out when &#39;infinity&#39; is.
Definition: Database.php:4652
static string $NOT_APPLICABLE
Idiom used when a cancelable atomic section started the transaction.
Definition: Database.php:274
string $lastQuery
SQL query.
Definition: Database.php:75
array $trxAtomicLevels
Array of levels of atomicity within transactions.
Definition: Database.php:213
string [] $trxWriteCallers
Track the write query callers of the current transaction.
Definition: Database.php:225
runTransactionListenerCallbacks( $trigger)
Actually run any "transaction listener" callbacks.
Definition: Database.php:3640
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:780
getApproximateLagStatus()
Get a replica DB lag estimate for this server.
Definition: Database.php:4261
lastError()
Get a description of the last error.
setFlag( $flag, $remember=self::REMEMBER_NOTHING)
Set a flag for this connection.
Definition: Database.php:800
makeUpdateOptions( $options)
Make UPDATE options for the Database::update function.
Definition: Database.php:2180
$buffer
IDatabase null $lazyMasterHandle
Lazy handle to the master DB this server replicates from.
Definition: Database.php:257
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:2634
prependDatabaseOrSchema( $namespace, $relation, $format)
Definition: Database.php:2508
integer null $affectedRowCount
Rows affected by the last query to query() or its CRUD wrappers.
Definition: Database.php:140
doReleaseSavepoint( $identifier, $fname)
Release a savepoint.
Definition: Database.php:3686
isInsertSelectSafe(array $insertOptions, array $selectOptions)
Definition: Database.php:3079
float $trxWriteAdjDuration
Like trxWriteQueryCount but excludes lock-bound, easy to replicate, queries.
Definition: Database.php:241
initConnection()
Initialize the connection to the database over the wire (or to local files)
Definition: Database.php:345
lockForUpdate( $table, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Lock all rows meeting the given conditions/options FOR UPDATE.
Definition: Database.php:2023
restoreFlags( $state=self::RESTORE_PRIOR)
Restore the flags to their prior state before the last setFlag/clearFlag call.
Definition: Database.php:822
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:1787
makeList( $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.
Definition: Database.php:2200
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:294
makeUpdateOptionsArray( $options)
Make UPDATE options array for Database::makeUpdateOptions.
Definition: Database.php:2160
connectionErrorLogger( $errno, $errstr)
Error handler for logging errors during database connection This method should not be used outside of...
Definition: Database.php:917
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:3166
qualifiedTableComponents( $name)
Get the table components needed for a query given the currently selected database.
Definition: Database.php:2468
tableExists( $table, $fname=__METHOD__)
Query whether a given table exists.
getRecordedTransactionLagStatus()
Get the replica DB lag when the current transaction started.
Definition: Database.php:4249
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings, LocalSettings).
Definition: Setup.php:123
runOnTransactionPreCommitCallbacks()
Actually consume and run any "on transaction pre-commit" callbacks.
Definition: Database.php:3605
const LIST_SET
Definition: Defines.php:44
setTableAliases(array $aliases)
Make certain table names use their own database, schema, and table prefix when passed into SQL querie...
Definition: Database.php:4687
trxTimestamp()
Get the UNIX timestamp of the time that the transaction was established.
Definition: Database.php:592
tablePrefix( $prefix=null)
Get/set the table prefix.
Definition: Database.php:604
const LIST_OR
Definition: Defines.php:46
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:2763
deadlockLoop()
Perform a deadlock-prone transaction.
Definition: Database.php:3347
BagOStuff $srvCache
APC cache.
Definition: Database.php:97
const DEADLOCK_TRIES
Number of times to re-try an operation in case of deadlock.
Definition: Database.php:50
cancelAtomic( $fname=__METHOD__, AtomicSectionIdentifier $sectionId=null)
Cancel an atomic section of SQL statements.
Definition: Database.php:3787
getServerInfo()
A string describing the current software version, and possibly other details in a user-friendly way...
Definition: Database.php:573
float bool $lastWriteTime
UNIX timestamp of last write query.
Definition: Database.php:77
const DBO_NOBUFFER
Definition: defines.php:10
string $agent
Agent name for query profiling.
Definition: Database.php:93
unlock( $lockName, $method)
Release a lock.
Definition: Database.php:4543
int $trxStatus
Transaction status.
Definition: Database.php:145
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:4632
ping(&$rtt=null)
Ping the server and try to reconnect if it there is no connection.
Definition: Database.php:4165
lastDoneWrites()
Returns the last time the connection may have been used for write queries.
Definition: Database.php:691
LoggerInterface $connLogger
Definition: Database.php:99
setLazyMasterHandle(IDatabase $conn)
Set a lazy-connecting DB handle to the master DB (for replication status purposes) ...
Definition: Database.php:662
tableNamesWithAlias( $tables)
Gets an array of aliased table names.
Definition: Database.php:2578
bitOr( $fieldLeft, $fieldRight)
Definition: Database.php:2311
Class to handle database/prefix specification for IDatabase domains.
array [] $trxIdleCallbacks
List of (callable, method name, atomic section id)
Definition: Database.php:113
bool $trxDoneWrites
Record if possible write queries were done in the last transaction started.
Definition: Database.php:194
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
float null $trxTimestamp
The UNIX time that the transaction started.
Definition: Database.php:177
getLogContext(array $extras=[])
Create a log context to pass to PSR-3 logger functions.
Definition: Database.php:927
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Single row SELECT wrapper.
Definition: Database.php:1870
string bool $phpError
Definition: Database.php:79
assertNoOpenTransactions()
Assert that all explicit transactions or atomic sections have been closed.
Definition: Database.php:1439
upsert( $table, array $rows, $uniqueIndexes, array $set, $fname=__METHOD__)
INSERT ON DUPLICATE KEY UPDATE wrapper, upserts an array into a table.
Definition: Database.php:2930
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:2530
Relational database abstraction object.
Definition: Database.php:48
array [] $trxEndCallbacks
List of (callable, method name, atomic section id)
Definition: Database.php:117
unionQueries( $sqls, $all)
Construct a UNION query This is used for providing overload point for other DB abstractions not compa...
Definition: Database.php:3210
static attributesFromType( $dbType, $driver=null)
Definition: Database.php:489
wasErrorReissuable()
Determines if the last query error was due to something outside of the query itself.
Definition: Database.php:3319
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:1779
Advanced database interface for IDatabase handles that include maintenance methods.
buildSubstring( $input, $startPosition, $length=null)
Definition: Database.php:2327
wasConnectionError( $errno)
Do not use this method outside of Database/DBError classes.
Definition: Database.php:3333
bool $trxEndCallbacksSuppressed
Whether to suppress triggering of transaction end callbacks.
Definition: Database.php:121
lastQuery()
Return the last query that went through IDatabase::query()
Definition: Database.php:683
pendingWriteAndCallbackCallers()
List the methods that have write queries or callbacks for the current transaction.
Definition: Database.php:772
commit( $fname=__METHOD__, $flush=self::FLUSHING_ONE)
Commits a transaction previously started using begin().
Definition: Database.php:3939
onTransactionResolution(callable $callback, $fname=__METHOD__)
Run a callback as soon as the current transaction commits or rolls back.
Definition: Database.php:3402
listTables( $prefix=null, $fname=__METHOD__)
List all tables on the database.
Definition: Database.php:4106
$line
Definition: cdb.php:59
estimateRowCount( $table, $var=' *', $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Estimate the number of rows in dataset.
Definition: Database.php:1890
doAtomicSection( $fname, callable $callback, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Perform an atomic section of reversable SQL statements from a callback.
Definition: Database.php:3860
restoreErrorHandler()
Restore the previous error handler and return the last PHP error for this DB.
Definition: Database.php:887
array [] $trxPreCommitCallbacks
List of (callable, method name, atomic section id)
Definition: Database.php:115
object resource null $conn
Database connection.
Definition: Database.php:108
assertIsWritableMaster()
Make sure that this server is not marked as a replica nor read-only as a sanity check.
Definition: Database.php:1033
setTransactionListener( $name, callable $callback=null)
Run a callback after each time any transaction commits or rolls back.
Definition: Database.php:3514
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:617
rollback( $fname=__METHOD__, $flush='')
Rollback a transaction previously started using begin().
Definition: Database.php:4013
dropTable( $tableName, $fName=__METHOD__)
Delete a table.
Definition: Database.php:4643
hasFlags( $field, $flags)
Definition: Database.php:4700
const PING_TTL
How long before it is worth doing a dummy query to test the connection.
Definition: Database.php:57
doInitConnection()
Actually connect to the database over the wire (or to local files)
Definition: Database.php:360
deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname=__METHOD__)
DELETE where the condition is a join.
Definition: Database.php:2980
masterPosWait(DBMasterPos $pos, $timeout)
Wait for the replica DB to catch up to a given master position.
Definition: Database.php:3383
string $user
User that this instance is currently connected under the name of.
Definition: Database.php:83
lock( $lockName, $method, $timeout=5)
Acquire a named lock.
Definition: Database.php:4537
callable $deprecationLogger
Deprecation logging callback.
Definition: Database.php:105
addIdentifierQuotes( $s)
Quotes an identifier, in order to make user controlled input safe.
Definition: Database.php:2750
Error thrown when a query times out.
strencode( $s)
Wrapper for addslashes()
selectFieldsOrOptionsAggregate( $fields, $options)
Definition: Database.php:1956
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:4287
lockTables(array $read, array $write, $method)
Lock specific tables.
Definition: Database.php:4592
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
makeOrderBy( $options)
Returns an optional ORDER BY.
Definition: Database.php:1767
wasReadOnlyError()
Determines if the last failure was due to the database being read-only.
Definition: Database.php:3315
selectRowCount( $tables, $var=' *', $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Get the number of rows in dataset.
Definition: Database.php:1907
wasConnectionLoss()
Determines if the last query error was due to a dropped connection.
Definition: Database.php:3311
int $trxWriteQueryCount
Number of write queries for the current transaction.
Definition: Database.php:233
tableLocksHaveTransactionScope()
Checks if table locks acquired by lockTables() are transaction-bound in their scope.
Definition: Database.php:4588
getSchemaVars()
Get schema variables.
Definition: Database.php:4510
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:263
closeConnection()
Closes underlying database connection.
lockIsFree( $lockName, $method)
Check to see if a named lock is not locked by any thread (non-blocking)
Definition: Database.php:4530
array null $preparedArgs
Definition: Database.php:132
makeGroupByWithHaving( $options)
Returns an optional GROUP BY with an optional HAVING.
Definition: Database.php:1741
bool $cliMode
Whether this PHP instance is for a CLI script.
Definition: Database.php:91
doBegin( $fname)
Issues the BEGIN command to the database server.
Definition: Database.php:3934
wasDeadlock()
Determines if the last failure was due to a deadlock.
Definition: Database.php:3303
replace( $table, $uniqueIndexes, $rows, $fname=__METHOD__)
REPLACE query wrapper.
Definition: Database.php:2845
streamStatementEnd(&$sql, &$newLine)
Called by sourceStream() to check if we&#39;ve reached a statement end.
Definition: Database.php:4439
setLBInfo( $name, $value=null)
Set the LB info array, or a member of it.
Definition: Database.php:654
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:4304
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:4100
anyString()
Returns a token for buildLike() that denotes a &#39;&#39; to be used in a LIKE query.
Definition: Database.php:2809
array [] $tableAliases
Map of (table => (dbname, schema, prefix) map)
Definition: Database.php:87
wasLockTimeout()
Determines if the last failure was due to a lock timeout.
Definition: Database.php:3307
buildLike()
LIKE statement wrapper, receives a variable-length argument list with parts of pattern to match conta...
Definition: Database.php:2778
useIndexClause( $index)
USE INDEX clause.
Definition: Database.php:2827
unionSupportsOrderAndLimit()
Returns true if current database backend supports ORDER BY or LIMIT for separate subqueries within th...
Definition: Database.php:3206
updateTrxWriteQueryTime( $sql, $runtime, $affected)
Update the estimated run-time of a query, not counting large row lock times.
Definition: Database.php:1383
wasQueryTimeout( $error, $errno)
Checks whether the cause of the error is detected to be a timeout.
Definition: Database.php:1537
getBindingHandle()
Get the underlying binding connection handle.
Definition: Database.php:4715
static configuration should be added through ResourceLoaderGetConfigVars instead & $vars
Definition: hooks.txt:2217
pendingWriteRowsAffected()
Get the number of affected rows from pending write queries.
Definition: Database.php:760
registerTempTableWrite( $sql, $pseudoPermanent)
Definition: Database.php:1153
string [] $indexAliases
Map of (index alias => index)
Definition: Database.php:89
float $rttEstimate
RTT time estimate.
Definition: Database.php:249
array $namedLocksHeld
Map of (name => 1) for locks obtained via lock()
Definition: Database.php:252
resultObject( $result)
Take the result from a query, and wrap it in a ResultWrapper if necessary.
Definition: Database.php:4152
string $password
Password used to establish the current connection.
Definition: Database.php:85
float $trxWriteDuration
Seconds spent in write queries for the current transaction.
Definition: Database.php:229
getQueryExceptionAndLog( $error, $errno, $sql, $fname)
Definition: Database.php:1569
onTransactionPreCommitOrIdle(callable $callback, $fname=__METHOD__)
Run a callback before the current transaction commits or now if there is none.
Definition: Database.php:3426
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:4362
$matches
getReplicaPos()
Get the replication position of this replica DB.
Definition: Database.php:3388
getDefaultSchemaVars()
Get schema variables to use if none have been set via setSchemaVars().
Definition: Database.php:4526