MediaWiki fundraising/REL1_35
Database.php
Go to the documentation of this file.
1<?php
26namespace Wikimedia\Rdbms;
27
28use BagOStuff;
29use Exception;
31use InvalidArgumentException;
32use LogicException;
33use Psr\Log\LoggerAwareInterface;
34use Psr\Log\LoggerInterface;
35use Psr\Log\NullLogger;
36use RuntimeException;
37use Throwable;
38use UnexpectedValueException;
39use Wikimedia\AtEase\AtEase;
40use Wikimedia\ScopedCallback;
41use Wikimedia\Timestamp\ConvertibleTimestamp;
42
50abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAwareInterface {
52 protected $srvCache;
54 protected $connLogger;
56 protected $queryLogger;
58 protected $replLogger;
60 protected $errorLogger;
64 protected $profiler;
66 protected $trxProfiler;
67
69 protected $currentDomain;
70
71 // phpcs:ignore MediaWiki.Commenting.PropertyDocumentation.ObjectTypeHintVar
73 protected $conn;
74
77
79 protected $server;
81 protected $user;
83 protected $password;
85 protected $cliMode;
87 protected $agent;
89 protected $topologyRole;
98
100 protected $flags;
102 protected $lbInfo = [];
104 protected $delimiter = ';';
106 protected $tableAliases = [];
108 protected $indexAliases = [];
110 protected $schemaVars;
111
113 private $htmlErrors;
115 private $priorFlags = [];
116
118 protected $sessionNamedLocks = [];
120 protected $sessionTempTables = [];
123
125 private $trxShortId = '';
127 private $trxStatus = self::STATUS_TRX_NONE;
133 private $trxTimestamp = null;
135 private $trxReplicaLag = null;
137 private $trxFname = null;
139 private $trxDoneWrites = false;
141 private $trxAutomatic = false;
143 private $trxAtomicCounter = 0;
145 private $trxAtomicLevels = [];
147 private $trxAutomaticAtomic = false;
149 private $trxWriteCallers = [];
151 private $trxWriteDuration = 0.0;
157 private $trxWriteAdjDuration = 0.0;
161 private $trxIdleCallbacks = [];
168 private $trxEndCallbacks = [];
175
178
180 private $lastPing = 0.0;
182 private $lastQuery = '';
184 private $lastWriteTime = false;
186 private $lastPhpError = false;
189
191 private $ownerId;
192
194 public const ATTR_DB_IS_FILE = 'db-is-file';
196 public const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
198 public const ATTR_SCHEMAS_AS_TABLE_GROUPS = 'supports-schemas';
199
201 public const NEW_UNCONNECTED = 0;
203 public const NEW_CONNECTED = 1;
204
206 public const STATUS_TRX_ERROR = 1;
208 public const STATUS_TRX_OK = 2;
210 public const STATUS_TRX_NONE = 3;
211
213 private static $NOT_APPLICABLE = 'n/a';
215 private static $SAVEPOINT_PREFIX = 'wikimedia_rdbms_atomic';
216
218 private static $TEMP_NORMAL = 1;
220 private static $TEMP_PSEUDO_PERMANENT = 2;
221
223 private static $DEADLOCK_TRIES = 4;
225 private static $DEADLOCK_DELAY_MIN = 500000;
227 private static $DEADLOCK_DELAY_MAX = 1500000;
228
230 private static $PING_TTL = 1.0;
232 private static $PING_QUERY = 'SELECT 1 AS ping';
233
235 private static $TINY_WRITE_SEC = 0.010;
237 private static $SLOW_WRITE_SEC = 0.500;
239 private static $SMALL_WRITE_ROWS = 100;
240
242 protected static $MUTABLE_FLAGS = [
243 'DBO_DEBUG',
244 'DBO_NOBUFFER',
245 'DBO_TRX',
246 'DBO_DDLMODE',
247 ];
249 protected static $DBO_MUTABLE = (
250 self::DBO_DEBUG | self::DBO_NOBUFFER | self::DBO_TRX | self::DBO_DDLMODE
251 );
252
258 public function __construct( array $params ) {
259 $this->connectionParams = [
260 'host' => ( isset( $params['host'] ) && $params['host'] !== '' )
261 ? $params['host']
262 : null,
263 'user' => ( isset( $params['user'] ) && $params['user'] !== '' )
264 ? $params['user']
265 : null,
266 'dbname' => ( isset( $params['dbname'] ) && $params['dbname'] !== '' )
267 ? $params['dbname']
268 : null,
269 'schema' => ( isset( $params['schema'] ) && $params['schema'] !== '' )
270 ? $params['schema']
271 : null,
272 'password' => is_string( $params['password'] ) ? $params['password'] : null,
273 'tablePrefix' => (string)$params['tablePrefix']
274 ];
275
276 $this->lbInfo = $params['lbInfo'] ?? [];
277 $this->lazyMasterHandle = $params['lazyMasterHandle'] ?? null;
278 $this->connectionVariables = $params['variables'] ?? [];
279
280 $this->flags = (int)$params['flags'];
281 $this->cliMode = (bool)$params['cliMode'];
282 $this->agent = (string)$params['agent'];
283 $this->topologyRole = (string)$params['topologyRole'];
284 $this->topologyRootMaster = (string)$params['topologicalMaster'];
285 $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'] ?? 10000;
286
287 $this->srvCache = $params['srvCache'];
288 $this->profiler = is_callable( $params['profiler'] ) ? $params['profiler'] : null;
289 $this->trxProfiler = $params['trxProfiler'];
290 $this->connLogger = $params['connLogger'];
291 $this->queryLogger = $params['queryLogger'];
292 $this->replLogger = $params['replLogger'];
293 $this->errorLogger = $params['errorLogger'];
294 $this->deprecationLogger = $params['deprecationLogger'];
295
296 // Set initial dummy domain until open() sets the final DB/prefix
297 $this->currentDomain = new DatabaseDomain(
298 $params['dbname'] != '' ? $params['dbname'] : null,
299 $params['schema'] != '' ? $params['schema'] : null,
300 $params['tablePrefix']
301 );
302
303 $this->ownerId = $params['ownerId'] ?? null;
304 }
305
314 final public function initConnection() {
315 if ( $this->isOpen() ) {
316 throw new LogicException( __METHOD__ . ': already connected' );
317 }
318 // Establish the connection
319 $this->doInitConnection();
320 }
321
328 protected function doInitConnection() {
329 $this->open(
330 $this->connectionParams['host'],
331 $this->connectionParams['user'],
332 $this->connectionParams['password'],
333 $this->connectionParams['dbname'],
334 $this->connectionParams['schema'],
335 $this->connectionParams['tablePrefix']
336 );
337 }
338
350 abstract protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix );
351
405 final public static function factory( $type, $params = [], $connect = self::NEW_CONNECTED ) {
406 $class = self::getClass( $type, $params['driver'] ?? null );
407
408 if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
409 $params += [
410 // Default configuration
411 'host' => null,
412 'user' => null,
413 'password' => null,
414 'dbname' => null,
415 'schema' => null,
416 'tablePrefix' => '',
417 'flags' => 0,
418 'variables' => [],
419 'lbInfo' => [],
420 'cliMode' => ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ),
421 'agent' => '',
422 'ownerId' => null,
423 'topologyRole' => null,
424 'topologicalMaster' => null,
425 // Objects and callbacks
426 'lazyMasterHandle' => $params['lazyMasterHandle'] ?? null,
427 'srvCache' => $params['srvCache'] ?? new HashBagOStuff(),
428 'profiler' => $params['profiler'] ?? null,
429 'trxProfiler' => $params['trxProfiler'] ?? new TransactionProfiler(),
430 'connLogger' => $params['connLogger'] ?? new NullLogger(),
431 'queryLogger' => $params['queryLogger'] ?? new NullLogger(),
432 'replLogger' => $params['replLogger'] ?? new NullLogger(),
433 'errorLogger' => $params['errorLogger'] ?? function ( Throwable $e ) {
434 trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
435 },
436 'deprecationLogger' => $params['deprecationLogger'] ?? function ( $msg ) {
437 trigger_error( $msg, E_USER_DEPRECATED );
438 }
439 ];
440
442 $conn = new $class( $params );
443 if ( $connect === self::NEW_CONNECTED ) {
444 $conn->initConnection();
445 }
446 } else {
447 $conn = null;
448 }
449
450 return $conn;
451 }
452
460 final public static function attributesFromType( $dbType, $driver = null ) {
461 static $defaults = [
462 self::ATTR_DB_IS_FILE => false,
463 self::ATTR_DB_LEVEL_LOCKING => false,
464 self::ATTR_SCHEMAS_AS_TABLE_GROUPS => false
465 ];
466
467 $class = self::getClass( $dbType, $driver );
468
469 return call_user_func( [ $class, 'getAttributes' ] ) + $defaults;
470 }
471
478 private static function getClass( $dbType, $driver = null ) {
479 // For database types with built-in support, the below maps type to IDatabase
480 // implementations. For types with multiple driver implementations (PHP extensions),
481 // an array can be used, keyed by extension name. In case of an array, the
482 // optional 'driver' parameter can be used to force a specific driver. Otherwise,
483 // we auto-detect the first available driver. For types without built-in support,
484 // an class named "Database<Type>" us used, eg. DatabaseFoo for type 'foo'.
485 static $builtinTypes = [
486 'mysql' => [ 'mysqli' => DatabaseMysqli::class ],
487 'sqlite' => DatabaseSqlite::class,
488 'postgres' => DatabasePostgres::class,
489 ];
490
491 $dbType = strtolower( $dbType );
492 $class = false;
493
494 if ( isset( $builtinTypes[$dbType] ) ) {
495 $possibleDrivers = $builtinTypes[$dbType];
496 if ( is_string( $possibleDrivers ) ) {
497 $class = $possibleDrivers;
498 } elseif ( (string)$driver !== '' ) {
499 if ( !isset( $possibleDrivers[$driver] ) ) {
500 throw new InvalidArgumentException( __METHOD__ .
501 " type '$dbType' does not support driver '{$driver}'" );
502 }
503
504 $class = $possibleDrivers[$driver];
505 } else {
506 foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
507 if ( extension_loaded( $posDriver ) ) {
508 $class = $possibleClass;
509 break;
510 }
511 }
512 }
513 } else {
514 $class = 'Database' . ucfirst( $dbType );
515 }
516
517 if ( $class === false ) {
518 throw new InvalidArgumentException( __METHOD__ .
519 " no viable database extension found for type '$dbType'" );
520 }
521
522 return $class;
523 }
524
530 protected static function getAttributes() {
531 return [];
532 }
533
541 public function setLogger( LoggerInterface $logger ) {
542 $this->queryLogger = $logger;
543 }
544
545 public function getServerInfo() {
546 return $this->getServerVersion();
547 }
548
549 public function getTopologyRole() {
550 return $this->topologyRole;
551 }
552
553 public function getTopologyRootMaster() {
555 }
556
557 final public function trxLevel() {
558 return ( $this->trxShortId != '' ) ? 1 : 0;
559 }
560
561 public function trxTimestamp() {
562 return $this->trxLevel() ? $this->trxTimestamp : null;
563 }
564
569 public function trxStatus() {
570 return $this->trxStatus;
571 }
572
573 public function tablePrefix( $prefix = null ) {
574 $old = $this->currentDomain->getTablePrefix();
575
576 if ( $prefix !== null ) {
577 $this->currentDomain = new DatabaseDomain(
578 $this->currentDomain->getDatabase(),
579 $this->currentDomain->getSchema(),
580 $prefix
581 );
582 }
583
584 return $old;
585 }
586
587 public function dbSchema( $schema = null ) {
588 $old = $this->currentDomain->getSchema();
589
590 if ( $schema !== null ) {
591 if ( $schema !== '' && $this->getDBname() === null ) {
592 throw new DBUnexpectedError(
593 $this,
594 "Cannot set schema to '$schema'; no database set"
595 );
596 }
597
598 $this->currentDomain = new DatabaseDomain(
599 $this->currentDomain->getDatabase(),
600 // DatabaseDomain uses null for unspecified schemas
601 ( $schema !== '' ) ? $schema : null,
602 $this->currentDomain->getTablePrefix()
603 );
604 }
605
606 return (string)$old;
607 }
608
613 protected function relationSchemaQualifier() {
614 return $this->dbSchema();
615 }
616
617 public function getLBInfo( $name = null ) {
618 if ( $name === null ) {
619 return $this->lbInfo;
620 }
621
622 if ( array_key_exists( $name, $this->lbInfo ) ) {
623 return $this->lbInfo[$name];
624 }
625
626 return null;
627 }
628
629 public function setLBInfo( $nameOrArray, $value = null ) {
630 if ( is_array( $nameOrArray ) ) {
631 $this->lbInfo = $nameOrArray;
632 } elseif ( is_string( $nameOrArray ) ) {
633 if ( $value !== null ) {
634 $this->lbInfo[$nameOrArray] = $value;
635 } else {
636 unset( $this->lbInfo[$nameOrArray] );
637 }
638 } else {
639 throw new InvalidArgumentException( "Got non-string key" );
640 }
641 }
642
649 protected function getLazyMasterHandle() {
651 }
652
657 public function implicitOrderby() {
658 return true;
659 }
660
661 public function lastQuery() {
662 return $this->lastQuery;
663 }
664
665 public function lastDoneWrites() {
666 return $this->lastWriteTime ?: false;
667 }
668
669 public function writesPending() {
670 return $this->trxLevel() && $this->trxDoneWrites;
671 }
672
673 public function writesOrCallbacksPending() {
674 return $this->trxLevel() && (
675 $this->trxDoneWrites ||
676 $this->trxIdleCallbacks ||
677 $this->trxPreCommitCallbacks ||
678 $this->trxEndCallbacks ||
680 );
681 }
682
683 public function preCommitCallbacksPending() {
684 return $this->trxLevel() && $this->trxPreCommitCallbacks;
685 }
686
690 final protected function getTransactionRoundId() {
691 // If transaction round participation is enabled, see if one is active
692 if ( $this->getFlag( self::DBO_TRX ) ) {
693 $id = $this->getLBInfo( self::LB_TRX_ROUND_ID );
694
695 return is_string( $id ) ? $id : null;
696 }
697
698 return null;
699 }
700
701 public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
702 if ( !$this->trxLevel() ) {
703 return false;
704 } elseif ( !$this->trxDoneWrites ) {
705 return 0.0;
706 }
707
708 switch ( $type ) {
709 case self::ESTIMATE_DB_APPLY:
710 return $this->pingAndCalculateLastTrxApplyTime();
711 default: // everything
713 }
714 }
715
720 $this->ping( $rtt );
721
722 $rttAdjTotal = $this->trxWriteAdjQueryCount * $rtt;
723 $applyTime = max( $this->trxWriteAdjDuration - $rttAdjTotal, 0 );
724 // For omitted queries, make them count as something at least
725 $omitted = $this->trxWriteQueryCount - $this->trxWriteAdjQueryCount;
726 $applyTime += self::$TINY_WRITE_SEC * $omitted;
727
728 return $applyTime;
729 }
730
731 public function pendingWriteCallers() {
732 return $this->trxLevel() ? $this->trxWriteCallers : [];
733 }
734
735 public function pendingWriteRowsAffected() {
737 }
738
748 $fnames = $this->pendingWriteCallers();
749 foreach ( [
750 $this->trxIdleCallbacks,
751 $this->trxPreCommitCallbacks,
752 $this->trxEndCallbacks,
753 $this->trxSectionCancelCallbacks
754 ] as $callbacks ) {
755 foreach ( $callbacks as $callback ) {
756 $fnames[] = $callback[1];
757 }
758 }
759
760 return $fnames;
761 }
762
766 private function flatAtomicSectionList() {
767 return array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) {
768 return $accum === null ? $v[0] : "$accum, " . $v[0];
769 } );
770 }
771
772 public function isOpen() {
773 return (bool)$this->conn;
774 }
775
776 public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
777 if ( $flag & ~static::$DBO_MUTABLE ) {
778 throw new DBUnexpectedError(
779 $this,
780 "Got $flag (allowed: " . implode( ', ', static::$MUTABLE_FLAGS ) . ')'
781 );
782 }
783
784 if ( $remember === self::REMEMBER_PRIOR ) {
785 array_push( $this->priorFlags, $this->flags );
786 }
787
788 $this->flags |= $flag;
789 }
790
791 public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
792 if ( $flag & ~static::$DBO_MUTABLE ) {
793 throw new DBUnexpectedError(
794 $this,
795 "Got $flag (allowed: " . implode( ', ', static::$MUTABLE_FLAGS ) . ')'
796 );
797 }
798
799 if ( $remember === self::REMEMBER_PRIOR ) {
800 array_push( $this->priorFlags, $this->flags );
801 }
802
803 $this->flags &= ~$flag;
804 }
805
806 public function restoreFlags( $state = self::RESTORE_PRIOR ) {
807 if ( !$this->priorFlags ) {
808 return;
809 }
810
811 if ( $state === self::RESTORE_INITIAL ) {
812 $this->flags = reset( $this->priorFlags );
813 $this->priorFlags = [];
814 } else {
815 $this->flags = array_pop( $this->priorFlags );
816 }
817 }
818
819 public function getFlag( $flag ) {
820 return ( ( $this->flags & $flag ) === $flag );
821 }
822
823 public function getDomainID() {
824 return $this->currentDomain->getId();
825 }
826
836 abstract public function indexInfo( $table, $index, $fname = __METHOD__ );
837
845 abstract public function strencode( $s );
846
850 protected function installErrorHandler() {
851 $this->lastPhpError = false;
852 $this->htmlErrors = ini_set( 'html_errors', '0' );
853 set_error_handler( [ $this, 'connectionErrorLogger' ] );
854 }
855
861 protected function restoreErrorHandler() {
862 restore_error_handler();
863 if ( $this->htmlErrors !== false ) {
864 ini_set( 'html_errors', $this->htmlErrors );
865 }
866
867 return $this->getLastPHPError();
868 }
869
873 protected function getLastPHPError() {
874 if ( $this->lastPhpError ) {
875 $error = preg_replace( '!\[<a.*</a>\]!', '', $this->lastPhpError );
876 $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
877
878 return $error;
879 }
880
881 return false;
882 }
883
891 public function connectionErrorLogger( $errno, $errstr ) {
892 $this->lastPhpError = $errstr;
893 }
894
901 protected function getLogContext( array $extras = [] ) {
902 return array_merge(
903 [
904 'db_server' => $this->server,
905 'db_name' => $this->getDBname(),
906 'db_user' => $this->user,
907 ],
908 $extras
909 );
910 }
911
912 final public function close( $fname = __METHOD__, $owner = null ) {
913 $error = null; // error to throw after disconnecting
914
915 $wasOpen = (bool)$this->conn;
916 // This should mostly do nothing if the connection is already closed
917 if ( $this->conn ) {
918 // Roll back any dangling transaction first
919 if ( $this->trxLevel() ) {
920 if ( $this->trxAtomicLevels ) {
921 // Cannot let incomplete atomic sections be committed
922 $levels = $this->flatAtomicSectionList();
923 $error = "$fname: atomic sections $levels are still open";
924 } elseif ( $this->trxAutomatic ) {
925 // Only the connection manager can commit non-empty DBO_TRX transactions
926 // (empty ones we can silently roll back)
927 if ( $this->writesOrCallbacksPending() ) {
928 $error = "$fname: " .
929 "expected mass rollback of all peer transactions (DBO_TRX set)";
930 }
931 } else {
932 // Manual transactions should have been committed or rolled
933 // back, even if empty.
934 $error = "$fname: transaction is still open (from {$this->trxFname})";
935 }
936
937 if ( $this->trxEndCallbacksSuppressed && $error === null ) {
938 $error = "$fname: callbacks are suppressed; cannot properly commit";
939 }
940
941 // Rollback the changes and run any callbacks as needed
942 $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
943 }
944
945 // Close the actual connection in the binding handle
946 $closed = $this->closeConnection();
947 } else {
948 $closed = true; // already closed; nothing to do
949 }
950
951 $this->conn = null;
952
953 // Log or throw any unexpected errors after having disconnected
954 if ( $error !== null ) {
955 // T217819, T231443: if this is probably just LoadBalancer trying to recover from
956 // errors and shutdown, then log any problems and move on since the request has to
957 // end one way or another. Throwing errors is not very useful at some point.
958 if ( $this->ownerId !== null && $owner === $this->ownerId ) {
959 $this->queryLogger->error( $error );
960 } else {
961 throw new DBUnexpectedError( $this, $error );
962 }
963 }
964
965 // Note that various subclasses call close() at the start of open(), which itself is
966 // called by replaceLostConnection(). In that case, just because onTransactionResolution()
967 // callbacks are pending does not mean that an exception should be thrown. Rather, they
968 // will be executed after the reconnection step.
969 if ( $wasOpen ) {
970 // Sanity check that no callbacks are dangling
971 $fnames = $this->pendingWriteAndCallbackCallers();
972 if ( $fnames ) {
973 throw new RuntimeException(
974 "Transaction callbacks are still pending: " . implode( ', ', $fnames )
975 );
976 }
977 }
978
979 return $closed;
980 }
981
990 final protected function assertHasConnectionHandle() {
991 if ( !$this->isOpen() ) {
992 throw new DBUnexpectedError( $this, "DB connection was already closed" );
993 }
994 }
995
1001 protected function assertIsWritableMaster() {
1002 $info = $this->getReadOnlyReason();
1003 if ( $info ) {
1004 list( $reason, $source ) = $info;
1005 if ( $source === 'role' ) {
1006 throw new DBReadOnlyRoleError( $this, "Database is read-only: $reason" );
1007 } else {
1008 throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
1009 }
1010 }
1011 }
1012
1018 abstract protected function closeConnection();
1019
1049 abstract protected function doQuery( $sql );
1050
1068 protected function isWriteQuery( $sql, $flags ) {
1069 if (
1070 $this->fieldHasBit( $flags, self::QUERY_CHANGE_ROWS ) ||
1071 $this->fieldHasBit( $flags, self::QUERY_CHANGE_SCHEMA )
1072 ) {
1073 return true;
1074 } elseif ( $this->fieldHasBit( $flags, self::QUERY_CHANGE_NONE ) ) {
1075 return false;
1076 }
1077 // BEGIN and COMMIT queries are considered read queries here.
1078 // Database backends and drivers (MySQL, MariaDB, php-mysqli) generally
1079 // treat these as write queries, in that their results have "affected rows"
1080 // as meta data as from writes, instead of "num rows" as from reads.
1081 // But, we treat them as read queries because when reading data (from
1082 // either replica or master) we use transactions to enable repeatable-read
1083 // snapshots, which ensures we get consistent results from the same snapshot
1084 // for all queries within a request. Use cases:
1085 // - Treating these as writes would trigger ChronologyProtector (see method doc).
1086 // - We use this method to reject writes to replicas, but we need to allow
1087 // use of transactions on replicas for read snapshots. This is fine given
1088 // that transactions by themselves don't make changes, only actual writes
1089 // within the transaction matter, which we still detect.
1090 return !preg_match(
1091 '/^\s*(?:SELECT|BEGIN|ROLLBACK|COMMIT|SAVEPOINT|RELEASE|SET|SHOW|EXPLAIN|USE|\‍(SELECT)\b/i',
1092 $sql
1093 );
1094 }
1095
1100 protected function getQueryVerb( $sql ) {
1101 return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
1102 }
1103
1118 protected function isTransactableQuery( $sql ) {
1119 return !in_array(
1120 $this->getQueryVerb( $sql ),
1121 [ 'BEGIN', 'ROLLBACK', 'COMMIT', 'SET', 'SHOW', 'CREATE', 'ALTER', 'USE', 'SHOW' ],
1122 true
1123 );
1124 }
1125
1134 protected function getTempTableWrites( $sql, $pseudoPermanent ) {
1135 // Regexes for basic queries that can create/change/drop temporary tables.
1136 // For simplicity, this only looks for tables with sane, alphanumeric, names;
1137 // temporary tables only need simple programming names anyway.
1138 static $regexes = null;
1139 if ( $regexes === null ) {
1140 // Regex with a group for quoted table 0 and a group for quoted tables 1..N
1141 $qts = '((?:\w+|`\w+`|\'\w+\'|"\w+")(?:\s*,\s*(?:\w+|`\w+`|\'\w+\'|"\w+"))*)';
1142 // Regex to get query verb, table 0, and tables 1..N
1143 $regexes = [
1144 // DML write queries
1145 "/^(INSERT|REPLACE)\s+(?:\w+\s+)*?INTO\s+$qts/i",
1146 "/^(UPDATE)(?:\s+OR\s+\w+|\s+IGNORE|\s+ONLY)?\s+$qts/i",
1147 "/^(DELETE)\s+(?:\w+\s+)*?FROM(?:\s+ONLY)?\s+$qts/i",
1148 // DDL write queries
1149 "/^(CREATE)\s+TEMPORARY\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+$qts/i",
1150 "/^(DROP)\s+(?:TEMPORARY\s+)?TABLE(?:\s+IF\s+EXISTS)?\s+$qts/i",
1151 "/^(TRUNCATE)\s+(?:TEMPORARY\s+)?TABLE\s+$qts/i",
1152 "/^(ALTER)\s+TABLE\s+$qts/i"
1153 ];
1154 }
1155
1156 $queryVerb = null;
1157 $queryTables = [];
1158 foreach ( $regexes as $regex ) {
1159 if ( preg_match( $regex, $sql, $m, PREG_UNMATCHED_AS_NULL ) ) {
1160 $queryVerb = $m[1];
1161 $allTables = preg_split( '/\s*,\s*/', $m[2] );
1162 foreach ( $allTables as $quotedTable ) {
1163 $queryTables[] = trim( $quotedTable, "\"'`" );
1164 }
1165 break;
1166 }
1167 }
1168
1169 $tempTableChanges = [];
1170 foreach ( $queryTables as $table ) {
1171 if ( $queryVerb === 'CREATE' ) {
1172 // Record the type of temporary table being created
1173 $tableType = $pseudoPermanent ? self::$TEMP_PSEUDO_PERMANENT : self::$TEMP_NORMAL;
1174 } else {
1175 $tableType = $this->sessionTempTables[$table] ?? null;
1176 }
1177
1178 if ( $tableType !== null ) {
1179 $tempTableChanges[] = [ $tableType, $queryVerb, $table ];
1180 }
1181 }
1182
1183 return $tempTableChanges;
1184 }
1185
1190 protected function registerTempWrites( $ret, array $changes ) {
1191 if ( $ret === false ) {
1192 return;
1193 }
1194
1195 foreach ( $changes as list( $tmpTableType, $verb, $table ) ) {
1196 switch ( $verb ) {
1197 case 'CREATE':
1198 $this->sessionTempTables[$table] = $tmpTableType;
1199 break;
1200 case 'DROP':
1201 unset( $this->sessionTempTables[$table] );
1202 unset( $this->sessionDirtyTempTables[$table] );
1203 break;
1204 case 'TRUNCATE':
1205 unset( $this->sessionDirtyTempTables[$table] );
1206 break;
1207 default:
1208 $this->sessionDirtyTempTables[$table] = 1;
1209 break;
1210 }
1211 }
1212 }
1213
1221 protected function isPristineTemporaryTable( $table ) {
1222 $rawTable = $this->tableName( $table, 'raw' );
1223
1224 return (
1225 isset( $this->sessionTempTables[$rawTable] ) &&
1226 !isset( $this->sessionDirtyTempTables[$rawTable] )
1227 );
1228 }
1229
1230 public function query( $sql, $fname = __METHOD__, $flags = self::QUERY_NORMAL ) {
1231 $flags = (int)$flags; // b/c; this field used to be a bool
1232 // Sanity check that the SQL query is appropriate in the current context and is
1233 // allowed for an outside caller (e.g. does not break transaction/session tracking).
1234 $this->assertQueryIsCurrentlyAllowed( $sql, $fname );
1235
1236 // Send the query to the server and fetch any corresponding errors
1237 list( $ret, $err, $errno, $unignorable ) = $this->executeQuery( $sql, $fname, $flags );
1238 if ( $ret === false ) {
1239 $ignoreErrors = $this->fieldHasBit( $flags, self::QUERY_SILENCE_ERRORS );
1240 // Throw an error unless both the ignore flag was set and a rollback is not needed
1241 $this->reportQueryError( $err, $errno, $sql, $fname, $ignoreErrors && !$unignorable );
1242 }
1243
1244 return $this->resultObject( $ret );
1245 }
1246
1267 final protected function executeQuery( $sql, $fname, $flags ) {
1269
1270 $priorTransaction = $this->trxLevel();
1271
1272 if ( $this->isWriteQuery( $sql, $flags ) ) {
1273 // Do not treat temporary table writes as "meaningful writes" since they are only
1274 // visible to one session and are not permanent. Profile them as reads. Integration
1275 // tests can override this behavior via $flags.
1276 $pseudoPermanent = $this->fieldHasBit( $flags, self::QUERY_PSEUDO_PERMANENT );
1277 $tempTableChanges = $this->getTempTableWrites( $sql, $pseudoPermanent );
1278 $isPermWrite = !$tempTableChanges;
1279 foreach ( $tempTableChanges as list( $tmpType ) ) {
1280 $isPermWrite = $isPermWrite || ( $tmpType !== self::$TEMP_NORMAL );
1281 }
1282
1283 // Permit temporary table writes on replica DB connections
1284 // but require a writable master connection for any persistent writes.
1285 if ( $isPermWrite ) {
1286 $this->assertIsWritableMaster();
1287
1288 // DBConnRef uses QUERY_REPLICA_ROLE to enforce the replica role for raw SQL queries
1289 if ( $this->fieldHasBit( $flags, self::QUERY_REPLICA_ROLE ) ) {
1290 throw new DBReadOnlyRoleError( $this, "Cannot write; target role is DB_REPLICA" );
1291 }
1292 }
1293 } else {
1294 // No permanent writes in this query
1295 $isPermWrite = false;
1296 // No temporary tables written to either
1297 $tempTableChanges = [];
1298 }
1299
1300 // Add trace comment to the begin of the sql string, right after the operator.
1301 // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598).
1302 $encAgent = str_replace( '/', '-', $this->agent );
1303 $commentedSql = preg_replace( '/\s|$/', " /* $fname $encAgent */ ", $sql, 1 );
1304
1305 // Send the query to the server and fetch any corresponding errors.
1306 // This also doubles as a "ping" to see if the connection was dropped.
1307 list( $ret, $err, $errno, $recoverableSR, $recoverableCL, $reconnected ) =
1308 $this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags );
1309
1310 // Check if the query failed due to a recoverable connection loss
1311 $allowRetry = !$this->fieldHasBit( $flags, self::QUERY_NO_RETRY );
1312 if ( $ret === false && $recoverableCL && $reconnected && $allowRetry ) {
1313 // Silently resend the query to the server since it is safe and possible
1314 list( $ret, $err, $errno, $recoverableSR, $recoverableCL ) =
1315 $this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags );
1316 }
1317
1318 // Register creation and dropping of temporary tables
1319 $this->registerTempWrites( $ret, $tempTableChanges );
1320
1321 $corruptedTrx = false;
1322
1323 if ( $ret === false ) {
1324 if ( $priorTransaction ) {
1325 if ( $recoverableSR ) {
1326 # We're ignoring an error that caused just the current query to be aborted.
1327 # But log the cause so we can log a deprecation notice if a caller actually
1328 # does ignore it.
1329 $this->trxStatusIgnoredCause = [ $err, $errno, $fname ];
1330 } elseif ( !$recoverableCL ) {
1331 # Either the query was aborted or all queries after BEGIN where aborted.
1332 # In the first case, the only options going forward are (a) ROLLBACK, or
1333 # (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
1334 # option is ROLLBACK, since the snapshots would have been released.
1335 $corruptedTrx = true; // cannot recover
1336 $this->trxStatus = self::STATUS_TRX_ERROR;
1337 $this->trxStatusCause = $this->getQueryException( $err, $errno, $sql, $fname );
1338 $this->trxStatusIgnoredCause = null;
1339 }
1340 }
1341 }
1342
1343 return [ $ret, $err, $errno, $corruptedTrx ];
1344 }
1345
1364 private function executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags ) {
1365 $priorWritesPending = $this->writesOrCallbacksPending();
1366
1367 if ( ( $flags & self::QUERY_IGNORE_DBO_TRX ) == 0 ) {
1368 $this->beginIfImplied( $sql, $fname );
1369 }
1370
1371 // Keep track of whether the transaction has write queries pending
1372 if ( $isPermWrite ) {
1373 $this->lastWriteTime = microtime( true );
1374 if ( $this->trxLevel() && !$this->trxDoneWrites ) {
1375 $this->trxDoneWrites = true;
1376 $this->trxProfiler->transactionWritingIn(
1377 $this->server, $this->getDomainID(), $this->trxShortId );
1378 }
1379 }
1380
1381 $prefix = $this->topologyRole ? 'query-m: ' : 'query: ';
1382 $generalizedSql = new GeneralizedSql( $sql, $this->trxShortId, $prefix );
1383
1384 $startTime = microtime( true );
1385 $ps = $this->profiler
1386 ? ( $this->profiler )( $generalizedSql->stringify() )
1387 : null;
1388 $this->affectedRowCount = null;
1389 $this->lastQuery = $sql;
1390 $ret = $this->doQuery( $commentedSql );
1391 $lastError = $this->lastError();
1392 $lastErrno = $this->lastErrno();
1393
1394 $this->affectedRowCount = $this->affectedRows();
1395 unset( $ps ); // profile out (if set)
1396 $queryRuntime = max( microtime( true ) - $startTime, 0.0 );
1397
1398 $recoverableSR = false; // recoverable statement rollback?
1399 $recoverableCL = false; // recoverable connection loss?
1400 $reconnected = false; // reconnection both attempted and succeeded?
1401
1402 if ( $ret !== false ) {
1403 $this->lastPing = $startTime;
1404 if ( $isPermWrite && $this->trxLevel() ) {
1405 $this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() );
1406 $this->trxWriteCallers[] = $fname;
1407 }
1408 } elseif ( $this->wasConnectionError( $lastErrno ) ) {
1409 # Check if no meaningful session state was lost
1410 $recoverableCL = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
1411 # Update session state tracking and try to restore the connection
1412 $reconnected = $this->replaceLostConnection( __METHOD__ );
1413 } else {
1414 # Check if only the last query was rolled back
1415 $recoverableSR = $this->wasKnownStatementRollbackError();
1416 }
1417
1418 if ( $sql === self::$PING_QUERY ) {
1419 $this->lastRoundTripEstimate = $queryRuntime;
1420 }
1421
1422 $this->trxProfiler->recordQueryCompletion(
1423 $generalizedSql,
1424 $startTime,
1425 $isPermWrite,
1426 $isPermWrite ? $this->affectedRows() : $this->numRows( $ret )
1427 );
1428
1429 // Avoid the overhead of logging calls unless debug mode is enabled
1430 if ( $this->getFlag( self::DBO_DEBUG ) ) {
1431 $this->queryLogger->debug(
1432 "{method} [{runtime}s] {db_host}: {sql}",
1433 [
1434 'method' => $fname,
1435 'db_host' => $this->getServer(),
1436 'sql' => $sql,
1437 'domain' => $this->getDomainID(),
1438 'runtime' => round( $queryRuntime, 3 )
1439 ]
1440 );
1441 }
1442
1443 return [ $ret, $lastError, $lastErrno, $recoverableSR, $recoverableCL, $reconnected ];
1444 }
1445
1452 private function beginIfImplied( $sql, $fname ) {
1453 if (
1454 !$this->trxLevel() &&
1455 $this->getFlag( self::DBO_TRX ) &&
1456 $this->isTransactableQuery( $sql )
1457 ) {
1458 $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
1459 $this->trxAutomatic = true;
1460 }
1461 }
1462
1475 private function updateTrxWriteQueryTime( $sql, $runtime, $affected ) {
1476 // Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
1477 $indicativeOfReplicaRuntime = true;
1478 if ( $runtime > self::$SLOW_WRITE_SEC ) {
1479 $verb = $this->getQueryVerb( $sql );
1480 // insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
1481 if ( $verb === 'INSERT' ) {
1482 $indicativeOfReplicaRuntime = $this->affectedRows() > self::$SMALL_WRITE_ROWS;
1483 } elseif ( $verb === 'REPLACE' ) {
1484 $indicativeOfReplicaRuntime = $this->affectedRows() > self::$SMALL_WRITE_ROWS / 2;
1485 }
1486 }
1487
1488 $this->trxWriteDuration += $runtime;
1489 $this->trxWriteQueryCount += 1;
1490 $this->trxWriteAffectedRows += $affected;
1491 if ( $indicativeOfReplicaRuntime ) {
1492 $this->trxWriteAdjDuration += $runtime;
1493 $this->trxWriteAdjQueryCount += 1;
1494 }
1495 }
1496
1504 private function assertQueryIsCurrentlyAllowed( $sql, $fname ) {
1505 $verb = $this->getQueryVerb( $sql );
1506 if ( $verb === 'USE' ) {
1507 throw new DBUnexpectedError( $this, "Got USE query; use selectDomain() instead" );
1508 }
1509
1510 if ( $verb === 'ROLLBACK' ) { // transaction/savepoint
1511 return;
1512 }
1513
1514 if ( $this->trxStatus < self::STATUS_TRX_OK ) {
1515 throw new DBTransactionStateError(
1516 $this,
1517 "Cannot execute query from $fname while transaction status is ERROR",
1518 [],
1519 $this->trxStatusCause
1520 );
1521 } elseif ( $this->trxStatus === self::STATUS_TRX_OK && $this->trxStatusIgnoredCause ) {
1522 list( $iLastError, $iLastErrno, $iFname ) = $this->trxStatusIgnoredCause;
1523 call_user_func( $this->deprecationLogger,
1524 "Caller from $fname ignored an error originally raised from $iFname: " .
1525 "[$iLastErrno] $iLastError"
1526 );
1527 $this->trxStatusIgnoredCause = null;
1528 }
1529 }
1530
1531 public function assertNoOpenTransactions() {
1532 if ( $this->explicitTrxActive() ) {
1533 throw new DBTransactionError(
1534 $this,
1535 "Explicit transaction still active. A caller may have caught an error. "
1536 . "Open transactions: " . $this->flatAtomicSectionList()
1537 );
1538 }
1539 }
1540
1550 private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
1551 # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
1552 # Dropped connections also mean that named locks are automatically released.
1553 # Only allow error suppression in autocommit mode or when the lost transaction
1554 # didn't matter anyway (aside from DBO_TRX snapshot loss).
1555 if ( $this->sessionNamedLocks ) {
1556 return false; // possible critical section violation
1557 } elseif ( $this->sessionTempTables ) {
1558 return false; // tables might be queried latter
1559 } elseif ( $sql === 'COMMIT' ) {
1560 return !$priorWritesPending; // nothing written anyway? (T127428)
1561 } elseif ( $sql === 'ROLLBACK' ) {
1562 return true; // transaction lost...which is also what was requested :)
1563 } elseif ( $this->explicitTrxActive() ) {
1564 return false; // don't drop atomicity and explicit snapshots
1565 } elseif ( $priorWritesPending ) {
1566 return false; // prior writes lost from implicit transaction
1567 }
1568
1569 return true;
1570 }
1571
1575 private function handleSessionLossPreconnect() {
1576 // Clean up tracking of session-level things...
1577 // https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html
1578 // https://www.postgresql.org/docs/9.2/static/sql-createtable.html (ignoring ON COMMIT)
1579 $this->sessionTempTables = [];
1580 $this->sessionDirtyTempTables = [];
1581 // https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
1582 // https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
1583 $this->sessionNamedLocks = [];
1584 // Session loss implies transaction loss
1585 $oldTrxShortId = $this->consumeTrxShortId();
1586 $this->trxAtomicCounter = 0;
1587 $this->trxIdleCallbacks = []; // T67263; transaction already lost
1588 $this->trxPreCommitCallbacks = []; // T67263; transaction already lost
1589 // Clear additional subclass fields
1591 // @note: leave trxRecurringCallbacks in place
1592 if ( $this->trxDoneWrites ) {
1593 $this->trxProfiler->transactionWritingOut(
1594 $this->server,
1595 $this->getDomainID(),
1596 $oldTrxShortId,
1597 $this->pendingWriteQueryDuration( self::ESTIMATE_TOTAL ),
1598 $this->trxWriteAffectedRows
1599 );
1600 }
1601 }
1602
1607 protected function doHandleSessionLossPreconnect() {
1608 // no-op
1609 }
1610
1614 private function handleSessionLossPostconnect() {
1615 try {
1616 // Handle callbacks in trxEndCallbacks, e.g. onTransactionResolution().
1617 // If callback suppression is set then the array will remain unhandled.
1618 $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
1619 } catch ( Throwable $ex ) {
1620 // Already logged; move on...
1621 }
1622 try {
1623 // Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener()
1624 $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
1625 } catch ( Throwable $ex ) {
1626 // Already logged; move on...
1627 }
1628 }
1629
1635 private function consumeTrxShortId() {
1636 $old = $this->trxShortId;
1637 $this->trxShortId = '';
1638
1639 return $old;
1640 }
1641
1653 protected function wasQueryTimeout( $error, $errno ) {
1654 return false;
1655 }
1656
1668 public function reportQueryError( $error, $errno, $sql, $fname, $ignore = false ) {
1669 if ( $ignore ) {
1670 $this->queryLogger->debug( "SQL ERROR (ignored): $error" );
1671 } else {
1672 throw $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
1673 }
1674 }
1675
1683 private function getQueryExceptionAndLog( $error, $errno, $sql, $fname ) {
1684 // Information that instances of the same problem have in common should
1685 // not be normalized (T255202).
1686 $this->queryLogger->error(
1687 "Error $errno from $fname, {error} {sql1line} {db_server}",
1688 $this->getLogContext( [
1689 'method' => __METHOD__,
1690 'errno' => $errno,
1691 'error' => $error,
1692 'sql1line' => mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 ),
1693 'fname' => $fname,
1694 'exception' => new RuntimeException()
1695 ] )
1696 );
1697 return $this->getQueryException( $error, $errno, $sql, $fname );
1698 }
1699
1707 private function getQueryException( $error, $errno, $sql, $fname ) {
1708 if ( $this->wasQueryTimeout( $error, $errno ) ) {
1709 return new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
1710 } elseif ( $this->wasConnectionError( $errno ) ) {
1711 return new DBQueryDisconnectedError( $this, $error, $errno, $sql, $fname );
1712 } else {
1713 return new DBQueryError( $this, $error, $errno, $sql, $fname );
1714 }
1715 }
1716
1721 final protected function newExceptionAfterConnectError( $error ) {
1722 // Connection was not fully initialized and is not safe for use
1723 $this->conn = null;
1724
1725 $this->connLogger->error(
1726 "Error connecting to {db_server} as user {db_user}: {error}",
1727 $this->getLogContext( [
1728 'error' => $error,
1729 'exception' => new RuntimeException()
1730 ] )
1731 );
1732
1733 return new DBConnectionError( $this, $error );
1734 }
1735
1740 public function freeResult( $res ) {
1741 }
1742
1746 public function newSelectQueryBuilder() {
1747 return new SelectQueryBuilder( $this );
1748 }
1749
1750 public function selectField(
1751 $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1752 ) {
1753 if ( $var === '*' ) { // sanity
1754 throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1755 }
1756
1757 $options = $this->normalizeOptions( $options );
1758 $options['LIMIT'] = 1;
1759
1760 $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1761 if ( $res === false ) {
1762 throw new DBUnexpectedError( $this, "Got false from select()" );
1763 }
1764
1765 $row = $this->fetchRow( $res );
1766 if ( $row === false ) {
1767 return false;
1768 }
1769
1770 return reset( $row );
1771 }
1772
1773 public function selectFieldValues(
1774 $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1775 ) {
1776 if ( $var === '*' ) { // sanity
1777 throw new DBUnexpectedError( $this, "Cannot use a * field" );
1778 } elseif ( !is_string( $var ) ) { // sanity
1779 throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1780 }
1781
1782 $options = $this->normalizeOptions( $options );
1783 $res = $this->select( $table, [ 'value' => $var ], $cond, $fname, $options, $join_conds );
1784 if ( $res === false ) {
1785 throw new DBUnexpectedError( $this, "Got false from select()" );
1786 }
1787
1788 $values = [];
1789 foreach ( $res as $row ) {
1790 $values[] = $row->value;
1791 }
1792
1793 return $values;
1794 }
1795
1807 protected function makeSelectOptions( array $options ) {
1808 $preLimitTail = $postLimitTail = '';
1809 $startOpts = '';
1810
1811 $noKeyOptions = [];
1812
1813 foreach ( $options as $key => $option ) {
1814 if ( is_numeric( $key ) ) {
1815 $noKeyOptions[$option] = true;
1816 }
1817 }
1818
1819 $preLimitTail .= $this->makeGroupByWithHaving( $options );
1820
1821 $preLimitTail .= $this->makeOrderBy( $options );
1822
1823 if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1824 $postLimitTail .= ' FOR UPDATE';
1825 }
1826
1827 if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1828 $postLimitTail .= ' LOCK IN SHARE MODE';
1829 }
1830
1831 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1832 $startOpts .= 'DISTINCT';
1833 }
1834
1835 # Various MySQL extensions
1836 if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1837 $startOpts .= ' /*! STRAIGHT_JOIN */';
1838 }
1839
1840 if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1841 $startOpts .= ' SQL_BIG_RESULT';
1842 }
1843
1844 if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1845 $startOpts .= ' SQL_BUFFER_RESULT';
1846 }
1847
1848 if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1849 $startOpts .= ' SQL_SMALL_RESULT';
1850 }
1851
1852 if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1853 $startOpts .= ' SQL_CALC_FOUND_ROWS';
1854 }
1855
1856 if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1857 $useIndex = $this->useIndexClause( $options['USE INDEX'] );
1858 } else {
1859 $useIndex = '';
1860 }
1861 if ( isset( $options['IGNORE INDEX'] ) && is_string( $options['IGNORE INDEX'] ) ) {
1862 $ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
1863 } else {
1864 $ignoreIndex = '';
1865 }
1866
1867 return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1868 }
1869
1878 protected function makeGroupByWithHaving( $options ) {
1879 $sql = '';
1880 if ( isset( $options['GROUP BY'] ) ) {
1881 $gb = is_array( $options['GROUP BY'] )
1882 ? implode( ',', $options['GROUP BY'] )
1883 : $options['GROUP BY'];
1884 $sql .= ' GROUP BY ' . $gb;
1885 }
1886 if ( isset( $options['HAVING'] ) ) {
1887 $having = is_array( $options['HAVING'] )
1888 ? $this->makeList( $options['HAVING'], self::LIST_AND )
1889 : $options['HAVING'];
1890 $sql .= ' HAVING ' . $having;
1891 }
1892
1893 return $sql;
1894 }
1895
1904 protected function makeOrderBy( $options ) {
1905 if ( isset( $options['ORDER BY'] ) ) {
1906 $ob = is_array( $options['ORDER BY'] )
1907 ? implode( ',', $options['ORDER BY'] )
1908 : $options['ORDER BY'];
1909
1910 return ' ORDER BY ' . $ob;
1911 }
1912
1913 return '';
1914 }
1915
1916 public function select(
1917 $table, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1918 ) {
1919 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1920
1921 return $this->query( $sql, $fname, self::QUERY_CHANGE_NONE );
1922 }
1923
1928 public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1929 $options = [], $join_conds = []
1930 ) {
1931 if ( is_array( $vars ) ) {
1932 $fields = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1933 } else {
1934 $fields = $vars;
1935 }
1936
1937 $options = (array)$options;
1938 $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1939 ? $options['USE INDEX']
1940 : [];
1941 $ignoreIndexes = (
1942 isset( $options['IGNORE INDEX'] ) &&
1943 is_array( $options['IGNORE INDEX'] )
1944 )
1945 ? $options['IGNORE INDEX']
1946 : [];
1947
1948 if (
1949 $this->selectOptionsIncludeLocking( $options ) &&
1950 $this->selectFieldsOrOptionsAggregate( $vars, $options )
1951 ) {
1952 // Some DB types (e.g. postgres) disallow FOR UPDATE with aggregate
1953 // functions. Discourage use of such queries to encourage compatibility.
1954 call_user_func(
1955 $this->deprecationLogger,
1956 __METHOD__ . ": aggregation used with a locking SELECT ($fname)"
1957 );
1958 }
1959
1960 if ( is_array( $table ) ) {
1961 if ( count( $table ) === 0 ) {
1962 $from = '';
1963 } else {
1964 $from = ' FROM ' .
1966 $table, $useIndexes, $ignoreIndexes, $join_conds );
1967 }
1968 } elseif ( $table != '' ) {
1969 $from = ' FROM ' .
1971 [ $table ], $useIndexes, $ignoreIndexes, [] );
1972 } else {
1973 $from = '';
1974 }
1975
1976 list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
1977 $this->makeSelectOptions( $options );
1978
1979 if ( is_array( $conds ) ) {
1980 $conds = $this->makeList( $conds, self::LIST_AND );
1981 }
1982
1983 if ( $conds === null || $conds === false ) {
1984 $this->queryLogger->warning(
1985 __METHOD__
1986 . ' called from '
1987 . $fname
1988 . ' with incorrect parameters: $conds must be a string or an array'
1989 );
1990 $conds = '';
1991 }
1992
1993 if ( $conds === '' || $conds === '*' ) {
1994 $sql = "SELECT $startOpts $fields $from $useIndex $ignoreIndex $preLimitTail";
1995 } elseif ( is_string( $conds ) ) {
1996 $sql = "SELECT $startOpts $fields $from $useIndex $ignoreIndex " .
1997 "WHERE $conds $preLimitTail";
1998 } else {
1999 throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
2000 }
2001
2002 if ( isset( $options['LIMIT'] ) ) {
2003 $sql = $this->limitResult( $sql, $options['LIMIT'],
2004 $options['OFFSET'] ?? false );
2005 }
2006 $sql = "$sql $postLimitTail";
2007
2008 if ( isset( $options['EXPLAIN'] ) ) {
2009 $sql = 'EXPLAIN ' . $sql;
2010 }
2011
2012 return $sql;
2013 }
2014
2015 public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
2016 $options = [], $join_conds = []
2017 ) {
2018 $options = (array)$options;
2019 $options['LIMIT'] = 1;
2020
2021 $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
2022 if ( $res === false ) {
2023 throw new DBUnexpectedError( $this, "Got false from select()" );
2024 }
2025
2026 if ( !$this->numRows( $res ) ) {
2027 return false;
2028 }
2029
2030 return $this->fetchObject( $res );
2031 }
2032
2037 public function estimateRowCount(
2038 $tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
2039 ) {
2040 $conds = $this->normalizeConditions( $conds, $fname );
2041 $column = $this->extractSingleFieldFromList( $var );
2042 if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
2043 $conds[] = "$column IS NOT NULL";
2044 }
2045
2046 $res = $this->select(
2047 $tables, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options, $join_conds
2048 );
2049 $row = $res ? $this->fetchRow( $res ) : [];
2050
2051 return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
2052 }
2053
2054 public function selectRowCount(
2055 $tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
2056 ) {
2057 $conds = $this->normalizeConditions( $conds, $fname );
2058 $column = $this->extractSingleFieldFromList( $var );
2059 if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
2060 $conds[] = "$column IS NOT NULL";
2061 }
2062
2063 $res = $this->select(
2064 [
2065 'tmp_count' => $this->buildSelectSubquery(
2066 $tables,
2067 '1',
2068 $conds,
2069 $fname,
2070 $options,
2071 $join_conds
2072 )
2073 ],
2074 [ 'rowcount' => 'COUNT(*)' ],
2075 [],
2076 $fname
2077 );
2078 $row = $res ? $this->fetchRow( $res ) : [];
2079
2080 return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
2081 }
2082
2087 private function selectOptionsIncludeLocking( $options ) {
2088 $options = (array)$options;
2089 foreach ( [ 'FOR UPDATE', 'LOCK IN SHARE MODE' ] as $lock ) {
2090 if ( in_array( $lock, $options, true ) ) {
2091 return true;
2092 }
2093 }
2094
2095 return false;
2096 }
2097
2103 private function selectFieldsOrOptionsAggregate( $fields, $options ) {
2104 foreach ( (array)$options as $key => $value ) {
2105 if ( is_string( $key ) ) {
2106 if ( preg_match( '/^(?:GROUP BY|HAVING)$/i', $key ) ) {
2107 return true;
2108 }
2109 } elseif ( is_string( $value ) ) {
2110 if ( preg_match( '/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
2111 return true;
2112 }
2113 }
2114 }
2115
2116 $regex = '/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
2117 foreach ( (array)$fields as $field ) {
2118 if ( is_string( $field ) && preg_match( $regex, $field ) ) {
2119 return true;
2120 }
2121 }
2122
2123 return false;
2124 }
2125
2131 final protected function normalizeRowArray( array $rowOrRows ) {
2132 if ( !$rowOrRows ) {
2133 $rows = [];
2134 } elseif ( isset( $rowOrRows[0] ) ) {
2135 $rows = $rowOrRows;
2136 } else {
2137 $rows = [ $rowOrRows ];
2138 }
2139
2140 foreach ( $rows as $row ) {
2141 if ( !is_array( $row ) ) {
2142 throw new DBUnexpectedError( $this, "Got non-array in row array" );
2143 } elseif ( !$row ) {
2144 throw new DBUnexpectedError( $this, "Got empty array in row array" );
2145 }
2146 }
2147
2148 return $rows;
2149 }
2150
2157 final protected function normalizeConditions( $conds, $fname ) {
2158 if ( $conds === null || $conds === false ) {
2159 $this->queryLogger->warning(
2160 __METHOD__
2161 . ' called from '
2162 . $fname
2163 . ' with incorrect parameters: $conds must be a string or an array'
2164 );
2165 return [];
2166 } elseif ( $conds === '' ) {
2167 return [];
2168 }
2169
2170 return is_array( $conds ) ? $conds : [ $conds ];
2171 }
2172
2178 final protected function normalizeUpsertKeys( $uniqueKeys ) {
2179 if ( is_string( $uniqueKeys ) ) {
2180 return [ [ $uniqueKeys ] ];
2181 }
2182
2183 if ( !is_array( $uniqueKeys ) || !$uniqueKeys ) {
2184 throw new DBUnexpectedError( $this, 'Invalid or empty unique key array' );
2185 }
2186
2187 $oldStyle = false;
2188 $uniqueColumnSets = [];
2189 foreach ( $uniqueKeys as $i => $uniqueKey ) {
2190 if ( !is_int( $i ) ) {
2191 throw new DBUnexpectedError( $this, 'Unique key array should be a list' );
2192 } elseif ( is_string( $uniqueKey ) ) {
2193 $oldStyle = true;
2194 $uniqueColumnSets[] = [ $uniqueKey ];
2195 } elseif ( is_array( $uniqueKey ) && $uniqueKey ) {
2196 $uniqueColumnSets[] = $uniqueKey;
2197 } else {
2198 throw new DBUnexpectedError( $this, 'Invalid unique key array entry' );
2199 }
2200 }
2201
2202 if ( count( $uniqueColumnSets ) > 1 ) {
2203 // If an existing row conflicts with new row X on key A and new row Y on key B,
2204 // it is not well defined how many UPDATEs should apply to the existing row and
2205 // in what order the new rows are checked
2206 $this->queryLogger->warning(
2207 __METHOD__ . " called with multiple unique keys",
2208 [ 'exception' => new RuntimeException() ]
2209 );
2210 }
2211
2212 if ( $oldStyle ) {
2213 // Passing a list of strings for single-column unique keys is too
2214 // easily confused with passing the columns of composite unique key
2215 $this->queryLogger->warning(
2216 __METHOD__ . " called with deprecated parameter style: " .
2217 "the unique key array should be a string or array of string arrays",
2218 [ 'exception' => new RuntimeException() ]
2219 );
2220 }
2221
2222 return $uniqueColumnSets;
2223 }
2224
2230 final protected function normalizeOptions( $options ) {
2231 if ( is_array( $options ) ) {
2232 return $options;
2233 } elseif ( is_string( $options ) ) {
2234 return ( $options === '' ) ? [] : [ $options ];
2235 } else {
2236 throw new DBUnexpectedError( $this, __METHOD__ . ': expected string or array' );
2237 }
2238 }
2239
2246 final protected function isFlagInOptions( $option, array $options ) {
2247 foreach ( array_keys( $options, $option, true ) as $k ) {
2248 if ( is_int( $k ) ) {
2249 return true;
2250 }
2251 }
2252
2253 return false;
2254 }
2255
2260 final protected function extractSingleFieldFromList( $var ) {
2261 if ( is_array( $var ) ) {
2262 if ( !$var ) {
2263 $column = null;
2264 } elseif ( count( $var ) == 1 ) {
2265 $column = $var[0] ?? reset( $var );
2266 } else {
2267 throw new DBUnexpectedError( $this, __METHOD__ . ': got multiple columns' );
2268 }
2269 } else {
2270 $column = $var;
2271 }
2272
2273 return $column;
2274 }
2275
2276 public function lockForUpdate(
2277 $table, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
2278 ) {
2279 if ( !$this->trxLevel() && !$this->getFlag( self::DBO_TRX ) ) {
2280 throw new DBUnexpectedError(
2281 $this,
2282 __METHOD__ . ': no transaction is active nor is DBO_TRX set'
2283 );
2284 }
2285
2286 $options = (array)$options;
2287 $options[] = 'FOR UPDATE';
2288
2289 return $this->selectRowCount( $table, '*', $conds, $fname, $options, $join_conds );
2290 }
2291
2292 public function fieldExists( $table, $field, $fname = __METHOD__ ) {
2293 $info = $this->fieldInfo( $table, $field );
2294
2295 return (bool)$info;
2296 }
2297
2298 public function indexExists( $table, $index, $fname = __METHOD__ ) {
2299 if ( !$this->tableExists( $table, $fname ) ) {
2300 return null;
2301 }
2302
2303 $info = $this->indexInfo( $table, $index, $fname );
2304 if ( $info === null ) {
2305 return null;
2306 } else {
2307 return $info !== false;
2308 }
2309 }
2310
2311 abstract public function tableExists( $table, $fname = __METHOD__ );
2312
2317 public function indexUnique( $table, $index, $fname = __METHOD__ ) {
2318 $indexInfo = $this->indexInfo( $table, $index, $fname );
2319
2320 if ( !$indexInfo ) {
2321 return null;
2322 }
2323
2324 return !$indexInfo[0]->Non_unique;
2325 }
2326
2327 public function insert( $table, $rows, $fname = __METHOD__, $options = [] ) {
2328 $rows = $this->normalizeRowArray( $rows );
2329 if ( !$rows ) {
2330 return true;
2331 }
2332
2333 $options = $this->normalizeOptions( $options );
2334 if ( $this->isFlagInOptions( 'IGNORE', $options ) ) {
2335 $this->doInsertNonConflicting( $table, $rows, $fname );
2336 } else {
2337 $this->doInsert( $table, $rows, $fname );
2338 }
2339
2340 return true;
2341 }
2342
2351 protected function doInsert( $table, array $rows, $fname ) {
2352 $encTable = $this->tableName( $table );
2353 list( $sqlColumns, $sqlTuples ) = $this->makeInsertLists( $rows );
2354
2355 $sql = "INSERT INTO $encTable ($sqlColumns) VALUES $sqlTuples";
2356
2357 $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
2358 }
2359
2368 protected function doInsertNonConflicting( $table, array $rows, $fname ) {
2369 $encTable = $this->tableName( $table );
2370 list( $sqlColumns, $sqlTuples ) = $this->makeInsertLists( $rows );
2371 list( $sqlVerb, $sqlOpts ) = $this->makeInsertNonConflictingVerbAndOptions();
2372
2373 $sql = rtrim( "$sqlVerb $encTable ($sqlColumns) VALUES $sqlTuples $sqlOpts" );
2374
2375 $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
2376 }
2377
2384 return [ 'INSERT IGNORE INTO', '' ];
2385 }
2386
2397 protected function makeInsertLists( array $rows ) {
2398 $firstRow = $rows[0];
2399 if ( !is_array( $firstRow ) || !$firstRow ) {
2400 throw new DBUnexpectedError( $this, 'Got an empty row list or empty row' );
2401 }
2402 // List of columns that define the value tuple ordering
2403 $tupleColumns = array_keys( $firstRow );
2404
2405 $valueTuples = [];
2406 foreach ( $rows as $row ) {
2407 $rowColumns = array_keys( $row );
2408 // VALUES(...) requires a uniform correspondance of (column => value)
2409 if ( $rowColumns !== $tupleColumns ) {
2410 throw new DBUnexpectedError(
2411 $this,
2412 'Got row columns (' . implode( ', ', $rowColumns ) . ') ' .
2413 'instead of expected (' . implode( ', ', $tupleColumns ) . ')'
2414 );
2415 }
2416 // Make the value tuple that defines this row
2417 $valueTuples[] = '(' . $this->makeList( $row, self::LIST_COMMA ) . ')';
2418 }
2419
2420 return [
2421 $this->makeList( $tupleColumns, self::LIST_NAMES ),
2422 implode( ',', $valueTuples )
2423 ];
2424 }
2425
2433 protected function makeUpdateOptionsArray( $options ) {
2434 $options = $this->normalizeOptions( $options );
2435
2436 $opts = [];
2437
2438 if ( in_array( 'IGNORE', $options ) ) {
2439 $opts[] = 'IGNORE';
2440 }
2441
2442 return $opts;
2443 }
2444
2452 protected function makeUpdateOptions( $options ) {
2453 $opts = $this->makeUpdateOptionsArray( $options );
2454
2455 return implode( ' ', $opts );
2456 }
2457
2458 public function update( $table, $set, $conds, $fname = __METHOD__, $options = [] ) {
2459 $this->assertConditionIsNotEmpty( $conds, __METHOD__, true );
2460 $table = $this->tableName( $table );
2461 $opts = $this->makeUpdateOptions( $options );
2462 $sql = "UPDATE $opts $table SET " . $this->makeList( $set, self::LIST_SET );
2463
2464 if ( $conds && $conds !== IDatabase::ALL_ROWS ) {
2465 if ( is_array( $conds ) ) {
2466 $conds = $this->makeList( $conds, self::LIST_AND );
2467 }
2468 $sql .= ' WHERE ' . $conds;
2469 }
2470
2471 $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
2472
2473 return true;
2474 }
2475
2476 public function makeList( array $a, $mode = self::LIST_COMMA ) {
2477 $first = true;
2478 $list = '';
2479
2480 foreach ( $a as $field => $value ) {
2481 if ( !$first ) {
2482 if ( $mode == self::LIST_AND ) {
2483 $list .= ' AND ';
2484 } elseif ( $mode == self::LIST_OR ) {
2485 $list .= ' OR ';
2486 } else {
2487 $list .= ',';
2488 }
2489 } else {
2490 $first = false;
2491 }
2492
2493 if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
2494 $list .= "($value)";
2495 } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
2496 $list .= "$value";
2497 } elseif (
2498 ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
2499 ) {
2500 // Remove null from array to be handled separately if found
2501 $includeNull = false;
2502 foreach ( array_keys( $value, null, true ) as $nullKey ) {
2503 $includeNull = true;
2504 unset( $value[$nullKey] );
2505 }
2506 if ( count( $value ) == 0 && !$includeNull ) {
2507 throw new InvalidArgumentException(
2508 __METHOD__ . ": empty input for field $field" );
2509 } elseif ( count( $value ) == 0 ) {
2510 // only check if $field is null
2511 $list .= "$field IS NULL";
2512 } else {
2513 // IN clause contains at least one valid element
2514 if ( $includeNull ) {
2515 // Group subconditions to ensure correct precedence
2516 $list .= '(';
2517 }
2518 if ( count( $value ) == 1 ) {
2519 // Special-case single values, as IN isn't terribly efficient
2520 // Don't necessarily assume the single key is 0; we don't
2521 // enforce linear numeric ordering on other arrays here.
2522 $value = array_values( $value )[0];
2523 $list .= $field . " = " . $this->addQuotes( $value );
2524 } else {
2525 $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
2526 }
2527 // if null present in array, append IS NULL
2528 if ( $includeNull ) {
2529 $list .= " OR $field IS NULL)";
2530 }
2531 }
2532 } elseif ( $value === null ) {
2533 if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
2534 $list .= "$field IS ";
2535 } elseif ( $mode == self::LIST_SET ) {
2536 $list .= "$field = ";
2537 }
2538 $list .= 'NULL';
2539 } else {
2540 if (
2541 $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
2542 ) {
2543 $list .= "$field = ";
2544 }
2545 $list .= $mode == self::LIST_NAMES ? $value : $this->addQuotes( $value );
2546 }
2547 }
2548
2549 return $list;
2550 }
2551
2552 public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
2553 $conds = [];
2554
2555 foreach ( $data as $base => $sub ) {
2556 if ( count( $sub ) ) {
2557 $conds[] = $this->makeList(
2558 [ $baseKey => $base, $subKey => array_map( 'strval', array_keys( $sub ) ) ],
2559 self::LIST_AND );
2560 }
2561 }
2562
2563 if ( $conds ) {
2564 return $this->makeList( $conds, self::LIST_OR );
2565 } else {
2566 // Nothing to search for...
2567 return false;
2568 }
2569 }
2570
2575 public function aggregateValue( $valuedata, $valuename = 'value' ) {
2576 return $valuename;
2577 }
2578
2583 public function bitNot( $field ) {
2584 return "(~$field)";
2585 }
2586
2591 public function bitAnd( $fieldLeft, $fieldRight ) {
2592 return "($fieldLeft & $fieldRight)";
2593 }
2594
2599 public function bitOr( $fieldLeft, $fieldRight ) {
2600 return "($fieldLeft | $fieldRight)";
2601 }
2602
2607 public function buildConcat( $stringList ) {
2608 return 'CONCAT(' . implode( ',', $stringList ) . ')';
2609 }
2610
2615 public function buildGroupConcatField(
2616 $delim, $table, $field, $conds = '', $join_conds = []
2617 ) {
2618 $fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
2619
2620 return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
2621 }
2622
2627 public function buildGreatest( $fields, $values ) {
2628 return $this->buildSuperlative( 'GREATEST', $fields, $values );
2629 }
2630
2635 public function buildLeast( $fields, $values ) {
2636 return $this->buildSuperlative( 'LEAST', $fields, $values );
2637 }
2638
2655 protected function buildSuperlative( $sqlfunc, $fields, $values ) {
2656 $fields = is_array( $fields ) ? $fields : [ $fields ];
2657 $values = is_array( $values ) ? $values : [ $values ];
2658
2659 $encValues = [];
2660 foreach ( $fields as $alias => $field ) {
2661 if ( is_int( $alias ) ) {
2662 $encValues[] = $this->addIdentifierQuotes( $field );
2663 } else {
2664 $encValues[] = $field; // expression
2665 }
2666 }
2667 foreach ( $values as $value ) {
2668 if ( is_int( $value ) || is_float( $value ) ) {
2669 $encValues[] = $value;
2670 } elseif ( is_string( $value ) ) {
2671 $encValues[] = $this->addQuotes( $value );
2672 } elseif ( $value === null ) {
2673 throw new DBUnexpectedError( $this, 'Null value in superlative' );
2674 } else {
2675 throw new DBUnexpectedError( $this, 'Unexpected value type in superlative' );
2676 }
2677 }
2678
2679 return $sqlfunc . '(' . implode( ',', $encValues ) . ')';
2680 }
2681
2686 public function buildSubstring( $input, $startPosition, $length = null ) {
2687 $this->assertBuildSubstringParams( $startPosition, $length );
2688 $functionBody = "$input FROM $startPosition";
2689 if ( $length !== null ) {
2690 $functionBody .= " FOR $length";
2691 }
2692 return 'SUBSTRING(' . $functionBody . ')';
2693 }
2694
2707 protected function assertBuildSubstringParams( $startPosition, $length ) {
2708 if ( !is_int( $startPosition ) || $startPosition <= 0 ) {
2709 throw new InvalidArgumentException(
2710 '$startPosition must be a positive integer'
2711 );
2712 }
2713 if ( !( is_int( $length ) && $length >= 0 || $length === null ) ) {
2714 throw new InvalidArgumentException(
2715 '$length must be null or an integer greater than or equal to 0'
2716 );
2717 }
2718 }
2719
2733 protected function assertConditionIsNotEmpty( $conds, string $fname, bool $deprecate ) {
2734 $isCondValid = ( is_string( $conds ) || is_array( $conds ) ) && $conds;
2735 if ( !$isCondValid ) {
2736 if ( $deprecate ) {
2737 wfDeprecated( $fname . ' called with empty $conds', '1.35', false, 3 );
2738 } else {
2739 throw new DBUnexpectedError( $this, $fname . ' called with empty conditions' );
2740 }
2741 }
2742 }
2743
2748 public function buildStringCast( $field ) {
2749 // In theory this should work for any standards-compliant
2750 // SQL implementation, although it may not be the best way to do it.
2751 return "CAST( $field AS CHARACTER )";
2752 }
2753
2758 public function buildIntegerCast( $field ) {
2759 return 'CAST( ' . $field . ' AS INTEGER )';
2760 }
2761
2762 public function buildSelectSubquery(
2763 $table, $vars, $conds = '', $fname = __METHOD__,
2764 $options = [], $join_conds = []
2765 ) {
2766 return new Subquery(
2767 $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
2768 );
2769 }
2770
2775 public function databasesAreIndependent() {
2776 return false;
2777 }
2778
2779 final public function selectDB( $db ) {
2780 $this->selectDomain( new DatabaseDomain(
2781 $db,
2782 $this->currentDomain->getSchema(),
2783 $this->currentDomain->getTablePrefix()
2784 ) );
2785
2786 return true;
2787 }
2788
2789 final public function selectDomain( $domain ) {
2790 $this->doSelectDomain( DatabaseDomain::newFromId( $domain ) );
2791 }
2792
2800 protected function doSelectDomain( DatabaseDomain $domain ) {
2801 $this->currentDomain = $domain;
2802 }
2803
2804 public function getDBname() {
2805 return $this->currentDomain->getDatabase();
2806 }
2807
2808 public function getServer() {
2809 return $this->server;
2810 }
2811
2816 public function tableName( $name, $format = 'quoted' ) {
2817 if ( $name instanceof Subquery ) {
2818 throw new DBUnexpectedError(
2819 $this,
2820 __METHOD__ . ': got Subquery instance when expecting a string'
2821 );
2822 }
2823
2824 # Skip the entire process when we have a string quoted on both ends.
2825 # Note that we check the end so that we will still quote any use of
2826 # use of `database`.table. But won't break things if someone wants
2827 # to query a database table with a dot in the name.
2828 if ( $this->isQuotedIdentifier( $name ) ) {
2829 return $name;
2830 }
2831
2832 # Lets test for any bits of text that should never show up in a table
2833 # name. Basically anything like JOIN or ON which are actually part of
2834 # SQL queries, but may end up inside of the table value to combine
2835 # sql. Such as how the API is doing.
2836 # Note that we use a whitespace test rather than a \b test to avoid
2837 # any remote case where a word like on may be inside of a table name
2838 # surrounded by symbols which may be considered word breaks.
2839 if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
2840 $this->queryLogger->warning(
2841 __METHOD__ . ": use of subqueries is not supported this way",
2842 [ 'exception' => new RuntimeException() ]
2843 );
2844
2845 return $name;
2846 }
2847
2848 # Split database and table into proper variables.
2849 list( $database, $schema, $prefix, $table ) = $this->qualifiedTableComponents( $name );
2850
2851 # Quote $table and apply the prefix if not quoted.
2852 # $tableName might be empty if this is called from Database::replaceVars()
2853 $tableName = "{$prefix}{$table}";
2854 if ( $format === 'quoted'
2855 && !$this->isQuotedIdentifier( $tableName )
2856 && $tableName !== ''
2857 ) {
2858 $tableName = $this->addIdentifierQuotes( $tableName );
2859 }
2860
2861 # Quote $schema and $database and merge them with the table name if needed
2862 $tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
2863 $tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
2864
2865 return $tableName;
2866 }
2867
2874 protected function qualifiedTableComponents( $name ) {
2875 # We reverse the explode so that database.table and table both output the correct table.
2876 $dbDetails = explode( '.', $name, 3 );
2877 if ( count( $dbDetails ) == 3 ) {
2878 list( $database, $schema, $table ) = $dbDetails;
2879 # We don't want any prefix added in this case
2880 $prefix = '';
2881 } elseif ( count( $dbDetails ) == 2 ) {
2882 list( $database, $table ) = $dbDetails;
2883 # We don't want any prefix added in this case
2884 $prefix = '';
2885 # In dbs that support it, $database may actually be the schema
2886 # but that doesn't affect any of the functionality here
2887 $schema = '';
2888 } else {
2889 list( $table ) = $dbDetails;
2890 if ( isset( $this->tableAliases[$table] ) ) {
2891 $database = $this->tableAliases[$table]['dbname'];
2892 $schema = is_string( $this->tableAliases[$table]['schema'] )
2893 ? $this->tableAliases[$table]['schema']
2894 : $this->relationSchemaQualifier();
2895 $prefix = is_string( $this->tableAliases[$table]['prefix'] )
2896 ? $this->tableAliases[$table]['prefix']
2897 : $this->tablePrefix();
2898 } else {
2899 $database = '';
2900 $schema = $this->relationSchemaQualifier(); # Default schema
2901 $prefix = $this->tablePrefix(); # Default prefix
2902 }
2903 }
2904
2905 return [ $database, $schema, $prefix, $table ];
2906 }
2907
2914 private function prependDatabaseOrSchema( $namespace, $relation, $format ) {
2915 if ( $namespace !== null && $namespace !== '' ) {
2916 if ( $format === 'quoted' && !$this->isQuotedIdentifier( $namespace ) ) {
2917 $namespace = $this->addIdentifierQuotes( $namespace );
2918 }
2919 $relation = $namespace . '.' . $relation;
2920 }
2921
2922 return $relation;
2923 }
2924
2925 public function tableNames( ...$tables ) {
2926 $retVal = [];
2927
2928 foreach ( $tables as $name ) {
2929 $retVal[$name] = $this->tableName( $name );
2930 }
2931
2932 return $retVal;
2933 }
2934
2935 public function tableNamesN( ...$tables ) {
2936 $retVal = [];
2937
2938 foreach ( $tables as $name ) {
2939 $retVal[] = $this->tableName( $name );
2940 }
2941
2942 return $retVal;
2943 }
2944
2956 protected function tableNameWithAlias( $table, $alias = false ) {
2957 if ( is_string( $table ) ) {
2958 $quotedTable = $this->tableName( $table );
2959 } elseif ( $table instanceof Subquery ) {
2960 $quotedTable = (string)$table;
2961 } else {
2962 throw new InvalidArgumentException( "Table must be a string or Subquery" );
2963 }
2964
2965 if ( $alias === false || $alias === $table ) {
2966 if ( $table instanceof Subquery ) {
2967 throw new InvalidArgumentException( "Subquery table missing alias" );
2968 }
2969
2970 return $quotedTable;
2971 } else {
2972 return $quotedTable . ' ' . $this->addIdentifierQuotes( $alias );
2973 }
2974 }
2975
2985 protected function fieldNameWithAlias( $name, $alias = false ) {
2986 if ( !$alias || (string)$alias === (string)$name ) {
2987 return $name;
2988 } else {
2989 return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
2990 }
2991 }
2992
2999 protected function fieldNamesWithAlias( $fields ) {
3000 $retval = [];
3001 foreach ( $fields as $alias => $field ) {
3002 if ( is_numeric( $alias ) ) {
3003 $alias = $field;
3004 }
3005 $retval[] = $this->fieldNameWithAlias( $field, $alias );
3006 }
3007
3008 return $retval;
3009 }
3010
3022 $tables, $use_index = [], $ignore_index = [], $join_conds = []
3023 ) {
3024 $ret = [];
3025 $retJOIN = [];
3026 $use_index = (array)$use_index;
3027 $ignore_index = (array)$ignore_index;
3028 $join_conds = (array)$join_conds;
3029
3030 foreach ( $tables as $alias => $table ) {
3031 if ( !is_string( $alias ) ) {
3032 // No alias? Set it equal to the table name
3033 $alias = $table;
3034 }
3035
3036 if ( is_array( $table ) ) {
3037 // A parenthesized group
3038 if ( count( $table ) > 1 ) {
3039 $joinedTable = '(' .
3041 $table, $use_index, $ignore_index, $join_conds ) . ')';
3042 } else {
3043 // Degenerate case
3044 $innerTable = reset( $table );
3045 $innerAlias = key( $table );
3046 $joinedTable = $this->tableNameWithAlias(
3047 $innerTable,
3048 is_string( $innerAlias ) ? $innerAlias : $innerTable
3049 );
3050 }
3051 } else {
3052 $joinedTable = $this->tableNameWithAlias( $table, $alias );
3053 }
3054
3055 // Is there a JOIN clause for this table?
3056 if ( isset( $join_conds[$alias] ) ) {
3057 list( $joinType, $conds ) = $join_conds[$alias];
3058 $tableClause = $joinType;
3059 $tableClause .= ' ' . $joinedTable;
3060 if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
3061 $use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
3062 if ( $use != '' ) {
3063 $tableClause .= ' ' . $use;
3064 }
3065 }
3066 if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
3067 $ignore = $this->ignoreIndexClause(
3068 implode( ',', (array)$ignore_index[$alias] ) );
3069 if ( $ignore != '' ) {
3070 $tableClause .= ' ' . $ignore;
3071 }
3072 }
3073 $on = $this->makeList( (array)$conds, self::LIST_AND );
3074 if ( $on != '' ) {
3075 $tableClause .= ' ON (' . $on . ')';
3076 }
3077
3078 $retJOIN[] = $tableClause;
3079 } elseif ( isset( $use_index[$alias] ) ) {
3080 // Is there an INDEX clause for this table?
3081 $tableClause = $joinedTable;
3082 $tableClause .= ' ' . $this->useIndexClause(
3083 implode( ',', (array)$use_index[$alias] )
3084 );
3085
3086 $ret[] = $tableClause;
3087 } elseif ( isset( $ignore_index[$alias] ) ) {
3088 // Is there an INDEX clause for this table?
3089 $tableClause = $joinedTable;
3090 $tableClause .= ' ' . $this->ignoreIndexClause(
3091 implode( ',', (array)$ignore_index[$alias] )
3092 );
3093
3094 $ret[] = $tableClause;
3095 } else {
3096 $tableClause = $joinedTable;
3097
3098 $ret[] = $tableClause;
3099 }
3100 }
3101
3102 // We can't separate explicit JOIN clauses with ',', use ' ' for those
3103 $implicitJoins = implode( ',', $ret );
3104 $explicitJoins = implode( ' ', $retJOIN );
3105
3106 // Compile our final table clause
3107 return implode( ' ', [ $implicitJoins, $explicitJoins ] );
3108 }
3109
3116 protected function indexName( $index ) {
3117 return $this->indexAliases[$index] ?? $index;
3118 }
3119
3124 public function addQuotes( $s ) {
3125 if ( $s instanceof Blob ) {
3126 $s = $s->fetch();
3127 }
3128 if ( $s === null ) {
3129 return 'NULL';
3130 } elseif ( is_bool( $s ) ) {
3131 return (string)(int)$s;
3132 } elseif ( is_int( $s ) ) {
3133 return (string)$s;
3134 } else {
3135 return "'" . $this->strencode( $s ) . "'";
3136 }
3137 }
3138
3143 public function addIdentifierQuotes( $s ) {
3144 return '"' . str_replace( '"', '""', $s ) . '"';
3145 }
3146
3157 public function isQuotedIdentifier( $name ) {
3158 return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
3159 }
3160
3167 protected function escapeLikeInternal( $s, $escapeChar = '`' ) {
3168 return str_replace( [ $escapeChar, '%', '_' ],
3169 [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ],
3170 $s );
3171 }
3172
3177 public function buildLike( $param, ...$params ) {
3178 if ( is_array( $param ) ) {
3179 $params = $param;
3180 } else {
3181 $params = func_get_args();
3182 }
3183
3184 $s = '';
3185
3186 // We use ` instead of \ as the default LIKE escape character, since addQuotes()
3187 // may escape backslashes, creating problems of double escaping. The `
3188 // character has good cross-DBMS compatibility, avoiding special operators
3189 // in MS SQL like ^ and %
3190 $escapeChar = '`';
3191
3192 foreach ( $params as $value ) {
3193 if ( $value instanceof LikeMatch ) {
3194 $s .= $value->toString();
3195 } else {
3196 $s .= $this->escapeLikeInternal( $value, $escapeChar );
3197 }
3198 }
3199
3200 return ' LIKE ' .
3201 $this->addQuotes( $s ) . ' ESCAPE ' . $this->addQuotes( $escapeChar ) . ' ';
3202 }
3203
3204 public function anyChar() {
3205 return new LikeMatch( '_' );
3206 }
3207
3208 public function anyString() {
3209 return new LikeMatch( '%' );
3210 }
3211
3212 public function nextSequenceValue( $seqName ) {
3213 return null;
3214 }
3215
3229 public function useIndexClause( $index ) {
3230 return '';
3231 }
3232
3242 public function ignoreIndexClause( $index ) {
3243 return '';
3244 }
3245
3246 public function replace( $table, $uniqueKeys, $rows, $fname = __METHOD__ ) {
3247 $rows = $this->normalizeRowArray( $rows );
3248 if ( !$rows ) {
3249 return;
3250 }
3251
3252 if ( $uniqueKeys ) {
3253 $uniqueKeys = $this->normalizeUpsertKeys( $uniqueKeys );
3254 $this->doReplace( $table, $uniqueKeys, $rows, $fname );
3255 } else {
3256 $this->queryLogger->warning(
3257 __METHOD__ . " called with no unique keys",
3258 [ 'exception' => new RuntimeException() ]
3259 );
3260 $this->doInsert( $table, $rows, $fname );
3261 }
3262 }
3263
3273 protected function doReplace( $table, array $uniqueKeys, array $rows, $fname ) {
3275 $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
3276 try {
3277 foreach ( $rows as $row ) {
3278 // Delete any conflicting rows (including ones inserted from $rows)
3279 $sqlCondition = $this->makeConditionCollidesUponKeys( [ $row ], $uniqueKeys );
3280 $this->delete( $table, [ $sqlCondition ], $fname );
3281 $affectedRowCount += $this->affectedRows();
3282 // Now insert the row
3283 $this->insert( $table, $row, $fname );
3284 $affectedRowCount += $this->affectedRows();
3285 }
3286 $this->endAtomic( $fname );
3287 } catch ( Throwable $e ) {
3288 $this->cancelAtomic( $fname );
3289 throw $e;
3290 }
3291 $this->affectedRowCount = $affectedRowCount;
3292 }
3293
3299 private function makeConditionCollidesUponKey( array $rows, array $uniqueKey ) {
3300 if ( !$rows ) {
3301 throw new DBUnexpectedError( $this, "Empty row array" );
3302 } elseif ( !$uniqueKey ) {
3303 throw new DBUnexpectedError( $this, "Empty unique key array" );
3304 }
3305
3306 if ( count( $uniqueKey ) == 1 ) {
3307 // Use a simple IN(...) clause
3308 $column = reset( $uniqueKey );
3309 $values = array_column( $rows, $column );
3310 if ( count( $values ) !== count( $rows ) ) {
3311 throw new DBUnexpectedError( $this, "Missing values for unique key ($column)" );
3312 }
3313
3314 return $this->makeList( [ $column => $values ], self::LIST_AND );
3315 }
3316
3317 $disjunctions = [];
3318 foreach ( $rows as $row ) {
3319 $rowKeyMap = array_intersect_key( $row, array_flip( $uniqueKey ) );
3320 if ( count( $rowKeyMap ) != count( $uniqueKey ) ) {
3321 throw new DBUnexpectedError(
3322 $this,
3323 "Missing values for unique key (" . implode( ',', $uniqueKey ) . ")"
3324 );
3325 }
3326 $disjunctions[] = $this->makeList( $rowKeyMap, self::LIST_AND );
3327 }
3328
3329 return count( $disjunctions ) > 1
3330 ? $this->makeList( $disjunctions, self::LIST_OR )
3331 : $disjunctions[0];
3332 }
3333
3340 final protected function makeConditionCollidesUponKeys( array $rows, array $uniqueKeys ) {
3341 if ( !$uniqueKeys ) {
3342 throw new DBUnexpectedError( $this, "Empty unique key array" );
3343 }
3344
3345 $disjunctions = [];
3346 foreach ( $uniqueKeys as $uniqueKey ) {
3347 $disjunctions[] = $this->makeConditionCollidesUponKey( $rows, $uniqueKey );
3348 }
3349
3350 return count( $disjunctions ) > 1
3351 ? $this->makeList( $disjunctions, self::LIST_OR )
3352 : $disjunctions[0];
3353 }
3354
3355 public function upsert( $table, array $rows, $uniqueKeys, array $set, $fname = __METHOD__ ) {
3356 $rows = $this->normalizeRowArray( $rows );
3357 if ( !$rows ) {
3358 return true;
3359 }
3360
3361 if ( $uniqueKeys ) {
3362 $uniqueKeys = $this->normalizeUpsertKeys( $uniqueKeys );
3363 $this->doUpsert( $table, $rows, $uniqueKeys, $set, $fname );
3364 } else {
3365 $this->queryLogger->warning(
3366 __METHOD__ . " called with no unique keys",
3367 [ 'exception' => new RuntimeException() ]
3368 );
3369 $this->doInsert( $table, $rows, $fname );
3370 }
3371
3372 return true;
3373 }
3374
3385 protected function doUpsert( $table, array $rows, array $uniqueKeys, array $set, $fname ) {
3387 $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
3388 try {
3389 foreach ( $rows as $row ) {
3390 // Update any existing conflicting rows (including ones inserted from $rows)
3391 $sqlConditions = $this->makeConditionCollidesUponKeys( [ $row ], $uniqueKeys );
3392 $this->update( $table, $set, [ $sqlConditions ], $fname );
3393 $rowsUpdated = $this->affectedRows();
3394 $affectedRowCount += $rowsUpdated;
3395 if ( $rowsUpdated <= 0 ) {
3396 // Now insert the row if there are no conflicts
3397 $this->insert( $table, $row, $fname );
3398 $affectedRowCount += $this->affectedRows();
3399 }
3400 }
3401 $this->endAtomic( $fname );
3402 } catch ( Throwable $e ) {
3403 $this->cancelAtomic( $fname );
3404 throw $e;
3405 }
3406 $this->affectedRowCount = $affectedRowCount;
3407 }
3408
3413 public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
3414 $fname = __METHOD__
3415 ) {
3416 if ( !$conds ) {
3417 throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
3418 }
3419
3420 $delTable = $this->tableName( $delTable );
3421 $joinTable = $this->tableName( $joinTable );
3422 $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
3423 if ( $conds != '*' ) {
3424 $sql .= 'WHERE ' . $this->makeList( $conds, self::LIST_AND );
3425 }
3426 $sql .= ')';
3427
3428 $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
3429 }
3430
3435 public function textFieldSize( $table, $field ) {
3436 $table = $this->tableName( $table );
3437 $sql = "SHOW COLUMNS FROM $table LIKE \"$field\"";
3438 $res = $this->query( $sql, __METHOD__, self::QUERY_CHANGE_NONE );
3439 $row = $this->fetchObject( $res );
3440
3441 $m = [];
3442
3443 if ( preg_match( '/\‍((.*)\‍)/', $row->Type, $m ) ) {
3444 $size = $m[1];
3445 } else {
3446 $size = -1;
3447 }
3448
3449 return $size;
3450 }
3451
3452 public function delete( $table, $conds, $fname = __METHOD__ ) {
3453 $this->assertConditionIsNotEmpty( $conds, __METHOD__, false );
3454
3455 $table = $this->tableName( $table );
3456 $sql = "DELETE FROM $table";
3457
3458 if ( $conds !== IDatabase::ALL_ROWS ) {
3459 if ( is_array( $conds ) ) {
3460 $conds = $this->makeList( $conds, self::LIST_AND );
3461 }
3462 $sql .= ' WHERE ' . $conds;
3463 }
3464
3465 $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
3466
3467 return true;
3468 }
3469
3470 final public function insertSelect(
3471 $destTable,
3472 $srcTable,
3473 $varMap,
3474 $conds,
3475 $fname = __METHOD__,
3476 $insertOptions = [],
3477 $selectOptions = [],
3478 $selectJoinConds = []
3479 ) {
3480 static $hints = [ 'NO_AUTO_COLUMNS' ];
3481
3482 $insertOptions = $this->normalizeOptions( $insertOptions );
3483 $selectOptions = $this->normalizeOptions( $selectOptions );
3484
3485 if ( $this->cliMode && $this->isInsertSelectSafe( $insertOptions, $selectOptions ) ) {
3486 // For massive migrations with downtime, we don't want to select everything
3487 // into memory and OOM, so do all this native on the server side if possible.
3488 $this->doInsertSelectNative(
3489 $destTable,
3490 $srcTable,
3491 $varMap,
3492 $conds,
3493 $fname,
3494 array_diff( $insertOptions, $hints ),
3495 $selectOptions,
3496 $selectJoinConds
3497 );
3498 } else {
3499 $this->doInsertSelectGeneric(
3500 $destTable,
3501 $srcTable,
3502 $varMap,
3503 $conds,
3504 $fname,
3505 array_diff( $insertOptions, $hints ),
3506 $selectOptions,
3507 $selectJoinConds
3508 );
3509 }
3510
3511 return true;
3512 }
3513
3521 protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
3522 return true;
3523 }
3524
3539 protected function doInsertSelectGeneric(
3540 $destTable,
3541 $srcTable,
3542 array $varMap,
3543 $conds,
3544 $fname,
3545 array $insertOptions,
3546 array $selectOptions,
3547 $selectJoinConds
3548 ) {
3549 // For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
3550 // on only the master (without needing row-based-replication). It also makes it easy to
3551 // know how big the INSERT is going to be.
3552 $fields = [];
3553 foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
3554 $fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
3555 }
3556 $res = $this->select(
3557 $srcTable,
3558 implode( ',', $fields ),
3559 $conds,
3560 $fname,
3561 array_merge( $selectOptions, [ 'FOR UPDATE' ] ),
3562 $selectJoinConds
3563 );
3564 if ( !$res ) {
3565 return;
3566 }
3567
3569 $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
3570 try {
3571 $rows = [];
3572 foreach ( $res as $row ) {
3573 $rows[] = (array)$row;
3574 }
3575 // Avoid inserts that are too huge
3576 $rowBatches = array_chunk( $rows, $this->nonNativeInsertSelectBatchSize );
3577 foreach ( $rowBatches as $rows ) {
3578 $this->insert( $destTable, $rows, $fname, $insertOptions );
3579 $affectedRowCount += $this->affectedRows();
3580 }
3581 } catch ( Throwable $e ) {
3582 $this->cancelAtomic( $fname );
3583 throw $e;
3584 }
3585 $this->endAtomic( $fname );
3586 $this->affectedRowCount = $affectedRowCount;
3587 }
3588
3604 protected function doInsertSelectNative(
3605 $destTable,
3606 $srcTable,
3607 array $varMap,
3608 $conds,
3609 $fname,
3610 array $insertOptions,
3611 array $selectOptions,
3612 $selectJoinConds
3613 ) {
3614 list( $sqlVerb, $sqlOpts ) = $this->isFlagInOptions( 'IGNORE', $insertOptions )
3616 : [ 'INSERT INTO', '' ];
3617 $encDstTable = $this->tableName( $destTable );
3618 $sqlDstColumns = implode( ',', array_keys( $varMap ) );
3619 $selectSql = $this->selectSQLText(
3620 $srcTable,
3621 array_values( $varMap ),
3622 $conds,
3623 $fname,
3624 $selectOptions,
3625 $selectJoinConds
3626 );
3627
3628 $sql = rtrim( "$sqlVerb $encDstTable ($sqlDstColumns) $selectSql $sqlOpts" );
3629
3630 $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
3631 }
3632
3637 public function limitResult( $sql, $limit, $offset = false ) {
3638 if ( !is_numeric( $limit ) ) {
3639 throw new DBUnexpectedError(
3640 $this,
3641 "Invalid non-numeric limit passed to " . __METHOD__
3642 );
3643 }
3644 // This version works in MySQL and SQLite. It will very likely need to be
3645 // overridden for most other RDBMS subclasses.
3646 return "$sql LIMIT "
3647 . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
3648 . "{$limit} ";
3649 }
3650
3655 public function unionSupportsOrderAndLimit() {
3656 return true; // True for almost every DB supported
3657 }
3658
3663 public function unionQueries( $sqls, $all ) {
3664 $glue = $all ? ') UNION ALL (' : ') UNION (';
3665
3666 return '(' . implode( $glue, $sqls ) . ')';
3667 }
3668
3670 $table, $vars, array $permute_conds, $extra_conds = '', $fname = __METHOD__,
3671 $options = [], $join_conds = []
3672 ) {
3673 // First, build the Cartesian product of $permute_conds
3674 $conds = [ [] ];
3675 foreach ( $permute_conds as $field => $values ) {
3676 if ( !$values ) {
3677 // Skip empty $values
3678 continue;
3679 }
3680 $values = array_unique( $values ); // For sanity
3681 $newConds = [];
3682 foreach ( $conds as $cond ) {
3683 foreach ( $values as $value ) {
3684 $cond[$field] = $value;
3685 $newConds[] = $cond; // Arrays are by-value, not by-reference, so this works
3686 }
3687 }
3688 $conds = $newConds;
3689 }
3690
3691 $extra_conds = $extra_conds === '' ? [] : (array)$extra_conds;
3692
3693 // If there's just one condition and no subordering, hand off to
3694 // selectSQLText directly.
3695 if ( count( $conds ) === 1 &&
3696 ( !isset( $options['INNER ORDER BY'] ) || !$this->unionSupportsOrderAndLimit() )
3697 ) {
3698 return $this->selectSQLText(
3699 $table, $vars, $conds[0] + $extra_conds, $fname, $options, $join_conds
3700 );
3701 }
3702
3703 // Otherwise, we need to pull out the order and limit to apply after
3704 // the union. Then build the SQL queries for each set of conditions in
3705 // $conds. Then union them together (using UNION ALL, because the
3706 // product *should* already be distinct).
3707 $orderBy = $this->makeOrderBy( $options );
3708 $limit = $options['LIMIT'] ?? null;
3709 $offset = $options['OFFSET'] ?? false;
3710 $all = empty( $options['NOTALL'] ) && !in_array( 'NOTALL', $options );
3711 if ( !$this->unionSupportsOrderAndLimit() ) {
3712 unset( $options['ORDER BY'], $options['LIMIT'], $options['OFFSET'] );
3713 } else {
3714 if ( array_key_exists( 'INNER ORDER BY', $options ) ) {
3715 $options['ORDER BY'] = $options['INNER ORDER BY'];
3716 }
3717 if ( $limit !== null && is_numeric( $offset ) && $offset != 0 ) {
3718 // We need to increase the limit by the offset rather than
3719 // using the offset directly, otherwise it'll skip incorrectly
3720 // in the subqueries.
3721 $options['LIMIT'] = $limit + $offset;
3722 unset( $options['OFFSET'] );
3723 }
3724 }
3725
3726 $sqls = [];
3727 foreach ( $conds as $cond ) {
3728 $sqls[] = $this->selectSQLText(
3729 $table, $vars, $cond + $extra_conds, $fname, $options, $join_conds
3730 );
3731 }
3732 $sql = $this->unionQueries( $sqls, $all ) . $orderBy;
3733 if ( $limit !== null ) {
3734 $sql = $this->limitResult( $sql, $limit, $offset );
3735 }
3736
3737 return $sql;
3738 }
3739
3744 public function conditional( $cond, $trueVal, $falseVal ) {
3745 if ( is_array( $cond ) ) {
3746 $cond = $this->makeList( $cond, self::LIST_AND );
3747 }
3748
3749 return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
3750 }
3751
3756 public function strreplace( $orig, $old, $new ) {
3757 return "REPLACE({$orig}, {$old}, {$new})";
3758 }
3759
3764 public function getServerUptime() {
3765 return 0;
3766 }
3767
3772 public function wasDeadlock() {
3773 return false;
3774 }
3775
3780 public function wasLockTimeout() {
3781 return false;
3782 }
3783
3788 public function wasConnectionLoss() {
3789 return $this->wasConnectionError( $this->lastErrno() );
3790 }
3791
3796 public function wasReadOnlyError() {
3797 return false;
3798 }
3799
3800 public function wasErrorReissuable() {
3801 return (
3802 $this->wasDeadlock() ||
3803 $this->wasLockTimeout() ||
3804 $this->wasConnectionLoss()
3805 );
3806 }
3807
3815 public function wasConnectionError( $errno ) {
3816 return false;
3817 }
3818
3826 protected function wasKnownStatementRollbackError() {
3827 return false; // don't know; it could have caused a transaction rollback
3828 }
3829
3834 public function deadlockLoop( ...$args ) {
3835 $function = array_shift( $args );
3836 $tries = self::$DEADLOCK_TRIES;
3837
3838 $this->begin( __METHOD__ );
3839
3840 $retVal = null;
3842 $e = null;
3843 do {
3844 try {
3845 $retVal = $function( ...$args );
3846 break;
3847 } catch ( DBQueryError $e ) {
3848 if ( $this->wasDeadlock() ) {
3849 // Retry after a randomized delay
3850 usleep( mt_rand( self::$DEADLOCK_DELAY_MIN, self::$DEADLOCK_DELAY_MAX ) );
3851 } else {
3852 // Throw the error back up
3853 throw $e;
3854 }
3855 }
3856 } while ( --$tries > 0 );
3857
3858 if ( $tries <= 0 ) {
3859 // Too many deadlocks; give up
3860 $this->rollback( __METHOD__ );
3861 throw $e;
3862 } else {
3863 $this->commit( __METHOD__ );
3864
3865 return $retVal;
3866 }
3867 }
3868
3873 public function masterPosWait( DBMasterPos $pos, $timeout ) {
3874 # Real waits are implemented in the subclass.
3875 return 0;
3876 }
3877
3882 public function getReplicaPos() {
3883 # Stub
3884 return false;
3885 }
3886
3891 public function getMasterPos() {
3892 # Stub
3893 return false;
3894 }
3895
3900 public function serverIsReadOnly() {
3901 return false;
3902 }
3903
3904 final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
3905 if ( !$this->trxLevel() ) {
3906 throw new DBUnexpectedError( $this, "No transaction is active" );
3907 }
3908 $this->trxEndCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3909 }
3910
3911 final public function onTransactionCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
3912 if ( !$this->trxLevel() && $this->getTransactionRoundId() ) {
3913 // Start an implicit transaction similar to how query() does
3914 $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
3915 $this->trxAutomatic = true;
3916 }
3917
3918 $this->trxIdleCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3919 if ( !$this->trxLevel() ) {
3920 $this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
3921 }
3922 }
3923
3924 final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
3925 $this->onTransactionCommitOrIdle( $callback, $fname );
3926 }
3927
3928 final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
3929 if ( !$this->trxLevel() && $this->getTransactionRoundId() ) {
3930 // Start an implicit transaction similar to how query() does
3931 $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
3932 $this->trxAutomatic = true;
3933 }
3934
3935 if ( $this->trxLevel() ) {
3936 $this->trxPreCommitCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3937 } else {
3938 // No transaction is active nor will start implicitly, so make one for this callback
3939 $this->startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
3940 try {
3941 $callback( $this );
3942 $this->endAtomic( __METHOD__ );
3943 } catch ( Throwable $e ) {
3944 $this->cancelAtomic( __METHOD__ );
3945 throw $e;
3946 }
3947 }
3948 }
3949
3950 final public function onAtomicSectionCancel( callable $callback, $fname = __METHOD__ ) {
3951 if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
3952 throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)" );
3953 }
3954 $this->trxSectionCancelCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3955 }
3956
3960 private function currentAtomicSectionId() {
3961 if ( $this->trxLevel() && $this->trxAtomicLevels ) {
3962 $levelInfo = end( $this->trxAtomicLevels );
3963
3964 return $levelInfo[1];
3965 }
3966
3967 return null;
3968 }
3969
3978 ) {
3979 foreach ( $this->trxPreCommitCallbacks as $key => $info ) {
3980 if ( $info[2] === $old ) {
3981 $this->trxPreCommitCallbacks[$key][2] = $new;
3982 }
3983 }
3984 foreach ( $this->trxIdleCallbacks as $key => $info ) {
3985 if ( $info[2] === $old ) {
3986 $this->trxIdleCallbacks[$key][2] = $new;
3987 }
3988 }
3989 foreach ( $this->trxEndCallbacks as $key => $info ) {
3990 if ( $info[2] === $old ) {
3991 $this->trxEndCallbacks[$key][2] = $new;
3992 }
3993 }
3994 foreach ( $this->trxSectionCancelCallbacks as $key => $info ) {
3995 if ( $info[2] === $old ) {
3996 $this->trxSectionCancelCallbacks[$key][2] = $new;
3997 }
3998 }
3999 }
4000
4021 array $sectionIds, AtomicSectionIdentifier $newSectionId = null
4022 ) {
4023 // Cancel the "on commit" callbacks owned by this savepoint
4024 $this->trxIdleCallbacks = array_filter(
4025 $this->trxIdleCallbacks,
4026 function ( $entry ) use ( $sectionIds ) {
4027 return !in_array( $entry[2], $sectionIds, true );
4028 }
4029 );
4030 $this->trxPreCommitCallbacks = array_filter(
4031 $this->trxPreCommitCallbacks,
4032 function ( $entry ) use ( $sectionIds ) {
4033 return !in_array( $entry[2], $sectionIds, true );
4034 }
4035 );
4036 // Make "on resolution" callbacks owned by this savepoint to perceive a rollback
4037 foreach ( $this->trxEndCallbacks as $key => $entry ) {
4038 if ( in_array( $entry[2], $sectionIds, true ) ) {
4039 $callback = $entry[0];
4040 $this->trxEndCallbacks[$key][0] = function () use ( $callback ) {
4041 return $callback( self::TRIGGER_ROLLBACK, $this );
4042 };
4043 // This "on resolution" callback no longer belongs to a section.
4044 $this->trxEndCallbacks[$key][2] = null;
4045 }
4046 }
4047 // Hoist callback ownership for section cancel callbacks to the new top section
4048 foreach ( $this->trxSectionCancelCallbacks as $key => $entry ) {
4049 if ( in_array( $entry[2], $sectionIds, true ) ) {
4050 $this->trxSectionCancelCallbacks[$key][2] = $newSectionId;
4051 }
4052 }
4053 }
4054
4055 final public function setTransactionListener( $name, callable $callback = null ) {
4056 if ( $callback ) {
4057 $this->trxRecurringCallbacks[$name] = $callback;
4058 } else {
4059 unset( $this->trxRecurringCallbacks[$name] );
4060 }
4061 }
4062
4071 final public function setTrxEndCallbackSuppression( $suppress ) {
4072 $this->trxEndCallbacksSuppressed = $suppress;
4073 }
4074
4085 public function runOnTransactionIdleCallbacks( $trigger ) {
4086 if ( $this->trxLevel() ) { // sanity
4087 throw new DBUnexpectedError( $this, __METHOD__ . ': a transaction is still open' );
4088 }
4089
4090 if ( $this->trxEndCallbacksSuppressed ) {
4091 return 0;
4092 }
4093
4094 $count = 0;
4095 $autoTrx = $this->getFlag( self::DBO_TRX ); // automatic begin() enabled?
4097 $e = null; // first exception
4098 do { // callbacks may add callbacks :)
4099 $callbacks = array_merge(
4100 $this->trxIdleCallbacks,
4101 $this->trxEndCallbacks // include "transaction resolution" callbacks
4102 );
4103 $this->trxIdleCallbacks = []; // consumed (and recursion guard)
4104 $this->trxEndCallbacks = []; // consumed (recursion guard)
4105
4106 // Only run trxSectionCancelCallbacks on rollback, not commit.
4107 // But always consume them.
4108 if ( $trigger === self::TRIGGER_ROLLBACK ) {
4109 $callbacks = array_merge( $callbacks, $this->trxSectionCancelCallbacks );
4110 }
4111 $this->trxSectionCancelCallbacks = []; // consumed (recursion guard)
4112
4113 foreach ( $callbacks as $callback ) {
4114 ++$count;
4115 list( $phpCallback ) = $callback;
4116 $this->clearFlag( self::DBO_TRX ); // make each query its own transaction
4117 try {
4118 call_user_func( $phpCallback, $trigger, $this );
4119 } catch ( Throwable $ex ) {
4120 call_user_func( $this->errorLogger, $ex );
4121 $e = $e ?: $ex;
4122 // Some callbacks may use startAtomic/endAtomic, so make sure
4123 // their transactions are ended so other callbacks don't fail
4124 if ( $this->trxLevel() ) {
4125 $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
4126 }
4127 } finally {
4128 if ( $autoTrx ) {
4129 $this->setFlag( self::DBO_TRX ); // restore automatic begin()
4130 } else {
4131 $this->clearFlag( self::DBO_TRX ); // restore auto-commit
4132 }
4133 }
4134 }
4135 // @phan-suppress-next-line PhanImpossibleConditionInLoop
4136 } while ( count( $this->trxIdleCallbacks ) );
4137
4138 if ( $e instanceof Throwable ) {
4139 throw $e; // re-throw any first exception
4140 }
4141
4142 return $count;
4143 }
4144
4155 $count = 0;
4156
4157 $e = null; // first exception
4158 do { // callbacks may add callbacks :)
4159 $callbacks = $this->trxPreCommitCallbacks;
4160 $this->trxPreCommitCallbacks = []; // consumed (and recursion guard)
4161 foreach ( $callbacks as $callback ) {
4162 try {
4163 ++$count;
4164 list( $phpCallback ) = $callback;
4165 // @phan-suppress-next-line PhanUndeclaredInvokeInCallable
4166 $phpCallback( $this );
4167 } catch ( Throwable $ex ) {
4168 ( $this->errorLogger )( $ex );
4169 $e = $e ?: $ex;
4170 }
4171 }
4172 // @phan-suppress-next-line PhanImpossibleConditionInLoop
4173 } while ( count( $this->trxPreCommitCallbacks ) );
4174
4175 if ( $e instanceof Throwable ) {
4176 throw $e; // re-throw any first exception
4177 }
4178
4179 return $count;
4180 }
4181
4190 $trigger, array $sectionIds = null
4191 ) {
4193 $e = null; // first exception
4194
4195 $notCancelled = [];
4196 do {
4198 $this->trxSectionCancelCallbacks = []; // consumed (recursion guard)
4199 foreach ( $callbacks as $entry ) {
4200 if ( $sectionIds === null || in_array( $entry[2], $sectionIds, true ) ) {
4201 try {
4202 // @phan-suppress-next-line PhanUndeclaredInvokeInCallable
4203 $entry[0]( $trigger, $this );
4204 } catch ( Throwable $ex ) {
4205 ( $this->errorLogger )( $ex );
4206 $e = $e ?: $ex;
4207 }
4208 } else {
4209 $notCancelled[] = $entry;
4210 }
4211 }
4212 // @phan-suppress-next-line PhanImpossibleConditionInLoop
4213 } while ( count( $this->trxSectionCancelCallbacks ) );
4214 $this->trxSectionCancelCallbacks = $notCancelled;
4215
4216 if ( $e !== null ) {
4217 throw $e; // re-throw any first Throwable
4218 }
4219 }
4220
4230 public function runTransactionListenerCallbacks( $trigger ) {
4231 if ( $this->trxEndCallbacksSuppressed ) {
4232 return;
4233 }
4234
4236 $e = null; // first exception
4237
4238 foreach ( $this->trxRecurringCallbacks as $phpCallback ) {
4239 try {
4240 $phpCallback( $trigger, $this );
4241 } catch ( Throwable $ex ) {
4242 ( $this->errorLogger )( $ex );
4243 $e = $e ?: $ex;
4244 }
4245 }
4246
4247 if ( $e instanceof Throwable ) {
4248 throw $e; // re-throw any first exception
4249 }
4250 }
4251
4263 protected function doSavepoint( $identifier, $fname ) {
4264 $sql = 'SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
4265 $this->query( $sql, $fname, self::QUERY_CHANGE_TRX );
4266 }
4267
4279 protected function doReleaseSavepoint( $identifier, $fname ) {
4280 $sql = 'RELEASE SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
4281 $this->query( $sql, $fname, self::QUERY_CHANGE_TRX );
4282 }
4283
4295 protected function doRollbackToSavepoint( $identifier, $fname ) {
4296 $sql = 'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
4297 $this->query( $sql, $fname, self::QUERY_CHANGE_TRX );
4298 }
4299
4304 private function nextSavepointId( $fname ) {
4305 $savepointId = self::$SAVEPOINT_PREFIX . ++$this->trxAtomicCounter;
4306 if ( strlen( $savepointId ) > 30 ) {
4307 // 30 == Oracle's identifier length limit (pre 12c)
4308 // With a 22 character prefix, that puts the highest number at 99999999.
4309 throw new DBUnexpectedError(
4310 $this,
4311 'There have been an excessively large number of atomic sections in a transaction'
4312 . " started by $this->trxFname (at $fname)"
4313 );
4314 }
4315
4316 return $savepointId;
4317 }
4318
4319 final public function startAtomic(
4320 $fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE
4321 ) {
4322 $savepointId = $cancelable === self::ATOMIC_CANCELABLE ? self::$NOT_APPLICABLE : null;
4323
4324 if ( !$this->trxLevel() ) {
4325 $this->begin( $fname, self::TRANSACTION_INTERNAL ); // sets trxAutomatic
4326 // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
4327 // in all changes being in one transaction to keep requests transactional.
4328 if ( $this->getFlag( self::DBO_TRX ) ) {
4329 // Since writes could happen in between the topmost atomic sections as part
4330 // of the transaction, those sections will need savepoints.
4331 $savepointId = $this->nextSavepointId( $fname );
4332 $this->doSavepoint( $savepointId, $fname );
4333 } else {
4334 $this->trxAutomaticAtomic = true;
4335 }
4336 } elseif ( $cancelable === self::ATOMIC_CANCELABLE ) {
4337 $savepointId = $this->nextSavepointId( $fname );
4338 $this->doSavepoint( $savepointId, $fname );
4339 }
4340
4341 $sectionId = new AtomicSectionIdentifier;
4342 $this->trxAtomicLevels[] = [ $fname, $sectionId, $savepointId ];
4343 $this->queryLogger->debug( 'startAtomic: entering level ' .
4344 ( count( $this->trxAtomicLevels ) - 1 ) . " ($fname)" );
4345
4346 return $sectionId;
4347 }
4348
4349 final public function endAtomic( $fname = __METHOD__ ) {
4350 if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
4351 throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)" );
4352 }
4353
4354 // Check if the current section matches $fname
4355 $pos = count( $this->trxAtomicLevels ) - 1;
4356 list( $savedFname, $sectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
4357 $this->queryLogger->debug( "endAtomic: leaving level $pos ($fname)" );
4358
4359 if ( $savedFname !== $fname ) {
4360 throw new DBUnexpectedError(
4361 $this,
4362 "Invalid atomic section ended (got $fname but expected $savedFname)"
4363 );
4364 }
4365
4366 // Remove the last section (no need to re-index the array)
4367 array_pop( $this->trxAtomicLevels );
4368
4369 if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
4370 $this->commit( $fname, self::FLUSHING_INTERNAL );
4371 } elseif ( $savepointId !== null && $savepointId !== self::$NOT_APPLICABLE ) {
4372 $this->doReleaseSavepoint( $savepointId, $fname );
4373 }
4374
4375 // Hoist callback ownership for callbacks in the section that just ended;
4376 // all callbacks should have an owner that is present in trxAtomicLevels.
4377 $currentSectionId = $this->currentAtomicSectionId();
4378 if ( $currentSectionId ) {
4379 $this->reassignCallbacksForSection( $sectionId, $currentSectionId );
4380 }
4381 }
4382
4383 final public function cancelAtomic(
4384 $fname = __METHOD__, AtomicSectionIdentifier $sectionId = null
4385 ) {
4386 if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
4387 throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)" );
4388 }
4389
4390 $excisedIds = [];
4391 $newTopSection = $this->currentAtomicSectionId();
4392 try {
4393 $excisedFnames = [];
4394 if ( $sectionId !== null ) {
4395 // Find the (last) section with the given $sectionId
4396 $pos = -1;
4397 foreach ( $this->trxAtomicLevels as $i => list( $asFname, $asId, $spId ) ) {
4398 if ( $asId === $sectionId ) {
4399 $pos = $i;
4400 }
4401 }
4402 if ( $pos < 0 ) {
4403 throw new DBUnexpectedError( $this, "Atomic section not found (for $fname)" );
4404 }
4405 // Remove all descendant sections and re-index the array
4406 $len = count( $this->trxAtomicLevels );
4407 for ( $i = $pos + 1; $i < $len; ++$i ) {
4408 $excisedFnames[] = $this->trxAtomicLevels[$i][0];
4409 $excisedIds[] = $this->trxAtomicLevels[$i][1];
4410 }
4411 $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 );
4412 $newTopSection = $this->currentAtomicSectionId();
4413 }
4414
4415 // Check if the current section matches $fname
4416 $pos = count( $this->trxAtomicLevels ) - 1;
4417 list( $savedFname, $savedSectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
4418
4419 if ( $excisedFnames ) {
4420 $this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname) " .
4421 "and descendants " . implode( ', ', $excisedFnames ) );
4422 } else {
4423 $this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname)" );
4424 }
4425
4426 if ( $savedFname !== $fname ) {
4427 throw new DBUnexpectedError(
4428 $this,
4429 "Invalid atomic section ended (got $fname but expected $savedFname)"
4430 );
4431 }
4432
4433 // Remove the last section (no need to re-index the array)
4434 array_pop( $this->trxAtomicLevels );
4435 $excisedIds[] = $savedSectionId;
4436 $newTopSection = $this->currentAtomicSectionId();
4437
4438 if ( $savepointId !== null ) {
4439 // Rollback the transaction to the state just before this atomic section
4440 if ( $savepointId === self::$NOT_APPLICABLE ) {
4441 $this->rollback( $fname, self::FLUSHING_INTERNAL );
4442 // Note: rollback() will run trxSectionCancelCallbacks
4443 } else {
4444 $this->doRollbackToSavepoint( $savepointId, $fname );
4445 $this->trxStatus = self::STATUS_TRX_OK; // no exception; recovered
4446 $this->trxStatusIgnoredCause = null;
4447
4448 // Run trxSectionCancelCallbacks now.
4449 $this->runOnAtomicSectionCancelCallbacks( self::TRIGGER_CANCEL, $excisedIds );
4450 }
4451 } elseif ( $this->trxStatus > self::STATUS_TRX_ERROR ) {
4452 // Put the transaction into an error state if it's not already in one
4453 $this->trxStatus = self::STATUS_TRX_ERROR;
4454 $this->trxStatusCause = new DBUnexpectedError(
4455 $this,
4456 "Uncancelable atomic section canceled (got $fname)"
4457 );
4458 }
4459 } finally {
4460 // Fix up callbacks owned by the sections that were just cancelled.
4461 // All callbacks should have an owner that is present in trxAtomicLevels.
4462 $this->modifyCallbacksForCancel( $excisedIds, $newTopSection );
4463 }
4464
4465 $this->affectedRowCount = 0; // for the sake of consistency
4466 }
4467
4468 final public function doAtomicSection(
4469 $fname, callable $callback, $cancelable = self::ATOMIC_NOT_CANCELABLE
4470 ) {
4471 $sectionId = $this->startAtomic( $fname, $cancelable );
4472 try {
4473 $res = $callback( $this, $fname );
4474 } catch ( Throwable $e ) {
4475 $this->cancelAtomic( $fname, $sectionId );
4476
4477 throw $e;
4478 }
4479 $this->endAtomic( $fname );
4480
4481 return $res;
4482 }
4483
4484 final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
4485 static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
4486 if ( !in_array( $mode, $modes, true ) ) {
4487 throw new DBUnexpectedError( $this, "$fname: invalid mode parameter '$mode'" );
4488 }
4489
4490 // Protect against mismatched atomic section, transaction nesting, and snapshot loss
4491 if ( $this->trxLevel() ) {
4492 if ( $this->trxAtomicLevels ) {
4493 $levels = $this->flatAtomicSectionList();
4494 $msg = "$fname: got explicit BEGIN while atomic section(s) $levels are open";
4495 throw new DBUnexpectedError( $this, $msg );
4496 } elseif ( !$this->trxAutomatic ) {
4497 $msg = "$fname: explicit transaction already active (from {$this->trxFname})";
4498 throw new DBUnexpectedError( $this, $msg );
4499 } else {
4500 $msg = "$fname: implicit transaction already active (from {$this->trxFname})";
4501 throw new DBUnexpectedError( $this, $msg );
4502 }
4503 } elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
4504 $msg = "$fname: implicit transaction expected (DBO_TRX set)";
4505 throw new DBUnexpectedError( $this, $msg );
4506 }
4507
4509
4510 $this->doBegin( $fname );
4511 $this->trxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
4512 $this->trxStatus = self::STATUS_TRX_OK;
4513 $this->trxStatusIgnoredCause = null;
4514 $this->trxAtomicCounter = 0;
4515 $this->trxTimestamp = microtime( true );
4516 $this->trxFname = $fname;
4517 $this->trxDoneWrites = false;
4518 $this->trxAutomaticAtomic = false;
4519 $this->trxAtomicLevels = [];
4520 $this->trxWriteDuration = 0.0;
4521 $this->trxWriteQueryCount = 0;
4522 $this->trxWriteAffectedRows = 0;
4523 $this->trxWriteAdjDuration = 0.0;
4524 $this->trxWriteAdjQueryCount = 0;
4525 $this->trxWriteCallers = [];
4526 // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
4527 // Get an estimate of the replication lag before any such queries.
4528 $this->trxReplicaLag = null; // clear cached value first
4529 $this->trxReplicaLag = $this->getApproximateLagStatus()['lag'];
4530 // T147697: make explicitTrxActive() return true until begin() finishes. This way, no
4531 // caller will think its OK to muck around with the transaction just because startAtomic()
4532 // has not yet completed (e.g. setting trxAtomicLevels).
4533 $this->trxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
4534 }
4535
4544 protected function doBegin( $fname ) {
4545 $this->query( 'BEGIN', $fname, self::QUERY_CHANGE_TRX );
4546 }
4547
4548 final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4549 static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
4550 if ( !in_array( $flush, $modes, true ) ) {
4551 throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'" );
4552 }
4553
4554 if ( $this->trxLevel() && $this->trxAtomicLevels ) {
4555 // There are still atomic sections open; this cannot be ignored
4556 $levels = $this->flatAtomicSectionList();
4557 throw new DBUnexpectedError(
4558 $this,
4559 "$fname: got COMMIT while atomic sections $levels are still open"
4560 );
4561 }
4562
4563 if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
4564 if ( !$this->trxLevel() ) {
4565 return; // nothing to do
4566 } elseif ( !$this->trxAutomatic ) {
4567 throw new DBUnexpectedError(
4568 $this,
4569 "$fname: flushing an explicit transaction, getting out of sync"
4570 );
4571 }
4572 } elseif ( !$this->trxLevel() ) {
4573 $this->queryLogger->error(
4574 "$fname: no transaction to commit, something got out of sync" );
4575 return; // nothing to do
4576 } elseif ( $this->trxAutomatic ) {
4577 throw new DBUnexpectedError(
4578 $this,
4579 "$fname: expected mass commit of all peer transactions (DBO_TRX set)"
4580 );
4581 }
4582
4584
4586
4587 $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
4588 $this->doCommit( $fname );
4589 $oldTrxShortId = $this->consumeTrxShortId();
4590 $this->trxStatus = self::STATUS_TRX_NONE;
4591
4592 if ( $this->trxDoneWrites ) {
4593 $this->lastWriteTime = microtime( true );
4594 $this->trxProfiler->transactionWritingOut(
4595 $this->server,
4596 $this->getDomainID(),
4597 $oldTrxShortId,
4598 $writeTime,
4599 $this->trxWriteAffectedRows
4600 );
4601 }
4602
4603 // With FLUSHING_ALL_PEERS, callbacks will be explicitly run later
4604 if ( $flush !== self::FLUSHING_ALL_PEERS ) {
4605 $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
4606 $this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
4607 }
4608 }
4609
4618 protected function doCommit( $fname ) {
4619 if ( $this->trxLevel() ) {
4620 $this->query( 'COMMIT', $fname, self::QUERY_CHANGE_TRX );
4621 }
4622 }
4623
4624 final public function rollback( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4625 $trxActive = $this->trxLevel();
4626
4627 if ( $flush !== self::FLUSHING_INTERNAL
4628 && $flush !== self::FLUSHING_ALL_PEERS
4629 && $this->getFlag( self::DBO_TRX )
4630 ) {
4631 throw new DBUnexpectedError(
4632 $this,
4633 "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)"
4634 );
4635 }
4636
4637 if ( $trxActive ) {
4639
4640 $this->doRollback( $fname );
4641 $oldTrxShortId = $this->consumeTrxShortId();
4642 $this->trxStatus = self::STATUS_TRX_NONE;
4643 $this->trxAtomicLevels = [];
4644 // Estimate the RTT via a query now that trxStatus is OK
4645 $writeTime = $this->pingAndCalculateLastTrxApplyTime();
4646
4647 if ( $this->trxDoneWrites ) {
4648 $this->trxProfiler->transactionWritingOut(
4649 $this->server,
4650 $this->getDomainID(),
4651 $oldTrxShortId,
4652 $writeTime,
4653 $this->trxWriteAffectedRows
4654 );
4655 }
4656 }
4657
4658 // Clear any commit-dependant callbacks. They might even be present
4659 // only due to transaction rounds, with no SQL transaction being active
4660 $this->trxIdleCallbacks = [];
4661 $this->trxPreCommitCallbacks = [];
4662
4663 // With FLUSHING_ALL_PEERS, callbacks will be explicitly run later
4664 if ( $trxActive && $flush !== self::FLUSHING_ALL_PEERS ) {
4665 try {
4666 $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
4667 } catch ( Throwable $e ) {
4668 // already logged; finish and let LoadBalancer move on during mass-rollback
4669 }
4670 try {
4671 $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
4672 } catch ( Throwable $e ) {
4673 // already logged; let LoadBalancer move on during mass-rollback
4674 }
4675
4676 $this->affectedRowCount = 0; // for the sake of consistency
4677 }
4678 }
4679
4688 protected function doRollback( $fname ) {
4689 if ( $this->trxLevel() ) {
4690 # Disconnects cause rollback anyway, so ignore those errors
4691 $this->query( 'ROLLBACK', $fname, self::QUERY_SILENCE_ERRORS | self::QUERY_CHANGE_TRX );
4692 }
4693 }
4694
4695 public function flushSnapshot( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4696 if ( $this->explicitTrxActive() ) {
4697 // Committing this transaction would break callers that assume it is still open
4698 throw new DBUnexpectedError(
4699 $this,
4700 "$fname: Cannot flush snapshot; " .
4701 "explicit transaction '{$this->trxFname}' is still open"
4702 );
4703 } elseif ( $this->writesOrCallbacksPending() ) {
4704 // This only flushes transactions to clear snapshots, not to write data
4705 $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
4706 throw new DBUnexpectedError(
4707 $this,
4708 "$fname: Cannot flush snapshot; " .
4709 "writes from transaction {$this->trxFname} are still pending ($fnames)"
4710 );
4711 } elseif (
4712 $this->trxLevel() &&
4713 $this->getTransactionRoundId() &&
4714 $flush !== self::FLUSHING_INTERNAL &&
4715 $flush !== self::FLUSHING_ALL_PEERS
4716 ) {
4717 $this->queryLogger->warning(
4718 "$fname: Expected mass snapshot flush of all peer transactions " .
4719 "in the explicit transactions round '{$this->getTransactionRoundId()}'",
4720 [ 'exception' => new RuntimeException() ]
4721 );
4722 }
4723
4724 $this->commit( $fname, self::FLUSHING_INTERNAL );
4725 }
4726
4727 public function explicitTrxActive() {
4728 return $this->trxLevel() && ( $this->trxAtomicLevels || !$this->trxAutomatic );
4729 }
4730
4736 $oldName, $newName, $temporary = false, $fname = __METHOD__
4737 ) {
4738 throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
4739 }
4740
4745 public function listTables( $prefix = null, $fname = __METHOD__ ) {
4746 throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
4747 }
4748
4753 public function listViews( $prefix = null, $fname = __METHOD__ ) {
4754 throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
4755 }
4756
4761 public function timestamp( $ts = 0 ) {
4762 $t = new ConvertibleTimestamp( $ts );
4763 // Let errors bubble up to avoid putting garbage in the DB
4764 return $t->getTimestamp( TS_MW );
4765 }
4766
4767 public function timestampOrNull( $ts = null ) {
4768 if ( $ts === null ) {
4769 return null;
4770 } else {
4771 return $this->timestamp( $ts );
4772 }
4773 }
4774
4775 public function affectedRows() {
4776 return ( $this->affectedRowCount === null )
4777 ? $this->fetchAffectedRowCount() // default to driver value
4779 }
4780
4784 abstract protected function fetchAffectedRowCount();
4785
4798 protected function resultObject( $result ) {
4799 if ( !$result ) {
4800 return false; // failed query
4801 } elseif ( $result instanceof IResultWrapper ) {
4802 return $result;
4803 } elseif ( $result === true ) {
4804 return $result; // successful write query
4805 } else {
4806 return new ResultWrapper( $this, $result );
4807 }
4808 }
4809
4810 public function ping( &$rtt = null ) {
4811 // Avoid hitting the server if it was hit recently
4812 if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::$PING_TTL ) {
4813 if ( !func_num_args() || $this->lastRoundTripEstimate > 0 ) {
4815 return true; // don't care about $rtt
4816 }
4817 }
4818
4819 // This will reconnect if possible or return false if not
4820 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_SILENCE_ERRORS | self::QUERY_CHANGE_NONE;
4821 $ok = ( $this->query( self::$PING_QUERY, __METHOD__, $flags ) !== false );
4822 if ( $ok ) {
4824 }
4825
4826 return $ok;
4827 }
4828
4835 protected function replaceLostConnection( $fname ) {
4836 $this->closeConnection();
4837 $this->conn = null;
4838
4840
4841 try {
4842 $this->open(
4843 $this->server,
4844 $this->user,
4845 $this->password,
4846 $this->currentDomain->getDatabase(),
4847 $this->currentDomain->getSchema(),
4848 $this->tablePrefix()
4849 );
4850 $this->lastPing = microtime( true );
4851 $ok = true;
4852
4853 $this->connLogger->warning(
4854 $fname . ': lost connection to {dbserver}; reconnected',
4855 [
4856 'dbserver' => $this->getServer(),
4857 'exception' => new RuntimeException()
4858 ]
4859 );
4860 } catch ( DBConnectionError $e ) {
4861 $ok = false;
4862
4863 $this->connLogger->error(
4864 $fname . ': lost connection to {dbserver} permanently',
4865 [ 'dbserver' => $this->getServer() ]
4866 );
4867 }
4868
4870
4871 return $ok;
4872 }
4873
4874 public function getSessionLagStatus() {
4875 return $this->getRecordedTransactionLagStatus() ?: $this->getApproximateLagStatus();
4876 }
4877
4891 final protected function getRecordedTransactionLagStatus() {
4892 return ( $this->trxLevel() && $this->trxReplicaLag !== null )
4893 ? [ 'lag' => $this->trxReplicaLag, 'since' => $this->trxTimestamp() ]
4894 : null;
4895 }
4896
4906 protected function getApproximateLagStatus() {
4907 return [
4908 'lag' => ( $this->topologyRole === self::ROLE_STREAMING_REPLICA ) ? $this->getLag() : 0,
4909 'since' => microtime( true )
4910 ];
4911 }
4912
4932 public static function getCacheSetOptions( IDatabase $db1, IDatabase $db2 = null ) {
4933 $res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
4934 foreach ( func_get_args() as $db ) {
4936 $status = $db->getSessionLagStatus();
4937 if ( $status['lag'] === false ) {
4938 $res['lag'] = false;
4939 } elseif ( $res['lag'] !== false ) {
4940 $res['lag'] = max( $res['lag'], $status['lag'] );
4941 }
4942 $res['since'] = min( $res['since'], $status['since'] );
4943 $res['pending'] = $res['pending'] ?: $db->writesPending();
4944 }
4945
4946 return $res;
4947 }
4948
4949 public function getLag() {
4950 if ( $this->topologyRole === self::ROLE_STREAMING_MASTER ) {
4951 return 0; // this is the master
4952 } elseif ( $this->topologyRole === self::ROLE_STATIC_CLONE ) {
4953 return 0; // static dataset
4954 }
4955
4956 return $this->doGetLag();
4957 }
4958
4963 protected function doGetLag() {
4964 return 0;
4965 }
4966
4971 public function maxListLen() {
4972 return 0;
4973 }
4974
4979 public function encodeBlob( $b ) {
4980 return $b;
4981 }
4982
4987 public function decodeBlob( $b ) {
4988 if ( $b instanceof Blob ) {
4989 $b = $b->fetch();
4990 }
4991 return $b;
4992 }
4993
4998 public function setSessionOptions( array $options ) {
4999 }
5000
5001 public function sourceFile(
5002 $filename,
5003 callable $lineCallback = null,
5004 callable $resultCallback = null,
5005 $fname = false,
5006 callable $inputCallback = null
5007 ) {
5008 AtEase::suppressWarnings();
5009 $fp = fopen( $filename, 'r' );
5010 AtEase::restoreWarnings();
5011
5012 if ( $fp === false ) {
5013 throw new RuntimeException( "Could not open \"{$filename}\"" );
5014 }
5015
5016 if ( !$fname ) {
5017 $fname = __METHOD__ . "( $filename )";
5018 }
5019
5020 try {
5021 $error = $this->sourceStream(
5022 $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
5023 } catch ( Throwable $e ) {
5024 fclose( $fp );
5025 throw $e;
5026 }
5027
5028 fclose( $fp );
5029
5030 return $error;
5031 }
5032
5033 public function setSchemaVars( $vars ) {
5034 $this->schemaVars = is_array( $vars ) ? $vars : null;
5035 }
5036
5037 public function sourceStream(
5038 $fp,
5039 callable $lineCallback = null,
5040 callable $resultCallback = null,
5041 $fname = __METHOD__,
5042 callable $inputCallback = null
5043 ) {
5044 $delimiterReset = new ScopedCallback(
5045 function ( $delimiter ) {
5046 $this->delimiter = $delimiter;
5047 },
5049 );
5050 $cmd = '';
5051
5052 while ( !feof( $fp ) ) {
5053 if ( $lineCallback ) {
5054 call_user_func( $lineCallback );
5055 }
5056
5057 $line = trim( fgets( $fp ) );
5058
5059 if ( $line == '' ) {
5060 continue;
5061 }
5062
5063 if ( $line[0] == '-' && $line[1] == '-' ) {
5064 continue;
5065 }
5066
5067 if ( $cmd != '' ) {
5068 $cmd .= ' ';
5069 }
5070
5071 $done = $this->streamStatementEnd( $cmd, $line );
5072
5073 $cmd .= "$line\n";
5074
5075 if ( $done || feof( $fp ) ) {
5076 $cmd = $this->replaceVars( $cmd );
5077
5078 if ( $inputCallback ) {
5079 $callbackResult = $inputCallback( $cmd );
5080
5081 if ( is_string( $callbackResult ) || !$callbackResult ) {
5082 $cmd = $callbackResult;
5083 }
5084 }
5085
5086 if ( $cmd ) {
5087 $res = $this->query( $cmd, $fname );
5088
5089 if ( $resultCallback ) {
5090 $resultCallback( $res, $this );
5091 }
5092
5093 if ( $res === false ) {
5094 $err = $this->lastError();
5095
5096 return "Query \"{$cmd}\" failed with error code \"$err\".\n";
5097 }
5098 }
5099 $cmd = '';
5100 }
5101 }
5102
5103 ScopedCallback::consume( $delimiterReset );
5104 return true;
5105 }
5106
5115 public function streamStatementEnd( &$sql, &$newLine ) {
5116 if ( $this->delimiter ) {
5117 $prev = $newLine;
5118 $newLine = preg_replace(
5119 '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
5120 if ( $newLine != $prev ) {
5121 return true;
5122 }
5123 }
5124
5125 return false;
5126 }
5127
5149 protected function replaceVars( $ins ) {
5150 $vars = $this->getSchemaVars();
5151 return preg_replace_callback(
5152 '!
5153 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
5154 \'\{\$ (\w+) }\' | # 3. addQuotes
5155 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
5156 /\*\$ (\w+) \*/ # 5. leave unencoded
5157 !x',
5158 function ( $m ) use ( $vars ) {
5159 // Note: Because of <https://bugs.php.net/bug.php?id=51881>,
5160 // check for both nonexistent keys *and* the empty string.
5161 if ( isset( $m[1] ) && $m[1] !== '' ) {
5162 if ( $m[1] === 'i' ) {
5163 return $this->indexName( $m[2] );
5164 } else {
5165 return $this->tableName( $m[2] );
5166 }
5167 } elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
5168 return $this->addQuotes( $vars[$m[3]] );
5169 } elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
5170 return $this->addIdentifierQuotes( $vars[$m[4]] );
5171 } elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
5172 return $vars[$m[5]];
5173 } else {
5174 return $m[0];
5175 }
5176 },
5177 $ins
5178 );
5179 }
5180
5187 protected function getSchemaVars() {
5188 return $this->schemaVars ?? $this->getDefaultSchemaVars();
5189 }
5190
5200 protected function getDefaultSchemaVars() {
5201 return [];
5202 }
5203
5208 public function lockIsFree( $lockName, $method ) {
5209 // RDBMs methods for checking named locks may or may not count this thread itself.
5210 // In MySQL, IS_FREE_LOCK() returns 0 if the thread already has the lock. This is
5211 // the behavior chosen by the interface for this method.
5212 return !isset( $this->sessionNamedLocks[$lockName] );
5213 }
5214
5219 public function lock( $lockName, $method, $timeout = 5 ) {
5220 $this->sessionNamedLocks[$lockName] = 1;
5221
5222 return true;
5223 }
5224
5229 public function unlock( $lockName, $method ) {
5230 unset( $this->sessionNamedLocks[$lockName] );
5231
5232 return true;
5233 }
5234
5235 public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
5236 if ( $this->writesOrCallbacksPending() ) {
5237 // This only flushes transactions to clear snapshots, not to write data
5238 $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
5239 throw new DBUnexpectedError(
5240 $this,
5241 "$fname: Cannot flush pre-lock snapshot; " .
5242 "writes from transaction {$this->trxFname} are still pending ($fnames)"
5243 );
5244 }
5245
5246 if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
5247 return null;
5248 }
5249
5250 $unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
5251 if ( $this->trxLevel() ) {
5252 // There is a good chance an exception was thrown, causing any early return
5253 // from the caller. Let any error handler get a chance to issue rollback().
5254 // If there isn't one, let the error bubble up and trigger server-side rollback.
5256 function () use ( $lockKey, $fname ) {
5257 $this->unlock( $lockKey, $fname );
5258 },
5259 $fname
5260 );
5261 } else {
5262 $this->unlock( $lockKey, $fname );
5263 }
5264 } );
5265
5266 $this->commit( $fname, self::FLUSHING_INTERNAL );
5267
5268 return $unlocker;
5269 }
5270
5275 public function namedLocksEnqueue() {
5276 return false;
5277 }
5278
5280 return true;
5281 }
5282
5283 final public function lockTables( array $read, array $write, $method ) {
5284 if ( $this->writesOrCallbacksPending() ) {
5285 throw new DBUnexpectedError( $this, "Transaction writes or callbacks still pending" );
5286 }
5287
5288 if ( $this->tableLocksHaveTransactionScope() ) {
5289 $this->startAtomic( $method );
5290 }
5291
5292 return $this->doLockTables( $read, $write, $method );
5293 }
5294
5304 protected function doLockTables( array $read, array $write, $method ) {
5305 return true;
5306 }
5307
5308 final public function unlockTables( $method ) {
5309 if ( $this->tableLocksHaveTransactionScope() ) {
5310 $this->endAtomic( $method );
5311
5312 return true; // locks released on COMMIT/ROLLBACK
5313 }
5314
5315 return $this->doUnlockTables( $method );
5316 }
5317
5325 protected function doUnlockTables( $method ) {
5326 return true;
5327 }
5328
5329 public function dropTable( $table, $fname = __METHOD__ ) {
5330 if ( !$this->tableExists( $table, $fname ) ) {
5331 return false;
5332 }
5333
5334 $this->doDropTable( $table, $fname );
5335
5336 return true;
5337 }
5338
5345 protected function doDropTable( $table, $fname ) {
5346 // https://mariadb.com/kb/en/drop-table/
5347 // https://dev.mysql.com/doc/refman/8.0/en/drop-table.html
5348 // https://www.postgresql.org/docs/9.2/sql-truncate.html
5349 $sql = "DROP TABLE " . $this->tableName( $table ) . " CASCADE";
5350 $this->query( $sql, $fname, self::QUERY_IGNORE_DBO_TRX );
5351 }
5352
5353 public function truncate( $tables, $fname = __METHOD__ ) {
5354 $tables = is_array( $tables ) ? $tables : [ $tables ];
5355
5356 $tablesTruncate = [];
5357 foreach ( $tables as $table ) {
5358 // Skip TEMPORARY tables with no writes nor sequence updates detected.
5359 // This mostly is an optimization for integration testing.
5360 if ( !$this->isPristineTemporaryTable( $table ) ) {
5361 $tablesTruncate[] = $table;
5362 }
5363 }
5364
5365 if ( $tablesTruncate ) {
5366 $this->doTruncate( $tablesTruncate, $fname );
5367 }
5368 }
5369
5376 protected function doTruncate( array $tables, $fname ) {
5377 foreach ( $tables as $table ) {
5378 $sql = "TRUNCATE TABLE " . $this->tableName( $table );
5379 $this->query( $sql, $fname, self::QUERY_CHANGE_SCHEMA );
5380 }
5381 }
5382
5387 public function getInfinity() {
5388 return 'infinity';
5389 }
5390
5391 public function encodeExpiry( $expiry ) {
5392 return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
5393 ? $this->getInfinity()
5394 : $this->timestamp( $expiry );
5395 }
5396
5397 public function decodeExpiry( $expiry, $format = TS_MW ) {
5398 if ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) {
5399 return 'infinity';
5400 }
5401
5402 return ConvertibleTimestamp::convert( $format, $expiry );
5403 }
5404
5409 public function setBigSelects( $value = true ) {
5410 // no-op
5411 }
5412
5413 public function isReadOnly() {
5414 return ( $this->getReadOnlyReason() !== false );
5415 }
5416
5420 protected function getReadOnlyReason() {
5421 if ( $this->topologyRole === self::ROLE_STREAMING_REPLICA ) {
5422 return [ 'Server is configured as a read-only replica database.', 'role' ];
5423 } elseif ( $this->topologyRole === self::ROLE_STATIC_CLONE ) {
5424 return [ 'Server is configured as a read-only static clone database.', 'role' ];
5425 }
5426
5427 $reason = $this->getLBInfo( self::LB_READ_ONLY_REASON );
5428 if ( is_string( $reason ) ) {
5429 return [ $reason, 'lb' ];
5430 }
5431
5432 return false;
5433 }
5434
5439 public function setTableAliases( array $aliases ) {
5440 $this->tableAliases = $aliases;
5441 }
5442
5447 public function setIndexAliases( array $aliases ) {
5448 $this->indexAliases = $aliases;
5449 }
5450
5457 final protected function fieldHasBit( $field, $flags ) {
5458 return ( ( $field & $flags ) === $flags );
5459 }
5460
5473 protected function getBindingHandle() {
5474 if ( !$this->conn ) {
5475 throw new DBUnexpectedError(
5476 $this,
5477 'DB connection was already closed or the connection dropped'
5478 );
5479 }
5480
5481 return $this->conn;
5482 }
5483
5484 public function __toString() {
5485 // spl_object_id is PHP >= 7.2
5486 $id = function_exists( 'spl_object_id' )
5487 ? spl_object_id( $this )
5488 : spl_object_hash( $this );
5489
5490 $description = $this->getType() . ' object #' . $id;
5491 // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.is_resource
5492 if ( is_resource( $this->conn ) ) {
5493 $description .= ' (' . (string)$this->conn . ')'; // "resource id #<ID>"
5494 } elseif ( is_object( $this->conn ) ) {
5495 // spl_object_id is PHP >= 7.2
5496 $handleId = function_exists( 'spl_object_id' )
5497 ? spl_object_id( $this->conn )
5498 : spl_object_hash( $this->conn );
5499 $description .= " (handle id #$handleId)";
5500 }
5501
5502 return $description;
5503 }
5504
5509 public function __clone() {
5510 $this->connLogger->warning(
5511 "Cloning " . static::class . " is not recommended; forking connection",
5512 [ 'exception' => new RuntimeException() ]
5513 );
5514
5515 if ( $this->isOpen() ) {
5516 // Open a new connection resource without messing with the old one
5517 $this->conn = null;
5518 $this->trxEndCallbacks = []; // don't copy
5519 $this->trxSectionCancelCallbacks = []; // don't copy
5520 $this->handleSessionLossPreconnect(); // no trx or locks anymore
5521 $this->open(
5522 $this->server,
5523 $this->user,
5524 $this->password,
5525 $this->currentDomain->getDatabase(),
5526 $this->currentDomain->getSchema(),
5527 $this->tablePrefix()
5528 );
5529 $this->lastPing = microtime( true );
5530 }
5531 }
5532
5538 public function __sleep() {
5539 throw new RuntimeException( 'Database serialization may cause problems, since ' .
5540 'the connection is not restored on wakeup' );
5541 }
5542
5546 public function __destruct() {
5547 if ( $this->trxLevel() && $this->trxDoneWrites ) {
5548 trigger_error( "Uncommitted DB writes (transaction from {$this->trxFname})" );
5549 }
5550
5551 $danglingWriters = $this->pendingWriteAndCallbackCallers();
5552 if ( $danglingWriters ) {
5553 $fnames = implode( ', ', $danglingWriters );
5554 trigger_error( "DB transaction writes or callbacks still pending ($fnames)" );
5555 }
5556
5557 if ( $this->conn ) {
5558 // Avoid connection leaks for sanity. Normally, resources close at script completion.
5559 // The connection might already be closed in PHP by now, so suppress warnings.
5560 AtEase::suppressWarnings();
5561 $this->closeConnection();
5562 AtEase::restoreWarnings();
5563 $this->conn = null;
5564 }
5565 }
5566}
5567
5571class_alias( Database::class, 'DatabaseBase' );
5572
5576class_alias( Database::class, 'Database' );
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:71
Simple store for keeping values in an associative array for the current process.
Class used for token representing identifiers for atomic sections from IDatabase instances.
@newable Stable to extend
Definition Blob.php:9
@newable Stable to extend
Error thrown when a query times out.
Exception class for attempted DB write access to a DBConnRef with the DB_REPLICA role.
Class to handle database/schema/prefix specifications for IDatabase.
Relational database abstraction object.
Definition Database.php:50
bool $cliMode
Whether this PHP instance is for a CLI script.
Definition Database.php:85
getServerInfo()
Get a human-readable string describing the current software version.
Definition Database.php:545
lockTables(array $read, array $write, $method)
Lock specific tables.
encodeBlob( $b)
Some DBMSs have a special format for inserting into blob fields, they don't allow simple quoted strin...
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Wrapper to IDatabase::select() that only fetches one row (via LIMIT)
runOnTransactionPreCommitCallbacks()
Actually consume and run any "on transaction pre-commit" callbacks.
setLBInfo( $nameOrArray, $value=null)
Set the entire array or a particular key of the managing load balancer info array.
Definition Database.php:629
buildIntegerCast( $field)
string 1.31 Stable to override
static int $TEMP_NORMAL
Writes to this temporary table do not affect lastDoneWrites()
Definition Database.php:218
pendingWriteRowsAffected()
Get the number of affected rows from pending write queries.
Definition Database.php:735
integer null $affectedRowCount
Rows affected by the last query to query() or its CRUD wrappers.
Definition Database.php:177
makeInsertNonConflictingVerbAndOptions()
Stable to override.
doInsertSelectGeneric( $destTable, $srcTable, array $varMap, $conds, $fname, array $insertOptions, array $selectOptions, $selectJoinConds)
Implementation of insertSelect() based on select() and insert()
begin( $fname=__METHOD__, $mode=self::TRANSACTION_EXPLICIT)
Begin a transaction.
makeUpdateOptions( $options)
Make UPDATE options for the Database::update function.
string null $topologyRootMaster
Host (or address) of the root master server for the replication topology.
Definition Database.php:91
static int $DEADLOCK_DELAY_MAX
Maximum time to wait before retry.
Definition Database.php:227
deadlockLoop(... $args)
Perform a deadlock-prone transaction.This function invokes a callback function to perform a set of wr...
array[] $trxIdleCallbacks
List of (callable, method name, atomic section id)
Definition Database.php:161
static string $SAVEPOINT_PREFIX
Prefix to the atomic section counter used to make savepoint IDs.
Definition Database.php:215
restoreErrorHandler()
Restore the previous error handler and return the last PHP error for this DB.
Definition Database.php:861
getApproximateLagStatus()
Get a replica DB lag estimate for this server at the start of a transaction.
callable $errorLogger
Error logging callback.
Definition Database.php:60
bool $trxDoneWrites
Whether possible write queries were done in the last transaction started.
Definition Database.php:139
strencode( $s)
Wrapper for addslashes()
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...
relationSchemaQualifier()
Stable to override.
Definition Database.php:613
__toString()
Get a debugging string that mentions the database type, the ID of this instance, and the ID of any un...
static float $TINY_WRITE_SEC
Guess of how many seconds it takes to replicate a small insert.
Definition Database.php:235
int[] $priorFlags
Prior flags member variable values.
Definition Database.php:115
object resource null $conn
Database connection.
Definition Database.php:73
makeConditionCollidesUponKeys(array $rows, array $uniqueKeys)
assertQueryIsCurrentlyAllowed( $sql, $fname)
Error out if the DB is not in a valid state for a query via query()
doInitConnection()
Actually connect to the database over the wire (or to local files)
Definition Database.php:328
conditional( $cond, $trueVal, $falseVal)
Returns an SQL expression for a simple conditional.This doesn't need to be overridden unless CASE isn...
selectDB( $db)
Change the current database.
trxTimestamp()
Get the UNIX timestamp of the time that the transaction was established.
Definition Database.php:561
static int $DEADLOCK_TRIES
Number of times to re-try an operation in case of deadlock.
Definition Database.php:223
reassignCallbacksForSection(AtomicSectionIdentifier $old, AtomicSectionIdentifier $new)
Hoist callback ownership for callbacks in a section to a parent section.
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.string Stable to override
fieldNameWithAlias( $name, $alias=false)
Get an aliased field name e.g.
tablePrefix( $prefix=null)
Get/set the table prefix.
Definition Database.php:573
newSelectQueryBuilder()
Create an empty SelectQueryBuilder which can be used to run queries against this connection....
setLogger(LoggerInterface $logger)
Set the PSR-3 logger interface to use for query logging.
Definition Database.php:541
int $trxWriteQueryCount
Number of write queries for the current transaction.
Definition Database.php:153
selectFieldValues( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a list of single field values from result rows.
isInsertSelectSafe(array $insertOptions, array $selectOptions)
Stable to override.
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...
fieldNamesWithAlias( $fields)
Gets an array of aliased field names.
static int $DBO_MUTABLE
Bit field of all DBO_* flags that can be changed after connection.
Definition Database.php:249
getSessionLagStatus()
Get the replica DB lag when the current transaction started or a general lag estimate if not transact...
listTables( $prefix=null, $fname=__METHOD__)
List all tables on the database.array Stable to override
handleSessionLossPostconnect()
Clean things up after session (and thus transaction) loss after reconnect.
fieldExists( $table, $field, $fname=__METHOD__)
Determines whether a field exists in a table.
open( $server, $user, $password, $dbName, $schema, $tablePrefix)
Open a new connection to the database (closing any existing one)
nextSequenceValue( $seqName)
Deprecated method, calls should be removed.
newExceptionAfterConnectError( $error)
__destruct()
Run a few simple sanity checks and close dangling connections.
dbSchema( $schema=null)
Get/set the db schema.
Definition Database.php:587
wasLockTimeout()
Determines if the last failure was due to a lock timeout.Note that during a lock wait timeout,...
endAtomic( $fname=__METHOD__)
Ends an atomic section of SQL statements.
cancelAtomic( $fname=__METHOD__, AtomicSectionIdentifier $sectionId=null)
Cancel an atomic section of SQL statements.
setSessionOptions(array $options)
Override database's default behavior.$options include: 'connTimeout' : Set the connection timeout val...
string[] int[] float[] $connectionVariables
SQL variables values to use for all new connections.
Definition Database.php:95
string $agent
Agent name for query profiling.
Definition Database.php:87
normalizeRowArray(array $rowOrRows)
static float $SMALL_WRITE_ROWS
Assume an insert of this many rows or less should be fast to replicate.
Definition Database.php:239
indexName( $index)
Allows for index remapping in queries where this is not consistent across DBMS.
assertNoOpenTransactions()
Assert that all explicit transactions or atomic sections have been closed.
static string[] $MUTABLE_FLAGS
List of DBO_* flags that can be changed after connection.
Definition Database.php:242
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 ...
callable[] $trxRecurringCallbacks
Map of (name => callable)
Definition Database.php:172
getDomainID()
Return the currently selected domain ID.
Definition Database.php:823
makeUpdateOptionsArray( $options)
Make UPDATE options array for Database::makeUpdateOptions.
listViews( $prefix=null, $fname=__METHOD__)
Lists all the VIEWs in the database.array Stable to override
array[] $trxPreCommitCallbacks
List of (callable, method name, atomic section id)
Definition Database.php:163
doCommit( $fname)
Issues the COMMIT command to the database server.
closeConnection()
Closes underlying database connection.
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
lockForUpdate( $table, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Lock all rows meeting the given conditions/options FOR UPDATE.
bitNot( $field)
string Stable to override
buildLeast( $fields, $values)
Build a LEAST function statement comparing columns/values.Integer and float values in $values will no...
getSchemaVars()
Get schema variables.
runTransactionListenerCallbacks( $trigger)
Actually run any "transaction listener" callbacks.
static getAttributes()
Stable to override.
Definition Database.php:530
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
string $user
User that this instance is currently connected under the name of.
Definition Database.php:81
getTopologyRole()
Get the replication topology role of this server.
Definition Database.php:549
static getCacheSetOptions(IDatabase $db1, IDatabase $db2=null)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
getRecordedTransactionLagStatus()
Get the replica DB lag when the current transaction started.
array $sessionTempTables
Map of (table name => 1) for current TEMPORARY tables.
Definition Database.php:120
serverIsReadOnly()
bool Whether the DB is marked as read-only server-side 1.28 Stable to override
wasKnownStatementRollbackError()
Stable to override.
lastDoneWrites()
Get the last time the connection may have been used for a write query.
Definition Database.php:665
isTransactableQuery( $sql)
Determine whether a SQL statement is sensitive to isolation level.
int $trxStatus
Transaction status.
Definition Database.php:127
buildSuperlative( $sqlfunc, $fields, $values)
Build a superlative function statement comparing columns/values.
replaceLostConnection( $fname)
Close any existing (dead) database connection and open a new connection.
getServerUptime()
Determines how long the server has been up.int Stable to override
int $flags
Current bit field of class DBO_* constants.
Definition Database.php:100
bitAnd( $fieldLeft, $fieldRight)
string Stable to override
setIndexAliases(array $aliases)
Convert certain index names to alternative names before querying the DB.Note that this applies to ind...
getDefaultSchemaVars()
Get schema variables to use if none have been set via setSchemaVars().
static int $TEMP_PSEUDO_PERMANENT
Writes to this temporary table effect lastDoneWrites()
Definition Database.php:220
doBegin( $fname)
Issues the BEGIN command to the database server.
writesOrCallbacksPending()
Whether there is a transaction open with either possible write queries or unresolved pre-commit/commi...
Definition Database.php:673
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...
handleSessionLossPreconnect()
Clean things up after session (and thus transaction) loss before reconnect.
initConnection()
Initialize the connection to the database over the wire (or to local files)
Definition Database.php:314
tableNames(... $tables)
Fetch a number of table names into an array This is handy when you need to construct SQL for joins.
onTransactionResolution(callable $callback, $fname=__METHOD__)
Run a callback as soon as the current transaction commits or rolls back.
unionConditionPermutations( $table, $vars, array $permute_conds, $extra_conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Construct a UNION query for permutations of conditions.
string[] $trxWriteCallers
Write query callers of the current transaction.
Definition Database.php:149
dropTable( $table, $fname=__METHOD__)
Delete a table.
onTransactionIdle(callable $callback, $fname=__METHOD__)
Alias for onTransactionCommitOrIdle() for backwards-compatibility.
selectOptionsIncludeLocking( $options)
indexUnique( $table, $index, $fname=__METHOD__)
Determines if a given index is unique.bool Stable to override
setBigSelects( $value=true)
Allow or deny "big selects" for this session only.This is done by setting the sql_big_selects session...
string[] $indexAliases
Current map of (index alias => index)
Definition Database.php:108
array null $trxStatusIgnoredCause
Error details of the last statement-only rollback.
Definition Database.php:131
wasConnectionLoss()
Determines if the last query error was due to a dropped connection.Note that during a connection loss...
databasesAreIndependent()
Returns true if DBs are assumed to be on potentially different servers.In systems like mysql/mariadb,...
assertHasConnectionHandle()
Make sure there is an open connection handle (alive or not) as a sanity check.
Definition Database.php:990
lock( $lockName, $method, $timeout=5)
Acquire a named lock.Named locks are not related to transactionsbool Success Stable to override
wasConnectionError( $errno)
Do not use this method outside of Database/DBError classes.
callable $deprecationLogger
Deprecation logging callback.
Definition Database.php:62
doHandleSessionLossPreconnect()
Reset any additional subclass trx* and session* fields Stable to override.
isFlagInOptions( $option, array $options)
makeGroupByWithHaving( $options)
Returns an optional GROUP BY with an optional HAVING.
int $trxWriteAffectedRows
Number of rows affected by write queries for the current transaction.
Definition Database.php:155
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,...
registerTempWrites( $ret, array $changes)
unionSupportsOrderAndLimit()
Determine if the RDBMS supports ORDER BY and LIMIT for separate subqueries within UNION....
selectFieldsOrOptionsAggregate( $fields, $options)
static int $PING_TTL
How long before it is worth doing a dummy query to test the connection.
Definition Database.php:230
query( $sql, $fname=__METHOD__, $flags=self::QUERY_NORMAL)
Run an SQL query and return the result.
assertIsWritableMaster()
Make sure that this server is not marked as a replica nor read-only as a sanity check.
static float $SLOW_WRITE_SEC
Consider a write slow if it took more than this many seconds.
Definition Database.php:237
duplicateTableStructure( $oldName, $newName, $temporary=false, $fname=__METHOD__)
Creates a new table with structure copied from existing table.Note that unlike most database abstract...
doRollbackToSavepoint( $identifier, $fname)
Rollback to a savepoint.
anyChar()
Returns a token for buildLike() that denotes a '_' to be used in a LIKE query.
array[] $trxSectionCancelCallbacks
List of (callable, method name, atomic section id)
Definition Database.php:170
doReplace( $table, array $uniqueKeys, array $rows, $fname)
restoreFlags( $state=self::RESTORE_PRIOR)
Restore the flags to their prior state before the last setFlag/clearFlag call.
Definition Database.php:806
doAtomicSection( $fname, callable $callback, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Perform an atomic section of reversable SQL statements from a callback.
flushSnapshot( $fname=__METHOD__, $flush=self::FLUSHING_ONE)
Commit any transaction but error out if writes or callbacks are pending.
executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags)
Wrapper for doQuery() that handles DBO_TRX, profiling, logging, affected row count tracking,...
static int $DEADLOCK_DELAY_MIN
Minimum time to wait before retry, in microseconds.
Definition Database.php:225
setTrxEndCallbackSuppression( $suppress)
Whether to disable running of post-COMMIT/ROLLBACK callbacks.
installErrorHandler()
Set a custom error handler for logging errors during database connection.
Definition Database.php:850
affectedRows()
Get the number of rows affected by the last write query.
makeConditionCollidesUponKey(array $rows, array $uniqueKey)
buildSubstring( $input, $startPosition, $length=null)
Stable to override
getQueryExceptionAndLog( $error, $errno, $sql, $fname)
getReplicaPos()
Get the replication position of this replica DB.DBMasterPos|bool False if this is not a replica DB S...
wasReadOnlyError()
Determines if the last failure was due to the database being read-only.bool Stable to override
float $lastRoundTripEstimate
Query round trip time estimate.
Definition Database.php:188
tableName( $name, $format='quoted')
Format a table name ready for use in constructing an SQL query.This does two important things: it quo...
replaceVars( $ins)
Database independent variable replacement.
insert( $table, $rows, $fname=__METHOD__, $options=[])
Insert the given row(s) into a table.
wasQueryTimeout( $error, $errno)
Checks whether the cause of the error is detected to be a timeout.
selectDomain( $domain)
Set the current domain (database, schema, and table prefix)
freeResult( $res)
Free a result object returned by query() or select()It's usually not necessary to call this,...
tableNameWithAlias( $table, $alias=false)
Get an aliased table name.
streamStatementEnd(&$sql, &$newLine)
Called by sourceStream() to check if we've reached a statement end.
array $lbInfo
Current LoadBalancer tracking information.
Definition Database.php:102
float null $trxTimestamp
UNIX timestamp at the time of BEGIN for the last transaction.
Definition Database.php:133
getTopologyRootMaster()
Get the host (or address) of the root master server for the replication topology.
Definition Database.php:553
wasDeadlock()
Determines if the last failure was due to a deadlock.Note that during a deadlock, the prior transacti...
selectRowCount( $tables, $var=' *', $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Get the number of rows in dataset.
buildConcat( $stringList)
Build a concatenation list to feed into a SQL query.string Stable to override
ignoreIndexClause( $index)
IGNORE INDEX clause.
string $trxFname
Name of the function that start the last transaction.
Definition Database.php:137
string $delimiter
Current SQL query delimiter.
Definition Database.php:104
bitOr( $fieldLeft, $fieldRight)
string Stable to override
string bool $lastPhpError
Definition Database.php:186
clearFlag( $flag, $remember=self::REMEMBER_NOTHING)
Clear a flag for this connection.
Definition Database.php:791
useIndexClause( $index)
USE INDEX clause.
DatabaseDomain $currentDomain
Definition Database.php:69
tableNamesN(... $tables)
Fetch a number of table names into an zero-indexed numerical array This is handy when you need to con...
namedLocksEnqueue()
Check to see if a named lock used by lock() use blocking queues.bool 1.26 Stable to override
close( $fname=__METHOD__, $owner=null)
Close the database connection.
Definition Database.php:912
connectionErrorLogger( $errno, $errstr)
Error handler for logging errors during database connection This method should not be used outside of...
Definition Database.php:891
maxListLen()
Return the maximum number of items allowed in a list, or 0 for unlimited.int Stable to override
array $connectionParams
Parameters used by initConnection() to establish a connection.
Definition Database.php:93
LoggerInterface $queryLogger
Definition Database.php:56
int $trxAtomicCounter
Counter for atomic savepoint identifiers (reset with each transaction)
Definition Database.php:143
pendingWriteAndCallbackCallers()
List the methods that have write queries or callbacks for the current transaction.
Definition Database.php:747
bool $trxEndCallbacksSuppressed
Whether to suppress triggering of transaction end callbacks.
Definition Database.php:174
Exception null $trxStatusCause
The last error that caused the status to become STATUS_TRX_ERROR.
Definition Database.php:129
sourceStream( $fp, callable $lineCallback=null, callable $resultCallback=null, $fname=__METHOD__, callable $inputCallback=null)
Read and execute commands from an open file handle.
replace( $table, $uniqueKeys, $rows, $fname=__METHOD__)
Insert row(s) into a table, deleting all conflicting rows beforehand.
qualifiedTableComponents( $name)
Get the table components needed for a query given the currently selected database.
indexExists( $table, $index, $fname=__METHOD__)
Determines whether an index exists.
limitResult( $sql, $limit, $offset=false)
Construct a LIMIT query with optional offset.The SQL should be adjusted so that only the first $limit...
doUpsert( $table, array $rows, array $uniqueKeys, array $set, $fname)
getTempTableWrites( $sql, $pseudoPermanent)
pendingWriteCallers()
Get the list of method names that did write queries for this transaction.
Definition Database.php:731
pendingWriteQueryDuration( $type=self::ESTIMATE_TOTAL)
Get the time spend running write queries for this transaction.
Definition Database.php:701
isPristineTemporaryTable( $table)
Check if the table is both a TEMPORARY table and has not yet received CRUD operations.
beginIfImplied( $sql, $fname)
Start an implicit transaction if DBO_TRX is enabled and no transaction is active.
startAtomic( $fname=__METHOD__, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Begin an atomic section of SQL statements.
commit( $fname=__METHOD__, $flush=self::FLUSHING_ONE)
Commits a transaction previously started using begin()
getLBInfo( $name=null)
Get properties passed down from the server info array of the load balancer.
Definition Database.php:617
static string $PING_QUERY
Dummy SQL query.
Definition Database.php:232
aggregateValue( $valuedata, $valuename='value')
Return aggregated value alias.array|string Since 1.33 Stable to override
array[] $tableAliases
Current map of (table => (dbname, schema, prefix) map)
Definition Database.php:106
getMasterPos()
Get the position of this master.DBMasterPos|bool False if this is not a master Stable to override
buildSelectSubquery( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Equivalent to IDatabase::selectSQLText() except wraps the result in Subquery.
__sleep()
Called by serialize.
addIdentifierQuotes( $s)
Escape a SQL identifier (e.g.table, column, database) for use in a SQL queryDepending on the database...
callable null $profiler
Definition Database.php:64
buildStringCast( $field)
string 1.28 Stable to override
LoggerInterface $replLogger
Definition Database.php:58
doSavepoint( $identifier, $fname)
Create a savepoint.
int $nonNativeInsertSelectBatchSize
Row batch size to use for emulated INSERT SELECT queries.
Definition Database.php:97
indexInfo( $table, $index, $fname=__METHOD__)
Get information about an index into an object.
normalizeUpsertKeys( $uniqueKeys)
doSelectDomain(DatabaseDomain $domain)
Stable to override.
getLogContext(array $extras=[])
Create a log context to pass to PSR-3 logger functions.
Definition Database.php:901
modifyCallbacksForCancel(array $sectionIds, AtomicSectionIdentifier $newSectionId=null)
Update callbacks that were owned by cancelled atomic sections.
trxLevel()
Gets the current transaction level.
Definition Database.php:557
isWriteQuery( $sql, $flags)
Determine whether a query writes to the DB.
prependDatabaseOrSchema( $namespace, $relation, $format)
runOnTransactionIdleCallbacks( $trigger)
Actually consume and run any "on transaction idle/resolution" callbacks.
unlockTables( $method)
Unlock all tables locked via lockTables()
setSchemaVars( $vars)
Set schema variables to be used when streaming commands from SQL files or stdin.
upsert( $table, array $rows, $uniqueKeys, array $set, $fname=__METHOD__)
Upsert the given row(s) into a table.
fieldHasBit( $field, $flags)
decodeBlob( $b)
Some DBMSs return a special placeholder object representing blob fields in result objects....
setFlag( $flag, $remember=self::REMEMBER_NOTHING)
Set a flag for this connection.
Definition Database.php:776
setTableAliases(array $aliases)
Make certain table names use their own database, schema, and table prefix when passed into SQL querie...
lastQuery()
Get the last query that sent on account of IDatabase::query()
Definition Database.php:661
static getClass( $dbType, $driver=null)
Definition Database.php:478
executeQuery( $sql, $fname, $flags)
Execute a query, retrying it if there is a recoverable connection loss.
sourceFile( $filename, callable $lineCallback=null, callable $resultCallback=null, $fname=false, callable $inputCallback=null)
Read and execute SQL commands from a file.
buildLike( $param,... $params)
LIKE statement wrapper.This takes a variable-length argument list with parts of pattern to match cont...
assertBuildSubstringParams( $startPosition, $length)
Check type and bounds for parameters to self::buildSubstring()
array $sessionNamedLocks
Map of (name => 1) for locks obtained via lock()
Definition Database.php:118
makeWhereFrom2d( $data, $baseKey, $subKey)
Build a partial where clause from a 2-d array such as used for LinkBatch.
unlock( $lockName, $method)
Release a lock.Named locks are not related to transactionsbool Success Stable to override
getQueryException( $error, $errno, $sql, $fname)
static attributesFromType( $dbType, $driver=null)
Definition Database.php:460
IDatabase null $lazyMasterHandle
Lazy handle to the master DB this server replicates from.
Definition Database.php:76
float bool $lastWriteTime
UNIX timestamp of last write query.
Definition Database.php:184
getServer()
Get the server hostname or IP address.
getLag()
Get the amount of replication lag for this database server.
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...
string $lastQuery
The last SQL query attempted.
Definition Database.php:182
onTransactionPreCommitOrIdle(callable $callback, $fname=__METHOD__)
Run a callback before the current transaction commits or now if there is none.
runOnAtomicSectionCancelCallbacks( $trigger, array $sectionIds=null)
Actually run any "atomic section cancel" callbacks.
array $sessionDirtyTempTables
Map of (table name => 1) for current TEMPORARY tables.
Definition Database.php:122
unionQueries( $sqls, $all)
Construct a UNION query.This is used for providing overload point for other DB abstractions not compa...
wasErrorReissuable()
Determines if the last query error was due to something outside of the query itself.
doReleaseSavepoint( $identifier, $fname)
Release a savepoint.
consumeTrxShortId()
Reset the transaction ID and return the old one.
string $password
Password used to establish the current connection.
Definition Database.php:83
tableLocksHaveTransactionScope()
Checks if table locks acquired by lockTables() are transaction-bound in their scope.
escapeLikeInternal( $s, $escapeChar='`')
Stable to override.
bool $trxAutomatic
Whether the current transaction was started implicitly due to DBO_TRX.
Definition Database.php:141
ping(&$rtt=null)
Ping the server and try to reconnect if it there is no connection.
truncate( $tables, $fname=__METHOD__)
Delete all data in a table(s) and reset any sequences owned by that table(s)
__clone()
Make sure that copies do not share the same client binding handle.
string bool null $htmlErrors
Stashed value of html_errors INI setting.
Definition Database.php:113
anyString()
Returns a token for buildLike() that denotes a '' to be used in a LIKE query.
LoggerInterface $connLogger
Definition Database.php:54
assertConditionIsNotEmpty( $conds, string $fname, bool $deprecate)
Check type and bounds conditions parameters for update.
string $trxShortId
ID of the active transaction or the empty string otherwise.
Definition Database.php:125
bool $trxAutomaticAtomic
Whether the current transaction was started implicitly by startAtomic()
Definition Database.php:147
getInfinity()
Find out when 'infinity' is.Most DBMSes support this. This is a special keyword for timestamps in Pos...
string $server
Server that this instance is currently connected to.
Definition Database.php:79
reportQueryError( $error, $errno, $sql, $fname, $ignore=false)
Report a query error.
updateTrxWriteQueryTime( $sql, $runtime, $affected)
Update the estimated run-time of a query, not counting large row lock times.
static factory( $type, $params=[], $connect=self::NEW_CONNECTED)
Construct a Database subclass instance given a database type and parameters.
Definition Database.php:405
canRecoverFromDisconnect( $sql, $priorWritesPending)
Determine whether it is safe to retry queries after a database connection is lost.
resultObject( $result)
Take a query result and wrap it in an iterable result wrapper if necessary.
doInsert( $table, array $rows, $fname)
getScopedLockAndFlush( $lockKey, $fname, $timeout)
Acquire a named lock, flush any transaction, and return an RAII style unlocker object.
encodeExpiry( $expiry)
Encode an expiry time into the DBMS dependent format.
doRollback( $fname)
Issues the ROLLBACK command to the database server.
tableExists( $table, $fname=__METHOD__)
Query whether a given table exists.
decodeExpiry( $expiry, $format=TS_MW)
Decode an expiry time into a DBMS independent format.
rollback( $fname=__METHOD__, $flush=self::FLUSHING_ONE)
Rollback a transaction previously started using begin()
setTransactionListener( $name, callable $callback=null)
Run a callback after each time any transaction commits or rolls back.
getLazyMasterHandle()
Get a handle to the master server of the cluster to which this server belongs.
Definition Database.php:649
normalizeConditions( $conds, $fname)
isQuotedIdentifier( $name)
Returns if the given identifier looks quoted or not according to the database convention for quoting ...
insertSelect( $destTable, $srcTable, $varMap, $conds, $fname=__METHOD__, $insertOptions=[], $selectOptions=[], $selectJoinConds=[])
INSERT SELECT wrapper.
string $topologyRole
Replication topology role of the server; one of the class ROLE_* constants.
Definition Database.php:89
lockIsFree( $lockName, $method)
Check to see if a named lock is not locked by any thread (non-blocking)bool 1.20 Stable to override
getFlag( $flag)
Returns a boolean whether the flag $flag is set for this connection.
Definition Database.php:819
masterPosWait(DBMasterPos $pos, $timeout)
Wait for the replica DB to catch up to a given master position.Note that this does not start any new ...
int null $ownerId
Integer ID of the managing LBFactory instance or null if none.
Definition Database.php:191
makeList(array $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.
array $trxAtomicLevels
List of (name, unique ID, savepoint ID) for each active atomic section level.
Definition Database.php:145
float $trxWriteAdjDuration
Like trxWriteQueryCount but excludes lock-bound, easy to replicate, queries.
Definition Database.php:157
float $trxReplicaLag
Replication lag estimate at the time of BEGIN for the last transaction.
Definition Database.php:135
timestampOrNull( $ts=null)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
float $lastPing
UNIX timestamp.
Definition Database.php:180
array[] $trxEndCallbacks
List of (callable, method name, atomic section id) -var array<array{0:callable,1:string,...
Definition Database.php:168
int $trxWriteAdjQueryCount
Number of write queries counted in trxWriteAdjDuration.
Definition Database.php:159
TransactionProfiler $trxProfiler
Definition Database.php:66
update( $table, $set, $conds, $fname=__METHOD__, $options=[])
Update all rows in a table that match a given condition.
doInsertNonConflicting( $table, array $rows, $fname)
doLockTables(array $read, array $write, $method)
Helper function for lockTables() that handles the actual table locking.
doUnlockTables( $method)
Helper function for unlockTables() that handles the actual table unlocking.
onAtomicSectionCancel(callable $callback, $fname=__METHOD__)
Run a callback when the atomic section is cancelled.
BagOStuff $srvCache
APC cache.
Definition Database.php:52
float $trxWriteDuration
Seconds spent in write queries for the current transaction.
Definition Database.php:151
buildGreatest( $fields, $values)
Build a GREATEST function statement comparing columns/values.Integer and float values in $values will...
makeInsertLists(array $rows)
Make SQL lists of columns, row tuples for INSERT/VALUES expressions.
getBindingHandle()
Get the underlying binding connection handle.
array null $schemaVars
Current variables use for schema element placeholders.
Definition Database.php:110
doGetLag()
Stable to override
static string $NOT_APPLICABLE
Idiom used when a cancelable atomic section started the transaction.
Definition Database.php:213
doDropTable( $table, $fname)
doTruncate(array $tables, $fname)
doQuery( $sql)
Run a query and return a DBMS-dependent wrapper or boolean.
implicitOrderby()
Returns true if this database does an implicit order by when the column has an index For example: SEL...
Definition Database.php:657
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...
makeOrderBy( $options)
Returns an optional ORDER BY.
strreplace( $orig, $old, $new)
Returns a SQL expression for simple string replacement (e.g.REPLACE() in mysql)string Stable to overr...
__construct(array $params)
Definition Database.php:258
textFieldSize( $table, $field)
Returns the size of a text field, or -1 for "unlimited".int Stable to override
getDBname()
Get the current DB name.
onTransactionCommitOrIdle(callable $callback, $fname=__METHOD__)
Run a callback as soon as there is no transaction pending.
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a single field from a single result row.
Lazy-loaded wrapper for simplification and scrubbing of SQL queries for profiling.
Used by Database::buildLike() to represent characters that have special meaning in SQL LIKE clauses a...
Definition LikeMatch.php:10
Result wrapper for grabbing data queried from an IDatabase object.
Detect high-contention DB queries via profiling calls.
An object representing a master or replica DB position in a replicated setup.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
lastErrno()
Get the last error number.
fetchRow( $res)
Fetch the next row from the given result object, in associative array form.
lastError()
Get a description of the last error.
getType()
Get the type of the DBMS (e.g.
getSessionLagStatus()
Get the replica DB lag when the current transaction started or a general lag estimate if not transact...
fetchObject( $res)
Fetch the next row from the given result object, in object form.
numRows( $res)
Get the number of rows in a query result.
getServerVersion()
A string describing the current software version, like from mysql_get_server_info()
Advanced database interface for IDatabase handles that include maintenance methods.
fieldInfo( $table, $field)
mysql_fetch_field() wrapper Returns false if the field doesn't exist
Result wrapper for grabbing data queried from an IDatabase object.
$line
Definition mcc.php:119
if( $line===false) $args
Definition mcc.php:124
$source
const DBO_DDLMODE
Definition defines.php:16