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  protected $server;
52  protected $user;
54  protected $password;
56  protected $tableAliases = [];
58  protected $indexAliases = [];
60  protected $cliMode;
62  protected $agent;
64  protected $flags;
66  protected $lbInfo = [];
68  protected $schemaVars = false;
70  protected $connectionParams = [];
72  protected $connectionVariables = [];
74  protected $delimiter = ';';
76  protected $htmlErrors;
79 
81  protected $srvCache;
83  protected $connLogger;
85  protected $queryLogger;
87  protected $errorLogger;
89  protected $deprecationLogger;
91  protected $profiler;
93  protected $trxProfiler;
95  protected $currentDomain;
98 
100  protected $conn = null;
102  protected $opened = false;
103 
105  protected $sessionNamedLocks = [];
107  protected $sessionTempTables = [];
108 
110  protected $trxLevel = 0;
112  protected $trxShortId = '';
114  protected $trxStatus = self::STATUS_TRX_NONE;
116  protected $trxStatusCause;
120  private $trxTimestamp = null;
122  private $trxReplicaLag = null;
124  private $trxFname = null;
126  private $trxDoneWrites = false;
128  private $trxAutomatic = false;
130  private $trxAtomicCounter = 0;
132  private $trxAtomicLevels = [];
134  private $trxAutomaticAtomic = false;
136  private $trxWriteCallers = [];
138  private $trxWriteDuration = 0.0;
140  private $trxWriteQueryCount = 0;
144  private $trxWriteAdjDuration = 0.0;
148  private $trxIdleCallbacks = [];
152  private $trxEndCallbacks = [];
156  private $trxEndCallbacksSuppressed = false;
157 
159  private $priorFlags = [];
160 
162  protected $affectedRowCount;
163 
165  private $lastPing = 0.0;
167  private $lastQuery = '';
169  private $lastWriteTime = false;
171  private $lastPhpError = false;
173  private $lastRoundTripEstimate = 0.0;
174 
176  const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
178  const ATTR_SCHEMAS_AS_TABLE_GROUPS = 'supports-schemas';
179 
181  const NEW_UNCONNECTED = 0;
183  const NEW_CONNECTED = 1;
184 
186  const STATUS_TRX_ERROR = 1;
188  const STATUS_TRX_OK = 2;
190  const STATUS_TRX_NONE = 3;
191 
193  private static $NOT_APPLICABLE = 'n/a';
195  private static $SAVEPOINT_PREFIX = 'wikimedia_rdbms_atomic';
196 
198  private static $TEMP_NORMAL = 1;
200  private static $TEMP_PSEUDO_PERMANENT = 2;
201 
203  private static $DEADLOCK_TRIES = 4;
205  private static $DEADLOCK_DELAY_MIN = 500000;
207  private static $DEADLOCK_DELAY_MAX = 1500000;
208 
210  private static $PING_TTL = 1.0;
211  private static $PING_QUERY = 'SELECT 1 AS ping';
212 
213  private static $TINY_WRITE_SEC = 0.010;
214  private static $SLOW_WRITE_SEC = 0.500;
215  private static $SMALL_WRITE_ROWS = 100;
216 
221  protected function __construct( array $params ) {
222  foreach ( [ 'host', 'user', 'password', 'dbname', 'schema', 'tablePrefix' ] as $name ) {
223  $this->connectionParams[$name] = $params[$name];
224  }
225 
226  $this->cliMode = $params['cliMode'];
227  // Agent name is added to SQL queries in a comment, so make sure it can't break out
228  $this->agent = str_replace( '/', '-', $params['agent'] );
229 
230  $this->flags = $params['flags'];
231  if ( $this->flags & self::DBO_DEFAULT ) {
232  if ( $this->cliMode ) {
233  $this->flags &= ~self::DBO_TRX;
234  } else {
235  $this->flags |= self::DBO_TRX;
236  }
237  }
238  // Disregard deprecated DBO_IGNORE flag (T189999)
239  $this->flags &= ~self::DBO_IGNORE;
240 
241  $this->connectionVariables = $params['variables'];
242 
243  $this->srvCache = $params['srvCache'] ?? new HashBagOStuff();
244 
245  $this->profiler = is_callable( $params['profiler'] ) ? $params['profiler'] : null;
246  $this->trxProfiler = $params['trxProfiler'];
247  $this->connLogger = $params['connLogger'];
248  $this->queryLogger = $params['queryLogger'];
249  $this->errorLogger = $params['errorLogger'];
250  $this->deprecationLogger = $params['deprecationLogger'];
251 
252  if ( isset( $params['nonNativeInsertSelectBatchSize'] ) ) {
253  $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'];
254  }
255 
256  // Set initial dummy domain until open() sets the final DB/prefix
257  $this->currentDomain = new DatabaseDomain(
258  $params['dbname'] != '' ? $params['dbname'] : null,
259  $params['schema'] != '' ? $params['schema'] : null,
260  $params['tablePrefix']
261  );
262  }
263 
272  final public function initConnection() {
273  if ( $this->isOpen() ) {
274  throw new LogicException( __METHOD__ . ': already connected.' );
275  }
276  // Establish the connection
277  $this->doInitConnection();
278  }
279 
287  protected function doInitConnection() {
288  if ( strlen( $this->connectionParams['user'] ) ) {
289  $this->open(
290  $this->connectionParams['host'],
291  $this->connectionParams['user'],
292  $this->connectionParams['password'],
293  $this->connectionParams['dbname'],
294  $this->connectionParams['schema'],
295  $this->connectionParams['tablePrefix']
296  );
297  } else {
298  throw new InvalidArgumentException( "No database user provided." );
299  }
300  }
301 
314  abstract protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix );
315 
361  final public static function factory( $dbType, $p = [], $connect = self::NEW_CONNECTED ) {
362  $class = self::getClass( $dbType, $p['driver'] ?? null );
363 
364  if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
365  // Resolve some defaults for b/c
366  $p['host'] = $p['host'] ?? false;
367  $p['user'] = $p['user'] ?? false;
368  $p['password'] = $p['password'] ?? false;
369  $p['dbname'] = $p['dbname'] ?? false;
370  $p['flags'] = $p['flags'] ?? 0;
371  $p['variables'] = $p['variables'] ?? [];
372  $p['tablePrefix'] = $p['tablePrefix'] ?? '';
373  $p['schema'] = $p['schema'] ?? null;
374  $p['cliMode'] = $p['cliMode'] ?? ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' );
375  $p['agent'] = $p['agent'] ?? '';
376  if ( !isset( $p['connLogger'] ) ) {
377  $p['connLogger'] = new NullLogger();
378  }
379  if ( !isset( $p['queryLogger'] ) ) {
380  $p['queryLogger'] = new NullLogger();
381  }
382  $p['profiler'] = $p['profiler'] ?? null;
383  if ( !isset( $p['trxProfiler'] ) ) {
384  $p['trxProfiler'] = new TransactionProfiler();
385  }
386  if ( !isset( $p['errorLogger'] ) ) {
387  $p['errorLogger'] = function ( Exception $e ) {
388  trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
389  };
390  }
391  if ( !isset( $p['deprecationLogger'] ) ) {
392  $p['deprecationLogger'] = function ( $msg ) {
393  trigger_error( $msg, E_USER_DEPRECATED );
394  };
395  }
396 
398  $conn = new $class( $p );
399  if ( $connect == self::NEW_CONNECTED ) {
400  $conn->initConnection();
401  }
402  } else {
403  $conn = null;
404  }
405 
406  return $conn;
407  }
408 
416  final public static function attributesFromType( $dbType, $driver = null ) {
417  static $defaults = [
418  self::ATTR_DB_LEVEL_LOCKING => false,
419  self::ATTR_SCHEMAS_AS_TABLE_GROUPS => false
420  ];
421 
422  $class = self::getClass( $dbType, $driver );
423 
424  return call_user_func( [ $class, 'getAttributes' ] ) + $defaults;
425  }
426 
433  private static function getClass( $dbType, $driver = null ) {
434  // For database types with built-in support, the below maps type to IDatabase
435  // implementations. For types with multipe driver implementations (PHP extensions),
436  // an array can be used, keyed by extension name. In case of an array, the
437  // optional 'driver' parameter can be used to force a specific driver. Otherwise,
438  // we auto-detect the first available driver. For types without built-in support,
439  // an class named "Database<Type>" us used, eg. DatabaseFoo for type 'foo'.
440  static $builtinTypes = [
441  'mssql' => DatabaseMssql::class,
442  'mysql' => [ 'mysqli' => DatabaseMysqli::class ],
443  'sqlite' => DatabaseSqlite::class,
444  'postgres' => DatabasePostgres::class,
445  ];
446 
447  $dbType = strtolower( $dbType );
448  $class = false;
449 
450  if ( isset( $builtinTypes[$dbType] ) ) {
451  $possibleDrivers = $builtinTypes[$dbType];
452  if ( is_string( $possibleDrivers ) ) {
453  $class = $possibleDrivers;
454  } elseif ( (string)$driver !== '' ) {
455  if ( !isset( $possibleDrivers[$driver] ) ) {
456  throw new InvalidArgumentException( __METHOD__ .
457  " type '$dbType' does not support driver '{$driver}'" );
458  }
459 
460  $class = $possibleDrivers[$driver];
461  } else {
462  foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
463  if ( extension_loaded( $posDriver ) ) {
464  $class = $possibleClass;
465  break;
466  }
467  }
468  }
469  } else {
470  $class = 'Database' . ucfirst( $dbType );
471  }
472 
473  if ( $class === false ) {
474  throw new InvalidArgumentException( __METHOD__ .
475  " no viable database extension found for type '$dbType'" );
476  }
477 
478  return $class;
479  }
480 
485  protected static function getAttributes() {
486  return [];
487  }
488 
496  public function setLogger( LoggerInterface $logger ) {
497  $this->queryLogger = $logger;
498  }
499 
500  public function getServerInfo() {
501  return $this->getServerVersion();
502  }
503 
504  public function bufferResults( $buffer = null ) {
505  $res = !$this->getFlag( self::DBO_NOBUFFER );
506  if ( $buffer !== null ) {
507  $buffer
508  ? $this->clearFlag( self::DBO_NOBUFFER )
509  : $this->setFlag( self::DBO_NOBUFFER );
510  }
511 
512  return $res;
513  }
514 
515  public function trxLevel() {
516  return $this->trxLevel;
517  }
518 
519  public function trxTimestamp() {
520  return $this->trxLevel ? $this->trxTimestamp : null;
521  }
522 
527  public function trxStatus() {
528  return $this->trxStatus;
529  }
530 
531  public function tablePrefix( $prefix = null ) {
532  $old = $this->currentDomain->getTablePrefix();
533  if ( $prefix !== null ) {
534  $this->currentDomain = new DatabaseDomain(
535  $this->currentDomain->getDatabase(),
536  $this->currentDomain->getSchema(),
537  $prefix
538  );
539  }
540 
541  return $old;
542  }
543 
544  public function dbSchema( $schema = null ) {
545  if ( strlen( $schema ) && $this->getDBname() === null ) {
546  throw new DBUnexpectedError( $this, "Cannot set schema to '$schema'; no database set." );
547  }
548 
549  $old = $this->currentDomain->getSchema();
550  if ( $schema !== null ) {
551  $this->currentDomain = new DatabaseDomain(
552  $this->currentDomain->getDatabase(),
553  // DatabaseDomain uses null for unspecified schemas
554  strlen( $schema ) ? $schema : null,
555  $this->currentDomain->getTablePrefix()
556  );
557  }
558 
559  return (string)$old;
560  }
561 
565  protected function relationSchemaQualifier() {
566  return $this->dbSchema();
567  }
568 
569  public function getLBInfo( $name = null ) {
570  if ( is_null( $name ) ) {
571  return $this->lbInfo;
572  }
573 
574  if ( array_key_exists( $name, $this->lbInfo ) ) {
575  return $this->lbInfo[$name];
576  }
577 
578  return null;
579  }
580 
581  public function setLBInfo( $name, $value = null ) {
582  if ( is_null( $value ) ) {
583  $this->lbInfo = $name;
584  } else {
585  $this->lbInfo[$name] = $value;
586  }
587  }
588 
589  public function setLazyMasterHandle( IDatabase $conn ) {
590  $this->lazyMasterHandle = $conn;
591  }
592 
598  protected function getLazyMasterHandle() {
600  }
601 
602  public function implicitGroupby() {
603  return true;
604  }
605 
606  public function implicitOrderby() {
607  return true;
608  }
609 
610  public function lastQuery() {
611  return $this->lastQuery;
612  }
613 
614  public function doneWrites() {
615  return (bool)$this->lastWriteTime;
616  }
617 
618  public function lastDoneWrites() {
619  return $this->lastWriteTime ?: false;
620  }
621 
622  public function writesPending() {
623  return $this->trxLevel && $this->trxDoneWrites;
624  }
625 
626  public function writesOrCallbacksPending() {
627  return $this->trxLevel && (
628  $this->trxDoneWrites ||
629  $this->trxIdleCallbacks ||
630  $this->trxPreCommitCallbacks ||
632  );
633  }
634 
635  public function preCommitCallbacksPending() {
636  return $this->trxLevel && $this->trxPreCommitCallbacks;
637  }
638 
642  final protected function getTransactionRoundId() {
643  // If transaction round participation is enabled, see if one is active
644  if ( $this->getFlag( self::DBO_TRX ) ) {
645  $id = $this->getLBInfo( 'trxRoundId' );
646 
647  return is_string( $id ) ? $id : null;
648  }
649 
650  return null;
651  }
652 
653  public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
654  if ( !$this->trxLevel ) {
655  return false;
656  } elseif ( !$this->trxDoneWrites ) {
657  return 0.0;
658  }
659 
660  switch ( $type ) {
661  case self::ESTIMATE_DB_APPLY:
662  return $this->pingAndCalculateLastTrxApplyTime();
663  default: // everything
665  }
666  }
667 
671  private function pingAndCalculateLastTrxApplyTime() {
672  $this->ping( $rtt );
673 
674  $rttAdjTotal = $this->trxWriteAdjQueryCount * $rtt;
675  $applyTime = max( $this->trxWriteAdjDuration - $rttAdjTotal, 0 );
676  // For omitted queries, make them count as something at least
677  $omitted = $this->trxWriteQueryCount - $this->trxWriteAdjQueryCount;
678  $applyTime += self::$TINY_WRITE_SEC * $omitted;
679 
680  return $applyTime;
681  }
682 
683  public function pendingWriteCallers() {
684  return $this->trxLevel ? $this->trxWriteCallers : [];
685  }
686 
687  public function pendingWriteRowsAffected() {
689  }
690 
699  public function pendingWriteAndCallbackCallers() {
700  $fnames = $this->pendingWriteCallers();
701  foreach ( [
702  $this->trxIdleCallbacks,
703  $this->trxPreCommitCallbacks,
704  $this->trxEndCallbacks
705  ] as $callbacks ) {
706  foreach ( $callbacks as $callback ) {
707  $fnames[] = $callback[1];
708  }
709  }
710 
711  return $fnames;
712  }
713 
717  private function flatAtomicSectionList() {
718  return array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) {
719  return $accum === null ? $v[0] : "$accum, " . $v[0];
720  } );
721  }
722 
723  public function isOpen() {
724  return $this->opened;
725  }
726 
727  public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
728  if ( ( $flag & self::DBO_IGNORE ) ) {
729  throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
730  }
731 
732  if ( $remember === self::REMEMBER_PRIOR ) {
733  array_push( $this->priorFlags, $this->flags );
734  }
735  $this->flags |= $flag;
736  }
737 
738  public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
739  if ( ( $flag & self::DBO_IGNORE ) ) {
740  throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
741  }
742 
743  if ( $remember === self::REMEMBER_PRIOR ) {
744  array_push( $this->priorFlags, $this->flags );
745  }
746  $this->flags &= ~$flag;
747  }
748 
749  public function restoreFlags( $state = self::RESTORE_PRIOR ) {
750  if ( !$this->priorFlags ) {
751  return;
752  }
753 
754  if ( $state === self::RESTORE_INITIAL ) {
755  $this->flags = reset( $this->priorFlags );
756  $this->priorFlags = [];
757  } else {
758  $this->flags = array_pop( $this->priorFlags );
759  }
760  }
761 
762  public function getFlag( $flag ) {
763  return (bool)( $this->flags & $flag );
764  }
765 
771  public function getProperty( $name ) {
772  return $this->$name;
773  }
774 
775  public function getDomainID() {
776  return $this->currentDomain->getId();
777  }
778 
779  final public function getWikiID() {
780  return $this->getDomainID();
781  }
782 
790  abstract function indexInfo( $table, $index, $fname = __METHOD__ );
791 
798  abstract function strencode( $s );
799 
803  protected function installErrorHandler() {
804  $this->lastPhpError = false;
805  $this->htmlErrors = ini_set( 'html_errors', '0' );
806  set_error_handler( [ $this, 'connectionErrorLogger' ] );
807  }
808 
814  protected function restoreErrorHandler() {
815  restore_error_handler();
816  if ( $this->htmlErrors !== false ) {
817  ini_set( 'html_errors', $this->htmlErrors );
818  }
819 
820  return $this->getLastPHPError();
821  }
822 
826  protected function getLastPHPError() {
827  if ( $this->lastPhpError ) {
828  $error = preg_replace( '!\[<a.*</a>\]!', '', $this->lastPhpError );
829  $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
830 
831  return $error;
832  }
833 
834  return false;
835  }
836 
844  public function connectionErrorLogger( $errno, $errstr ) {
845  $this->lastPhpError = $errstr;
846  }
847 
854  protected function getLogContext( array $extras = [] ) {
855  return array_merge(
856  [
857  'db_server' => $this->server,
858  'db_name' => $this->getDBname(),
859  'db_user' => $this->user,
860  ],
861  $extras
862  );
863  }
864 
865  final public function close() {
866  $exception = null; // error to throw after disconnecting
867 
868  $wasOpen = $this->opened;
869  // This should mostly do nothing if the connection is already closed
870  if ( $this->conn ) {
871  // Roll back any dangling transaction first
872  if ( $this->trxLevel ) {
873  if ( $this->trxAtomicLevels ) {
874  // Cannot let incomplete atomic sections be committed
875  $levels = $this->flatAtomicSectionList();
876  $exception = new DBUnexpectedError(
877  $this,
878  __METHOD__ . ": atomic sections $levels are still open."
879  );
880  } elseif ( $this->trxAutomatic ) {
881  // Only the connection manager can commit non-empty DBO_TRX transactions
882  // (empty ones we can silently roll back)
883  if ( $this->writesOrCallbacksPending() ) {
884  $exception = new DBUnexpectedError(
885  $this,
886  __METHOD__ .
887  ": mass commit/rollback of peer transaction required (DBO_TRX set)."
888  );
889  }
890  } else {
891  // Manual transactions should have been committed or rolled
892  // back, even if empty.
893  $exception = new DBUnexpectedError(
894  $this,
895  __METHOD__ . ": transaction is still open (from {$this->trxFname})."
896  );
897  }
898 
899  if ( $this->trxEndCallbacksSuppressed ) {
900  $exception = $exception ?: new DBUnexpectedError(
901  $this,
902  __METHOD__ . ': callbacks are suppressed; cannot properly commit.'
903  );
904  }
905 
906  // Rollback the changes and run any callbacks as needed
907  $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
908  }
909 
910  // Close the actual connection in the binding handle
911  $closed = $this->closeConnection();
912  } else {
913  $closed = true; // already closed; nothing to do
914  }
915 
916  $this->conn = false;
917  $this->opened = false;
918 
919  // Throw any unexpected errors after having disconnected
920  if ( $exception instanceof Exception ) {
921  throw $exception;
922  }
923 
924  // Note that various subclasses call close() at the start of open(), which itself is
925  // called by replaceLostConnection(). In that case, just because onTransactionResolution()
926  // callbacks are pending does not mean that an exception should be thrown. Rather, they
927  // will be executed after the reconnection step.
928  if ( $wasOpen ) {
929  // Sanity check that no callbacks are dangling
930  $fnames = $this->pendingWriteAndCallbackCallers();
931  if ( $fnames ) {
932  throw new RuntimeException(
933  "Transaction callbacks are still pending:\n" . implode( ', ', $fnames )
934  );
935  }
936  }
937 
938  return $closed;
939  }
940 
949  final protected function assertHasConnectionHandle() {
950  if ( !$this->isOpen() ) {
951  throw new DBUnexpectedError( $this, "DB connection was already closed." );
952  }
953  }
954 
961  protected function assertIsWritableMaster() {
962  if ( $this->getLBInfo( 'replica' ) === true ) {
963  throw new DBReadOnlyRoleError(
964  $this,
965  'Write operations are not allowed on replica database connections.'
966  );
967  }
968  $reason = $this->getReadOnlyReason();
969  if ( $reason !== false ) {
970  throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
971  }
972  }
973 
979  abstract protected function closeConnection();
980 
986  public function reportConnectionError( $error = 'Unknown error' ) {
987  call_user_func( $this->deprecationLogger, 'Use of ' . __METHOD__ . ' is deprecated.' );
988  throw new DBConnectionError( $this, $this->lastError() ?: $error );
989  }
990 
1020  abstract protected function doQuery( $sql );
1021 
1038  protected function isWriteQuery( $sql ) {
1039  // BEGIN and COMMIT queries are considered read queries here.
1040  // Database backends and drivers (MySQL, MariaDB, php-mysqli) generally
1041  // treat these as write queries, in that their results have "affected rows"
1042  // as meta data as from writes, instead of "num rows" as from reads.
1043  // But, we treat them as read queries because when reading data (from
1044  // either replica or master) we use transactions to enable repeatable-read
1045  // snapshots, which ensures we get consistent results from the same snapshot
1046  // for all queries within a request. Use cases:
1047  // - Treating these as writes would trigger ChronologyProtector (see method doc).
1048  // - We use this method to reject writes to replicas, but we need to allow
1049  // use of transactions on replicas for read snapshots. This is fine given
1050  // that transactions by themselves don't make changes, only actual writes
1051  // within the transaction matter, which we still detect.
1052  return !preg_match(
1053  '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SAVEPOINT|RELEASE|SET|SHOW|EXPLAIN|USE|\(SELECT)\b/i',
1054  $sql
1055  );
1056  }
1057 
1062  protected function getQueryVerb( $sql ) {
1063  return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
1064  }
1065 
1079  protected function isTransactableQuery( $sql ) {
1080  return !in_array(
1081  $this->getQueryVerb( $sql ),
1082  [ 'BEGIN', 'ROLLBACK', 'COMMIT', 'SET', 'SHOW', 'CREATE', 'ALTER', 'USE' ],
1083  true
1084  );
1085  }
1086 
1092  protected function registerTempTableWrite( $sql, $pseudoPermanent ) {
1093  static $qt = '[`"\']?(\w+)[`"\']?'; // quoted table
1094 
1095  if ( preg_match(
1096  '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?' . $qt . '/i',
1097  $sql,
1098  $matches
1099  ) ) {
1100  $type = $pseudoPermanent ? self::$TEMP_PSEUDO_PERMANENT : self::$TEMP_NORMAL;
1101  $this->sessionTempTables[$matches[1]] = $type;
1102 
1103  return $type;
1104  } elseif ( preg_match(
1105  '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?' . $qt . '/i',
1106  $sql,
1107  $matches
1108  ) ) {
1109  $type = $this->sessionTempTables[$matches[1]] ?? null;
1110  unset( $this->sessionTempTables[$matches[1]] );
1111 
1112  return $type;
1113  } elseif ( preg_match(
1114  '/^TRUNCATE\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?' . $qt . '/i',
1115  $sql,
1116  $matches
1117  ) ) {
1118  return $this->sessionTempTables[$matches[1]] ?? null;
1119  } elseif ( preg_match(
1120  '/^(?:(?:INSERT|REPLACE)\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+' . $qt . '/i',
1121  $sql,
1122  $matches
1123  ) ) {
1124  return $this->sessionTempTables[$matches[1]] ?? null;
1125  }
1126 
1127  return null;
1128  }
1129 
1130  public function query( $sql, $fname = __METHOD__, $flags = 0 ) {
1131  $flags = (int)$flags; // b/c; this field used to be a bool
1132  // Sanity check that the SQL query is appropriate in the current context and is
1133  // allowed for an outside caller (e.g. does not break transaction/session tracking).
1134  $this->assertQueryIsCurrentlyAllowed( $sql, $fname );
1135 
1136  // Send the query to the server and fetch any corresponding errors
1137  list( $ret, $err, $errno, $unignorable ) = $this->executeQuery( $sql, $fname, $flags );
1138  if ( $ret === false ) {
1139  $ignoreErrors = $this->hasFlags( $flags, self::QUERY_SILENCE_ERRORS );
1140  // Throw an error unless both the ignore flag was set and a rollback is not needed
1141  $this->reportQueryError( $err, $errno, $sql, $fname, $ignoreErrors && !$unignorable );
1142  }
1143 
1144  return $this->resultObject( $ret );
1145  }
1146 
1167  final protected function executeQuery( $sql, $fname, $flags ) {
1168  $this->assertHasConnectionHandle();
1169 
1170  $priorTransaction = $this->trxLevel;
1171 
1172  if ( $this->isWriteQuery( $sql ) ) {
1173  # In theory, non-persistent writes are allowed in read-only mode, but due to things
1174  # like https://bugs.mysql.com/bug.php?id=33669 that might not work anyway...
1175  $this->assertIsWritableMaster();
1176  # Do not treat temporary table writes as "meaningful writes" since they are only
1177  # visible to one session and are not permanent. Profile them as reads. Integration
1178  # tests can override this behavior via $flags.
1179  $pseudoPermanent = $this->hasFlags( $flags, self::QUERY_PSEUDO_PERMANENT );
1180  $tableType = $this->registerTempTableWrite( $sql, $pseudoPermanent );
1181  $isPermWrite = ( $tableType !== self::$TEMP_NORMAL );
1182  # DBConnRef uses QUERY_REPLICA_ROLE to enforce the replica role for raw SQL queries
1183  if ( $isPermWrite && $this->hasFlags( $flags, self::QUERY_REPLICA_ROLE ) ) {
1184  throw new DBReadOnlyRoleError( $this, "Cannot write; target role is DB_REPLICA" );
1185  }
1186  } else {
1187  $isPermWrite = false;
1188  }
1189 
1190  // Add trace comment to the begin of the sql string, right after the operator.
1191  // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
1192  $commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
1193 
1194  // Send the query to the server and fetch any corresponding errors
1195  list( $ret, $err, $errno, $recoverableSR, $recoverableCL, $reconnected ) =
1196  $this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags );
1197  // Check if the query failed due to a recoverable connection loss
1198  if ( $ret === false && $recoverableCL && $reconnected ) {
1199  // Silently resend the query to the server since it is safe and possible
1200  list( $ret, $err, $errno, $recoverableSR, $recoverableCL ) =
1201  $this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags );
1202  }
1203 
1204  $corruptedTrx = false;
1205 
1206  if ( $ret === false ) {
1207  if ( $priorTransaction ) {
1208  if ( $recoverableSR ) {
1209  # We're ignoring an error that caused just the current query to be aborted.
1210  # But log the cause so we can log a deprecation notice if a caller actually
1211  # does ignore it.
1212  $this->trxStatusIgnoredCause = [ $err, $errno, $fname ];
1213  } elseif ( !$recoverableCL ) {
1214  # Either the query was aborted or all queries after BEGIN where aborted.
1215  # In the first case, the only options going forward are (a) ROLLBACK, or
1216  # (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
1217  # option is ROLLBACK, since the snapshots would have been released.
1218  $corruptedTrx = true; // cannot recover
1219  $this->trxStatus = self::STATUS_TRX_ERROR;
1220  $this->trxStatusCause =
1221  $this->getQueryExceptionAndLog( $err, $errno, $sql, $fname );
1222  $this->trxStatusIgnoredCause = null;
1223  }
1224  }
1225  }
1226 
1227  return [ $ret, $err, $errno, $corruptedTrx ];
1228  }
1229 
1248  private function executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags ) {
1249  $priorWritesPending = $this->writesOrCallbacksPending();
1250 
1251  if ( ( $flags & self::QUERY_IGNORE_DBO_TRX ) == 0 ) {
1252  $this->beginIfImplied( $sql, $fname );
1253  }
1254 
1255  // Keep track of whether the transaction has write queries pending
1256  if ( $isPermWrite ) {
1257  $this->lastWriteTime = microtime( true );
1258  if ( $this->trxLevel && !$this->trxDoneWrites ) {
1259  $this->trxDoneWrites = true;
1260  $this->trxProfiler->transactionWritingIn(
1261  $this->server, $this->getDomainID(), $this->trxShortId );
1262  }
1263  }
1264 
1265  $prefix = !is_null( $this->getLBInfo( 'master' ) ) ? 'query-m: ' : 'query: ';
1266  $generalizedSql = new GeneralizedSql( $sql, $this->trxShortId, $prefix );
1267 
1268  $startTime = microtime( true );
1269  $ps = $this->profiler
1270  ? ( $this->profiler )( $generalizedSql->stringify() )
1271  : null;
1272  $this->affectedRowCount = null;
1273  $this->lastQuery = $sql;
1274  $ret = $this->doQuery( $commentedSql );
1275  $lastError = $this->lastError();
1276  $lastErrno = $this->lastErrno();
1277 
1278  $this->affectedRowCount = $this->affectedRows();
1279  unset( $ps ); // profile out (if set)
1280  $queryRuntime = max( microtime( true ) - $startTime, 0.0 );
1281 
1282  $recoverableSR = false; // recoverable statement rollback?
1283  $recoverableCL = false; // recoverable connection loss?
1284  $reconnected = false; // reconnection both attempted and succeeded?
1285 
1286  if ( $ret !== false ) {
1287  $this->lastPing = $startTime;
1288  if ( $isPermWrite && $this->trxLevel ) {
1289  $this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() );
1290  $this->trxWriteCallers[] = $fname;
1291  }
1292  } elseif ( $this->wasConnectionError( $lastErrno ) ) {
1293  # Check if no meaningful session state was lost
1294  $recoverableCL = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
1295  # Update session state tracking and try to restore the connection
1296  $reconnected = $this->replaceLostConnection( __METHOD__ );
1297  } else {
1298  # Check if only the last query was rolled back
1299  $recoverableSR = $this->wasKnownStatementRollbackError();
1300  }
1301 
1302  if ( $sql === self::$PING_QUERY ) {
1303  $this->lastRoundTripEstimate = $queryRuntime;
1304  }
1305 
1306  $this->trxProfiler->recordQueryCompletion(
1307  $generalizedSql,
1308  $startTime,
1309  $isPermWrite,
1310  $isPermWrite ? $this->affectedRows() : $this->numRows( $ret )
1311  );
1312 
1313  // Avoid the overhead of logging calls unless debug mode is enabled
1314  if ( $this->getFlag( self::DBO_DEBUG ) ) {
1315  $this->queryLogger->debug(
1316  "{method} [{runtime}s]: $sql",
1317  [
1318  'method' => $fname,
1319  'db_host' => $this->getServer(),
1320  'domain' => $this->getDomainID(),
1321  'runtime' => round( $queryRuntime, 3 )
1322  ]
1323  );
1324  }
1325 
1326  return [ $ret, $lastError, $lastErrno, $recoverableSR, $recoverableCL, $reconnected ];
1327  }
1328 
1335  private function beginIfImplied( $sql, $fname ) {
1336  if (
1337  !$this->trxLevel &&
1338  $this->getFlag( self::DBO_TRX ) &&
1339  $this->isTransactableQuery( $sql )
1340  ) {
1341  $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
1342  $this->trxAutomatic = true;
1343  }
1344  }
1345 
1358  private function updateTrxWriteQueryTime( $sql, $runtime, $affected ) {
1359  // Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
1360  $indicativeOfReplicaRuntime = true;
1361  if ( $runtime > self::$SLOW_WRITE_SEC ) {
1362  $verb = $this->getQueryVerb( $sql );
1363  // insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
1364  if ( $verb === 'INSERT' ) {
1365  $indicativeOfReplicaRuntime = $this->affectedRows() > self::$SMALL_WRITE_ROWS;
1366  } elseif ( $verb === 'REPLACE' ) {
1367  $indicativeOfReplicaRuntime = $this->affectedRows() > self::$SMALL_WRITE_ROWS / 2;
1368  }
1369  }
1370 
1371  $this->trxWriteDuration += $runtime;
1372  $this->trxWriteQueryCount += 1;
1373  $this->trxWriteAffectedRows += $affected;
1374  if ( $indicativeOfReplicaRuntime ) {
1375  $this->trxWriteAdjDuration += $runtime;
1376  $this->trxWriteAdjQueryCount += 1;
1377  }
1378  }
1379 
1387  private function assertQueryIsCurrentlyAllowed( $sql, $fname ) {
1388  $verb = $this->getQueryVerb( $sql );
1389  if ( $verb === 'USE' ) {
1390  throw new DBUnexpectedError( $this, "Got USE query; use selectDomain() instead." );
1391  }
1392 
1393  if ( $verb === 'ROLLBACK' ) { // transaction/savepoint
1394  return;
1395  }
1396 
1397  if ( $this->trxStatus < self::STATUS_TRX_OK ) {
1398  throw new DBTransactionStateError(
1399  $this,
1400  "Cannot execute query from $fname while transaction status is ERROR.",
1401  [],
1402  $this->trxStatusCause
1403  );
1404  } elseif ( $this->trxStatus === self::STATUS_TRX_OK && $this->trxStatusIgnoredCause ) {
1405  list( $iLastError, $iLastErrno, $iFname ) = $this->trxStatusIgnoredCause;
1406  call_user_func( $this->deprecationLogger,
1407  "Caller from $fname ignored an error originally raised from $iFname: " .
1408  "[$iLastErrno] $iLastError"
1409  );
1410  $this->trxStatusIgnoredCause = null;
1411  }
1412  }
1413 
1414  public function assertNoOpenTransactions() {
1415  if ( $this->explicitTrxActive() ) {
1416  throw new DBTransactionError(
1417  $this,
1418  "Explicit transaction still active. A caller may have caught an error. "
1419  . "Open transactions: " . $this->flatAtomicSectionList()
1420  );
1421  }
1422  }
1423 
1433  private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
1434  # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
1435  # Dropped connections also mean that named locks are automatically released.
1436  # Only allow error suppression in autocommit mode or when the lost transaction
1437  # didn't matter anyway (aside from DBO_TRX snapshot loss).
1438  if ( $this->sessionNamedLocks ) {
1439  return false; // possible critical section violation
1440  } elseif ( $this->sessionTempTables ) {
1441  return false; // tables might be queried latter
1442  } elseif ( $sql === 'COMMIT' ) {
1443  return !$priorWritesPending; // nothing written anyway? (T127428)
1444  } elseif ( $sql === 'ROLLBACK' ) {
1445  return true; // transaction lost...which is also what was requested :)
1446  } elseif ( $this->explicitTrxActive() ) {
1447  return false; // don't drop atomocity and explicit snapshots
1448  } elseif ( $priorWritesPending ) {
1449  return false; // prior writes lost from implicit transaction
1450  }
1451 
1452  return true;
1453  }
1454 
1458  private function handleSessionLossPreconnect() {
1459  // Clean up tracking of session-level things...
1460  // https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html
1461  // https://www.postgresql.org/docs/9.2/static/sql-createtable.html (ignoring ON COMMIT)
1462  $this->sessionTempTables = [];
1463  // https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
1464  // https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
1465  $this->sessionNamedLocks = [];
1466  // Session loss implies transaction loss
1467  $this->trxLevel = 0;
1468  $this->trxAtomicCounter = 0;
1469  $this->trxIdleCallbacks = []; // T67263; transaction already lost
1470  $this->trxPreCommitCallbacks = []; // T67263; transaction already lost
1471  // @note: leave trxRecurringCallbacks in place
1472  if ( $this->trxDoneWrites ) {
1473  $this->trxProfiler->transactionWritingOut(
1474  $this->server,
1475  $this->getDomainID(),
1476  $this->trxShortId,
1477  $this->pendingWriteQueryDuration( self::ESTIMATE_TOTAL ),
1478  $this->trxWriteAffectedRows
1479  );
1480  }
1481  }
1482 
1486  private function handleSessionLossPostconnect() {
1487  try {
1488  // Handle callbacks in trxEndCallbacks, e.g. onTransactionResolution().
1489  // If callback suppression is set then the array will remain unhandled.
1490  $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
1491  } catch ( Exception $ex ) {
1492  // Already logged; move on...
1493  }
1494  try {
1495  // Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener()
1496  $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
1497  } catch ( Exception $ex ) {
1498  // Already logged; move on...
1499  }
1500  }
1501 
1512  protected function wasQueryTimeout( $error, $errno ) {
1513  return false;
1514  }
1515 
1527  public function reportQueryError( $error, $errno, $sql, $fname, $ignore = false ) {
1528  if ( $ignore ) {
1529  $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
1530  } else {
1531  $exception = $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
1532 
1533  throw $exception;
1534  }
1535  }
1536 
1544  private function getQueryExceptionAndLog( $error, $errno, $sql, $fname ) {
1545  $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
1546  $this->queryLogger->error(
1547  "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
1548  $this->getLogContext( [
1549  'method' => __METHOD__,
1550  'errno' => $errno,
1551  'error' => $error,
1552  'sql1line' => $sql1line,
1553  'fname' => $fname,
1554  'trace' => ( new RuntimeException() )->getTraceAsString()
1555  ] )
1556  );
1557  $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
1558  if ( $this->wasQueryTimeout( $error, $errno ) ) {
1559  $e = new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
1560  } elseif ( $this->wasConnectionError( $errno ) ) {
1561  $e = new DBQueryDisconnectedError( $this, $error, $errno, $sql, $fname );
1562  } else {
1563  $e = new DBQueryError( $this, $error, $errno, $sql, $fname );
1564  }
1565 
1566  return $e;
1567  }
1568 
1569  public function freeResult( $res ) {
1570  }
1571 
1572  public function selectField(
1573  $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1574  ) {
1575  if ( $var === '*' ) { // sanity
1576  throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1577  }
1578 
1579  if ( !is_array( $options ) ) {
1580  $options = [ $options ];
1581  }
1582 
1583  $options['LIMIT'] = 1;
1584 
1585  $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1586  if ( $res === false ) {
1587  throw new DBUnexpectedError( $this, "Got false from select()" );
1588  }
1589 
1590  $row = $this->fetchRow( $res );
1591  if ( $row === false ) {
1592  return false;
1593  }
1594 
1595  return reset( $row );
1596  }
1597 
1598  public function selectFieldValues(
1599  $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1600  ) {
1601  if ( $var === '*' ) { // sanity
1602  throw new DBUnexpectedError( $this, "Cannot use a * field" );
1603  } elseif ( !is_string( $var ) ) { // sanity
1604  throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1605  }
1606 
1607  if ( !is_array( $options ) ) {
1608  $options = [ $options ];
1609  }
1610 
1611  $res = $this->select( $table, [ 'value' => $var ], $cond, $fname, $options, $join_conds );
1612  if ( $res === false ) {
1613  throw new DBUnexpectedError( $this, "Got false from select()" );
1614  }
1615 
1616  $values = [];
1617  foreach ( $res as $row ) {
1618  $values[] = $row->value;
1619  }
1620 
1621  return $values;
1622  }
1623 
1633  protected function makeSelectOptions( $options ) {
1634  $preLimitTail = $postLimitTail = '';
1635  $startOpts = '';
1636 
1637  $noKeyOptions = [];
1638 
1639  foreach ( $options as $key => $option ) {
1640  if ( is_numeric( $key ) ) {
1641  $noKeyOptions[$option] = true;
1642  }
1643  }
1644 
1645  $preLimitTail .= $this->makeGroupByWithHaving( $options );
1646 
1647  $preLimitTail .= $this->makeOrderBy( $options );
1648 
1649  if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1650  $postLimitTail .= ' FOR UPDATE';
1651  }
1652 
1653  if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1654  $postLimitTail .= ' LOCK IN SHARE MODE';
1655  }
1656 
1657  if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1658  $startOpts .= 'DISTINCT';
1659  }
1660 
1661  # Various MySQL extensions
1662  if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1663  $startOpts .= ' /*! STRAIGHT_JOIN */';
1664  }
1665 
1666  if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
1667  $startOpts .= ' HIGH_PRIORITY';
1668  }
1669 
1670  if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1671  $startOpts .= ' SQL_BIG_RESULT';
1672  }
1673 
1674  if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1675  $startOpts .= ' SQL_BUFFER_RESULT';
1676  }
1677 
1678  if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1679  $startOpts .= ' SQL_SMALL_RESULT';
1680  }
1681 
1682  if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1683  $startOpts .= ' SQL_CALC_FOUND_ROWS';
1684  }
1685 
1686  if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
1687  $startOpts .= ' SQL_CACHE';
1688  }
1689 
1690  if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
1691  $startOpts .= ' SQL_NO_CACHE';
1692  }
1693 
1694  if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1695  $useIndex = $this->useIndexClause( $options['USE INDEX'] );
1696  } else {
1697  $useIndex = '';
1698  }
1699  if ( isset( $options['IGNORE INDEX'] ) && is_string( $options['IGNORE INDEX'] ) ) {
1700  $ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
1701  } else {
1702  $ignoreIndex = '';
1703  }
1704 
1705  return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1706  }
1707 
1716  protected function makeGroupByWithHaving( $options ) {
1717  $sql = '';
1718  if ( isset( $options['GROUP BY'] ) ) {
1719  $gb = is_array( $options['GROUP BY'] )
1720  ? implode( ',', $options['GROUP BY'] )
1721  : $options['GROUP BY'];
1722  $sql .= ' GROUP BY ' . $gb;
1723  }
1724  if ( isset( $options['HAVING'] ) ) {
1725  $having = is_array( $options['HAVING'] )
1726  ? $this->makeList( $options['HAVING'], self::LIST_AND )
1727  : $options['HAVING'];
1728  $sql .= ' HAVING ' . $having;
1729  }
1730 
1731  return $sql;
1732  }
1733 
1742  protected function makeOrderBy( $options ) {
1743  if ( isset( $options['ORDER BY'] ) ) {
1744  $ob = is_array( $options['ORDER BY'] )
1745  ? implode( ',', $options['ORDER BY'] )
1746  : $options['ORDER BY'];
1747 
1748  return ' ORDER BY ' . $ob;
1749  }
1750 
1751  return '';
1752  }
1753 
1754  public function select(
1755  $table, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1756  ) {
1757  $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1758 
1759  return $this->query( $sql, $fname );
1760  }
1761 
1762  public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1763  $options = [], $join_conds = []
1764  ) {
1765  if ( is_array( $vars ) ) {
1766  $fields = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1767  } else {
1768  $fields = $vars;
1769  }
1770 
1771  $options = (array)$options;
1772  $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1773  ? $options['USE INDEX']
1774  : [];
1775  $ignoreIndexes = (
1776  isset( $options['IGNORE INDEX'] ) &&
1777  is_array( $options['IGNORE INDEX'] )
1778  )
1779  ? $options['IGNORE INDEX']
1780  : [];
1781 
1782  if (
1785  ) {
1786  // Some DB types (postgres/oracle) disallow FOR UPDATE with aggregate
1787  // functions. Discourage use of such queries to encourage compatibility.
1788  call_user_func(
1789  $this->deprecationLogger,
1790  __METHOD__ . ": aggregation used with a locking SELECT ($fname)."
1791  );
1792  }
1793 
1794  if ( is_array( $table ) ) {
1795  $from = ' FROM ' .
1797  $table, $useIndexes, $ignoreIndexes, $join_conds );
1798  } elseif ( $table != '' ) {
1799  $from = ' FROM ' .
1801  [ $table ], $useIndexes, $ignoreIndexes, [] );
1802  } else {
1803  $from = '';
1804  }
1805 
1806  list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
1807  $this->makeSelectOptions( $options );
1808 
1809  if ( is_array( $conds ) ) {
1810  $conds = $this->makeList( $conds, self::LIST_AND );
1811  }
1812 
1813  if ( $conds === null || $conds === false ) {
1814  $this->queryLogger->warning(
1815  __METHOD__
1816  . ' called from '
1817  . $fname
1818  . ' with incorrect parameters: $conds must be a string or an array'
1819  );
1820  $conds = '';
1821  }
1822 
1823  if ( $conds === '' || $conds === '*' ) {
1824  $sql = "SELECT $startOpts $fields $from $useIndex $ignoreIndex $preLimitTail";
1825  } elseif ( is_string( $conds ) ) {
1826  $sql = "SELECT $startOpts $fields $from $useIndex $ignoreIndex " .
1827  "WHERE $conds $preLimitTail";
1828  } else {
1829  throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
1830  }
1831 
1832  if ( isset( $options['LIMIT'] ) ) {
1833  $sql = $this->limitResult( $sql, $options['LIMIT'],
1834  $options['OFFSET'] ?? false );
1835  }
1836  $sql = "$sql $postLimitTail";
1837 
1838  if ( isset( $options['EXPLAIN'] ) ) {
1839  $sql = 'EXPLAIN ' . $sql;
1840  }
1841 
1842  return $sql;
1843  }
1844 
1845  public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1846  $options = [], $join_conds = []
1847  ) {
1848  $options = (array)$options;
1849  $options['LIMIT'] = 1;
1850 
1851  $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1852  if ( $res === false ) {
1853  throw new DBUnexpectedError( $this, "Got false from select()" );
1854  }
1855 
1856  if ( !$this->numRows( $res ) ) {
1857  return false;
1858  }
1859 
1860  return $this->fetchObject( $res );
1861  }
1862 
1863  public function estimateRowCount(
1864  $table, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1865  ) {
1866  $conds = $this->normalizeConditions( $conds, $fname );
1867  $column = $this->extractSingleFieldFromList( $var );
1868  if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
1869  $conds[] = "$column IS NOT NULL";
1870  }
1871 
1872  $res = $this->select(
1873  $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options, $join_conds
1874  );
1875  $row = $res ? $this->fetchRow( $res ) : [];
1876 
1877  return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
1878  }
1879 
1880  public function selectRowCount(
1881  $tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1882  ) {
1883  $conds = $this->normalizeConditions( $conds, $fname );
1884  $column = $this->extractSingleFieldFromList( $var );
1885  if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
1886  $conds[] = "$column IS NOT NULL";
1887  }
1888 
1889  $res = $this->select(
1890  [
1891  'tmp_count' => $this->buildSelectSubquery(
1892  $tables,
1893  '1',
1894  $conds,
1895  $fname,
1896  $options,
1897  $join_conds
1898  )
1899  ],
1900  [ 'rowcount' => 'COUNT(*)' ],
1901  [],
1902  $fname
1903  );
1904  $row = $res ? $this->fetchRow( $res ) : [];
1905 
1906  return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
1907  }
1908 
1914  $options = (array)$options;
1915  foreach ( [ 'FOR UPDATE', 'LOCK IN SHARE MODE' ] as $lock ) {
1916  if ( in_array( $lock, $options, true ) ) {
1917  return true;
1918  }
1919  }
1920 
1921  return false;
1922  }
1923 
1929  private function selectFieldsOrOptionsAggregate( $fields, $options ) {
1930  foreach ( (array)$options as $key => $value ) {
1931  if ( is_string( $key ) ) {
1932  if ( preg_match( '/^(?:GROUP BY|HAVING)$/i', $key ) ) {
1933  return true;
1934  }
1935  } elseif ( is_string( $value ) ) {
1936  if ( preg_match( '/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
1937  return true;
1938  }
1939  }
1940  }
1941 
1942  $regex = '/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
1943  foreach ( (array)$fields as $field ) {
1944  if ( is_string( $field ) && preg_match( $regex, $field ) ) {
1945  return true;
1946  }
1947  }
1948 
1949  return false;
1950  }
1951 
1957  final protected function normalizeConditions( $conds, $fname ) {
1958  if ( $conds === null || $conds === false ) {
1959  $this->queryLogger->warning(
1960  __METHOD__
1961  . ' called from '
1962  . $fname
1963  . ' with incorrect parameters: $conds must be a string or an array'
1964  );
1965  $conds = '';
1966  }
1967 
1968  if ( !is_array( $conds ) ) {
1969  $conds = ( $conds === '' ) ? [] : [ $conds ];
1970  }
1971 
1972  return $conds;
1973  }
1974 
1980  final protected function extractSingleFieldFromList( $var ) {
1981  if ( is_array( $var ) ) {
1982  if ( !$var ) {
1983  $column = null;
1984  } elseif ( count( $var ) == 1 ) {
1985  $column = $var[0] ?? reset( $var );
1986  } else {
1987  throw new DBUnexpectedError( $this, __METHOD__ . ': got multiple columns.' );
1988  }
1989  } else {
1990  $column = $var;
1991  }
1992 
1993  return $column;
1994  }
1995 
1996  public function lockForUpdate(
1997  $table, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1998  ) {
1999  if ( !$this->trxLevel && !$this->getFlag( self::DBO_TRX ) ) {
2000  throw new DBUnexpectedError(
2001  $this,
2002  __METHOD__ . ': no transaction is active nor is DBO_TRX set'
2003  );
2004  }
2005 
2006  $options = (array)$options;
2007  $options[] = 'FOR UPDATE';
2008 
2009  return $this->selectRowCount( $table, '*', $conds, $fname, $options, $join_conds );
2010  }
2011 
2012  public function fieldExists( $table, $field, $fname = __METHOD__ ) {
2013  $info = $this->fieldInfo( $table, $field );
2014 
2015  return (bool)$info;
2016  }
2017 
2018  public function indexExists( $table, $index, $fname = __METHOD__ ) {
2019  if ( !$this->tableExists( $table ) ) {
2020  return null;
2021  }
2022 
2023  $info = $this->indexInfo( $table, $index, $fname );
2024  if ( is_null( $info ) ) {
2025  return null;
2026  } else {
2027  return $info !== false;
2028  }
2029  }
2030 
2031  abstract public function tableExists( $table, $fname = __METHOD__ );
2032 
2033  public function indexUnique( $table, $index ) {
2034  $indexInfo = $this->indexInfo( $table, $index );
2035 
2036  if ( !$indexInfo ) {
2037  return null;
2038  }
2039 
2040  return !$indexInfo[0]->Non_unique;
2041  }
2042 
2049  protected function makeInsertOptions( $options ) {
2050  return implode( ' ', $options );
2051  }
2052 
2053  public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
2054  # No rows to insert, easy just return now
2055  if ( !count( $a ) ) {
2056  return true;
2057  }
2058 
2059  $table = $this->tableName( $table );
2060 
2061  if ( !is_array( $options ) ) {
2062  $options = [ $options ];
2063  }
2064 
2065  $options = $this->makeInsertOptions( $options );
2066 
2067  if ( isset( $a[0] ) && is_array( $a[0] ) ) {
2068  $multi = true;
2069  $keys = array_keys( $a[0] );
2070  } else {
2071  $multi = false;
2072  $keys = array_keys( $a );
2073  }
2074 
2075  $sql = 'INSERT ' . $options .
2076  " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
2077 
2078  if ( $multi ) {
2079  $first = true;
2080  foreach ( $a as $row ) {
2081  if ( $first ) {
2082  $first = false;
2083  } else {
2084  $sql .= ',';
2085  }
2086  $sql .= '(' . $this->makeList( $row ) . ')';
2087  }
2088  } else {
2089  $sql .= '(' . $this->makeList( $a ) . ')';
2090  }
2091 
2092  $this->query( $sql, $fname );
2093 
2094  return true;
2095  }
2096 
2103  protected function makeUpdateOptionsArray( $options ) {
2104  if ( !is_array( $options ) ) {
2105  $options = [ $options ];
2106  }
2107 
2108  $opts = [];
2109 
2110  if ( in_array( 'IGNORE', $options ) ) {
2111  $opts[] = 'IGNORE';
2112  }
2113 
2114  return $opts;
2115  }
2116 
2123  protected function makeUpdateOptions( $options ) {
2124  $opts = $this->makeUpdateOptionsArray( $options );
2125 
2126  return implode( ' ', $opts );
2127  }
2128 
2129  public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
2130  $table = $this->tableName( $table );
2131  $opts = $this->makeUpdateOptions( $options );
2132  $sql = "UPDATE $opts $table SET " . $this->makeList( $values, self::LIST_SET );
2133 
2134  if ( $conds !== [] && $conds !== '*' ) {
2135  $sql .= " WHERE " . $this->makeList( $conds, self::LIST_AND );
2136  }
2137 
2138  $this->query( $sql, $fname );
2139 
2140  return true;
2141  }
2142 
2143  public function makeList( $a, $mode = self::LIST_COMMA ) {
2144  if ( !is_array( $a ) ) {
2145  throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
2146  }
2147 
2148  $first = true;
2149  $list = '';
2150 
2151  foreach ( $a as $field => $value ) {
2152  if ( !$first ) {
2153  if ( $mode == self::LIST_AND ) {
2154  $list .= ' AND ';
2155  } elseif ( $mode == self::LIST_OR ) {
2156  $list .= ' OR ';
2157  } else {
2158  $list .= ',';
2159  }
2160  } else {
2161  $first = false;
2162  }
2163 
2164  if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
2165  $list .= "($value)";
2166  } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
2167  $list .= "$value";
2168  } elseif (
2169  ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
2170  ) {
2171  // Remove null from array to be handled separately if found
2172  $includeNull = false;
2173  foreach ( array_keys( $value, null, true ) as $nullKey ) {
2174  $includeNull = true;
2175  unset( $value[$nullKey] );
2176  }
2177  if ( count( $value ) == 0 && !$includeNull ) {
2178  throw new InvalidArgumentException(
2179  __METHOD__ . ": empty input for field $field" );
2180  } elseif ( count( $value ) == 0 ) {
2181  // only check if $field is null
2182  $list .= "$field IS NULL";
2183  } else {
2184  // IN clause contains at least one valid element
2185  if ( $includeNull ) {
2186  // Group subconditions to ensure correct precedence
2187  $list .= '(';
2188  }
2189  if ( count( $value ) == 1 ) {
2190  // Special-case single values, as IN isn't terribly efficient
2191  // Don't necessarily assume the single key is 0; we don't
2192  // enforce linear numeric ordering on other arrays here.
2193  $value = array_values( $value )[0];
2194  $list .= $field . " = " . $this->addQuotes( $value );
2195  } else {
2196  $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
2197  }
2198  // if null present in array, append IS NULL
2199  if ( $includeNull ) {
2200  $list .= " OR $field IS NULL)";
2201  }
2202  }
2203  } elseif ( $value === null ) {
2204  if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
2205  $list .= "$field IS ";
2206  } elseif ( $mode == self::LIST_SET ) {
2207  $list .= "$field = ";
2208  }
2209  $list .= 'NULL';
2210  } else {
2211  if (
2212  $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
2213  ) {
2214  $list .= "$field = ";
2215  }
2216  $list .= $mode == self::LIST_NAMES ? $value : $this->addQuotes( $value );
2217  }
2218  }
2219 
2220  return $list;
2221  }
2222 
2223  public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
2224  $conds = [];
2225 
2226  foreach ( $data as $base => $sub ) {
2227  if ( count( $sub ) ) {
2228  $conds[] = $this->makeList(
2229  [ $baseKey => $base, $subKey => array_keys( $sub ) ],
2230  self::LIST_AND );
2231  }
2232  }
2233 
2234  if ( $conds ) {
2235  return $this->makeList( $conds, self::LIST_OR );
2236  } else {
2237  // Nothing to search for...
2238  return false;
2239  }
2240  }
2241 
2242  public function aggregateValue( $valuedata, $valuename = 'value' ) {
2243  return $valuename;
2244  }
2245 
2246  public function bitNot( $field ) {
2247  return "(~$field)";
2248  }
2249 
2250  public function bitAnd( $fieldLeft, $fieldRight ) {
2251  return "($fieldLeft & $fieldRight)";
2252  }
2253 
2254  public function bitOr( $fieldLeft, $fieldRight ) {
2255  return "($fieldLeft | $fieldRight)";
2256  }
2257 
2258  public function buildConcat( $stringList ) {
2259  return 'CONCAT(' . implode( ',', $stringList ) . ')';
2260  }
2261 
2262  public function buildGroupConcatField(
2263  $delim, $table, $field, $conds = '', $join_conds = []
2264  ) {
2265  $fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
2266 
2267  return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
2268  }
2269 
2270  public function buildSubstring( $input, $startPosition, $length = null ) {
2271  $this->assertBuildSubstringParams( $startPosition, $length );
2272  $functionBody = "$input FROM $startPosition";
2273  if ( $length !== null ) {
2274  $functionBody .= " FOR $length";
2275  }
2276  return 'SUBSTRING(' . $functionBody . ')';
2277  }
2278 
2291  protected function assertBuildSubstringParams( $startPosition, $length ) {
2292  if ( !is_int( $startPosition ) || $startPosition <= 0 ) {
2293  throw new InvalidArgumentException(
2294  '$startPosition must be a positive integer'
2295  );
2296  }
2297  if ( !( is_int( $length ) && $length >= 0 || $length === null ) ) {
2298  throw new InvalidArgumentException(
2299  '$length must be null or an integer greater than or equal to 0'
2300  );
2301  }
2302  }
2303 
2304  public function buildStringCast( $field ) {
2305  // In theory this should work for any standards-compliant
2306  // SQL implementation, although it may not be the best way to do it.
2307  return "CAST( $field AS CHARACTER )";
2308  }
2309 
2310  public function buildIntegerCast( $field ) {
2311  return 'CAST( ' . $field . ' AS INTEGER )';
2312  }
2313 
2314  public function buildSelectSubquery(
2315  $table, $vars, $conds = '', $fname = __METHOD__,
2316  $options = [], $join_conds = []
2317  ) {
2318  return new Subquery(
2319  $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
2320  );
2321  }
2322 
2323  public function databasesAreIndependent() {
2324  return false;
2325  }
2326 
2327  final public function selectDB( $db ) {
2328  $this->selectDomain( new DatabaseDomain(
2329  $db,
2330  $this->currentDomain->getSchema(),
2331  $this->currentDomain->getTablePrefix()
2332  ) );
2333 
2334  return true;
2335  }
2336 
2337  final public function selectDomain( $domain ) {
2338  $this->doSelectDomain( DatabaseDomain::newFromId( $domain ) );
2339  }
2340 
2341  protected function doSelectDomain( DatabaseDomain $domain ) {
2342  $this->currentDomain = $domain;
2343  }
2344 
2345  public function getDBname() {
2346  return $this->currentDomain->getDatabase();
2347  }
2348 
2349  public function getServer() {
2350  return $this->server;
2351  }
2352 
2353  public function tableName( $name, $format = 'quoted' ) {
2354  if ( $name instanceof Subquery ) {
2355  throw new DBUnexpectedError(
2356  $this,
2357  __METHOD__ . ': got Subquery instance when expecting a string.'
2358  );
2359  }
2360 
2361  # Skip the entire process when we have a string quoted on both ends.
2362  # Note that we check the end so that we will still quote any use of
2363  # use of `database`.table. But won't break things if someone wants
2364  # to query a database table with a dot in the name.
2365  if ( $this->isQuotedIdentifier( $name ) ) {
2366  return $name;
2367  }
2368 
2369  # Lets test for any bits of text that should never show up in a table
2370  # name. Basically anything like JOIN or ON which are actually part of
2371  # SQL queries, but may end up inside of the table value to combine
2372  # sql. Such as how the API is doing.
2373  # Note that we use a whitespace test rather than a \b test to avoid
2374  # any remote case where a word like on may be inside of a table name
2375  # surrounded by symbols which may be considered word breaks.
2376  if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
2377  $this->queryLogger->warning(
2378  __METHOD__ . ": use of subqueries is not supported this way.",
2379  [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
2380  );
2381 
2382  return $name;
2383  }
2384 
2385  # Split database and table into proper variables.
2386  list( $database, $schema, $prefix, $table ) = $this->qualifiedTableComponents( $name );
2387 
2388  # Quote $table and apply the prefix if not quoted.
2389  # $tableName might be empty if this is called from Database::replaceVars()
2390  $tableName = "{$prefix}{$table}";
2391  if ( $format === 'quoted'
2392  && !$this->isQuotedIdentifier( $tableName )
2393  && $tableName !== ''
2394  ) {
2395  $tableName = $this->addIdentifierQuotes( $tableName );
2396  }
2397 
2398  # Quote $schema and $database and merge them with the table name if needed
2399  $tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
2400  $tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
2401 
2402  return $tableName;
2403  }
2404 
2411  protected function qualifiedTableComponents( $name ) {
2412  # We reverse the explode so that database.table and table both output the correct table.
2413  $dbDetails = explode( '.', $name, 3 );
2414  if ( count( $dbDetails ) == 3 ) {
2415  list( $database, $schema, $table ) = $dbDetails;
2416  # We don't want any prefix added in this case
2417  $prefix = '';
2418  } elseif ( count( $dbDetails ) == 2 ) {
2419  list( $database, $table ) = $dbDetails;
2420  # We don't want any prefix added in this case
2421  $prefix = '';
2422  # In dbs that support it, $database may actually be the schema
2423  # but that doesn't affect any of the functionality here
2424  $schema = '';
2425  } else {
2426  list( $table ) = $dbDetails;
2427  if ( isset( $this->tableAliases[$table] ) ) {
2428  $database = $this->tableAliases[$table]['dbname'];
2429  $schema = is_string( $this->tableAliases[$table]['schema'] )
2430  ? $this->tableAliases[$table]['schema']
2431  : $this->relationSchemaQualifier();
2432  $prefix = is_string( $this->tableAliases[$table]['prefix'] )
2433  ? $this->tableAliases[$table]['prefix']
2434  : $this->tablePrefix();
2435  } else {
2436  $database = '';
2437  $schema = $this->relationSchemaQualifier(); # Default schema
2438  $prefix = $this->tablePrefix(); # Default prefix
2439  }
2440  }
2441 
2442  return [ $database, $schema, $prefix, $table ];
2443  }
2444 
2451  private function prependDatabaseOrSchema( $namespace, $relation, $format ) {
2452  if ( strlen( $namespace ) ) {
2453  if ( $format === 'quoted' && !$this->isQuotedIdentifier( $namespace ) ) {
2454  $namespace = $this->addIdentifierQuotes( $namespace );
2455  }
2456  $relation = $namespace . '.' . $relation;
2457  }
2458 
2459  return $relation;
2460  }
2461 
2462  public function tableNames() {
2463  $inArray = func_get_args();
2464  $retVal = [];
2465 
2466  foreach ( $inArray as $name ) {
2467  $retVal[$name] = $this->tableName( $name );
2468  }
2469 
2470  return $retVal;
2471  }
2472 
2473  public function tableNamesN() {
2474  $inArray = func_get_args();
2475  $retVal = [];
2476 
2477  foreach ( $inArray as $name ) {
2478  $retVal[] = $this->tableName( $name );
2479  }
2480 
2481  return $retVal;
2482  }
2483 
2495  protected function tableNameWithAlias( $table, $alias = false ) {
2496  if ( is_string( $table ) ) {
2497  $quotedTable = $this->tableName( $table );
2498  } elseif ( $table instanceof Subquery ) {
2499  $quotedTable = (string)$table;
2500  } else {
2501  throw new InvalidArgumentException( "Table must be a string or Subquery." );
2502  }
2503 
2504  if ( $alias === false || $alias === $table ) {
2505  if ( $table instanceof Subquery ) {
2506  throw new InvalidArgumentException( "Subquery table missing alias." );
2507  }
2508 
2509  return $quotedTable;
2510  } else {
2511  return $quotedTable . ' ' . $this->addIdentifierQuotes( $alias );
2512  }
2513  }
2514 
2521  protected function tableNamesWithAlias( $tables ) {
2522  $retval = [];
2523  foreach ( $tables as $alias => $table ) {
2524  if ( is_numeric( $alias ) ) {
2525  $alias = $table;
2526  }
2527  $retval[] = $this->tableNameWithAlias( $table, $alias );
2528  }
2529 
2530  return $retval;
2531  }
2532 
2541  protected function fieldNameWithAlias( $name, $alias = false ) {
2542  if ( !$alias || (string)$alias === (string)$name ) {
2543  return $name;
2544  } else {
2545  return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
2546  }
2547  }
2548 
2555  protected function fieldNamesWithAlias( $fields ) {
2556  $retval = [];
2557  foreach ( $fields as $alias => $field ) {
2558  if ( is_numeric( $alias ) ) {
2559  $alias = $field;
2560  }
2561  $retval[] = $this->fieldNameWithAlias( $field, $alias );
2562  }
2563 
2564  return $retval;
2565  }
2566 
2578  $tables, $use_index = [], $ignore_index = [], $join_conds = []
2579  ) {
2580  $ret = [];
2581  $retJOIN = [];
2582  $use_index = (array)$use_index;
2583  $ignore_index = (array)$ignore_index;
2584  $join_conds = (array)$join_conds;
2585 
2586  foreach ( $tables as $alias => $table ) {
2587  if ( !is_string( $alias ) ) {
2588  // No alias? Set it equal to the table name
2589  $alias = $table;
2590  }
2591 
2592  if ( is_array( $table ) ) {
2593  // A parenthesized group
2594  if ( count( $table ) > 1 ) {
2595  $joinedTable = '(' .
2597  $table, $use_index, $ignore_index, $join_conds ) . ')';
2598  } else {
2599  // Degenerate case
2600  $innerTable = reset( $table );
2601  $innerAlias = key( $table );
2602  $joinedTable = $this->tableNameWithAlias(
2603  $innerTable,
2604  is_string( $innerAlias ) ? $innerAlias : $innerTable
2605  );
2606  }
2607  } else {
2608  $joinedTable = $this->tableNameWithAlias( $table, $alias );
2609  }
2610 
2611  // Is there a JOIN clause for this table?
2612  if ( isset( $join_conds[$alias] ) ) {
2613  list( $joinType, $conds ) = $join_conds[$alias];
2614  $tableClause = $joinType;
2615  $tableClause .= ' ' . $joinedTable;
2616  if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
2617  $use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
2618  if ( $use != '' ) {
2619  $tableClause .= ' ' . $use;
2620  }
2621  }
2622  if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
2623  $ignore = $this->ignoreIndexClause(
2624  implode( ',', (array)$ignore_index[$alias] ) );
2625  if ( $ignore != '' ) {
2626  $tableClause .= ' ' . $ignore;
2627  }
2628  }
2629  $on = $this->makeList( (array)$conds, self::LIST_AND );
2630  if ( $on != '' ) {
2631  $tableClause .= ' ON (' . $on . ')';
2632  }
2633 
2634  $retJOIN[] = $tableClause;
2635  } elseif ( isset( $use_index[$alias] ) ) {
2636  // Is there an INDEX clause for this table?
2637  $tableClause = $joinedTable;
2638  $tableClause .= ' ' . $this->useIndexClause(
2639  implode( ',', (array)$use_index[$alias] )
2640  );
2641 
2642  $ret[] = $tableClause;
2643  } elseif ( isset( $ignore_index[$alias] ) ) {
2644  // Is there an INDEX clause for this table?
2645  $tableClause = $joinedTable;
2646  $tableClause .= ' ' . $this->ignoreIndexClause(
2647  implode( ',', (array)$ignore_index[$alias] )
2648  );
2649 
2650  $ret[] = $tableClause;
2651  } else {
2652  $tableClause = $joinedTable;
2653 
2654  $ret[] = $tableClause;
2655  }
2656  }
2657 
2658  // We can't separate explicit JOIN clauses with ',', use ' ' for those
2659  $implicitJoins = implode( ',', $ret );
2660  $explicitJoins = implode( ' ', $retJOIN );
2661 
2662  // Compile our final table clause
2663  return implode( ' ', [ $implicitJoins, $explicitJoins ] );
2664  }
2665 
2672  protected function indexName( $index ) {
2673  return $this->indexAliases[$index] ?? $index;
2674  }
2675 
2676  public function addQuotes( $s ) {
2677  if ( $s instanceof Blob ) {
2678  $s = $s->fetch();
2679  }
2680  if ( $s === null ) {
2681  return 'NULL';
2682  } elseif ( is_bool( $s ) ) {
2683  return (int)$s;
2684  } else {
2685  # This will also quote numeric values. This should be harmless,
2686  # and protects against weird problems that occur when they really
2687  # _are_ strings such as article titles and string->number->string
2688  # conversion is not 1:1.
2689  return "'" . $this->strencode( $s ) . "'";
2690  }
2691  }
2692 
2693  public function addIdentifierQuotes( $s ) {
2694  return '"' . str_replace( '"', '""', $s ) . '"';
2695  }
2696 
2706  public function isQuotedIdentifier( $name ) {
2707  return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
2708  }
2709 
2715  protected function escapeLikeInternal( $s, $escapeChar = '`' ) {
2716  return str_replace( [ $escapeChar, '%', '_' ],
2717  [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ],
2718  $s );
2719  }
2720 
2721  public function buildLike( $param, ...$params ) {
2722  if ( is_array( $param ) ) {
2723  $params = $param;
2724  } else {
2725  $params = func_get_args();
2726  }
2727 
2728  $s = '';
2729 
2730  // We use ` instead of \ as the default LIKE escape character, since addQuotes()
2731  // may escape backslashes, creating problems of double escaping. The `
2732  // character has good cross-DBMS compatibility, avoiding special operators
2733  // in MS SQL like ^ and %
2734  $escapeChar = '`';
2735 
2736  foreach ( $params as $value ) {
2737  if ( $value instanceof LikeMatch ) {
2738  $s .= $value->toString();
2739  } else {
2740  $s .= $this->escapeLikeInternal( $value, $escapeChar );
2741  }
2742  }
2743 
2744  return ' LIKE ' .
2745  $this->addQuotes( $s ) . ' ESCAPE ' . $this->addQuotes( $escapeChar ) . ' ';
2746  }
2747 
2748  public function anyChar() {
2749  return new LikeMatch( '_' );
2750  }
2751 
2752  public function anyString() {
2753  return new LikeMatch( '%' );
2754  }
2755 
2756  public function nextSequenceValue( $seqName ) {
2757  return null;
2758  }
2759 
2770  public function useIndexClause( $index ) {
2771  return '';
2772  }
2773 
2784  public function ignoreIndexClause( $index ) {
2785  return '';
2786  }
2787 
2788  public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2789  if ( count( $rows ) == 0 ) {
2790  return;
2791  }
2792 
2793  $uniqueIndexes = (array)$uniqueIndexes;
2794  // Single row case
2795  if ( !is_array( reset( $rows ) ) ) {
2796  $rows = [ $rows ];
2797  }
2798 
2799  try {
2800  $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
2801  $affectedRowCount = 0;
2802  foreach ( $rows as $row ) {
2803  // Delete rows which collide with this one
2804  $indexWhereClauses = [];
2805  foreach ( $uniqueIndexes as $index ) {
2806  $indexColumns = (array)$index;
2807  $indexRowValues = array_intersect_key( $row, array_flip( $indexColumns ) );
2808  if ( count( $indexRowValues ) != count( $indexColumns ) ) {
2809  throw new DBUnexpectedError(
2810  $this,
2811  'New record does not provide all values for unique key (' .
2812  implode( ', ', $indexColumns ) . ')'
2813  );
2814  } elseif ( in_array( null, $indexRowValues, true ) ) {
2815  throw new DBUnexpectedError(
2816  $this,
2817  'New record has a null value for unique key (' .
2818  implode( ', ', $indexColumns ) . ')'
2819  );
2820  }
2821  $indexWhereClauses[] = $this->makeList( $indexRowValues, LIST_AND );
2822  }
2823 
2824  if ( $indexWhereClauses ) {
2825  $this->delete( $table, $this->makeList( $indexWhereClauses, LIST_OR ), $fname );
2826  $affectedRowCount += $this->affectedRows();
2827  }
2828 
2829  // Now insert the row
2830  $this->insert( $table, $row, $fname );
2831  $affectedRowCount += $this->affectedRows();
2832  }
2833  $this->endAtomic( $fname );
2834  $this->affectedRowCount = $affectedRowCount;
2835  } catch ( Exception $e ) {
2836  $this->cancelAtomic( $fname );
2837  throw $e;
2838  }
2839  }
2840 
2849  protected function nativeReplace( $table, $rows, $fname ) {
2850  $table = $this->tableName( $table );
2851 
2852  # Single row case
2853  if ( !is_array( reset( $rows ) ) ) {
2854  $rows = [ $rows ];
2855  }
2856 
2857  $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
2858  $first = true;
2859 
2860  foreach ( $rows as $row ) {
2861  if ( $first ) {
2862  $first = false;
2863  } else {
2864  $sql .= ',';
2865  }
2866 
2867  $sql .= '(' . $this->makeList( $row ) . ')';
2868  }
2869 
2870  $this->query( $sql, $fname );
2871  }
2872 
2873  public function upsert( $table, array $rows, $uniqueIndexes, array $set,
2874  $fname = __METHOD__
2875  ) {
2876  if ( $rows === [] ) {
2877  return true; // nothing to do
2878  }
2879 
2880  $uniqueIndexes = (array)$uniqueIndexes;
2881  if ( !is_array( reset( $rows ) ) ) {
2882  $rows = [ $rows ];
2883  }
2884 
2885  if ( count( $uniqueIndexes ) ) {
2886  $clauses = []; // list WHERE clauses that each identify a single row
2887  foreach ( $rows as $row ) {
2888  foreach ( $uniqueIndexes as $index ) {
2889  $index = is_array( $index ) ? $index : [ $index ]; // columns
2890  $rowKey = []; // unique key to this row
2891  foreach ( $index as $column ) {
2892  $rowKey[$column] = $row[$column];
2893  }
2894  $clauses[] = $this->makeList( $rowKey, self::LIST_AND );
2895  }
2896  }
2897  $where = [ $this->makeList( $clauses, self::LIST_OR ) ];
2898  } else {
2899  $where = false;
2900  }
2901 
2902  $affectedRowCount = 0;
2903  try {
2904  $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
2905  # Update any existing conflicting row(s)
2906  if ( $where !== false ) {
2907  $this->update( $table, $set, $where, $fname );
2908  $affectedRowCount += $this->affectedRows();
2909  }
2910  # Now insert any non-conflicting row(s)
2911  $this->insert( $table, $rows, $fname, [ 'IGNORE' ] );
2912  $affectedRowCount += $this->affectedRows();
2913  $this->endAtomic( $fname );
2914  $this->affectedRowCount = $affectedRowCount;
2915  } catch ( Exception $e ) {
2916  $this->cancelAtomic( $fname );
2917  throw $e;
2918  }
2919 
2920  return true;
2921  }
2922 
2923  public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2924  $fname = __METHOD__
2925  ) {
2926  if ( !$conds ) {
2927  throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
2928  }
2929 
2930  $delTable = $this->tableName( $delTable );
2931  $joinTable = $this->tableName( $joinTable );
2932  $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2933  if ( $conds != '*' ) {
2934  $sql .= 'WHERE ' . $this->makeList( $conds, self::LIST_AND );
2935  }
2936  $sql .= ')';
2937 
2938  $this->query( $sql, $fname );
2939  }
2940 
2941  public function textFieldSize( $table, $field ) {
2942  $table = $this->tableName( $table );
2943  $sql = "SHOW COLUMNS FROM $table LIKE \"$field\"";
2944  $res = $this->query( $sql, __METHOD__ );
2945  $row = $this->fetchObject( $res );
2946 
2947  $m = [];
2948 
2949  if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
2950  $size = $m[1];
2951  } else {
2952  $size = -1;
2953  }
2954 
2955  return $size;
2956  }
2957 
2958  public function delete( $table, $conds, $fname = __METHOD__ ) {
2959  if ( !$conds ) {
2960  throw new DBUnexpectedError( $this, __METHOD__ . ' called with no conditions' );
2961  }
2962 
2963  $table = $this->tableName( $table );
2964  $sql = "DELETE FROM $table";
2965 
2966  if ( $conds != '*' ) {
2967  if ( is_array( $conds ) ) {
2968  $conds = $this->makeList( $conds, self::LIST_AND );
2969  }
2970  $sql .= ' WHERE ' . $conds;
2971  }
2972 
2973  $this->query( $sql, $fname );
2974 
2975  return true;
2976  }
2977 
2978  final public function insertSelect(
2979  $destTable, $srcTable, $varMap, $conds,
2980  $fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = []
2981  ) {
2982  static $hints = [ 'NO_AUTO_COLUMNS' ];
2983 
2984  $insertOptions = (array)$insertOptions;
2985  $selectOptions = (array)$selectOptions;
2986 
2987  if ( $this->cliMode && $this->isInsertSelectSafe( $insertOptions, $selectOptions ) ) {
2988  // For massive migrations with downtime, we don't want to select everything
2989  // into memory and OOM, so do all this native on the server side if possible.
2990  $this->nativeInsertSelect(
2991  $destTable,
2992  $srcTable,
2993  $varMap,
2994  $conds,
2995  $fname,
2996  array_diff( $insertOptions, $hints ),
2997  $selectOptions,
2998  $selectJoinConds
2999  );
3000  } else {
3001  $this->nonNativeInsertSelect(
3002  $destTable,
3003  $srcTable,
3004  $varMap,
3005  $conds,
3006  $fname,
3007  array_diff( $insertOptions, $hints ),
3008  $selectOptions,
3009  $selectJoinConds
3010  );
3011  }
3012 
3013  return true;
3014  }
3015 
3022  protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
3023  return true;
3024  }
3025 
3040  protected function nonNativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
3041  $fname = __METHOD__,
3042  $insertOptions = [], $selectOptions = [], $selectJoinConds = []
3043  ) {
3044  // For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
3045  // on only the master (without needing row-based-replication). It also makes it easy to
3046  // know how big the INSERT is going to be.
3047  $fields = [];
3048  foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
3049  $fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
3050  }
3051  $selectOptions[] = 'FOR UPDATE';
3052  $res = $this->select(
3053  $srcTable, implode( ',', $fields ), $conds, $fname, $selectOptions, $selectJoinConds
3054  );
3055  if ( !$res ) {
3056  return;
3057  }
3058 
3059  try {
3060  $affectedRowCount = 0;
3061  $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
3062  $rows = [];
3063  $ok = true;
3064  foreach ( $res as $row ) {
3065  $rows[] = (array)$row;
3066 
3067  // Avoid inserts that are too huge
3068  if ( count( $rows ) >= $this->nonNativeInsertSelectBatchSize ) {
3069  $ok = $this->insert( $destTable, $rows, $fname, $insertOptions );
3070  if ( !$ok ) {
3071  break;
3072  }
3073  $affectedRowCount += $this->affectedRows();
3074  $rows = [];
3075  }
3076  }
3077  if ( $rows && $ok ) {
3078  $ok = $this->insert( $destTable, $rows, $fname, $insertOptions );
3079  if ( $ok ) {
3080  $affectedRowCount += $this->affectedRows();
3081  }
3082  }
3083  if ( $ok ) {
3084  $this->endAtomic( $fname );
3085  $this->affectedRowCount = $affectedRowCount;
3086  } else {
3087  $this->cancelAtomic( $fname );
3088  }
3089  } catch ( Exception $e ) {
3090  $this->cancelAtomic( $fname );
3091  throw $e;
3092  }
3093  }
3094 
3109  protected function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
3110  $fname = __METHOD__,
3111  $insertOptions = [], $selectOptions = [], $selectJoinConds = []
3112  ) {
3113  $destTable = $this->tableName( $destTable );
3114 
3115  if ( !is_array( $insertOptions ) ) {
3116  $insertOptions = [ $insertOptions ];
3117  }
3118 
3119  $insertOptions = $this->makeInsertOptions( $insertOptions );
3120 
3121  $selectSql = $this->selectSQLText(
3122  $srcTable,
3123  array_values( $varMap ),
3124  $conds,
3125  $fname,
3126  $selectOptions,
3127  $selectJoinConds
3128  );
3129 
3130  $sql = "INSERT $insertOptions" .
3131  " INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ') ' .
3132  $selectSql;
3133 
3134  $this->query( $sql, $fname );
3135  }
3136 
3137  public function limitResult( $sql, $limit, $offset = false ) {
3138  if ( !is_numeric( $limit ) ) {
3139  throw new DBUnexpectedError( $this,
3140  "Invalid non-numeric limit passed to limitResult()\n" );
3141  }
3142  // This version works in MySQL and SQLite. It will very likely need to be
3143  // overridden for most other RDBMS subclasses.
3144  return "$sql LIMIT "
3145  . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
3146  . "{$limit} ";
3147  }
3148 
3149  public function unionSupportsOrderAndLimit() {
3150  return true; // True for almost every DB supported
3151  }
3152 
3153  public function unionQueries( $sqls, $all ) {
3154  $glue = $all ? ') UNION ALL (' : ') UNION (';
3155 
3156  return '(' . implode( $glue, $sqls ) . ')';
3157  }
3158 
3160  $table, $vars, array $permute_conds, $extra_conds = '', $fname = __METHOD__,
3161  $options = [], $join_conds = []
3162  ) {
3163  // First, build the Cartesian product of $permute_conds
3164  $conds = [ [] ];
3165  foreach ( $permute_conds as $field => $values ) {
3166  if ( !$values ) {
3167  // Skip empty $values
3168  continue;
3169  }
3170  $values = array_unique( $values ); // For sanity
3171  $newConds = [];
3172  foreach ( $conds as $cond ) {
3173  foreach ( $values as $value ) {
3174  $cond[$field] = $value;
3175  $newConds[] = $cond; // Arrays are by-value, not by-reference, so this works
3176  }
3177  }
3178  $conds = $newConds;
3179  }
3180 
3181  $extra_conds = $extra_conds === '' ? [] : (array)$extra_conds;
3182 
3183  // If there's just one condition and no subordering, hand off to
3184  // selectSQLText directly.
3185  if ( count( $conds ) === 1 &&
3186  ( !isset( $options['INNER ORDER BY'] ) || !$this->unionSupportsOrderAndLimit() )
3187  ) {
3188  return $this->selectSQLText(
3189  $table, $vars, $conds[0] + $extra_conds, $fname, $options, $join_conds
3190  );
3191  }
3192 
3193  // Otherwise, we need to pull out the order and limit to apply after
3194  // the union. Then build the SQL queries for each set of conditions in
3195  // $conds. Then union them together (using UNION ALL, because the
3196  // product *should* already be distinct).
3197  $orderBy = $this->makeOrderBy( $options );
3198  $limit = $options['LIMIT'] ?? null;
3199  $offset = $options['OFFSET'] ?? false;
3200  $all = empty( $options['NOTALL'] ) && !in_array( 'NOTALL', $options );
3201  if ( !$this->unionSupportsOrderAndLimit() ) {
3202  unset( $options['ORDER BY'], $options['LIMIT'], $options['OFFSET'] );
3203  } else {
3204  if ( array_key_exists( 'INNER ORDER BY', $options ) ) {
3205  $options['ORDER BY'] = $options['INNER ORDER BY'];
3206  }
3207  if ( $limit !== null && is_numeric( $offset ) && $offset != 0 ) {
3208  // We need to increase the limit by the offset rather than
3209  // using the offset directly, otherwise it'll skip incorrectly
3210  // in the subqueries.
3211  $options['LIMIT'] = $limit + $offset;
3212  unset( $options['OFFSET'] );
3213  }
3214  }
3215 
3216  $sqls = [];
3217  foreach ( $conds as $cond ) {
3218  $sqls[] = $this->selectSQLText(
3219  $table, $vars, $cond + $extra_conds, $fname, $options, $join_conds
3220  );
3221  }
3222  $sql = $this->unionQueries( $sqls, $all ) . $orderBy;
3223  if ( $limit !== null ) {
3224  $sql = $this->limitResult( $sql, $limit, $offset );
3225  }
3226 
3227  return $sql;
3228  }
3229 
3230  public function conditional( $cond, $trueVal, $falseVal ) {
3231  if ( is_array( $cond ) ) {
3232  $cond = $this->makeList( $cond, self::LIST_AND );
3233  }
3234 
3235  return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
3236  }
3237 
3238  public function strreplace( $orig, $old, $new ) {
3239  return "REPLACE({$orig}, {$old}, {$new})";
3240  }
3241 
3242  public function getServerUptime() {
3243  return 0;
3244  }
3245 
3246  public function wasDeadlock() {
3247  return false;
3248  }
3249 
3250  public function wasLockTimeout() {
3251  return false;
3252  }
3253 
3254  public function wasConnectionLoss() {
3255  return $this->wasConnectionError( $this->lastErrno() );
3256  }
3257 
3258  public function wasReadOnlyError() {
3259  return false;
3260  }
3261 
3262  public function wasErrorReissuable() {
3263  return (
3264  $this->wasDeadlock() ||
3265  $this->wasLockTimeout() ||
3266  $this->wasConnectionLoss()
3267  );
3268  }
3269 
3276  public function wasConnectionError( $errno ) {
3277  return false;
3278  }
3279 
3286  protected function wasKnownStatementRollbackError() {
3287  return false; // don't know; it could have caused a transaction rollback
3288  }
3289 
3290  public function deadlockLoop() {
3291  $args = func_get_args();
3292  $function = array_shift( $args );
3293  $tries = self::$DEADLOCK_TRIES;
3294 
3295  $this->begin( __METHOD__ );
3296 
3297  $retVal = null;
3299  $e = null;
3300  do {
3301  try {
3302  $retVal = $function( ...$args );
3303  break;
3304  } catch ( DBQueryError $e ) {
3305  if ( $this->wasDeadlock() ) {
3306  // Retry after a randomized delay
3307  usleep( mt_rand( self::$DEADLOCK_DELAY_MIN, self::$DEADLOCK_DELAY_MAX ) );
3308  } else {
3309  // Throw the error back up
3310  throw $e;
3311  }
3312  }
3313  } while ( --$tries > 0 );
3314 
3315  if ( $tries <= 0 ) {
3316  // Too many deadlocks; give up
3317  $this->rollback( __METHOD__ );
3318  throw $e;
3319  } else {
3320  $this->commit( __METHOD__ );
3321 
3322  return $retVal;
3323  }
3324  }
3325 
3326  public function masterPosWait( DBMasterPos $pos, $timeout ) {
3327  # Real waits are implemented in the subclass.
3328  return 0;
3329  }
3330 
3331  public function getReplicaPos() {
3332  # Stub
3333  return false;
3334  }
3335 
3336  public function getMasterPos() {
3337  # Stub
3338  return false;
3339  }
3340 
3341  public function serverIsReadOnly() {
3342  return false;
3343  }
3344 
3345  final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
3346  if ( !$this->trxLevel ) {
3347  throw new DBUnexpectedError( $this, "No transaction is active." );
3348  }
3349  $this->trxEndCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3350  }
3351 
3352  final public function onTransactionCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
3353  if ( !$this->trxLevel && $this->getTransactionRoundId() ) {
3354  // Start an implicit transaction similar to how query() does
3355  $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
3356  $this->trxAutomatic = true;
3357  }
3358 
3359  $this->trxIdleCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3360  if ( !$this->trxLevel ) {
3361  $this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
3362  }
3363  }
3364 
3365  final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
3366  $this->onTransactionCommitOrIdle( $callback, $fname );
3367  }
3368 
3369  final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
3370  if ( !$this->trxLevel && $this->getTransactionRoundId() ) {
3371  // Start an implicit transaction similar to how query() does
3372  $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
3373  $this->trxAutomatic = true;
3374  }
3375 
3376  if ( $this->trxLevel ) {
3377  $this->trxPreCommitCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3378  } else {
3379  // No transaction is active nor will start implicitly, so make one for this callback
3380  $this->startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
3381  try {
3382  $callback( $this );
3383  $this->endAtomic( __METHOD__ );
3384  } catch ( Exception $e ) {
3385  $this->cancelAtomic( __METHOD__ );
3386  throw $e;
3387  }
3388  }
3389  }
3390 
3394  private function currentAtomicSectionId() {
3395  if ( $this->trxLevel && $this->trxAtomicLevels ) {
3396  $levelInfo = end( $this->trxAtomicLevels );
3397 
3398  return $levelInfo[1];
3399  }
3400 
3401  return null;
3402  }
3403 
3410  ) {
3411  foreach ( $this->trxPreCommitCallbacks as $key => $info ) {
3412  if ( $info[2] === $old ) {
3413  $this->trxPreCommitCallbacks[$key][2] = $new;
3414  }
3415  }
3416  foreach ( $this->trxIdleCallbacks as $key => $info ) {
3417  if ( $info[2] === $old ) {
3418  $this->trxIdleCallbacks[$key][2] = $new;
3419  }
3420  }
3421  foreach ( $this->trxEndCallbacks as $key => $info ) {
3422  if ( $info[2] === $old ) {
3423  $this->trxEndCallbacks[$key][2] = $new;
3424  }
3425  }
3426  }
3427 
3432  private function modifyCallbacksForCancel( array $sectionIds ) {
3433  // Cancel the "on commit" callbacks owned by this savepoint
3434  $this->trxIdleCallbacks = array_filter(
3435  $this->trxIdleCallbacks,
3436  function ( $entry ) use ( $sectionIds ) {
3437  return !in_array( $entry[2], $sectionIds, true );
3438  }
3439  );
3440  $this->trxPreCommitCallbacks = array_filter(
3441  $this->trxPreCommitCallbacks,
3442  function ( $entry ) use ( $sectionIds ) {
3443  return !in_array( $entry[2], $sectionIds, true );
3444  }
3445  );
3446  // Make "on resolution" callbacks owned by this savepoint to perceive a rollback
3447  foreach ( $this->trxEndCallbacks as $key => $entry ) {
3448  if ( in_array( $entry[2], $sectionIds, true ) ) {
3449  $callback = $entry[0];
3450  $this->trxEndCallbacks[$key][0] = function () use ( $callback ) {
3451  return $callback( self::TRIGGER_ROLLBACK, $this );
3452  };
3453  }
3454  }
3455  }
3456 
3457  final public function setTransactionListener( $name, callable $callback = null ) {
3458  if ( $callback ) {
3459  $this->trxRecurringCallbacks[$name] = $callback;
3460  } else {
3461  unset( $this->trxRecurringCallbacks[$name] );
3462  }
3463  }
3464 
3473  final public function setTrxEndCallbackSuppression( $suppress ) {
3474  $this->trxEndCallbacksSuppressed = $suppress;
3475  }
3476 
3487  public function runOnTransactionIdleCallbacks( $trigger ) {
3488  if ( $this->trxLevel ) { // sanity
3489  throw new DBUnexpectedError( $this, __METHOD__ . ': a transaction is still open.' );
3490  }
3491 
3492  if ( $this->trxEndCallbacksSuppressed ) {
3493  return 0;
3494  }
3495 
3496  $count = 0;
3497  $autoTrx = $this->getFlag( self::DBO_TRX ); // automatic begin() enabled?
3499  $e = null; // first exception
3500  do { // callbacks may add callbacks :)
3501  $callbacks = array_merge(
3502  $this->trxIdleCallbacks,
3503  $this->trxEndCallbacks // include "transaction resolution" callbacks
3504  );
3505  $this->trxIdleCallbacks = []; // consumed (and recursion guard)
3506  $this->trxEndCallbacks = []; // consumed (recursion guard)
3507  foreach ( $callbacks as $callback ) {
3508  ++$count;
3509  list( $phpCallback ) = $callback;
3510  $this->clearFlag( self::DBO_TRX ); // make each query its own transaction
3511  try {
3512  // @phan-suppress-next-line PhanParamTooManyCallable
3513  call_user_func( $phpCallback, $trigger, $this );
3514  } catch ( Exception $ex ) {
3515  call_user_func( $this->errorLogger, $ex );
3516  $e = $e ?: $ex;
3517  // Some callbacks may use startAtomic/endAtomic, so make sure
3518  // their transactions are ended so other callbacks don't fail
3519  if ( $this->trxLevel() ) {
3520  $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
3521  }
3522  } finally {
3523  if ( $autoTrx ) {
3524  $this->setFlag( self::DBO_TRX ); // restore automatic begin()
3525  } else {
3526  $this->clearFlag( self::DBO_TRX ); // restore auto-commit
3527  }
3528  }
3529  }
3530  } while ( count( $this->trxIdleCallbacks ) );
3531 
3532  if ( $e instanceof Exception ) {
3533  throw $e; // re-throw any first exception
3534  }
3535 
3536  return $count;
3537  }
3538 
3549  $count = 0;
3550 
3551  $e = null; // first exception
3552  do { // callbacks may add callbacks :)
3553  $callbacks = $this->trxPreCommitCallbacks;
3554  $this->trxPreCommitCallbacks = []; // consumed (and recursion guard)
3555  foreach ( $callbacks as $callback ) {
3556  try {
3557  ++$count;
3558  list( $phpCallback ) = $callback;
3559  $phpCallback( $this );
3560  } catch ( Exception $ex ) {
3561  ( $this->errorLogger )( $ex );
3562  $e = $e ?: $ex;
3563  }
3564  }
3565  } while ( count( $this->trxPreCommitCallbacks ) );
3566 
3567  if ( $e instanceof Exception ) {
3568  throw $e; // re-throw any first exception
3569  }
3570 
3571  return $count;
3572  }
3573 
3583  public function runTransactionListenerCallbacks( $trigger ) {
3584  if ( $this->trxEndCallbacksSuppressed ) {
3585  return;
3586  }
3587 
3589  $e = null; // first exception
3590 
3591  foreach ( $this->trxRecurringCallbacks as $phpCallback ) {
3592  try {
3593  $phpCallback( $trigger, $this );
3594  } catch ( Exception $ex ) {
3595  ( $this->errorLogger )( $ex );
3596  $e = $e ?: $ex;
3597  }
3598  }
3599 
3600  if ( $e instanceof Exception ) {
3601  throw $e; // re-throw any first exception
3602  }
3603  }
3604 
3615  protected function doSavepoint( $identifier, $fname ) {
3616  $this->query( 'SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
3617  }
3618 
3629  protected function doReleaseSavepoint( $identifier, $fname ) {
3630  $this->query( 'RELEASE SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
3631  }
3632 
3643  protected function doRollbackToSavepoint( $identifier, $fname ) {
3644  $this->query( 'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
3645  }
3646 
3651  private function nextSavepointId( $fname ) {
3652  $savepointId = self::$SAVEPOINT_PREFIX . ++$this->trxAtomicCounter;
3653  if ( strlen( $savepointId ) > 30 ) {
3654  // 30 == Oracle's identifier length limit (pre 12c)
3655  // With a 22 character prefix, that puts the highest number at 99999999.
3656  throw new DBUnexpectedError(
3657  $this,
3658  'There have been an excessively large number of atomic sections in a transaction'
3659  . " started by $this->trxFname (at $fname)"
3660  );
3661  }
3662 
3663  return $savepointId;
3664  }
3665 
3666  final public function startAtomic(
3667  $fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE
3668  ) {
3669  $savepointId = $cancelable === self::ATOMIC_CANCELABLE ? self::$NOT_APPLICABLE : null;
3670 
3671  if ( !$this->trxLevel ) {
3672  $this->begin( $fname, self::TRANSACTION_INTERNAL ); // sets trxAutomatic
3673  // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
3674  // in all changes being in one transaction to keep requests transactional.
3675  if ( $this->getFlag( self::DBO_TRX ) ) {
3676  // Since writes could happen in between the topmost atomic sections as part
3677  // of the transaction, those sections will need savepoints.
3678  $savepointId = $this->nextSavepointId( $fname );
3679  $this->doSavepoint( $savepointId, $fname );
3680  } else {
3681  $this->trxAutomaticAtomic = true;
3682  }
3683  } elseif ( $cancelable === self::ATOMIC_CANCELABLE ) {
3684  $savepointId = $this->nextSavepointId( $fname );
3685  $this->doSavepoint( $savepointId, $fname );
3686  }
3687 
3688  $sectionId = new AtomicSectionIdentifier;
3689  $this->trxAtomicLevels[] = [ $fname, $sectionId, $savepointId ];
3690  $this->queryLogger->debug( 'startAtomic: entering level ' .
3691  ( count( $this->trxAtomicLevels ) - 1 ) . " ($fname)" );
3692 
3693  return $sectionId;
3694  }
3695 
3696  final public function endAtomic( $fname = __METHOD__ ) {
3697  if ( !$this->trxLevel || !$this->trxAtomicLevels ) {
3698  throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
3699  }
3700 
3701  // Check if the current section matches $fname
3702  $pos = count( $this->trxAtomicLevels ) - 1;
3703  list( $savedFname, $sectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
3704  $this->queryLogger->debug( "endAtomic: leaving level $pos ($fname)" );
3705 
3706  if ( $savedFname !== $fname ) {
3707  throw new DBUnexpectedError(
3708  $this,
3709  "Invalid atomic section ended (got $fname but expected $savedFname)."
3710  );
3711  }
3712 
3713  // Remove the last section (no need to re-index the array)
3714  array_pop( $this->trxAtomicLevels );
3715 
3716  if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
3717  $this->commit( $fname, self::FLUSHING_INTERNAL );
3718  } elseif ( $savepointId !== null && $savepointId !== self::$NOT_APPLICABLE ) {
3719  $this->doReleaseSavepoint( $savepointId, $fname );
3720  }
3721 
3722  // Hoist callback ownership for callbacks in the section that just ended;
3723  // all callbacks should have an owner that is present in trxAtomicLevels.
3724  $currentSectionId = $this->currentAtomicSectionId();
3725  if ( $currentSectionId ) {
3726  $this->reassignCallbacksForSection( $sectionId, $currentSectionId );
3727  }
3728  }
3729 
3730  final public function cancelAtomic(
3731  $fname = __METHOD__, AtomicSectionIdentifier $sectionId = null
3732  ) {
3733  if ( !$this->trxLevel || !$this->trxAtomicLevels ) {
3734  throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
3735  }
3736 
3737  $excisedFnames = [];
3738  if ( $sectionId !== null ) {
3739  // Find the (last) section with the given $sectionId
3740  $pos = -1;
3741  foreach ( $this->trxAtomicLevels as $i => list( $asFname, $asId, $spId ) ) {
3742  if ( $asId === $sectionId ) {
3743  $pos = $i;
3744  }
3745  }
3746  if ( $pos < 0 ) {
3747  throw new DBUnexpectedError( $this, "Atomic section not found (for $fname)" );
3748  }
3749  // Remove all descendant sections and re-index the array
3750  $excisedIds = [];
3751  $len = count( $this->trxAtomicLevels );
3752  for ( $i = $pos + 1; $i < $len; ++$i ) {
3753  $excisedFnames[] = $this->trxAtomicLevels[$i][0];
3754  $excisedIds[] = $this->trxAtomicLevels[$i][1];
3755  }
3756  $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 );
3757  $this->modifyCallbacksForCancel( $excisedIds );
3758  }
3759 
3760  // Check if the current section matches $fname
3761  $pos = count( $this->trxAtomicLevels ) - 1;
3762  list( $savedFname, $savedSectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
3763 
3764  if ( $excisedFnames ) {
3765  $this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname) " .
3766  "and descendants " . implode( ', ', $excisedFnames ) );
3767  } else {
3768  $this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname)" );
3769  }
3770 
3771  if ( $savedFname !== $fname ) {
3772  throw new DBUnexpectedError(
3773  $this,
3774  "Invalid atomic section ended (got $fname but expected $savedFname)."
3775  );
3776  }
3777 
3778  // Remove the last section (no need to re-index the array)
3779  array_pop( $this->trxAtomicLevels );
3780  $this->modifyCallbacksForCancel( [ $savedSectionId ] );
3781 
3782  if ( $savepointId !== null ) {
3783  // Rollback the transaction to the state just before this atomic section
3784  if ( $savepointId === self::$NOT_APPLICABLE ) {
3785  $this->rollback( $fname, self::FLUSHING_INTERNAL );
3786  } else {
3787  $this->doRollbackToSavepoint( $savepointId, $fname );
3788  $this->trxStatus = self::STATUS_TRX_OK; // no exception; recovered
3789  $this->trxStatusIgnoredCause = null;
3790  }
3791  } elseif ( $this->trxStatus > self::STATUS_TRX_ERROR ) {
3792  // Put the transaction into an error state if it's not already in one
3793  $this->trxStatus = self::STATUS_TRX_ERROR;
3794  $this->trxStatusCause = new DBUnexpectedError(
3795  $this,
3796  "Uncancelable atomic section canceled (got $fname)."
3797  );
3798  }
3799 
3800  $this->affectedRowCount = 0; // for the sake of consistency
3801  }
3802 
3803  final public function doAtomicSection(
3804  $fname, callable $callback, $cancelable = self::ATOMIC_NOT_CANCELABLE
3805  ) {
3806  $sectionId = $this->startAtomic( $fname, $cancelable );
3807  try {
3808  $res = $callback( $this, $fname );
3809  } catch ( Exception $e ) {
3810  $this->cancelAtomic( $fname, $sectionId );
3811 
3812  throw $e;
3813  }
3814  $this->endAtomic( $fname );
3815 
3816  return $res;
3817  }
3818 
3819  final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
3820  static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
3821  if ( !in_array( $mode, $modes, true ) ) {
3822  throw new DBUnexpectedError( $this, "$fname: invalid mode parameter '$mode'." );
3823  }
3824 
3825  // Protect against mismatched atomic section, transaction nesting, and snapshot loss
3826  if ( $this->trxLevel ) {
3827  if ( $this->trxAtomicLevels ) {
3828  $levels = $this->flatAtomicSectionList();
3829  $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
3830  throw new DBUnexpectedError( $this, $msg );
3831  } elseif ( !$this->trxAutomatic ) {
3832  $msg = "$fname: Explicit transaction already active (from {$this->trxFname}).";
3833  throw new DBUnexpectedError( $this, $msg );
3834  } else {
3835  $msg = "$fname: Implicit transaction already active (from {$this->trxFname}).";
3836  throw new DBUnexpectedError( $this, $msg );
3837  }
3838  } elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
3839  $msg = "$fname: Implicit transaction expected (DBO_TRX set).";
3840  throw new DBUnexpectedError( $this, $msg );
3841  }
3842 
3843  $this->assertHasConnectionHandle();
3844 
3845  $this->doBegin( $fname );
3846  $this->trxStatus = self::STATUS_TRX_OK;
3847  $this->trxStatusIgnoredCause = null;
3848  $this->trxAtomicCounter = 0;
3849  $this->trxTimestamp = microtime( true );
3850  $this->trxFname = $fname;
3851  $this->trxDoneWrites = false;
3852  $this->trxAutomaticAtomic = false;
3853  $this->trxAtomicLevels = [];
3854  $this->trxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
3855  $this->trxWriteDuration = 0.0;
3856  $this->trxWriteQueryCount = 0;
3857  $this->trxWriteAffectedRows = 0;
3858  $this->trxWriteAdjDuration = 0.0;
3859  $this->trxWriteAdjQueryCount = 0;
3860  $this->trxWriteCallers = [];
3861  // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
3862  // Get an estimate of the replication lag before any such queries.
3863  $this->trxReplicaLag = null; // clear cached value first
3864  $this->trxReplicaLag = $this->getApproximateLagStatus()['lag'];
3865  // T147697: make explicitTrxActive() return true until begin() finishes. This way, no
3866  // caller will think its OK to muck around with the transaction just because startAtomic()
3867  // has not yet completed (e.g. setting trxAtomicLevels).
3868  $this->trxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
3869  }
3870 
3877  protected function doBegin( $fname ) {
3878  $this->query( 'BEGIN', $fname );
3879  $this->trxLevel = 1;
3880  }
3881 
3882  final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
3883  static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
3884  if ( !in_array( $flush, $modes, true ) ) {
3885  throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'." );
3886  }
3887 
3888  if ( $this->trxLevel && $this->trxAtomicLevels ) {
3889  // There are still atomic sections open; this cannot be ignored
3890  $levels = $this->flatAtomicSectionList();
3891  throw new DBUnexpectedError(
3892  $this,
3893  "$fname: Got COMMIT while atomic sections $levels are still open."
3894  );
3895  }
3896 
3897  if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
3898  if ( !$this->trxLevel ) {
3899  return; // nothing to do
3900  } elseif ( !$this->trxAutomatic ) {
3901  throw new DBUnexpectedError(
3902  $this,
3903  "$fname: Flushing an explicit transaction, getting out of sync."
3904  );
3905  }
3906  } elseif ( !$this->trxLevel ) {
3907  $this->queryLogger->error(
3908  "$fname: No transaction to commit, something got out of sync." );
3909  return; // nothing to do
3910  } elseif ( $this->trxAutomatic ) {
3911  throw new DBUnexpectedError(
3912  $this,
3913  "$fname: Expected mass commit of all peer transactions (DBO_TRX set)."
3914  );
3915  }
3916 
3917  $this->assertHasConnectionHandle();
3918 
3920 
3921  $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
3922  $this->doCommit( $fname );
3923  $this->trxStatus = self::STATUS_TRX_NONE;
3924 
3925  if ( $this->trxDoneWrites ) {
3926  $this->lastWriteTime = microtime( true );
3927  $this->trxProfiler->transactionWritingOut(
3928  $this->server,
3929  $this->getDomainID(),
3930  $this->trxShortId,
3931  $writeTime,
3932  $this->trxWriteAffectedRows
3933  );
3934  }
3935 
3936  // With FLUSHING_ALL_PEERS, callbacks will be explicitly run later
3937  if ( $flush !== self::FLUSHING_ALL_PEERS ) {
3938  $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
3939  $this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
3940  }
3941  }
3942 
3949  protected function doCommit( $fname ) {
3950  if ( $this->trxLevel ) {
3951  $this->query( 'COMMIT', $fname );
3952  $this->trxLevel = 0;
3953  }
3954  }
3955 
3956  final public function rollback( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
3957  $trxActive = $this->trxLevel;
3958 
3959  if ( $flush !== self::FLUSHING_INTERNAL
3960  && $flush !== self::FLUSHING_ALL_PEERS
3961  && $this->getFlag( self::DBO_TRX )
3962  ) {
3963  throw new DBUnexpectedError(
3964  $this,
3965  "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)."
3966  );
3967  }
3968 
3969  if ( $trxActive ) {
3970  $this->assertHasConnectionHandle();
3971 
3972  $this->doRollback( $fname );
3973  $this->trxStatus = self::STATUS_TRX_NONE;
3974  $this->trxAtomicLevels = [];
3975  // Estimate the RTT via a query now that trxStatus is OK
3976  $writeTime = $this->pingAndCalculateLastTrxApplyTime();
3977 
3978  if ( $this->trxDoneWrites ) {
3979  $this->trxProfiler->transactionWritingOut(
3980  $this->server,
3981  $this->getDomainID(),
3982  $this->trxShortId,
3983  $writeTime,
3984  $this->trxWriteAffectedRows
3985  );
3986  }
3987  }
3988 
3989  // Clear any commit-dependant callbacks. They might even be present
3990  // only due to transaction rounds, with no SQL transaction being active
3991  $this->trxIdleCallbacks = [];
3992  $this->trxPreCommitCallbacks = [];
3993 
3994  // With FLUSHING_ALL_PEERS, callbacks will be explicitly run later
3995  if ( $trxActive && $flush !== self::FLUSHING_ALL_PEERS ) {
3996  try {
3997  $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
3998  } catch ( Exception $e ) {
3999  // already logged; finish and let LoadBalancer move on during mass-rollback
4000  }
4001  try {
4002  $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
4003  } catch ( Exception $e ) {
4004  // already logged; let LoadBalancer move on during mass-rollback
4005  }
4006 
4007  $this->affectedRowCount = 0; // for the sake of consistency
4008  }
4009  }
4010 
4017  protected function doRollback( $fname ) {
4018  if ( $this->trxLevel ) {
4019  # Disconnects cause rollback anyway, so ignore those errors
4020  $ignoreErrors = true;
4021  $this->query( 'ROLLBACK', $fname, $ignoreErrors );
4022  $this->trxLevel = 0;
4023  }
4024  }
4025 
4026  public function flushSnapshot( $fname = __METHOD__ ) {
4027  if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
4028  // This only flushes transactions to clear snapshots, not to write data
4029  $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
4030  throw new DBUnexpectedError(
4031  $this,
4032  "$fname: Cannot flush snapshot because writes are pending ($fnames)."
4033  );
4034  }
4035 
4036  $this->commit( $fname, self::FLUSHING_INTERNAL );
4037  }
4038 
4039  public function explicitTrxActive() {
4040  return $this->trxLevel && ( $this->trxAtomicLevels || !$this->trxAutomatic );
4041  }
4042 
4043  public function duplicateTableStructure(
4044  $oldName, $newName, $temporary = false, $fname = __METHOD__
4045  ) {
4046  throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
4047  }
4048 
4049  public function listTables( $prefix = null, $fname = __METHOD__ ) {
4050  throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
4051  }
4052 
4053  public function listViews( $prefix = null, $fname = __METHOD__ ) {
4054  throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
4055  }
4056 
4057  public function timestamp( $ts = 0 ) {
4058  $t = new ConvertibleTimestamp( $ts );
4059  // Let errors bubble up to avoid putting garbage in the DB
4060  return $t->getTimestamp( TS_MW );
4061  }
4062 
4063  public function timestampOrNull( $ts = null ) {
4064  if ( is_null( $ts ) ) {
4065  return null;
4066  } else {
4067  return $this->timestamp( $ts );
4068  }
4069  }
4070 
4071  public function affectedRows() {
4072  return ( $this->affectedRowCount === null )
4073  ? $this->fetchAffectedRowCount() // default to driver value
4075  }
4076 
4080  abstract protected function fetchAffectedRowCount();
4081 
4095  protected function resultObject( $result ) {
4096  if ( !$result ) {
4097  return false;
4098  } elseif ( $result instanceof ResultWrapper ) {
4099  return $result;
4100  } elseif ( $result === true ) {
4101  // Successful write query
4102  return $result;
4103  } else {
4104  return new ResultWrapper( $this, $result );
4105  }
4106  }
4107 
4108  public function ping( &$rtt = null ) {
4109  // Avoid hitting the server if it was hit recently
4110  if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::$PING_TTL ) {
4111  if ( !func_num_args() || $this->lastRoundTripEstimate > 0 ) {
4113  return true; // don't care about $rtt
4114  }
4115  }
4116 
4117  // This will reconnect if possible or return false if not
4118  $this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
4119  $ok = ( $this->query( self::$PING_QUERY, __METHOD__, true ) !== false );
4120  $this->restoreFlags( self::RESTORE_PRIOR );
4121 
4122  if ( $ok ) {
4124  }
4125 
4126  return $ok;
4127  }
4128 
4135  protected function replaceLostConnection( $fname ) {
4136  $this->closeConnection();
4137  $this->opened = false;
4138  $this->conn = false;
4139 
4140  $this->handleSessionLossPreconnect();
4141 
4142  try {
4143  $this->open(
4144  $this->server,
4145  $this->user,
4146  $this->password,
4147  $this->getDBname(),
4148  $this->dbSchema(),
4149  $this->tablePrefix()
4150  );
4151  $this->lastPing = microtime( true );
4152  $ok = true;
4153 
4154  $this->connLogger->warning(
4155  $fname . ': lost connection to {dbserver}; reconnected',
4156  [
4157  'dbserver' => $this->getServer(),
4158  'trace' => ( new RuntimeException() )->getTraceAsString()
4159  ]
4160  );
4161  } catch ( DBConnectionError $e ) {
4162  $ok = false;
4163 
4164  $this->connLogger->error(
4165  $fname . ': lost connection to {dbserver} permanently',
4166  [ 'dbserver' => $this->getServer() ]
4167  );
4168  }
4169 
4171 
4172  return $ok;
4173  }
4174 
4175  public function getSessionLagStatus() {
4176  return $this->getRecordedTransactionLagStatus() ?: $this->getApproximateLagStatus();
4177  }
4178 
4192  final protected function getRecordedTransactionLagStatus() {
4193  return ( $this->trxLevel && $this->trxReplicaLag !== null )
4194  ? [ 'lag' => $this->trxReplicaLag, 'since' => $this->trxTimestamp() ]
4195  : null;
4196  }
4197 
4204  protected function getApproximateLagStatus() {
4205  return [
4206  'lag' => $this->getLBInfo( 'replica' ) ? $this->getLag() : 0,
4207  'since' => microtime( true )
4208  ];
4209  }
4210 
4230  public static function getCacheSetOptions( IDatabase $db1, IDatabase $db2 = null ) {
4231  $res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
4232  foreach ( func_get_args() as $db ) {
4234  $status = $db->getSessionLagStatus();
4235  if ( $status['lag'] === false ) {
4236  $res['lag'] = false;
4237  } elseif ( $res['lag'] !== false ) {
4238  $res['lag'] = max( $res['lag'], $status['lag'] );
4239  }
4240  $res['since'] = min( $res['since'], $status['since'] );
4241  $res['pending'] = $res['pending'] ?: $db->writesPending();
4242  }
4243 
4244  return $res;
4245  }
4246 
4247  public function getLag() {
4248  return 0;
4249  }
4250 
4251  public function maxListLen() {
4252  return 0;
4253  }
4254 
4255  public function encodeBlob( $b ) {
4256  return $b;
4257  }
4258 
4259  public function decodeBlob( $b ) {
4260  if ( $b instanceof Blob ) {
4261  $b = $b->fetch();
4262  }
4263  return $b;
4264  }
4265 
4266  public function setSessionOptions( array $options ) {
4267  }
4268 
4269  public function sourceFile(
4270  $filename,
4271  callable $lineCallback = null,
4272  callable $resultCallback = null,
4273  $fname = false,
4274  callable $inputCallback = null
4275  ) {
4276  Wikimedia\suppressWarnings();
4277  $fp = fopen( $filename, 'r' );
4278  Wikimedia\restoreWarnings();
4279 
4280  if ( $fp === false ) {
4281  throw new RuntimeException( "Could not open \"{$filename}\".\n" );
4282  }
4283 
4284  if ( !$fname ) {
4285  $fname = __METHOD__ . "( $filename )";
4286  }
4287 
4288  try {
4289  $error = $this->sourceStream(
4290  $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
4291  } catch ( Exception $e ) {
4292  fclose( $fp );
4293  throw $e;
4294  }
4295 
4296  fclose( $fp );
4297 
4298  return $error;
4299  }
4300 
4301  public function setSchemaVars( $vars ) {
4302  $this->schemaVars = $vars;
4303  }
4304 
4305  public function sourceStream(
4306  $fp,
4307  callable $lineCallback = null,
4308  callable $resultCallback = null,
4309  $fname = __METHOD__,
4310  callable $inputCallback = null
4311  ) {
4312  $delimiterReset = new ScopedCallback(
4313  function ( $delimiter ) {
4314  $this->delimiter = $delimiter;
4315  },
4316  [ $this->delimiter ]
4317  );
4318  $cmd = '';
4319 
4320  while ( !feof( $fp ) ) {
4321  if ( $lineCallback ) {
4322  call_user_func( $lineCallback );
4323  }
4324 
4325  $line = trim( fgets( $fp ) );
4326 
4327  if ( $line == '' ) {
4328  continue;
4329  }
4330 
4331  if ( $line[0] == '-' && $line[1] == '-' ) {
4332  continue;
4333  }
4334 
4335  if ( $cmd != '' ) {
4336  $cmd .= ' ';
4337  }
4338 
4339  $done = $this->streamStatementEnd( $cmd, $line );
4340 
4341  $cmd .= "$line\n";
4342 
4343  if ( $done || feof( $fp ) ) {
4344  $cmd = $this->replaceVars( $cmd );
4345 
4346  if ( $inputCallback ) {
4347  $callbackResult = $inputCallback( $cmd );
4348 
4349  if ( is_string( $callbackResult ) || !$callbackResult ) {
4350  $cmd = $callbackResult;
4351  }
4352  }
4353 
4354  if ( $cmd ) {
4355  $res = $this->query( $cmd, $fname );
4356 
4357  if ( $resultCallback ) {
4358  $resultCallback( $res, $this );
4359  }
4360 
4361  if ( $res === false ) {
4362  $err = $this->lastError();
4363 
4364  return "Query \"{$cmd}\" failed with error code \"$err\".\n";
4365  }
4366  }
4367  $cmd = '';
4368  }
4369  }
4370 
4371  ScopedCallback::consume( $delimiterReset );
4372  return true;
4373  }
4374 
4382  public function streamStatementEnd( &$sql, &$newLine ) {
4383  if ( $this->delimiter ) {
4384  $prev = $newLine;
4385  $newLine = preg_replace(
4386  '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
4387  if ( $newLine != $prev ) {
4388  return true;
4389  }
4390  }
4391 
4392  return false;
4393  }
4394 
4415  protected function replaceVars( $ins ) {
4416  $vars = $this->getSchemaVars();
4417  return preg_replace_callback(
4418  '!
4419  /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
4420  \'\{\$ (\w+) }\' | # 3. addQuotes
4421  `\{\$ (\w+) }` | # 4. addIdentifierQuotes
4422  /\*\$ (\w+) \*/ # 5. leave unencoded
4423  !x',
4424  function ( $m ) use ( $vars ) {
4425  // Note: Because of <https://bugs.php.net/bug.php?id=51881>,
4426  // check for both nonexistent keys *and* the empty string.
4427  if ( isset( $m[1] ) && $m[1] !== '' ) {
4428  if ( $m[1] === 'i' ) {
4429  return $this->indexName( $m[2] );
4430  } else {
4431  return $this->tableName( $m[2] );
4432  }
4433  } elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
4434  return $this->addQuotes( $vars[$m[3]] );
4435  } elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
4436  return $this->addIdentifierQuotes( $vars[$m[4]] );
4437  } elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
4438  return $vars[$m[5]];
4439  } else {
4440  return $m[0];
4441  }
4442  },
4443  $ins
4444  );
4445  }
4446 
4453  protected function getSchemaVars() {
4454  if ( $this->schemaVars ) {
4455  return $this->schemaVars;
4456  } else {
4457  return $this->getDefaultSchemaVars();
4458  }
4459  }
4460 
4469  protected function getDefaultSchemaVars() {
4470  return [];
4471  }
4472 
4473  public function lockIsFree( $lockName, $method ) {
4474  // RDBMs methods for checking named locks may or may not count this thread itself.
4475  // In MySQL, IS_FREE_LOCK() returns 0 if the thread already has the lock. This is
4476  // the behavior choosen by the interface for this method.
4477  return !isset( $this->sessionNamedLocks[$lockName] );
4478  }
4479 
4480  public function lock( $lockName, $method, $timeout = 5 ) {
4481  $this->sessionNamedLocks[$lockName] = 1;
4482 
4483  return true;
4484  }
4485 
4486  public function unlock( $lockName, $method ) {
4487  unset( $this->sessionNamedLocks[$lockName] );
4488 
4489  return true;
4490  }
4491 
4492  public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
4493  if ( $this->writesOrCallbacksPending() ) {
4494  // This only flushes transactions to clear snapshots, not to write data
4495  $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
4496  throw new DBUnexpectedError(
4497  $this,
4498  "$fname: Cannot flush pre-lock snapshot because writes are pending ($fnames)."
4499  );
4500  }
4501 
4502  if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
4503  return null;
4504  }
4505 
4506  $unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
4507  if ( $this->trxLevel() ) {
4508  // There is a good chance an exception was thrown, causing any early return
4509  // from the caller. Let any error handler get a chance to issue rollback().
4510  // If there isn't one, let the error bubble up and trigger server-side rollback.
4511  $this->onTransactionResolution(
4512  function () use ( $lockKey, $fname ) {
4513  $this->unlock( $lockKey, $fname );
4514  },
4515  $fname
4516  );
4517  } else {
4518  $this->unlock( $lockKey, $fname );
4519  }
4520  } );
4521 
4522  $this->commit( $fname, self::FLUSHING_INTERNAL );
4523 
4524  return $unlocker;
4525  }
4526 
4527  public function namedLocksEnqueue() {
4528  return false;
4529  }
4530 
4532  return true;
4533  }
4534 
4535  final public function lockTables( array $read, array $write, $method ) {
4536  if ( $this->writesOrCallbacksPending() ) {
4537  throw new DBUnexpectedError( $this, "Transaction writes or callbacks still pending." );
4538  }
4539 
4540  if ( $this->tableLocksHaveTransactionScope() ) {
4541  $this->startAtomic( $method );
4542  }
4543 
4544  return $this->doLockTables( $read, $write, $method );
4545  }
4546 
4555  protected function doLockTables( array $read, array $write, $method ) {
4556  return true;
4557  }
4558 
4559  final public function unlockTables( $method ) {
4560  if ( $this->tableLocksHaveTransactionScope() ) {
4561  $this->endAtomic( $method );
4562 
4563  return true; // locks released on COMMIT/ROLLBACK
4564  }
4565 
4566  return $this->doUnlockTables( $method );
4567  }
4568 
4575  protected function doUnlockTables( $method ) {
4576  return true;
4577  }
4578 
4586  public function dropTable( $tableName, $fName = __METHOD__ ) {
4587  if ( !$this->tableExists( $tableName, $fName ) ) {
4588  return false;
4589  }
4590  $sql = "DROP TABLE " . $this->tableName( $tableName ) . " CASCADE";
4591 
4592  return $this->query( $sql, $fName );
4593  }
4594 
4595  public function getInfinity() {
4596  return 'infinity';
4597  }
4598 
4599  public function encodeExpiry( $expiry ) {
4600  return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
4601  ? $this->getInfinity()
4602  : $this->timestamp( $expiry );
4603  }
4604 
4605  public function decodeExpiry( $expiry, $format = TS_MW ) {
4606  if ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) {
4607  return 'infinity';
4608  }
4609 
4610  return ConvertibleTimestamp::convert( $format, $expiry );
4611  }
4612 
4613  public function setBigSelects( $value = true ) {
4614  // no-op
4615  }
4616 
4617  public function isReadOnly() {
4618  return ( $this->getReadOnlyReason() !== false );
4619  }
4620 
4624  protected function getReadOnlyReason() {
4625  $reason = $this->getLBInfo( 'readOnlyReason' );
4626 
4627  return is_string( $reason ) ? $reason : false;
4628  }
4629 
4630  public function setTableAliases( array $aliases ) {
4631  $this->tableAliases = $aliases;
4632  }
4633 
4634  public function setIndexAliases( array $aliases ) {
4635  $this->indexAliases = $aliases;
4636  }
4637 
4643  protected function hasFlags( $field, $flags ) {
4644  return ( ( $field & $flags ) === $flags );
4645  }
4646 
4658  protected function getBindingHandle() {
4659  if ( !$this->conn ) {
4660  throw new DBUnexpectedError(
4661  $this,
4662  'DB connection was already closed or the connection dropped.'
4663  );
4664  }
4665 
4666  return $this->conn;
4667  }
4668 
4669  public function __toString() {
4670  // spl_object_id is PHP >= 7.2
4671  $id = function_exists( 'spl_object_id' )
4672  ? spl_object_id( $this )
4673  : spl_object_hash( $this );
4674 
4675  $description = $this->getType() . ' object #' . $id;
4676  if ( is_resource( $this->conn ) ) {
4677  $description .= ' (' . (string)$this->conn . ')'; // "resource id #<ID>"
4678  } elseif ( is_object( $this->conn ) ) {
4679  // spl_object_id is PHP >= 7.2
4680  $handleId = function_exists( 'spl_object_id' )
4681  ? spl_object_id( $this->conn )
4682  : spl_object_hash( $this->conn );
4683  $description .= " (handle id #$handleId)";
4684  }
4685 
4686  return $description;
4687  }
4688 
4693  public function __clone() {
4694  $this->connLogger->warning(
4695  "Cloning " . static::class . " is not recommended; forking connection:\n" .
4696  ( new RuntimeException() )->getTraceAsString()
4697  );
4698 
4699  if ( $this->isOpen() ) {
4700  // Open a new connection resource without messing with the old one
4701  $this->opened = false;
4702  $this->conn = false;
4703  $this->trxEndCallbacks = []; // don't copy
4704  $this->handleSessionLossPreconnect(); // no trx or locks anymore
4705  $this->open(
4706  $this->server,
4707  $this->user,
4708  $this->password,
4709  $this->getDBname(),
4710  $this->dbSchema(),
4711  $this->tablePrefix()
4712  );
4713  $this->lastPing = microtime( true );
4714  }
4715  }
4716 
4722  public function __sleep() {
4723  throw new RuntimeException( 'Database serialization may cause problems, since ' .
4724  'the connection is not restored on wakeup.' );
4725  }
4726 
4730  public function __destruct() {
4731  if ( $this->trxLevel && $this->trxDoneWrites ) {
4732  trigger_error( "Uncommitted DB writes (transaction from {$this->trxFname})." );
4733  }
4734 
4735  $danglingWriters = $this->pendingWriteAndCallbackCallers();
4736  if ( $danglingWriters ) {
4737  $fnames = implode( ', ', $danglingWriters );
4738  trigger_error( "DB transaction writes or callbacks still pending ($fnames)." );
4739  }
4740 
4741  if ( $this->conn ) {
4742  // Avoid connection leaks for sanity. Normally, resources close at script completion.
4743  // The connection might already be closed in zend/hhvm by now, so suppress warnings.
4744  Wikimedia\suppressWarnings();
4745  $this->closeConnection();
4746  Wikimedia\restoreWarnings();
4747  $this->conn = false;
4748  $this->opened = false;
4749  }
4750  }
4751 }
4752 
4756 class_alias( Database::class, 'DatabaseBase' );
4757 
4761 class_alias( Database::class, 'Database' );
Helper class that detects high-contention DB queries via profiling calls.
normalizeConditions( $conds, $fname)
Definition: Database.php:1957
setIndexAliases(array $aliases)
Convert certain index names to alternative names before querying the DB.
Definition: Database.php:4634
rollback( $fname=__METHOD__, $flush=self::FLUSHING_ONE)
Rollback a transaction previously started using begin().
Definition: Database.php:3956
canRecoverFromDisconnect( $sql, $priorWritesPending)
Determine whether it is safe to retry queries after a database connection is lost.
Definition: Database.php:1433
const DBO_IGNORE
Definition: defines.php:11
conditional( $cond, $trueVal, $falseVal)
Returns an SQL expression for a simple conditional.
Definition: Database.php:3230
clearFlag( $flag, $remember=self::REMEMBER_NOTHING)
Clear a flag for this connection.
Definition: Database.php:738
doneWrites()
Returns true if the connection may have been used for write queries.
Definition: Database.php:614
executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags)
Wrapper for doQuery() that handles DBO_TRX, profiling, logging, affected row count tracking...
Definition: Database.php:1248
decodeExpiry( $expiry, $format=TS_MW)
Decode an expiry time into a DBMS independent format.
Definition: Database.php:4605
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
array $connectionVariables
SQL variables values to use for all new connections.
Definition: Database.php:72
isTransactableQuery( $sql)
Determine whether a SQL statement is sensitive to isolation level.
Definition: Database.php:1079
setSessionOptions(array $options)
Override database&#39;s default behavior.
Definition: Database.php:4266
namedLocksEnqueue()
Check to see if a named lock used by lock() use blocking queues.
Definition: Database.php:4527
getFlag( $flag)
Returns a boolean whether the flag $flag is set for this connection.
Definition: Database.php:762
freeResult( $res)
Free a result object returned by query() or select().
Definition: Database.php:1569
databasesAreIndependent()
Returns true if DBs are assumed to be on potentially different servers.
Definition: Database.php:2323
bool $trxAutomatic
Whether the current transaction was started implicitly due to DBO_TRX.
Definition: Database.php:128
unlockTables( $method)
Unlock all tables locked via lockTables()
Definition: Database.php:4559
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by wfTimestamp() to the format used for inserting ...
Definition: Database.php:4057
affectedRows()
Get the number of rows affected by the last write query.
Definition: Database.php:4071
encodeBlob( $b)
Some DBMSs have a special format for inserting into blob fields, they don&#39;t allow simple quoted strin...
Definition: Database.php:4255
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:146
if(is_array( $mode)) switch( $mode) $input
array $sessionTempTables
Map of (table name => 1) for TEMPORARY tables.
Definition: Database.php:107
doSelectDomain(DatabaseDomain $domain)
Definition: Database.php:2341
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
lastErrno()
Get the last error number.
escapeLikeInternal( $s, $escapeChar='`')
Definition: Database.php:2715
buildConcat( $stringList)
Build a concatenation list to feed into a SQL query.
Definition: Database.php:2258
selectDomain( $domain)
Set the current domain (database, schema, and table prefix)
Definition: Database.php:2337
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:653
begin( $fname=__METHOD__, $mode=self::TRANSACTION_EXPLICIT)
Begin a transaction.
Definition: Database.php:3819
encodeExpiry( $expiry)
Encode an expiry time into the DBMS dependent format.
Definition: Database.php:4599
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:683
static int $TEMP_PSEUDO_PERMANENT
Writes to this temporary table effect lastDoneWrites()
Definition: Database.php:200
array null $trxStatusIgnoredCause
Error details of the last statement-only rollback.
Definition: Database.php:118
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:41
ignoreIndexClause( $index)
IGNORE INDEX clause.
Definition: Database.php:2784
static string $SAVEPOINT_PREFIX
Prefix to the atomic section counter used to make savepoint IDs.
Definition: Database.php:195
insert( $table, $a, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
Definition: Database.php:2053
getScopedLockAndFlush( $lockKey, $fname, $timeout)
Acquire a named lock, flush any transaction, and return an RAII style unlocker object.
Definition: Database.php:4492
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:2748
executeQuery( $sql, $fname, $flags)
Execute a query, retrying it if there is a recoverable connection loss.
Definition: Database.php:1167
__sleep()
Called by serialize.
Definition: Database.php:4722
getServer()
Get the server hostname or IP address.
Definition: Database.php:2349
assertBuildSubstringParams( $startPosition, $length)
Check type and bounds for parameters to self::buildSubstring()
Definition: Database.php:2291
string $trxShortId
Hexidecimal string if a transaction is active or empty string otherwise.
Definition: Database.php:112
Exception null $trxStatusCause
The last error that caused the status to become STATUS_TRX_ERROR.
Definition: Database.php:116
int $trxLevel
Whether there is an active transaction (1 or 0)
Definition: Database.php:110
setTrxEndCallbackSuppression( $suppress)
Whether to disable running of post-COMMIT/ROLLBACK callbacks.
Definition: Database.php:3473
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:1598
getSessionLagStatus()
Get the replica DB lag when the current transaction started or a general lag estimate if not transact...
Definition: Database.php:4175
__clone()
Make sure that copies do not share the same client binding handle.
Definition: Database.php:4693
array $connectionParams
Parameters used by initConnection() to establish a connection.
Definition: Database.php:70
selectDB( $db)
Change the current database.
Definition: Database.php:2327
reassignCallbacksForSection(AtomicSectionIdentifier $old, AtomicSectionIdentifier $new)
Definition: Database.php:3408
writesOrCallbacksPending()
Whether there is a transaction open with either possible write queries or unresolved pre-commit/commi...
Definition: Database.php:626
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:1572
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:95
$value
int $flags
Bitfield of class DBO_* constants.
Definition: Database.php:64
makeWhereFrom2d( $data, $baseKey, $subKey)
Build a partial where clause from a 2-d array such as used for LinkBatch.
Definition: Database.php:2223
onTransactionIdle(callable $callback, $fname=__METHOD__)
Alias for onTransactionCommitOrIdle() for backwards-compatibility.
Definition: Database.php:3365
getDomainID()
Return the currently selected domain ID.
Definition: Database.php:775
buildSelectSubquery( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Equivalent to IDatabase::selectSQLText() except wraps the result in Subqyery.
Definition: Database.php:2314
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:3137
nonNativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname=__METHOD__, $insertOptions=[], $selectOptions=[], $selectJoinConds=[])
Implementation of insertSelect() based on select() and insert()
Definition: Database.php:3040
trxLevel()
Gets the current transaction level.
Definition: Database.php:515
callable [] $trxRecurringCallbacks
Map of (name => callable)
Definition: Database.php:154
startAtomic( $fname=__METHOD__, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Begin an atomic section of SQL statements.
Definition: Database.php:3666
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
Definition: Database.php:2129
replaceVars( $ins)
Database independent variable replacement.
Definition: Database.php:4415
string bool null $htmlErrors
Stashed value of html_errors INI setting.
Definition: Database.php:76
fieldNameWithAlias( $name, $alias=false)
Get an aliased field name e.g.
Definition: Database.php:2541
nativeReplace( $table, $rows, $fname)
REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE statement.
Definition: Database.php:2849
implicitOrderby()
Returns true if this database does an implicit order by when the column has an index For example: SEL...
Definition: Database.php:606
static factory( $dbType, $p=[], $connect=self::NEW_CONNECTED)
Construct a Database subclass instance given a database type and parameters.
Definition: Database.php:361
endAtomic( $fname=__METHOD__)
Ends an atomic section of SQL statements.
Definition: Database.php:3696
doCommit( $fname)
Issues the COMMIT command to the database server.
Definition: Database.php:3949
query( $sql, $fname=__METHOD__, $flags=0)
Run an SQL query and return the result.
Definition: Database.php:1130
string $server
Server that this instance is currently connected to.
Definition: Database.php:50
getLBInfo( $name=null)
Get properties passed down from the server info array of the load balancer.
Definition: Database.php:569
nextSequenceValue( $seqName)
Deprecated method, calls should be removed.
Definition: Database.php:2756
handleSessionLossPreconnect()
Clean things up after session (and thus transaction) loss before reconnect.
Definition: Database.php:1458
buildGroupConcatField( $delim, $table, $field, $conds='', $join_conds=[])
Build a GROUP_CONCAT or equivalent statement for a query.
Definition: Database.php:2262
doLockTables(array $read, array $write, $method)
Helper function for lockTables() that handles the actual table locking.
Definition: Database.php:4555
textFieldSize( $table, $field)
Returns the size of a text field, or -1 for "unlimited".
Definition: Database.php:2941
reportConnectionError( $error='Unknown error')
Definition: Database.php:986
bufferResults( $buffer=null)
Turns buffering of SQL result sets on (true) or off (false).
Definition: Database.php:504
fieldExists( $table, $field, $fname=__METHOD__)
Determines whether a field exists in a table.
Definition: Database.php:2012
float $lastPing
UNIX timestamp.
Definition: Database.php:165
callable $errorLogger
Error logging callback.
Definition: Database.php:87
bitAnd( $fieldLeft, $fieldRight)
Definition: Database.php:2250
callable null $profiler
Definition: Database.php:91
Lazy-loaded wrapper for simplification and scrubbing of SQL queries for profiling.
this hook is for auditing only RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist & $tables
Definition: hooks.txt:979
getServerVersion()
A string describing the current software version, like from mysql_get_server_info().
getWikiID()
Alias for getDomainID()
Definition: Database.php:779
maxListLen()
Return the maximum number of items allowed in a list, or 0 for unlimited.
Definition: Database.php:4251
doSavepoint( $identifier, $fname)
Create a savepoint.
Definition: Database.php:3615
modifyCallbacksForCancel(array $sectionIds)
Definition: Database.php:3432
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:1633
if( $line===false) $args
Definition: cdb.php:64
isWriteQuery( $sql)
Determine whether a query writes to the DB.
Definition: Database.php:1038
tableNames()
Fetch a number of table names into an array This is handy when you need to construct SQL for joins...
Definition: Database.php:2462
close()
Close the database connection.
Definition: Database.php:865
listViews( $prefix=null, $fname=__METHOD__)
Lists all the VIEWs in the database.
Definition: Database.php:4053
strreplace( $orig, $old, $new)
Returns a command for str_replace function in SQL query.
Definition: Database.php:3238
getMasterPos()
Get the position of this master.
Definition: Database.php:3336
__destruct()
Run a few simple sanity checks and close dangling connections.
Definition: Database.php:4730
isOpen()
Is a connection to the database open?
Definition: Database.php:723
installErrorHandler()
Set a custom error handler for logging errors during database connection.
Definition: Database.php:803
getDBname()
Get the current DB name.
Definition: Database.php:2345
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
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:85
implicitGroupby()
Returns true if this database does an implicit sort when doing GROUP BY.
Definition: Database.php:602
setLogger(LoggerInterface $logger)
Set the PSR-3 logger interface to use for query logging.
Definition: Database.php:496
array $sessionNamedLocks
Map of (name => 1) for locks obtained via lock()
Definition: Database.php:105
setBigSelects( $value=true)
Allow or deny "big selects" for this session only.
Definition: Database.php:4613
replaceLostConnection( $fname)
Close any existing (dead) database connection and open a new connection.
Definition: Database.php:4135
float $trxReplicaLag
Replication lag estimate at the time of BEGIN for the last transaction.
Definition: Database.php:122
decodeBlob( $b)
Some DBMSs return a special placeholder object representing blob fields in result objects...
Definition: Database.php:4259
buildLike( $param,... $params)
Definition: Database.php:2721
aggregateValue( $valuedata, $valuename='value')
Return aggregated value alias.
Definition: Database.php:2242
array $lbInfo
LoadBalancer tracking information.
Definition: Database.php:66
timestampOrNull( $ts=null)
Convert a timestamp in one of the formats accepted by wfTimestamp() to the format used for inserting ...
Definition: Database.php:4063
addQuotes( $s)
Adds quotes and backslashes.
Definition: Database.php:2676
int $trxWriteAffectedRows
Number of rows affected by write queries for the current transaction.
Definition: Database.php:142
TransactionProfiler $trxProfiler
Definition: Database.php:93
const DBO_DEBUG
Definition: defines.php:9
const LIST_AND
Definition: Defines.php:39
makeInsertOptions( $options)
Helper for Database::insert().
Definition: Database.php:2049
static getClass( $dbType, $driver=null)
Definition: Database.php:433
assertHasConnectionHandle()
Make sure there is an open connection handle (alive or not) as a sanity check.
Definition: Database.php:949
selectOptionsIncludeLocking( $options)
Definition: Database.php:1913
sourceFile( $filename, callable $lineCallback=null, callable $resultCallback=null, $fname=false, callable $inputCallback=null)
Read and execute SQL commands from a file.
Definition: Database.php:4269
static $PING_TTL
How long before it is worth doing a dummy query to test the connection.
Definition: Database.php:210
doRollback( $fname)
Issues the ROLLBACK command to the database server.
Definition: Database.php:4017
beginIfImplied( $sql, $fname)
Start an implicit transaction if DBO_TRX is enabled and no transaction is active. ...
Definition: Database.php:1335
reportQueryError( $error, $errno, $sql, $fname, $ignore=false)
Report a query error.
Definition: Database.php:1527
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:2018
assertQueryIsCurrentlyAllowed( $sql, $fname)
Error out if the DB is not in a valid state for a query via query()
Definition: Database.php:1387
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:2672
handleSessionLossPostconnect()
Clean things up after session (and thus transaction) loss after reconnect.
Definition: Database.php:1486
string $trxFname
Name of the function that start the last transaction.
Definition: Database.php:124
const LIST_COMMA
Definition: Defines.php:38
$res
Definition: database.txt:21
fieldNamesWithAlias( $fields)
Gets an array of aliased field names.
Definition: Database.php:2555
insertSelect( $destTable, $srcTable, $varMap, $conds, $fname=__METHOD__, $insertOptions=[], $selectOptions=[], $selectJoinConds=[])
INSERT SELECT wrapper.
Definition: Database.php:2978
doRollbackToSavepoint( $identifier, $fname)
Rollback to a savepoint.
Definition: Database.php:3643
tableName( $name, $format='quoted')
Format a table name ready for use in constructing an SQL query.
Definition: Database.php:2353
runOnTransactionIdleCallbacks( $trigger)
Actually consume and run any "on transaction idle/resolution" callbacks.
Definition: Database.php:3487
getServerUptime()
Determines how long the server has been up.
Definition: Database.php:3242
static $DEADLOCK_DELAY_MIN
Minimum time to wait before retry, in microseconds.
Definition: Database.php:205
unionConditionPermutations( $table, $vars, array $permute_conds, $extra_conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Construct a UNION query for permutations of conditions.
Definition: Database.php:3159
string $delimiter
Current SQL query delimiter.
Definition: Database.php:74
setSchemaVars( $vars)
Set variables to be used in sourceFile/sourceStream, in preference to the ones in $GLOBALS...
Definition: Database.php:4301
bool $trxAutomaticAtomic
Whether the current transaction was started implicitly by startAtomic()
Definition: Database.php:134
int $trxAtomicCounter
Counter for atomic savepoint identifiers (reset with each transaction)
Definition: Database.php:130
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:3352
flushSnapshot( $fname=__METHOD__)
Commit any transaction but error out if writes or callbacks are pending.
Definition: Database.php:4026
$params
indexUnique( $table, $index)
Determines if a given index is unique.
Definition: Database.php:2033
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:2495
getInfinity()
Find out when &#39;infinity&#39; is.
Definition: Database.php:4595
static string $NOT_APPLICABLE
Idiom used when a cancelable atomic section started the transaction.
Definition: Database.php:193
string $lastQuery
The last SQL query attempted.
Definition: Database.php:167
array $trxAtomicLevels
List of (name, unique ID, savepoint ID) for each active atomic section level.
Definition: Database.php:132
string [] $trxWriteCallers
Write query callers of the current transaction.
Definition: Database.php:136
runTransactionListenerCallbacks( $trigger)
Actually run any "transaction listener" callbacks.
Definition: Database.php:3583
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:4204
lastError()
Get a description of the last error.
setFlag( $flag, $remember=self::REMEMBER_NOTHING)
Set a flag for this connection.
Definition: Database.php:727
makeUpdateOptions( $options)
Make UPDATE options for the Database::update function.
Definition: Database.php:2123
$buffer
IDatabase null $lazyMasterHandle
Lazy handle to the master DB this server replicates from.
Definition: Database.php:97
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:2577
prependDatabaseOrSchema( $namespace, $relation, $format)
Definition: Database.php:2451
integer null $affectedRowCount
Rows affected by the last query to query() or its CRUD wrappers.
Definition: Database.php:162
doReleaseSavepoint( $identifier, $fname)
Release a savepoint.
Definition: Database.php:3629
isInsertSelectSafe(array $insertOptions, array $selectOptions)
Definition: Database.php:3022
float $trxWriteAdjDuration
Like trxWriteQueryCount but excludes lock-bound, easy to replicate, queries.
Definition: Database.php:144
initConnection()
Initialize the connection to the database over the wire (or to local files)
Definition: Database.php:272
lockForUpdate( $table, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Lock all rows meeting the given conditions/options FOR UPDATE.
Definition: Database.php:1996
restoreFlags( $state=self::RESTORE_PRIOR)
Restore the flags to their prior state before the last setFlag/clearFlag call.
Definition: Database.php:749
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:1762
makeList( $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.
Definition: Database.php:2143
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:221
makeUpdateOptionsArray( $options)
Make UPDATE options array for Database::makeUpdateOptions.
Definition: Database.php:2103
connectionErrorLogger( $errno, $errstr)
Error handler for logging errors during database connection This method should not be used outside of...
Definition: Database.php:844
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:3109
qualifiedTableComponents( $name)
Get the table components needed for a query given the currently selected database.
Definition: Database.php:2411
tableExists( $table, $fname=__METHOD__)
Query whether a given table exists.
getRecordedTransactionLagStatus()
Get the replica DB lag when the current transaction started.
Definition: Database.php:4192
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:3548
const LIST_SET
Definition: Defines.php:40
setTableAliases(array $aliases)
Make certain table names use their own database, schema, and table prefix when passed into SQL querie...
Definition: Database.php:4630
trxTimestamp()
Get the UNIX timestamp of the time that the transaction was established.
Definition: Database.php:519
tablePrefix( $prefix=null)
Get/set the table prefix.
Definition: Database.php:531
const LIST_OR
Definition: Defines.php:42
const DBO_TRX
Definition: defines.php:12
isQuotedIdentifier( $name)
Returns if the given identifier looks quoted or not according to the database convention for quoting ...
Definition: Database.php:2706
deadlockLoop()
Perform a deadlock-prone transaction.
Definition: Database.php:3290
bool $opened
Whether a connection handle is open (connection itself might be dead)
Definition: Database.php:102
BagOStuff $srvCache
APC cache.
Definition: Database.php:81
cancelAtomic( $fname=__METHOD__, AtomicSectionIdentifier $sectionId=null)
Cancel an atomic section of SQL statements.
Definition: Database.php:3730
getServerInfo()
A string describing the current software version, and possibly other details in a user-friendly way...
Definition: Database.php:500
float bool $lastWriteTime
UNIX timestamp of last write query.
Definition: Database.php:169
const DBO_NOBUFFER
Definition: defines.php:10
string $agent
Agent name for query profiling.
Definition: Database.php:62
unlock( $lockName, $method)
Release a lock.
Definition: Database.php:4486
int $trxStatus
Transaction status.
Definition: Database.php:114
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:4575
ping(&$rtt=null)
Ping the server and try to reconnect if it there is no connection.
Definition: Database.php:4108
getType()
Get the type of the DBMS, as it appears in $wgDBtype.
lastDoneWrites()
Returns the last time the connection may have been used for write queries.
Definition: Database.php:618
LoggerInterface $connLogger
Definition: Database.php:83
setLazyMasterHandle(IDatabase $conn)
Set a lazy-connecting DB handle to the master DB (for replication status purposes) ...
Definition: Database.php:589
tableNamesWithAlias( $tables)
Gets an array of aliased table names.
Definition: Database.php:2521
bitOr( $fieldLeft, $fieldRight)
Definition: Database.php:2254
Class to handle database/prefix specification for IDatabase domains.
array [] $trxIdleCallbacks
List of (callable, method name, atomic section id)
Definition: Database.php:148
bool $trxDoneWrites
Whether possible write queries were done in the last transaction started.
Definition: Database.php:126
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
float null $trxTimestamp
UNIX timestamp at the time of BEGIN for the last transaction.
Definition: Database.php:120
getLogContext(array $extras=[])
Create a log context to pass to PSR-3 logger functions.
Definition: Database.php:854
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Single row SELECT wrapper.
Definition: Database.php:1845
assertNoOpenTransactions()
Assert that all explicit transactions or atomic sections have been closed.
Definition: Database.php:1414
static $DEADLOCK_DELAY_MAX
Maximum time to wait before retry.
Definition: Database.php:207
upsert( $table, array $rows, $uniqueIndexes, array $set, $fname=__METHOD__)
INSERT ON DUPLICATE KEY UPDATE wrapper, upserts an array into a table.
Definition: Database.php:2873
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:2473
Relational database abstraction object.
Definition: Database.php:48
array [] $trxEndCallbacks
List of (callable, method name, atomic section id)
Definition: Database.php:152
unionQueries( $sqls, $all)
Construct a UNION query This is used for providing overload point for other DB abstractions not compa...
Definition: Database.php:3153
static attributesFromType( $dbType, $driver=null)
Definition: Database.php:416
wasErrorReissuable()
Determines if the last query error was due to something outside of the query itself.
Definition: Database.php:3262
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:1754
Advanced database interface for IDatabase handles that include maintenance methods.
buildSubstring( $input, $startPosition, $length=null)
Definition: Database.php:2270
wasConnectionError( $errno)
Do not use this method outside of Database/DBError classes.
Definition: Database.php:3276
bool $trxEndCallbacksSuppressed
Whether to suppress triggering of transaction end callbacks.
Definition: Database.php:156
lastQuery()
Return the last query that sent on account of IDatabase::query()
Definition: Database.php:610
pendingWriteAndCallbackCallers()
List the methods that have write queries or callbacks for the current transaction.
Definition: Database.php:699
commit( $fname=__METHOD__, $flush=self::FLUSHING_ONE)
Commits a transaction previously started using begin().
Definition: Database.php:3882
onTransactionResolution(callable $callback, $fname=__METHOD__)
Run a callback as soon as the current transaction commits or rolls back.
Definition: Database.php:3345
listTables( $prefix=null, $fname=__METHOD__)
List all tables on the database.
Definition: Database.php:4049
$line
Definition: cdb.php:59
estimateRowCount( $table, $var=' *', $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Estimate the number of rows in dataset.
Definition: Database.php:1863
doAtomicSection( $fname, callable $callback, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Perform an atomic section of reversable SQL statements from a callback.
Definition: Database.php:3803
restoreErrorHandler()
Restore the previous error handler and return the last PHP error for this DB.
Definition: Database.php:814
array [] $trxPreCommitCallbacks
List of (callable, method name, atomic section id)
Definition: Database.php:150
object resource null $conn
Database connection.
Definition: Database.php:100
assertIsWritableMaster()
Make sure that this server is not marked as a replica nor read-only as a sanity check.
Definition: Database.php:961
setTransactionListener( $name, callable $callback=null)
Run a callback after each time any transaction commits or rolls back.
Definition: Database.php:3457
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:544
static int $TEMP_NORMAL
Writes to this temporary table do not affect lastDoneWrites()
Definition: Database.php:198
dropTable( $tableName, $fName=__METHOD__)
Delete a table.
Definition: Database.php:4586
hasFlags( $field, $flags)
Definition: Database.php:4643
doInitConnection()
Actually connect to the database over the wire (or to local files)
Definition: Database.php:287
deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname=__METHOD__)
DELETE where the condition is a join.
Definition: Database.php:2923
masterPosWait(DBMasterPos $pos, $timeout)
Wait for the replica DB to catch up to a given master position.
Definition: Database.php:3326
string $user
User that this instance is currently connected under the name of.
Definition: Database.php:52
lock( $lockName, $method, $timeout=5)
Acquire a named lock.
Definition: Database.php:4480
static $DEADLOCK_TRIES
Number of times to re-try an operation in case of deadlock.
Definition: Database.php:203
callable $deprecationLogger
Deprecation logging callback.
Definition: Database.php:89
addIdentifierQuotes( $s)
Quotes an identifier, in order to make user controlled input safe.
Definition: Database.php:2693
Error thrown when a query times out.
strencode( $s)
Wrapper for addslashes()
selectFieldsOrOptionsAggregate( $fields, $options)
Definition: Database.php:1929
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:4230
lockTables(array $read, array $write, $method)
Lock specific tables.
Definition: Database.php:4535
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:1742
wasReadOnlyError()
Determines if the last failure was due to the database being read-only.
Definition: Database.php:3258
selectRowCount( $tables, $var=' *', $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Get the number of rows in dataset.
Definition: Database.php:1880
wasConnectionLoss()
Determines if the last query error was due to a dropped connection.
Definition: Database.php:3254
int $trxWriteQueryCount
Number of write queries for the current transaction.
Definition: Database.php:140
tableLocksHaveTransactionScope()
Checks if table locks acquired by lockTables() are transaction-bound in their scope.
Definition: Database.php:4531
getSchemaVars()
Get schema variables.
Definition: Database.php:4453
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:159
closeConnection()
Closes underlying database connection.
string bool $lastPhpError
Definition: Database.php:171
lockIsFree( $lockName, $method)
Check to see if a named lock is not locked by any thread (non-blocking)
Definition: Database.php:4473
makeGroupByWithHaving( $options)
Returns an optional GROUP BY with an optional HAVING.
Definition: Database.php:1716
bool $cliMode
Whether this PHP instance is for a CLI script.
Definition: Database.php:60
doBegin( $fname)
Issues the BEGIN command to the database server.
Definition: Database.php:3877
wasDeadlock()
Determines if the last failure was due to a deadlock.
Definition: Database.php:3246
replace( $table, $uniqueIndexes, $rows, $fname=__METHOD__)
REPLACE query wrapper.
Definition: Database.php:2788
streamStatementEnd(&$sql, &$newLine)
Called by sourceStream() to check if we&#39;ve reached a statement end.
Definition: Database.php:4382
setLBInfo( $name, $value=null)
Set the LB info array, or a member of it.
Definition: Database.php:581
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:4247
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:4043
anyString()
Returns a token for buildLike() that denotes a &#39;&#39; to be used in a LIKE query.
Definition: Database.php:2752
array [] $tableAliases
Map of (table => (dbname, schema, prefix) map)
Definition: Database.php:56
wasLockTimeout()
Determines if the last failure was due to a lock timeout.
Definition: Database.php:3250
useIndexClause( $index)
USE INDEX clause.
Definition: Database.php:2770
unionSupportsOrderAndLimit()
Returns true if current database backend supports ORDER BY or LIMIT for separate subqueries within th...
Definition: Database.php:3149
updateTrxWriteQueryTime( $sql, $runtime, $affected)
Update the estimated run-time of a query, not counting large row lock times.
Definition: Database.php:1358
wasQueryTimeout( $error, $errno)
Checks whether the cause of the error is detected to be a timeout.
Definition: Database.php:1512
getBindingHandle()
Get the underlying binding connection handle.
Definition: Database.php:4658
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:687
array bool $schemaVars
Variables use for schema element placeholders.
Definition: Database.php:68
__toString()
Get a debugging string that mentions the database type, the ID of this instance, and the ID of any un...
Definition: Database.php:4669
registerTempTableWrite( $sql, $pseudoPermanent)
Definition: Database.php:1092
string [] $indexAliases
Map of (index alias => index)
Definition: Database.php:58
resultObject( $result)
Take the result from a query, and wrap it in a ResultWrapper if necessary.
Definition: Database.php:4095
string $password
Password used to establish the current connection.
Definition: Database.php:54
float $trxWriteDuration
Seconds spent in write queries for the current transaction.
Definition: Database.php:138
getQueryExceptionAndLog( $error, $errno, $sql, $fname)
Definition: Database.php:1544
onTransactionPreCommitOrIdle(callable $callback, $fname=__METHOD__)
Run a callback before the current transaction commits or now if there is none.
Definition: Database.php:3369
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:4305
$matches
float $lastRoundTripEstimate
Query rount trip time estimate.
Definition: Database.php:173
getReplicaPos()
Get the replication position of this replica DB.
Definition: Database.php:3331
getDefaultSchemaVars()
Get schema variables to use if none have been set via setSchemaVars().
Definition: Database.php:4469