MediaWiki  master
DatabaseMysqlBase.php
Go to the documentation of this file.
1 <?php
23 namespace Wikimedia\Rdbms;
24 
25 use InvalidArgumentException;
26 use RuntimeException;
27 use stdClass;
28 use Wikimedia\AtEase\AtEase;
31 
45 abstract class DatabaseMysqlBase extends Database {
51  protected $lagDetectionOptions = [];
53  protected $useGTIDs = false;
55  protected $sslKeyPath;
57  protected $sslCertPath;
59  protected $sslCAFile;
61  protected $sslCAPath;
67  protected $sslCiphers;
69  protected $utf8Mode;
71  protected $defaultBigSelects;
72 
77 
78  // Cache getServerId() for 24 hours
79  private const SERVER_ID_CACHE_TTL = 86400;
80 
82  private const LAG_STALE_WARN_THRESHOLD = 0.100;
83 
85  protected $platform;
86 
106  public function __construct( array $params ) {
107  $this->lagDetectionMethod = $params['lagDetectionMethod'] ?? 'Seconds_Behind_Master';
108  $this->lagDetectionOptions = $params['lagDetectionOptions'] ?? [];
109  $this->useGTIDs = !empty( $params['useGTIDs' ] );
110  foreach ( [ 'KeyPath', 'CertPath', 'CAFile', 'CAPath', 'Ciphers' ] as $name ) {
111  $var = "ssl{$name}";
112  if ( isset( $params[$var] ) ) {
113  $this->$var = $params[$var];
114  }
115  }
116  $this->utf8Mode = !empty( $params['utf8Mode'] );
117  $this->insertSelectIsSafe = isset( $params['insertSelectIsSafe'] )
118  ? (bool)$params['insertSelectIsSafe'] : null;
119  parent::__construct( $params );
120  $this->platform = new MySQLPlatform(
121  $this,
122  $params['queryLogger'],
123  $this->currentDomain->getSchema(),
124  $this->currentDomain->getTablePrefix()
125  );
126  }
127 
131  public function getType() {
132  return 'mysql';
133  }
134 
135  protected function open( $server, $user, $password, $db, $schema, $tablePrefix ) {
136  $this->close( __METHOD__ );
137 
138  if ( $schema !== null ) {
139  throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." );
140  }
141 
142  $this->installErrorHandler();
143  try {
144  $this->conn = $this->mysqlConnect( $server, $user, $password, $db );
145  } catch ( RuntimeException $e ) {
146  $this->restoreErrorHandler();
147  throw $this->newExceptionAfterConnectError( $e->getMessage() );
148  }
149  $error = $this->restoreErrorHandler();
150 
151  if ( !$this->conn ) {
152  throw $this->newExceptionAfterConnectError( $error ?: $this->lastError() );
153  }
154 
155  try {
156  $this->currentDomain = new DatabaseDomain(
157  $db && strlen( $db ) ? $db : null,
158  null,
159  $tablePrefix
160  );
161  $this->platform->setPrefix( $tablePrefix );
162  // Abstract over any excessive MySQL defaults
163  $set = [ 'group_concat_max_len = 262144' ];
164  // Set any custom settings defined by site config
165  // https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html
166  foreach ( $this->connectionVariables as $var => $val ) {
167  // Escape strings but not numbers to avoid MySQL complaining
168  if ( !is_int( $val ) && !is_float( $val ) ) {
169  $val = $this->addQuotes( $val );
170  }
171  $set[] = $this->platform->addIdentifierQuotes( $var ) . ' = ' . $val;
172  }
173 
174  // @phan-suppress-next-next-line PhanRedundantCondition
175  // Safety check to avoid empty SET query
176  if ( $set ) {
177  $sql = 'SET ' . implode( ', ', $set );
178  $flags = self::QUERY_NO_RETRY | self::QUERY_CHANGE_TRX;
179  // Avoid using query() so that replaceLostConnection() does not throw
180  // errors if the transaction status is STATUS_TRX_ERROR
181  $qs = $this->executeQuery( $sql, __METHOD__, $flags, $sql );
182  if ( $qs->res === false ) {
183  $this->reportQueryError( $qs->message, $qs->code, $sql, __METHOD__ );
184  }
185  }
186  } catch ( RuntimeException $e ) {
187  throw $this->newExceptionAfterConnectError( $e->getMessage() );
188  }
189  }
190 
191  protected function doSelectDomain( DatabaseDomain $domain ) {
192  if ( $domain->getSchema() !== null ) {
193  throw new DBExpectedError(
194  $this,
195  __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
196  );
197  }
198 
199  $database = $domain->getDatabase();
200  // A null database means "don't care" so leave it as is and update the table prefix
201  if ( $database === null ) {
202  $this->currentDomain = new DatabaseDomain(
203  $this->currentDomain->getDatabase(),
204  null,
205  $domain->getTablePrefix()
206  );
207  $this->platform->setPrefix( $domain->getTablePrefix() );
208 
209  return true;
210  }
211 
212  if ( $database !== $this->getDBname() ) {
213  $sql = 'USE ' . $this->addIdentifierQuotes( $database );
214  $qs = $this->executeQuery( $sql, __METHOD__, self::QUERY_IGNORE_DBO_TRX, $sql );
215  if ( $qs->res === false ) {
216  $this->reportQueryError( $qs->message, $qs->code, $sql, __METHOD__ );
217  return false; // unreachable
218  }
219  }
220 
221  // Update that domain fields on success (no exception thrown)
222  $this->currentDomain = $domain;
223  $this->platform->setPrefix( $domain->getTablePrefix() );
224 
225  return true;
226  }
227 
238  abstract protected function mysqlConnect( $server, $user, $password, $db );
239 
243  public function lastError() {
244  if ( $this->conn ) {
245  # Even if it's non-zero, it can still be invalid
246  AtEase::suppressWarnings();
247  $error = $this->mysqlError( $this->conn );
248  if ( !$error ) {
249  $error = $this->mysqlError();
250  }
251  AtEase::restoreWarnings();
252  } else {
253  $error = $this->mysqlError();
254  }
255 
256  return $error;
257  }
258 
265  abstract protected function mysqlError( $conn = null );
266 
267  protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
268  $row = $this->getReplicationSafetyInfo();
269  // For row-based-replication, the resulting changes will be relayed, not the query
270  if ( $row->binlog_format === 'ROW' ) {
271  return true;
272  }
273  // LIMIT requires ORDER BY on a unique key or it is non-deterministic
274  if ( isset( $selectOptions['LIMIT'] ) ) {
275  return false;
276  }
277  // In MySQL, an INSERT SELECT is only replication safe with row-based
278  // replication or if innodb_autoinc_lock_mode is 0. When those
279  // conditions aren't met, use non-native mode.
280  // While we could try to determine if the insert is safe anyway by
281  // checking if the target table has an auto-increment column that
282  // isn't set in $varMap, that seems unlikely to be worth the extra
283  // complexity.
284  return (
285  in_array( 'NO_AUTO_COLUMNS', $insertOptions ) ||
286  (int)$row->innodb_autoinc_lock_mode === 0
287  );
288  }
289 
293  protected function getReplicationSafetyInfo() {
294  if ( $this->replicationInfoRow === null ) {
295  $this->replicationInfoRow = $this->selectRow(
296  false,
297  [
298  'innodb_autoinc_lock_mode' => '@@innodb_autoinc_lock_mode',
299  'binlog_format' => '@@binlog_format',
300  ],
301  [],
302  __METHOD__
303  );
304  }
305 
307  }
308 
322  public function estimateRowCount(
323  $tables,
324  $var = '*',
325  $conds = '',
326  $fname = __METHOD__,
327  $options = [],
328  $join_conds = []
329  ) {
330  $conds = $this->normalizeConditions( $conds, $fname );
331  $column = $this->extractSingleFieldFromList( $var );
332  if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
333  $conds[] = "$column IS NOT NULL";
334  }
335 
336  $options['EXPLAIN'] = true;
337  $res = $this->select( $tables, $var, $conds, $fname, $options, $join_conds );
338  if ( $res === false ) {
339  return false;
340  }
341  if ( !$res->numRows() ) {
342  return 0;
343  }
344 
345  $rows = 1;
346  foreach ( $res as $plan ) {
347  $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero
348  }
349 
350  return (int)$rows;
351  }
352 
353  public function tableExists( $table, $fname = __METHOD__ ) {
354  // Split database and table into proper variables as Database::tableName() returns
355  // shared tables prefixed with their database, which do not work in SHOW TABLES statements
356  list( $database, , $prefix, $table ) = $this->platform->qualifiedTableComponents( $table );
357  $tableName = "{$prefix}{$table}";
358 
359  if ( isset( $this->sessionTempTables[$tableName] ) ) {
360  return true; // already known to exist and won't show in SHOW TABLES anyway
361  }
362 
363  // We can't use buildLike() here, because it specifies an escape character
364  // other than the backslash, which is the only one supported by SHOW TABLES
365  // TODO: Avoid using platform's internal methods
366  $encLike = $this->platform->escapeLikeInternal( $tableName, '\\' );
367 
368  // If the database has been specified (such as for shared tables), use "FROM"
369  if ( $database !== '' ) {
370  $encDatabase = $this->platform->addIdentifierQuotes( $database );
371  $sql = "SHOW TABLES FROM $encDatabase LIKE '$encLike'";
372  } else {
373  $sql = "SHOW TABLES LIKE '$encLike'";
374  }
375 
376  $res = $this->query(
377  $sql,
378  $fname,
379  self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
380  );
381 
382  return $res->numRows() > 0;
383  }
384 
390  public function fieldInfo( $table, $field ) {
391  $res = $this->query(
392  "SELECT * FROM " . $this->tableName( $table ) . " LIMIT 1",
393  __METHOD__,
394  self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
395  );
396  if ( !$res ) {
397  return false;
398  }
400  '@phan-var MysqliResultWrapper $res';
401  return $res->getInternalFieldInfo( $field );
402  }
403 
413  public function indexInfo( $table, $index, $fname = __METHOD__ ) {
414  # https://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
415  $index = $this->indexName( $index );
416 
417  $res = $this->query(
418  'SHOW INDEX FROM ' . $this->tableName( $table ),
419  $fname,
420  self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
421  );
422 
423  if ( !$res ) {
424  return null;
425  }
426 
427  $result = [];
428 
429  foreach ( $res as $row ) {
430  if ( $row->Key_name == $index ) {
431  $result[] = $row;
432  }
433  }
434 
435  return $result ?: false;
436  }
437 
442  public function strencode( $s ) {
443  return $this->mysqlRealEscapeString( $s );
444  }
445 
452  abstract protected function mysqlRealEscapeString( $s );
453 
454  protected function doGetLag() {
455  if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
456  return $this->getLagFromPtHeartbeat();
457  } else {
458  return $this->getLagFromSlaveStatus();
459  }
460  }
461 
465  protected function getLagDetectionMethod() {
467  }
468 
472  protected function getLagFromSlaveStatus() {
473  $res = $this->query(
474  'SHOW SLAVE STATUS',
475  __METHOD__,
476  self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
477  );
478  $row = $res ? $res->fetchObject() : false;
479  // If the server is not replicating, there will be no row
480  if ( $row && strval( $row->Seconds_Behind_Master ) !== '' ) {
481  // https://mariadb.com/kb/en/delayed-replication/
482  // https://dev.mysql.com/doc/refman/5.6/en/replication-delayed.html
483  return intval( $row->Seconds_Behind_Master + ( $row->SQL_Remaining_Delay ?? 0 ) );
484  }
485 
486  return false;
487  }
488 
492  protected function getLagFromPtHeartbeat() {
493  $options = $this->lagDetectionOptions;
494 
495  $currentTrxInfo = $this->getRecordedTransactionLagStatus();
496  if ( $currentTrxInfo ) {
497  // There is an active transaction and the initial lag was already queried
498  $staleness = microtime( true ) - $currentTrxInfo['since'];
499  if ( $staleness > self::LAG_STALE_WARN_THRESHOLD ) {
500  // Avoid returning higher and higher lag value due to snapshot age
501  // given that the isolation level will typically be REPEATABLE-READ
502  $this->queryLogger->warning(
503  "Using cached lag value for {db_server} due to active transaction",
504  $this->getLogContext( [
505  'method' => __METHOD__,
506  'age' => $staleness,
507  'exception' => new RuntimeException()
508  ] )
509  );
510  }
511 
512  return $currentTrxInfo['lag'];
513  }
514 
515  if ( isset( $options['conds'] ) ) {
516  // Best method for multi-DC setups: use logical channel names
517  $ago = $this->fetchSecondsSinceHeartbeat( $options['conds'] );
518  } else {
519  // Standard method: use primary server ID (works with stock pt-heartbeat)
520  $masterInfo = $this->getPrimaryServerInfo();
521  if ( !$masterInfo ) {
522  $this->queryLogger->error(
523  "Unable to query primary of {db_server} for server ID",
524  $this->getLogContext( [
525  'method' => __METHOD__
526  ] )
527  );
528 
529  return false; // could not get primary server ID
530  }
531 
532  $conds = [ 'server_id' => intval( $masterInfo['serverId'] ) ];
533  $ago = $this->fetchSecondsSinceHeartbeat( $conds );
534  }
535 
536  if ( $ago !== null ) {
537  return max( $ago, 0.0 );
538  }
539 
540  $this->queryLogger->error(
541  "Unable to find pt-heartbeat row for {db_server}",
542  $this->getLogContext( [
543  'method' => __METHOD__
544  ] )
545  );
546 
547  return false;
548  }
549 
550  protected function getPrimaryServerInfo() {
552  $key = $cache->makeGlobalKey(
553  'mysql',
554  'master-info',
555  // Using one key for all cluster replica DBs is preferable
556  $this->topologyRootMaster ?? $this->getServerName()
557  );
558  $fname = __METHOD__;
559 
560  return $cache->getWithSetCallback(
561  $key,
562  $cache::TTL_INDEFINITE,
563  function () use ( $cache, $key, $fname ) {
564  // Get and leave a lock key in place for a short period
565  if ( !$cache->lock( $key, 0, 10 ) ) {
566  return false; // avoid primary DB connection spike slams
567  }
568 
569  $conn = $this->getLazyMasterHandle();
570  if ( !$conn ) {
571  return false; // something is misconfigured
572  }
573 
574  $flags = self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
575  // Connect to and query the primary DB; catch errors to avoid outages
576  try {
577  $res = $conn->query( 'SELECT @@server_id AS id', $fname, $flags );
578  $row = $res ? $res->fetchObject() : false;
579  $id = $row ? (int)$row->id : 0;
580  } catch ( DBError $e ) {
581  $id = 0;
582  }
583 
584  // Cache the ID if it was retrieved
585  return $id ? [ 'serverId' => $id, 'asOf' => time() ] : false;
586  }
587  );
588  }
589 
590  protected function getMasterServerInfo() {
591  wfDeprecated( __METHOD__, '1.37' );
592  return $this->getPrimaryServerInfo();
593  }
594 
600  protected function fetchSecondsSinceHeartbeat( array $conds ) {
601  $whereSQL = $this->makeList( $conds, self::LIST_AND );
602  // User mysql server time so that query time and trip time are not counted.
603  // Use ORDER BY for channel based queries since that field might not be UNIQUE.
604  $res = $this->query(
605  "SELECT TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6)) AS us_ago " .
606  "FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1",
607  __METHOD__,
608  self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
609  );
610  $row = $res ? $res->fetchObject() : false;
611 
612  return $row ? ( $row->us_ago / 1e6 ) : null;
613  }
614 
615  protected function getApproximateLagStatus() {
616  if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
617  // Disable caching since this is fast enough and we don't want
618  // to be *too* pessimistic by having both the cache TTL and the
619  // pt-heartbeat interval count as lag in getSessionLagStatus()
620  return parent::getApproximateLagStatus();
621  }
622 
623  $key = $this->srvCache->makeGlobalKey( 'mysql-lag', $this->getServerName() );
624  $approxLag = $this->srvCache->get( $key );
625  if ( !$approxLag ) {
626  $approxLag = parent::getApproximateLagStatus();
627  $this->srvCache->set( $key, $approxLag, 1 );
628  }
629 
630  return $approxLag;
631  }
632 
633  public function primaryPosWait( DBPrimaryPos $pos, $timeout ) {
634  if ( !( $pos instanceof MySQLPrimaryPos ) ) {
635  throw new InvalidArgumentException( "Position not an instance of MySQLPrimaryPos" );
636  }
637 
638  if ( $this->topologyRole === self::ROLE_STATIC_CLONE ) {
639  $this->queryLogger->debug(
640  "Bypassed replication wait; database has a static dataset",
641  $this->getLogContext( [ 'method' => __METHOD__, 'raw_pos' => $pos ] )
642  );
643 
644  return 0; // this is a copy of a read-only dataset with no primary DB
645  } elseif ( $this->lastKnownReplicaPos && $this->lastKnownReplicaPos->hasReached( $pos ) ) {
646  $this->queryLogger->debug(
647  "Bypassed replication wait; replication known to have reached {raw_pos}",
648  $this->getLogContext( [ 'method' => __METHOD__, 'raw_pos' => $pos ] )
649  );
650 
651  return 0; // already reached this point for sure
652  }
653 
654  // Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
655  if ( $pos->getGTIDs() ) {
656  // Get the GTIDs from this replica server too see the domains (channels)
657  $refPos = $this->getReplicaPos();
658  if ( !$refPos ) {
659  $this->queryLogger->error(
660  "Could not get replication position on replica DB to compare to {raw_pos}",
661  $this->getLogContext( [ 'method' => __METHOD__, 'raw_pos' => $pos ] )
662  );
663 
664  return -1; // this is the primary DB itself?
665  }
666  // GTIDs with domains (channels) that are active and are present on the replica
667  $gtidsWait = $pos::getRelevantActiveGTIDs( $pos, $refPos );
668  if ( !$gtidsWait ) {
669  $this->queryLogger->error(
670  "No active GTIDs in {raw_pos} share a domain with those in {current_pos}",
671  $this->getLogContext( [
672  'method' => __METHOD__,
673  'raw_pos' => $pos,
674  'current_pos' => $refPos
675  ] )
676  );
677 
678  return -1; // $pos is from the wrong cluster?
679  }
680  // Wait on the GTID set
681  $gtidArg = $this->addQuotes( implode( ',', $gtidsWait ) );
682  if ( strpos( $gtidArg, ':' ) !== false ) {
683  // MySQL GTIDs, e.g "source_id:transaction_id"
684  $sql = "SELECT WAIT_FOR_EXECUTED_GTID_SET($gtidArg, $timeout)";
685  } else {
686  // MariaDB GTIDs, e.g."domain:server:sequence"
687  $sql = "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)";
688  }
689  $waitPos = implode( ',', $gtidsWait );
690  } else {
691  // Wait on the binlog coordinates
692  $encFile = $this->addQuotes( $pos->getLogFile() );
693  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
694  $encPos = intval( $pos->getLogPosition()[$pos::CORD_EVENT] );
695  $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)";
696  $waitPos = $pos->__toString();
697  }
698 
699  $start = microtime( true );
700  $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
701  $res = $this->query( $sql, __METHOD__, $flags );
702  $row = $res->fetchRow();
703  $seconds = max( microtime( true ) - $start, 0 );
704 
705  // Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual
706  $status = ( $row[0] !== null ) ? intval( $row[0] ) : null;
707  if ( $status === null ) {
708  $this->replLogger->error(
709  "An error occurred while waiting for replication to reach {wait_pos}",
710  $this->getLogContext( [
711  'raw_pos' => $pos,
712  'wait_pos' => $waitPos,
713  'sql' => $sql,
714  'seconds_waited' => $seconds,
715  'exception' => new RuntimeException()
716  ] )
717  );
718  } elseif ( $status < 0 ) {
719  $this->replLogger->error(
720  "Timed out waiting for replication to reach {wait_pos}",
721  $this->getLogContext( [
722  'raw_pos' => $pos,
723  'wait_pos' => $waitPos,
724  'timeout' => $timeout,
725  'sql' => $sql,
726  'seconds_waited' => $seconds,
727  'exception' => new RuntimeException()
728  ] )
729  );
730  } elseif ( $status >= 0 ) {
731  $this->replLogger->debug(
732  "Replication has reached {wait_pos}",
733  $this->getLogContext( [
734  'raw_pos' => $pos,
735  'wait_pos' => $waitPos,
736  'seconds_waited' => $seconds,
737  ] )
738  );
739  // Remember that this position was reached to save queries next time
740  $this->lastKnownReplicaPos = $pos;
741  }
742 
743  return $status;
744  }
745 
751  public function getReplicaPos() {
752  $now = microtime( true ); // as-of-time *before* fetching GTID variables
753 
754  if ( $this->useGTIDs() ) {
755  // Try to use GTIDs, fallbacking to binlog positions if not possible
756  $data = $this->getServerGTIDs( __METHOD__ );
757  // Use gtid_slave_pos for MariaDB and gtid_executed for MySQL
758  foreach ( [ 'gtid_slave_pos', 'gtid_executed' ] as $name ) {
759  if ( isset( $data[$name] ) && strlen( $data[$name] ) ) {
760  return new MySQLPrimaryPos( $data[$name], $now );
761  }
762  }
763  }
764 
765  $data = $this->getServerRoleStatus( 'SLAVE', __METHOD__ );
766  if ( $data && strlen( $data['Relay_Master_Log_File'] ) ) {
767  return new MySQLPrimaryPos(
768  "{$data['Relay_Master_Log_File']}/{$data['Exec_Master_Log_Pos']}",
769  $now
770  );
771  }
772 
773  return false;
774  }
775 
781  public function getPrimaryPos() {
782  $now = microtime( true ); // as-of-time *before* fetching GTID variables
783 
784  $pos = false;
785  if ( $this->useGTIDs() ) {
786  // Try to use GTIDs, fallbacking to binlog positions if not possible
787  $data = $this->getServerGTIDs( __METHOD__ );
788  // Use gtid_binlog_pos for MariaDB and gtid_executed for MySQL
789  foreach ( [ 'gtid_binlog_pos', 'gtid_executed' ] as $name ) {
790  if ( isset( $data[$name] ) && strlen( $data[$name] ) ) {
791  $pos = new MySQLPrimaryPos( $data[$name], $now );
792  break;
793  }
794  }
795  // Filter domains that are inactive or not relevant to the session
796  if ( $pos ) {
797  $pos->setActiveOriginServerId( $this->getServerId() );
798  $pos->setActiveOriginServerUUID( $this->getServerUUID() );
799  if ( isset( $data['gtid_domain_id'] ) ) {
800  $pos->setActiveDomain( $data['gtid_domain_id'] );
801  }
802  }
803  }
804 
805  if ( !$pos ) {
806  $data = $this->getServerRoleStatus( 'MASTER', __METHOD__ );
807  if ( $data && strlen( $data['File'] ) ) {
808  $pos = new MySQLPrimaryPos( "{$data['File']}/{$data['Position']}", $now );
809  }
810  }
811 
812  return $pos;
813  }
814 
819  public function getTopologyBasedServerId() {
820  return $this->getServerId();
821  }
822 
827  protected function getServerId() {
828  $fname = __METHOD__;
829  return $this->srvCache->getWithSetCallback(
830  $this->srvCache->makeGlobalKey( 'mysql-server-id', $this->getServerName() ),
831  self::SERVER_ID_CACHE_TTL,
832  function () use ( $fname ) {
833  $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
834  $res = $this->query( "SELECT @@server_id AS id", $fname, $flags );
835 
836  return $res->fetchObject()->id;
837  }
838  );
839  }
840 
845  protected function getServerUUID() {
846  $fname = __METHOD__;
847  return $this->srvCache->getWithSetCallback(
848  $this->srvCache->makeGlobalKey( 'mysql-server-uuid', $this->getServerName() ),
849  self::SERVER_ID_CACHE_TTL,
850  function () use ( $fname ) {
851  $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
852  $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'server_uuid'", $fname, $flags );
853  $row = $res->fetchObject();
854 
855  return $row ? $row->Value : null;
856  }
857  );
858  }
859 
864  protected function getServerGTIDs( $fname = __METHOD__ ) {
865  $map = [];
866 
867  $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
868 
869  // Get global-only variables like gtid_executed
870  $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_%'", $fname, $flags );
871  foreach ( $res as $row ) {
872  $map[$row->Variable_name] = $row->Value;
873  }
874  // Get session-specific (e.g. gtid_domain_id since that is were writes will log)
875  $res = $this->query( "SHOW SESSION VARIABLES LIKE 'gtid_%'", $fname, $flags );
876  foreach ( $res as $row ) {
877  $map[$row->Variable_name] = $row->Value;
878  }
879 
880  return $map;
881  }
882 
888  protected function getServerRoleStatus( $role, $fname = __METHOD__ ) {
889  $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
890  $res = $this->query( "SHOW $role STATUS", $fname, $flags );
891 
892  return $res->fetchRow() ?: [];
893  }
894 
895  public function serverIsReadOnly() {
896  // Avoid SHOW to avoid internal temporary tables
897  $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
898  $res = $this->query( "SELECT @@GLOBAL.read_only AS Value", __METHOD__, $flags );
899  $row = $res->fetchObject();
900 
901  return $row ? (bool)$row->Value : false;
902  }
903 
907  public function getSoftwareLink() {
908  list( $variant ) = $this->getMySqlServerVariant();
909  if ( $variant === 'MariaDB' ) {
910  return '[{{int:version-db-mariadb-url}} MariaDB]';
911  }
912 
913  return '[{{int:version-db-mysql-url}} MySQL]';
914  }
915 
919  protected function getMySqlServerVariant() {
920  $version = $this->getServerVersion();
921 
922  // MariaDB includes its name in its version string; this is how MariaDB's version of
923  // the mysql command-line client identifies MariaDB servers.
924  // https://dev.mysql.com/doc/refman/8.0/en/information-functions.html#function_version
925  // https://mariadb.com/kb/en/version/
926  $parts = explode( '-', $version, 2 );
927  $number = $parts[0];
928  $suffix = $parts[1] ?? '';
929  if ( strpos( $suffix, 'MariaDB' ) !== false || strpos( $suffix, '-maria-' ) !== false ) {
930  $vendor = 'MariaDB';
931  } else {
932  $vendor = 'MySQL';
933  }
934 
935  return [ $vendor, $number ];
936  }
937 
941  public function getServerVersion() {
943  $fname = __METHOD__;
944 
945  return $cache->getWithSetCallback(
946  $cache->makeGlobalKey( 'mysql-server-version', $this->getServerName() ),
947  $cache::TTL_HOUR,
948  function () use ( $fname ) {
949  // Not using mysql_get_server_info() or similar for consistency: in the handshake,
950  // MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip
951  // it off (see RPL_VERSION_HACK in include/mysql_com.h).
952  return $this->selectField( '', 'VERSION()', '', $fname );
953  }
954  );
955  }
956 
960  public function setSessionOptions( array $options ) {
961  $sqlAssignments = [];
962 
963  if ( isset( $options['connTimeout'] ) ) {
964  $encTimeout = (int)$options['connTimeout'];
965  $sqlAssignments[] = "net_read_timeout=$encTimeout";
966  $sqlAssignments[] = "net_write_timeout=$encTimeout";
967  }
968 
969  if ( $sqlAssignments ) {
970  $this->query(
971  'SET ' . implode( ', ', $sqlAssignments ),
972  __METHOD__,
973  self::QUERY_CHANGE_TRX | self::QUERY_CHANGE_NONE
974  );
975  }
976  }
977 
983  public function streamStatementEnd( &$sql, &$newLine ) {
984  if ( preg_match( '/^DELIMITER\s+(\S+)/i', $newLine, $m ) ) {
985  $this->delimiter = $m[1];
986  $newLine = '';
987  }
988 
989  return parent::streamStatementEnd( $sql, $newLine );
990  }
991 
992  public function doLockIsFree( string $lockName, string $method ) {
993  $encName = $this->addQuotes( $this->makeLockName( $lockName ) );
994 
995  $res = $this->query(
996  "SELECT IS_FREE_LOCK($encName) AS unlocked",
997  $method,
998  self::QUERY_CHANGE_LOCKS
999  );
1000  $row = $res->fetchObject();
1001 
1002  return ( $row->unlocked == 1 );
1003  }
1004 
1005  public function doLock( string $lockName, string $method, int $timeout ) {
1006  $encName = $this->addQuotes( $this->makeLockName( $lockName ) );
1007  // Unlike NOW(), SYSDATE() gets the time at invocation rather than query start.
1008  // The precision argument is silently ignored for MySQL < 5.6 and MariaDB < 5.3.
1009  // https://dev.mysql.com/doc/refman/5.6/en/date-and-time-functions.html#function_sysdate
1010  // https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html
1011  $res = $this->query(
1012  "SELECT IF(GET_LOCK($encName,$timeout),UNIX_TIMESTAMP(SYSDATE(6)),NULL) AS acquired",
1013  $method,
1014  self::QUERY_CHANGE_LOCKS
1015  );
1016  $row = $res->fetchObject();
1017 
1018  return ( $row->acquired !== null ) ? (float)$row->acquired : null;
1019  }
1020 
1021  public function doUnlock( string $lockName, string $method ) {
1022  $encName = $this->addQuotes( $this->makeLockName( $lockName ) );
1023 
1024  $res = $this->query(
1025  "SELECT RELEASE_LOCK($encName) AS released",
1026  $method,
1027  self::QUERY_CHANGE_LOCKS
1028  );
1029  $row = $res->fetchObject();
1030 
1031  return ( $row->released == 1 );
1032  }
1033 
1034  private function makeLockName( $lockName ) {
1035  // https://dev.mysql.com/doc/refman/5.7/en/locking-functions.html#function_get-lock
1036  // MySQL 5.7+ enforces a 64 char length limit.
1037  return ( strlen( $lockName ) > 64 ) ? sha1( $lockName ) : $lockName;
1038  }
1039 
1040  public function namedLocksEnqueue() {
1041  return true;
1042  }
1043 
1044  protected function doFlushSession( $fname ) {
1045  $flags = self::QUERY_CHANGE_LOCKS | self::QUERY_NO_RETRY;
1046  // Note that RELEASE_ALL_LOCKS() is not supported well enough to use here.
1047  // https://mariadb.com/kb/en/release_all_locks/
1048  $releaseLockFields = [];
1049  foreach ( $this->sessionNamedLocks as $name => $info ) {
1050  $encName = $this->addQuotes( $this->makeLockName( $name ) );
1051  $releaseLockFields[] = "RELEASE_LOCK($encName)";
1052  }
1053  if ( $releaseLockFields ) {
1054  $sql = 'SELECT ' . implode( ',', $releaseLockFields );
1055  $qs = $this->executeQuery( $sql, __METHOD__, $flags, $sql );
1056  if ( $qs->res === false ) {
1057  $this->reportQueryError( $qs->message, $qs->code, $sql, $fname, true );
1058  }
1059  }
1060  }
1061 
1065  public function setBigSelects( $value = true ) {
1066  if ( $value === 'default' ) {
1067  if ( $this->defaultBigSelects === null ) {
1068  # Function hasn't been called before so it must already be set to the default
1069  return;
1070  } else {
1071  $value = $this->defaultBigSelects;
1072  }
1073  } elseif ( $this->defaultBigSelects === null ) {
1074  $this->defaultBigSelects =
1075  (bool)$this->selectField( false, '@@sql_big_selects', '', __METHOD__ );
1076  }
1077 
1078  $this->query(
1079  "SET sql_big_selects=" . ( $value ? '1' : '0' ),
1080  __METHOD__,
1081  self::QUERY_CHANGE_TRX
1082  );
1083  }
1084 
1095  public function deleteJoin(
1096  $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__
1097  ) {
1098  if ( !$conds ) {
1099  throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
1100  }
1101 
1102  $delTable = $this->tableName( $delTable );
1103  $joinTable = $this->tableName( $joinTable );
1104  $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
1105 
1106  if ( $conds != '*' ) {
1107  $sql .= ' AND ' . $this->makeList( $conds, self::LIST_AND );
1108  }
1109 
1110  $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
1111  }
1112 
1113  protected function doUpsert(
1114  string $table,
1115  array $rows,
1116  array $identityKey,
1117  array $set,
1118  string $fname
1119  ) {
1120  $encTable = $this->tableName( $table );
1121  list( $sqlColumns, $sqlTuples ) = $this->makeInsertLists( $rows );
1122  $sqlColumnAssignments = $this->makeList( $set, self::LIST_SET );
1123  // No need to expose __NEW.* since buildExcludedValue() uses VALUES(column)
1124 
1125  // https://mariadb.com/kb/en/insert-on-duplicate-key-update/
1126  // https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html
1127  $sql =
1128  "INSERT INTO $encTable " .
1129  "($sqlColumns) VALUES $sqlTuples " .
1130  "ON DUPLICATE KEY UPDATE $sqlColumnAssignments";
1131 
1132  $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
1133  }
1134 
1135  protected function doReplace( $table, array $identityKey, array $rows, $fname ) {
1136  $encTable = $this->tableName( $table );
1137  list( $sqlColumns, $sqlTuples ) = $this->makeInsertLists( $rows );
1138 
1139  $sql = "REPLACE INTO $encTable ($sqlColumns) VALUES $sqlTuples";
1140 
1141  $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
1142  }
1143 
1149  public function wasDeadlock() {
1150  return $this->lastErrno() == 1213;
1151  }
1152 
1158  public function wasLockTimeout() {
1159  return $this->lastErrno() == 1205;
1160  }
1161 
1167  public function wasReadOnlyError() {
1168  return $this->lastErrno() == 1223 ||
1169  ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false );
1170  }
1171 
1172  protected function isConnectionError( $errno ) {
1173  // https://mariadb.com/kb/en/mariadb-error-codes/
1174  // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
1175  // https://dev.mysql.com/doc/mysql-errors/8.0/en/client-error-reference.html
1176  return in_array( $errno, [ 2013, 2006, 2003, 1927, 1053 ], true );
1177  }
1178 
1179  protected function isQueryTimeoutError( $errno ) {
1180  // https://mariadb.com/kb/en/mariadb-error-codes/
1181  // https://dev.mysql.com/doc/refman/8.0/en/client-error-reference.html
1182  // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
1183  return in_array( $errno, [ 3024, 2062, 1969, 1028 ], true );
1184  }
1185 
1186  protected function isKnownStatementRollbackError( $errno ) {
1187  // https://mariadb.com/kb/en/mariadb-error-codes/
1188  // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
1189  if ( $errno === 1205 ) { // lock wait timeout
1190  // Note that this is uncached to avoid stale values if SET is used
1191  $res = $this->query(
1192  "SELECT @@innodb_rollback_on_timeout AS Value",
1193  __METHOD__,
1194  self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
1195  );
1196  $row = $res ? $res->fetchObject() : false;
1197  // https://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
1198  // https://dev.mysql.com/doc/refman/5.5/en/innodb-parameters.html
1199  return ( $row && !$row->Value );
1200  }
1201 
1202  return in_array(
1203  $errno,
1204  [ 3024, 1969, 1022, 1062, 1216, 1217, 1137, 1146, 1051, 1054 ],
1205  true
1206  );
1207  }
1208 
1216  public function duplicateTableStructure(
1217  $oldName, $newName, $temporary = false, $fname = __METHOD__
1218  ) {
1219  $tmp = $temporary ? 'TEMPORARY ' : '';
1220  $newName = $this->addIdentifierQuotes( $newName );
1221  $oldName = $this->addIdentifierQuotes( $oldName );
1222 
1223  return $this->query(
1224  "CREATE $tmp TABLE $newName (LIKE $oldName)",
1225  $fname,
1226  self::QUERY_PSEUDO_PERMANENT | self::QUERY_CHANGE_SCHEMA
1227  );
1228  }
1229 
1237  public function listTables( $prefix = null, $fname = __METHOD__ ) {
1238  $result = $this->query(
1239  "SHOW TABLES",
1240  $fname,
1241  self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
1242  );
1243 
1244  $endArray = [];
1245 
1246  foreach ( $result as $table ) {
1247  $vars = get_object_vars( $table );
1248  $table = array_pop( $vars );
1249 
1250  if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
1251  $endArray[] = $table;
1252  }
1253  }
1254 
1255  return $endArray;
1256  }
1257 
1267  public function listViews( $prefix = null, $fname = __METHOD__ ) {
1268  // The name of the column containing the name of the VIEW
1269  $propertyName = 'Tables_in_' . $this->getDBname();
1270 
1271  // Query for the VIEWS
1272  $res = $this->query(
1273  'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"',
1274  $fname,
1275  self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
1276  );
1277 
1278  $allViews = [];
1279  foreach ( $res as $row ) {
1280  array_push( $allViews, $row->$propertyName );
1281  }
1282 
1283  if ( $prefix === null || $prefix === '' ) {
1284  return $allViews;
1285  }
1286 
1287  $filteredViews = [];
1288  foreach ( $allViews as $viewName ) {
1289  // Does the name of this VIEW start with the table-prefix?
1290  if ( strpos( $viewName, $prefix ) === 0 ) {
1291  array_push( $filteredViews, $viewName );
1292  }
1293  }
1294 
1295  return $filteredViews;
1296  }
1297 
1306  public function isView( $name, $prefix = null ) {
1307  return in_array( $name, $this->listViews( $prefix, __METHOD__ ) );
1308  }
1309 
1310  protected function isTransactableQuery( $sql ) {
1311  return parent::isTransactableQuery( $sql ) &&
1312  !preg_match( '/^SELECT\s+(GET|RELEASE|IS_FREE)_LOCK\‍(/', $sql );
1313  }
1314 
1315  public function selectSQLText(
1316  $table,
1317  $vars,
1318  $conds = '',
1319  $fname = __METHOD__,
1320  $options = [],
1321  $join_conds = []
1322  ) {
1323  $sql = parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1324  // https://dev.mysql.com/doc/refman/5.7/en/optimizer-hints.html
1325  // https://mariadb.com/kb/en/library/aborting-statements/
1326  $timeoutMsec = intval( $options['MAX_EXECUTION_TIME'] ?? 0 );
1327  if ( $timeoutMsec > 0 ) {
1328  list( $vendor, $number ) = $this->getMySqlServerVariant();
1329  if ( $vendor === 'MariaDB' && version_compare( $number, '10.1.2', '>=' ) ) {
1330  $timeoutSec = $timeoutMsec / 1000;
1331  $sql = "SET STATEMENT max_statement_time=$timeoutSec FOR $sql";
1332  } elseif ( $vendor === 'MySQL' && version_compare( $number, '5.7.0', '>=' ) ) {
1333  $sql = preg_replace(
1334  '/^SELECT(?=\s)/',
1335  "SELECT /*+ MAX_EXECUTION_TIME($timeoutMsec)*/",
1336  $sql
1337  );
1338  }
1339  }
1340 
1341  return $sql;
1342  }
1343 
1344  public function buildExcludedValue( $column ) {
1345  /* @see DatabaseMysqlBase::doUpsert() */
1346  // Within "INSERT INTO ON DUPLICATE KEY UPDATE" statements:
1347  // - MySQL>= 8.0.20 supports and prefers "VALUES ... AS".
1348  // - MariaDB >= 10.3.3 supports and prefers VALUE().
1349  // - Both support the old VALUES() function
1350  // https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html
1351  // https://mariadb.com/kb/en/insert-on-duplicate-key-update/
1352  return "VALUES($column)";
1353  }
1354 
1358  protected function useGTIDs() {
1359  return $this->useGTIDs;
1360  }
1361 }
1362 
1366 class_alias( DatabaseMysqlBase::class, 'DatabaseMysqlBase' );
const LIST_SET
Definition: Defines.php:44
const LIST_AND
Definition: Defines.php:43
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition: WebStart.php:82
Database error base class.
Definition: DBError.php:32
Base class for the more common types of database errors.
Class to handle database/schema/prefix specifications for IDatabase.
Database abstraction object for MySQL.
doSelectDomain(DatabaseDomain $domain)
__construct(array $params)
Additional $params include:
string null $sslCiphers
Open SSL cipher list string.
duplicateTableStructure( $oldName, $newName, $temporary=false, $fname=__METHOD__)
doLock(string $lockName, string $method, int $timeout)
namedLocksEnqueue()
Check to see if a named lock used by lock() use blocking queues.bool 1.26Stability: stableto override
array $lagDetectionOptions
Method to detect replica DB lag.
isInsertSelectSafe(array $insertOptions, array $selectOptions)
doUnlock(string $lockName, string $method)
wasReadOnlyError()
Determines if the last failure was due to the database being read-only.
string $lagDetectionMethod
Method to detect replica DB lag.
getApproximateLagStatus()
Get a replica DB lag estimate for this server at the start of a transaction.
doUpsert(string $table, array $rows, array $identityKey, array $set, string $fname)
Perform an UPSERT query.
isConnectionError( $errno)
Do not use this method outside of Database/DBError classes.
doFlushSession( $fname)
Reset the server-side session state for named locks and table locks.
serverIsReadOnly()
bool Whether the DB is marked as read-only server-side If an error occurs, {query} 1....
buildExcludedValue( $column)
Build a reference to a column value from the conflicting proposed upsert() row.
indexInfo( $table, $index, $fname=__METHOD__)
Get information about an index into an object Returns false if the index does not exist.
selectSQLText( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Take the same arguments as IDatabase::select() and return the SQL it would use.
mysqlError( $conn=null)
Returns the text of the error message from previous MySQL operation.
isTransactableQuery( $sql)
Determine whether a SQL statement is sensitive to isolation level.
open( $server, $user, $password, $db, $schema, $tablePrefix)
Open a new connection to the database (closing any existing one)
bool $utf8Mode
Use experimental UTF-8 transmission encoding.
doLockIsFree(string $lockName, string $method)
listTables( $prefix=null, $fname=__METHOD__)
List all tables on the database.
bool $useGTIDs
bool Whether to use GTID methods
mysqlRealEscapeString( $s)
Escape special characters in a string for use in an SQL statement.
getServerRoleStatus( $role, $fname=__METHOD__)
getTopologyBasedServerId()
Get a non-recycled ID that uniquely identifies this server within the replication topology....
wasLockTimeout()
Determines if the last failure was due to a lock timeout.
doReplace( $table, array $identityKey, array $rows, $fname)
getPrimaryPos()
Get the position of the primary DB from SHOW MASTER STATUS.
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 ...
estimateRowCount( $tables, $var=' *', $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Estimate rows in dataset Returns estimated count, based on EXPLAIN output Takes same arguments as Dat...
mysqlConnect( $server, $user, $password, $db)
Open a connection to a MySQL server.
isQueryTimeoutError( $errno)
Checks whether the cause of the error is detected to be a timeout.
getReplicaPos()
Get the position of the primary DB from SHOW SLAVE STATUS.
listViews( $prefix=null, $fname=__METHOD__)
Lists VIEWs in the database.
isView( $name, $prefix=null)
Differentiates between a TABLE and a VIEW.
tableExists( $table, $fname=__METHOD__)
Query whether a given table exists.
wasDeadlock()
Determines if the last failure was due to a deadlock.
deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname=__METHOD__)
DELETE where the condition is a join.
doGetLag()
Get the amount of replication lag for this database server.
Relational database abstraction object.
Definition: Database.php:50
string null $password
Password used to establish the current connection.
Definition: Database.php:85
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Wrapper to IDatabase::select() that only fetches one row (via LIMIT)
Definition: Database.php:2007
restoreErrorHandler()
Restore the previous error handler and return the last PHP error for this DB.
Definition: Database.php:820
object resource null $conn
Database connection.
Definition: Database.php:75
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.string
Definition: Database.php:2644
makeInsertLists(array $rows, $aliasPrefix='')
Make SQL lists of columns, row tuples, and column aliases for INSERT/VALUES expressions.
Definition: Database.php:2452
newExceptionAfterConnectError( $error)
Definition: Database.php:1922
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
Definition: Database.php:1993
getRecordedTransactionLagStatus()
Get the replica DB lag when the current transaction started.
Definition: Database.php:3986
int $flags
Current bit field of class DBO_* constants.
Definition: Database.php:104
installErrorHandler()
Set a custom error handler for logging errors during database connection.
Definition: Database.php:809
tableName( $name, $format='quoted')
Format a table name ready for use in constructing an SQL query.
Definition: Database.php:4813
addIdentifierQuotes( $s)
Escape a SQL identifier (e.g.
Definition: Database.php:4829
getLogContext(array $extras=[])
Create a log context to pass to PSR-3 logger functions.
Definition: Database.php:861
string null $server
Server that this instance is currently connected to.
Definition: Database.php:81
close( $fname=__METHOD__)
Close the database connection.
Definition: Database.php:872
getServerName()
Get the readable name for the server.
Definition: Database.php:2619
reportQueryError( $error, $errno, $sql, $fname, $ignore=false)
Report a query error.
Definition: Database.php:1865
normalizeConditions( $conds, $fname)
Definition: Database.php:2106
string null $user
User that this instance is currently connected under the name of.
Definition: Database.php:83
makeList(array $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.
Definition: Database.php:4765
BagOStuff $srvCache
APC cache.
Definition: Database.php:52
query( $sql, $fname=__METHOD__, $flags=0)
Run an SQL query statement and return the result.
Definition: Database.php:1228
executeQuery( $sqls, $fname, $flags, $summarySql)
Execute a set of queries without enforcing public (non-Database) caller restrictions.
Definition: Database.php:1324
getDBname()
Get the current database name; null if there isn't one.
Definition: Database.php:2611
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:1945
DBPrimaryPos class for MySQL/MariaDB.
An object representing a primary or replica DB position in a replicated setup.
lastErrno()
Get the RDBMS-specific error code from the last query statement.
Interface for query language.
$cache
Definition: mcc.php:33
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s