MediaWiki  master
Database.php
Go to the documentation of this file.
1 <?php
26 namespace Wikimedia\Rdbms;
27 
28 use BagOStuff;
29 use HashBagOStuff;
30 use InvalidArgumentException;
31 use LogicException;
32 use Psr\Log\LoggerAwareInterface;
33 use Psr\Log\LoggerInterface;
34 use Psr\Log\NullLogger;
35 use RuntimeException;
36 use Throwable;
37 use UnexpectedValueException;
38 use Wikimedia\Assert\Assert;
39 use Wikimedia\AtEase\AtEase;
40 use Wikimedia\RequestTimeout\CriticalSectionProvider;
41 use Wikimedia\RequestTimeout\CriticalSectionScope;
42 use Wikimedia\ScopedCallback;
43 use Wikimedia\Timestamp\ConvertibleTimestamp;
44 
52 abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAwareInterface {
54  protected $srvCache;
56  protected $csProvider;
58  protected $connLogger;
60  protected $queryLogger;
62  protected $replLogger;
64  protected $errorLogger;
66  protected $deprecationLogger;
68  protected $profiler;
70  protected $trxProfiler;
71 
73  protected $currentDomain;
74 
75  // phpcs:ignore MediaWiki.Commenting.PropertyDocumentation.ObjectTypeHintVar
77  protected $conn;
78 
81 
83  protected $server;
85  protected $user;
87  protected $password;
89  protected $serverName;
91  protected $cliMode;
93  protected $agent;
95  protected $topologyRole;
99  protected $connectionParams;
104 
106  protected $flags;
108  protected $lbInfo = [];
110  protected $delimiter = ';';
112  protected $tableAliases = [];
114  protected $indexAliases = [];
116  protected $schemaVars;
117 
119  private $htmlErrors;
121  private $priorFlags = [];
122 
124  protected $sessionNamedLocks = [];
126  protected $sessionTempTables = [];
128  protected $sessionDirtyTempTables = [];
129 
131  private $trxShortId = '';
133  private $trxStatus = self::STATUS_TRX_NONE;
139  private $trxTimestamp = null;
141  private $trxReplicaLagStatus = null;
143  private $trxFname = null;
145  private $trxDoneWrites = false;
147  private $trxAutomatic = false;
149  private $trxAtomicCounter = 0;
151  private $trxAtomicLevels = [];
153  private $trxAutomaticAtomic = false;
155  private $trxWriteCallers = [];
157  private $trxWriteDuration = 0.0;
159  private $trxWriteQueryCount = 0;
163  private $trxWriteAdjDuration = 0.0;
174  private $trxEndCallbacks = [];
180  private $trxEndCallbacksSuppressed = false;
181 
183  protected $affectedRowCount;
184 
186  private $lastPing = 0.0;
188  private $lastQuery = '';
190  private $lastWriteTime = false;
192  private $lastPhpError = false;
194  private $lastRoundTripEstimate = 0.0;
195 
197  private $csmId;
199  private $csmFname;
201  private $csmError;
202 
204  private $ownerId;
205 
207  public const ATTR_DB_IS_FILE = 'db-is-file';
209  public const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
211  public const ATTR_SCHEMAS_AS_TABLE_GROUPS = 'supports-schemas';
212 
214  public const NEW_UNCONNECTED = 0;
216  public const NEW_CONNECTED = 1;
217 
219  public const STATUS_TRX_ERROR = 1;
221  public const STATUS_TRX_OK = 2;
223  public const STATUS_TRX_NONE = 3;
224 
226  private static $NOT_APPLICABLE = 'n/a';
228  private static $SAVEPOINT_PREFIX = 'wikimedia_rdbms_atomic';
229 
231  private static $TEMP_NORMAL = 1;
233  private static $TEMP_PSEUDO_PERMANENT = 2;
234 
236  private static $DEADLOCK_TRIES = 4;
238  private static $DEADLOCK_DELAY_MIN = 500000;
240  private static $DEADLOCK_DELAY_MAX = 1500000;
241 
243  private static $PING_TTL = 1.0;
245  private static $PING_QUERY = 'SELECT 1 AS ping';
246 
248  private static $TINY_WRITE_SEC = 0.010;
250  private static $SLOW_WRITE_SEC = 0.500;
252  private static $SMALL_WRITE_ROWS = 100;
253 
255  protected static $MUTABLE_FLAGS = [
256  'DBO_DEBUG',
257  'DBO_NOBUFFER',
258  'DBO_TRX',
259  'DBO_DDLMODE',
260  ];
262  protected static $DBO_MUTABLE = (
264  );
265 
267  protected const CONN_HOST = 'host';
269  protected const CONN_USER = 'user';
271  protected const CONN_PASSWORD = 'password';
273  protected const CONN_INITIAL_DB = 'dbname';
275  protected const CONN_INITIAL_SCHEMA = 'schema';
277  protected const CONN_INITIAL_TABLE_PREFIX = 'tablePrefix';
278 
284  public function __construct( array $params ) {
285  $this->connectionParams = [
286  self::CONN_HOST => ( isset( $params['host'] ) && $params['host'] !== '' )
287  ? $params['host']
288  : null,
289  self::CONN_USER => ( isset( $params['user'] ) && $params['user'] !== '' )
290  ? $params['user']
291  : null,
292  self::CONN_INITIAL_DB => ( isset( $params['dbname'] ) && $params['dbname'] !== '' )
293  ? $params['dbname']
294  : null,
295  self::CONN_INITIAL_SCHEMA => ( isset( $params['schema'] ) && $params['schema'] !== '' )
296  ? $params['schema']
297  : null,
298  self::CONN_PASSWORD => is_string( $params['password'] ) ? $params['password'] : null,
299  self::CONN_INITIAL_TABLE_PREFIX => (string)$params['tablePrefix']
300  ];
301 
302  $this->lbInfo = $params['lbInfo'] ?? [];
303  $this->lazyMasterHandle = $params['lazyMasterHandle'] ?? null;
304  $this->connectionVariables = $params['variables'] ?? [];
305 
306  $this->flags = (int)$params['flags'];
307  $this->cliMode = (bool)$params['cliMode'];
308  $this->agent = (string)$params['agent'];
309  $this->serverName = $params['serverName'];
310  $this->topologyRole = $params['topologyRole'];
311  $this->topologyRootMaster = $params['topologicalMaster'];
312  $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'] ?? 10000;
313 
314  $this->srvCache = $params['srvCache'];
315  $this->profiler = is_callable( $params['profiler'] ) ? $params['profiler'] : null;
316  $this->trxProfiler = $params['trxProfiler'];
317  $this->connLogger = $params['connLogger'];
318  $this->queryLogger = $params['queryLogger'];
319  $this->replLogger = $params['replLogger'];
320  $this->errorLogger = $params['errorLogger'];
321  $this->deprecationLogger = $params['deprecationLogger'];
322 
323  $this->csProvider = $params['criticalSectionProvider'] ?? null;
324 
325  // Set initial dummy domain until open() sets the final DB/prefix
326  $this->currentDomain = new DatabaseDomain(
327  $params['dbname'] != '' ? $params['dbname'] : null,
328  $params['schema'] != '' ? $params['schema'] : null,
329  $params['tablePrefix']
330  );
331 
332  $this->ownerId = $params['ownerId'] ?? null;
333  }
334 
343  final public function initConnection() {
344  if ( $this->isOpen() ) {
345  throw new LogicException( __METHOD__ . ': already connected' );
346  }
347  // Establish the connection
348  $this->doInitConnection();
349  }
350 
357  protected function doInitConnection() {
358  $this->open(
359  $this->connectionParams[self::CONN_HOST],
360  $this->connectionParams[self::CONN_USER],
361  $this->connectionParams[self::CONN_PASSWORD],
362  $this->connectionParams[self::CONN_INITIAL_DB],
363  $this->connectionParams[self::CONN_INITIAL_SCHEMA],
364  $this->connectionParams[self::CONN_INITIAL_TABLE_PREFIX]
365  );
366  }
367 
379  abstract protected function open( $server, $user, $password, $db, $schema, $tablePrefix );
380 
436  final public static function factory( $type, $params = [], $connect = self::NEW_CONNECTED ) {
437  $class = self::getClass( $type, $params['driver'] ?? null );
438 
439  if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
440  $params += [
441  // Default configuration
442  'host' => null,
443  'user' => null,
444  'password' => null,
445  'dbname' => null,
446  'schema' => null,
447  'tablePrefix' => '',
448  'flags' => 0,
449  'variables' => [],
450  'lbInfo' => [],
451  'cliMode' => ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ),
452  'agent' => '',
453  'ownerId' => null,
454  'serverName' => null,
455  'topologyRole' => null,
456  'topologicalMaster' => null,
457  // Objects and callbacks
458  'lazyMasterHandle' => $params['lazyMasterHandle'] ?? null,
459  'srvCache' => $params['srvCache'] ?? new HashBagOStuff(),
460  'profiler' => $params['profiler'] ?? null,
461  'trxProfiler' => $params['trxProfiler'] ?? new TransactionProfiler(),
462  'connLogger' => $params['connLogger'] ?? new NullLogger(),
463  'queryLogger' => $params['queryLogger'] ?? new NullLogger(),
464  'replLogger' => $params['replLogger'] ?? new NullLogger(),
465  'errorLogger' => $params['errorLogger'] ?? static function ( Throwable $e ) {
466  trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
467  },
468  'deprecationLogger' => $params['deprecationLogger'] ?? static function ( $msg ) {
469  trigger_error( $msg, E_USER_DEPRECATED );
470  }
471  ];
472 
474  $conn = new $class( $params );
475  if ( $connect === self::NEW_CONNECTED ) {
476  $conn->initConnection();
477  }
478  } else {
479  $conn = null;
480  }
481 
482  return $conn;
483  }
484 
492  final public static function attributesFromType( $dbType, $driver = null ) {
493  static $defaults = [
494  self::ATTR_DB_IS_FILE => false,
495  self::ATTR_DB_LEVEL_LOCKING => false,
496  self::ATTR_SCHEMAS_AS_TABLE_GROUPS => false
497  ];
498 
499  $class = self::getClass( $dbType, $driver );
500 
501  return call_user_func( [ $class, 'getAttributes' ] ) + $defaults;
502  }
503 
510  private static function getClass( $dbType, $driver = null ) {
511  // For database types with built-in support, the below maps type to IDatabase
512  // implementations. For types with multiple driver implementations (PHP extensions),
513  // an array can be used, keyed by extension name. In case of an array, the
514  // optional 'driver' parameter can be used to force a specific driver. Otherwise,
515  // we auto-detect the first available driver. For types without built-in support,
516  // an class named "Database<Type>" us used, eg. DatabaseFoo for type 'foo'.
517  static $builtinTypes = [
518  'mysql' => [ 'mysqli' => DatabaseMysqli::class ],
519  'sqlite' => DatabaseSqlite::class,
520  'postgres' => DatabasePostgres::class,
521  ];
522 
523  $dbType = strtolower( $dbType );
524 
525  if ( !isset( $builtinTypes[$dbType] ) ) {
526  // Not a built in type, assume standard naming scheme
527  return 'Database' . ucfirst( $dbType );
528  }
529 
530  $class = false;
531  $possibleDrivers = $builtinTypes[$dbType];
532  if ( is_string( $possibleDrivers ) ) {
533  $class = $possibleDrivers;
534  } elseif ( (string)$driver !== '' ) {
535  if ( !isset( $possibleDrivers[$driver] ) ) {
536  throw new InvalidArgumentException( __METHOD__ .
537  " type '$dbType' does not support driver '{$driver}'" );
538  }
539 
540  $class = $possibleDrivers[$driver];
541  } else {
542  foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
543  if ( extension_loaded( $posDriver ) ) {
544  $class = $possibleClass;
545  break;
546  }
547  }
548  }
549 
550  if ( $class === false ) {
551  throw new InvalidArgumentException( __METHOD__ .
552  " no viable database extension found for type '$dbType'" );
553  }
554 
555  return $class;
556  }
557 
563  protected static function getAttributes() {
564  return [];
565  }
566 
574  public function setLogger( LoggerInterface $logger ) {
575  $this->queryLogger = $logger;
576  }
577 
578  public function getServerInfo() {
579  return $this->getServerVersion();
580  }
581 
582  public function getTopologyBasedServerId() {
583  return null;
584  }
585 
586  public function getTopologyRole() {
587  return $this->topologyRole;
588  }
589 
590  public function getTopologyRootPrimary() {
592  }
593 
594  public function getTopologyRootMaster() {
595  wfDeprecated( __METHOD__, '1.37' );
597  }
598 
599  final public function trxLevel() {
600  return ( $this->trxShortId != '' ) ? 1 : 0;
601  }
602 
603  public function trxTimestamp() {
604  return $this->trxLevel() ? $this->trxTimestamp : null;
605  }
606 
611  public function trxStatus() {
612  return $this->trxStatus;
613  }
614 
615  public function tablePrefix( $prefix = null ) {
616  $old = $this->currentDomain->getTablePrefix();
617 
618  if ( $prefix !== null ) {
619  $this->currentDomain = new DatabaseDomain(
620  $this->currentDomain->getDatabase(),
621  $this->currentDomain->getSchema(),
622  $prefix
623  );
624  }
625 
626  return $old;
627  }
628 
629  public function dbSchema( $schema = null ) {
630  $old = $this->currentDomain->getSchema();
631 
632  if ( $schema !== null ) {
633  if ( $schema !== '' && $this->getDBname() === null ) {
634  throw new DBUnexpectedError(
635  $this,
636  "Cannot set schema to '$schema'; no database set"
637  );
638  }
639 
640  $this->currentDomain = new DatabaseDomain(
641  $this->currentDomain->getDatabase(),
642  // DatabaseDomain uses null for unspecified schemas
643  ( $schema !== '' ) ? $schema : null,
644  $this->currentDomain->getTablePrefix()
645  );
646  }
647 
648  return (string)$old;
649  }
650 
655  protected function relationSchemaQualifier() {
656  return $this->dbSchema();
657  }
658 
659  public function getLBInfo( $name = null ) {
660  if ( $name === null ) {
661  return $this->lbInfo;
662  }
663 
664  if ( array_key_exists( $name, $this->lbInfo ) ) {
665  return $this->lbInfo[$name];
666  }
667 
668  return null;
669  }
670 
671  public function setLBInfo( $nameOrArray, $value = null ) {
672  if ( is_array( $nameOrArray ) ) {
673  $this->lbInfo = $nameOrArray;
674  } elseif ( is_string( $nameOrArray ) ) {
675  if ( $value !== null ) {
676  $this->lbInfo[$nameOrArray] = $value;
677  } else {
678  unset( $this->lbInfo[$nameOrArray] );
679  }
680  } else {
681  throw new InvalidArgumentException( "Got non-string key" );
682  }
683  }
684 
691  protected function getLazyMasterHandle() {
693  }
694 
699  public function implicitOrderby() {
700  return true;
701  }
702 
703  public function lastQuery() {
704  return $this->lastQuery;
705  }
706 
707  public function lastDoneWrites() {
708  return $this->lastWriteTime ?: false;
709  }
710 
711  public function writesPending() {
712  return $this->trxLevel() && $this->trxDoneWrites;
713  }
714 
715  public function writesOrCallbacksPending() {
716  return $this->trxLevel() && (
717  $this->trxDoneWrites ||
718  $this->trxPostCommitOrIdleCallbacks ||
719  $this->trxPreCommitOrIdleCallbacks ||
720  $this->trxEndCallbacks ||
722  );
723  }
724 
725  public function preCommitCallbacksPending() {
726  return $this->trxLevel() && $this->trxPreCommitOrIdleCallbacks;
727  }
728 
732  final protected function getTransactionRoundId() {
733  if ( $this->getFlag( self::DBO_TRX ) ) {
734  // LoadBalancer transaction round participation is enabled for this DB handle;
735  // get the ID of the active explicit transaction round (if any)
736  $id = $this->getLBInfo( self::LB_TRX_ROUND_ID );
737 
738  return is_string( $id ) ? $id : null;
739  }
740 
741  return null;
742  }
743 
744  public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
745  if ( !$this->trxLevel() ) {
746  return false;
747  } elseif ( !$this->trxDoneWrites ) {
748  return 0.0;
749  }
750 
751  switch ( $type ) {
752  case self::ESTIMATE_DB_APPLY:
753  return $this->pingAndCalculateLastTrxApplyTime();
754  default: // everything
756  }
757  }
758 
762  private function pingAndCalculateLastTrxApplyTime() {
763  // passed by reference
764  $rtt = null;
765  $this->ping( $rtt );
766 
767  $rttAdjTotal = $this->trxWriteAdjQueryCount * $rtt;
768  $applyTime = max( $this->trxWriteAdjDuration - $rttAdjTotal, 0 );
769  // For omitted queries, make them count as something at least
770  $omitted = $this->trxWriteQueryCount - $this->trxWriteAdjQueryCount;
771  $applyTime += self::$TINY_WRITE_SEC * $omitted;
772 
773  return $applyTime;
774  }
775 
776  public function pendingWriteCallers() {
777  return $this->trxLevel() ? $this->trxWriteCallers : [];
778  }
779 
780  public function pendingWriteRowsAffected() {
782  }
783 
792  public function pendingWriteAndCallbackCallers() {
793  $fnames = $this->pendingWriteCallers();
794  foreach ( [
795  $this->trxPostCommitOrIdleCallbacks,
796  $this->trxPreCommitOrIdleCallbacks,
797  $this->trxEndCallbacks,
798  $this->trxSectionCancelCallbacks
799  ] as $callbacks ) {
800  foreach ( $callbacks as $callback ) {
801  $fnames[] = $callback[1];
802  }
803  }
804 
805  return $fnames;
806  }
807 
811  private function flatAtomicSectionList() {
812  return array_reduce( $this->trxAtomicLevels, static function ( $accum, $v ) {
813  return $accum === null ? $v[0] : "$accum, " . $v[0];
814  } );
815  }
816 
817  public function isOpen() {
818  return (bool)$this->conn;
819  }
820 
821  public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
822  if ( $flag & ~static::$DBO_MUTABLE ) {
823  throw new DBUnexpectedError(
824  $this,
825  "Got $flag (allowed: " . implode( ', ', static::$MUTABLE_FLAGS ) . ')'
826  );
827  }
828 
829  if ( $remember === self::REMEMBER_PRIOR ) {
830  $this->priorFlags[] = $this->flags;
831  }
832 
833  $this->flags |= $flag;
834  }
835 
836  public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
837  if ( $flag & ~static::$DBO_MUTABLE ) {
838  throw new DBUnexpectedError(
839  $this,
840  "Got $flag (allowed: " . implode( ', ', static::$MUTABLE_FLAGS ) . ')'
841  );
842  }
843 
844  if ( $remember === self::REMEMBER_PRIOR ) {
845  $this->priorFlags[] = $this->flags;
846  }
847 
848  $this->flags &= ~$flag;
849  }
850 
851  public function restoreFlags( $state = self::RESTORE_PRIOR ) {
852  if ( !$this->priorFlags ) {
853  return;
854  }
855 
856  if ( $state === self::RESTORE_INITIAL ) {
857  $this->flags = reset( $this->priorFlags );
858  $this->priorFlags = [];
859  } else {
860  $this->flags = array_pop( $this->priorFlags );
861  }
862  }
863 
864  public function getFlag( $flag ) {
865  return ( ( $this->flags & $flag ) === $flag );
866  }
867 
868  public function getDomainID() {
869  return $this->currentDomain->getId();
870  }
871 
872  public function fetchObject( IResultWrapper $res ) {
873  return $res->fetchObject();
874  }
875 
876  public function fetchRow( IResultWrapper $res ) {
877  return $res->fetchRow();
878  }
879 
880  public function numRows( $res ) {
881  if ( is_bool( $res ) ) {
882  return 0;
883  } else {
884  return $res->numRows();
885  }
886  }
887 
888  public function numFields( IResultWrapper $res ) {
889  return count( $res->getFieldNames() );
890  }
891 
892  public function fieldName( IResultWrapper $res, $n ) {
893  return $res->getFieldNames()[$n];
894  }
895 
896  public function dataSeek( IResultWrapper $res, $pos ) {
897  $res->seek( $pos );
898  }
899 
900  public function freeResult( IResultWrapper $res ) {
901  $res->free();
902  }
903 
913  abstract public function indexInfo( $table, $index, $fname = __METHOD__ );
914 
922  abstract public function strencode( $s );
923 
927  protected function installErrorHandler() {
928  $this->lastPhpError = false;
929  $this->htmlErrors = ini_set( 'html_errors', '0' );
930  set_error_handler( [ $this, 'connectionErrorLogger' ] );
931  }
932 
938  protected function restoreErrorHandler() {
939  restore_error_handler();
940  if ( $this->htmlErrors !== false ) {
941  ini_set( 'html_errors', $this->htmlErrors );
942  }
943 
944  return $this->getLastPHPError();
945  }
946 
950  protected function getLastPHPError() {
951  if ( $this->lastPhpError ) {
952  $error = preg_replace( '!\[<a.*</a>\]!', '', $this->lastPhpError );
953  $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
954 
955  return $error;
956  }
957 
958  return false;
959  }
960 
969  public function connectionErrorLogger( $errno, $errstr ) {
970  $this->lastPhpError = $errstr;
971  }
972 
979  protected function getLogContext( array $extras = [] ) {
980  return array_merge(
981  [
982  'db_server' => $this->getServerName(),
983  'db_name' => $this->getDBname(),
984  'db_user' => $this->connectionParams[self::CONN_USER],
985  ],
986  $extras
987  );
988  }
989 
990  final public function close( $fname = __METHOD__, $owner = null ) {
991  $error = null; // error to throw after disconnecting
992 
993  $wasOpen = (bool)$this->conn;
994  // This should mostly do nothing if the connection is already closed
995  if ( $this->conn ) {
996  // Roll back any dangling transaction first
997  if ( $this->trxLevel() ) {
998  if ( $this->trxAtomicLevels ) {
999  // Cannot let incomplete atomic sections be committed
1000  $levels = $this->flatAtomicSectionList();
1001  $error = "$fname: atomic sections $levels are still open";
1002  } elseif ( $this->trxAutomatic ) {
1003  // Only the connection manager can commit non-empty DBO_TRX transactions
1004  // (empty ones we can silently roll back)
1005  if ( $this->writesOrCallbacksPending() ) {
1006  $error = "$fname: " .
1007  "expected mass rollback of all peer transactions (DBO_TRX set)";
1008  }
1009  } else {
1010  // Manual transactions should have been committed or rolled
1011  // back, even if empty.
1012  $error = "$fname: transaction is still open (from {$this->trxFname})";
1013  }
1014 
1015  if ( $this->trxEndCallbacksSuppressed && $error === null ) {
1016  $error = "$fname: callbacks are suppressed; cannot properly commit";
1017  }
1018 
1019  // Rollback the changes and run any callbacks as needed
1020  $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
1022  }
1023 
1024  // Close the actual connection in the binding handle
1025  $closed = $this->closeConnection();
1026  } else {
1027  $closed = true; // already closed; nothing to do
1028  }
1029 
1030  $this->conn = null;
1031 
1032  // Log or throw any unexpected errors after having disconnected
1033  if ( $error !== null ) {
1034  // T217819, T231443: if this is probably just LoadBalancer trying to recover from
1035  // errors and shutdown, then log any problems and move on since the request has to
1036  // end one way or another. Throwing errors is not very useful at some point.
1037  if ( $this->ownerId !== null && $owner === $this->ownerId ) {
1038  $this->queryLogger->error( $error );
1039  } else {
1040  throw new DBUnexpectedError( $this, $error );
1041  }
1042  }
1043 
1044  // Note that various subclasses call close() at the start of open(), which itself is
1045  // called by replaceLostConnection(). In that case, just because onTransactionResolution()
1046  // callbacks are pending does not mean that an exception should be thrown. Rather, they
1047  // will be executed after the reconnection step.
1048  if ( $wasOpen ) {
1049  // Sanity check that no callbacks are dangling
1050  $fnames = $this->pendingWriteAndCallbackCallers();
1051  if ( $fnames ) {
1052  throw new RuntimeException(
1053  "Transaction callbacks are still pending: " . implode( ', ', $fnames )
1054  );
1055  }
1056  }
1057 
1058  return $closed;
1059  }
1060 
1069  final protected function assertHasConnectionHandle() {
1070  if ( !$this->isOpen() ) {
1071  throw new DBUnexpectedError( $this, "DB connection was already closed" );
1072  }
1073  }
1074 
1081  protected function assertIsWritablePrimary() {
1082  $info = $this->getReadOnlyReason();
1083  if ( $info ) {
1084  list( $reason, $source ) = $info;
1085  if ( $source === 'role' ) {
1086  throw new DBReadOnlyRoleError( $this, "Database is read-only: $reason" );
1087  } else {
1088  throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
1089  }
1090  }
1091  }
1092 
1097  protected function assertIsWritableMaster() {
1098  wfDeprecated( __METHOD__, '1.37' );
1099  $this->assertIsWritablePrimary();
1100  }
1101 
1107  abstract protected function closeConnection();
1108 
1135  abstract protected function doQuery( $sql );
1136 
1154  protected function isWriteQuery( $sql, $flags ) {
1155  if (
1156  $this->fieldHasBit( $flags, self::QUERY_CHANGE_ROWS ) ||
1157  $this->fieldHasBit( $flags, self::QUERY_CHANGE_SCHEMA )
1158  ) {
1159  return true;
1160  } elseif ( $this->fieldHasBit( $flags, self::QUERY_CHANGE_NONE ) ) {
1161  return false;
1162  }
1163  // BEGIN and COMMIT queries are considered read queries here.
1164  // Database backends and drivers (MySQL, MariaDB, php-mysqli) generally
1165  // treat these as write queries, in that their results have "affected rows"
1166  // as meta data as from writes, instead of "num rows" as from reads.
1167  // But, we treat them as read queries because when reading data (from
1168  // either replica or primary DB) we use transactions to enable repeatable-read
1169  // snapshots, which ensures we get consistent results from the same snapshot
1170  // for all queries within a request. Use cases:
1171  // - Treating these as writes would trigger ChronologyProtector (see method doc).
1172  // - We use this method to reject writes to replicas, but we need to allow
1173  // use of transactions on replicas for read snapshots. This is fine given
1174  // that transactions by themselves don't make changes, only actual writes
1175  // within the transaction matter, which we still detect.
1176  return !preg_match(
1177  '/^\s*(?:SELECT|BEGIN|ROLLBACK|COMMIT|SAVEPOINT|RELEASE|SET|SHOW|EXPLAIN|USE|\‍(SELECT)\b/i',
1178  $sql
1179  );
1180  }
1181 
1186  protected function getQueryVerb( $sql ) {
1187  return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
1188  }
1189 
1204  protected function isTransactableQuery( $sql ) {
1205  return !in_array(
1206  $this->getQueryVerb( $sql ),
1207  [ 'BEGIN', 'ROLLBACK', 'COMMIT', 'SET', 'SHOW', 'CREATE', 'ALTER', 'USE', 'SHOW' ],
1208  true
1209  );
1210  }
1211 
1220  protected function getTempTableWrites( $sql, $pseudoPermanent ) {
1221  // Regexes for basic queries that can create/change/drop temporary tables.
1222  // For simplicity, this only looks for tables with sane, alphanumeric, names;
1223  // temporary tables only need simple programming names anyway.
1224  static $regexes = null;
1225  if ( $regexes === null ) {
1226  // Regex with a group for quoted table 0 and a group for quoted tables 1..N
1227  $qts = '((?:\w+|`\w+`|\'\w+\'|"\w+")(?:\s*,\s*(?:\w+|`\w+`|\'\w+\'|"\w+"))*)';
1228  // Regex to get query verb, table 0, and tables 1..N
1229  $regexes = [
1230  // DML write queries
1231  "/^(INSERT|REPLACE)\s+(?:\w+\s+)*?INTO\s+$qts/i",
1232  "/^(UPDATE)(?:\s+OR\s+\w+|\s+IGNORE|\s+ONLY)?\s+$qts/i",
1233  "/^(DELETE)\s+(?:\w+\s+)*?FROM(?:\s+ONLY)?\s+$qts/i",
1234  // DDL write queries
1235  "/^(CREATE)\s+TEMPORARY\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+$qts/i",
1236  "/^(DROP)\s+(?:TEMPORARY\s+)?TABLE(?:\s+IF\s+EXISTS)?\s+$qts/i",
1237  "/^(TRUNCATE)\s+(?:TEMPORARY\s+)?TABLE\s+$qts/i",
1238  "/^(ALTER)\s+TABLE\s+$qts/i"
1239  ];
1240  }
1241 
1242  $queryVerb = null;
1243  $queryTables = [];
1244  foreach ( $regexes as $regex ) {
1245  if ( preg_match( $regex, $sql, $m, PREG_UNMATCHED_AS_NULL ) ) {
1246  $queryVerb = $m[1];
1247  $allTables = preg_split( '/\s*,\s*/', $m[2] );
1248  foreach ( $allTables as $quotedTable ) {
1249  $queryTables[] = trim( $quotedTable, "\"'`" );
1250  }
1251  break;
1252  }
1253  }
1254 
1255  $tempTableChanges = [];
1256  foreach ( $queryTables as $table ) {
1257  if ( $queryVerb === 'CREATE' ) {
1258  // Record the type of temporary table being created
1259  $tableType = $pseudoPermanent ? self::$TEMP_PSEUDO_PERMANENT : self::$TEMP_NORMAL;
1260  } else {
1261  $tableType = $this->sessionTempTables[$table] ?? null;
1262  }
1263 
1264  if ( $tableType !== null ) {
1265  $tempTableChanges[] = [ $tableType, $queryVerb, $table ];
1266  }
1267  }
1268 
1269  return $tempTableChanges;
1270  }
1271 
1276  protected function registerTempWrites( $ret, array $changes ) {
1277  if ( $ret === false ) {
1278  return;
1279  }
1280 
1281  foreach ( $changes as list( $tmpTableType, $verb, $table ) ) {
1282  switch ( $verb ) {
1283  case 'CREATE':
1284  $this->sessionTempTables[$table] = $tmpTableType;
1285  break;
1286  case 'DROP':
1287  unset( $this->sessionTempTables[$table] );
1288  unset( $this->sessionDirtyTempTables[$table] );
1289  break;
1290  case 'TRUNCATE':
1291  unset( $this->sessionDirtyTempTables[$table] );
1292  break;
1293  default:
1294  $this->sessionDirtyTempTables[$table] = 1;
1295  break;
1296  }
1297  }
1298  }
1299 
1307  protected function isPristineTemporaryTable( $table ) {
1308  $rawTable = $this->tableName( $table, 'raw' );
1309 
1310  return (
1311  isset( $this->sessionTempTables[$rawTable] ) &&
1312  !isset( $this->sessionDirtyTempTables[$rawTable] )
1313  );
1314  }
1315 
1316  public function query( $sql, $fname = __METHOD__, $flags = self::QUERY_NORMAL ) {
1317  $flags = (int)$flags; // b/c; this field used to be a bool
1318  // Sanity check that the SQL query is appropriate in the current context and is
1319  // allowed for an outside caller (e.g. does not break transaction/session tracking).
1320  $this->assertQueryIsCurrentlyAllowed( $sql, $fname );
1321 
1322  // Send the query to the server and fetch any corresponding errors
1323  list( $ret, $err, $errno, $unignorable ) = $this->executeQuery( $sql, $fname, $flags );
1324  if ( $ret === false ) {
1325  $ignoreErrors = $this->fieldHasBit( $flags, self::QUERY_SILENCE_ERRORS );
1326  // Throw an error unless both the ignore flag was set and a rollback is not needed
1327  $this->reportQueryError( $err, $errno, $sql, $fname, $ignoreErrors && !$unignorable );
1328  }
1329 
1330  return $ret;
1331  }
1332 
1353  final protected function executeQuery( $sql, $fname, $flags ) {
1354  $this->assertHasConnectionHandle();
1355 
1356  $priorTransaction = $this->trxLevel();
1357 
1358  if ( $this->isWriteQuery( $sql, $flags ) ) {
1359  // Do not treat temporary table writes as "meaningful writes" since they are only
1360  // visible to one session and are not permanent. Profile them as reads. Integration
1361  // tests can override this behavior via $flags.
1362  $pseudoPermanent = $this->fieldHasBit( $flags, self::QUERY_PSEUDO_PERMANENT );
1363  $tempTableChanges = $this->getTempTableWrites( $sql, $pseudoPermanent );
1364  $isPermWrite = !$tempTableChanges;
1365  foreach ( $tempTableChanges as list( $tmpType ) ) {
1366  $isPermWrite = $isPermWrite || ( $tmpType !== self::$TEMP_NORMAL );
1367  }
1368 
1369  // Permit temporary table writes on replica DB connections
1370  // but require a writable primary DB connection for any persistent writes.
1371  if ( $isPermWrite ) {
1372  $this->assertIsWritablePrimary();
1373 
1374  // DBConnRef uses QUERY_REPLICA_ROLE to enforce the replica role for raw SQL queries
1375  if ( $this->fieldHasBit( $flags, self::QUERY_REPLICA_ROLE ) ) {
1376  throw new DBReadOnlyRoleError( $this, "Cannot write; target role is DB_REPLICA" );
1377  }
1378  }
1379  } else {
1380  // No permanent writes in this query
1381  $isPermWrite = false;
1382  // No temporary tables written to either
1383  $tempTableChanges = [];
1384  }
1385 
1386  // Add trace comment to the begin of the sql string, right after the operator.
1387  // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598).
1388  $encName = preg_replace( '/[\x00-\x1F\/]/', '-', "$fname {$this->agent}" );
1389  $commentedSql = preg_replace( '/\s|$/', " /* $encName */ ", $sql, 1 );
1390 
1391  $corruptedTrx = false;
1392 
1393  $cs = $this->commenceCriticalSection( __METHOD__ );
1394 
1395  // Send the query to the server and fetch any corresponding errors.
1396  // This also doubles as a "ping" to see if the connection was dropped.
1397  list( $ret, $err, $errno, $recoverableSR, $recoverableCL, $reconnected ) =
1398  $this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags );
1399 
1400  // Check if the query failed due to a recoverable connection loss
1401  $allowRetry = !$this->fieldHasBit( $flags, self::QUERY_NO_RETRY );
1402  if ( $ret === false && $recoverableCL && $reconnected && $allowRetry ) {
1403  // Silently resend the query to the server since it is safe and possible
1404  list( $ret, $err, $errno, $recoverableSR, $recoverableCL ) =
1405  $this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags );
1406  }
1407 
1408  // Register creation and dropping of temporary tables
1409  $this->registerTempWrites( $ret, $tempTableChanges );
1410 
1411  if ( $ret === false && $priorTransaction ) {
1412  if ( $recoverableSR ) {
1413  # We're ignoring an error that caused just the current query to be aborted.
1414  # But log the cause so we can log a deprecation notice if a caller actually
1415  # does ignore it.
1416  $this->trxStatusIgnoredCause = [ $err, $errno, $fname ];
1417  } elseif ( !$recoverableCL ) {
1418  # Either the query was aborted or all queries after BEGIN where aborted.
1419  # In the first case, the only options going forward are (a) ROLLBACK, or
1420  # (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
1421  # option is ROLLBACK, since the snapshots would have been released.
1422  $corruptedTrx = true; // cannot recover
1423  $trxError = $this->getQueryException( $err, $errno, $sql, $fname );
1424  $this->setTransactionError( $trxError );
1425  }
1426  }
1427 
1428  $this->completeCriticalSection( __METHOD__, $cs );
1429 
1430  return [ $ret, $err, $errno, $corruptedTrx ];
1431  }
1432 
1451  private function executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags ) {
1452  $priorWritesPending = $this->writesOrCallbacksPending();
1453 
1454  if ( ( $flags & self::QUERY_IGNORE_DBO_TRX ) == 0 ) {
1455  $this->beginIfImplied( $sql, $fname );
1456  }
1457 
1458  // Keep track of whether the transaction has write queries pending
1459  if ( $isPermWrite ) {
1460  $this->lastWriteTime = microtime( true );
1461  if ( $this->trxLevel() && !$this->trxDoneWrites ) {
1462  $this->trxDoneWrites = true;
1463  $this->trxProfiler->transactionWritingIn(
1464  $this->getServerName(),
1465  $this->getDomainID(),
1466  $this->trxShortId
1467  );
1468  }
1469  }
1470 
1471  $prefix = $this->topologyRole ? 'query-m: ' : 'query: ';
1472  $generalizedSql = new GeneralizedSql( $sql, $this->trxShortId, $prefix );
1473 
1474  $startTime = microtime( true );
1475  $ps = $this->profiler
1476  ? ( $this->profiler )( $generalizedSql->stringify() )
1477  : null;
1478  $this->affectedRowCount = null;
1479  $this->lastQuery = $sql;
1480  $ret = $this->doQuery( $commentedSql );
1481  $lastError = $this->lastError();
1482  $lastErrno = $this->lastErrno();
1483 
1484  $this->affectedRowCount = $this->affectedRows();
1485  unset( $ps ); // profile out (if set)
1486  $queryRuntime = max( microtime( true ) - $startTime, 0.0 );
1487 
1488  $recoverableSR = false; // recoverable statement rollback?
1489  $recoverableCL = false; // recoverable connection loss?
1490  $reconnected = false; // reconnection both attempted and succeeded?
1491 
1492  if ( $ret !== false ) {
1493  $this->lastPing = $startTime;
1494  if ( $isPermWrite && $this->trxLevel() ) {
1495  $this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() );
1496  $this->trxWriteCallers[] = $fname;
1497  }
1498  } elseif ( $this->wasConnectionError( $lastErrno ) ) {
1499  # Check if no meaningful session state was lost
1500  $recoverableCL = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
1501  # Update session state tracking and try to restore the connection
1502  $reconnected = $this->replaceLostConnection( __METHOD__ );
1503  } else {
1504  # Check if only the last query was rolled back
1505  $recoverableSR = $this->wasKnownStatementRollbackError();
1506  }
1507 
1508  if ( $sql === self::$PING_QUERY ) {
1509  $this->lastRoundTripEstimate = $queryRuntime;
1510  }
1511 
1512  $this->trxProfiler->recordQueryCompletion(
1513  $generalizedSql,
1514  $startTime,
1515  $isPermWrite,
1516  $isPermWrite ? $this->affectedRows() : $this->numRows( $ret )
1517  );
1518 
1519  // Avoid the overhead of logging calls unless debug mode is enabled
1520  if ( $this->getFlag( self::DBO_DEBUG ) ) {
1521  $this->queryLogger->debug(
1522  "{method} [{runtime}s] {db_server}: {sql}",
1523  $this->getLogContext( [
1524  'method' => $fname,
1525  'sql' => $sql,
1526  'domain' => $this->getDomainID(),
1527  'runtime' => round( $queryRuntime, 3 )
1528  ] )
1529  );
1530  }
1531 
1532  if ( !is_bool( $ret ) && $ret !== null && !( $ret instanceof IResultWrapper ) ) {
1533  throw new DBUnexpectedError( $this,
1534  static::class . '::doQuery() should return an IResultWrapper' );
1535  }
1536 
1537  return [ $ret, $lastError, $lastErrno, $recoverableSR, $recoverableCL, $reconnected ];
1538  }
1539 
1546  private function beginIfImplied( $sql, $fname ) {
1547  if (
1548  !$this->trxLevel() &&
1549  $this->getFlag( self::DBO_TRX ) &&
1550  $this->isTransactableQuery( $sql )
1551  ) {
1552  $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
1553  $this->trxAutomatic = true;
1554  }
1555  }
1556 
1569  private function updateTrxWriteQueryTime( $sql, $runtime, $affected ) {
1570  // Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
1571  $indicativeOfReplicaRuntime = true;
1572  if ( $runtime > self::$SLOW_WRITE_SEC ) {
1573  $verb = $this->getQueryVerb( $sql );
1574  // insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
1575  if ( $verb === 'INSERT' ) {
1576  $indicativeOfReplicaRuntime = $this->affectedRows() > self::$SMALL_WRITE_ROWS;
1577  } elseif ( $verb === 'REPLACE' ) {
1578  $indicativeOfReplicaRuntime = $this->affectedRows() > self::$SMALL_WRITE_ROWS / 2;
1579  }
1580  }
1581 
1582  $this->trxWriteDuration += $runtime;
1583  $this->trxWriteQueryCount += 1;
1584  $this->trxWriteAffectedRows += $affected;
1585  if ( $indicativeOfReplicaRuntime ) {
1586  $this->trxWriteAdjDuration += $runtime;
1587  $this->trxWriteAdjQueryCount += 1;
1588  }
1589  }
1590 
1599  private function assertQueryIsCurrentlyAllowed( $sql, $fname ) {
1600  $verb = $this->getQueryVerb( $sql );
1601  if ( $verb === 'USE' ) {
1602  throw new DBUnexpectedError( $this, "Got USE query; use selectDomain() instead" );
1603  }
1604 
1605  if ( $verb === 'ROLLBACK' ) { // transaction/savepoint
1606  return;
1607  }
1608 
1609  if ( $this->csmError ) {
1610  throw new DBTransactionStateError(
1611  $this,
1612  "Cannot execute query from $fname while session state is out of sync.\n\n" .
1613  $this->csmError->getMessage() . "\n" .
1614  $this->csmError->getTraceAsString()
1615  );
1616  }
1617 
1618  if ( $this->trxStatus < self::STATUS_TRX_OK ) {
1619  throw new DBTransactionStateError(
1620  $this,
1621  "Cannot execute query from $fname while transaction status is ERROR",
1622  [],
1623  $this->trxStatusCause
1624  );
1625  } elseif ( $this->trxStatus === self::STATUS_TRX_OK && $this->trxStatusIgnoredCause ) {
1626  list( $iLastError, $iLastErrno, $iFname ) = $this->trxStatusIgnoredCause;
1627  call_user_func( $this->deprecationLogger,
1628  "Caller from $fname ignored an error originally raised from $iFname: " .
1629  "[$iLastErrno] $iLastError"
1630  );
1631  $this->trxStatusIgnoredCause = null;
1632  }
1633  }
1634 
1635  public function assertNoOpenTransactions() {
1636  if ( $this->explicitTrxActive() ) {
1637  throw new DBTransactionError(
1638  $this,
1639  "Explicit transaction still active. A caller may have caught an error. "
1640  . "Open transactions: " . $this->flatAtomicSectionList()
1641  );
1642  }
1643  }
1644 
1654  private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
1655  # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
1656  # Dropped connections also mean that named locks are automatically released.
1657  # Only allow error suppression in autocommit mode or when the lost transaction
1658  # didn't matter anyway (aside from DBO_TRX snapshot loss).
1659  if ( $this->sessionNamedLocks ) {
1660  return false; // possible critical section violation
1661  } elseif ( $this->sessionTempTables ) {
1662  return false; // tables might be queried latter
1663  } elseif ( $sql === 'COMMIT' ) {
1664  return !$priorWritesPending; // nothing written anyway? (T127428)
1665  } elseif ( $sql === 'ROLLBACK' ) {
1666  return true; // transaction lost...which is also what was requested :)
1667  } elseif ( $this->explicitTrxActive() ) {
1668  return false; // don't drop atomicity and explicit snapshots
1669  } elseif ( $priorWritesPending ) {
1670  return false; // prior writes lost from implicit transaction
1671  }
1672 
1673  return true;
1674  }
1675 
1679  private function handleSessionLossPreconnect() {
1680  // Clean up tracking of session-level things...
1681  // https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html
1682  // https://www.postgresql.org/docs/9.2/static/sql-createtable.html (ignoring ON COMMIT)
1683  $this->sessionTempTables = [];
1684  $this->sessionDirtyTempTables = [];
1685  // https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
1686  // https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
1687  $this->sessionNamedLocks = [];
1688  // Session loss implies transaction loss
1689  $oldTrxShortId = $this->consumeTrxShortId();
1690  $this->trxAtomicCounter = 0;
1691  $this->trxPostCommitOrIdleCallbacks = []; // T67263; transaction already lost
1692  $this->trxPreCommitOrIdleCallbacks = []; // T67263; transaction already lost
1693  // Clear additional subclass fields
1695  // @note: leave trxRecurringCallbacks in place
1696  if ( $this->trxDoneWrites ) {
1697  $this->trxProfiler->transactionWritingOut(
1698  $this->getServerName(),
1699  $this->getDomainID(),
1700  $oldTrxShortId,
1701  $this->pendingWriteQueryDuration( self::ESTIMATE_TOTAL ),
1702  $this->trxWriteAffectedRows
1703  );
1704  }
1705  }
1706 
1711  protected function doHandleSessionLossPreconnect() {
1712  // no-op
1713  }
1714 
1718  private function handleSessionLossPostconnect() {
1719  // Handle callbacks in trxEndCallbacks, e.g. onTransactionResolution().
1720  // If callback suppression is set then the array will remain unhandled.
1721  $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
1722  // Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener()
1723  $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
1724  }
1725 
1731  private function consumeTrxShortId() {
1732  $old = $this->trxShortId;
1733  $this->trxShortId = '';
1734 
1735  return $old;
1736  }
1737 
1749  protected function wasQueryTimeout( $error, $errno ) {
1750  return false;
1751  }
1752 
1764  public function reportQueryError( $error, $errno, $sql, $fname, $ignore = false ) {
1765  if ( $ignore ) {
1766  $this->queryLogger->debug( "SQL ERROR (ignored): $error" );
1767  } else {
1768  throw $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
1769  }
1770  }
1771 
1779  private function getQueryExceptionAndLog( $error, $errno, $sql, $fname ) {
1780  // Information that instances of the same problem have in common should
1781  // not be normalized (T255202).
1782  $this->queryLogger->error(
1783  "Error $errno from $fname, {error} {sql1line} {db_server}",
1784  $this->getLogContext( [
1785  'method' => __METHOD__,
1786  'errno' => $errno,
1787  'error' => $error,
1788  'sql1line' => mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 ),
1789  'fname' => $fname,
1790  'exception' => new RuntimeException()
1791  ] )
1792  );
1793  return $this->getQueryException( $error, $errno, $sql, $fname );
1794  }
1795 
1803  private function getQueryException( $error, $errno, $sql, $fname ) {
1804  if ( $this->wasQueryTimeout( $error, $errno ) ) {
1805  return new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
1806  } elseif ( $this->wasConnectionError( $errno ) ) {
1807  return new DBQueryDisconnectedError( $this, $error, $errno, $sql, $fname );
1808  } else {
1809  return new DBQueryError( $this, $error, $errno, $sql, $fname );
1810  }
1811  }
1812 
1817  final protected function newExceptionAfterConnectError( $error ) {
1818  // Connection was not fully initialized and is not safe for use
1819  $this->conn = null;
1820 
1821  $this->connLogger->error(
1822  "Error connecting to {db_server} as user {db_user}: {error}",
1823  $this->getLogContext( [
1824  'error' => $error,
1825  'exception' => new RuntimeException()
1826  ] )
1827  );
1828 
1829  return new DBConnectionError( $this, $error );
1830  }
1831 
1836  return new SelectQueryBuilder( $this );
1837  }
1838 
1839  public function selectField(
1840  $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1841  ) {
1842  if ( $var === '*' ) { // sanity
1843  throw new DBUnexpectedError( $this, "Cannot use a * field" );
1844  } elseif ( is_array( $var ) && count( $var ) !== 1 ) {
1845  throw new DBUnexpectedError( $this, 'Cannot use more than one field' );
1846  }
1847 
1848  $options = $this->normalizeOptions( $options );
1849  $options['LIMIT'] = 1;
1850 
1851  $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1852  if ( $res === false ) {
1853  throw new DBUnexpectedError( $this, "Got false from select()" );
1854  }
1855 
1856  $row = $this->fetchRow( $res );
1857  if ( $row === false ) {
1858  return false;
1859  }
1860 
1861  return reset( $row );
1862  }
1863 
1864  public function selectFieldValues(
1865  $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1866  ): array {
1867  if ( $var === '*' ) { // sanity
1868  throw new DBUnexpectedError( $this, "Cannot use a * field" );
1869  } elseif ( !is_string( $var ) ) { // sanity
1870  throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1871  }
1872 
1873  $options = $this->normalizeOptions( $options );
1874  $res = $this->select( $table, [ 'value' => $var ], $cond, $fname, $options, $join_conds );
1875  if ( $res === false ) {
1876  throw new DBUnexpectedError( $this, "Got false from select()" );
1877  }
1878 
1879  $values = [];
1880  foreach ( $res as $row ) {
1881  $values[] = $row->value;
1882  }
1883 
1884  return $values;
1885  }
1886 
1898  protected function makeSelectOptions( array $options ) {
1899  $preLimitTail = $postLimitTail = '';
1900  $startOpts = '';
1901 
1902  $noKeyOptions = [];
1903 
1904  foreach ( $options as $key => $option ) {
1905  if ( is_numeric( $key ) ) {
1906  $noKeyOptions[$option] = true;
1907  }
1908  }
1909 
1910  $preLimitTail .= $this->makeGroupByWithHaving( $options );
1911 
1912  $preLimitTail .= $this->makeOrderBy( $options );
1913 
1914  if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1915  $postLimitTail .= ' FOR UPDATE';
1916  }
1917 
1918  if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1919  $postLimitTail .= ' LOCK IN SHARE MODE';
1920  }
1921 
1922  if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1923  $startOpts .= 'DISTINCT';
1924  }
1925 
1926  # Various MySQL extensions
1927  if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1928  $startOpts .= ' /*! STRAIGHT_JOIN */';
1929  }
1930 
1931  if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1932  $startOpts .= ' SQL_BIG_RESULT';
1933  }
1934 
1935  if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1936  $startOpts .= ' SQL_BUFFER_RESULT';
1937  }
1938 
1939  if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1940  $startOpts .= ' SQL_SMALL_RESULT';
1941  }
1942 
1943  if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1944  $startOpts .= ' SQL_CALC_FOUND_ROWS';
1945  }
1946 
1947  if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1948  $useIndex = $this->useIndexClause( $options['USE INDEX'] );
1949  } else {
1950  $useIndex = '';
1951  }
1952  if ( isset( $options['IGNORE INDEX'] ) && is_string( $options['IGNORE INDEX'] ) ) {
1953  $ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
1954  } else {
1955  $ignoreIndex = '';
1956  }
1957 
1958  return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1959  }
1960 
1969  protected function makeGroupByWithHaving( $options ) {
1970  $sql = '';
1971  if ( isset( $options['GROUP BY'] ) ) {
1972  $gb = is_array( $options['GROUP BY'] )
1973  ? implode( ',', $options['GROUP BY'] )
1974  : $options['GROUP BY'];
1975  $sql .= ' GROUP BY ' . $gb;
1976  }
1977  if ( isset( $options['HAVING'] ) ) {
1978  $having = is_array( $options['HAVING'] )
1979  ? $this->makeList( $options['HAVING'], self::LIST_AND )
1980  : $options['HAVING'];
1981  $sql .= ' HAVING ' . $having;
1982  }
1983 
1984  return $sql;
1985  }
1986 
1995  protected function makeOrderBy( $options ) {
1996  if ( isset( $options['ORDER BY'] ) ) {
1997  $ob = is_array( $options['ORDER BY'] )
1998  ? implode( ',', $options['ORDER BY'] )
1999  : $options['ORDER BY'];
2000 
2001  return ' ORDER BY ' . $ob;
2002  }
2003 
2004  return '';
2005  }
2006 
2007  public function select(
2008  $table, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
2009  ) {
2010  $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
2011 
2012  return $this->query( $sql, $fname, self::QUERY_CHANGE_NONE );
2013  }
2014 
2019  public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
2020  $options = [], $join_conds = []
2021  ) {
2022  if ( is_array( $vars ) ) {
2023  $fields = implode( ',', $this->fieldNamesWithAlias( $vars ) );
2024  } else {
2025  $fields = $vars;
2026  }
2027 
2028  $options = (array)$options;
2029  $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
2030  ? $options['USE INDEX']
2031  : [];
2032  $ignoreIndexes = (
2033  isset( $options['IGNORE INDEX'] ) &&
2034  is_array( $options['IGNORE INDEX'] )
2035  )
2036  ? $options['IGNORE INDEX']
2037  : [];
2038 
2039  if (
2040  $this->selectOptionsIncludeLocking( $options ) &&
2041  $this->selectFieldsOrOptionsAggregate( $vars, $options )
2042  ) {
2043  // Some DB types (e.g. postgres) disallow FOR UPDATE with aggregate
2044  // functions. Discourage use of such queries to encourage compatibility.
2045  call_user_func(
2046  $this->deprecationLogger,
2047  __METHOD__ . ": aggregation used with a locking SELECT ($fname)"
2048  );
2049  }
2050 
2051  if ( is_array( $table ) ) {
2052  if ( count( $table ) === 0 ) {
2053  $from = '';
2054  } else {
2055  $from = ' FROM ' .
2057  $table, $useIndexes, $ignoreIndexes, $join_conds );
2058  }
2059  } elseif ( $table != '' ) {
2060  $from = ' FROM ' .
2062  [ $table ], $useIndexes, $ignoreIndexes, [] );
2063  } else {
2064  $from = '';
2065  }
2066 
2067  list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
2068  $this->makeSelectOptions( $options );
2069 
2070  if ( is_array( $conds ) ) {
2071  $conds = $this->makeList( $conds, self::LIST_AND );
2072  }
2073 
2074  if ( $conds === null || $conds === false ) {
2075  $this->queryLogger->warning(
2076  __METHOD__
2077  . ' called from '
2078  . $fname
2079  . ' with incorrect parameters: $conds must be a string or an array'
2080  );
2081  $conds = '';
2082  }
2083 
2084  if ( $conds === '' || $conds === '*' ) {
2085  $sql = "SELECT $startOpts $fields $from $useIndex $ignoreIndex $preLimitTail";
2086  } elseif ( is_string( $conds ) ) {
2087  $sql = "SELECT $startOpts $fields $from $useIndex $ignoreIndex " .
2088  "WHERE $conds $preLimitTail";
2089  } else {
2090  throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
2091  }
2092 
2093  if ( isset( $options['LIMIT'] ) ) {
2094  $sql = $this->limitResult( $sql, $options['LIMIT'],
2095  $options['OFFSET'] ?? false );
2096  }
2097  $sql = "$sql $postLimitTail";
2098 
2099  if ( isset( $options['EXPLAIN'] ) ) {
2100  $sql = 'EXPLAIN ' . $sql;
2101  }
2102 
2103  return $sql;
2104  }
2105 
2106  public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
2107  $options = [], $join_conds = []
2108  ) {
2109  $options = (array)$options;
2110  $options['LIMIT'] = 1;
2111 
2112  $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
2113  if ( $res === false ) {
2114  throw new DBUnexpectedError( $this, "Got false from select()" );
2115  }
2116 
2117  if ( !$this->numRows( $res ) ) {
2118  return false;
2119  }
2120 
2121  return $this->fetchObject( $res );
2122  }
2123 
2128  public function estimateRowCount(
2129  $tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
2130  ) {
2131  $conds = $this->normalizeConditions( $conds, $fname );
2132  $column = $this->extractSingleFieldFromList( $var );
2133  if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
2134  $conds[] = "$column IS NOT NULL";
2135  }
2136 
2137  $res = $this->select(
2138  $tables, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options, $join_conds
2139  );
2140  $row = $res ? $this->fetchRow( $res ) : [];
2141 
2142  return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
2143  }
2144 
2145  public function selectRowCount(
2146  $tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
2147  ) {
2148  $conds = $this->normalizeConditions( $conds, $fname );
2149  $column = $this->extractSingleFieldFromList( $var );
2150  if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
2151  $conds[] = "$column IS NOT NULL";
2152  }
2153 
2154  $res = $this->select(
2155  [
2156  'tmp_count' => $this->buildSelectSubquery(
2157  $tables,
2158  '1',
2159  $conds,
2160  $fname,
2161  $options,
2162  $join_conds
2163  )
2164  ],
2165  [ 'rowcount' => 'COUNT(*)' ],
2166  [],
2167  $fname
2168  );
2169  $row = $res ? $this->fetchRow( $res ) : [];
2170 
2171  return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
2172  }
2173 
2178  private function selectOptionsIncludeLocking( $options ) {
2179  $options = (array)$options;
2180  foreach ( [ 'FOR UPDATE', 'LOCK IN SHARE MODE' ] as $lock ) {
2181  if ( in_array( $lock, $options, true ) ) {
2182  return true;
2183  }
2184  }
2185 
2186  return false;
2187  }
2188 
2194  private function selectFieldsOrOptionsAggregate( $fields, $options ) {
2195  foreach ( (array)$options as $key => $value ) {
2196  if ( is_string( $key ) ) {
2197  if ( preg_match( '/^(?:GROUP BY|HAVING)$/i', $key ) ) {
2198  return true;
2199  }
2200  } elseif ( is_string( $value ) ) {
2201  if ( preg_match( '/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
2202  return true;
2203  }
2204  }
2205  }
2206 
2207  $regex = '/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
2208  foreach ( (array)$fields as $field ) {
2209  if ( is_string( $field ) && preg_match( $regex, $field ) ) {
2210  return true;
2211  }
2212  }
2213 
2214  return false;
2215  }
2216 
2222  final protected function normalizeRowArray( array $rowOrRows ) {
2223  if ( !$rowOrRows ) {
2224  $rows = [];
2225  } elseif ( isset( $rowOrRows[0] ) ) {
2226  $rows = $rowOrRows;
2227  } else {
2228  $rows = [ $rowOrRows ];
2229  }
2230 
2231  foreach ( $rows as $row ) {
2232  if ( !is_array( $row ) ) {
2233  throw new DBUnexpectedError( $this, "Got non-array in row array" );
2234  } elseif ( !$row ) {
2235  throw new DBUnexpectedError( $this, "Got empty array in row array" );
2236  }
2237  }
2238 
2239  return $rows;
2240  }
2241 
2248  final protected function normalizeConditions( $conds, $fname ) {
2249  if ( $conds === null || $conds === false ) {
2250  $this->queryLogger->warning(
2251  __METHOD__
2252  . ' called from '
2253  . $fname
2254  . ' with incorrect parameters: $conds must be a string or an array'
2255  );
2256  return [];
2257  } elseif ( $conds === '' ) {
2258  return [];
2259  }
2260 
2261  return is_array( $conds ) ? $conds : [ $conds ];
2262  }
2263 
2273  final protected function normalizeUpsertParams( $uniqueKeys, &$rows ) {
2274  $rows = $this->normalizeRowArray( $rows );
2275  if ( !$rows ) {
2276  return null;
2277  }
2278  if ( !$uniqueKeys ) {
2279  // For backwards compatibility, allow insertion of rows with no applicable key
2280  $this->queryLogger->warning(
2281  "upsert/replace called with no unique key",
2282  [ 'exception' => new RuntimeException() ]
2283  );
2284  return null;
2285  }
2286  $identityKey = $this->normalizeUpsertKeys( $uniqueKeys );
2287  if ( $identityKey ) {
2288  $allDefaultKeyValues = $this->assertValidUpsertRowArray( $rows, $identityKey );
2289  if ( $allDefaultKeyValues ) {
2290  // For backwards compatibility, allow insertion of rows with all-NULL
2291  // values for the unique columns (e.g. for an AUTOINCREMENT column)
2292  $this->queryLogger->warning(
2293  "upsert/replace called with all-null values for unique key",
2294  [ 'exception' => new RuntimeException() ]
2295  );
2296  return null;
2297  }
2298  }
2299  return $identityKey;
2300  }
2301 
2308  private function normalizeUpsertKeys( $uniqueKeys ) {
2309  if ( is_string( $uniqueKeys ) ) {
2310  return [ $uniqueKeys ];
2311  } elseif ( !is_array( $uniqueKeys ) ) {
2312  throw new DBUnexpectedError( $this, 'Invalid unique key array' );
2313  } else {
2314  if ( count( $uniqueKeys ) !== 1 || !isset( $uniqueKeys[0] ) ) {
2315  throw new DBUnexpectedError( $this,
2316  "The unique key array should contain a single unique index" );
2317  }
2318 
2319  $uniqueKey = $uniqueKeys[0];
2320  if ( is_string( $uniqueKey ) ) {
2321  // Passing a list of strings for single-column unique keys is too
2322  // easily confused with passing the columns of composite unique key
2323  $this->queryLogger->warning( __METHOD__ .
2324  " called with deprecated parameter style: " .
2325  "the unique key array should be a string or array of string arrays",
2326  [ 'exception' => new RuntimeException() ] );
2327  return $uniqueKeys;
2328  } elseif ( is_array( $uniqueKey ) ) {
2329  return $uniqueKey;
2330  } else {
2331  throw new DBUnexpectedError( $this, 'Invalid unique key array entry' );
2332  }
2333  }
2334  }
2335 
2341  final protected function normalizeOptions( $options ) {
2342  if ( is_array( $options ) ) {
2343  return $options;
2344  } elseif ( is_string( $options ) ) {
2345  return ( $options === '' ) ? [] : [ $options ];
2346  } else {
2347  throw new DBUnexpectedError( $this, __METHOD__ . ': expected string or array' );
2348  }
2349  }
2350 
2357  final protected function assertValidUpsertRowArray( array $rows, array $identityKey ) {
2358  $numNulls = 0;
2359  foreach ( $rows as $row ) {
2360  foreach ( $identityKey as $column ) {
2361  $numNulls += ( isset( $row[$column] ) ? 0 : 1 );
2362  }
2363  }
2364 
2365  if (
2366  $numNulls &&
2367  $numNulls !== ( count( $rows ) * count( $identityKey ) )
2368  ) {
2369  throw new DBUnexpectedError(
2370  $this,
2371  "NULL/absent values for unique key (" . implode( ',', $identityKey ) . ")"
2372  );
2373  }
2374 
2375  return (bool)$numNulls;
2376  }
2377 
2384  final protected function assertValidUpsertSetArray(
2385  array $set,
2386  array $identityKey,
2387  array $rows
2388  ) {
2389  // Sloppy callers might construct the SET array using the ROW array, leaving redundant
2390  // column definitions for identity key columns. Detect this for backwards compatibility.
2391  $soleRow = ( count( $rows ) == 1 ) ? reset( $rows ) : null;
2392  // Disallow value changes for any columns in the identity key. This avoids additional
2393  // insertion order dependencies that are unwieldy and difficult to implement efficiently
2394  // in PostgreSQL.
2395  foreach ( $set as $k => $v ) {
2396  if ( is_string( $k ) ) {
2397  // Key is a column name and value is a literal (e.g. string, int, null, ...)
2398  if ( in_array( $k, $identityKey, true ) ) {
2399  if ( $soleRow && array_key_exists( $k, $soleRow ) && $soleRow[$k] === $v ) {
2400  $this->queryLogger->warning(
2401  __METHOD__ . " called with redundant assignment to column '$k'",
2402  [ 'exception' => new RuntimeException() ]
2403  );
2404  } else {
2405  throw new DBUnexpectedError(
2406  $this,
2407  "Cannot reassign column '$k' since it belongs to identity key"
2408  );
2409  }
2410  }
2411  } elseif ( preg_match( '/^([a-zA-Z0-9_]+)\s*=/', $v, $m ) ) {
2412  // Value is of the form "<unquoted alphanumeric column> = <SQL expression>"
2413  if ( in_array( $m[1], $identityKey, true ) ) {
2414  throw new DBUnexpectedError(
2415  $this,
2416  "Cannot reassign column '{$m[1]}' since it belongs to identity key"
2417  );
2418  }
2419  }
2420  }
2421  }
2422 
2429  final protected function isFlagInOptions( $option, array $options ) {
2430  foreach ( array_keys( $options, $option, true ) as $k ) {
2431  if ( is_int( $k ) ) {
2432  return true;
2433  }
2434  }
2435 
2436  return false;
2437  }
2438 
2443  final protected function extractSingleFieldFromList( $var ) {
2444  if ( is_array( $var ) ) {
2445  if ( !$var ) {
2446  $column = null;
2447  } elseif ( count( $var ) == 1 ) {
2448  $column = $var[0] ?? reset( $var );
2449  } else {
2450  throw new DBUnexpectedError( $this, __METHOD__ . ': got multiple columns' );
2451  }
2452  } else {
2453  $column = $var;
2454  }
2455 
2456  return $column;
2457  }
2458 
2459  public function lockForUpdate(
2460  $table, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
2461  ) {
2462  if ( !$this->trxLevel() && !$this->getFlag( self::DBO_TRX ) ) {
2463  throw new DBUnexpectedError(
2464  $this,
2465  __METHOD__ . ': no transaction is active nor is DBO_TRX set'
2466  );
2467  }
2468 
2469  $options = (array)$options;
2470  $options[] = 'FOR UPDATE';
2471 
2472  return $this->selectRowCount( $table, '*', $conds, $fname, $options, $join_conds );
2473  }
2474 
2475  public function fieldExists( $table, $field, $fname = __METHOD__ ) {
2476  $info = $this->fieldInfo( $table, $field );
2477 
2478  return (bool)$info;
2479  }
2480 
2481  public function indexExists( $table, $index, $fname = __METHOD__ ) {
2482  if ( !$this->tableExists( $table, $fname ) ) {
2483  return null;
2484  }
2485 
2486  $info = $this->indexInfo( $table, $index, $fname );
2487  if ( $info === null ) {
2488  return null;
2489  } else {
2490  return $info !== false;
2491  }
2492  }
2493 
2494  abstract public function tableExists( $table, $fname = __METHOD__ );
2495 
2500  public function indexUnique( $table, $index, $fname = __METHOD__ ) {
2501  $indexInfo = $this->indexInfo( $table, $index, $fname );
2502 
2503  if ( !$indexInfo ) {
2504  return null;
2505  }
2506 
2507  return !$indexInfo[0]->Non_unique;
2508  }
2509 
2510  public function insert( $table, $rows, $fname = __METHOD__, $options = [] ) {
2511  $rows = $this->normalizeRowArray( $rows );
2512  if ( !$rows ) {
2513  return true;
2514  }
2515 
2516  $options = $this->normalizeOptions( $options );
2517  if ( $this->isFlagInOptions( 'IGNORE', $options ) ) {
2518  $this->doInsertNonConflicting( $table, $rows, $fname );
2519  } else {
2520  $this->doInsert( $table, $rows, $fname );
2521  }
2522 
2523  return true;
2524  }
2525 
2534  protected function doInsert( $table, array $rows, $fname ) {
2535  $encTable = $this->tableName( $table );
2536  list( $sqlColumns, $sqlTuples ) = $this->makeInsertLists( $rows );
2537 
2538  $sql = "INSERT INTO $encTable ($sqlColumns) VALUES $sqlTuples";
2539 
2540  $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
2541  }
2542 
2551  protected function doInsertNonConflicting( $table, array $rows, $fname ) {
2552  $encTable = $this->tableName( $table );
2553  list( $sqlColumns, $sqlTuples ) = $this->makeInsertLists( $rows );
2554  list( $sqlVerb, $sqlOpts ) = $this->makeInsertNonConflictingVerbAndOptions();
2555 
2556  $sql = rtrim( "$sqlVerb $encTable ($sqlColumns) VALUES $sqlTuples $sqlOpts" );
2557 
2558  $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
2559  }
2560 
2567  return [ 'INSERT IGNORE INTO', '' ];
2568  }
2569 
2580  protected function makeInsertLists( array $rows ) {
2581  $firstRow = $rows[0];
2582  if ( !is_array( $firstRow ) || !$firstRow ) {
2583  throw new DBUnexpectedError( $this, 'Got an empty row list or empty row' );
2584  }
2585  // List of columns that define the value tuple ordering
2586  $tupleColumns = array_keys( $firstRow );
2587 
2588  $valueTuples = [];
2589  foreach ( $rows as $row ) {
2590  $rowColumns = array_keys( $row );
2591  // VALUES(...) requires a uniform correspondance of (column => value)
2592  if ( $rowColumns !== $tupleColumns ) {
2593  throw new DBUnexpectedError(
2594  $this,
2595  'Got row columns (' . implode( ', ', $rowColumns ) . ') ' .
2596  'instead of expected (' . implode( ', ', $tupleColumns ) . ')'
2597  );
2598  }
2599  // Make the value tuple that defines this row
2600  $valueTuples[] = '(' . $this->makeList( $row, self::LIST_COMMA ) . ')';
2601  }
2602 
2603  return [
2604  $this->makeList( $tupleColumns, self::LIST_NAMES ),
2605  implode( ',', $valueTuples )
2606  ];
2607  }
2608 
2616  protected function makeUpdateOptionsArray( $options ) {
2617  $options = $this->normalizeOptions( $options );
2618 
2619  $opts = [];
2620 
2621  if ( in_array( 'IGNORE', $options ) ) {
2622  $opts[] = 'IGNORE';
2623  }
2624 
2625  return $opts;
2626  }
2627 
2635  protected function makeUpdateOptions( $options ) {
2636  $opts = $this->makeUpdateOptionsArray( $options );
2637 
2638  return implode( ' ', $opts );
2639  }
2640 
2641  public function update( $table, $set, $conds, $fname = __METHOD__, $options = [] ) {
2642  $this->assertConditionIsNotEmpty( $conds, __METHOD__, true );
2643  $table = $this->tableName( $table );
2644  $opts = $this->makeUpdateOptions( $options );
2645  $sql = "UPDATE $opts $table SET " . $this->makeList( $set, self::LIST_SET );
2646 
2647  if ( $conds && $conds !== IDatabase::ALL_ROWS ) {
2648  if ( is_array( $conds ) ) {
2649  $conds = $this->makeList( $conds, self::LIST_AND );
2650  }
2651  $sql .= ' WHERE ' . $conds;
2652  }
2653 
2654  $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
2655 
2656  return true;
2657  }
2658 
2659  public function makeList( array $a, $mode = self::LIST_COMMA ) {
2660  $first = true;
2661  $list = '';
2662 
2663  foreach ( $a as $field => $value ) {
2664  if ( $first ) {
2665  $first = false;
2666  } else {
2667  if ( $mode == self::LIST_AND ) {
2668  $list .= ' AND ';
2669  } elseif ( $mode == self::LIST_OR ) {
2670  $list .= ' OR ';
2671  } else {
2672  $list .= ',';
2673  }
2674  }
2675 
2676  if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
2677  $list .= "($value)";
2678  } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
2679  $list .= "$value";
2680  } elseif (
2681  ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
2682  ) {
2683  // Remove null from array to be handled separately if found
2684  $includeNull = false;
2685  foreach ( array_keys( $value, null, true ) as $nullKey ) {
2686  $includeNull = true;
2687  unset( $value[$nullKey] );
2688  }
2689  if ( count( $value ) == 0 && !$includeNull ) {
2690  throw new InvalidArgumentException(
2691  __METHOD__ . ": empty input for field $field" );
2692  } elseif ( count( $value ) == 0 ) {
2693  // only check if $field is null
2694  $list .= "$field IS NULL";
2695  } else {
2696  // IN clause contains at least one valid element
2697  if ( $includeNull ) {
2698  // Group subconditions to ensure correct precedence
2699  $list .= '(';
2700  }
2701  if ( count( $value ) == 1 ) {
2702  // Special-case single values, as IN isn't terribly efficient
2703  // Don't necessarily assume the single key is 0; we don't
2704  // enforce linear numeric ordering on other arrays here.
2705  $value = array_values( $value )[0];
2706  $list .= $field . " = " . $this->addQuotes( $value );
2707  } else {
2708  $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
2709  }
2710  // if null present in array, append IS NULL
2711  if ( $includeNull ) {
2712  $list .= " OR $field IS NULL)";
2713  }
2714  }
2715  } elseif ( $value === null ) {
2716  if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
2717  $list .= "$field IS ";
2718  } elseif ( $mode == self::LIST_SET ) {
2719  $list .= "$field = ";
2720  }
2721  $list .= 'NULL';
2722  } else {
2723  if (
2724  $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
2725  ) {
2726  $list .= "$field = ";
2727  }
2728  $list .= $mode == self::LIST_NAMES ? $value : $this->addQuotes( $value );
2729  }
2730  }
2731 
2732  return $list;
2733  }
2734 
2735  public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
2736  $conds = [];
2737 
2738  foreach ( $data as $base => $sub ) {
2739  if ( count( $sub ) ) {
2740  $conds[] = $this->makeList(
2741  [ $baseKey => $base, $subKey => array_map( 'strval', array_keys( $sub ) ) ],
2743  );
2744  }
2745  }
2746 
2747  if ( $conds ) {
2748  return $this->makeList( $conds, self::LIST_OR );
2749  } else {
2750  // Nothing to search for...
2751  return false;
2752  }
2753  }
2754 
2759  public function aggregateValue( $valuedata, $valuename = 'value' ) {
2760  return $valuename;
2761  }
2762 
2767  public function bitNot( $field ) {
2768  return "(~$field)";
2769  }
2770 
2775  public function bitAnd( $fieldLeft, $fieldRight ) {
2776  return "($fieldLeft & $fieldRight)";
2777  }
2778 
2783  public function bitOr( $fieldLeft, $fieldRight ) {
2784  return "($fieldLeft | $fieldRight)";
2785  }
2786 
2791  public function buildConcat( $stringList ) {
2792  return 'CONCAT(' . implode( ',', $stringList ) . ')';
2793  }
2794 
2799  public function buildGroupConcatField(
2800  $delim, $table, $field, $conds = '', $join_conds = []
2801  ) {
2802  $fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
2803 
2804  return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
2805  }
2806 
2811  public function buildGreatest( $fields, $values ) {
2812  return $this->buildSuperlative( 'GREATEST', $fields, $values );
2813  }
2814 
2819  public function buildLeast( $fields, $values ) {
2820  return $this->buildSuperlative( 'LEAST', $fields, $values );
2821  }
2822 
2839  protected function buildSuperlative( $sqlfunc, $fields, $values ) {
2840  $fields = is_array( $fields ) ? $fields : [ $fields ];
2841  $values = is_array( $values ) ? $values : [ $values ];
2842 
2843  $encValues = [];
2844  foreach ( $fields as $alias => $field ) {
2845  if ( is_int( $alias ) ) {
2846  $encValues[] = $this->addIdentifierQuotes( $field );
2847  } else {
2848  $encValues[] = $field; // expression
2849  }
2850  }
2851  foreach ( $values as $value ) {
2852  if ( is_int( $value ) || is_float( $value ) ) {
2853  $encValues[] = $value;
2854  } elseif ( is_string( $value ) ) {
2855  $encValues[] = $this->addQuotes( $value );
2856  } elseif ( $value === null ) {
2857  throw new DBUnexpectedError( $this, 'Null value in superlative' );
2858  } else {
2859  throw new DBUnexpectedError( $this, 'Unexpected value type in superlative' );
2860  }
2861  }
2862 
2863  return $sqlfunc . '(' . implode( ',', $encValues ) . ')';
2864  }
2865 
2870  public function buildSubstring( $input, $startPosition, $length = null ) {
2871  $this->assertBuildSubstringParams( $startPosition, $length );
2872  $functionBody = "$input FROM $startPosition";
2873  if ( $length !== null ) {
2874  $functionBody .= " FOR $length";
2875  }
2876  return 'SUBSTRING(' . $functionBody . ')';
2877  }
2878 
2891  protected function assertBuildSubstringParams( $startPosition, $length ) {
2892  if ( $startPosition === 0 ) {
2893  // The DBMSs we support use 1-based indexing here.
2894  throw new InvalidArgumentException( 'Use 1 as $startPosition for the beginning of the string' );
2895  }
2896  if ( !is_int( $startPosition ) || $startPosition < 0 ) {
2897  throw new InvalidArgumentException(
2898  '$startPosition must be a positive integer'
2899  );
2900  }
2901  if ( !( is_int( $length ) && $length >= 0 || $length === null ) ) {
2902  throw new InvalidArgumentException(
2903  '$length must be null or an integer greater than or equal to 0'
2904  );
2905  }
2906  }
2907 
2921  protected function assertConditionIsNotEmpty( $conds, string $fname, bool $deprecate ) {
2922  $isCondValid = ( is_string( $conds ) || is_array( $conds ) ) && $conds;
2923  if ( !$isCondValid ) {
2924  if ( $deprecate ) {
2925  wfDeprecated( $fname . ' called with empty $conds', '1.35', false, 3 );
2926  } else {
2927  throw new DBUnexpectedError( $this, $fname . ' called with empty conditions' );
2928  }
2929  }
2930  }
2931 
2936  public function buildStringCast( $field ) {
2937  // In theory this should work for any standards-compliant
2938  // SQL implementation, although it may not be the best way to do it.
2939  return "CAST( $field AS CHARACTER )";
2940  }
2941 
2946  public function buildIntegerCast( $field ) {
2947  return 'CAST( ' . $field . ' AS INTEGER )';
2948  }
2949 
2950  public function buildSelectSubquery(
2951  $table, $vars, $conds = '', $fname = __METHOD__,
2952  $options = [], $join_conds = []
2953  ) {
2954  return new Subquery(
2955  $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
2956  );
2957  }
2958 
2963  public function databasesAreIndependent() {
2964  return false;
2965  }
2966 
2967  final public function selectDB( $db ) {
2968  $this->selectDomain( new DatabaseDomain(
2969  $db,
2970  $this->currentDomain->getSchema(),
2971  $this->currentDomain->getTablePrefix()
2972  ) );
2973 
2974  return true;
2975  }
2976 
2977  final public function selectDomain( $domain ) {
2978  $cs = $this->commenceCriticalSection( __METHOD__ );
2979 
2980  try {
2981  $this->doSelectDomain( DatabaseDomain::newFromId( $domain ) );
2982  } catch ( DBError $e ) {
2983  $this->completeCriticalSection( __METHOD__, $cs );
2984  throw $e;
2985  }
2986 
2987  $this->completeCriticalSection( __METHOD__, $cs );
2988  }
2989 
2997  protected function doSelectDomain( DatabaseDomain $domain ) {
2998  $this->currentDomain = $domain;
2999  }
3000 
3001  public function getDBname() {
3002  return $this->currentDomain->getDatabase();
3003  }
3004 
3005  public function getServer() {
3006  return $this->connectionParams[self::CONN_HOST] ?? null;
3007  }
3008 
3009  public function getServerName() {
3010  return $this->serverName ?? $this->getServer();
3011  }
3012 
3017  public function tableName( $name, $format = 'quoted' ) {
3018  if ( $name instanceof Subquery ) {
3019  throw new DBUnexpectedError(
3020  $this,
3021  __METHOD__ . ': got Subquery instance when expecting a string'
3022  );
3023  }
3024 
3025  # Skip the entire process when we have a string quoted on both ends.
3026  # Note that we check the end so that we will still quote any use of
3027  # use of `database`.table. But won't break things if someone wants
3028  # to query a database table with a dot in the name.
3029  if ( $this->isQuotedIdentifier( $name ) ) {
3030  return $name;
3031  }
3032 
3033  # Lets test for any bits of text that should never show up in a table
3034  # name. Basically anything like JOIN or ON which are actually part of
3035  # SQL queries, but may end up inside of the table value to combine
3036  # sql. Such as how the API is doing.
3037  # Note that we use a whitespace test rather than a \b test to avoid
3038  # any remote case where a word like on may be inside of a table name
3039  # surrounded by symbols which may be considered word breaks.
3040  if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
3041  $this->queryLogger->warning(
3042  __METHOD__ . ": use of subqueries is not supported this way",
3043  [ 'exception' => new RuntimeException() ]
3044  );
3045 
3046  return $name;
3047  }
3048 
3049  # Split database and table into proper variables.
3050  list( $database, $schema, $prefix, $table ) = $this->qualifiedTableComponents( $name );
3051 
3052  # Quote $table and apply the prefix if not quoted.
3053  # $tableName might be empty if this is called from Database::replaceVars()
3054  $tableName = "{$prefix}{$table}";
3055  if ( $format === 'quoted'
3056  && !$this->isQuotedIdentifier( $tableName )
3057  && $tableName !== ''
3058  ) {
3059  $tableName = $this->addIdentifierQuotes( $tableName );
3060  }
3061 
3062  # Quote $schema and $database and merge them with the table name if needed
3063  $tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
3064  $tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
3065 
3066  return $tableName;
3067  }
3068 
3075  protected function qualifiedTableComponents( $name ) {
3076  # We reverse the explode so that database.table and table both output the correct table.
3077  $dbDetails = explode( '.', $name, 3 );
3078  if ( count( $dbDetails ) == 3 ) {
3079  list( $database, $schema, $table ) = $dbDetails;
3080  # We don't want any prefix added in this case
3081  $prefix = '';
3082  } elseif ( count( $dbDetails ) == 2 ) {
3083  list( $database, $table ) = $dbDetails;
3084  # We don't want any prefix added in this case
3085  $prefix = '';
3086  # In dbs that support it, $database may actually be the schema
3087  # but that doesn't affect any of the functionality here
3088  $schema = '';
3089  } else {
3090  list( $table ) = $dbDetails;
3091  if ( isset( $this->tableAliases[$table] ) ) {
3092  $database = $this->tableAliases[$table]['dbname'];
3093  $schema = is_string( $this->tableAliases[$table]['schema'] )
3094  ? $this->tableAliases[$table]['schema']
3095  : $this->relationSchemaQualifier();
3096  $prefix = is_string( $this->tableAliases[$table]['prefix'] )
3097  ? $this->tableAliases[$table]['prefix']
3098  : $this->tablePrefix();
3099  } else {
3100  $database = '';
3101  $schema = $this->relationSchemaQualifier(); # Default schema
3102  $prefix = $this->tablePrefix(); # Default prefix
3103  }
3104  }
3105 
3106  return [ $database, $schema, $prefix, $table ];
3107  }
3108 
3115  private function prependDatabaseOrSchema( $namespace, $relation, $format ) {
3116  if ( $namespace !== null && $namespace !== '' ) {
3117  if ( $format === 'quoted' && !$this->isQuotedIdentifier( $namespace ) ) {
3118  $namespace = $this->addIdentifierQuotes( $namespace );
3119  }
3120  $relation = $namespace . '.' . $relation;
3121  }
3122 
3123  return $relation;
3124  }
3125 
3126  public function tableNames( ...$tables ) {
3127  $retVal = [];
3128 
3129  foreach ( $tables as $name ) {
3130  $retVal[$name] = $this->tableName( $name );
3131  }
3132 
3133  return $retVal;
3134  }
3135 
3136  public function tableNamesN( ...$tables ) {
3137  $retVal = [];
3138 
3139  foreach ( $tables as $name ) {
3140  $retVal[] = $this->tableName( $name );
3141  }
3142 
3143  return $retVal;
3144  }
3145 
3157  protected function tableNameWithAlias( $table, $alias = false ) {
3158  if ( is_string( $table ) ) {
3159  $quotedTable = $this->tableName( $table );
3160  } elseif ( $table instanceof Subquery ) {
3161  $quotedTable = (string)$table;
3162  } else {
3163  throw new InvalidArgumentException( "Table must be a string or Subquery" );
3164  }
3165 
3166  if ( $alias === false || $alias === $table ) {
3167  if ( $table instanceof Subquery ) {
3168  throw new InvalidArgumentException( "Subquery table missing alias" );
3169  }
3170 
3171  return $quotedTable;
3172  } else {
3173  return $quotedTable . ' ' . $this->addIdentifierQuotes( $alias );
3174  }
3175  }
3176 
3186  protected function fieldNameWithAlias( $name, $alias = false ) {
3187  if ( !$alias || (string)$alias === (string)$name ) {
3188  return $name;
3189  } else {
3190  return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
3191  }
3192  }
3193 
3200  protected function fieldNamesWithAlias( $fields ) {
3201  $retval = [];
3202  foreach ( $fields as $alias => $field ) {
3203  if ( is_numeric( $alias ) ) {
3204  $alias = $field;
3205  }
3206  $retval[] = $this->fieldNameWithAlias( $field, $alias );
3207  }
3208 
3209  return $retval;
3210  }
3211 
3223  $tables,
3224  $use_index = [],
3225  $ignore_index = [],
3226  $join_conds = []
3227  ) {
3228  $ret = [];
3229  $retJOIN = [];
3230  $use_index = (array)$use_index;
3231  $ignore_index = (array)$ignore_index;
3232  $join_conds = (array)$join_conds;
3233 
3234  foreach ( $tables as $alias => $table ) {
3235  if ( !is_string( $alias ) ) {
3236  // No alias? Set it equal to the table name
3237  $alias = $table;
3238  }
3239 
3240  if ( is_array( $table ) ) {
3241  // A parenthesized group
3242  if ( count( $table ) > 1 ) {
3243  $joinedTable = '(' .
3245  $table, $use_index, $ignore_index, $join_conds ) . ')';
3246  } else {
3247  // Degenerate case
3248  $innerTable = reset( $table );
3249  $innerAlias = key( $table );
3250  $joinedTable = $this->tableNameWithAlias(
3251  $innerTable,
3252  is_string( $innerAlias ) ? $innerAlias : $innerTable
3253  );
3254  }
3255  } else {
3256  $joinedTable = $this->tableNameWithAlias( $table, $alias );
3257  }
3258 
3259  // Is there a JOIN clause for this table?
3260  if ( isset( $join_conds[$alias] ) ) {
3261  Assert::parameterType( 'array', $join_conds[$alias], "join_conds[$alias]" );
3262  list( $joinType, $conds ) = $join_conds[$alias];
3263  $tableClause = $joinType;
3264  $tableClause .= ' ' . $joinedTable;
3265  if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
3266  $use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
3267  if ( $use != '' ) {
3268  $tableClause .= ' ' . $use;
3269  }
3270  }
3271  if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
3272  $ignore = $this->ignoreIndexClause(
3273  implode( ',', (array)$ignore_index[$alias] ) );
3274  if ( $ignore != '' ) {
3275  $tableClause .= ' ' . $ignore;
3276  }
3277  }
3278  $on = $this->makeList( (array)$conds, self::LIST_AND );
3279  if ( $on != '' ) {
3280  $tableClause .= ' ON (' . $on . ')';
3281  }
3282 
3283  $retJOIN[] = $tableClause;
3284  } elseif ( isset( $use_index[$alias] ) ) {
3285  // Is there an INDEX clause for this table?
3286  $tableClause = $joinedTable;
3287  $tableClause .= ' ' . $this->useIndexClause(
3288  implode( ',', (array)$use_index[$alias] )
3289  );
3290 
3291  $ret[] = $tableClause;
3292  } elseif ( isset( $ignore_index[$alias] ) ) {
3293  // Is there an INDEX clause for this table?
3294  $tableClause = $joinedTable;
3295  $tableClause .= ' ' . $this->ignoreIndexClause(
3296  implode( ',', (array)$ignore_index[$alias] )
3297  );
3298 
3299  $ret[] = $tableClause;
3300  } else {
3301  $tableClause = $joinedTable;
3302 
3303  $ret[] = $tableClause;
3304  }
3305  }
3306 
3307  // We can't separate explicit JOIN clauses with ',', use ' ' for those
3308  $implicitJoins = implode( ',', $ret );
3309  $explicitJoins = implode( ' ', $retJOIN );
3310 
3311  // Compile our final table clause
3312  return implode( ' ', [ $implicitJoins, $explicitJoins ] );
3313  }
3314 
3321  protected function indexName( $index ) {
3322  return $this->indexAliases[$index] ?? $index;
3323  }
3324 
3329  public function addQuotes( $s ) {
3330  if ( $s instanceof Blob ) {
3331  $s = $s->fetch();
3332  }
3333  if ( $s === null ) {
3334  return 'NULL';
3335  } elseif ( is_bool( $s ) ) {
3336  return (string)(int)$s;
3337  } elseif ( is_int( $s ) ) {
3338  return (string)$s;
3339  } else {
3340  return "'" . $this->strencode( $s ) . "'";
3341  }
3342  }
3343 
3348  public function addIdentifierQuotes( $s ) {
3349  return '"' . str_replace( '"', '""', $s ) . '"';
3350  }
3351 
3362  public function isQuotedIdentifier( $name ) {
3363  return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
3364  }
3365 
3372  protected function escapeLikeInternal( $s, $escapeChar = '`' ) {
3373  return str_replace(
3374  [ $escapeChar, '%', '_' ],
3375  [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ],
3376  $s
3377  );
3378  }
3379 
3384  public function buildLike( $param, ...$params ) {
3385  if ( is_array( $param ) ) {
3386  $params = $param;
3387  } else {
3388  $params = func_get_args();
3389  }
3390 
3391  $s = '';
3392 
3393  // We use ` instead of \ as the default LIKE escape character, since addQuotes()
3394  // may escape backslashes, creating problems of double escaping. The `
3395  // character has good cross-DBMS compatibility, avoiding special operators
3396  // in MS SQL like ^ and %
3397  $escapeChar = '`';
3398 
3399  foreach ( $params as $value ) {
3400  if ( $value instanceof LikeMatch ) {
3401  $s .= $value->toString();
3402  } else {
3403  $s .= $this->escapeLikeInternal( $value, $escapeChar );
3404  }
3405  }
3406 
3407  return ' LIKE ' .
3408  $this->addQuotes( $s ) . ' ESCAPE ' . $this->addQuotes( $escapeChar ) . ' ';
3409  }
3410 
3411  public function anyChar() {
3412  return new LikeMatch( '_' );
3413  }
3414 
3415  public function anyString() {
3416  return new LikeMatch( '%' );
3417  }
3418 
3419  public function nextSequenceValue( $seqName ) {
3420  return null;
3421  }
3422 
3436  public function useIndexClause( $index ) {
3437  return '';
3438  }
3439 
3449  public function ignoreIndexClause( $index ) {
3450  return '';
3451  }
3452 
3453  public function replace( $table, $uniqueKeys, $rows, $fname = __METHOD__ ) {
3454  $identityKey = $this->normalizeUpsertParams( $uniqueKeys, $rows );
3455  if ( !$rows ) {
3456  return;
3457  }
3458  if ( $identityKey ) {
3459  $this->doReplace( $table, $identityKey, $rows, $fname );
3460  } else {
3461  $this->doInsert( $table, $rows, $fname );
3462  }
3463  }
3464 
3474  protected function doReplace( $table, array $identityKey, array $rows, $fname ) {
3475  $affectedRowCount = 0;
3476  $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
3477  try {
3478  foreach ( $rows as $row ) {
3479  // Delete any conflicting rows (including ones inserted from $rows)
3480  $sqlCondition = $this->makeKeyCollisionCondition( [ $row ], $identityKey );
3481  $this->delete( $table, [ $sqlCondition ], $fname );
3482  $affectedRowCount += $this->affectedRows();
3483  // Now insert the row
3484  $this->insert( $table, $row, $fname );
3485  $affectedRowCount += $this->affectedRows();
3486  }
3487  $this->endAtomic( $fname );
3488  } catch ( DBError $e ) {
3489  $this->cancelAtomic( $fname );
3490  throw $e;
3491  }
3492  $this->affectedRowCount = $affectedRowCount;
3493  }
3494 
3502  private function makeKeyCollisionCondition( array $rows, array $uniqueKey ) {
3503  if ( !$rows ) {
3504  throw new DBUnexpectedError( $this, "Empty row array" );
3505  } elseif ( !$uniqueKey ) {
3506  throw new DBUnexpectedError( $this, "Empty unique key array" );
3507  }
3508 
3509  if ( count( $uniqueKey ) == 1 ) {
3510  // Use a simple IN(...) clause
3511  $column = reset( $uniqueKey );
3512  $values = array_column( $rows, $column );
3513  if ( count( $values ) !== count( $rows ) ) {
3514  throw new DBUnexpectedError( $this, "Missing values for unique key ($column)" );
3515  }
3516 
3517  return $this->makeList( [ $column => $values ], self::LIST_AND );
3518  }
3519 
3520  $nullByUniqueKeyColumn = array_fill_keys( $uniqueKey, null );
3521 
3522  $orConds = [];
3523  foreach ( $rows as $row ) {
3524  $rowKeyMap = array_intersect_key( $row, $nullByUniqueKeyColumn );
3525  if ( count( $rowKeyMap ) != count( $uniqueKey ) ) {
3526  throw new DBUnexpectedError(
3527  $this,
3528  "Missing values for unique key (" . implode( ',', $uniqueKey ) . ")"
3529  );
3530  }
3531  $orConds[] = $this->makeList( $rowKeyMap, self::LIST_AND );
3532  }
3533 
3534  return count( $orConds ) > 1
3535  ? $this->makeList( $orConds, self::LIST_OR )
3536  : $orConds[0];
3537  }
3538 
3539  public function upsert( $table, array $rows, $uniqueKeys, array $set, $fname = __METHOD__ ) {
3540  $identityKey = $this->normalizeUpsertParams( $uniqueKeys, $rows );
3541  if ( !$rows ) {
3542  return true;
3543  }
3544  if ( $identityKey ) {
3545  $this->assertValidUpsertSetArray( $set, $identityKey, $rows );
3546  $this->doUpsert( $table, $rows, $identityKey, $set, $fname );
3547  } else {
3548  $this->doInsert( $table, $rows, $fname );
3549  }
3550 
3551  return true;
3552  }
3553 
3564  protected function doUpsert(
3565  string $table,
3566  array $rows,
3567  array $identityKey,
3568  array $set,
3569  string $fname
3570  ) {
3571  $affectedRowCount = 0;
3572  $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
3573  try {
3574  foreach ( $rows as $row ) {
3575  // Update any existing conflicting rows (including ones inserted from $rows)
3576  $sqlConditions = $this->makeKeyCollisionCondition( [ $row ], $identityKey );
3577  $this->update( $table, $set, [ $sqlConditions ], $fname );
3578  $rowsUpdated = $this->affectedRows();
3579  $affectedRowCount += $rowsUpdated;
3580  if ( $rowsUpdated <= 0 ) {
3581  // Now insert the row if there are no conflicts
3582  $this->insert( $table, $row, $fname );
3583  $affectedRowCount += $this->affectedRows();
3584  }
3585  }
3586  $this->endAtomic( $fname );
3587  } catch ( DBError $e ) {
3588  $this->cancelAtomic( $fname );
3589  throw $e;
3590  }
3591  $this->affectedRowCount = $affectedRowCount;
3592  }
3593 
3598  public function deleteJoin(
3599  $delTable,
3600  $joinTable,
3601  $delVar,
3602  $joinVar,
3603  $conds,
3604  $fname = __METHOD__
3605  ) {
3606  if ( !$conds ) {
3607  throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
3608  }
3609 
3610  $delTable = $this->tableName( $delTable );
3611  $joinTable = $this->tableName( $joinTable );
3612  $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
3613  if ( $conds != '*' ) {
3614  $sql .= 'WHERE ' . $this->makeList( $conds, self::LIST_AND );
3615  }
3616  $sql .= ')';
3617 
3618  $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
3619  }
3620 
3625  public function textFieldSize( $table, $field ) {
3626  $table = $this->tableName( $table );
3627  $sql = "SHOW COLUMNS FROM $table LIKE \"$field\"";
3628  $res = $this->query( $sql, __METHOD__, self::QUERY_CHANGE_NONE );
3629  $row = $this->fetchObject( $res );
3630 
3631  $m = [];
3632 
3633  if ( preg_match( '/\‍((.*)\‍)/', $row->Type, $m ) ) {
3634  $size = $m[1];
3635  } else {
3636  $size = -1;
3637  }
3638 
3639  return $size;
3640  }
3641 
3642  public function delete( $table, $conds, $fname = __METHOD__ ) {
3643  $this->assertConditionIsNotEmpty( $conds, __METHOD__, false );
3644 
3645  $table = $this->tableName( $table );
3646  $sql = "DELETE FROM $table";
3647 
3648  if ( $conds !== IDatabase::ALL_ROWS ) {
3649  if ( is_array( $conds ) ) {
3650  $conds = $this->makeList( $conds, self::LIST_AND );
3651  }
3652  $sql .= ' WHERE ' . $conds;
3653  }
3654 
3655  $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
3656 
3657  return true;
3658  }
3659 
3660  final public function insertSelect(
3661  $destTable,
3662  $srcTable,
3663  $varMap,
3664  $conds,
3665  $fname = __METHOD__,
3666  $insertOptions = [],
3667  $selectOptions = [],
3668  $selectJoinConds = []
3669  ) {
3670  static $hints = [ 'NO_AUTO_COLUMNS' ];
3671 
3672  $insertOptions = $this->normalizeOptions( $insertOptions );
3673  $selectOptions = $this->normalizeOptions( $selectOptions );
3674 
3675  if ( $this->cliMode && $this->isInsertSelectSafe( $insertOptions, $selectOptions ) ) {
3676  // For massive migrations with downtime, we don't want to select everything
3677  // into memory and OOM, so do all this native on the server side if possible.
3678  $this->doInsertSelectNative(
3679  $destTable,
3680  $srcTable,
3681  $varMap,
3682  $conds,
3683  $fname,
3684  array_diff( $insertOptions, $hints ),
3685  $selectOptions,
3686  $selectJoinConds
3687  );
3688  } else {
3689  $this->doInsertSelectGeneric(
3690  $destTable,
3691  $srcTable,
3692  $varMap,
3693  $conds,
3694  $fname,
3695  array_diff( $insertOptions, $hints ),
3696  $selectOptions,
3697  $selectJoinConds
3698  );
3699  }
3700 
3701  return true;
3702  }
3703 
3711  protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
3712  return true;
3713  }
3714 
3729  protected function doInsertSelectGeneric(
3730  $destTable,
3731  $srcTable,
3732  array $varMap,
3733  $conds,
3734  $fname,
3735  array $insertOptions,
3736  array $selectOptions,
3737  $selectJoinConds
3738  ) {
3739  // For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
3740  // on only the primary DB (without needing row-based-replication). It also makes it easy to
3741  // know how big the INSERT is going to be.
3742  $fields = [];
3743  foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
3744  $fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
3745  }
3746  $res = $this->select(
3747  $srcTable,
3748  implode( ',', $fields ),
3749  $conds,
3750  $fname,
3751  array_merge( $selectOptions, [ 'FOR UPDATE' ] ),
3752  $selectJoinConds
3753  );
3754  if ( !$res ) {
3755  return;
3756  }
3757 
3758  $affectedRowCount = 0;
3759  $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
3760  try {
3761  $rows = [];
3762  foreach ( $res as $row ) {
3763  $rows[] = (array)$row;
3764  }
3765  // Avoid inserts that are too huge
3766  $rowBatches = array_chunk( $rows, $this->nonNativeInsertSelectBatchSize );
3767  foreach ( $rowBatches as $rows ) {
3768  $this->insert( $destTable, $rows, $fname, $insertOptions );
3769  $affectedRowCount += $this->affectedRows();
3770  }
3771  } catch ( DBError $e ) {
3772  $this->cancelAtomic( $fname );
3773  throw $e;
3774  }
3775  $this->endAtomic( $fname );
3776  $this->affectedRowCount = $affectedRowCount;
3777  }
3778 
3794  protected function doInsertSelectNative(
3795  $destTable,
3796  $srcTable,
3797  array $varMap,
3798  $conds,
3799  $fname,
3800  array $insertOptions,
3801  array $selectOptions,
3802  $selectJoinConds
3803  ) {
3804  list( $sqlVerb, $sqlOpts ) = $this->isFlagInOptions( 'IGNORE', $insertOptions )
3806  : [ 'INSERT INTO', '' ];
3807  $encDstTable = $this->tableName( $destTable );
3808  $sqlDstColumns = implode( ',', array_keys( $varMap ) );
3809  $selectSql = $this->selectSQLText(
3810  $srcTable,
3811  array_values( $varMap ),
3812  $conds,
3813  $fname,
3814  $selectOptions,
3815  $selectJoinConds
3816  );
3817 
3818  $sql = rtrim( "$sqlVerb $encDstTable ($sqlDstColumns) $selectSql $sqlOpts" );
3819 
3820  $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
3821  }
3822 
3827  public function limitResult( $sql, $limit, $offset = false ) {
3828  if ( !is_numeric( $limit ) ) {
3829  throw new DBUnexpectedError(
3830  $this,
3831  "Invalid non-numeric limit passed to " . __METHOD__
3832  );
3833  }
3834  // This version works in MySQL and SQLite. It will very likely need to be
3835  // overridden for most other RDBMS subclasses.
3836  return "$sql LIMIT "
3837  . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
3838  . "{$limit} ";
3839  }
3840 
3845  public function unionSupportsOrderAndLimit() {
3846  return true; // True for almost every DB supported
3847  }
3848 
3853  public function unionQueries( $sqls, $all ) {
3854  $glue = $all ? ') UNION ALL (' : ') UNION (';
3855 
3856  return '(' . implode( $glue, $sqls ) . ')';
3857  }
3858 
3860  $table,
3861  $vars,
3862  array $permute_conds,
3863  $extra_conds = '',
3864  $fname = __METHOD__,
3865  $options = [],
3866  $join_conds = []
3867  ) {
3868  // First, build the Cartesian product of $permute_conds
3869  $conds = [ [] ];
3870  foreach ( $permute_conds as $field => $values ) {
3871  if ( !$values ) {
3872  // Skip empty $values
3873  continue;
3874  }
3875  $values = array_unique( $values ); // For sanity
3876  $newConds = [];
3877  foreach ( $conds as $cond ) {
3878  foreach ( $values as $value ) {
3879  $cond[$field] = $value;
3880  $newConds[] = $cond; // Arrays are by-value, not by-reference, so this works
3881  }
3882  }
3883  $conds = $newConds;
3884  }
3885 
3886  $extra_conds = $extra_conds === '' ? [] : (array)$extra_conds;
3887 
3888  // If there's just one condition and no subordering, hand off to
3889  // selectSQLText directly.
3890  if ( count( $conds ) === 1 &&
3891  ( !isset( $options['INNER ORDER BY'] ) || !$this->unionSupportsOrderAndLimit() )
3892  ) {
3893  return $this->selectSQLText(
3894  $table, $vars, $conds[0] + $extra_conds, $fname, $options, $join_conds
3895  );
3896  }
3897 
3898  // Otherwise, we need to pull out the order and limit to apply after
3899  // the union. Then build the SQL queries for each set of conditions in
3900  // $conds. Then union them together (using UNION ALL, because the
3901  // product *should* already be distinct).
3902  $orderBy = $this->makeOrderBy( $options );
3903  $limit = $options['LIMIT'] ?? null;
3904  $offset = $options['OFFSET'] ?? false;
3905  $all = empty( $options['NOTALL'] ) && !in_array( 'NOTALL', $options );
3906  if ( !$this->unionSupportsOrderAndLimit() ) {
3907  unset( $options['ORDER BY'], $options['LIMIT'], $options['OFFSET'] );
3908  } else {
3909  if ( array_key_exists( 'INNER ORDER BY', $options ) ) {
3910  $options['ORDER BY'] = $options['INNER ORDER BY'];
3911  }
3912  if ( $limit !== null && is_numeric( $offset ) && $offset != 0 ) {
3913  // We need to increase the limit by the offset rather than
3914  // using the offset directly, otherwise it'll skip incorrectly
3915  // in the subqueries.
3916  $options['LIMIT'] = $limit + $offset;
3917  unset( $options['OFFSET'] );
3918  }
3919  }
3920 
3921  $sqls = [];
3922  foreach ( $conds as $cond ) {
3923  $sqls[] = $this->selectSQLText(
3924  $table, $vars, $cond + $extra_conds, $fname, $options, $join_conds
3925  );
3926  }
3927  $sql = $this->unionQueries( $sqls, $all ) . $orderBy;
3928  if ( $limit !== null ) {
3929  $sql = $this->limitResult( $sql, $limit, $offset );
3930  }
3931 
3932  return $sql;
3933  }
3934 
3939  public function conditional( $cond, $caseTrueExpression, $caseFalseExpression ) {
3940  if ( is_array( $cond ) ) {
3941  $cond = $this->makeList( $cond, self::LIST_AND );
3942  }
3943 
3944  return "(CASE WHEN $cond THEN $caseTrueExpression ELSE $caseFalseExpression END)";
3945  }
3946 
3951  public function strreplace( $orig, $old, $new ) {
3952  return "REPLACE({$orig}, {$old}, {$new})";
3953  }
3954 
3959  public function getServerUptime() {
3960  return 0;
3961  }
3962 
3967  public function wasDeadlock() {
3968  return false;
3969  }
3970 
3975  public function wasLockTimeout() {
3976  return false;
3977  }
3978 
3983  public function wasConnectionLoss() {
3984  return $this->wasConnectionError( $this->lastErrno() );
3985  }
3986 
3991  public function wasReadOnlyError() {
3992  return false;
3993  }
3994 
3995  public function wasErrorReissuable() {
3996  return (
3997  $this->wasDeadlock() ||
3998  $this->wasLockTimeout() ||
3999  $this->wasConnectionLoss()
4000  );
4001  }
4002 
4010  public function wasConnectionError( $errno ) {
4011  return false;
4012  }
4013 
4021  protected function wasKnownStatementRollbackError() {
4022  return false; // don't know; it could have caused a transaction rollback
4023  }
4024 
4029  public function deadlockLoop( ...$args ) {
4030  $function = array_shift( $args );
4031  $tries = self::$DEADLOCK_TRIES;
4032 
4033  $this->begin( __METHOD__ );
4034 
4035  $retVal = null;
4037  $e = null;
4038  do {
4039  try {
4040  $retVal = $function( ...$args );
4041  break;
4042  } catch ( DBQueryError $e ) {
4043  if ( $this->wasDeadlock() ) {
4044  // Retry after a randomized delay
4045  usleep( mt_rand( self::$DEADLOCK_DELAY_MIN, self::$DEADLOCK_DELAY_MAX ) );
4046  } else {
4047  // Throw the error back up
4048  throw $e;
4049  }
4050  }
4051  } while ( --$tries > 0 );
4052 
4053  if ( $tries <= 0 ) {
4054  // Too many deadlocks; give up
4055  $this->rollback( __METHOD__ );
4056  throw $e;
4057  } else {
4058  $this->commit( __METHOD__ );
4059 
4060  return $retVal;
4061  }
4062  }
4063 
4069  public function primaryPosWait( DBPrimaryPos $pos, $timeout ) {
4070  # Real waits are implemented in the subclass.
4071  return 0;
4072  }
4073 
4077  public function masterPosWait( DBPrimaryPos $pos, $timeout ) {
4078  wfDeprecated( __METHOD__, '1.37' );
4079  return $this->primaryPosWait( $pos, $timeout );
4080  }
4081 
4086  public function getReplicaPos() {
4087  # Stub
4088  return false;
4089  }
4090 
4095  public function getPrimaryPos() {
4096  # Stub
4097  return false;
4098  }
4099 
4100  public function getMasterPos() {
4101  wfDeprecated( __METHOD__, '1.37' );
4102  return $this->getPrimaryPos();
4103  }
4104 
4109  public function serverIsReadOnly() {
4110  return false;
4111  }
4112 
4113  final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
4114  if ( !$this->trxLevel() ) {
4115  throw new DBUnexpectedError( $this, "No transaction is active" );
4116  }
4117  $this->trxEndCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
4118  }
4119 
4120  final public function onTransactionCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
4121  if ( !$this->trxLevel() && $this->getTransactionRoundId() ) {
4122  // This DB handle is set to participate in LoadBalancer transaction rounds and
4123  // an explicit transaction round is active. Start an implicit transaction on this
4124  // DB handle (setting trxAutomatic) similar to how query() does in such situations.
4125  $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
4126  }
4127 
4128  $this->trxPostCommitOrIdleCallbacks[] = [
4129  $callback,
4130  $fname,
4131  $this->currentAtomicSectionId()
4132  ];
4133 
4134  if ( !$this->trxLevel() ) {
4135  $dbErrors = [];
4136  $this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE, $dbErrors );
4137  if ( $dbErrors ) {
4138  throw $dbErrors[0];
4139  }
4140  }
4141  }
4142 
4143  final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
4144  $this->onTransactionCommitOrIdle( $callback, $fname );
4145  }
4146 
4147  final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
4148  if ( !$this->trxLevel() && $this->getTransactionRoundId() ) {
4149  // This DB handle is set to participate in LoadBalancer transaction rounds and
4150  // an explicit transaction round is active. Start an implicit transaction on this
4151  // DB handle (setting trxAutomatic) similar to how query() does in such situations.
4152  $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
4153  }
4154 
4155  if ( $this->trxLevel() ) {
4156  $this->trxPreCommitOrIdleCallbacks[] = [
4157  $callback,
4158  $fname,
4159  $this->currentAtomicSectionId()
4160  ];
4161  } else {
4162  // No transaction is active nor will start implicitly, so make one for this callback
4163  $this->startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
4164  try {
4165  $callback( $this );
4166  } catch ( Throwable $e ) {
4167  // Avoid confusing error reporting during critical section errors
4168  if ( !$this->csmError ) {
4169  $this->cancelAtomic( __METHOD__ );
4170  }
4171  throw $e;
4172  }
4173  $this->endAtomic( __METHOD__ );
4174  }
4175  }
4176 
4177  final public function onAtomicSectionCancel( callable $callback, $fname = __METHOD__ ) {
4178  if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
4179  throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)" );
4180  }
4181  $this->trxSectionCancelCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
4182  }
4183 
4187  private function currentAtomicSectionId() {
4188  if ( $this->trxLevel() && $this->trxAtomicLevels ) {
4189  $levelInfo = end( $this->trxAtomicLevels );
4190 
4191  return $levelInfo[1];
4192  }
4193 
4194  return null;
4195  }
4196 
4206  ) {
4207  foreach ( $this->trxPreCommitOrIdleCallbacks as $key => $info ) {
4208  if ( $info[2] === $old ) {
4209  $this->trxPreCommitOrIdleCallbacks[$key][2] = $new;
4210  }
4211  }
4212  foreach ( $this->trxPostCommitOrIdleCallbacks as $key => $info ) {
4213  if ( $info[2] === $old ) {
4214  $this->trxPostCommitOrIdleCallbacks[$key][2] = $new;
4215  }
4216  }
4217  foreach ( $this->trxEndCallbacks as $key => $info ) {
4218  if ( $info[2] === $old ) {
4219  $this->trxEndCallbacks[$key][2] = $new;
4220  }
4221  }
4222  foreach ( $this->trxSectionCancelCallbacks as $key => $info ) {
4223  if ( $info[2] === $old ) {
4224  $this->trxSectionCancelCallbacks[$key][2] = $new;
4225  }
4226  }
4227  }
4228 
4248  private function modifyCallbacksForCancel(
4249  array $sectionIds,
4250  AtomicSectionIdentifier $newSectionId = null
4251  ) {
4252  // Cancel the "on commit" callbacks owned by this savepoint
4253  $this->trxPostCommitOrIdleCallbacks = array_filter(
4254  $this->trxPostCommitOrIdleCallbacks,
4255  static function ( $entry ) use ( $sectionIds ) {
4256  return !in_array( $entry[2], $sectionIds, true );
4257  }
4258  );
4259  $this->trxPreCommitOrIdleCallbacks = array_filter(
4260  $this->trxPreCommitOrIdleCallbacks,
4261  static function ( $entry ) use ( $sectionIds ) {
4262  return !in_array( $entry[2], $sectionIds, true );
4263  }
4264  );
4265  // Make "on resolution" callbacks owned by this savepoint to perceive a rollback
4266  foreach ( $this->trxEndCallbacks as $key => $entry ) {
4267  if ( in_array( $entry[2], $sectionIds, true ) ) {
4268  $callback = $entry[0];
4269  $this->trxEndCallbacks[$key][0] = function () use ( $callback ) {
4270  return $callback( self::TRIGGER_ROLLBACK, $this );
4271  };
4272  // This "on resolution" callback no longer belongs to a section.
4273  $this->trxEndCallbacks[$key][2] = null;
4274  }
4275  }
4276  // Hoist callback ownership for section cancel callbacks to the new top section
4277  foreach ( $this->trxSectionCancelCallbacks as $key => $entry ) {
4278  if ( in_array( $entry[2], $sectionIds, true ) ) {
4279  $this->trxSectionCancelCallbacks[$key][2] = $newSectionId;
4280  }
4281  }
4282  }
4283 
4284  final public function setTransactionListener( $name, callable $callback = null ) {
4285  if ( $callback ) {
4286  $this->trxRecurringCallbacks[$name] = $callback;
4287  } else {
4288  unset( $this->trxRecurringCallbacks[$name] );
4289  }
4290  }
4291 
4300  final public function setTrxEndCallbackSuppression( $suppress ) {
4301  $this->trxEndCallbacksSuppressed = $suppress;
4302  }
4303 
4316  public function runOnTransactionIdleCallbacks( $trigger, array &$errors = [] ) {
4317  if ( $this->trxLevel() ) { // sanity
4318  throw new DBUnexpectedError( $this, __METHOD__ . ': a transaction is still open' );
4319  }
4320 
4321  if ( $this->trxEndCallbacksSuppressed ) {
4322  // Execution deferred by LoadBalancer for explicit execution later
4323  return 0;
4324  }
4325 
4326  $cs = $this->commenceCriticalSection( __METHOD__ );
4327 
4328  $count = 0;
4329  $autoTrx = $this->getFlag( self::DBO_TRX ); // automatic begin() enabled?
4330  // Drain the queues of transaction "idle" and "end" callbacks until they are empty
4331  do {
4332  $callbackEntries = array_merge(
4333  $this->trxPostCommitOrIdleCallbacks,
4334  $this->trxEndCallbacks,
4335  ( $trigger === self::TRIGGER_ROLLBACK )
4336  ? $this->trxSectionCancelCallbacks
4337  : [] // just consume them
4338  );
4339  $this->trxPostCommitOrIdleCallbacks = []; // consumed (and recursion guard)
4340  $this->trxEndCallbacks = []; // consumed (recursion guard)
4341  $this->trxSectionCancelCallbacks = []; // consumed (recursion guard)
4342 
4343  $count += count( $callbackEntries );
4344  foreach ( $callbackEntries as $entry ) {
4345  $this->clearFlag( self::DBO_TRX ); // make each query its own transaction
4346  try {
4347  $entry[0]( $trigger, $this );
4348  } catch ( DBError $ex ) {
4349  call_user_func( $this->errorLogger, $ex );
4350  $errors[] = $ex;
4351  // Some callbacks may use startAtomic/endAtomic, so make sure
4352  // their transactions are ended so other callbacks don't fail
4353  if ( $this->trxLevel() ) {
4354  $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
4355  }
4356  } finally {
4357  if ( $autoTrx ) {
4358  $this->setFlag( self::DBO_TRX ); // restore automatic begin()
4359  } else {
4360  $this->clearFlag( self::DBO_TRX ); // restore auto-commit
4361  }
4362  }
4363  }
4364  // @phan-suppress-next-line PhanImpossibleConditionInLoop
4365  } while ( count( $this->trxPostCommitOrIdleCallbacks ) );
4366 
4367  $this->completeCriticalSection( __METHOD__, $cs );
4368 
4369  return $count;
4370  }
4371 
4382  $count = 0;
4383 
4384  // Drain the queues of transaction "precommit" callbacks until it is empty
4385  do {
4386  $callbackEntries = $this->trxPreCommitOrIdleCallbacks;
4387  $this->trxPreCommitOrIdleCallbacks = []; // consumed (and recursion guard)
4388  $count += count( $callbackEntries );
4389  foreach ( $callbackEntries as $entry ) {
4390  try {
4391  // @phan-suppress-next-line PhanUndeclaredInvokeInCallable
4392  $entry[0]( $this );
4393  } catch ( Throwable $trxError ) {
4394  $this->setTransactionError( $trxError );
4395  throw $trxError;
4396  }
4397  }
4398  // @phan-suppress-next-line PhanImpossibleConditionInLoop
4399  } while ( $this->trxPreCommitOrIdleCallbacks );
4400 
4401  return $count;
4402  }
4403 
4411  private function runOnAtomicSectionCancelCallbacks( $trigger, array $sectionIds ) {
4412  // Drain the queue of matching "atomic section cancel" callbacks until there are none
4413  $unrelatedCallbackEntries = [];
4414  do {
4415  $callbackEntries = $this->trxSectionCancelCallbacks;
4416  $this->trxSectionCancelCallbacks = []; // consumed (recursion guard)
4417  foreach ( $callbackEntries as $entry ) {
4418  if ( in_array( $entry[2], $sectionIds, true ) ) {
4419  try {
4420  // @phan-suppress-next-line PhanUndeclaredInvokeInCallable
4421  $entry[0]( $trigger, $this );
4422  } catch ( Throwable $trxError ) {
4423  $this->setTransactionError( $trxError );
4424  throw $trxError;
4425  }
4426  } else {
4427  $unrelatedCallbackEntries[] = $entry;
4428  }
4429  }
4430  // @phan-suppress-next-line PhanImpossibleConditionInLoop
4431  } while ( $this->trxSectionCancelCallbacks );
4432 
4433  $this->trxSectionCancelCallbacks = $unrelatedCallbackEntries;
4434  }
4435 
4446  public function runTransactionListenerCallbacks( $trigger, array &$errors = [] ) {
4447  if ( $this->trxEndCallbacksSuppressed ) {
4448  // Execution deferred by LoadBalancer for explicit execution later
4449  return;
4450  }
4451 
4452  // These callbacks should only be registered in setup, thus no iteration is needed
4453  foreach ( $this->trxRecurringCallbacks as $callback ) {
4454  try {
4455  $callback( $trigger, $this );
4456  } catch ( DBError $ex ) {
4457  ( $this->errorLogger )( $ex );
4458  $errors[] = $ex;
4459  }
4460  }
4461  }
4462 
4470  $dbErrors = [];
4471  $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT, $dbErrors );
4472  $this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT, $dbErrors );
4473  $this->affectedRowCount = 0; // for the sake of consistency
4474  if ( $dbErrors ) {
4475  throw $dbErrors[0];
4476  }
4477  }
4478 
4487  $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
4488  $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
4489  $this->affectedRowCount = 0; // for the sake of consistency
4490  }
4491 
4503  protected function doSavepoint( $identifier, $fname ) {
4504  $sql = 'SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
4505  $this->query( $sql, $fname, self::QUERY_CHANGE_TRX );
4506  }
4507 
4519  protected function doReleaseSavepoint( $identifier, $fname ) {
4520  $sql = 'RELEASE SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
4521  $this->query( $sql, $fname, self::QUERY_CHANGE_TRX );
4522  }
4523 
4535  protected function doRollbackToSavepoint( $identifier, $fname ) {
4536  $sql = 'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
4537  $this->query( $sql, $fname, self::QUERY_CHANGE_TRX );
4538  }
4539 
4544  private function nextSavepointId( $fname ) {
4545  $savepointId = self::$SAVEPOINT_PREFIX . ++$this->trxAtomicCounter;
4546  if ( strlen( $savepointId ) > 30 ) {
4547  // 30 == Oracle's identifier length limit (pre 12c)
4548  // With a 22 character prefix, that puts the highest number at 99999999.
4549  throw new DBUnexpectedError(
4550  $this,
4551  'There have been an excessively large number of atomic sections in a transaction'
4552  . " started by $this->trxFname (at $fname)"
4553  );
4554  }
4555 
4556  return $savepointId;
4557  }
4558 
4559  final public function startAtomic(
4560  $fname = __METHOD__,
4561  $cancelable = self::ATOMIC_NOT_CANCELABLE
4562  ) {
4563  $cs = $this->commenceCriticalSection( __METHOD__ );
4564 
4565  if ( $this->trxLevel() ) {
4566  // This atomic section is only one part of a larger transaction
4567  $sectionOwnsTrx = false;
4568  } else {
4569  // Start an implicit transaction (sets trxAutomatic)
4570  try {
4571  $this->begin( $fname, self::TRANSACTION_INTERNAL );
4572  } catch ( DBError $e ) {
4573  $this->completeCriticalSection( __METHOD__, $cs );
4574  throw $e;
4575  }
4576  if ( $this->getFlag( self::DBO_TRX ) ) {
4577  // This DB handle participates in LoadBalancer transaction rounds; all atomic
4578  // sections should be buffered into one transaction (e.g. to keep web requests
4579  // transactional). Note that an implicit transaction round is considered to be
4580  // active when no there is no explicit transaction round.
4581  $sectionOwnsTrx = false;
4582  } else {
4583  // This DB handle does not participate in LoadBalancer transaction rounds;
4584  // each topmost atomic section will use its own transaction.
4585  $sectionOwnsTrx = true;
4586  }
4587  $this->trxAutomaticAtomic = $sectionOwnsTrx;
4588  }
4589 
4590  if ( $cancelable === self::ATOMIC_CANCELABLE ) {
4591  if ( $sectionOwnsTrx ) {
4592  // This atomic section is synonymous with the whole transaction; just
4593  // use full COMMIT/ROLLBACK in endAtomic()/cancelAtomic(), respectively
4594  $savepointId = self::$NOT_APPLICABLE;
4595  } else {
4596  // This atomic section is only part of the whole transaction; use a SAVEPOINT
4597  // query so that its changes can be cancelled without losing the rest of the
4598  // transaction (e.g. changes from other sections or from outside of sections)
4599  try {
4600  $savepointId = $this->nextSavepointId( $fname );
4601  $this->doSavepoint( $savepointId, $fname );
4602  } catch ( DBError $e ) {
4603  $this->completeCriticalSection( __METHOD__, $cs, $e );
4604  throw $e;
4605  }
4606  }
4607  } else {
4608  $savepointId = null;
4609  }
4610 
4611  $sectionId = new AtomicSectionIdentifier;
4612  $this->trxAtomicLevels[] = [ $fname, $sectionId, $savepointId ];
4613  $this->queryLogger->debug( 'startAtomic: entering level ' .
4614  ( count( $this->trxAtomicLevels ) - 1 ) . " ($fname)" );
4615 
4616  $this->completeCriticalSection( __METHOD__, $cs );
4617 
4618  return $sectionId;
4619  }
4620 
4621  final public function endAtomic( $fname = __METHOD__ ) {
4622  if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
4623  throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)" );
4624  }
4625 
4626  // Check if the current section matches $fname
4627  $pos = count( $this->trxAtomicLevels ) - 1;
4628  list( $savedFname, $sectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
4629  $this->queryLogger->debug( "endAtomic: leaving level $pos ($fname)" );
4630 
4631  if ( $savedFname !== $fname ) {
4632  throw new DBUnexpectedError(
4633  $this,
4634  "Invalid atomic section ended (got $fname but expected $savedFname)"
4635  );
4636  }
4637 
4638  $runPostCommitCallbacks = false;
4639 
4640  $cs = $this->commenceCriticalSection( __METHOD__ );
4641 
4642  // Remove the last section (no need to re-index the array)
4643  array_pop( $this->trxAtomicLevels );
4644 
4645  try {
4646  if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
4647  $this->commit( $fname, self::FLUSHING_INTERNAL );
4648  $runPostCommitCallbacks = true;
4649  } elseif ( $savepointId !== null && $savepointId !== self::$NOT_APPLICABLE ) {
4650  $this->doReleaseSavepoint( $savepointId, $fname );
4651  }
4652  } catch ( DBError $e ) {
4653  $this->completeCriticalSection( __METHOD__, $cs, $e );
4654  throw $e;
4655  }
4656 
4657  // Hoist callback ownership for callbacks in the section that just ended;
4658  // all callbacks should have an owner that is present in trxAtomicLevels.
4659  $currentSectionId = $this->currentAtomicSectionId();
4660  if ( $currentSectionId ) {
4661  $this->reassignCallbacksForSection( $sectionId, $currentSectionId );
4662  }
4663 
4664  $this->completeCriticalSection( __METHOD__, $cs );
4665 
4666  if ( $runPostCommitCallbacks ) {
4668  }
4669  }
4670 
4671  final public function cancelAtomic(
4672  $fname = __METHOD__,
4673  AtomicSectionIdentifier $sectionId = null
4674  ) {
4675  if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
4676  throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)" );
4677  }
4678 
4679  if ( $sectionId !== null ) {
4680  // Find the (last) section with the given $sectionId
4681  $pos = -1;
4682  foreach ( $this->trxAtomicLevels as $i => list( $asFname, $asId, $spId ) ) {
4683  if ( $asId === $sectionId ) {
4684  $pos = $i;
4685  }
4686  }
4687  if ( $pos < 0 ) {
4688  throw new DBUnexpectedError( $this, "Atomic section not found (for $fname)" );
4689  }
4690  } else {
4691  $pos = null;
4692  }
4693 
4694  $cs = $this->commenceCriticalSection( __METHOD__ );
4695 
4696  $excisedIds = [];
4697  $excisedFnames = [];
4698  $newTopSection = $this->currentAtomicSectionId();
4699  if ( $pos !== null ) {
4700  // Remove all descendant sections and re-index the array
4701  $len = count( $this->trxAtomicLevels );
4702  for ( $i = $pos + 1; $i < $len; ++$i ) {
4703  $excisedFnames[] = $this->trxAtomicLevels[$i][0];
4704  $excisedIds[] = $this->trxAtomicLevels[$i][1];
4705  }
4706  $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 );
4707  $newTopSection = $this->currentAtomicSectionId();
4708  }
4709 
4710  $runPostRollbackCallbacks = false;
4711  try {
4712  // Check if the current section matches $fname
4713  $pos = count( $this->trxAtomicLevels ) - 1;
4714  list( $savedFname, $savedSectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
4715 
4716  if ( $excisedFnames ) {
4717  $this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname) " .
4718  "and descendants " . implode( ', ', $excisedFnames ) );
4719  } else {
4720  $this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname)" );
4721  }
4722 
4723  if ( $savedFname !== $fname ) {
4724  $e = new DBUnexpectedError(
4725  $this,
4726  "Invalid atomic section ended (got $fname but expected $savedFname)"
4727  );
4728  $this->completeCriticalSection( __METHOD__, $cs, $e );
4729  throw $e;
4730  }
4731 
4732  // Remove the last section (no need to re-index the array)
4733  array_pop( $this->trxAtomicLevels );
4734  $excisedIds[] = $savedSectionId;
4735  $newTopSection = $this->currentAtomicSectionId();
4736 
4737  if ( $savepointId !== null ) {
4738  // Rollback the transaction changes proposed within this atomic section
4739  if ( $savepointId === self::$NOT_APPLICABLE ) {
4740  // Atomic section started the transaction; rollback the whole transaction
4741  // and trigger cancellation callbacks for all active atomic sections
4742  $this->rollback( $fname, self::FLUSHING_INTERNAL );
4743  $runPostRollbackCallbacks = true;
4744  } else {
4745  // Atomic section nested within the transaction; rollback the transaction
4746  // to the state prior to this section and trigger its cancellation callbacks
4747  $this->doRollbackToSavepoint( $savepointId, $fname );
4748  $this->trxStatus = self::STATUS_TRX_OK; // no exception; recovered
4749  $this->trxStatusIgnoredCause = null;
4750  $this->runOnAtomicSectionCancelCallbacks( self::TRIGGER_CANCEL, $excisedIds );
4751  }
4752  } elseif ( $this->trxStatus > self::STATUS_TRX_ERROR ) {
4753  // Put the transaction into an error state if it's not already in one
4754  $trxError = new DBUnexpectedError(
4755  $this,
4756  "Uncancelable atomic section canceled (got $fname)"
4757  );
4758  $this->setTransactionError( $trxError );
4759  }
4760  } finally {
4761  // Fix up callbacks owned by the sections that were just cancelled.
4762  // All callbacks should have an owner that is present in trxAtomicLevels.
4763  $this->modifyCallbacksForCancel( $excisedIds, $newTopSection );
4764  }
4765 
4766  $this->affectedRowCount = 0; // for the sake of consistency
4767 
4768  $this->completeCriticalSection( __METHOD__, $cs );
4769 
4770  if ( $runPostRollbackCallbacks ) {
4772  }
4773  }
4774 
4775  final public function doAtomicSection(
4776  $fname,
4777  callable $callback,
4778  $cancelable = self::ATOMIC_NOT_CANCELABLE
4779  ) {
4780  $sectionId = $this->startAtomic( $fname, $cancelable );
4781  try {
4782  $res = $callback( $this, $fname );
4783  } catch ( Throwable $e ) {
4784  // Avoid confusing error reporting during critical section errors
4785  if ( !$this->csmError ) {
4786  $this->cancelAtomic( $fname, $sectionId );
4787  }
4788 
4789  throw $e;
4790  }
4791  $this->endAtomic( $fname );
4792 
4793  return $res;
4794  }
4795 
4796  final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
4797  static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
4798  if ( !in_array( $mode, $modes, true ) ) {
4799  throw new DBUnexpectedError( $this, "$fname: invalid mode parameter '$mode'" );
4800  }
4801 
4802  // Protect against mismatched atomic section, transaction nesting, and snapshot loss
4803  if ( $this->trxLevel() ) {
4804  if ( $this->trxAtomicLevels ) {
4805  $levels = $this->flatAtomicSectionList();
4806  $msg = "$fname: got explicit BEGIN while atomic section(s) $levels are open";
4807  throw new DBUnexpectedError( $this, $msg );
4808  } elseif ( !$this->trxAutomatic ) {
4809  $msg = "$fname: explicit transaction already active (from {$this->trxFname})";
4810  throw new DBUnexpectedError( $this, $msg );
4811  } else {
4812  $msg = "$fname: implicit transaction already active (from {$this->trxFname})";
4813  throw new DBUnexpectedError( $this, $msg );
4814  }
4815  } elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
4816  $msg = "$fname: implicit transaction expected (DBO_TRX set)";
4817  throw new DBUnexpectedError( $this, $msg );
4818  }
4819 
4820  $this->assertHasConnectionHandle();
4821 
4822  $cs = $this->commenceCriticalSection( __METHOD__ );
4823  try {
4824  $this->doBegin( $fname );
4825  } catch ( DBError $e ) {
4826  $this->completeCriticalSection( __METHOD__, $cs );
4827  throw $e;
4828  }
4829  $this->trxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
4830  $this->trxStatus = self::STATUS_TRX_OK;
4831  $this->trxStatusIgnoredCause = null;
4832  $this->trxAtomicCounter = 0;
4833  $this->trxTimestamp = microtime( true );
4834  $this->trxFname = $fname;
4835  $this->trxDoneWrites = false;
4836  $this->trxAutomaticAtomic = false;
4837  $this->trxAtomicLevels = [];
4838  $this->trxWriteDuration = 0.0;
4839  $this->trxWriteQueryCount = 0;
4840  $this->trxWriteAffectedRows = 0;
4841  $this->trxWriteAdjDuration = 0.0;
4842  $this->trxWriteAdjQueryCount = 0;
4843  $this->trxWriteCallers = [];
4844  // With REPEATABLE-READ isolation, the first SELECT establishes the read snapshot,
4845  // so get the replication lag estimate before any transaction SELECT queries come in.
4846  // This way, the lag estimate reflects what will actually be read. Also, if heartbeat
4847  // tables are used, this avoids counting snapshot lag as part of replication lag.
4848  $this->trxReplicaLagStatus = null; // clear cached value first
4849  $this->trxReplicaLagStatus = $this->getApproximateLagStatus();
4850  // T147697: make explicitTrxActive() return true until begin() finishes. This way,
4851  // no caller triggered by getApproximateLagStatus() will think its OK to muck around
4852  // with the transaction just because startAtomic() has not yet finished updating the
4853  // tracking fields (e.g. trxAtomicLevels).
4854  $this->trxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
4855  $this->completeCriticalSection( __METHOD__, $cs );
4856  }
4857 
4866  protected function doBegin( $fname ) {
4867  $this->query( 'BEGIN', $fname, self::QUERY_CHANGE_TRX );
4868  }
4869 
4870  final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4871  static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
4872  if ( !in_array( $flush, $modes, true ) ) {
4873  throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'" );
4874  }
4875 
4876  if ( $this->trxLevel() && $this->trxAtomicLevels ) {
4877  // There are still atomic sections open; this cannot be ignored
4878  $levels = $this->flatAtomicSectionList();
4879  throw new DBUnexpectedError(
4880  $this,
4881  "$fname: got COMMIT while atomic sections $levels are still open"
4882  );
4883  }
4884 
4885  if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
4886  if ( !$this->trxLevel() ) {
4887  return; // nothing to do
4888  } elseif ( !$this->trxAutomatic ) {
4889  throw new DBUnexpectedError(
4890  $this,
4891  "$fname: flushing an explicit transaction, getting out of sync"
4892  );
4893  }
4894  } elseif ( !$this->trxLevel() ) {
4895  $this->queryLogger->error(
4896  "$fname: no transaction to commit, something got out of sync",
4897  [ 'exception' => new RuntimeException() ]
4898  );
4899 
4900  return; // nothing to do
4901  } elseif ( $this->trxAutomatic ) {
4902  throw new DBUnexpectedError(
4903  $this,
4904  "$fname: expected mass commit of all peer transactions (DBO_TRX set)"
4905  );
4906  }
4907 
4908  $this->assertHasConnectionHandle();
4909 
4911  $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
4912 
4913  $cs = $this->commenceCriticalSection( __METHOD__ );
4914  try {
4915  $this->doCommit( $fname );
4916  } catch ( DBError $e ) {
4917  $this->completeCriticalSection( __METHOD__, $cs );
4918  throw $e;
4919  }
4920  $oldTrxShortId = $this->consumeTrxShortId();
4921  $this->trxStatus = self::STATUS_TRX_NONE;
4922  if ( $this->trxDoneWrites ) {
4923  $this->lastWriteTime = microtime( true );
4924  $this->trxProfiler->transactionWritingOut(
4925  $this->getServerName(),
4926  $this->getDomainID(),
4927  $oldTrxShortId,
4928  $writeTime,
4929  $this->trxWriteAffectedRows
4930  );
4931  }
4932  // With FLUSHING_ALL_PEERS, callbacks will run when requested by a dedicated phase
4933  // within LoadBalancer. With FLUSHING_INTERNAL, callbacks will run when requested by
4934  // the Database caller during a safe point. This avoids isolation and recursion issues.
4935  if ( $flush === self::FLUSHING_ONE ) {
4937  }
4938  $this->completeCriticalSection( __METHOD__, $cs );
4939  }
4940 
4949  protected function doCommit( $fname ) {
4950  if ( $this->trxLevel() ) {
4951  $this->query( 'COMMIT', $fname, self::QUERY_CHANGE_TRX );
4952  }
4953  }
4954 
4955  final public function rollback( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4956  if (
4957  $flush !== self::FLUSHING_INTERNAL &&
4958  $flush !== self::FLUSHING_ALL_PEERS &&
4959  $this->getFlag( self::DBO_TRX )
4960  ) {
4961  throw new DBUnexpectedError(
4962  $this,
4963  "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)"
4964  );
4965  }
4966 
4967  if ( !$this->trxLevel() ) {
4968  // Clear any commit-dependant callbacks left over from transaction rounds
4969  $this->trxPostCommitOrIdleCallbacks = [];
4970  $this->trxPreCommitOrIdleCallbacks = [];
4971 
4972  return;
4973  }
4974 
4975  $this->assertHasConnectionHandle();
4976 
4977  $cs = $this->commenceCriticalSection( __METHOD__ );
4978  $this->doRollback( $fname );
4979  $oldTrxShortId = $this->consumeTrxShortId();
4980  $this->trxStatus = self::STATUS_TRX_NONE;
4981  $this->trxAtomicLevels = [];
4982  // Clear callbacks that depend on transaction or transaction round commit
4983  $this->trxPostCommitOrIdleCallbacks = [];
4984  $this->trxPreCommitOrIdleCallbacks = [];
4985  // Estimate the RTT via a query now that trxStatus is OK
4986  $writeTime = $this->pingAndCalculateLastTrxApplyTime();
4987  if ( $this->trxDoneWrites ) {
4988  $this->trxProfiler->transactionWritingOut(
4989  $this->getServerName(),
4990  $this->getDomainID(),
4991  $oldTrxShortId,
4992  $writeTime,
4993  $this->trxWriteAffectedRows
4994  );
4995  }
4996  // With FLUSHING_ALL_PEERS, callbacks will run when requested by a dedicated phase
4997  // within LoadBalancer. With FLUSHING_INTERNAL, callbacks will run when requested by
4998  // the Database caller during a safe point. This avoids isolation and recursion issues.
4999  if ( $flush === self::FLUSHING_ONE ) {
5001  }
5002  $this->completeCriticalSection( __METHOD__, $cs );
5003  }
5004 
5013  protected function doRollback( $fname ) {
5014  if ( $this->trxLevel() ) {
5015  # Disconnects cause rollback anyway, so ignore those errors
5016  $this->query( 'ROLLBACK', $fname, self::QUERY_SILENCE_ERRORS | self::QUERY_CHANGE_TRX );
5017  }
5018  }
5019 
5020  public function flushSnapshot( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
5021  if ( $this->explicitTrxActive() ) {
5022  // Committing this transaction would break callers that assume it is still open
5023  throw new DBUnexpectedError(
5024  $this,
5025  "$fname: Cannot flush snapshot; " .
5026  "explicit transaction '{$this->trxFname}' is still open"
5027  );
5028  } elseif ( $this->writesOrCallbacksPending() ) {
5029  // This only flushes transactions to clear snapshots, not to write data
5030  $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
5031  throw new DBUnexpectedError(
5032  $this,
5033  "$fname: Cannot flush snapshot; " .
5034  "writes from transaction {$this->trxFname} are still pending ($fnames)"
5035  );
5036  } elseif (
5037  $this->trxLevel() &&
5038  $this->getTransactionRoundId() &&
5039  $flush !== self::FLUSHING_INTERNAL &&
5040  $flush !== self::FLUSHING_ALL_PEERS
5041  ) {
5042  $this->queryLogger->warning(
5043  "$fname: Expected mass snapshot flush of all peer transactions " .
5044  "in the explicit transactions round '{$this->getTransactionRoundId()}'",
5045  [ 'exception' => new RuntimeException() ]
5046  );
5047  }
5048 
5049  $this->commit( $fname, self::FLUSHING_INTERNAL );
5050  }
5051 
5052  public function explicitTrxActive() {
5053  return $this->trxLevel() && ( $this->trxAtomicLevels || !$this->trxAutomatic );
5054  }
5055 
5060  public function duplicateTableStructure(
5061  $oldName,
5062  $newName,
5063  $temporary = false,
5064  $fname = __METHOD__
5065  ) {
5066  throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
5067  }
5068 
5073  public function listTables( $prefix = null, $fname = __METHOD__ ) {
5074  throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
5075  }
5076 
5081  public function listViews( $prefix = null, $fname = __METHOD__ ) {
5082  throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
5083  }
5084 
5089  public function timestamp( $ts = 0 ) {
5090  $t = new ConvertibleTimestamp( $ts );
5091  // Let errors bubble up to avoid putting garbage in the DB
5092  return $t->getTimestamp( TS_MW );
5093  }
5094 
5095  public function timestampOrNull( $ts = null ) {
5096  if ( $ts === null ) {
5097  return null;
5098  } else {
5099  return $this->timestamp( $ts );
5100  }
5101  }
5102 
5103  public function affectedRows() {
5104  return $this->affectedRowCount ?? $this->fetchAffectedRowCount();
5105  }
5106 
5110  abstract protected function fetchAffectedRowCount();
5111 
5112  public function ping( &$rtt = null ) {
5113  // Avoid hitting the server if it was hit recently
5114  if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::$PING_TTL ) {
5115  if ( !func_num_args() || $this->lastRoundTripEstimate > 0 ) {
5117  return true; // don't care about $rtt
5118  }
5119  }
5120 
5121  // This will reconnect if possible or return false if not
5122  $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_SILENCE_ERRORS | self::QUERY_CHANGE_NONE;
5123  $ok = ( $this->query( self::$PING_QUERY, __METHOD__, $flags ) !== false );
5124  if ( $ok ) {
5126  }
5127 
5128  return $ok;
5129  }
5130 
5137  protected function replaceLostConnection( $fname ) {
5138  $this->closeConnection();
5139  $this->conn = null;
5140 
5141  $this->handleSessionLossPreconnect();
5142 
5143  try {
5144  $this->open(
5145  $this->connectionParams[self::CONN_HOST],
5146  $this->connectionParams[self::CONN_USER],
5147  $this->connectionParams[self::CONN_PASSWORD],
5148  $this->currentDomain->getDatabase(),
5149  $this->currentDomain->getSchema(),
5150  $this->tablePrefix()
5151  );
5152  $this->lastPing = microtime( true );
5153  $ok = true;
5154 
5155  $this->connLogger->warning(
5156  $fname . ': lost connection to {db_server}; reconnected',
5157  $this->getLogContext( [ 'exception' => new RuntimeException() ] )
5158  );
5159  } catch ( DBConnectionError $e ) {
5160  $ok = false;
5161 
5162  $this->connLogger->error(
5163  $fname . ': lost connection to {db_server} permanently',
5164  $this->getLogContext( [ 'exception' => new RuntimeException() ] )
5165  );
5166  }
5167 
5169 
5170  return $ok;
5171  }
5172 
5173  public function getSessionLagStatus() {
5174  return $this->getRecordedTransactionLagStatus() ?: $this->getApproximateLagStatus();
5175  }
5176 
5190  final protected function getRecordedTransactionLagStatus() {
5191  return $this->trxLevel() ? $this->trxReplicaLagStatus : null;
5192  }
5193 
5203  protected function getApproximateLagStatus() {
5204  if ( $this->topologyRole === self::ROLE_STREAMING_REPLICA ) {
5205  // Avoid exceptions as this is used internally in critical sections
5206  try {
5207  $lag = $this->getLag();
5208  } catch ( DBError $e ) {
5209  $lag = false;
5210  }
5211  } else {
5212  $lag = 0;
5213  }
5214 
5215  return [ 'lag' => $lag, 'since' => microtime( true ) ];
5216  }
5217 
5240  public static function getCacheSetOptions( ?IDatabase ...$dbs ) {
5241  $res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
5242 
5243  foreach ( func_get_args() as $db ) {
5244  if ( $db instanceof IDatabase ) {
5245  $status = $db->getSessionLagStatus();
5246 
5247  if ( $status['lag'] === false ) {
5248  $res['lag'] = false;
5249  } elseif ( $res['lag'] !== false ) {
5250  $res['lag'] = max( $res['lag'], $status['lag'] );
5251  }
5252  $res['since'] = min( $res['since'], $status['since'] );
5253  $res['pending'] = $res['pending'] ?: $db->writesPending();
5254  }
5255  }
5256 
5257  return $res;
5258  }
5259 
5260  public function getLag() {
5261  if ( $this->topologyRole === self::ROLE_STREAMING_MASTER ) {
5262  return 0; // this is the primary DB
5263  } elseif ( $this->topologyRole === self::ROLE_STATIC_CLONE ) {
5264  return 0; // static dataset
5265  }
5266 
5267  return $this->doGetLag();
5268  }
5269 
5281  protected function doGetLag() {
5282  return 0;
5283  }
5284 
5289  public function maxListLen() {
5290  return 0;
5291  }
5292 
5297  public function encodeBlob( $b ) {
5298  return $b;
5299  }
5300 
5305  public function decodeBlob( $b ) {
5306  if ( $b instanceof Blob ) {
5307  $b = $b->fetch();
5308  }
5309  return $b;
5310  }
5311 
5316  public function setSessionOptions( array $options ) {
5317  }
5318 
5319  public function sourceFile(
5320  $filename,
5321  callable $lineCallback = null,
5322  callable $resultCallback = null,
5323  $fname = false,
5324  callable $inputCallback = null
5325  ) {
5326  AtEase::suppressWarnings();
5327  $fp = fopen( $filename, 'r' );
5328  AtEase::restoreWarnings();
5329 
5330  if ( $fp === false ) {
5331  throw new RuntimeException( "Could not open \"{$filename}\"" );
5332  }
5333 
5334  if ( !$fname ) {
5335  $fname = __METHOD__ . "( $filename )";
5336  }
5337 
5338  try {
5339  $error = $this->sourceStream(
5340  $fp,
5341  $lineCallback,
5342  $resultCallback,
5343  $fname,
5344  $inputCallback
5345  );
5346  } finally {
5347  fclose( $fp );
5348  }
5349 
5350  return $error;
5351  }
5352 
5353  public function setSchemaVars( $vars ) {
5354  $this->schemaVars = is_array( $vars ) ? $vars : null;
5355  }
5356 
5357  public function sourceStream(
5358  $fp,
5359  callable $lineCallback = null,
5360  callable $resultCallback = null,
5361  $fname = __METHOD__,
5362  callable $inputCallback = null
5363  ) {
5364  $delimiterReset = new ScopedCallback(
5365  function ( $delimiter ) {
5366  $this->delimiter = $delimiter;
5367  },
5368  [ $this->delimiter ]
5369  );
5370  $cmd = '';
5371 
5372  while ( !feof( $fp ) ) {
5373  if ( $lineCallback ) {
5374  call_user_func( $lineCallback );
5375  }
5376 
5377  $line = trim( fgets( $fp ) );
5378 
5379  if ( $line == '' ) {
5380  continue;
5381  }
5382 
5383  if ( $line[0] == '-' && $line[1] == '-' ) {
5384  continue;
5385  }
5386 
5387  if ( $cmd != '' ) {
5388  $cmd .= ' ';
5389  }
5390 
5391  $done = $this->streamStatementEnd( $cmd, $line );
5392 
5393  $cmd .= "$line\n";
5394 
5395  if ( $done || feof( $fp ) ) {
5396  $cmd = $this->replaceVars( $cmd );
5397 
5398  if ( $inputCallback ) {
5399  $callbackResult = $inputCallback( $cmd );
5400 
5401  if ( is_string( $callbackResult ) || !$callbackResult ) {
5402  $cmd = $callbackResult;
5403  }
5404  }
5405 
5406  if ( $cmd ) {
5407  $res = $this->query( $cmd, $fname );
5408 
5409  if ( $resultCallback ) {
5410  $resultCallback( $res, $this );
5411  }
5412 
5413  if ( $res === false ) {
5414  $err = $this->lastError();
5415 
5416  return "Query \"{$cmd}\" failed with error code \"$err\".\n";
5417  }
5418  }
5419  $cmd = '';
5420  }
5421  }
5422 
5423  ScopedCallback::consume( $delimiterReset );
5424  return true;
5425  }
5426 
5435  public function streamStatementEnd( &$sql, &$newLine ) {
5436  if ( $this->delimiter ) {
5437  $prev = $newLine;
5438  $newLine = preg_replace(
5439  '/' . preg_quote( $this->delimiter, '/' ) . '$/',
5440  '',
5441  $newLine
5442  );
5443  if ( $newLine != $prev ) {
5444  return true;
5445  }
5446  }
5447 
5448  return false;
5449  }
5450 
5472  protected function replaceVars( $ins ) {
5473  $vars = $this->getSchemaVars();
5474  return preg_replace_callback(
5475  '!
5476  /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
5477  \'\{\$ (\w+) }\' | # 3. addQuotes
5478  `\{\$ (\w+) }` | # 4. addIdentifierQuotes
5479  /\*\$ (\w+) \*/ # 5. leave unencoded
5480  !x',
5481  function ( $m ) use ( $vars ) {
5482  // Note: Because of <https://bugs.php.net/bug.php?id=51881>,
5483  // check for both nonexistent keys *and* the empty string.
5484  if ( isset( $m[1] ) && $m[1] !== '' ) {
5485  if ( $m[1] === 'i' ) {
5486  return $this->indexName( $m[2] );
5487  } else {
5488  return $this->tableName( $m[2] );
5489  }
5490  } elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
5491  return $this->addQuotes( $vars[$m[3]] );
5492  } elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
5493  return $this->addIdentifierQuotes( $vars[$m[4]] );
5494  } elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
5495  return $vars[$m[5]];
5496  } else {
5497  return $m[0];
5498  }
5499  },
5500  $ins
5501  );
5502  }
5503 
5510  protected function getSchemaVars() {
5511  return $this->schemaVars ?? $this->getDefaultSchemaVars();
5512  }
5513 
5523  protected function getDefaultSchemaVars() {
5524  return [];
5525  }
5526 
5530  public function lockIsFree( $lockName, $method ) {
5531  // RDBMs methods for checking named locks may or may not count this thread itself.
5532  // In MySQL, IS_FREE_LOCK() returns 0 if the thread already has the lock. This is
5533  // the behavior chosen by the interface for this method.
5534  if ( isset( $this->sessionNamedLocks[$lockName] ) ) {
5535  $lockIsFree = false;
5536  } else {
5537  $lockIsFree = $this->doLockIsFree( $lockName, $method );
5538  }
5539 
5540  return $lockIsFree;
5541  }
5542 
5552  protected function doLockIsFree( string $lockName, string $method ) {
5553  return true; // not implemented
5554  }
5555 
5559  public function lock( $lockName, $method, $timeout = 5, $flags = 0 ) {
5560  $lockTsUnix = $this->doLock( $lockName, $method, $timeout );
5561  if ( $lockTsUnix !== null ) {
5562  $locked = true;
5563  $this->sessionNamedLocks[$lockName] = $lockTsUnix;
5564  } else {
5565  $locked = false;
5566  $this->queryLogger->info( __METHOD__ . " failed to acquire lock '{lockname}'",
5567  [ 'lockname' => $lockName ] );
5568  }
5569 
5570  if ( $this->fieldHasBit( $flags, self::LOCK_TIMESTAMP ) ) {
5571  return $lockTsUnix;
5572  } else {
5573  return $locked;
5574  }
5575  }
5576 
5587  protected function doLock( string $lockName, string $method, int $timeout ) {
5588  return microtime( true ); // not implemented
5589  }
5590 
5594  public function unlock( $lockName, $method ) {
5595  $released = $this->doUnlock( $lockName, $method );
5596  if ( $released ) {
5597  unset( $this->sessionNamedLocks[$lockName] );
5598  } else {
5599  $this->queryLogger->warning( __METHOD__ . " failed to release lock '$lockName'\n" );
5600  }
5601 
5602  return $released;
5603  }
5604 
5614  protected function doUnlock( string $lockName, string $method ) {
5615  return true; // not implemented
5616  }
5617 
5618  public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
5619  if ( $this->writesOrCallbacksPending() ) {
5620  // This only flushes transactions to clear snapshots, not to write data
5621  $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
5622  throw new DBUnexpectedError(
5623  $this,
5624  "$fname: Cannot flush pre-lock snapshot; " .
5625  "writes from transaction {$this->trxFname} are still pending ($fnames)"
5626  );
5627  }
5628 
5629  if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
5630  return null;
5631  }
5632 
5633  $unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
5634  if ( $this->trxLevel() ) {
5635  // There is a good chance an exception was thrown, causing any early return
5636  // from the caller. Let any error handler get a chance to issue rollback().
5637  // If there isn't one, let the error bubble up and trigger server-side rollback.
5638  $this->onTransactionResolution(
5639  function () use ( $lockKey, $fname ) {
5640  $this->unlock( $lockKey, $fname );
5641  },
5642  $fname
5643  );
5644  } else {
5645  $this->unlock( $lockKey, $fname );
5646  }
5647  } );
5648 
5649  $this->commit( $fname, self::FLUSHING_INTERNAL );
5650 
5651  return $unlocker;
5652  }
5653 
5658  public function namedLocksEnqueue() {
5659  return false;
5660  }
5661 
5663  return true;
5664  }
5665 
5666  final public function lockTables( array $read, array $write, $method ) {
5667  if ( $this->writesOrCallbacksPending() ) {
5668  throw new DBUnexpectedError( $this, "Transaction writes or callbacks still pending" );
5669  }
5670 
5671  if ( $this->tableLocksHaveTransactionScope() ) {
5672  $this->startAtomic( $method );
5673  }
5674 
5675  return $this->doLockTables( $read, $write, $method );
5676  }
5677 
5687  protected function doLockTables( array $read, array $write, $method ) {
5688  return true;
5689  }
5690 
5691  final public function unlockTables( $method ) {
5692  if ( $this->tableLocksHaveTransactionScope() ) {
5693  $this->endAtomic( $method );
5694 
5695  return true; // locks released on COMMIT/ROLLBACK
5696  }
5697 
5698  return $this->doUnlockTables( $method );
5699  }
5700 
5708  protected function doUnlockTables( $method ) {
5709  return true;
5710  }
5711 
5712  public function dropTable( $table, $fname = __METHOD__ ) {
5713  if ( !$this->tableExists( $table, $fname ) ) {
5714  return false;
5715  }
5716 
5717  $this->doDropTable( $table, $fname );
5718 
5719  return true;
5720  }
5721 
5728  protected function doDropTable( $table, $fname ) {
5729  // https://mariadb.com/kb/en/drop-table/
5730  // https://dev.mysql.com/doc/refman/8.0/en/drop-table.html
5731  // https://www.postgresql.org/docs/9.2/sql-truncate.html
5732  $sql = "DROP TABLE " . $this->tableName( $table ) . " CASCADE";
5733  $this->query( $sql, $fname, self::QUERY_IGNORE_DBO_TRX );
5734  }
5735 
5736  public function truncate( $tables, $fname = __METHOD__ ) {
5737  $tables = is_array( $tables ) ? $tables : [ $tables ];
5738 
5739  $tablesTruncate = [];
5740  foreach ( $tables as $table ) {
5741  // Skip TEMPORARY tables with no writes nor sequence updates detected.
5742  // This mostly is an optimization for integration testing.
5743  if ( !$this->isPristineTemporaryTable( $table ) ) {
5744  $tablesTruncate[] = $table;
5745  }
5746  }
5747 
5748  if ( $tablesTruncate ) {
5749  $this->doTruncate( $tablesTruncate, $fname );
5750  }
5751  }
5752 
5759  protected function doTruncate( array $tables, $fname ) {
5760  foreach ( $tables as $table ) {
5761  $sql = "TRUNCATE TABLE " . $this->tableName( $table );
5762  $this->query( $sql, $fname, self::QUERY_CHANGE_SCHEMA );
5763  }
5764  }
5765 
5770  public function getInfinity() {
5771  return 'infinity';
5772  }
5773 
5774  public function encodeExpiry( $expiry ) {
5775  return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
5776  ? $this->getInfinity()
5777  : $this->timestamp( $expiry );
5778  }
5779 
5780  public function decodeExpiry( $expiry, $format = TS_MW ) {
5781  if ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) {
5782  return 'infinity';
5783  }
5784 
5785  return ConvertibleTimestamp::convert( $format, $expiry );
5786  }
5787 
5792  public function setBigSelects( $value = true ) {
5793  // no-op
5794  }
5795 
5796  public function isReadOnly() {
5797  return ( $this->getReadOnlyReason() !== false );
5798  }
5799 
5803  protected function getReadOnlyReason() {
5804  if ( $this->topologyRole === self::ROLE_STREAMING_REPLICA ) {
5805  return [ 'Server is configured as a read-only replica database.', 'role' ];
5806  } elseif ( $this->topologyRole === self::ROLE_STATIC_CLONE ) {
5807  return [ 'Server is configured as a read-only static clone database.', 'role' ];
5808  }
5809 
5810  $reason = $this->getLBInfo( self::LB_READ_ONLY_REASON );
5811  if ( is_string( $reason ) ) {
5812  return [ $reason, 'lb' ];
5813  }
5814 
5815  return false;
5816  }
5817 
5822  public function setTableAliases( array $aliases ) {
5823  $this->tableAliases = $aliases;
5824  }
5825 
5830  public function setIndexAliases( array $aliases ) {
5831  $this->indexAliases = $aliases;
5832  }
5833 
5840  final protected function fieldHasBit( int $flags, int $bit ) {
5841  return ( ( $flags & $bit ) === $bit );
5842  }
5843 
5856  protected function getBindingHandle() {
5857  if ( !$this->conn ) {
5858  throw new DBUnexpectedError(
5859  $this,
5860  'DB connection was already closed or the connection dropped'
5861  );
5862  }
5863 
5864  return $this->conn;
5865  }
5866 
5872  private function setTransactionError( Throwable $trxError ) {
5873  if ( $this->trxStatus > self::STATUS_TRX_ERROR ) {
5874  $this->trxStatus = self::STATUS_TRX_ERROR;
5875  $this->trxStatusCause = $trxError;
5876  }
5877  }
5878 
5919  protected function commenceCriticalSection( string $fname ) {
5920  if ( $this->csmError ) {
5921  throw new DBUnexpectedError(
5922  $this,
5923  "Cannot execute $fname critical section while session state is out of sync.\n\n" .
5924  $this->csmError->getMessage() . "\n" .
5925  $this->csmError->getTraceAsString()
5926  );
5927  }
5928 
5929  if ( $this->csmId ) {
5930  $csm = null; // fold into the outer critical section
5931  } elseif ( $this->csProvider ) {
5932  $csm = $this->csProvider->scopedEnter(
5933  $fname,
5934  null, // emergency limit (default)
5935  null, // emergency callback (default)
5936  function () use ( $fname ) {
5937  // Mark a critical section as having been aborted by an error
5938  $e = new RuntimeException( "A critical section from {$fname} has failed" );
5939  $this->csmError = $e;
5940  $this->csmId = null;
5941  }
5942  );
5943  $this->csmId = $csm->getId();
5944  $this->csmFname = $fname;
5945  } else {
5946  $csm = null; // not supported
5947  }
5948 
5949  return $csm;
5950  }
5951 
5962  protected function completeCriticalSection(
5963  string $fname,
5964  ?CriticalSectionScope $csm,
5965  Throwable $trxError = null
5966  ) {
5967  if ( $csm !== null ) {
5968  if ( $this->csmId === null ) {
5969  throw new LogicException( "$fname critical section is not active" );
5970  } elseif ( $csm->getId() !== $this->csmId ) {
5971  throw new LogicException(
5972  "$fname critical section is not the active ({$this->csmFname}) one"
5973  );
5974  }
5975 
5976  $csm->exit();
5977  $this->csmId = null;
5978  }
5979 
5980  if ( $trxError ) {
5981  $this->setTransactionError( $trxError );
5982  }
5983  }
5984 
5985  public function __toString() {
5986  // spl_object_id is PHP >= 7.2
5987  $id = function_exists( 'spl_object_id' )
5988  ? spl_object_id( $this )
5989  : spl_object_hash( $this );
5990 
5991  $description = $this->getType() . ' object #' . $id;
5992  if ( is_resource( $this->conn ) ) {
5993  $description .= ' (' . (string)$this->conn . ')'; // "resource id #<ID>"
5994  } elseif ( is_object( $this->conn ) ) {
5995  // spl_object_id is PHP >= 7.2
5996  $handleId = function_exists( 'spl_object_id' )
5997  ? spl_object_id( $this->conn )
5998  : spl_object_hash( $this->conn );
5999  $description .= " (handle id #$handleId)";
6000  }
6001 
6002  return $description;
6003  }
6004 
6009  public function __clone() {
6010  $this->connLogger->warning(
6011  "Cloning " . static::class . " is not recommended; forking connection",
6012  [ 'exception' => new RuntimeException() ]
6013  );
6014 
6015  if ( $this->isOpen() ) {
6016  // Open a new connection resource without messing with the old one
6017  $this->conn = null;
6018  $this->trxEndCallbacks = []; // don't copy
6019  $this->trxSectionCancelCallbacks = []; // don't copy
6020  $this->handleSessionLossPreconnect(); // no trx or locks anymore
6021  $this->open(
6022  $this->connectionParams[self::CONN_HOST],
6023  $this->connectionParams[self::CONN_USER],
6024  $this->connectionParams[self::CONN_PASSWORD],
6025  $this->currentDomain->getDatabase(),
6026  $this->currentDomain->getSchema(),
6027  $this->tablePrefix()
6028  );
6029  $this->lastPing = microtime( true );
6030  }
6031  }
6032 
6039  public function __sleep() {
6040  throw new RuntimeException( 'Database serialization may cause problems, since ' .
6041  'the connection is not restored on wakeup' );
6042  }
6043 
6047  public function __destruct() {
6048  if ( $this->trxLevel() && $this->trxDoneWrites ) {
6049  trigger_error( "Uncommitted DB writes (transaction from {$this->trxFname})" );
6050  }
6051 
6052  $danglingWriters = $this->pendingWriteAndCallbackCallers();
6053  if ( $danglingWriters ) {
6054  $fnames = implode( ', ', $danglingWriters );
6055  trigger_error( "DB transaction writes or callbacks still pending ($fnames)" );
6056  }
6057 
6058  if ( $this->conn ) {
6059  // Avoid connection leaks for sanity. Normally, resources close at script completion.
6060  // The connection might already be closed in PHP by now, so suppress warnings.
6061  AtEase::suppressWarnings();
6062  $this->closeConnection();
6063  AtEase::restoreWarnings();
6064  $this->conn = null;
6065  }
6066  }
6067 }
6068 
6072 class_alias( Database::class, 'DatabaseBase' );
6073 
6077 class_alias( Database::class, 'Database' );
Wikimedia\Rdbms\Database\CONN_INITIAL_TABLE_PREFIX
const CONN_INITIAL_TABLE_PREFIX
Table prefix to use on initial connection.
Definition: Database.php:277
Wikimedia\Rdbms\Database\implicitOrderby
implicitOrderby()
Returns true if this database does an implicit order by when the column has an index For example: SEL...
Definition: Database.php:699
LIST_OR
const LIST_OR
Definition: Defines.php:46
Wikimedia\Rdbms\Database\$schemaVars
array null $schemaVars
Current variables use for schema element placeholders.
Definition: Database.php:116
Wikimedia\Rdbms\Database\tablePrefix
tablePrefix( $prefix=null)
Get/set the table prefix.
Definition: Database.php:615
Wikimedia\Rdbms\Database\$sessionNamedLocks
array< string, float > $sessionNamedLocks
Map of (name => UNIX timestamp) for locks obtained via lock()
Definition: Database.php:124
Wikimedia\Rdbms\Database\$serverName
string null $serverName
Readible name or host/IP of the database server.
Definition: Database.php:89
Wikimedia\Rdbms\Database\getLastPHPError
getLastPHPError()
Definition: Database.php:950
Wikimedia\Rdbms\Database\makeKeyCollisionCondition
makeKeyCollisionCondition(array $rows, array $uniqueKey)
Build an SQL condition to find rows with matching key values to those in $rows.
Definition: Database.php:3502
Wikimedia\Rdbms\Database\setSessionOptions
setSessionOptions(array $options)
Override database's default behavior.$options include: 'connTimeout' : Set the connection timeout val...
Definition: Database.php:5316
Wikimedia\Rdbms\Database
Relational database abstraction object.
Definition: Database.php:52
Wikimedia\Rdbms\Database\limitResult
limitResult( $sql, $limit, $offset=false)
Construct a LIMIT query with optional offset.The SQL should be adjusted so that only the first $limit...
Definition: Database.php:3827
Wikimedia\Rdbms\Database\$trxDoneWrites
bool $trxDoneWrites
Whether possible write queries were done in the last transaction started.
Definition: Database.php:145
Wikimedia\Rdbms\Database\completeCriticalSection
completeCriticalSection(string $fname, ?CriticalSectionScope $csm, Throwable $trxError=null)
Demark the completion of a critical section of session/transaction state changes.
Definition: Database.php:5962
Wikimedia\Rdbms\Database\$trxEndCallbacksSuppressed
bool $trxEndCallbacksSuppressed
Whether to suppress triggering of transaction end callbacks.
Definition: Database.php:180
Wikimedia\Rdbms\Database\$DBO_MUTABLE
static int $DBO_MUTABLE
Bit field of all DBO_* flags that can be changed after connection.
Definition: Database.php:262
Wikimedia\Rdbms\Database\makeUpdateOptionsArray
makeUpdateOptionsArray( $options)
Make UPDATE options array for Database::makeUpdateOptions.
Definition: Database.php:2616
Wikimedia\Rdbms\Database\replaceLostConnection
replaceLostConnection( $fname)
Close any existing (dead) database connection and open a new connection.
Definition: Database.php:5137
Wikimedia\Rdbms\Database\$trxWriteAdjQueryCount
int $trxWriteAdjQueryCount
Number of write queries counted in trxWriteAdjDuration.
Definition: Database.php:165
Wikimedia\Rdbms\Database\getBindingHandle
getBindingHandle()
Get the underlying binding connection handle.
Definition: Database.php:5856
LIST_NAMES
const LIST_NAMES
Definition: Defines.php:45
Wikimedia\Rdbms\Database\$lastPhpError
string bool $lastPhpError
Definition: Database.php:192
Wikimedia\Rdbms\Database\canRecoverFromDisconnect
canRecoverFromDisconnect( $sql, $priorWritesPending)
Determine whether it is safe to retry queries after a database connection is lost.
Definition: Database.php:1654
Wikimedia\Rdbms\Database\doInsert
doInsert( $table, array $rows, $fname)
Definition: Database.php:2534
Wikimedia\Rdbms\Database\buildGroupConcatField
buildGroupConcatField( $delim, $table, $field, $conds='', $join_conds=[])
Build a GROUP_CONCAT or equivalent statement for a query.This is useful for combining a field for sev...
Definition: Database.php:2799
Wikimedia\Rdbms\Database\doInitConnection
doInitConnection()
Actually connect to the database over the wire (or to local files)
Definition: Database.php:357
Wikimedia\Rdbms\Database\isQuotedIdentifier
isQuotedIdentifier( $name)
Returns if the given identifier looks quoted or not according to the database convention for quoting ...
Definition: Database.php:3362
Wikimedia\Rdbms\IDatabase\getServerVersion
getServerVersion()
A string describing the current software version, like from mysql_get_server_info()
Wikimedia\Rdbms\Database\upsert
upsert( $table, array $rows, $uniqueKeys, array $set, $fname=__METHOD__)
Upsert the given row(s) into a table.
Definition: Database.php:3539
Wikimedia\Rdbms\Database\fieldExists
fieldExists( $table, $field, $fname=__METHOD__)
Determines whether a field exists in a table.
Definition: Database.php:2475
Wikimedia\Rdbms\Database\deadlockLoop
deadlockLoop(... $args)
Perform a deadlock-prone transaction.This function invokes a callback function to perform a set of wr...
Definition: Database.php:4029
Wikimedia\Rdbms\Database\bitOr
bitOr( $fieldLeft, $fieldRight)
string
Definition: Database.php:2783
Wikimedia\Rdbms\Database\update
update( $table, $set, $conds, $fname=__METHOD__, $options=[])
Update all rows in a table that match a given condition.
Definition: Database.php:2641
Wikimedia\Rdbms\Database\selectRowCount
selectRowCount( $tables, $var=' *', $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Get the number of rows in dataset.
Definition: Database.php:2145
Wikimedia\Rdbms\Database\ignoreIndexClause
ignoreIndexClause( $index)
IGNORE INDEX clause.
Definition: Database.php:3449
Wikimedia\Rdbms\Database\$sessionDirtyTempTables
array $sessionDirtyTempTables
Map of (table name => 1) for current TEMPORARY tables.
Definition: Database.php:128
Wikimedia\Rdbms\Database\getTopologyRootPrimary
getTopologyRootPrimary()
Get the readable name of the sole root primary DB server for the replication topology.
Definition: Database.php:590
Wikimedia\Rdbms\Database\trxStatus
trxStatus()
Definition: Database.php:611
Wikimedia\Rdbms\Database\fieldHasBit
fieldHasBit(int $flags, int $bit)
Definition: Database.php:5840
Wikimedia\Rdbms\Database\listTables
listTables( $prefix=null, $fname=__METHOD__)
List all tables on the database.Only show tables with this prefix, e.g. mw_ Calling function name arr...
Definition: Database.php:5073
Wikimedia\Rdbms\Database\indexUnique
indexUnique( $table, $index, $fname=__METHOD__)
Determines if a given index is unique.Calling function namebool
Definition: Database.php:2500
HashBagOStuff
Simple store for keeping values in an associative array for the current process.
Definition: HashBagOStuff.php:32
Wikimedia\Rdbms\Database\$SMALL_WRITE_ROWS
static int $SMALL_WRITE_ROWS
Assume an insert of this many rows or less should be fast to replicate.
Definition: Database.php:252
Wikimedia\Rdbms\Database\updateTrxWriteQueryTime
updateTrxWriteQueryTime( $sql, $runtime, $affected)
Update the estimated run-time of a query, not counting large row lock times.
Definition: Database.php:1569
Wikimedia\Rdbms\Database\makeWhereFrom2d
makeWhereFrom2d( $data, $baseKey, $subKey)
Build a partial where clause from a 2-d array such as used for LinkBatch.
Definition: Database.php:2735
Wikimedia\Rdbms\Database\$queryLogger
LoggerInterface $queryLogger
Definition: Database.php:60
Wikimedia\Rdbms\DatabaseDomain\newFromId
static newFromId( $domain)
Definition: DatabaseDomain.php:77
Wikimedia\Rdbms\Database\encodeBlob
encodeBlob( $b)
Some DBMSs have a special format for inserting into blob fields, they don't allow simple quoted strin...
Definition: Database.php:5297
Wikimedia\Rdbms\Database\tableNamesN
tableNamesN(... $tables)
Fetch a number of table names into an zero-indexed numerical array This is handy when you need to con...
Definition: Database.php:3136
Wikimedia\Rdbms\Database\getDomainID
getDomainID()
Return the currently selected domain ID.
Definition: Database.php:868
Wikimedia\Rdbms\Database\getClass
static getClass( $dbType, $driver=null)
Definition: Database.php:510
Wikimedia\Rdbms\Database\pingAndCalculateLastTrxApplyTime
pingAndCalculateLastTrxApplyTime()
Definition: Database.php:762
Wikimedia\Rdbms\Database\$PING_TTL
static float $PING_TTL
How long before it is worth doing a dummy query to test the connection.
Definition: Database.php:243
Wikimedia\Rdbms\Database\$topologyRole
string $topologyRole
Replication topology role of the server; one of the class ROLE_* constants.
Definition: Database.php:95
Wikimedia\Rdbms\Database\doReplace
doReplace( $table, array $identityKey, array $rows, $fname)
Definition: Database.php:3474
Wikimedia\Rdbms\Database\assertHasConnectionHandle
assertHasConnectionHandle()
Make sure there is an open connection handle (alive or not) as a sanity check.
Definition: Database.php:1069
Wikimedia\Rdbms\Database\onTransactionPreCommitOrIdle
onTransactionPreCommitOrIdle(callable $callback, $fname=__METHOD__)
Run a callback before the current transaction commits or now if there is none.
Definition: Database.php:4147
Wikimedia\Rdbms\Database\tableLocksHaveTransactionScope
tableLocksHaveTransactionScope()
Checks if table locks acquired by lockTables() are transaction-bound in their scope.
Definition: Database.php:5662
Wikimedia\Rdbms\Database\wasErrorReissuable
wasErrorReissuable()
Determines if the last query error was due to something outside of the query itself.
Definition: Database.php:3995
Wikimedia\Rdbms\Database\CONN_USER
const CONN_USER
Database server username to use on all connections.
Definition: Database.php:269
Wikimedia\Rdbms\Database\deleteJoin
deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname=__METHOD__)
DELETE where the condition is a join.MySQL overrides this to use a multi-table DELETE syntax,...
Definition: Database.php:3598
Wikimedia\Rdbms\Database\$trxPostCommitOrIdleCallbacks
array[] $trxPostCommitOrIdleCallbacks
List of (callable, method name, atomic section id)
Definition: Database.php:167
Wikimedia\Rdbms\Database\$csmFname
string null $csmFname
Last critical section caller name.
Definition: Database.php:199
Wikimedia\Rdbms\Database\CONN_INITIAL_DB
const CONN_INITIAL_DB
Database name to use on initial connection.
Definition: Database.php:273
Wikimedia\Rdbms\Database\setIndexAliases
setIndexAliases(array $aliases)
Convert certain index names to alternative names before querying the DB.Note that this applies to ind...
Definition: Database.php:5830
Wikimedia\Rdbms\Database\decodeBlob
decodeBlob( $b)
Some DBMSs return a special placeholder object representing blob fields in result objects....
Definition: Database.php:5305
Wikimedia\Rdbms\Database\unionConditionPermutations
unionConditionPermutations( $table, $vars, array $permute_conds, $extra_conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Construct a UNION query for permutations of conditions.
Definition: Database.php:3859
Wikimedia\Rdbms\Database\factory
static factory( $type, $params=[], $connect=self::NEW_CONNECTED)
Construct a Database subclass instance given a database type and parameters.
Definition: Database.php:436
Wikimedia\Rdbms\Database\maxListLen
maxListLen()
Return the maximum number of items allowed in a list, or 0 for unlimited.int
Definition: Database.php:5289
Wikimedia\Rdbms\Database\selectDomain
selectDomain( $domain)
Set the current domain (database, schema, and table prefix)
Definition: Database.php:2977
Wikimedia\Rdbms\Database\buildIntegerCast
buildIntegerCast( $field)
Field or column to cast string 1.31
Definition: Database.php:2946
Wikimedia\Rdbms\Database\getServerUptime
getServerUptime()
Determines how long the server has been up.int
Definition: Database.php:3959
LIST_AND
const LIST_AND
Definition: Defines.php:43
Wikimedia\Rdbms\Database\prependDatabaseOrSchema
prependDatabaseOrSchema( $namespace, $relation, $format)
Definition: Database.php:3115
Wikimedia\Rdbms\Database\$delimiter
string $delimiter
Current SQL query delimiter.
Definition: Database.php:110
Wikimedia\Rdbms\Database\lock
lock( $lockName, $method, $timeout=5, $flags=0)
Acquire a named lock.Named locks are not related to transactionsName of lock to aquire Name of the ca...
Definition: Database.php:5559
Wikimedia\Rdbms\Database\__destruct
__destruct()
Run a few simple sanity checks and close dangling connections.
Definition: Database.php:6047
Wikimedia\Rdbms\Database\endAtomic
endAtomic( $fname=__METHOD__)
Ends an atomic section of SQL statements.
Definition: Database.php:4621
Wikimedia\Rdbms\Database\indexName
indexName( $index)
Allows for index remapping in queries where this is not consistent across DBMS.
Definition: Database.php:3321
Wikimedia\Rdbms\Database\executeQuery
executeQuery( $sql, $fname, $flags)
Execute a query, retrying it if there is a recoverable connection loss.
Definition: Database.php:1353
Wikimedia\Rdbms\IDatabase\lastError
lastError()
Get a description of the last error.
DBO_DEBUG
const DBO_DEBUG
Definition: defines.php:9
Wikimedia\Rdbms\Database\setBigSelects
setBigSelects( $value=true)
Allow or deny "big selects" for this session only.This is done by setting the sql_big_selects session...
Definition: Database.php:5792
Wikimedia\Rdbms\Database\doDropTable
doDropTable( $table, $fname)
Definition: Database.php:5728
Wikimedia\Rdbms\DBReadOnlyRoleError
Exception class for attempted DB write access to a DBConnRef with the DB_REPLICA role.
Definition: DBReadOnlyRoleError.php:30
Wikimedia\Rdbms\DBTransactionStateError
@newable
Definition: DBTransactionStateError.php:28
Wikimedia\Rdbms
Definition: ChronologyProtector.php:24
Wikimedia\Rdbms\Database\timestampOrNull
timestampOrNull( $ts=null)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
Definition: Database.php:5095
Wikimedia\Rdbms\Database\decodeExpiry
decodeExpiry( $expiry, $format=TS_MW)
Decode an expiry time into a DBMS independent format.
Definition: Database.php:5780
Wikimedia\Rdbms\Database\$trxAutomaticAtomic
bool $trxAutomaticAtomic
Whether the current transaction was started implicitly by startAtomic()
Definition: Database.php:153
Wikimedia\Rdbms\Database\initConnection
initConnection()
Initialize the connection to the database over the wire (or to local files)
Definition: Database.php:343
Wikimedia\Rdbms\Database\normalizeConditions
normalizeConditions( $conds, $fname)
Definition: Database.php:2248
Wikimedia\Rdbms\Database\anyString
anyString()
Returns a token for buildLike() that denotes a '' to be used in a LIKE query.
Definition: Database.php:3415
Wikimedia\Rdbms\Database\$server
string null $server
Server that this instance is currently connected to.
Definition: Database.php:83
Wikimedia\Rdbms\Database\unionSupportsOrderAndLimit
unionSupportsOrderAndLimit()
Determine if the RDBMS supports ORDER BY and LIMIT for separate subqueries within UNION....
Definition: Database.php:3845
Wikimedia\Rdbms\Database\isInsertSelectSafe
isInsertSelectSafe(array $insertOptions, array $selectOptions)
Definition: Database.php:3711
LIST_SET
const LIST_SET
Definition: Defines.php:44
Wikimedia\Rdbms\Database\fieldNameWithAlias
fieldNameWithAlias( $name, $alias=false)
Get an aliased field name e.g.
Definition: Database.php:3186
Wikimedia\Rdbms\Database\assertIsWritableMaster
assertIsWritableMaster()
Definition: Database.php:1097
Wikimedia\Rdbms\Database\normalizeOptions
normalizeOptions( $options)
Definition: Database.php:2341
Wikimedia\Rdbms\Database\getDefaultSchemaVars
getDefaultSchemaVars()
Get schema variables to use if none have been set via setSchemaVars().
Definition: Database.php:5523
BagOStuff
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:86
Wikimedia\Rdbms\Database\primaryPosWait
primaryPosWait(DBPrimaryPos $pos, $timeout)
Wait for the replica DB to catch up to a given primary DB position.Note that this does not start any ...
Definition: Database.php:4069
Wikimedia\Rdbms\GeneralizedSql
Lazy-loaded wrapper for simplification and scrubbing of SQL queries for profiling.
Definition: GeneralizedSql.php:30
Wikimedia\Rdbms\Database\extractSingleFieldFromList
extractSingleFieldFromList( $var)
Definition: Database.php:2443
Wikimedia\Rdbms\Database\duplicateTableStructure
duplicateTableStructure( $oldName, $newName, $temporary=false, $fname=__METHOD__)
Creates a new table with structure copied from existing table.Note that unlike most database abstract...
Definition: Database.php:5060
Wikimedia\Rdbms\Database\$trxSectionCancelCallbacks
array[] $trxSectionCancelCallbacks
List of (callable, method name, atomic section id)
Definition: Database.php:176
Wikimedia\Rdbms\Database\doTruncate
doTruncate(array $tables, $fname)
Definition: Database.php:5759
Wikimedia\Rdbms\Database\onTransactionCommitOrIdle
onTransactionCommitOrIdle(callable $callback, $fname=__METHOD__)
Run a callback when the current transaction commits or now if there is none.
Definition: Database.php:4120
Wikimedia\Rdbms\Database\registerTempWrites
registerTempWrites( $ret, array $changes)
Definition: Database.php:1276
Wikimedia\Rdbms\Database\namedLocksEnqueue
namedLocksEnqueue()
Check to see if a named lock used by lock() use blocking queues.bool 1.26
Definition: Database.php:5658
Wikimedia\Rdbms\Database\buildSubstring
buildSubstring( $input, $startPosition, $length=null)
Definition: Database.php:2870
Wikimedia\Rdbms\Database\tableNamesWithIndexClauseOrJOIN
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:3222
Wikimedia\Rdbms\Database\getReplicaPos
getReplicaPos()
Get the replication position of this replica DB.DBPrimaryPos|bool False if this is not a replica DB I...
Definition: Database.php:4086
Wikimedia\Rdbms\Database\getSchemaVars
getSchemaVars()
Get schema variables.
Definition: Database.php:5510
Wikimedia\Rdbms\Database\fieldName
fieldName(IResultWrapper $res, $n)
Get a field name in a result object.
Definition: Database.php:892
Wikimedia\Rdbms\Database\setLogger
setLogger(LoggerInterface $logger)
Set the PSR-3 logger interface to use for query logging.
Definition: Database.php:574
Wikimedia\Rdbms\Database\__clone
__clone()
Make sure that copies do not share the same client binding handle.
Definition: Database.php:6009
$res
$res
Definition: testCompression.php:57
Wikimedia\Rdbms\Database\cancelAtomic
cancelAtomic( $fname=__METHOD__, AtomicSectionIdentifier $sectionId=null)
Cancel an atomic section of SQL statements.
Definition: Database.php:4671
$base
$base
Definition: generateLocalAutoload.php:11
Wikimedia\Rdbms\Database\getFlag
getFlag( $flag)
Returns a boolean whether the flag $flag is set for this connection.
Definition: Database.php:864
Wikimedia\Rdbms\Database\doInsertSelectGeneric
doInsertSelectGeneric( $destTable, $srcTable, array $varMap, $conds, $fname, array $insertOptions, array $selectOptions, $selectJoinConds)
Implementation of insertSelect() based on select() and insert()
Definition: Database.php:3729
Wikimedia\Rdbms\Database\$trxAtomicLevels
array $trxAtomicLevels
List of (name, unique ID, savepoint ID) for each active atomic section level.
Definition: Database.php:151
Wikimedia\Rdbms\DBError
Database error base class @newable.
Definition: DBError.php:32
Wikimedia\Rdbms\Database\$PING_QUERY
static string $PING_QUERY
Dummy SQL query.
Definition: Database.php:245
LIST_COMMA
const LIST_COMMA
Definition: Defines.php:42
Wikimedia\Rdbms\Database\handleSessionLossPostconnect
handleSessionLossPostconnect()
Clean things up after session (and thus transaction) loss after reconnect.
Definition: Database.php:1718
Wikimedia\Rdbms\Database\isPristineTemporaryTable
isPristineTemporaryTable( $table)
Check if the table is both a TEMPORARY table and has not yet received CRUD operations.
Definition: Database.php:1307
Wikimedia\Rdbms\Database\$connectionVariables
string[] int[] float[] $connectionVariables
SQL variables values to use for all new connections.
Definition: Database.php:101
Wikimedia\Rdbms\Database\$lastQuery
string $lastQuery
The last SQL query attempted.
Definition: Database.php:188
Wikimedia\Rdbms\Database\$trxStatusIgnoredCause
array null $trxStatusIgnoredCause
Error details of the last statement-only rollback.
Definition: Database.php:137
DBO_TRX
const DBO_TRX
Definition: defines.php:12
Wikimedia\Rdbms\Database\textFieldSize
textFieldSize( $table, $field)
Returns the size of a text field, or -1 for "unlimited".int
Definition: Database.php:3625
Wikimedia\Rdbms\Database\$affectedRowCount
int null $affectedRowCount
Rows affected by the last query to query() or its CRUD wrappers.
Definition: Database.php:183
Wikimedia\Rdbms\Database\buildConcat
buildConcat( $stringList)
Build a concatenation list to feed into a SQL query.Raw SQL expression list; caller is responsible fo...
Definition: Database.php:2791
Wikimedia\Rdbms\Database\bitNot
bitNot( $field)
string
Definition: Database.php:2767
Wikimedia\Rdbms\Database\nextSavepointId
nextSavepointId( $fname)
Definition: Database.php:4544
Wikimedia\Rdbms\Database\$agent
string $agent
Agent name for query profiling.
Definition: Database.php:93
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
Wikimedia\Rdbms\Database\__sleep
__sleep()
Called by serialize.
Definition: Database.php:6039
DBO_NOBUFFER
const DBO_NOBUFFER
Definition: defines.php:10
Wikimedia\Rdbms\Database\assertValidUpsertSetArray
assertValidUpsertSetArray(array $set, array $identityKey, array $rows)
Definition: Database.php:2384
Wikimedia\Rdbms\Database\getTopologyRole
getTopologyRole()
Get the replication topology role of this server.
Definition: Database.php:586
Wikimedia\Rdbms\Database\doBegin
doBegin( $fname)
Issues the BEGIN command to the database server.
Definition: Database.php:4866
Wikimedia\Rdbms\Database\isWriteQuery
isWriteQuery( $sql, $flags)
Determine whether a query writes to the DB.
Definition: Database.php:1154
Wikimedia\Rdbms\DBPrimaryPos
An object representing a primary or replica DB position in a replicated setup.
Definition: DBPrimaryPos.php:15
Wikimedia\Rdbms\Database\isFlagInOptions
isFlagInOptions( $option, array $options)
Definition: Database.php:2429
Wikimedia\Rdbms\Database\clearFlag
clearFlag( $flag, $remember=self::REMEMBER_NOTHING)
Clear a flag for this connection.
Definition: Database.php:836
Wikimedia\Rdbms\Database\getQueryExceptionAndLog
getQueryExceptionAndLog( $error, $errno, $sql, $fname)
Definition: Database.php:1779
Wikimedia\Rdbms\Database\begin
begin( $fname=__METHOD__, $mode=self::TRANSACTION_EXPLICIT)
Begin a transaction.
Definition: Database.php:4796
Wikimedia\Rdbms\Database\doHandleSessionLossPreconnect
doHandleSessionLossPreconnect()
Reset any additional subclass trx* and session* fields.
Definition: Database.php:1711
Wikimedia\Rdbms\Database\$lastWriteTime
float bool $lastWriteTime
UNIX timestamp of last write query.
Definition: Database.php:190
Wikimedia\Rdbms\Database\doQuery
doQuery( $sql)
Run a query and return a DBMS-dependent wrapper or boolean.
Wikimedia\Rdbms\Database\fetchObject
fetchObject(IResultWrapper $res)
Fetch the next row from the given result object, in object form.
Definition: Database.php:872
Wikimedia\Rdbms\Database\close
close( $fname=__METHOD__, $owner=null)
Close the database connection.
Definition: Database.php:990
Wikimedia\Rdbms\Database\replaceVars
replaceVars( $ins)
Database-independent variable replacement.
Definition: Database.php:5472
Wikimedia\Rdbms\Database\handleSessionLossPreconnect
handleSessionLossPreconnect()
Clean things up after session (and thus transaction) loss before reconnect.
Definition: Database.php:1679
Wikimedia\Rdbms\Database\doInsertNonConflicting
doInsertNonConflicting( $table, array $rows, $fname)
Definition: Database.php:2551
Wikimedia\Rdbms\Database\$NOT_APPLICABLE
static string $NOT_APPLICABLE
Idiom used when a cancelable atomic section started the transaction.
Definition: Database.php:226
Wikimedia\Rdbms\Database\$trxStatus
int $trxStatus
Transaction status.
Definition: Database.php:133
Wikimedia\Rdbms\Database\runOnTransactionPreCommitCallbacks
runOnTransactionPreCommitCallbacks()
Consume and run any "on transaction pre-commit" callbacks.
Definition: Database.php:4381
Wikimedia\Rdbms\Database\wasLockTimeout
wasLockTimeout()
Determines if the last failure was due to a lock timeout.Note that during a lock wait timeout,...
Definition: Database.php:3975
Wikimedia\Rdbms\Database\$currentDomain
DatabaseDomain $currentDomain
Definition: Database.php:73
Wikimedia\Rdbms\Database\getLag
getLag()
Get the amount of replication lag for this database server.
Definition: Database.php:5260
Wikimedia\Rdbms\Database\$sessionTempTables
array $sessionTempTables
Map of (table name => 1) for current TEMPORARY tables.
Definition: Database.php:126
Wikimedia\Rdbms\Database\buildSelectSubquery
buildSelectSubquery( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Equivalent to IDatabase::selectSQLText() except wraps the result in Subquery.
Definition: Database.php:2950
Wikimedia\Rdbms\Database\$TEMP_PSEUDO_PERMANENT
static int $TEMP_PSEUDO_PERMANENT
Writes to this temporary table effect lastDoneWrites()
Definition: Database.php:233
Wikimedia\Rdbms\Database\getCacheSetOptions
static getCacheSetOptions(?IDatabase ... $dbs)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
Definition: Database.php:5240
Wikimedia\Rdbms\Database\assertBuildSubstringParams
assertBuildSubstringParams( $startPosition, $length)
Check type and bounds for parameters to self::buildSubstring()
Definition: Database.php:2891
Wikimedia\Rdbms\Database\isReadOnly
isReadOnly()
Definition: Database.php:5796
Wikimedia\Rdbms\Database\buildStringCast
buildStringCast( $field)
Field or column to cast string 1.28
Definition: Database.php:2936
Wikimedia\Rdbms\Database\$lazyMasterHandle
IDatabase null $lazyMasterHandle
Lazy handle to the primary DB this server replicates from.
Definition: Database.php:80
Wikimedia\Rdbms\Database\unlockTables
unlockTables( $method)
Unlock all tables locked via lockTables()
Definition: Database.php:5691
Wikimedia\Rdbms\Database\getSessionLagStatus
getSessionLagStatus()
Get the replica DB lag when the current transaction started or a general lag estimate if not transact...
Definition: Database.php:5173
Wikimedia\Rdbms\Database\$trxWriteDuration
float $trxWriteDuration
Seconds spent in write queries for the current transaction.
Definition: Database.php:157
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Definition: GlobalFunctions.php:997
Wikimedia\Rdbms\Database\doRollbackToSavepoint
doRollbackToSavepoint( $identifier, $fname)
Rollback to a savepoint.
Definition: Database.php:4535
Wikimedia\Rdbms\Database\dropTable
dropTable( $table, $fname=__METHOD__)
Delete a table.
Definition: Database.php:5712
Wikimedia\Rdbms\Database\$csmId
int null $csmId
Current critical section numeric ID.
Definition: Database.php:197
Wikimedia\Rdbms\Database\getTempTableWrites
getTempTableWrites( $sql, $pseudoPermanent)
Definition: Database.php:1220
Wikimedia\Rdbms\Database\doInsertSelectNative
doInsertSelectNative( $destTable, $srcTable, array $varMap, $conds, $fname, array $insertOptions, array $selectOptions, $selectJoinConds)
Native server-side implementation of insertSelect() for situations where we don't want to select ever...
Definition: Database.php:3794
Wikimedia\Rdbms\Database\startAtomic
startAtomic( $fname=__METHOD__, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Begin an atomic section of SQL statements.
Definition: Database.php:4559
Wikimedia\Rdbms\DBQueryTimeoutError
Error thrown when a query times out.
Definition: DBQueryTimeoutError.php:30
Wikimedia\Rdbms\Database\selectOptionsIncludeLocking
selectOptionsIncludeLocking( $options)
Definition: Database.php:2178
Wikimedia\Rdbms\IResultWrapper
Result wrapper for grabbing data queried from an IDatabase object.
Definition: IResultWrapper.php:26
Wikimedia\Rdbms\Database\getTransactionRoundId
getTransactionRoundId()
Definition: Database.php:732
Wikimedia\Rdbms\Database\trxLevel
trxLevel()
Gets the current transaction level.
Definition: Database.php:599
Wikimedia\Rdbms\Database\setFlag
setFlag( $flag, $remember=self::REMEMBER_NOTHING)
Set a flag for this connection.
Definition: Database.php:821
Wikimedia\Rdbms\Database\rollback
rollback( $fname=__METHOD__, $flush=self::FLUSHING_ONE)
Rollback a transaction previously started using begin()
Definition: Database.php:4955
Wikimedia\Rdbms\Database\assertQueryIsCurrentlyAllowed
assertQueryIsCurrentlyAllowed( $sql, $fname)
Error out if the DB is not in a valid state for a query via query()
Definition: Database.php:1599
Wikimedia\Rdbms\Database\setTransactionListener
setTransactionListener( $name, callable $callback=null)
Run a callback after each time any transaction commits or rolls back.
Definition: Database.php:4284
Wikimedia\Rdbms\Database\getPrimaryPos
getPrimaryPos()
Get the position of this primary DB.DBPrimaryPos|bool False if this is not a primary DB If an error o...
Definition: Database.php:4095
Wikimedia\Rdbms\Database\escapeLikeInternal
escapeLikeInternal( $s, $escapeChar='`')
Definition: Database.php:3372
Wikimedia\Rdbms\Database\$tableAliases
array[] $tableAliases
Current map of (table => (dbname, schema, prefix) map)
Definition: Database.php:112
Wikimedia\Rdbms\Database\reportQueryError
reportQueryError( $error, $errno, $sql, $fname, $ignore=false)
Report a query error.
Definition: Database.php:1764
Wikimedia\Rdbms\Database\$cliMode
bool $cliMode
Whether this PHP instance is for a CLI script.
Definition: Database.php:91
Wikimedia\Rdbms\Database\numRows
numRows( $res)
Get the number of rows in a query result.
Definition: Database.php:880
Wikimedia\Rdbms\Database\$trxShortId
string $trxShortId
ID of the active transaction or the empty string otherwise.
Definition: Database.php:131
Wikimedia\Rdbms\Database\isTransactableQuery
isTransactableQuery( $sql)
Determine whether a SQL statement is sensitive to isolation level.
Definition: Database.php:1204
Wikimedia\Rdbms\Database\lockForUpdate
lockForUpdate( $table, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Lock all rows meeting the given conditions/options FOR UPDATE.
Definition: Database.php:2459
Wikimedia\Rdbms\Database\indexExists
indexExists( $table, $index, $fname=__METHOD__)
Determines whether an index exists.
Definition: Database.php:2481
Wikimedia\Rdbms\Database\writesOrCallbacksPending
writesOrCallbacksPending()
Whether there is a transaction open with either possible write queries or unresolved pre-commit/commi...
Definition: Database.php:715
Wikimedia\Rdbms\Database\sourceStream
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:5357
Wikimedia\Rdbms\Database\selectSQLText
selectSQLText( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Take the same arguments as IDatabase::select() and return the SQL it would use.This can be useful for...
Definition: Database.php:2019
Wikimedia\Rdbms\SelectQueryBuilder
Definition: SelectQueryBuilder.php:11
Wikimedia\Rdbms\Database\makeInsertLists
makeInsertLists(array $rows)
Make SQL lists of columns, row tuples for INSERT/VALUES expressions.
Definition: Database.php:2580
$args
if( $line===false) $args
Definition: mcc.php:124
Wikimedia\Rdbms\Database\$TEMP_NORMAL
static int $TEMP_NORMAL
Writes to this temporary table do not affect lastDoneWrites()
Definition: Database.php:231
Wikimedia\Rdbms\Database\$trxWriteCallers
string[] $trxWriteCallers
Write query callers of the current transaction.
Definition: Database.php:155
Wikimedia\Rdbms\Database\$lastPing
float $lastPing
UNIX timestamp.
Definition: Database.php:186
Wikimedia\Rdbms\Database\bitAnd
bitAnd( $fieldLeft, $fieldRight)
string
Definition: Database.php:2775
Wikimedia\Rdbms\Database\buildLike
buildLike( $param,... $params)
LIKE statement wrapper.This takes a variable-length argument list with parts of pattern to match cont...
Definition: Database.php:3384
Wikimedia\Rdbms\Database\$trxProfiler
TransactionProfiler $trxProfiler
Definition: Database.php:70
Wikimedia\Rdbms\Database\assertNoOpenTransactions
assertNoOpenTransactions()
Assert that all explicit transactions or atomic sections have been closed.
Definition: Database.php:1635
Wikimedia\Rdbms\Database\selectDB
selectDB( $db)
Change the current database.
Definition: Database.php:2967
Wikimedia\Rdbms\Database\wasQueryTimeout
wasQueryTimeout( $error, $errno)
Checks whether the cause of the error is detected to be a timeout.
Definition: Database.php:1749
Wikimedia\Rdbms\Database\closeConnection
closeConnection()
Closes underlying database connection.
Wikimedia\Rdbms\Database\lastDoneWrites
lastDoneWrites()
Get the last time the connection may have been used for a write query.
Definition: Database.php:707
Wikimedia\Rdbms\Database\$SLOW_WRITE_SEC
static float $SLOW_WRITE_SEC
Consider a write slow if it took more than this many seconds.
Definition: Database.php:250
Wikimedia\Rdbms\Database\wasReadOnlyError
wasReadOnlyError()
Determines if the last failure was due to the database being read-only.bool
Definition: Database.php:3991
Wikimedia\Rdbms\Database\doUnlock
doUnlock(string $lockName, string $method)
Definition: Database.php:5614
Wikimedia\Rdbms\Database\beginIfImplied
beginIfImplied( $sql, $fname)
Start an implicit transaction if DBO_TRX is enabled and no transaction is active.
Definition: Database.php:1546
Wikimedia\Rdbms\Database\$csmError
DBUnexpectedError null $csmError
Last unresolved critical section error.
Definition: Database.php:201
Wikimedia\Rdbms\Database\getQueryException
getQueryException( $error, $errno, $sql, $fname)
Definition: Database.php:1803
Wikimedia\Rdbms\Database\runTransactionListenerCallbacks
runTransactionListenerCallbacks( $trigger, array &$errors=[])
Actually run any "transaction listener" callbacks.
Definition: Database.php:4446
Wikimedia\Rdbms\Database\getServerInfo
getServerInfo()
Get a human-readable string describing the current software version.
Definition: Database.php:578
Wikimedia\Rdbms\Database\commit
commit( $fname=__METHOD__, $flush=self::FLUSHING_ONE)
Commits a transaction previously started using begin()
Definition: Database.php:4870
Wikimedia\Rdbms\Database\buildLeast
buildLeast( $fields, $values)
Build a LEAST function statement comparing columns/values.Integer and float values in $values will no...
Definition: Database.php:2819
Wikimedia\Rdbms\Database\restoreFlags
restoreFlags( $state=self::RESTORE_PRIOR)
Restore the flags to their prior state before the last setFlag/clearFlag call.
Definition: Database.php:851
Wikimedia\Rdbms\Database\CONN_INITIAL_SCHEMA
const CONN_INITIAL_SCHEMA
Schema name to use on initial connection.
Definition: Database.php:275
Wikimedia\Rdbms\Database\connectionErrorLogger
connectionErrorLogger( $errno, $errstr)
Error handler for logging errors during database connection.
Definition: Database.php:969
Wikimedia\Rdbms\Database\setLBInfo
setLBInfo( $nameOrArray, $value=null)
Set the entire array or a particular key of the managing load balancer info array.
Definition: Database.php:671
Wikimedia\Rdbms\Database\getScopedLockAndFlush
getScopedLockAndFlush( $lockKey, $fname, $timeout)
Acquire a named lock, flush any transaction, and return an RAII style unlocker object.
Definition: Database.php:5618
Wikimedia\Rdbms\Database\fieldNamesWithAlias
fieldNamesWithAlias( $fields)
Gets an array of aliased field names.
Definition: Database.php:3200
Wikimedia\Rdbms\Database\dataSeek
dataSeek(IResultWrapper $res, $pos)
Change the position of the cursor in a result object.
Definition: Database.php:896
Wikimedia\Rdbms\Database\getLBInfo
getLBInfo( $name=null)
Get properties passed down from the server info array of the load balancer.
Definition: Database.php:659
Wikimedia\Rdbms\DBReadOnlyError
@newable
Definition: DBReadOnlyError.php:28
Wikimedia\Rdbms\Database\$trxWriteAffectedRows
int $trxWriteAffectedRows
Number of rows affected by write queries for the current transaction.
Definition: Database.php:161
Wikimedia\Rdbms\Database\$trxWriteAdjDuration
float $trxWriteAdjDuration
Like trxWriteQueryCount but excludes lock-bound, easy to replicate, queries.
Definition: Database.php:163
Wikimedia\Rdbms\Database\insertSelect
insertSelect( $destTable, $srcTable, $varMap, $conds, $fname=__METHOD__, $insertOptions=[], $selectOptions=[], $selectJoinConds=[])
INSERT SELECT wrapper.
Definition: Database.php:3660
Wikimedia\Rdbms\Database\normalizeRowArray
normalizeRowArray(array $rowOrRows)
Definition: Database.php:2222
Wikimedia\Rdbms\Database\$csProvider
CriticalSectionProvider null $csProvider
Definition: Database.php:56
Wikimedia\Rdbms\Database\selectRow
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Wrapper to IDatabase::select() that only fetches one row (via LIMIT)
Definition: Database.php:2106
Wikimedia\Rdbms\DBQueryError
@newable
Definition: DBQueryError.php:29
Wikimedia\Rdbms\Database\useIndexClause
useIndexClause( $index)
USE INDEX clause.
Definition: Database.php:3436
Wikimedia\Rdbms\Database\makeUpdateOptions
makeUpdateOptions( $options)
Make UPDATE options for the Database::update function.
Definition: Database.php:2635
Wikimedia\Rdbms\Database\getReadOnlyReason
getReadOnlyReason()
Definition: Database.php:5803
Wikimedia\Rdbms\Database\buildSuperlative
buildSuperlative( $sqlfunc, $fields, $values)
Build a superlative function statement comparing columns/values.
Definition: Database.php:2839
Wikimedia\Rdbms\DBQueryDisconnectedError
@newable
Definition: DBQueryDisconnectedError.php:29
Wikimedia\Rdbms\Database\$priorFlags
int[] $priorFlags
Prior flags member variable values.
Definition: Database.php:121
Wikimedia\Rdbms\Database\doGetLag
doGetLag()
Get the amount of replication lag for this database server.
Definition: Database.php:5281
Wikimedia\Rdbms\Database\installErrorHandler
installErrorHandler()
Set a custom error handler for logging errors during database connection.
Definition: Database.php:927
Wikimedia\Rdbms\Database\aggregateValue
aggregateValue( $valuedata, $valuename='value')
Return aggregated value alias.array|string Since 1.33
Definition: Database.php:2759
Wikimedia\Rdbms\Database\restoreErrorHandler
restoreErrorHandler()
Restore the previous error handler and return the last PHP error for this DB.
Definition: Database.php:938
Wikimedia\Rdbms\Database\preCommitCallbacksPending
preCommitCallbacksPending()
Definition: Database.php:725
Wikimedia\Rdbms\Database\assertIsWritablePrimary
assertIsWritablePrimary()
Make sure that this server is not marked as a replica nor read-only as a sanity check.
Definition: Database.php:1081
Wikimedia\Rdbms\Database\wasKnownStatementRollbackError
wasKnownStatementRollbackError()
Definition: Database.php:4021
Wikimedia\Rdbms\Database\pendingWriteAndCallbackCallers
pendingWriteAndCallbackCallers()
List the methods that have write queries or callbacks for the current transaction.
Definition: Database.php:792
Wikimedia\Rdbms\Database\$htmlErrors
string bool null $htmlErrors
Stashed value of html_errors INI setting.
Definition: Database.php:119
Wikimedia\Rdbms\Database\tableExists
tableExists( $table, $fname=__METHOD__)
Query whether a given table exists.
Wikimedia\Rdbms\Database\$SAVEPOINT_PREFIX
static string $SAVEPOINT_PREFIX
Prefix to the atomic section counter used to make savepoint IDs.
Definition: Database.php:228
Wikimedia\Rdbms\Database\pendingWriteCallers
pendingWriteCallers()
Get the list of method names that did write queries for this transaction.
Definition: Database.php:776
$s
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
Definition: mergeMessageFileList.php:206
Wikimedia\Rdbms\Database\$DEADLOCK_DELAY_MAX
static int $DEADLOCK_DELAY_MAX
Maximum time to wait before retry.
Definition: Database.php:240
Wikimedia\Rdbms\Database\$trxEndCallbacks
array[] $trxEndCallbacks
List of (callable, method name, atomic section id)
Definition: Database.php:174
Wikimedia\Rdbms\Database\tableNames
tableNames(... $tables)
Fetch a number of table names into an array This is handy when you need to construct SQL for joins.
Definition: Database.php:3126
Wikimedia\Rdbms\Database\conditional
conditional( $cond, $caseTrueExpression, $caseFalseExpression)
Returns an SQL expression for a simple conditional.This doesn't need to be overridden unless CASE isn...
Definition: Database.php:3939
Wikimedia\Rdbms\Database\__toString
__toString()
Get a debugging string that mentions the database type, the ID of this instance, and the ID of any un...
Definition: Database.php:5985
Wikimedia\Rdbms\Database\$lastRoundTripEstimate
float $lastRoundTripEstimate
Query round trip time estimate.
Definition: Database.php:194
Wikimedia\Rdbms\Database\getLogContext
getLogContext(array $extras=[])
Create a log context to pass to PSR-3 logger functions.
Definition: Database.php:979
Wikimedia\Rdbms\Database\$connectionParams
array< string, mixed > $connectionParams
Connection parameters used by initConnection() and open()
Definition: Database.php:99
DBO_DDLMODE
const DBO_DDLMODE
Definition: defines.php:16
Wikimedia\Rdbms\Database\$trxAutomatic
bool $trxAutomatic
Whether the current transaction was started implicitly due to DBO_TRX.
Definition: Database.php:147
Wikimedia\Rdbms\Database\flushSnapshot
flushSnapshot( $fname=__METHOD__, $flush=self::FLUSHING_ONE)
Commit any transaction but error out if writes or callbacks are pending.
Definition: Database.php:5020
Wikimedia\Rdbms\Database\$trxStatusCause
Throwable null $trxStatusCause
The last error that caused the status to become STATUS_TRX_ERROR.
Definition: Database.php:135
Wikimedia\Rdbms\Database\pendingWriteRowsAffected
pendingWriteRowsAffected()
Get the number of affected rows from pending write queries.
Definition: Database.php:780
Wikimedia\Rdbms\Database\fetchRow
fetchRow(IResultWrapper $res)
Fetch the next row from the given result object, in associative array form.
Definition: Database.php:876
Wikimedia\Rdbms\Database\runTransactionPostCommitCallbacks
runTransactionPostCommitCallbacks()
Handle "on transaction idle/resolution" and "transaction listener" callbacks post-COMMIT.
Definition: Database.php:4469
Wikimedia\Rdbms\Database\$trxPreCommitOrIdleCallbacks
array[] $trxPreCommitOrIdleCallbacks
List of (callable, method name, atomic section id)
Definition: Database.php:169
Wikimedia\Rdbms\Database\$trxWriteQueryCount
int $trxWriteQueryCount
Number of write queries for the current transaction.
Definition: Database.php:159
Wikimedia\Rdbms\AtomicSectionIdentifier
Class used for token representing identifiers for atomic sections from IDatabase instances.
Definition: AtomicSectionIdentifier.php:26
Wikimedia\Rdbms\Database\doReleaseSavepoint
doReleaseSavepoint( $identifier, $fname)
Release a savepoint.
Definition: Database.php:4519
Wikimedia\Rdbms\Database\executeQueryAttempt
executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags)
Wrapper for doQuery() that handles DBO_TRX, profiling, logging, affected row count tracking,...
Definition: Database.php:1451
$line
$line
Definition: mcc.php:119
Wikimedia\Rdbms\Database\currentAtomicSectionId
currentAtomicSectionId()
Definition: Database.php:4187
Wikimedia\Rdbms\Database\modifyCallbacksForCancel
modifyCallbacksForCancel(array $sectionIds, AtomicSectionIdentifier $newSectionId=null)
Update callbacks that were owned by cancelled atomic sections.
Definition: Database.php:4248
Wikimedia\Rdbms\Database\setTrxEndCallbackSuppression
setTrxEndCallbackSuppression( $suppress)
Whether to disable running of post-COMMIT/ROLLBACK callbacks.
Definition: Database.php:4300
Wikimedia\Rdbms\Database\numFields
numFields(IResultWrapper $res)
Get the number of fields in a result object.
Definition: Database.php:888
Wikimedia\Rdbms\Database\query
query( $sql, $fname=__METHOD__, $flags=self::QUERY_NORMAL)
Run an SQL query and return the result.
Definition: Database.php:1316
Wikimedia\Rdbms\Database\getMasterPos
getMasterPos()
Definition: Database.php:4100
Wikimedia\Rdbms\Database\masterPosWait
masterPosWait(DBPrimaryPos $pos, $timeout)
since 1.37; use primaryPosWait() instead. The maximum number of seconds to wait for synchronisation i...
Definition: Database.php:4077
Wikimedia\Rdbms\Database\pendingWriteQueryDuration
pendingWriteQueryDuration( $type=self::ESTIMATE_TOTAL)
Get the time spend running write queries for this transaction.
Definition: Database.php:744
Wikimedia\Rdbms\Database\$indexAliases
string[] $indexAliases
Current map of (index alias => index)
Definition: Database.php:114
Wikimedia\Rdbms\Database\normalizeUpsertParams
normalizeUpsertParams( $uniqueKeys, &$rows)
Validate and normalize parameters to upsert() or replace()
Definition: Database.php:2273
Wikimedia\Rdbms\LikeMatch
Used by Database::buildLike() to represent characters that have special meaning in SQL LIKE clauses a...
Definition: LikeMatch.php:10
Wikimedia\Rdbms\Database\tableName
tableName( $name, $format='quoted')
Format a table name ready for use in constructing an SQL query.This does two important things: it quo...
Definition: Database.php:3017
Wikimedia\Rdbms\Database\setTransactionError
setTransactionError(Throwable $trxError)
Mark the transaction as requiring rollback (STATUS_TRX_ERROR) due to an error.
Definition: Database.php:5872
Wikimedia\Rdbms\Database\wasConnectionError
wasConnectionError( $errno)
Do not use this method outside of Database/DBError classes.
Definition: Database.php:4010
Wikimedia\Rdbms\Database\doAtomicSection
doAtomicSection( $fname, callable $callback, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Perform an atomic section of reversable SQL statements from a callback.
Definition: Database.php:4775
Wikimedia\Rdbms\Database\$trxReplicaLagStatus
array null $trxReplicaLagStatus
Replication lag estimate at the time of BEGIN for the last transaction.
Definition: Database.php:141
Wikimedia\Rdbms\Database\replace
replace( $table, $uniqueKeys, $rows, $fname=__METHOD__)
Insert row(s) into a table, deleting all conflicting rows beforehand.
Definition: Database.php:3453
Wikimedia\Rdbms\Database\getRecordedTransactionLagStatus
getRecordedTransactionLagStatus()
Get the replica DB lag when the current transaction started.
Definition: Database.php:5190
Wikimedia\Rdbms\Database\strencode
strencode( $s)
Wrapper for addslashes()
Wikimedia\Rdbms\Database\CONN_PASSWORD
const CONN_PASSWORD
Database server password to use on all connections.
Definition: Database.php:271
Wikimedia\Rdbms\Database\$user
string null $user
User that this instance is currently connected under the name of.
Definition: Database.php:85
Wikimedia\Rdbms\Database\doLockIsFree
doLockIsFree(string $lockName, string $method)
Definition: Database.php:5552
Wikimedia\Rdbms\Database\getServer
getServer()
Get the hostname or IP address of the server.
Definition: Database.php:3005
Wikimedia\Rdbms\DBUnexpectedError
@newable
Definition: DBUnexpectedError.php:29
Wikimedia\Rdbms\Database\doLockTables
doLockTables(array $read, array $write, $method)
Helper function for lockTables() that handles the actual table locking.
Definition: Database.php:5687
Wikimedia\Rdbms\Database\doUnlockTables
doUnlockTables( $method)
Helper function for unlockTables() that handles the actual table unlocking.
Definition: Database.php:5708
Wikimedia\Rdbms\Database\timestamp
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
Definition: Database.php:5089
Wikimedia\Rdbms\Database\selectField
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:1839
Wikimedia\Rdbms\Database\anyChar
anyChar()
Returns a token for buildLike() that denotes a '_' to be used in a LIKE query.
Definition: Database.php:3411
Wikimedia\Rdbms\Database\onTransactionIdle
onTransactionIdle(callable $callback, $fname=__METHOD__)
Alias for onTransactionCommitOrIdle() for backwards-compatibility.
Definition: Database.php:4143
Wikimedia\Rdbms\Database\unlock
unlock( $lockName, $method)
Release a lock.Named locks are not related to transactionsName of lock to release Name of the calling...
Definition: Database.php:5594
Wikimedia\Rdbms\Database\getTopologyBasedServerId
getTopologyBasedServerId()
Get a non-recycled ID that uniquely identifies this server within the replication topology.
Definition: Database.php:582
Wikimedia\Rdbms\Database\$profiler
callable null $profiler
Definition: Database.php:68
Wikimedia\Rdbms\Database\addIdentifierQuotes
addIdentifierQuotes( $s)
Escape a SQL identifier (e.g.table, column, database) for use in a SQL queryDepending on the database...
Definition: Database.php:3348
Wikimedia\Rdbms\Database\getLazyMasterHandle
getLazyMasterHandle()
Get a handle to the primary DB server of the cluster to which this server belongs.
Definition: Database.php:691
Wikimedia\Rdbms\Database\consumeTrxShortId
consumeTrxShortId()
Reset the transaction ID and return the old one.
Definition: Database.php:1731
Wikimedia\Rdbms\Database\estimateRowCount
estimateRowCount( $tables, $var=' *', $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Estimate the number of rows in dataset.MySQL allows you to estimate the number of rows that would be ...
Definition: Database.php:2128
Wikimedia\Rdbms\Database\truncate
truncate( $tables, $fname=__METHOD__)
Delete all data in a table(s) and reset any sequences owned by that table(s)
Definition: Database.php:5736
Wikimedia\Rdbms\Database\commenceCriticalSection
commenceCriticalSection(string $fname)
Demark the start of a critical section of session/transaction state changes.
Definition: Database.php:5919
Wikimedia\Rdbms\Database\dbSchema
dbSchema( $schema=null)
Get/set the db schema.
Definition: Database.php:629
Wikimedia\Rdbms\Database\$trxRecurringCallbacks
callable[] $trxRecurringCallbacks
Map of (name => callable)
Definition: Database.php:178
Wikimedia\Rdbms\Database\getQueryVerb
getQueryVerb( $sql)
Definition: Database.php:1186
Wikimedia\Rdbms\Database\unionQueries
unionQueries( $sqls, $all)
Construct a UNION query.This is used for providing overload point for other DB abstractions not compa...
Definition: Database.php:3853
Wikimedia\Rdbms\Database\freeResult
freeResult(IResultWrapper $res)
Free a result object returned by query() or select()
Definition: Database.php:900
Wikimedia\Rdbms\DBTransactionError
@newable
Definition: DBTransactionError.php:29
Wikimedia\Rdbms\Database\reassignCallbacksForSection
reassignCallbacksForSection(AtomicSectionIdentifier $old, AtomicSectionIdentifier $new)
Hoist callback ownership for callbacks in a section to a parent section.
Definition: Database.php:4203
Wikimedia\Rdbms\Database\insert
insert( $table, $rows, $fname=__METHOD__, $options=[])
Insert the given row(s) into a table.
Definition: Database.php:2510
Wikimedia\Rdbms\IDatabase\lastErrno
lastErrno()
Get the last error number.
Wikimedia\Rdbms\Database\relationSchemaQualifier
relationSchemaQualifier()
Definition: Database.php:655
Wikimedia\Rdbms\Database\$DEADLOCK_TRIES
static int $DEADLOCK_TRIES
Number of times to re-try an operation in case of deadlock.
Definition: Database.php:236
Wikimedia\Rdbms\Database\makeInsertNonConflictingVerbAndOptions
makeInsertNonConflictingVerbAndOptions()
Definition: Database.php:2566
Wikimedia\Rdbms\Database\newExceptionAfterConnectError
newExceptionAfterConnectError( $error)
Definition: Database.php:1817
Wikimedia\Rdbms\Database\lockTables
lockTables(array $read, array $write, $method)
Lock specific tables.
Definition: Database.php:5666
Wikimedia\Rdbms\Database\$trxAtomicCounter
int $trxAtomicCounter
Counter for atomic savepoint identifiers (reset with each transaction)
Definition: Database.php:149
Wikimedia\Rdbms\Database\encodeExpiry
encodeExpiry( $expiry)
Encode an expiry time into the DBMS dependent format.
Definition: Database.php:5774
Wikimedia\Rdbms\Database\$srvCache
BagOStuff $srvCache
APC cache.
Definition: Database.php:54
Wikimedia\Rdbms\Database\buildGreatest
buildGreatest( $fields, $values)
Build a GREATEST function statement comparing columns/values.Integer and float values in $values will...
Definition: Database.php:2811
Wikimedia\Rdbms\Database\doSavepoint
doSavepoint( $identifier, $fname)
Create a savepoint.
Definition: Database.php:4503
Wikimedia\Rdbms\Database\setSchemaVars
setSchemaVars( $vars)
Set schema variables to be used when streaming commands from SQL files or stdin.
Definition: Database.php:5353
Wikimedia\Rdbms\Database\__construct
__construct(array $params)
Definition: Database.php:284
Wikimedia\Rdbms\Database\trxTimestamp
trxTimestamp()
Get the UNIX timestamp of the time that the transaction was established.
Definition: Database.php:603
Wikimedia\Rdbms\Database\$trxTimestamp
float null $trxTimestamp
UNIX timestamp at the time of BEGIN for the last transaction.
Definition: Database.php:139
Wikimedia\Rdbms\Database\makeGroupByWithHaving
makeGroupByWithHaving( $options)
Returns an optional GROUP BY with an optional HAVING.
Definition: Database.php:1969
Wikimedia\Rdbms\Database\tableNameWithAlias
tableNameWithAlias( $table, $alias=false)
Get an aliased table name.
Definition: Database.php:3157
Wikimedia\Rdbms\Database\getAttributes
static getAttributes()
Definition: Database.php:563
Wikimedia\Rdbms\Database\writesPending
writesPending()
Definition: Database.php:711
Wikimedia\Rdbms\Database\normalizeUpsertKeys
normalizeUpsertKeys( $uniqueKeys)
Definition: Database.php:2308
Wikimedia\Rdbms\Database\assertValidUpsertRowArray
assertValidUpsertRowArray(array $rows, array $identityKey)
Definition: Database.php:2357
Wikimedia\Rdbms\Database\selectFieldsOrOptionsAggregate
selectFieldsOrOptionsAggregate( $fields, $options)
Definition: Database.php:2194
Wikimedia\Rdbms\Database\lockIsFree
lockIsFree( $lockName, $method)
Check to see if a named lock is not locked by any thread (non-blocking)Name of lock to poll Name of m...
Definition: Database.php:5530
Wikimedia\Rdbms\Database\ping
ping(&$rtt=null)
Ping the server and try to reconnect if it there is no connection.
Definition: Database.php:5112
Wikimedia\Rdbms\Database\$nonNativeInsertSelectBatchSize
int $nonNativeInsertSelectBatchSize
Row batch size to use for emulated INSERT SELECT queries.
Definition: Database.php:103
Wikimedia\Rdbms\Database\onAtomicSectionCancel
onAtomicSectionCancel(callable $callback, $fname=__METHOD__)
Run a callback when the atomic section is cancelled.
Definition: Database.php:4177
Wikimedia\Rdbms\Database\getTopologyRootMaster
getTopologyRootMaster()
Definition: Database.php:594
Wikimedia\Rdbms\DBConnectionError
@newable
Definition: DBConnectionError.php:27
Wikimedia\Rdbms\Database\setTableAliases
setTableAliases(array $aliases)
Make certain table names use their own database, schema, and table prefix when passed into SQL querie...
Definition: Database.php:5822
Wikimedia\Rdbms\Database\$MUTABLE_FLAGS
static string[] $MUTABLE_FLAGS
List of DBO_* flags that can be changed after connection.
Definition: Database.php:255
Wikimedia\Rdbms\Database\getServerName
getServerName()
Get the readable name for the server.
Definition: Database.php:3009
Wikimedia\Rdbms\Database\onTransactionResolution
onTransactionResolution(callable $callback, $fname=__METHOD__)
Run a callback when the current transaction commits or rolls back.
Definition: Database.php:4113
Wikimedia\Rdbms\IDatabase\getType
getType()
Get the RDBMS type of the server (e.g.
Wikimedia\Rdbms\Database\explicitTrxActive
explicitTrxActive()
Definition: Database.php:5052
Wikimedia\Rdbms\Database\isOpen
isOpen()
Definition: Database.php:817
Wikimedia\Rdbms\Database\doCommit
doCommit( $fname)
Issues the COMMIT command to the database server.
Definition: Database.php:4949
$source
$source
Definition: mwdoc-filter.php:34
Wikimedia\Rdbms\IMaintainableDatabase\fieldInfo
fieldInfo( $table, $field)
Get information about a field Returns false if the field doesn't exist.
Wikimedia\Rdbms\Database\$topologyRootMaster
string null $topologyRootMaster
Host (or address) of the root primary server for the replication topology.
Definition: Database.php:97
Wikimedia\Rdbms\Database\runOnTransactionIdleCallbacks
runOnTransactionIdleCallbacks( $trigger, array &$errors=[])
Consume and run any "on transaction idle/resolution" callbacks.
Definition: Database.php:4316
Wikimedia\Rdbms\Database\makeOrderBy
makeOrderBy( $options)
Returns an optional ORDER BY.
Definition: Database.php:1995
Wikimedia\Rdbms\Database\$flags
int $flags
Current bit field of class DBO_* constants.
Definition: Database.php:106
Wikimedia\Rdbms\Database\serverIsReadOnly
serverIsReadOnly()
bool Whether the DB is marked as read-only server-side If an error occurs, {query} 1....
Definition: Database.php:4109
Wikimedia\Rdbms\Database\$trxFname
string null $trxFname
Name of the function that start the last transaction.
Definition: Database.php:143
Wikimedia\Rdbms\Database\makeSelectOptions
makeSelectOptions(array $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:1898
Wikimedia\Rdbms\Database\select
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
Definition: Database.php:2007
Wikimedia\Rdbms\Database\attributesFromType
static attributesFromType( $dbType, $driver=null)
Definition: Database.php:492
Wikimedia\Rdbms\Database\lastQuery
lastQuery()
Get the last query that sent on account of IDatabase::query()
Definition: Database.php:703
Wikimedia\Rdbms\Database\wasConnectionLoss
wasConnectionLoss()
Determines if the last query error was due to a dropped connection.Note that during a connection loss...
Definition: Database.php:3983
Wikimedia\Rdbms\Database\wasDeadlock
wasDeadlock()
Determines if the last failure was due to a deadlock.Note that during a deadlock, the prior transacti...
Definition: Database.php:3967
$t
$t
Definition: testCompression.php:74
Wikimedia\Rdbms\Database\doSelectDomain
doSelectDomain(DatabaseDomain $domain)
Definition: Database.php:2997
Wikimedia\Rdbms\Database\$connLogger
LoggerInterface $connLogger
Definition: Database.php:58
Wikimedia\Rdbms\Database\addQuotes
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.string
Definition: Database.php:3329
Wikimedia\Rdbms\Database\$replLogger
LoggerInterface $replLogger
Definition: Database.php:62
Wikimedia\Rdbms\Database\$password
string null $password
Password used to establish the current connection.
Definition: Database.php:87
Wikimedia\Rdbms\Database\assertConditionIsNotEmpty
assertConditionIsNotEmpty( $conds, string $fname, bool $deprecate)
Check type and bounds conditions parameters for update.
Definition: Database.php:2921
Wikimedia\Rdbms\Database\getApproximateLagStatus
getApproximateLagStatus()
Get a replica DB lag estimate for this server at the start of a transaction.
Definition: Database.php:5203
Wikimedia\Rdbms\Database\qualifiedTableComponents
qualifiedTableComponents( $name)
Get the table components needed for a query given the currently selected database.
Definition: Database.php:3075
Wikimedia\Rdbms\Database\affectedRows
affectedRows()
Get the number of rows affected by the last write query.
Definition: Database.php:5103
Wikimedia\Rdbms\Database\$DEADLOCK_DELAY_MIN
static int $DEADLOCK_DELAY_MIN
Minimum time to wait before retry, in microseconds.
Definition: Database.php:238
Wikimedia\Rdbms\Database\fetchAffectedRowCount
fetchAffectedRowCount()
Wikimedia\Rdbms\DatabaseDomain
Class to handle database/schema/prefix specifications for IDatabase.
Definition: DatabaseDomain.php:40
Wikimedia\Rdbms\Database\databasesAreIndependent
databasesAreIndependent()
Returns true if DBs are assumed to be on potentially different servers.In systems like mysql/mariadb,...
Definition: Database.php:2963
Wikimedia\Rdbms\TransactionProfiler
Detect high-contention DB queries via profiling calls.
Definition: TransactionProfiler.php:40
Wikimedia\Rdbms\Database\indexInfo
indexInfo( $table, $index, $fname=__METHOD__)
Get information about an index into an object.
Wikimedia\Rdbms\Database\strreplace
strreplace( $orig, $old, $new)
Returns a SQL expression for simple string replacement (e.g.REPLACE() in mysql)Column to modify Colum...
Definition: Database.php:3951
Wikimedia\Rdbms\Database\$ownerId
int null $ownerId
Integer ID of the managing LBFactory instance or null if none.
Definition: Database.php:204
Wikimedia\Rdbms\Database\sourceFile
sourceFile( $filename, callable $lineCallback=null, callable $resultCallback=null, $fname=false, callable $inputCallback=null)
Read and execute SQL commands from a file.
Definition: Database.php:5319
Wikimedia\Rdbms\Database\doLock
doLock(string $lockName, string $method, int $timeout)
Definition: Database.php:5587
Wikimedia\Rdbms\Database\listViews
listViews( $prefix=null, $fname=__METHOD__)
Lists all the VIEWs in the database.Only show VIEWs with this prefix, eg. unit_test_ Name of calling ...
Definition: Database.php:5081
Wikimedia\Rdbms\Database\runOnAtomicSectionCancelCallbacks
runOnAtomicSectionCancelCallbacks( $trigger, array $sectionIds)
Consume and run any relevant "on atomic section cancel" callbacks for the active transaction.
Definition: Database.php:4411
Wikimedia\Rdbms\Database\newSelectQueryBuilder
newSelectQueryBuilder()
Create an empty SelectQueryBuilder which can be used to run queries against this connection....
Definition: Database.php:1835
Wikimedia\Rdbms\Database\streamStatementEnd
streamStatementEnd(&$sql, &$newLine)
Called by sourceStream() to check if we've reached a statement end.
Definition: Database.php:5435
Wikimedia\Rdbms\Database\makeList
makeList(array $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.
Definition: Database.php:2659
Wikimedia\Rdbms\IMaintainableDatabase
Advanced database interface for IDatabase handles that include maintenance methods.
Definition: IMaintainableDatabase.php:38
Wikimedia\Rdbms\Database\runTransactionPostRollbackCallbacks
runTransactionPostRollbackCallbacks()
Handle "on transaction idle/resolution" and "transaction listener" callbacks post-ROLLBACK.
Definition: Database.php:4486
Wikimedia\Rdbms\Database\nextSequenceValue
nextSequenceValue( $seqName)
Deprecated method, calls should be removed.
Definition: Database.php:3419
Wikimedia\Rdbms\Subquery
Definition: Subquery.php:27
Wikimedia\Rdbms\Database\selectFieldValues
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:1864
Wikimedia\Rdbms\Database\flatAtomicSectionList
flatAtomicSectionList()
Definition: Database.php:811
Wikimedia\Rdbms\Database\open
open( $server, $user, $password, $db, $schema, $tablePrefix)
Open a new connection to the database (closing any existing one)
Wikimedia\Rdbms\Database\$deprecationLogger
callable $deprecationLogger
Deprecation logging callback.
Definition: Database.php:66
Wikimedia\Rdbms\Database\$errorLogger
callable $errorLogger
Error logging callback.
Definition: Database.php:64
Wikimedia\Rdbms\Database\CONN_HOST
const CONN_HOST
Hostname or IP address to use on all connections.
Definition: Database.php:267
Wikimedia\Rdbms\Database\$conn
object resource null $conn
Database connection.
Definition: Database.php:77
Wikimedia\Rdbms\Database\doRollback
doRollback( $fname)
Issues the ROLLBACK command to the database server.
Definition: Database.php:5013
Wikimedia\Rdbms\Database\$TINY_WRITE_SEC
static float $TINY_WRITE_SEC
Guess of how many seconds it takes to replicate a small insert.
Definition: Database.php:248
Wikimedia\Rdbms\Database\$lbInfo
array $lbInfo
Current LoadBalancer tracking information.
Definition: Database.php:108
Wikimedia\Rdbms\Database\getDBname
getDBname()
Get the current database name; null if there isn't one.
Definition: Database.php:3001
Wikimedia\Rdbms\Blob
@newable
Definition: Blob.php:9
Wikimedia\Rdbms\Database\getInfinity
getInfinity()
Find out when 'infinity' is.Most DBMSes support this. This is a special keyword for timestamps in Pos...
Definition: Database.php:5770
$type
$type
Definition: testCompression.php:52
Wikimedia\Rdbms\Database\doUpsert
doUpsert(string $table, array $rows, array $identityKey, array $set, string $fname)
Definition: Database.php:3564