MediaWiki REL1_37
DatabaseMysqlBase.php
Go to the documentation of this file.
1<?php
23namespace Wikimedia\Rdbms;
24
25use InvalidArgumentException;
26use mysqli_result;
27use RuntimeException;
28use stdClass;
29use Wikimedia\AtEase\AtEase;
30
44abstract class DatabaseMysqlBase extends Database {
50 protected $lagDetectionOptions = [];
52 protected $useGTIDs = false;
54 protected $sslKeyPath;
56 protected $sslCertPath;
58 protected $sslCAFile;
60 protected $sslCAPath;
66 protected $sslCiphers;
68 protected $sqlMode;
70 protected $utf8Mode;
73
78
79 // Cache getServerId() for 24 hours
80 private const SERVER_ID_CACHE_TTL = 86400;
81
83 private const LAG_STALE_WARN_THRESHOLD = 0.100;
84
104 public function __construct( array $params ) {
105 $this->lagDetectionMethod = $params['lagDetectionMethod'] ?? 'Seconds_Behind_Master';
106 $this->lagDetectionOptions = $params['lagDetectionOptions'] ?? [];
107 $this->useGTIDs = !empty( $params['useGTIDs' ] );
108 foreach ( [ 'KeyPath', 'CertPath', 'CAFile', 'CAPath', 'Ciphers' ] as $name ) {
109 $var = "ssl{$name}";
110 if ( isset( $params[$var] ) ) {
111 $this->$var = $params[$var];
112 }
113 }
114 $this->sqlMode = $params['sqlMode'] ?? null;
115 $this->utf8Mode = !empty( $params['utf8Mode'] );
116 $this->insertSelectIsSafe = isset( $params['insertSelectIsSafe'] )
117 ? (bool)$params['insertSelectIsSafe'] : null;
118
119 parent::__construct( $params );
120 }
121
125 public function getType() {
126 return 'mysql';
127 }
128
129 protected function open( $server, $user, $password, $db, $schema, $tablePrefix ) {
130 $this->close( __METHOD__ );
131
132 if ( $schema !== null ) {
133 throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." );
134 }
135
136 $this->installErrorHandler();
137 try {
138 $this->conn = $this->mysqlConnect( $server, $user, $password, $db );
139 } catch ( RuntimeException $e ) {
140 $this->restoreErrorHandler();
141 throw $this->newExceptionAfterConnectError( $e->getMessage() );
142 }
143 $error = $this->restoreErrorHandler();
144
145 if ( !$this->conn ) {
146 throw $this->newExceptionAfterConnectError( $error ?: $this->lastError() );
147 }
148
149 try {
150 $this->currentDomain = new DatabaseDomain(
151 $db && strlen( $db ) ? $db : null,
152 null,
153 $tablePrefix
154 );
155 // Abstract over any insane MySQL defaults
156 $set = [ 'group_concat_max_len = 262144' ];
157 // Set SQL mode, default is turning them all off, can be overridden or skipped with null
158 if ( is_string( $this->sqlMode ) ) {
159 $set[] = 'sql_mode = ' . $this->addQuotes( $this->sqlMode );
160 }
161 // Set any custom settings defined by site config
162 // https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html
163 foreach ( $this->connectionVariables as $var => $val ) {
164 // Escape strings but not numbers to avoid MySQL complaining
165 if ( !is_int( $val ) && !is_float( $val ) ) {
166 $val = $this->addQuotes( $val );
167 }
168 $set[] = $this->addIdentifierQuotes( $var ) . ' = ' . $val;
169 }
170
171 // @phan-suppress-next-next-line PhanRedundantCondition
172 // If kept for safety and to avoid broken query
173 if ( $set ) {
174 $this->query(
175 'SET ' . implode( ', ', $set ),
176 __METHOD__,
177 self::QUERY_IGNORE_DBO_TRX | self::QUERY_NO_RETRY | self::QUERY_CHANGE_TRX
178 );
179 }
180 } catch ( RuntimeException $e ) {
181 throw $this->newExceptionAfterConnectError( $e->getMessage() );
182 }
183 }
184
185 protected function doSelectDomain( DatabaseDomain $domain ) {
186 if ( $domain->getSchema() !== null ) {
187 throw new DBExpectedError(
188 $this,
189 __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
190 );
191 }
192
193 $database = $domain->getDatabase();
194 // A null database means "don't care" so leave it as is and update the table prefix
195 if ( $database === null ) {
196 $this->currentDomain = new DatabaseDomain(
197 $this->currentDomain->getDatabase(),
198 null,
199 $domain->getTablePrefix()
200 );
201
202 return true;
203 }
204
205 if ( $database !== $this->getDBname() ) {
206 $sql = 'USE ' . $this->addIdentifierQuotes( $database );
207 list( $res, $err, $errno ) =
208 $this->executeQuery( $sql, __METHOD__, self::QUERY_IGNORE_DBO_TRX );
209
210 if ( $res === false ) {
211 $this->reportQueryError( $err, $errno, $sql, __METHOD__ );
212 return false; // unreachable
213 }
214 }
215
216 // Update that domain fields on success (no exception thrown)
217 $this->currentDomain = $domain;
218
219 return true;
220 }
221
232 abstract protected function mysqlConnect( $server, $user, $password, $db );
233
245 public function fieldType( $res, $n ) {
246 wfDeprecated( __METHOD__, '1.37' );
247 return $this->mysqlFieldType( $res->getInternalResult(), $n );
248 }
249
259 abstract protected function mysqlFieldType( $res, $n );
260
264 public function lastError() {
265 if ( $this->conn ) {
266 # Even if it's non-zero, it can still be invalid
267 AtEase::suppressWarnings();
268 $error = $this->mysqlError( $this->conn );
269 if ( !$error ) {
270 $error = $this->mysqlError();
271 }
272 AtEase::restoreWarnings();
273 } else {
274 $error = $this->mysqlError();
275 }
276 if ( $error ) {
277 $error .= ' (' . $this->getServerName() . ')';
278 }
279
280 return $error;
281 }
282
289 abstract protected function mysqlError( $conn = null );
290
291 protected function wasQueryTimeout( $error, $errno ) {
292 // https://dev.mysql.com/doc/refman/8.0/en/client-error-reference.html
293 // https://phabricator.wikimedia.org/T170638
294 return in_array( $errno, [ 2062, 3024 ] );
295 }
296
297 protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
298 $row = $this->getReplicationSafetyInfo();
299 // For row-based-replication, the resulting changes will be relayed, not the query
300 if ( $row->binlog_format === 'ROW' ) {
301 return true;
302 }
303 // LIMIT requires ORDER BY on a unique key or it is non-deterministic
304 if ( isset( $selectOptions['LIMIT'] ) ) {
305 return false;
306 }
307 // In MySQL, an INSERT SELECT is only replication safe with row-based
308 // replication or if innodb_autoinc_lock_mode is 0. When those
309 // conditions aren't met, use non-native mode.
310 // While we could try to determine if the insert is safe anyway by
311 // checking if the target table has an auto-increment column that
312 // isn't set in $varMap, that seems unlikely to be worth the extra
313 // complexity.
314 return (
315 in_array( 'NO_AUTO_COLUMNS', $insertOptions ) ||
316 (int)$row->innodb_autoinc_lock_mode === 0
317 );
318 }
319
323 protected function getReplicationSafetyInfo() {
324 if ( $this->replicationInfoRow === null ) {
325 $this->replicationInfoRow = $this->selectRow(
326 false,
327 [
328 'innodb_autoinc_lock_mode' => '@@innodb_autoinc_lock_mode',
329 'binlog_format' => '@@binlog_format',
330 ],
331 [],
332 __METHOD__
333 );
334 }
335
337 }
338
352 public function estimateRowCount(
353 $tables,
354 $var = '*',
355 $conds = '',
356 $fname = __METHOD__,
357 $options = [],
358 $join_conds = []
359 ) {
360 $conds = $this->normalizeConditions( $conds, $fname );
361 $column = $this->extractSingleFieldFromList( $var );
362 if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
363 $conds[] = "$column IS NOT NULL";
364 }
365
366 $options['EXPLAIN'] = true;
367 $res = $this->select( $tables, $var, $conds, $fname, $options, $join_conds );
368 if ( $res === false ) {
369 return false;
370 }
371 if ( !$this->numRows( $res ) ) {
372 return 0;
373 }
374
375 $rows = 1;
376 foreach ( $res as $plan ) {
377 $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero
378 }
379
380 return (int)$rows;
381 }
382
383 public function tableExists( $table, $fname = __METHOD__ ) {
384 // Split database and table into proper variables as Database::tableName() returns
385 // shared tables prefixed with their database, which do not work in SHOW TABLES statements
386 list( $database, , $prefix, $table ) = $this->qualifiedTableComponents( $table );
387 $tableName = "{$prefix}{$table}";
388
389 if ( isset( $this->sessionTempTables[$tableName] ) ) {
390 return true; // already known to exist and won't show in SHOW TABLES anyway
391 }
392
393 // We can't use buildLike() here, because it specifies an escape character
394 // other than the backslash, which is the only one supported by SHOW TABLES
395 $encLike = $this->escapeLikeInternal( $tableName, '\\' );
396
397 // If the database has been specified (such as for shared tables), use "FROM"
398 if ( $database !== '' ) {
399 $encDatabase = $this->addIdentifierQuotes( $database );
400 $sql = "SHOW TABLES FROM $encDatabase LIKE '$encLike'";
401 } else {
402 $sql = "SHOW TABLES LIKE '$encLike'";
403 }
404
405 $res = $this->query(
406 $sql,
407 $fname,
408 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
409 );
410
411 return $res->numRows() > 0;
412 }
413
419 public function fieldInfo( $table, $field ) {
420 $res = $this->query(
421 "SELECT * FROM " . $this->tableName( $table ) . " LIMIT 1",
422 __METHOD__,
423 self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
424 );
425 if ( !$res ) {
426 return false;
427 }
429 '@phan-var MysqliResultWrapper $res';
430 return $res->getInternalFieldInfo( $field );
431 }
432
442 public function indexInfo( $table, $index, $fname = __METHOD__ ) {
443 # https://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
444 $index = $this->indexName( $index );
445
446 $res = $this->query(
447 'SHOW INDEX FROM ' . $this->tableName( $table ),
448 $fname,
449 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
450 );
451
452 if ( !$res ) {
453 return null;
454 }
455
456 $result = [];
457
458 foreach ( $res as $row ) {
459 if ( $row->Key_name == $index ) {
460 $result[] = $row;
461 }
462 }
463
464 return $result ?: false;
465 }
466
471 public function strencode( $s ) {
472 return $this->mysqlRealEscapeString( $s );
473 }
474
479 abstract protected function mysqlRealEscapeString( $s );
480
487 public function addIdentifierQuotes( $s ) {
488 // Characters in the range \u0001-\uFFFF are valid in a quoted identifier
489 // Remove NUL bytes and escape backticks by doubling
490 return '`' . str_replace( [ "\0", '`' ], [ '', '``' ], $s ) . '`';
491 }
492
497 public function isQuotedIdentifier( $name ) {
498 return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`';
499 }
500
501 protected function doGetLag() {
502 if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
503 return $this->getLagFromPtHeartbeat();
504 } else {
505 return $this->getLagFromSlaveStatus();
506 }
507 }
508
512 protected function getLagDetectionMethod() {
514 }
515
519 protected function getLagFromSlaveStatus() {
520 $res = $this->query(
521 'SHOW SLAVE STATUS',
522 __METHOD__,
523 self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
524 );
525 $row = $res ? $res->fetchObject() : false;
526 // If the server is not replicating, there will be no row
527 if ( $row && strval( $row->Seconds_Behind_Master ) !== '' ) {
528 // https://mariadb.com/kb/en/delayed-replication/
529 // https://dev.mysql.com/doc/refman/5.6/en/replication-delayed.html
530 return intval( $row->Seconds_Behind_Master + ( $row->SQL_Remaining_Delay ?? 0 ) );
531 }
532
533 return false;
534 }
535
539 protected function getLagFromPtHeartbeat() {
541
542 $currentTrxInfo = $this->getRecordedTransactionLagStatus();
543 if ( $currentTrxInfo ) {
544 // There is an active transaction and the initial lag was already queried
545 $staleness = microtime( true ) - $currentTrxInfo['since'];
546 if ( $staleness > self::LAG_STALE_WARN_THRESHOLD ) {
547 // Avoid returning higher and higher lag value due to snapshot age
548 // given that the isolation level will typically be REPEATABLE-READ
549 $this->queryLogger->warning(
550 "Using cached lag value for {db_server} due to active transaction",
551 $this->getLogContext( [
552 'method' => __METHOD__,
553 'age' => $staleness,
554 'exception' => new RuntimeException()
555 ] )
556 );
557 }
558
559 return $currentTrxInfo['lag'];
560 }
561
562 if ( isset( $options['conds'] ) ) {
563 // Best method for multi-DC setups: use logical channel names
564 $ago = $this->fetchSecondsSinceHeartbeat( $options['conds'] );
565 } else {
566 // Standard method: use primary server ID (works with stock pt-heartbeat)
567 $masterInfo = $this->getPrimaryServerInfo();
568 if ( !$masterInfo ) {
569 $this->queryLogger->error(
570 "Unable to query primary of {db_server} for server ID",
571 $this->getLogContext( [
572 'method' => __METHOD__
573 ] )
574 );
575
576 return false; // could not get primary server ID
577 }
578
579 $conds = [ 'server_id' => intval( $masterInfo['serverId'] ) ];
580 $ago = $this->fetchSecondsSinceHeartbeat( $conds );
581 }
582
583 if ( $ago !== null ) {
584 return max( $ago, 0.0 );
585 }
586
587 $this->queryLogger->error(
588 "Unable to find pt-heartbeat row for {db_server}",
589 $this->getLogContext( [
590 'method' => __METHOD__
591 ] )
592 );
593
594 return false;
595 }
596
597 protected function getPrimaryServerInfo() {
599 $key = $cache->makeGlobalKey(
600 'mysql',
601 'master-info',
602 // Using one key for all cluster replica DBs is preferable
603 $this->topologyRootMaster ?? $this->getServerName()
604 );
605 $fname = __METHOD__;
606
607 return $cache->getWithSetCallback(
608 $key,
609 $cache::TTL_INDEFINITE,
610 function () use ( $cache, $key, $fname ) {
611 // Get and leave a lock key in place for a short period
612 if ( !$cache->lock( $key, 0, 10 ) ) {
613 return false; // avoid primary DB connection spike slams
614 }
615
616 $conn = $this->getLazyMasterHandle();
617 if ( !$conn ) {
618 return false; // something is misconfigured
619 }
620
621 $flags = self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
622 // Connect to and query the primary DB; catch errors to avoid outages
623 try {
624 $res = $conn->query( 'SELECT @@server_id AS id', $fname, $flags );
625 $row = $res ? $res->fetchObject() : false;
626 $id = $row ? (int)$row->id : 0;
627 } catch ( DBError $e ) {
628 $id = 0;
629 }
630
631 // Cache the ID if it was retrieved
632 return $id ? [ 'serverId' => $id, 'asOf' => time() ] : false;
633 }
634 );
635 }
636
637 protected function getMasterServerInfo() {
638 wfDeprecated( __METHOD__, '1.37' );
639 return $this->getPrimaryServerInfo();
640 }
641
647 protected function fetchSecondsSinceHeartbeat( array $conds ) {
648 $whereSQL = $this->makeList( $conds, self::LIST_AND );
649 // User mysql server time so that query time and trip time are not counted.
650 // Use ORDER BY for channel based queries since that field might not be UNIQUE.
651 $res = $this->query(
652 "SELECT TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6)) AS us_ago " .
653 "FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1",
654 __METHOD__,
655 self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
656 );
657 $row = $res ? $res->fetchObject() : false;
658
659 return $row ? ( $row->us_ago / 1e6 ) : null;
660 }
661
662 protected function getApproximateLagStatus() {
663 if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
664 // Disable caching since this is fast enough and we don't wan't
665 // to be *too* pessimistic by having both the cache TTL and the
666 // pt-heartbeat interval count as lag in getSessionLagStatus()
667 return parent::getApproximateLagStatus();
668 }
669
670 $key = $this->srvCache->makeGlobalKey( 'mysql-lag', $this->getServerName() );
671 $approxLag = $this->srvCache->get( $key );
672 if ( !$approxLag ) {
673 $approxLag = parent::getApproximateLagStatus();
674 $this->srvCache->set( $key, $approxLag, 1 );
675 }
676
677 return $approxLag;
678 }
679
680 public function primaryPosWait( DBPrimaryPos $pos, $timeout ) {
681 if ( !( $pos instanceof MySQLPrimaryPos ) ) {
682 throw new InvalidArgumentException( "Position not an instance of MySQLPrimaryPos" );
683 }
684
685 if ( $this->topologyRole === self::ROLE_STATIC_CLONE ) {
686 $this->queryLogger->debug(
687 "Bypassed replication wait; database has a static dataset",
688 $this->getLogContext( [ 'method' => __METHOD__, 'raw_pos' => $pos ] )
689 );
690
691 return 0; // this is a copy of a read-only dataset with no primary DB
692 } elseif ( $this->lastKnownReplicaPos && $this->lastKnownReplicaPos->hasReached( $pos ) ) {
693 $this->queryLogger->debug(
694 "Bypassed replication wait; replication known to have reached {raw_pos}",
695 $this->getLogContext( [ 'method' => __METHOD__, 'raw_pos' => $pos ] )
696 );
697
698 return 0; // already reached this point for sure
699 }
700
701 // Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
702 if ( $pos->getGTIDs() ) {
703 // Get the GTIDs from this replica server too see the domains (channels)
704 $refPos = $this->getReplicaPos();
705 if ( !$refPos ) {
706 $this->queryLogger->error(
707 "Could not get replication position on replica DB to compare to {raw_pos}",
708 $this->getLogContext( [ 'method' => __METHOD__, 'raw_pos' => $pos ] )
709 );
710
711 return -1; // this is the primary DB itself?
712 }
713 // GTIDs with domains (channels) that are active and are present on the replica
714 $gtidsWait = $pos::getRelevantActiveGTIDs( $pos, $refPos );
715 if ( !$gtidsWait ) {
716 $this->queryLogger->error(
717 "No active GTIDs in {raw_pos} share a domain with those in {current_pos}",
718 $this->getLogContext( [
719 'method' => __METHOD__,
720 'raw_pos' => $pos,
721 'current_pos' => $refPos
722 ] )
723 );
724
725 return -1; // $pos is from the wrong cluster?
726 }
727 // Wait on the GTID set
728 $gtidArg = $this->addQuotes( implode( ',', $gtidsWait ) );
729 if ( strpos( $gtidArg, ':' ) !== false ) {
730 // MySQL GTIDs, e.g "source_id:transaction_id"
731 $sql = "SELECT WAIT_FOR_EXECUTED_GTID_SET($gtidArg, $timeout)";
732 } else {
733 // MariaDB GTIDs, e.g."domain:server:sequence"
734 $sql = "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)";
735 }
736 $waitPos = implode( ',', $gtidsWait );
737 } else {
738 // Wait on the binlog coordinates
739 $encFile = $this->addQuotes( $pos->getLogFile() );
740 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
741 $encPos = intval( $pos->getLogPosition()[$pos::CORD_EVENT] );
742 $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)";
743 $waitPos = $pos->__toString();
744 }
745
746 $start = microtime( true );
747 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
748 $res = $this->query( $sql, __METHOD__, $flags );
749 $row = $this->fetchRow( $res );
750 $seconds = max( microtime( true ) - $start, 0 );
751
752 // Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual
753 $status = ( $row[0] !== null ) ? intval( $row[0] ) : null;
754 if ( $status === null ) {
755 $this->replLogger->error(
756 "An error occurred while waiting for replication to reach {wait_pos}",
757 $this->getLogContext( [
758 'raw_pos' => $pos,
759 'wait_pos' => $waitPos,
760 'sql' => $sql,
761 'seconds_waited' => $seconds,
762 'exception' => new RuntimeException()
763 ] )
764 );
765 } elseif ( $status < 0 ) {
766 $this->replLogger->error(
767 "Timed out waiting for replication to reach {wait_pos}",
768 $this->getLogContext( [
769 'raw_pos' => $pos,
770 'wait_pos' => $waitPos,
771 'timeout' => $timeout,
772 'sql' => $sql,
773 'seconds_waited' => $seconds,
774 'exception' => new RuntimeException()
775 ] )
776 );
777 } elseif ( $status >= 0 ) {
778 $this->replLogger->debug(
779 "Replication has reached {wait_pos}",
780 $this->getLogContext( [
781 'raw_pos' => $pos,
782 'wait_pos' => $waitPos,
783 'seconds_waited' => $seconds,
784 ] )
785 );
786 // Remember that this position was reached to save queries next time
787 $this->lastKnownReplicaPos = $pos;
788 }
789
790 return $status;
791 }
792
798 public function getReplicaPos() {
799 $now = microtime( true ); // as-of-time *before* fetching GTID variables
800
801 if ( $this->useGTIDs() ) {
802 // Try to use GTIDs, fallbacking to binlog positions if not possible
803 $data = $this->getServerGTIDs( __METHOD__ );
804 // Use gtid_slave_pos for MariaDB and gtid_executed for MySQL
805 foreach ( [ 'gtid_slave_pos', 'gtid_executed' ] as $name ) {
806 if ( isset( $data[$name] ) && strlen( $data[$name] ) ) {
807 return new MySQLPrimaryPos( $data[$name], $now );
808 }
809 }
810 }
811
812 $data = $this->getServerRoleStatus( 'SLAVE', __METHOD__ );
813 if ( $data && strlen( $data['Relay_Master_Log_File'] ) ) {
814 return new MySQLPrimaryPos(
815 "{$data['Relay_Master_Log_File']}/{$data['Exec_Master_Log_Pos']}",
816 $now
817 );
818 }
819
820 return false;
821 }
822
828 public function getPrimaryPos() {
829 $now = microtime( true ); // as-of-time *before* fetching GTID variables
830
831 $pos = false;
832 if ( $this->useGTIDs() ) {
833 // Try to use GTIDs, fallbacking to binlog positions if not possible
834 $data = $this->getServerGTIDs( __METHOD__ );
835 // Use gtid_binlog_pos for MariaDB and gtid_executed for MySQL
836 foreach ( [ 'gtid_binlog_pos', 'gtid_executed' ] as $name ) {
837 if ( isset( $data[$name] ) && strlen( $data[$name] ) ) {
838 $pos = new MySQLPrimaryPos( $data[$name], $now );
839 break;
840 }
841 }
842 // Filter domains that are inactive or not relevant to the session
843 if ( $pos ) {
844 $pos->setActiveOriginServerId( $this->getServerId() );
845 $pos->setActiveOriginServerUUID( $this->getServerUUID() );
846 if ( isset( $data['gtid_domain_id'] ) ) {
847 $pos->setActiveDomain( $data['gtid_domain_id'] );
848 }
849 }
850 }
851
852 if ( !$pos ) {
853 $data = $this->getServerRoleStatus( 'MASTER', __METHOD__ );
854 if ( $data && strlen( $data['File'] ) ) {
855 $pos = new MySQLPrimaryPos( "{$data['File']}/{$data['Position']}", $now );
856 }
857 }
858
859 return $pos;
860 }
861
862 public function getMasterPos() {
863 wfDeprecated( __METHOD__, '1.37' );
864 return $this->getPrimaryPos();
865 }
866
871 public function getTopologyBasedServerId() {
872 return $this->getServerId();
873 }
874
879 protected function getServerId() {
880 $fname = __METHOD__;
881 return $this->srvCache->getWithSetCallback(
882 $this->srvCache->makeGlobalKey( 'mysql-server-id', $this->getServerName() ),
883 self::SERVER_ID_CACHE_TTL,
884 function () use ( $fname ) {
885 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
886 $res = $this->query( "SELECT @@server_id AS id", $fname, $flags );
887
888 return $this->fetchObject( $res )->id;
889 }
890 );
891 }
892
897 protected function getServerUUID() {
898 $fname = __METHOD__;
899 return $this->srvCache->getWithSetCallback(
900 $this->srvCache->makeGlobalKey( 'mysql-server-uuid', $this->getServerName() ),
901 self::SERVER_ID_CACHE_TTL,
902 function () use ( $fname ) {
903 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
904 $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'server_uuid'", $fname, $flags );
905 $row = $this->fetchObject( $res );
906
907 return $row ? $row->Value : null;
908 }
909 );
910 }
911
916 protected function getServerGTIDs( $fname = __METHOD__ ) {
917 $map = [];
918
919 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
920
921 // Get global-only variables like gtid_executed
922 $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_%'", $fname, $flags );
923 foreach ( $res as $row ) {
924 $map[$row->Variable_name] = $row->Value;
925 }
926 // Get session-specific (e.g. gtid_domain_id since that is were writes will log)
927 $res = $this->query( "SHOW SESSION VARIABLES LIKE 'gtid_%'", $fname, $flags );
928 foreach ( $res as $row ) {
929 $map[$row->Variable_name] = $row->Value;
930 }
931
932 return $map;
933 }
934
940 protected function getServerRoleStatus( $role, $fname = __METHOD__ ) {
941 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
942 $res = $this->query( "SHOW $role STATUS", $fname, $flags );
943
944 return $res->fetchRow() ?: [];
945 }
946
947 public function serverIsReadOnly() {
948 // Avoid SHOW to avoid internal temporary tables
949 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
950 $res = $this->query( "SELECT @@GLOBAL.read_only AS Value", __METHOD__, $flags );
951 $row = $this->fetchObject( $res );
952
953 return $row ? (bool)$row->Value : false;
954 }
955
960 public function useIndexClause( $index ) {
961 return "FORCE INDEX (" . $this->indexName( $index ) . ")";
962 }
963
968 public function ignoreIndexClause( $index ) {
969 return "IGNORE INDEX (" . $this->indexName( $index ) . ")";
970 }
971
975 public function getSoftwareLink() {
976 list( $variant ) = $this->getMySqlServerVariant();
977 if ( $variant === 'MariaDB' ) {
978 return '[{{int:version-db-mariadb-url}} MariaDB]';
979 }
980
981 return '[{{int:version-db-mysql-url}} MySQL]';
982 }
983
987 protected function getMySqlServerVariant() {
988 $version = $this->getServerVersion();
989
990 // MariaDB includes its name in its version string; this is how MariaDB's version of
991 // the mysql command-line client identifies MariaDB servers.
992 // https://dev.mysql.com/doc/refman/8.0/en/information-functions.html#function_version
993 // https://mariadb.com/kb/en/version/
994 $parts = explode( '-', $version, 2 );
995 $number = $parts[0];
996 $suffix = $parts[1] ?? '';
997 if ( strpos( $suffix, 'MariaDB' ) !== false || strpos( $suffix, '-maria-' ) !== false ) {
998 $vendor = 'MariaDB';
999 } else {
1000 $vendor = 'MySQL';
1001 }
1002
1003 return [ $vendor, $number ];
1004 }
1005
1009 public function getServerVersion() {
1011 $fname = __METHOD__;
1012
1013 return $cache->getWithSetCallback(
1014 $cache->makeGlobalKey( 'mysql-server-version', $this->getServerName() ),
1015 $cache::TTL_HOUR,
1016 function () use ( $fname ) {
1017 // Not using mysql_get_server_info() or similar for consistency: in the handshake,
1018 // MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip
1019 // it off (see RPL_VERSION_HACK in include/mysql_com.h).
1020 return $this->selectField( '', 'VERSION()', '', $fname );
1021 }
1022 );
1023 }
1024
1028 public function setSessionOptions( array $options ) {
1029 if ( isset( $options['connTimeout'] ) ) {
1030 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_TRX;
1031 $timeout = (int)$options['connTimeout'];
1032 $this->query( "SET net_read_timeout=$timeout", __METHOD__, $flags );
1033 $this->query( "SET net_write_timeout=$timeout", __METHOD__, $flags );
1034 }
1035 }
1036
1042 public function streamStatementEnd( &$sql, &$newLine ) {
1043 if ( preg_match( '/^DELIMITER\s+(\S+)/i', $newLine, $m ) ) {
1044 $this->delimiter = $m[1];
1045 $newLine = '';
1046 }
1047
1048 return parent::streamStatementEnd( $sql, $newLine );
1049 }
1050
1051 public function doLockIsFree( string $lockName, string $method ) {
1052 $encName = $this->addQuotes( $this->makeLockName( $lockName ) );
1053
1054 $res = $this->query(
1055 "SELECT IS_FREE_LOCK($encName) AS unlocked",
1056 $method,
1057 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
1058 );
1059 $row = $this->fetchObject( $res );
1060
1061 return ( $row->unlocked == 1 );
1062 }
1063
1064 public function doLock( string $lockName, string $method, int $timeout ) {
1065 $encName = $this->addQuotes( $this->makeLockName( $lockName ) );
1066 // Unlike NOW(), SYSDATE() gets the time at invokation rather than query start.
1067 // The precision argument is silently ignored for MySQL < 5.6 and MariaDB < 5.3.
1068 // https://dev.mysql.com/doc/refman/5.6/en/date-and-time-functions.html#function_sysdate
1069 // https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html
1070 $res = $this->query(
1071 "SELECT IF(GET_LOCK($encName,$timeout),UNIX_TIMESTAMP(SYSDATE(6)),NULL) AS acquired",
1072 $method,
1073 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
1074 );
1075 $row = $this->fetchObject( $res );
1076
1077 return ( $row->acquired !== null ) ? (float)$row->acquired : null;
1078 }
1079
1080 public function doUnlock( string $lockName, string $method ) {
1081 $encName = $this->addQuotes( $this->makeLockName( $lockName ) );
1082
1083 $res = $this->query(
1084 "SELECT RELEASE_LOCK($encName) AS released",
1085 $method,
1086 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
1087 );
1088 $row = $this->fetchObject( $res );
1089
1090 return ( $row->released == 1 );
1091 }
1092
1093 private function makeLockName( $lockName ) {
1094 // https://dev.mysql.com/doc/refman/5.7/en/locking-functions.html#function_get-lock
1095 // MySQL 5.7+ enforces a 64 char length limit.
1096 return ( strlen( $lockName ) > 64 ) ? sha1( $lockName ) : $lockName;
1097 }
1098
1099 public function namedLocksEnqueue() {
1100 return true;
1101 }
1102
1104 return false; // tied to TCP connection
1105 }
1106
1107 protected function doLockTables( array $read, array $write, $method ) {
1108 $items = [];
1109 foreach ( $write as $table ) {
1110 $items[] = $this->tableName( $table ) . ' WRITE';
1111 }
1112 foreach ( $read as $table ) {
1113 $items[] = $this->tableName( $table ) . ' READ';
1114 }
1115
1116 $this->query(
1117 "LOCK TABLES " . implode( ',', $items ),
1118 $method,
1119 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_ROWS
1120 );
1121
1122 return true;
1123 }
1124
1125 protected function doUnlockTables( $method ) {
1126 $this->query(
1127 "UNLOCK TABLES",
1128 $method,
1129 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_ROWS
1130 );
1131
1132 return true;
1133 }
1134
1138 public function setBigSelects( $value = true ) {
1139 if ( $value === 'default' ) {
1140 if ( $this->defaultBigSelects === null ) {
1141 # Function hasn't been called before so it must already be set to the default
1142 return;
1143 } else {
1144 $value = $this->defaultBigSelects;
1145 }
1146 } elseif ( $this->defaultBigSelects === null ) {
1147 $this->defaultBigSelects =
1148 (bool)$this->selectField( false, '@@sql_big_selects', '', __METHOD__ );
1149 }
1150
1151 $this->query(
1152 "SET sql_big_selects=" . ( $value ? '1' : '0' ),
1153 __METHOD__,
1154 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_TRX
1155 );
1156 }
1157
1168 public function deleteJoin(
1169 $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__
1170 ) {
1171 if ( !$conds ) {
1172 throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
1173 }
1174
1175 $delTable = $this->tableName( $delTable );
1176 $joinTable = $this->tableName( $joinTable );
1177 $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
1178
1179 if ( $conds != '*' ) {
1180 $sql .= ' AND ' . $this->makeList( $conds, self::LIST_AND );
1181 }
1182
1183 $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
1184 }
1185
1186 protected function doUpsert(
1187 string $table,
1188 array $rows,
1189 array $identityKey,
1190 array $set,
1191 string $fname
1192 ) {
1193 $encTable = $this->tableName( $table );
1194 list( $sqlColumns, $sqlTuples ) = $this->makeInsertLists( $rows );
1195 $sqlColumnAssignments = $this->makeList( $set, self::LIST_SET );
1196
1197 $sql =
1198 "INSERT INTO $encTable ($sqlColumns) VALUES $sqlTuples " .
1199 "ON DUPLICATE KEY UPDATE $sqlColumnAssignments";
1200
1201 $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
1202 }
1203
1204 protected function doReplace( $table, array $identityKey, array $rows, $fname ) {
1205 $encTable = $this->tableName( $table );
1206 list( $sqlColumns, $sqlTuples ) = $this->makeInsertLists( $rows );
1207
1208 $sql = "REPLACE INTO $encTable ($sqlColumns) VALUES $sqlTuples";
1209
1210 $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
1211 }
1212
1218 public function getServerUptime() {
1219 $vars = $this->getMysqlStatus( 'Uptime' );
1220
1221 return (int)$vars['Uptime'];
1222 }
1223
1229 public function wasDeadlock() {
1230 return $this->lastErrno() == 1213;
1231 }
1232
1238 public function wasLockTimeout() {
1239 return $this->lastErrno() == 1205;
1240 }
1241
1247 public function wasReadOnlyError() {
1248 return $this->lastErrno() == 1223 ||
1249 ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false );
1250 }
1251
1252 public function wasConnectionError( $errno ) {
1253 return $errno == 2013 || $errno == 2006;
1254 }
1255
1256 protected function wasKnownStatementRollbackError() {
1257 $errno = $this->lastErrno();
1258
1259 if ( $errno === 1205 ) { // lock wait timeout
1260 // Note that this is uncached to avoid stale values of SET is used
1261 $res = $this->query(
1262 "SELECT @@innodb_rollback_on_timeout AS Value",
1263 __METHOD__,
1264 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
1265 );
1266 $row = $res ? $res->fetchObject() : false;
1267 // https://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
1268 // https://dev.mysql.com/doc/refman/5.5/en/innodb-parameters.html
1269 return ( $row && !$row->Value );
1270 }
1271
1272 // See https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html
1273 return in_array( $errno, [ 1022, 1062, 1216, 1217, 1137, 1146, 1051, 1054 ], true );
1274 }
1275
1284 $oldName, $newName, $temporary = false, $fname = __METHOD__
1285 ) {
1286 $tmp = $temporary ? 'TEMPORARY ' : '';
1287 $newName = $this->addIdentifierQuotes( $newName );
1288 $oldName = $this->addIdentifierQuotes( $oldName );
1289
1290 return $this->query(
1291 "CREATE $tmp TABLE $newName (LIKE $oldName)",
1292 $fname,
1293 self::QUERY_PSEUDO_PERMANENT | self::QUERY_CHANGE_SCHEMA
1294 );
1295 }
1296
1304 public function listTables( $prefix = null, $fname = __METHOD__ ) {
1305 $result = $this->query(
1306 "SHOW TABLES",
1307 $fname,
1308 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
1309 );
1310
1311 $endArray = [];
1312
1313 foreach ( $result as $table ) {
1314 $vars = get_object_vars( $table );
1315 $table = array_pop( $vars );
1316
1317 if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
1318 $endArray[] = $table;
1319 }
1320 }
1321
1322 return $endArray;
1323 }
1324
1331 private function getMysqlStatus( $which = "%" ) {
1332 $res = $this->query(
1333 "SHOW STATUS LIKE '{$which}'",
1334 __METHOD__,
1335 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
1336 );
1337
1338 $status = [];
1339 foreach ( $res as $row ) {
1340 $status[$row->Variable_name] = $row->Value;
1341 }
1342
1343 return $status;
1344 }
1345
1355 public function listViews( $prefix = null, $fname = __METHOD__ ) {
1356 // The name of the column containing the name of the VIEW
1357 $propertyName = 'Tables_in_' . $this->getDBname();
1358
1359 // Query for the VIEWS
1360 $res = $this->query(
1361 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"',
1362 $fname,
1363 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
1364 );
1365
1366 $allViews = [];
1367 foreach ( $res as $row ) {
1368 array_push( $allViews, $row->$propertyName );
1369 }
1370
1371 if ( $prefix === null || $prefix === '' ) {
1372 return $allViews;
1373 }
1374
1375 $filteredViews = [];
1376 foreach ( $allViews as $viewName ) {
1377 // Does the name of this VIEW start with the table-prefix?
1378 if ( strpos( $viewName, $prefix ) === 0 ) {
1379 array_push( $filteredViews, $viewName );
1380 }
1381 }
1382
1383 return $filteredViews;
1384 }
1385
1394 public function isView( $name, $prefix = null ) {
1395 return in_array( $name, $this->listViews( $prefix, __METHOD__ ) );
1396 }
1397
1398 protected function isTransactableQuery( $sql ) {
1399 return parent::isTransactableQuery( $sql ) &&
1400 !preg_match( '/^SELECT\s+(GET|RELEASE|IS_FREE)_LOCK\‍(/', $sql );
1401 }
1402
1403 public function buildStringCast( $field ) {
1404 return "CAST( $field AS BINARY )";
1405 }
1406
1411 public function buildIntegerCast( $field ) {
1412 return 'CAST( ' . $field . ' AS SIGNED )';
1413 }
1414
1418 protected function useGTIDs() {
1419 return $this->useGTIDs;
1420 }
1421}
1422
1426class_alias( DatabaseMysqlBase::class, 'DatabaseMysqlBase' );
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
getWithSetCallback( $key, $exptime, $callback, $flags=0)
Get an item with the given key, regenerating and setting it if not found.
makeGlobalKey( $collection,... $components)
Make a cache key for the default keyspace and given components.
Database error base class @newable.
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__)
getMysqlStatus( $which="%")
Get status information from SHOW STATUS in an associative array.
doLock(string $lockName, string $method, int $timeout)
namedLocksEnqueue()
Check to see if a named lock used by lock() use blocking queues.bool 1.26to 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.
fieldType( $res, $n)
mysql_field_type() wrapper
string $lagDetectionMethod
Method to detect replica DB lag.
getApproximateLagStatus()
Get a replica DB lag estimate for this server at the start of a transaction.
mysqlFieldType( $res, $n)
Get the type of the specified field in a result.
doLockTables(array $read, array $write, $method)
Helper function for lockTables() that handles the actual table locking.
doUpsert(string $table, array $rows, array $identityKey, array $set, string $fname)
serverIsReadOnly()
bool Whether the DB is marked as read-only server-side query} 1.28to override
indexInfo( $table, $index, $fname=__METHOD__)
Get information about an index into an object Returns false if the index does not exist.
mysqlError( $conn=null)
Returns the text of the error message from previous MySQL operation.
addIdentifierQuotes( $s)
MySQL uses backticks for identifier quoting instead of the sql standard "double quotes".
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)
wasQueryTimeout( $error, $errno)
Checks whether the cause of the error is detected to be a timeout.
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
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)
string $sqlMode
sql_mode value to send on connection
getPrimaryPos()
Get the position of the primary DB from SHOW MASTER STATUS.
tableLocksHaveTransactionScope()
Checks if table locks acquired by lockTables() are transaction-bound in their scope.
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...
getServerUptime()
Determines how long the server has been up.
mysqlConnect( $server, $user, $password, $db)
Open a connection to a MySQL server.
doUnlockTables( $method)
Helper function for unlockTables() that handles the actual table unlocking.
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.
buildStringCast( $field)
string 1.28to override
wasDeadlock()
Determines if the last failure was due to a deadlock.
wasConnectionError( $errno)
Do not use this method outside of Database/DBError classes.
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:52
string null $password
Password used to establish the current connection.
Definition Database.php:87
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Wrapper to IDatabase::select() that only fetches one row (via LIMIT)
restoreErrorHandler()
Restore the previous error handler and return the last PHP error for this DB.
Definition Database.php:938
object resource null $conn
Database connection.
Definition Database.php:77
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.string
newExceptionAfterConnectError( $error)
indexName( $index)
Allows for index remapping in queries where this is not consistent across DBMS.
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
getRecordedTransactionLagStatus()
Get the replica DB lag when the current transaction started.
int $flags
Current bit field of class DBO_* constants.
Definition Database.php:106
numRows( $res)
Get the number of rows in a query result.
Definition Database.php:880
query( $sql, $fname=__METHOD__, $flags=self::QUERY_NORMAL)
Run an SQL query and return the result.
installErrorHandler()
Set a custom error handler for logging errors during database connection.
Definition Database.php:927
tableName( $name, $format='quoted')
Format a table name ready for use in constructing an SQL query.This does two important things: it quo...
close( $fname=__METHOD__, $owner=null)
Close the database connection.
Definition Database.php:990
qualifiedTableComponents( $name)
Get the table components needed for a query given the currently selected database.
fetchRow(IResultWrapper $res)
Fetch the next row from the given result object, in associative array form.
Definition Database.php:876
getLogContext(array $extras=[])
Create a log context to pass to PSR-3 logger functions.
Definition Database.php:979
executeQuery( $sql, $fname, $flags)
Execute a query, retrying it if there is a recoverable connection loss.
string null $server
Server that this instance is currently connected to.
Definition Database.php:83
escapeLikeInternal( $s, $escapeChar='`')
getServerName()
Get the readable name for the server.
reportQueryError( $error, $errno, $sql, $fname, $ignore=false)
Report a query error.
normalizeConditions( $conds, $fname)
string null $user
User that this instance is currently connected under the name of.
Definition Database.php:85
makeList(array $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.
BagOStuff $srvCache
APC cache.
Definition Database.php:54
makeInsertLists(array $rows)
Make SQL lists of columns, row tuples for INSERT/VALUES expressions.
getDBname()
Get the current database name; null if there isn't one.
fetchObject(IResultWrapper $res)
Fetch the next row from the given result object, in object form.
Definition Database.php:872
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a single field from a single result row.
DBPrimaryPos class for MySQL/MariaDB.
An object representing a primary or replica DB position in a replicated setup.
lastErrno()
Get the last error number.
$cache
Definition mcc.php:33
foreach( $mmfl['setupFiles'] as $fileName) if($queue) if(empty( $mmfl['quiet'])) $s