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