MediaWiki REL1_31
Database.php
Go to the documentation of this file.
1<?php
26namespace Wikimedia\Rdbms;
27
28use Psr\Log\LoggerAwareInterface;
29use Psr\Log\LoggerInterface;
30use Psr\Log\NullLogger;
31use Wikimedia\ScopedCallback;
32use Wikimedia\Timestamp\ConvertibleTimestamp;
33use Wikimedia;
34use BagOStuff;
36use LogicException;
37use InvalidArgumentException;
38use UnexpectedValueException;
39use Exception;
40use RuntimeException;
41
48abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAwareInterface {
50 const DEADLOCK_TRIES = 4;
52 const DEADLOCK_DELAY_MIN = 500000;
54 const DEADLOCK_DELAY_MAX = 1500000;
55
57 const PING_TTL = 1.0;
58 const PING_QUERY = 'SELECT 1 AS ping';
59
60 const TINY_WRITE_SEC = 0.010;
61 const SLOW_WRITE_SEC = 0.500;
62 const SMALL_WRITE_ROWS = 100;
63
65 const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
66
68 const NEW_UNCONNECTED = 0;
70 const NEW_CONNECTED = 1;
71
73 protected $lastQuery = '';
75 protected $lastWriteTime = false;
77 protected $phpError = false;
79 protected $server;
81 protected $user;
83 protected $password;
85 protected $dbName;
87 protected $tableAliases = [];
89 protected $indexAliases = [];
91 protected $cliMode;
93 protected $agent;
95 protected $connectionParams = [];
97 protected $srvCache;
99 protected $connLogger;
101 protected $queryLogger;
103 protected $errorLogger;
106
108 protected $conn = null;
110 protected $opened = false;
111
113 protected $trxIdleCallbacks = [];
117 protected $trxEndCallbacks = [];
121 protected $trxEndCallbacksSuppressed = false;
122
124 protected $tablePrefix = '';
126 protected $schema = '';
128 protected $flags;
130 protected $lbInfo = [];
132 protected $schemaVars = false;
134 protected $sessionVars = [];
136 protected $preparedArgs;
138 protected $htmlErrors;
140 protected $delimiter = ';';
142 protected $currentDomain;
145
149 protected $trxStatus = self::STATUS_TRX_NONE;
165 protected $trxLevel = 0;
172 protected $trxShortId = '';
181 private $trxTimestamp = null;
183 private $trxReplicaLag = null;
191 private $trxFname = null;
198 private $trxDoneWrites = false;
205 private $trxAutomatic = false;
211 private $trxAtomicCounter = 0;
217 private $trxAtomicLevels = [];
223 private $trxAutomaticAtomic = false;
229 private $trxWriteCallers = [];
233 private $trxWriteDuration = 0.0;
245 private $trxWriteAdjDuration = 0.0;
253 private $rttEstimate = 0.0;
254
256 private $namedLocksHeld = [];
258 protected $sessionTempTables = [];
259
262
264 protected $lastPing = 0.0;
265
267 private $priorFlags = [];
268
270 protected $profiler;
272 protected $trxProfiler;
273
276
278 private static $NOT_APPLICABLE = 'n/a';
280 private static $SAVEPOINT_PREFIX = 'wikimedia_rdbms_atomic';
281
283 const STATUS_TRX_ERROR = 1;
285 const STATUS_TRX_OK = 2;
287 const STATUS_TRX_NONE = 3;
288
293 protected function __construct( array $params ) {
294 foreach ( [ 'host', 'user', 'password', 'dbname' ] as $name ) {
295 $this->connectionParams[$name] = $params[$name];
296 }
297
298 $this->schema = $params['schema'];
299 $this->tablePrefix = $params['tablePrefix'];
300
301 $this->cliMode = $params['cliMode'];
302 // Agent name is added to SQL queries in a comment, so make sure it can't break out
303 $this->agent = str_replace( '/', '-', $params['agent'] );
304
305 $this->flags = $params['flags'];
306 if ( $this->flags & self::DBO_DEFAULT ) {
307 if ( $this->cliMode ) {
308 $this->flags &= ~self::DBO_TRX;
309 } else {
310 $this->flags |= self::DBO_TRX;
311 }
312 }
313 // Disregard deprecated DBO_IGNORE flag (T189999)
314 $this->flags &= ~self::DBO_IGNORE;
315
316 $this->sessionVars = $params['variables'];
317
318 $this->srvCache = isset( $params['srvCache'] )
319 ? $params['srvCache']
320 : new HashBagOStuff();
321
322 $this->profiler = $params['profiler'];
323 $this->trxProfiler = $params['trxProfiler'];
324 $this->connLogger = $params['connLogger'];
325 $this->queryLogger = $params['queryLogger'];
326 $this->errorLogger = $params['errorLogger'];
327 $this->deprecationLogger = $params['deprecationLogger'];
328
329 if ( isset( $params['nonNativeInsertSelectBatchSize'] ) ) {
330 $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'];
331 }
332
333 // Set initial dummy domain until open() sets the final DB/prefix
334 $this->currentDomain = DatabaseDomain::newUnspecified();
335 }
336
345 final public function initConnection() {
346 if ( $this->isOpen() ) {
347 throw new LogicException( __METHOD__ . ': already connected.' );
348 }
349 // Establish the connection
350 $this->doInitConnection();
351 // Set the domain object after open() sets the relevant fields
352 if ( $this->dbName != '' ) {
353 // Domains with server scope but a table prefix are not used by IDatabase classes
354 $this->currentDomain = new DatabaseDomain( $this->dbName, null, $this->tablePrefix );
355 }
356 }
357
365 protected function doInitConnection() {
366 if ( strlen( $this->connectionParams['user'] ) ) {
367 $this->open(
368 $this->connectionParams['host'],
369 $this->connectionParams['user'],
370 $this->connectionParams['password'],
371 $this->connectionParams['dbname']
372 );
373 } else {
374 throw new InvalidArgumentException( "No database user provided." );
375 }
376 }
377
422 final public static function factory( $dbType, $p = [], $connect = self::NEW_CONNECTED ) {
423 $class = self::getClass( $dbType, isset( $p['driver'] ) ? $p['driver'] : null );
424
425 if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
426 // Resolve some defaults for b/c
427 $p['host'] = isset( $p['host'] ) ? $p['host'] : false;
428 $p['user'] = isset( $p['user'] ) ? $p['user'] : false;
429 $p['password'] = isset( $p['password'] ) ? $p['password'] : false;
430 $p['dbname'] = isset( $p['dbname'] ) ? $p['dbname'] : false;
431 $p['flags'] = isset( $p['flags'] ) ? $p['flags'] : 0;
432 $p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
433 $p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : '';
434 $p['schema'] = isset( $p['schema'] ) ? $p['schema'] : '';
435 $p['cliMode'] = isset( $p['cliMode'] )
436 ? $p['cliMode']
437 : ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' );
438 $p['agent'] = isset( $p['agent'] ) ? $p['agent'] : '';
439 if ( !isset( $p['connLogger'] ) ) {
440 $p['connLogger'] = new NullLogger();
441 }
442 if ( !isset( $p['queryLogger'] ) ) {
443 $p['queryLogger'] = new NullLogger();
444 }
445 $p['profiler'] = isset( $p['profiler'] ) ? $p['profiler'] : null;
446 if ( !isset( $p['trxProfiler'] ) ) {
447 $p['trxProfiler'] = new TransactionProfiler();
448 }
449 if ( !isset( $p['errorLogger'] ) ) {
450 $p['errorLogger'] = function ( Exception $e ) {
451 trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
452 };
453 }
454 if ( !isset( $p['deprecationLogger'] ) ) {
455 $p['deprecationLogger'] = function ( $msg ) {
456 trigger_error( $msg, E_USER_DEPRECATED );
457 };
458 }
459
461 $conn = new $class( $p );
462 if ( $connect == self::NEW_CONNECTED ) {
463 $conn->initConnection();
464 }
465 } else {
466 $conn = null;
467 }
468
469 return $conn;
470 }
471
479 final public static function attributesFromType( $dbType, $driver = null ) {
480 static $defaults = [ self::ATTR_DB_LEVEL_LOCKING => false ];
481
482 $class = self::getClass( $dbType, $driver );
483
484 return call_user_func( [ $class, 'getAttributes' ] ) + $defaults;
485 }
486
493 private static function getClass( $dbType, $driver = null ) {
494 // For database types with built-in support, the below maps type to IDatabase
495 // implementations. For types with multipe driver implementations (PHP extensions),
496 // an array can be used, keyed by extension name. In case of an array, the
497 // optional 'driver' parameter can be used to force a specific driver. Otherwise,
498 // we auto-detect the first available driver. For types without built-in support,
499 // an class named "Database<Type>" us used, eg. DatabaseFoo for type 'foo'.
500 static $builtinTypes = [
501 'mssql' => DatabaseMssql::class,
502 'mysql' => [ 'mysqli' => DatabaseMysqli::class ],
503 'sqlite' => DatabaseSqlite::class,
504 'postgres' => DatabasePostgres::class,
505 ];
506
507 $dbType = strtolower( $dbType );
508 $class = false;
509
510 if ( isset( $builtinTypes[$dbType] ) ) {
511 $possibleDrivers = $builtinTypes[$dbType];
512 if ( is_string( $possibleDrivers ) ) {
513 $class = $possibleDrivers;
514 } else {
515 if ( (string)$driver !== '' ) {
516 if ( !isset( $possibleDrivers[$driver] ) ) {
517 throw new InvalidArgumentException( __METHOD__ .
518 " type '$dbType' does not support driver '{$driver}'" );
519 } else {
520 $class = $possibleDrivers[$driver];
521 }
522 } else {
523 foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
524 if ( extension_loaded( $posDriver ) ) {
525 $class = $possibleClass;
526 break;
527 }
528 }
529 }
530 }
531 } else {
532 $class = 'Database' . ucfirst( $dbType );
533 }
534
535 if ( $class === false ) {
536 throw new InvalidArgumentException( __METHOD__ .
537 " no viable database extension found for type '$dbType'" );
538 }
539
540 return $class;
541 }
542
547 protected static function getAttributes() {
548 return [];
549 }
550
558 public function setLogger( LoggerInterface $logger ) {
559 $this->queryLogger = $logger;
560 }
561
562 public function getServerInfo() {
563 return $this->getServerVersion();
564 }
565
566 public function bufferResults( $buffer = null ) {
567 $res = !$this->getFlag( self::DBO_NOBUFFER );
568 if ( $buffer !== null ) {
569 $buffer
570 ? $this->clearFlag( self::DBO_NOBUFFER )
571 : $this->setFlag( self::DBO_NOBUFFER );
572 }
573
574 return $res;
575 }
576
577 public function trxLevel() {
578 return $this->trxLevel;
579 }
580
581 public function trxTimestamp() {
582 return $this->trxLevel ? $this->trxTimestamp : null;
583 }
584
589 public function trxStatus() {
590 return $this->trxStatus;
591 }
592
593 public function tablePrefix( $prefix = null ) {
594 $old = $this->tablePrefix;
595 if ( $prefix !== null ) {
596 $this->tablePrefix = $prefix;
597 $this->currentDomain = ( $this->dbName != '' )
598 ? new DatabaseDomain( $this->dbName, null, $this->tablePrefix )
600 }
601
602 return $old;
603 }
604
605 public function dbSchema( $schema = null ) {
606 $old = $this->schema;
607 if ( $schema !== null ) {
608 $this->schema = $schema;
609 }
610
611 return $old;
612 }
613
614 public function getLBInfo( $name = null ) {
615 if ( is_null( $name ) ) {
616 return $this->lbInfo;
617 } else {
618 if ( array_key_exists( $name, $this->lbInfo ) ) {
619 return $this->lbInfo[$name];
620 } else {
621 return null;
622 }
623 }
624 }
625
626 public function setLBInfo( $name, $value = null ) {
627 if ( is_null( $value ) ) {
628 $this->lbInfo = $name;
629 } else {
630 $this->lbInfo[$name] = $value;
631 }
632 }
633
635 $this->lazyMasterHandle = $conn;
636 }
637
643 protected function getLazyMasterHandle() {
645 }
646
647 public function implicitGroupby() {
648 return true;
649 }
650
651 public function implicitOrderby() {
652 return true;
653 }
654
655 public function lastQuery() {
656 return $this->lastQuery;
657 }
658
659 public function doneWrites() {
660 return (bool)$this->lastWriteTime;
661 }
662
663 public function lastDoneWrites() {
664 return $this->lastWriteTime ?: false;
665 }
666
667 public function writesPending() {
668 return $this->trxLevel && $this->trxDoneWrites;
669 }
670
671 public function writesOrCallbacksPending() {
672 return $this->trxLevel && (
673 $this->trxDoneWrites ||
674 $this->trxIdleCallbacks ||
675 $this->trxPreCommitCallbacks ||
677 );
678 }
679
683 final protected function getTransactionRoundId() {
684 // If transaction round participation is enabled, see if one is active
685 if ( $this->getFlag( self::DBO_TRX ) ) {
686 $id = $this->getLBInfo( 'trxRoundId' );
687
688 return is_string( $id ) ? $id : null;
689 }
690
691 return null;
692 }
693
694 public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
695 if ( !$this->trxLevel ) {
696 return false;
697 } elseif ( !$this->trxDoneWrites ) {
698 return 0.0;
699 }
700
701 switch ( $type ) {
702 case self::ESTIMATE_DB_APPLY:
703 $this->ping( $rtt );
704 $rttAdjTotal = $this->trxWriteAdjQueryCount * $rtt;
705 $applyTime = max( $this->trxWriteAdjDuration - $rttAdjTotal, 0 );
706 // For omitted queries, make them count as something at least
707 $omitted = $this->trxWriteQueryCount - $this->trxWriteAdjQueryCount;
708 $applyTime += self::TINY_WRITE_SEC * $omitted;
709
710 return $applyTime;
711 default: // everything
713 }
714 }
715
716 public function pendingWriteCallers() {
717 return $this->trxLevel ? $this->trxWriteCallers : [];
718 }
719
720 public function pendingWriteRowsAffected() {
722 }
723
730 protected function pendingWriteAndCallbackCallers() {
731 if ( !$this->trxLevel ) {
732 return [];
733 }
734
735 $fnames = $this->trxWriteCallers;
736 foreach ( [
737 $this->trxIdleCallbacks,
738 $this->trxPreCommitCallbacks,
739 $this->trxEndCallbacks
740 ] as $callbacks ) {
741 foreach ( $callbacks as $callback ) {
742 $fnames[] = $callback[1];
743 }
744 }
745
746 return $fnames;
747 }
748
752 private function flatAtomicSectionList() {
753 return array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) {
754 return $accum === null ? $v[0] : "$accum, " . $v[0];
755 } );
756 }
757
758 public function isOpen() {
759 return $this->opened;
760 }
761
762 public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
763 if ( ( $flag & self::DBO_IGNORE ) ) {
764 throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
765 }
766
767 if ( $remember === self::REMEMBER_PRIOR ) {
768 array_push( $this->priorFlags, $this->flags );
769 }
770 $this->flags |= $flag;
771 }
772
773 public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
774 if ( ( $flag & self::DBO_IGNORE ) ) {
775 throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
776 }
777
778 if ( $remember === self::REMEMBER_PRIOR ) {
779 array_push( $this->priorFlags, $this->flags );
780 }
781 $this->flags &= ~$flag;
782 }
783
784 public function restoreFlags( $state = self::RESTORE_PRIOR ) {
785 if ( !$this->priorFlags ) {
786 return;
787 }
788
789 if ( $state === self::RESTORE_INITIAL ) {
790 $this->flags = reset( $this->priorFlags );
791 $this->priorFlags = [];
792 } else {
793 $this->flags = array_pop( $this->priorFlags );
794 }
795 }
796
797 public function getFlag( $flag ) {
798 return !!( $this->flags & $flag );
799 }
800
806 public function getProperty( $name ) {
807 return $this->$name;
808 }
809
810 public function getDomainID() {
811 return $this->currentDomain->getId();
812 }
813
814 final public function getWikiID() {
815 return $this->getDomainID();
816 }
817
825 abstract function indexInfo( $table, $index, $fname = __METHOD__ );
826
833 abstract function strencode( $s );
834
838 protected function installErrorHandler() {
839 $this->phpError = false;
840 $this->htmlErrors = ini_set( 'html_errors', '0' );
841 set_error_handler( [ $this, 'connectionErrorLogger' ] );
842 }
843
849 protected function restoreErrorHandler() {
850 restore_error_handler();
851 if ( $this->htmlErrors !== false ) {
852 ini_set( 'html_errors', $this->htmlErrors );
853 }
854
855 return $this->getLastPHPError();
856 }
857
861 protected function getLastPHPError() {
862 if ( $this->phpError ) {
863 $error = preg_replace( '!\[<a.*</a>\]!', '', $this->phpError );
864 $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
865
866 return $error;
867 }
868
869 return false;
870 }
871
879 public function connectionErrorLogger( $errno, $errstr ) {
880 $this->phpError = $errstr;
881 }
882
889 protected function getLogContext( array $extras = [] ) {
890 return array_merge(
891 [
892 'db_server' => $this->server,
893 'db_name' => $this->dbName,
894 'db_user' => $this->user,
895 ],
896 $extras
897 );
898 }
899
900 final public function close() {
901 $exception = null; // error to throw after disconnecting
902
903 if ( $this->conn ) {
904 // Resolve any dangling transaction first
905 if ( $this->trxLevel ) {
906 if ( $this->trxAtomicLevels ) {
907 // Cannot let incomplete atomic sections be committed
908 $levels = $this->flatAtomicSectionList();
909 $exception = new DBUnexpectedError(
910 $this,
911 __METHOD__ . ": atomic sections $levels are still open."
912 );
913 } elseif ( $this->trxAutomatic ) {
914 // Only the connection manager can commit non-empty DBO_TRX transactions
915 if ( $this->writesOrCallbacksPending() ) {
916 $exception = new DBUnexpectedError(
917 $this,
918 __METHOD__ .
919 ": mass commit/rollback of peer transaction required (DBO_TRX set)."
920 );
921 }
922 } elseif ( $this->trxLevel ) {
923 // Commit explicit transactions as if this was commit()
924 $this->queryLogger->warning(
925 __METHOD__ . ": writes or callbacks still pending.",
926 [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
927 );
928 }
929
930 if ( $this->trxEndCallbacksSuppressed ) {
931 $exception = $exception ?: new DBUnexpectedError(
932 $this,
933 __METHOD__ . ': callbacks are suppressed; cannot properly commit.'
934 );
935 }
936
937 // Commit or rollback the changes and run any callbacks as needed
938 if ( $this->trxStatus === self::STATUS_TRX_OK && !$exception ) {
939 $this->commit(
940 __METHOD__,
941 $this->trxAutomatic ? self::FLUSHING_INTERNAL : self::FLUSHING_ONE
942 );
943 } else {
944 $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
945 }
946 }
947
948 // Close the actual connection in the binding handle
949 $closed = $this->closeConnection();
950 $this->conn = false;
951 } else {
952 $closed = true; // already closed; nothing to do
953 }
954
955 $this->opened = false;
956
957 // Throw any unexpected errors after having disconnected
958 if ( $exception instanceof Exception ) {
959 throw $exception;
960 }
961
962 // Sanity check that no callbacks are dangling
963 if (
964 $this->trxIdleCallbacks || $this->trxPreCommitCallbacks || $this->trxEndCallbacks
965 ) {
966 throw new RuntimeException(
967 "Transaction callbacks are still pending:\n" .
968 implode( ', ', $this->pendingWriteAndCallbackCallers() )
969 );
970 }
971
972 return $closed;
973 }
974
980 protected function assertOpen() {
981 if ( !$this->isOpen() ) {
982 throw new DBUnexpectedError( $this, "DB connection was already closed." );
983 }
984 }
985
991 abstract protected function closeConnection();
992
997 public function reportConnectionError( $error = 'Unknown error' ) {
998 $myError = $this->lastError();
999 if ( $myError ) {
1000 $error = $myError;
1001 }
1002
1003 # New method
1004 throw new DBConnectionError( $this, $error );
1005 }
1006
1016 abstract protected function doQuery( $sql );
1017
1025 protected function isWriteQuery( $sql ) {
1026 return !preg_match(
1027 '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\‍(SELECT)\b/i', $sql );
1028 }
1029
1034 protected function getQueryVerb( $sql ) {
1035 return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
1036 }
1037
1047 protected function isTransactableQuery( $sql ) {
1048 return !in_array(
1049 $this->getQueryVerb( $sql ),
1050 [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET', 'CREATE', 'ALTER' ],
1051 true
1052 );
1053 }
1054
1059 protected function registerTempTableOperation( $sql ) {
1060 if ( preg_match(
1061 '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
1062 $sql,
1063 $matches
1064 ) ) {
1065 $this->sessionTempTables[$matches[1]] = 1;
1066
1067 return true;
1068 } elseif ( preg_match(
1069 '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
1070 $sql,
1071 $matches
1072 ) ) {
1073 $isTemp = isset( $this->sessionTempTables[$matches[1]] );
1074 unset( $this->sessionTempTables[$matches[1]] );
1075
1076 return $isTemp;
1077 } elseif ( preg_match(
1078 '/^TRUNCATE\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
1079 $sql,
1080 $matches
1081 ) ) {
1082 return isset( $this->sessionTempTables[$matches[1]] );
1083 } elseif ( preg_match(
1084 '/^(?:INSERT\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+[`"\']?(\w+)[`"\']?/i',
1085 $sql,
1086 $matches
1087 ) ) {
1088 return isset( $this->sessionTempTables[$matches[1]] );
1089 }
1090
1091 return false;
1092 }
1093
1094 public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
1095 $this->assertTransactionStatus( $sql, $fname );
1096
1097 # Avoid fatals if close() was called
1098 $this->assertOpen();
1099
1100 $priorWritesPending = $this->writesOrCallbacksPending();
1101 $this->lastQuery = $sql;
1102
1103 $isWrite = $this->isWriteQuery( $sql );
1104 if ( $isWrite ) {
1105 $isNonTempWrite = !$this->registerTempTableOperation( $sql );
1106 } else {
1107 $isNonTempWrite = false;
1108 }
1109
1110 if ( $isWrite ) {
1111 if ( $this->getLBInfo( 'replica' ) === true ) {
1112 throw new DBError(
1113 $this,
1114 'Write operations are not allowed on replica database connections.'
1115 );
1116 }
1117 # In theory, non-persistent writes are allowed in read-only mode, but due to things
1118 # like https://bugs.mysql.com/bug.php?id=33669 that might not work anyway...
1119 $reason = $this->getReadOnlyReason();
1120 if ( $reason !== false ) {
1121 throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
1122 }
1123 # Set a flag indicating that writes have been done
1124 $this->lastWriteTime = microtime( true );
1125 }
1126
1127 # Add trace comment to the begin of the sql string, right after the operator.
1128 # Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
1129 $commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
1130
1131 # Start implicit transactions that wrap the request if DBO_TRX is enabled
1132 if ( !$this->trxLevel && $this->getFlag( self::DBO_TRX )
1133 && $this->isTransactableQuery( $sql )
1134 ) {
1135 $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
1136 $this->trxAutomatic = true;
1137 }
1138
1139 # Keep track of whether the transaction has write queries pending
1140 if ( $this->trxLevel && !$this->trxDoneWrites && $isWrite ) {
1141 $this->trxDoneWrites = true;
1142 $this->trxProfiler->transactionWritingIn(
1143 $this->server, $this->dbName, $this->trxShortId );
1144 }
1145
1146 if ( $this->getFlag( self::DBO_DEBUG ) ) {
1147 $this->queryLogger->debug( "{$this->dbName} {$commentedSql}" );
1148 }
1149
1150 # Send the query to the server and fetch any corresponding errors
1151 $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
1152 $lastError = $this->lastError();
1153 $lastErrno = $this->lastErrno();
1154
1155 # Try reconnecting if the connection was lost
1156 if ( $ret === false && $this->wasConnectionLoss() ) {
1157 # Check if any meaningful session state was lost
1158 $recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
1159 # Update session state tracking and try to restore the connection
1160 $reconnected = $this->replaceLostConnection( __METHOD__ );
1161 # Silently resend the query to the server if it is safe and possible
1162 if ( $reconnected && $recoverable ) {
1163 $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
1164 $lastError = $this->lastError();
1165 $lastErrno = $this->lastErrno();
1166
1167 if ( $ret === false && $this->wasConnectionLoss() ) {
1168 # Query probably causes disconnects; reconnect and do not re-run it
1169 $this->replaceLostConnection( __METHOD__ );
1170 }
1171 }
1172 }
1173
1174 if ( $ret === false ) {
1175 if ( $this->trxLevel ) {
1176 if ( !$this->wasKnownStatementRollbackError() ) {
1177 # Either the query was aborted or all queries after BEGIN where aborted.
1178 if ( $this->explicitTrxActive() || $priorWritesPending ) {
1179 # In the first case, the only options going forward are (a) ROLLBACK, or
1180 # (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
1181 # option is ROLLBACK, since the snapshots would have been released.
1182 $this->trxStatus = self::STATUS_TRX_ERROR;
1183 $this->trxStatusCause =
1184 $this->makeQueryException( $lastError, $lastErrno, $sql, $fname );
1185 $tempIgnore = false; // cannot recover
1186 } else {
1187 # Nothing prior was there to lose from the transaction,
1188 # so just roll it back.
1189 $this->rollback( __METHOD__ . " ($fname)", self::FLUSHING_INTERNAL );
1190 }
1191 $this->trxStatusIgnoredCause = null;
1192 } else {
1193 # We're ignoring an error that caused just the current query to be aborted.
1194 # But log the cause so we can log a deprecation notice if a
1195 # caller actually does ignore it.
1196 $this->trxStatusIgnoredCause = [ $lastError, $lastErrno, $fname ];
1197 }
1198 }
1199
1200 $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $tempIgnore );
1201 }
1202
1203 return $this->resultObject( $ret );
1204 }
1205
1216 private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) {
1217 $isMaster = !is_null( $this->getLBInfo( 'master' ) );
1218 # generalizeSQL() will probably cut down the query to reasonable
1219 # logging size most of the time. The substr is really just a sanity check.
1220 if ( $isMaster ) {
1221 $queryProf = 'query-m: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
1222 } else {
1223 $queryProf = 'query: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
1224 }
1225
1226 # Include query transaction state
1227 $queryProf .= $this->trxShortId ? " [TRX#{$this->trxShortId}]" : "";
1228
1229 $startTime = microtime( true );
1230 if ( $this->profiler ) {
1231 call_user_func( [ $this->profiler, 'profileIn' ], $queryProf );
1232 }
1233 $this->affectedRowCount = null;
1234 $ret = $this->doQuery( $commentedSql );
1235 $this->affectedRowCount = $this->affectedRows();
1236 if ( $this->profiler ) {
1237 call_user_func( [ $this->profiler, 'profileOut' ], $queryProf );
1238 }
1239 $queryRuntime = max( microtime( true ) - $startTime, 0.0 );
1240
1241 unset( $queryProfSection ); // profile out (if set)
1242
1243 if ( $ret !== false ) {
1244 $this->lastPing = $startTime;
1245 if ( $isWrite && $this->trxLevel ) {
1246 $this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() );
1247 $this->trxWriteCallers[] = $fname;
1248 }
1249 }
1250
1251 if ( $sql === self::PING_QUERY ) {
1252 $this->rttEstimate = $queryRuntime;
1253 }
1254
1255 $this->trxProfiler->recordQueryCompletion(
1256 $queryProf, $startTime, $isWrite, $this->affectedRows()
1257 );
1258 $this->queryLogger->debug( $sql, [
1259 'method' => $fname,
1260 'master' => $isMaster,
1261 'runtime' => $queryRuntime,
1262 ] );
1263
1264 return $ret;
1265 }
1266
1279 private function updateTrxWriteQueryTime( $sql, $runtime, $affected ) {
1280 // Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
1281 $indicativeOfReplicaRuntime = true;
1282 if ( $runtime > self::SLOW_WRITE_SEC ) {
1283 $verb = $this->getQueryVerb( $sql );
1284 // insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
1285 if ( $verb === 'INSERT' ) {
1286 $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS;
1287 } elseif ( $verb === 'REPLACE' ) {
1288 $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS / 2;
1289 }
1290 }
1291
1292 $this->trxWriteDuration += $runtime;
1293 $this->trxWriteQueryCount += 1;
1294 $this->trxWriteAffectedRows += $affected;
1295 if ( $indicativeOfReplicaRuntime ) {
1296 $this->trxWriteAdjDuration += $runtime;
1297 $this->trxWriteAdjQueryCount += 1;
1298 }
1299 }
1300
1306 private function assertTransactionStatus( $sql, $fname ) {
1307 if ( $this->getQueryVerb( $sql ) === 'ROLLBACK' ) { // transaction/savepoint
1308 return;
1309 }
1310
1311 if ( $this->trxStatus < self::STATUS_TRX_OK ) {
1312 throw new DBTransactionStateError(
1313 $this,
1314 "Cannot execute query from $fname while transaction status is ERROR.",
1315 [],
1316 $this->trxStatusCause
1317 );
1318 } elseif ( $this->trxStatus === self::STATUS_TRX_OK && $this->trxStatusIgnoredCause ) {
1319 list( $iLastError, $iLastErrno, $iFname ) = $this->trxStatusIgnoredCause;
1320 call_user_func( $this->deprecationLogger,
1321 "Caller from $fname ignored an error originally raised from $iFname: " .
1322 "[$iLastErrno] $iLastError"
1323 );
1324 $this->trxStatusIgnoredCause = null;
1325 }
1326 }
1327
1338 private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
1339 # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
1340 # Dropped connections also mean that named locks are automatically released.
1341 # Only allow error suppression in autocommit mode or when the lost transaction
1342 # didn't matter anyway (aside from DBO_TRX snapshot loss).
1343 if ( $this->namedLocksHeld ) {
1344 return false; // possible critical section violation
1345 } elseif ( $this->sessionTempTables ) {
1346 return false; // tables might be queried latter
1347 } elseif ( $sql === 'COMMIT' ) {
1348 return !$priorWritesPending; // nothing written anyway? (T127428)
1349 } elseif ( $sql === 'ROLLBACK' ) {
1350 return true; // transaction lost...which is also what was requested :)
1351 } elseif ( $this->explicitTrxActive() ) {
1352 return false; // don't drop atomocity and explicit snapshots
1353 } elseif ( $priorWritesPending ) {
1354 return false; // prior writes lost from implicit transaction
1355 }
1356
1357 return true;
1358 }
1359
1363 private function handleSessionLoss() {
1364 // Clean up tracking of session-level things...
1365 // https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html
1366 // https://www.postgresql.org/docs/9.2/static/sql-createtable.html (ignoring ON COMMIT)
1367 $this->sessionTempTables = [];
1368 // https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
1369 // https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
1370 $this->namedLocksHeld = [];
1371 // Session loss implies transaction loss
1372 $this->handleTransactionLoss();
1373 }
1374
1378 private function handleTransactionLoss() {
1379 $this->trxLevel = 0;
1380 $this->trxAtomicCounter = 0;
1381 $this->trxIdleCallbacks = []; // T67263; transaction already lost
1382 $this->trxPreCommitCallbacks = []; // T67263; transaction already lost
1383 try {
1384 // Handle callbacks in trxEndCallbacks, e.g. onTransactionResolution().
1385 // If callback suppression is set then the array will remain unhandled.
1386 $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
1387 } catch ( Exception $ex ) {
1388 // Already logged; move on...
1389 }
1390 try {
1391 // Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener()
1392 $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
1393 } catch ( Exception $ex ) {
1394 // Already logged; move on...
1395 }
1396 }
1397
1408 protected function wasQueryTimeout( $error, $errno ) {
1409 return false;
1410 }
1411
1423 public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
1424 if ( $tempIgnore ) {
1425 $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
1426 } else {
1427 $exception = $this->makeQueryException( $error, $errno, $sql, $fname );
1428
1429 throw $exception;
1430 }
1431 }
1432
1440 private function makeQueryException( $error, $errno, $sql, $fname ) {
1441 $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
1442 $this->queryLogger->error(
1443 "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
1444 $this->getLogContext( [
1445 'method' => __METHOD__,
1446 'errno' => $errno,
1447 'error' => $error,
1448 'sql1line' => $sql1line,
1449 'fname' => $fname,
1450 ] )
1451 );
1452 $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
1453 $wasQueryTimeout = $this->wasQueryTimeout( $error, $errno );
1454 if ( $wasQueryTimeout ) {
1455 $e = new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
1456 } else {
1457 $e = new DBQueryError( $this, $error, $errno, $sql, $fname );
1458 }
1459
1460 return $e;
1461 }
1462
1463 public function freeResult( $res ) {
1464 }
1465
1466 public function selectField(
1467 $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1468 ) {
1469 if ( $var === '*' ) { // sanity
1470 throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1471 }
1472
1473 if ( !is_array( $options ) ) {
1474 $options = [ $options ];
1475 }
1476
1477 $options['LIMIT'] = 1;
1478
1479 $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1480 if ( $res === false || !$this->numRows( $res ) ) {
1481 return false;
1482 }
1483
1484 $row = $this->fetchRow( $res );
1485
1486 if ( $row !== false ) {
1487 return reset( $row );
1488 } else {
1489 return false;
1490 }
1491 }
1492
1493 public function selectFieldValues(
1494 $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1495 ) {
1496 if ( $var === '*' ) { // sanity
1497 throw new DBUnexpectedError( $this, "Cannot use a * field" );
1498 } elseif ( !is_string( $var ) ) { // sanity
1499 throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1500 }
1501
1502 if ( !is_array( $options ) ) {
1503 $options = [ $options ];
1504 }
1505
1506 $res = $this->select( $table, [ 'value' => $var ], $cond, $fname, $options, $join_conds );
1507 if ( $res === false ) {
1508 return false;
1509 }
1510
1511 $values = [];
1512 foreach ( $res as $row ) {
1513 $values[] = $row->value;
1514 }
1515
1516 return $values;
1517 }
1518
1528 protected function makeSelectOptions( $options ) {
1529 $preLimitTail = $postLimitTail = '';
1530 $startOpts = '';
1531
1532 $noKeyOptions = [];
1533
1534 foreach ( $options as $key => $option ) {
1535 if ( is_numeric( $key ) ) {
1536 $noKeyOptions[$option] = true;
1537 }
1538 }
1539
1540 $preLimitTail .= $this->makeGroupByWithHaving( $options );
1541
1542 $preLimitTail .= $this->makeOrderBy( $options );
1543
1544 if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1545 $postLimitTail .= ' FOR UPDATE';
1546 }
1547
1548 if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1549 $postLimitTail .= ' LOCK IN SHARE MODE';
1550 }
1551
1552 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1553 $startOpts .= 'DISTINCT';
1554 }
1555
1556 # Various MySQL extensions
1557 if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1558 $startOpts .= ' /*! STRAIGHT_JOIN */';
1559 }
1560
1561 if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
1562 $startOpts .= ' HIGH_PRIORITY';
1563 }
1564
1565 if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1566 $startOpts .= ' SQL_BIG_RESULT';
1567 }
1568
1569 if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1570 $startOpts .= ' SQL_BUFFER_RESULT';
1571 }
1572
1573 if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1574 $startOpts .= ' SQL_SMALL_RESULT';
1575 }
1576
1577 if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1578 $startOpts .= ' SQL_CALC_FOUND_ROWS';
1579 }
1580
1581 if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
1582 $startOpts .= ' SQL_CACHE';
1583 }
1584
1585 if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
1586 $startOpts .= ' SQL_NO_CACHE';
1587 }
1588
1589 if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1590 $useIndex = $this->useIndexClause( $options['USE INDEX'] );
1591 } else {
1592 $useIndex = '';
1593 }
1594 if ( isset( $options['IGNORE INDEX'] ) && is_string( $options['IGNORE INDEX'] ) ) {
1595 $ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
1596 } else {
1597 $ignoreIndex = '';
1598 }
1599
1600 return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1601 }
1602
1611 protected function makeGroupByWithHaving( $options ) {
1612 $sql = '';
1613 if ( isset( $options['GROUP BY'] ) ) {
1614 $gb = is_array( $options['GROUP BY'] )
1615 ? implode( ',', $options['GROUP BY'] )
1616 : $options['GROUP BY'];
1617 $sql .= ' GROUP BY ' . $gb;
1618 }
1619 if ( isset( $options['HAVING'] ) ) {
1620 $having = is_array( $options['HAVING'] )
1621 ? $this->makeList( $options['HAVING'], self::LIST_AND )
1622 : $options['HAVING'];
1623 $sql .= ' HAVING ' . $having;
1624 }
1625
1626 return $sql;
1627 }
1628
1637 protected function makeOrderBy( $options ) {
1638 if ( isset( $options['ORDER BY'] ) ) {
1639 $ob = is_array( $options['ORDER BY'] )
1640 ? implode( ',', $options['ORDER BY'] )
1641 : $options['ORDER BY'];
1642
1643 return ' ORDER BY ' . $ob;
1644 }
1645
1646 return '';
1647 }
1648
1649 public function select( $table, $vars, $conds = '', $fname = __METHOD__,
1650 $options = [], $join_conds = [] ) {
1651 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1652
1653 return $this->query( $sql, $fname );
1654 }
1655
1656 public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1657 $options = [], $join_conds = []
1658 ) {
1659 if ( is_array( $vars ) ) {
1660 $vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1661 }
1662
1663 $options = (array)$options;
1664 $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1665 ? $options['USE INDEX']
1666 : [];
1667 $ignoreIndexes = (
1668 isset( $options['IGNORE INDEX'] ) &&
1669 is_array( $options['IGNORE INDEX'] )
1670 )
1671 ? $options['IGNORE INDEX']
1672 : [];
1673
1674 if ( is_array( $table ) ) {
1675 $from = ' FROM ' .
1676 $this->tableNamesWithIndexClauseOrJOIN(
1677 $table, $useIndexes, $ignoreIndexes, $join_conds );
1678 } elseif ( $table != '' ) {
1679 $from = ' FROM ' .
1680 $this->tableNamesWithIndexClauseOrJOIN(
1681 [ $table ], $useIndexes, $ignoreIndexes, [] );
1682 } else {
1683 $from = '';
1684 }
1685
1686 list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
1687 $this->makeSelectOptions( $options );
1688
1689 if ( is_array( $conds ) ) {
1690 $conds = $this->makeList( $conds, self::LIST_AND );
1691 }
1692
1693 if ( $conds === null || $conds === false ) {
1694 $this->queryLogger->warning(
1695 __METHOD__
1696 . ' called from '
1697 . $fname
1698 . ' with incorrect parameters: $conds must be a string or an array'
1699 );
1700 $conds = '';
1701 }
1702
1703 if ( $conds === '' ) {
1704 $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
1705 } elseif ( is_string( $conds ) ) {
1706 $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex " .
1707 "WHERE $conds $preLimitTail";
1708 } else {
1709 throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
1710 }
1711
1712 if ( isset( $options['LIMIT'] ) ) {
1713 $sql = $this->limitResult( $sql, $options['LIMIT'],
1714 isset( $options['OFFSET'] ) ? $options['OFFSET'] : false );
1715 }
1716 $sql = "$sql $postLimitTail";
1717
1718 if ( isset( $options['EXPLAIN'] ) ) {
1719 $sql = 'EXPLAIN ' . $sql;
1720 }
1721
1722 return $sql;
1723 }
1724
1725 public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1726 $options = [], $join_conds = []
1727 ) {
1728 $options = (array)$options;
1729 $options['LIMIT'] = 1;
1730 $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1731
1732 if ( $res === false ) {
1733 return false;
1734 }
1735
1736 if ( !$this->numRows( $res ) ) {
1737 return false;
1738 }
1739
1740 $obj = $this->fetchObject( $res );
1741
1742 return $obj;
1743 }
1744
1745 public function estimateRowCount(
1746 $table, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1747 ) {
1748 $conds = $this->normalizeConditions( $conds, $fname );
1749 $column = $this->extractSingleFieldFromList( $var );
1750 if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
1751 $conds[] = "$column IS NOT NULL";
1752 }
1753
1754 $res = $this->select(
1755 $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options, $join_conds
1756 );
1757 $row = $res ? $this->fetchRow( $res ) : [];
1758
1759 return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
1760 }
1761
1762 public function selectRowCount(
1763 $tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1764 ) {
1765 $conds = $this->normalizeConditions( $conds, $fname );
1766 $column = $this->extractSingleFieldFromList( $var );
1767 if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
1768 $conds[] = "$column IS NOT NULL";
1769 }
1770
1771 $res = $this->select(
1772 [
1773 'tmp_count' => $this->buildSelectSubquery(
1774 $tables,
1775 '1',
1776 $conds,
1777 $fname,
1778 $options,
1779 $join_conds
1780 )
1781 ],
1782 [ 'rowcount' => 'COUNT(*)' ],
1783 [],
1784 $fname
1785 );
1786 $row = $res ? $this->fetchRow( $res ) : [];
1787
1788 return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
1789 }
1790
1796 final protected function normalizeConditions( $conds, $fname ) {
1797 if ( $conds === null || $conds === false ) {
1798 $this->queryLogger->warning(
1799 __METHOD__
1800 . ' called from '
1801 . $fname
1802 . ' with incorrect parameters: $conds must be a string or an array'
1803 );
1804 $conds = '';
1805 }
1806
1807 if ( !is_array( $conds ) ) {
1808 $conds = ( $conds === '' ) ? [] : [ $conds ];
1809 }
1810
1811 return $conds;
1812 }
1813
1819 final protected function extractSingleFieldFromList( $var ) {
1820 if ( is_array( $var ) ) {
1821 if ( !$var ) {
1822 $column = null;
1823 } elseif ( count( $var ) == 1 ) {
1824 $column = isset( $var[0] ) ? $var[0] : reset( $var );
1825 } else {
1826 throw new DBUnexpectedError( $this, __METHOD__ . ': got multiple columns.' );
1827 }
1828 } else {
1829 $column = $var;
1830 }
1831
1832 return $column;
1833 }
1834
1843 protected static function generalizeSQL( $sql ) {
1844 # This does the same as the regexp below would do, but in such a way
1845 # as to avoid crashing php on some large strings.
1846 # $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
1847
1848 $sql = str_replace( "\\\\", '', $sql );
1849 $sql = str_replace( "\\'", '', $sql );
1850 $sql = str_replace( "\\\"", '', $sql );
1851 $sql = preg_replace( "/'.*'/s", "'X'", $sql );
1852 $sql = preg_replace( '/".*"/s', "'X'", $sql );
1853
1854 # All newlines, tabs, etc replaced by single space
1855 $sql = preg_replace( '/\s+/', ' ', $sql );
1856
1857 # All numbers => N,
1858 # except the ones surrounded by characters, e.g. l10n
1859 $sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
1860 $sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
1861
1862 return $sql;
1863 }
1864
1865 public function fieldExists( $table, $field, $fname = __METHOD__ ) {
1866 $info = $this->fieldInfo( $table, $field );
1867
1868 return (bool)$info;
1869 }
1870
1871 public function indexExists( $table, $index, $fname = __METHOD__ ) {
1872 if ( !$this->tableExists( $table ) ) {
1873 return null;
1874 }
1875
1876 $info = $this->indexInfo( $table, $index, $fname );
1877 if ( is_null( $info ) ) {
1878 return null;
1879 } else {
1880 return $info !== false;
1881 }
1882 }
1883
1884 public function tableExists( $table, $fname = __METHOD__ ) {
1885 $tableRaw = $this->tableName( $table, 'raw' );
1886 if ( isset( $this->sessionTempTables[$tableRaw] ) ) {
1887 return true; // already known to exist
1888 }
1889
1890 $table = $this->tableName( $table );
1891 $ignoreErrors = true;
1892 $res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname, $ignoreErrors );
1893
1894 return (bool)$res;
1895 }
1896
1897 public function indexUnique( $table, $index ) {
1898 $indexInfo = $this->indexInfo( $table, $index );
1899
1900 if ( !$indexInfo ) {
1901 return null;
1902 }
1903
1904 return !$indexInfo[0]->Non_unique;
1905 }
1906
1913 protected function makeInsertOptions( $options ) {
1914 return implode( ' ', $options );
1915 }
1916
1917 public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
1918 # No rows to insert, easy just return now
1919 if ( !count( $a ) ) {
1920 return true;
1921 }
1922
1923 $table = $this->tableName( $table );
1924
1925 if ( !is_array( $options ) ) {
1926 $options = [ $options ];
1927 }
1928
1929 $fh = null;
1930 if ( isset( $options['fileHandle'] ) ) {
1931 $fh = $options['fileHandle'];
1932 }
1934
1935 if ( isset( $a[0] ) && is_array( $a[0] ) ) {
1936 $multi = true;
1937 $keys = array_keys( $a[0] );
1938 } else {
1939 $multi = false;
1940 $keys = array_keys( $a );
1941 }
1942
1943 $sql = 'INSERT ' . $options .
1944 " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
1945
1946 if ( $multi ) {
1947 $first = true;
1948 foreach ( $a as $row ) {
1949 if ( $first ) {
1950 $first = false;
1951 } else {
1952 $sql .= ',';
1953 }
1954 $sql .= '(' . $this->makeList( $row ) . ')';
1955 }
1956 } else {
1957 $sql .= '(' . $this->makeList( $a ) . ')';
1958 }
1959
1960 if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
1961 return false;
1962 } elseif ( $fh !== null ) {
1963 return true;
1964 }
1965
1966 return (bool)$this->query( $sql, $fname );
1967 }
1968
1975 protected function makeUpdateOptionsArray( $options ) {
1976 if ( !is_array( $options ) ) {
1977 $options = [ $options ];
1978 }
1979
1980 $opts = [];
1981
1982 if ( in_array( 'IGNORE', $options ) ) {
1983 $opts[] = 'IGNORE';
1984 }
1985
1986 return $opts;
1987 }
1988
1995 protected function makeUpdateOptions( $options ) {
1996 $opts = $this->makeUpdateOptionsArray( $options );
1997
1998 return implode( ' ', $opts );
1999 }
2000
2001 public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
2002 $table = $this->tableName( $table );
2003 $opts = $this->makeUpdateOptions( $options );
2004 $sql = "UPDATE $opts $table SET " . $this->makeList( $values, self::LIST_SET );
2005
2006 if ( $conds !== [] && $conds !== '*' ) {
2007 $sql .= " WHERE " . $this->makeList( $conds, self::LIST_AND );
2008 }
2009
2010 return (bool)$this->query( $sql, $fname );
2011 }
2012
2013 public function makeList( $a, $mode = self::LIST_COMMA ) {
2014 if ( !is_array( $a ) ) {
2015 throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
2016 }
2017
2018 $first = true;
2019 $list = '';
2020
2021 foreach ( $a as $field => $value ) {
2022 if ( !$first ) {
2023 if ( $mode == self::LIST_AND ) {
2024 $list .= ' AND ';
2025 } elseif ( $mode == self::LIST_OR ) {
2026 $list .= ' OR ';
2027 } else {
2028 $list .= ',';
2029 }
2030 } else {
2031 $first = false;
2032 }
2033
2034 if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
2035 $list .= "($value)";
2036 } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
2037 $list .= "$value";
2038 } elseif (
2039 ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
2040 ) {
2041 // Remove null from array to be handled separately if found
2042 $includeNull = false;
2043 foreach ( array_keys( $value, null, true ) as $nullKey ) {
2044 $includeNull = true;
2045 unset( $value[$nullKey] );
2046 }
2047 if ( count( $value ) == 0 && !$includeNull ) {
2048 throw new InvalidArgumentException(
2049 __METHOD__ . ": empty input for field $field" );
2050 } elseif ( count( $value ) == 0 ) {
2051 // only check if $field is null
2052 $list .= "$field IS NULL";
2053 } else {
2054 // IN clause contains at least one valid element
2055 if ( $includeNull ) {
2056 // Group subconditions to ensure correct precedence
2057 $list .= '(';
2058 }
2059 if ( count( $value ) == 1 ) {
2060 // Special-case single values, as IN isn't terribly efficient
2061 // Don't necessarily assume the single key is 0; we don't
2062 // enforce linear numeric ordering on other arrays here.
2063 $value = array_values( $value )[0];
2064 $list .= $field . " = " . $this->addQuotes( $value );
2065 } else {
2066 $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
2067 }
2068 // if null present in array, append IS NULL
2069 if ( $includeNull ) {
2070 $list .= " OR $field IS NULL)";
2071 }
2072 }
2073 } elseif ( $value === null ) {
2074 if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
2075 $list .= "$field IS ";
2076 } elseif ( $mode == self::LIST_SET ) {
2077 $list .= "$field = ";
2078 }
2079 $list .= 'NULL';
2080 } else {
2081 if (
2082 $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
2083 ) {
2084 $list .= "$field = ";
2085 }
2086 $list .= $mode == self::LIST_NAMES ? $value : $this->addQuotes( $value );
2087 }
2088 }
2089
2090 return $list;
2091 }
2092
2093 public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
2094 $conds = [];
2095
2096 foreach ( $data as $base => $sub ) {
2097 if ( count( $sub ) ) {
2098 $conds[] = $this->makeList(
2099 [ $baseKey => $base, $subKey => array_keys( $sub ) ],
2100 self::LIST_AND );
2101 }
2102 }
2103
2104 if ( $conds ) {
2105 return $this->makeList( $conds, self::LIST_OR );
2106 } else {
2107 // Nothing to search for...
2108 return false;
2109 }
2110 }
2111
2112 public function aggregateValue( $valuedata, $valuename = 'value' ) {
2113 return $valuename;
2114 }
2115
2116 public function bitNot( $field ) {
2117 return "(~$field)";
2118 }
2119
2120 public function bitAnd( $fieldLeft, $fieldRight ) {
2121 return "($fieldLeft & $fieldRight)";
2122 }
2123
2124 public function bitOr( $fieldLeft, $fieldRight ) {
2125 return "($fieldLeft | $fieldRight)";
2126 }
2127
2128 public function buildConcat( $stringList ) {
2129 return 'CONCAT(' . implode( ',', $stringList ) . ')';
2130 }
2131
2132 public function buildGroupConcatField(
2133 $delim, $table, $field, $conds = '', $join_conds = []
2134 ) {
2135 $fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
2136
2137 return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
2138 }
2139
2140 public function buildSubstring( $input, $startPosition, $length = null ) {
2141 $this->assertBuildSubstringParams( $startPosition, $length );
2142 $functionBody = "$input FROM $startPosition";
2143 if ( $length !== null ) {
2144 $functionBody .= " FOR $length";
2145 }
2146 return 'SUBSTRING(' . $functionBody . ')';
2147 }
2148
2161 protected function assertBuildSubstringParams( $startPosition, $length ) {
2162 if ( !is_int( $startPosition ) || $startPosition <= 0 ) {
2163 throw new InvalidArgumentException(
2164 '$startPosition must be a positive integer'
2165 );
2166 }
2167 if ( !( is_int( $length ) && $length >= 0 || $length === null ) ) {
2168 throw new InvalidArgumentException(
2169 '$length must be null or an integer greater than or equal to 0'
2170 );
2171 }
2172 }
2173
2174 public function buildStringCast( $field ) {
2175 return $field;
2176 }
2177
2178 public function buildIntegerCast( $field ) {
2179 return 'CAST( ' . $field . ' AS INTEGER )';
2180 }
2181
2182 public function buildSelectSubquery(
2183 $table, $vars, $conds = '', $fname = __METHOD__,
2184 $options = [], $join_conds = []
2185 ) {
2186 return new Subquery(
2187 $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
2188 );
2189 }
2190
2191 public function databasesAreIndependent() {
2192 return false;
2193 }
2194
2195 public function selectDB( $db ) {
2196 # Stub. Shouldn't cause serious problems if it's not overridden, but
2197 # if your database engine supports a concept similar to MySQL's
2198 # databases you may as well.
2199 $this->dbName = $db;
2200
2201 return true;
2202 }
2203
2204 public function getDBname() {
2205 return $this->dbName;
2206 }
2207
2208 public function getServer() {
2209 return $this->server;
2210 }
2211
2212 public function tableName( $name, $format = 'quoted' ) {
2213 if ( $name instanceof Subquery ) {
2214 throw new DBUnexpectedError(
2215 $this,
2216 __METHOD__ . ': got Subquery instance when expecting a string.'
2217 );
2218 }
2219
2220 # Skip the entire process when we have a string quoted on both ends.
2221 # Note that we check the end so that we will still quote any use of
2222 # use of `database`.table. But won't break things if someone wants
2223 # to query a database table with a dot in the name.
2224 if ( $this->isQuotedIdentifier( $name ) ) {
2225 return $name;
2226 }
2227
2228 # Lets test for any bits of text that should never show up in a table
2229 # name. Basically anything like JOIN or ON which are actually part of
2230 # SQL queries, but may end up inside of the table value to combine
2231 # sql. Such as how the API is doing.
2232 # Note that we use a whitespace test rather than a \b test to avoid
2233 # any remote case where a word like on may be inside of a table name
2234 # surrounded by symbols which may be considered word breaks.
2235 if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
2236 $this->queryLogger->warning(
2237 __METHOD__ . ": use of subqueries is not supported this way.",
2238 [ 'exception' => new RuntimeException() ]
2239 );
2240
2241 return $name;
2242 }
2243
2244 # Split database and table into proper variables.
2245 list( $database, $schema, $prefix, $table ) = $this->qualifiedTableComponents( $name );
2246
2247 # Quote $table and apply the prefix if not quoted.
2248 # $tableName might be empty if this is called from Database::replaceVars()
2249 $tableName = "{$prefix}{$table}";
2250 if ( $format === 'quoted'
2251 && !$this->isQuotedIdentifier( $tableName )
2252 && $tableName !== ''
2253 ) {
2254 $tableName = $this->addIdentifierQuotes( $tableName );
2255 }
2256
2257 # Quote $schema and $database and merge them with the table name if needed
2258 $tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
2259 $tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
2260
2261 return $tableName;
2262 }
2263
2270 protected function qualifiedTableComponents( $name ) {
2271 # We reverse the explode so that database.table and table both output the correct table.
2272 $dbDetails = explode( '.', $name, 3 );
2273 if ( count( $dbDetails ) == 3 ) {
2274 list( $database, $schema, $table ) = $dbDetails;
2275 # We don't want any prefix added in this case
2276 $prefix = '';
2277 } elseif ( count( $dbDetails ) == 2 ) {
2278 list( $database, $table ) = $dbDetails;
2279 # We don't want any prefix added in this case
2280 $prefix = '';
2281 # In dbs that support it, $database may actually be the schema
2282 # but that doesn't affect any of the functionality here
2283 $schema = '';
2284 } else {
2285 list( $table ) = $dbDetails;
2286 if ( isset( $this->tableAliases[$table] ) ) {
2287 $database = $this->tableAliases[$table]['dbname'];
2288 $schema = is_string( $this->tableAliases[$table]['schema'] )
2289 ? $this->tableAliases[$table]['schema']
2290 : $this->schema;
2291 $prefix = is_string( $this->tableAliases[$table]['prefix'] )
2292 ? $this->tableAliases[$table]['prefix']
2294 } else {
2295 $database = '';
2296 $schema = $this->schema; # Default schema
2297 $prefix = $this->tablePrefix; # Default prefix
2298 }
2299 }
2300
2301 return [ $database, $schema, $prefix, $table ];
2302 }
2303
2310 private function prependDatabaseOrSchema( $namespace, $relation, $format ) {
2311 if ( strlen( $namespace ) ) {
2312 if ( $format === 'quoted' && !$this->isQuotedIdentifier( $namespace ) ) {
2313 $namespace = $this->addIdentifierQuotes( $namespace );
2314 }
2315 $relation = $namespace . '.' . $relation;
2316 }
2317
2318 return $relation;
2319 }
2320
2321 public function tableNames() {
2322 $inArray = func_get_args();
2323 $retVal = [];
2324
2325 foreach ( $inArray as $name ) {
2326 $retVal[$name] = $this->tableName( $name );
2327 }
2328
2329 return $retVal;
2330 }
2331
2332 public function tableNamesN() {
2333 $inArray = func_get_args();
2334 $retVal = [];
2335
2336 foreach ( $inArray as $name ) {
2337 $retVal[] = $this->tableName( $name );
2338 }
2339
2340 return $retVal;
2341 }
2342
2354 protected function tableNameWithAlias( $table, $alias = false ) {
2355 if ( is_string( $table ) ) {
2356 $quotedTable = $this->tableName( $table );
2357 } elseif ( $table instanceof Subquery ) {
2358 $quotedTable = (string)$table;
2359 } else {
2360 throw new InvalidArgumentException( "Table must be a string or Subquery." );
2361 }
2362
2363 if ( !strlen( $alias ) || $alias === $table ) {
2364 if ( $table instanceof Subquery ) {
2365 throw new InvalidArgumentException( "Subquery table missing alias." );
2366 }
2367
2368 return $quotedTable;
2369 } else {
2370 return $quotedTable . ' ' . $this->addIdentifierQuotes( $alias );
2371 }
2372 }
2373
2380 protected function tableNamesWithAlias( $tables ) {
2381 $retval = [];
2382 foreach ( $tables as $alias => $table ) {
2383 if ( is_numeric( $alias ) ) {
2384 $alias = $table;
2385 }
2386 $retval[] = $this->tableNameWithAlias( $table, $alias );
2387 }
2388
2389 return $retval;
2390 }
2391
2400 protected function fieldNameWithAlias( $name, $alias = false ) {
2401 if ( !$alias || (string)$alias === (string)$name ) {
2402 return $name;
2403 } else {
2404 return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
2405 }
2406 }
2407
2414 protected function fieldNamesWithAlias( $fields ) {
2415 $retval = [];
2416 foreach ( $fields as $alias => $field ) {
2417 if ( is_numeric( $alias ) ) {
2418 $alias = $field;
2419 }
2420 $retval[] = $this->fieldNameWithAlias( $field, $alias );
2421 }
2422
2423 return $retval;
2424 }
2425
2437 $tables, $use_index = [], $ignore_index = [], $join_conds = []
2438 ) {
2439 $ret = [];
2440 $retJOIN = [];
2441 $use_index = (array)$use_index;
2442 $ignore_index = (array)$ignore_index;
2443 $join_conds = (array)$join_conds;
2444
2445 foreach ( $tables as $alias => $table ) {
2446 if ( !is_string( $alias ) ) {
2447 // No alias? Set it equal to the table name
2448 $alias = $table;
2449 }
2450
2451 if ( is_array( $table ) ) {
2452 // A parenthesized group
2453 if ( count( $table ) > 1 ) {
2454 $joinedTable = '(' .
2456 $table, $use_index, $ignore_index, $join_conds ) . ')';
2457 } else {
2458 // Degenerate case
2459 $innerTable = reset( $table );
2460 $innerAlias = key( $table );
2461 $joinedTable = $this->tableNameWithAlias(
2462 $innerTable,
2463 is_string( $innerAlias ) ? $innerAlias : $innerTable
2464 );
2465 }
2466 } else {
2467 $joinedTable = $this->tableNameWithAlias( $table, $alias );
2468 }
2469
2470 // Is there a JOIN clause for this table?
2471 if ( isset( $join_conds[$alias] ) ) {
2472 list( $joinType, $conds ) = $join_conds[$alias];
2473 $tableClause = $joinType;
2474 $tableClause .= ' ' . $joinedTable;
2475 if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
2476 $use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
2477 if ( $use != '' ) {
2478 $tableClause .= ' ' . $use;
2479 }
2480 }
2481 if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
2482 $ignore = $this->ignoreIndexClause(
2483 implode( ',', (array)$ignore_index[$alias] ) );
2484 if ( $ignore != '' ) {
2485 $tableClause .= ' ' . $ignore;
2486 }
2487 }
2488 $on = $this->makeList( (array)$conds, self::LIST_AND );
2489 if ( $on != '' ) {
2490 $tableClause .= ' ON (' . $on . ')';
2491 }
2492
2493 $retJOIN[] = $tableClause;
2494 } elseif ( isset( $use_index[$alias] ) ) {
2495 // Is there an INDEX clause for this table?
2496 $tableClause = $joinedTable;
2497 $tableClause .= ' ' . $this->useIndexClause(
2498 implode( ',', (array)$use_index[$alias] )
2499 );
2500
2501 $ret[] = $tableClause;
2502 } elseif ( isset( $ignore_index[$alias] ) ) {
2503 // Is there an INDEX clause for this table?
2504 $tableClause = $joinedTable;
2505 $tableClause .= ' ' . $this->ignoreIndexClause(
2506 implode( ',', (array)$ignore_index[$alias] )
2507 );
2508
2509 $ret[] = $tableClause;
2510 } else {
2511 $tableClause = $joinedTable;
2512
2513 $ret[] = $tableClause;
2514 }
2515 }
2516
2517 // We can't separate explicit JOIN clauses with ',', use ' ' for those
2518 $implicitJoins = $ret ? implode( ',', $ret ) : "";
2519 $explicitJoins = $retJOIN ? implode( ' ', $retJOIN ) : "";
2520
2521 // Compile our final table clause
2522 return implode( ' ', [ $implicitJoins, $explicitJoins ] );
2523 }
2524
2531 protected function indexName( $index ) {
2532 return isset( $this->indexAliases[$index] )
2533 ? $this->indexAliases[$index]
2534 : $index;
2535 }
2536
2537 public function addQuotes( $s ) {
2538 if ( $s instanceof Blob ) {
2539 $s = $s->fetch();
2540 }
2541 if ( $s === null ) {
2542 return 'NULL';
2543 } elseif ( is_bool( $s ) ) {
2544 return (int)$s;
2545 } else {
2546 # This will also quote numeric values. This should be harmless,
2547 # and protects against weird problems that occur when they really
2548 # _are_ strings such as article titles and string->number->string
2549 # conversion is not 1:1.
2550 return "'" . $this->strencode( $s ) . "'";
2551 }
2552 }
2553
2563 public function addIdentifierQuotes( $s ) {
2564 return '"' . str_replace( '"', '""', $s ) . '"';
2565 }
2566
2576 public function isQuotedIdentifier( $name ) {
2577 return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
2578 }
2579
2585 protected function escapeLikeInternal( $s, $escapeChar = '`' ) {
2586 return str_replace( [ $escapeChar, '%', '_' ],
2587 [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ],
2588 $s );
2589 }
2590
2591 public function buildLike() {
2592 $params = func_get_args();
2593
2594 if ( count( $params ) > 0 && is_array( $params[0] ) ) {
2595 $params = $params[0];
2596 }
2597
2598 $s = '';
2599
2600 // We use ` instead of \ as the default LIKE escape character, since addQuotes()
2601 // may escape backslashes, creating problems of double escaping. The `
2602 // character has good cross-DBMS compatibility, avoiding special operators
2603 // in MS SQL like ^ and %
2604 $escapeChar = '`';
2605
2606 foreach ( $params as $value ) {
2607 if ( $value instanceof LikeMatch ) {
2608 $s .= $value->toString();
2609 } else {
2610 $s .= $this->escapeLikeInternal( $value, $escapeChar );
2611 }
2612 }
2613
2614 return ' LIKE ' .
2615 $this->addQuotes( $s ) . ' ESCAPE ' . $this->addQuotes( $escapeChar ) . ' ';
2616 }
2617
2618 public function anyChar() {
2619 return new LikeMatch( '_' );
2620 }
2621
2622 public function anyString() {
2623 return new LikeMatch( '%' );
2624 }
2625
2626 public function nextSequenceValue( $seqName ) {
2627 return null;
2628 }
2629
2640 public function useIndexClause( $index ) {
2641 return '';
2642 }
2643
2654 public function ignoreIndexClause( $index ) {
2655 return '';
2656 }
2657
2658 public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2659 if ( count( $rows ) == 0 ) {
2660 return;
2661 }
2662
2663 // Single row case
2664 if ( !is_array( reset( $rows ) ) ) {
2665 $rows = [ $rows ];
2666 }
2667
2668 try {
2669 $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
2671 foreach ( $rows as $row ) {
2672 // Delete rows which collide with this one
2673 $indexWhereClauses = [];
2674 foreach ( $uniqueIndexes as $index ) {
2675 $indexColumns = (array)$index;
2676 $indexRowValues = array_intersect_key( $row, array_flip( $indexColumns ) );
2677 if ( count( $indexRowValues ) != count( $indexColumns ) ) {
2678 throw new DBUnexpectedError(
2679 $this,
2680 'New record does not provide all values for unique key (' .
2681 implode( ', ', $indexColumns ) . ')'
2682 );
2683 } elseif ( in_array( null, $indexRowValues, true ) ) {
2684 throw new DBUnexpectedError(
2685 $this,
2686 'New record has a null value for unique key (' .
2687 implode( ', ', $indexColumns ) . ')'
2688 );
2689 }
2690 $indexWhereClauses[] = $this->makeList( $indexRowValues, LIST_AND );
2691 }
2692
2693 if ( $indexWhereClauses ) {
2694 $this->delete( $table, $this->makeList( $indexWhereClauses, LIST_OR ), $fname );
2695 $affectedRowCount += $this->affectedRows();
2696 }
2697
2698 // Now insert the row
2699 $this->insert( $table, $row, $fname );
2700 $affectedRowCount += $this->affectedRows();
2701 }
2702 $this->endAtomic( $fname );
2703 $this->affectedRowCount = $affectedRowCount;
2704 } catch ( Exception $e ) {
2705 $this->cancelAtomic( $fname );
2706 throw $e;
2707 }
2708 }
2709
2720 protected function nativeReplace( $table, $rows, $fname ) {
2721 $table = $this->tableName( $table );
2722
2723 # Single row case
2724 if ( !is_array( reset( $rows ) ) ) {
2725 $rows = [ $rows ];
2726 }
2727
2728 $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
2729 $first = true;
2730
2731 foreach ( $rows as $row ) {
2732 if ( $first ) {
2733 $first = false;
2734 } else {
2735 $sql .= ',';
2736 }
2737
2738 $sql .= '(' . $this->makeList( $row ) . ')';
2739 }
2740
2741 return $this->query( $sql, $fname );
2742 }
2743
2744 public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
2745 $fname = __METHOD__
2746 ) {
2747 if ( !count( $rows ) ) {
2748 return true; // nothing to do
2749 }
2750
2751 if ( !is_array( reset( $rows ) ) ) {
2752 $rows = [ $rows ];
2753 }
2754
2755 if ( count( $uniqueIndexes ) ) {
2756 $clauses = []; // list WHERE clauses that each identify a single row
2757 foreach ( $rows as $row ) {
2758 foreach ( $uniqueIndexes as $index ) {
2759 $index = is_array( $index ) ? $index : [ $index ]; // columns
2760 $rowKey = []; // unique key to this row
2761 foreach ( $index as $column ) {
2762 $rowKey[$column] = $row[$column];
2763 }
2764 $clauses[] = $this->makeList( $rowKey, self::LIST_AND );
2765 }
2766 }
2767 $where = [ $this->makeList( $clauses, self::LIST_OR ) ];
2768 } else {
2769 $where = false;
2770 }
2771
2773 try {
2774 $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
2775 # Update any existing conflicting row(s)
2776 if ( $where !== false ) {
2777 $ok = $this->update( $table, $set, $where, $fname );
2778 $affectedRowCount += $this->affectedRows();
2779 } else {
2780 $ok = true;
2781 }
2782 # Now insert any non-conflicting row(s)
2783 $ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
2784 $affectedRowCount += $this->affectedRows();
2785 $this->endAtomic( $fname );
2786 $this->affectedRowCount = $affectedRowCount;
2787 } catch ( Exception $e ) {
2788 $this->cancelAtomic( $fname );
2789 throw $e;
2790 }
2791
2792 return $ok;
2793 }
2794
2795 public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2796 $fname = __METHOD__
2797 ) {
2798 if ( !$conds ) {
2799 throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
2800 }
2801
2802 $delTable = $this->tableName( $delTable );
2803 $joinTable = $this->tableName( $joinTable );
2804 $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2805 if ( $conds != '*' ) {
2806 $sql .= 'WHERE ' . $this->makeList( $conds, self::LIST_AND );
2807 }
2808 $sql .= ')';
2809
2810 $this->query( $sql, $fname );
2811 }
2812
2813 public function textFieldSize( $table, $field ) {
2814 $table = $this->tableName( $table );
2815 $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
2816 $res = $this->query( $sql, __METHOD__ );
2817 $row = $this->fetchObject( $res );
2818
2819 $m = [];
2820
2821 if ( preg_match( '/\‍((.*)\‍)/', $row->Type, $m ) ) {
2822 $size = $m[1];
2823 } else {
2824 $size = -1;
2825 }
2826
2827 return $size;
2828 }
2829
2830 public function delete( $table, $conds, $fname = __METHOD__ ) {
2831 if ( !$conds ) {
2832 throw new DBUnexpectedError( $this, __METHOD__ . ' called with no conditions' );
2833 }
2834
2835 $table = $this->tableName( $table );
2836 $sql = "DELETE FROM $table";
2837
2838 if ( $conds != '*' ) {
2839 if ( is_array( $conds ) ) {
2840 $conds = $this->makeList( $conds, self::LIST_AND );
2841 }
2842 $sql .= ' WHERE ' . $conds;
2843 }
2844
2845 return $this->query( $sql, $fname );
2846 }
2847
2848 final public function insertSelect(
2849 $destTable, $srcTable, $varMap, $conds,
2850 $fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = []
2851 ) {
2852 static $hints = [ 'NO_AUTO_COLUMNS' ];
2853
2854 $insertOptions = (array)$insertOptions;
2855 $selectOptions = (array)$selectOptions;
2856
2857 if ( $this->cliMode && $this->isInsertSelectSafe( $insertOptions, $selectOptions ) ) {
2858 // For massive migrations with downtime, we don't want to select everything
2859 // into memory and OOM, so do all this native on the server side if possible.
2860 return $this->nativeInsertSelect(
2861 $destTable,
2862 $srcTable,
2863 $varMap,
2864 $conds,
2865 $fname,
2866 array_diff( $insertOptions, $hints ),
2867 $selectOptions,
2868 $selectJoinConds
2869 );
2870 }
2871
2872 return $this->nonNativeInsertSelect(
2873 $destTable,
2874 $srcTable,
2875 $varMap,
2876 $conds,
2877 $fname,
2878 array_diff( $insertOptions, $hints ),
2879 $selectOptions,
2880 $selectJoinConds
2881 );
2882 }
2883
2890 protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
2891 return true;
2892 }
2893
2909 protected function nonNativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
2910 $fname = __METHOD__,
2911 $insertOptions = [], $selectOptions = [], $selectJoinConds = []
2912 ) {
2913 // For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
2914 // on only the master (without needing row-based-replication). It also makes it easy to
2915 // know how big the INSERT is going to be.
2916 $fields = [];
2917 foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
2918 $fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
2919 }
2920 $selectOptions[] = 'FOR UPDATE';
2921 $res = $this->select(
2922 $srcTable, implode( ',', $fields ), $conds, $fname, $selectOptions, $selectJoinConds
2923 );
2924 if ( !$res ) {
2925 return false;
2926 }
2927
2928 try {
2930 $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
2931 $rows = [];
2932 $ok = true;
2933 foreach ( $res as $row ) {
2934 $rows[] = (array)$row;
2935
2936 // Avoid inserts that are too huge
2937 if ( count( $rows ) >= $this->nonNativeInsertSelectBatchSize ) {
2938 $ok = $this->insert( $destTable, $rows, $fname, $insertOptions );
2939 if ( !$ok ) {
2940 break;
2941 }
2942 $affectedRowCount += $this->affectedRows();
2943 $rows = [];
2944 }
2945 }
2946 if ( $rows && $ok ) {
2947 $ok = $this->insert( $destTable, $rows, $fname, $insertOptions );
2948 if ( $ok ) {
2949 $affectedRowCount += $this->affectedRows();
2950 }
2951 }
2952 if ( $ok ) {
2953 $this->endAtomic( $fname );
2954 $this->affectedRowCount = $affectedRowCount;
2955 } else {
2956 $this->cancelAtomic( $fname );
2957 }
2958 return $ok;
2959 } catch ( Exception $e ) {
2960 $this->cancelAtomic( $fname );
2961 throw $e;
2962 }
2963 }
2964
2980 protected function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
2981 $fname = __METHOD__,
2982 $insertOptions = [], $selectOptions = [], $selectJoinConds = []
2983 ) {
2984 $destTable = $this->tableName( $destTable );
2985
2986 if ( !is_array( $insertOptions ) ) {
2987 $insertOptions = [ $insertOptions ];
2988 }
2989
2990 $insertOptions = $this->makeInsertOptions( $insertOptions );
2991
2992 $selectSql = $this->selectSQLText(
2993 $srcTable,
2994 array_values( $varMap ),
2995 $conds,
2996 $fname,
2997 $selectOptions,
2998 $selectJoinConds
2999 );
3000
3001 $sql = "INSERT $insertOptions" .
3002 " INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ') ' .
3003 $selectSql;
3004
3005 return $this->query( $sql, $fname );
3006 }
3007
3027 public function limitResult( $sql, $limit, $offset = false ) {
3028 if ( !is_numeric( $limit ) ) {
3029 throw new DBUnexpectedError( $this,
3030 "Invalid non-numeric limit passed to limitResult()\n" );
3031 }
3032
3033 return "$sql LIMIT "
3034 . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
3035 . "{$limit} ";
3036 }
3037
3038 public function unionSupportsOrderAndLimit() {
3039 return true; // True for almost every DB supported
3040 }
3041
3042 public function unionQueries( $sqls, $all ) {
3043 $glue = $all ? ') UNION ALL (' : ') UNION (';
3044
3045 return '(' . implode( $glue, $sqls ) . ')';
3046 }
3047
3049 $table, $vars, array $permute_conds, $extra_conds = '', $fname = __METHOD__,
3050 $options = [], $join_conds = []
3051 ) {
3052 // First, build the Cartesian product of $permute_conds
3053 $conds = [ [] ];
3054 foreach ( $permute_conds as $field => $values ) {
3055 if ( !$values ) {
3056 // Skip empty $values
3057 continue;
3058 }
3059 $values = array_unique( $values ); // For sanity
3060 $newConds = [];
3061 foreach ( $conds as $cond ) {
3062 foreach ( $values as $value ) {
3063 $cond[$field] = $value;
3064 $newConds[] = $cond; // Arrays are by-value, not by-reference, so this works
3065 }
3066 }
3067 $conds = $newConds;
3068 }
3069
3070 $extra_conds = $extra_conds === '' ? [] : (array)$extra_conds;
3071
3072 // If there's just one condition and no subordering, hand off to
3073 // selectSQLText directly.
3074 if ( count( $conds ) === 1 &&
3075 ( !isset( $options['INNER ORDER BY'] ) || !$this->unionSupportsOrderAndLimit() )
3076 ) {
3077 return $this->selectSQLText(
3078 $table, $vars, $conds[0] + $extra_conds, $fname, $options, $join_conds
3079 );
3080 }
3081
3082 // Otherwise, we need to pull out the order and limit to apply after
3083 // the union. Then build the SQL queries for each set of conditions in
3084 // $conds. Then union them together (using UNION ALL, because the
3085 // product *should* already be distinct).
3086 $orderBy = $this->makeOrderBy( $options );
3087 $limit = isset( $options['LIMIT'] ) ? $options['LIMIT'] : null;
3088 $offset = isset( $options['OFFSET'] ) ? $options['OFFSET'] : false;
3089 $all = empty( $options['NOTALL'] ) && !in_array( 'NOTALL', $options );
3090 if ( !$this->unionSupportsOrderAndLimit() ) {
3091 unset( $options['ORDER BY'], $options['LIMIT'], $options['OFFSET'] );
3092 } else {
3093 if ( array_key_exists( 'INNER ORDER BY', $options ) ) {
3094 $options['ORDER BY'] = $options['INNER ORDER BY'];
3095 }
3096 if ( $limit !== null && is_numeric( $offset ) && $offset != 0 ) {
3097 // We need to increase the limit by the offset rather than
3098 // using the offset directly, otherwise it'll skip incorrectly
3099 // in the subqueries.
3100 $options['LIMIT'] = $limit + $offset;
3101 unset( $options['OFFSET'] );
3102 }
3103 }
3104
3105 $sqls = [];
3106 foreach ( $conds as $cond ) {
3107 $sqls[] = $this->selectSQLText(
3108 $table, $vars, $cond + $extra_conds, $fname, $options, $join_conds
3109 );
3110 }
3111 $sql = $this->unionQueries( $sqls, $all ) . $orderBy;
3112 if ( $limit !== null ) {
3113 $sql = $this->limitResult( $sql, $limit, $offset );
3114 }
3115
3116 return $sql;
3117 }
3118
3119 public function conditional( $cond, $trueVal, $falseVal ) {
3120 if ( is_array( $cond ) ) {
3121 $cond = $this->makeList( $cond, self::LIST_AND );
3122 }
3123
3124 return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
3125 }
3126
3127 public function strreplace( $orig, $old, $new ) {
3128 return "REPLACE({$orig}, {$old}, {$new})";
3129 }
3130
3131 public function getServerUptime() {
3132 return 0;
3133 }
3134
3135 public function wasDeadlock() {
3136 return false;
3137 }
3138
3139 public function wasLockTimeout() {
3140 return false;
3141 }
3142
3143 public function wasConnectionLoss() {
3144 return $this->wasConnectionError( $this->lastErrno() );
3145 }
3146
3147 public function wasReadOnlyError() {
3148 return false;
3149 }
3150
3151 public function wasErrorReissuable() {
3152 return (
3153 $this->wasDeadlock() ||
3154 $this->wasLockTimeout() ||
3155 $this->wasConnectionLoss()
3156 );
3157 }
3158
3165 public function wasConnectionError( $errno ) {
3166 return false;
3167 }
3168
3175 protected function wasKnownStatementRollbackError() {
3176 return false; // don't know; it could have caused a transaction rollback
3177 }
3178
3179 public function deadlockLoop() {
3180 $args = func_get_args();
3181 $function = array_shift( $args );
3182 $tries = self::DEADLOCK_TRIES;
3183
3184 $this->begin( __METHOD__ );
3185
3186 $retVal = null;
3188 $e = null;
3189 do {
3190 try {
3191 $retVal = call_user_func_array( $function, $args );
3192 break;
3193 } catch ( DBQueryError $e ) {
3194 if ( $this->wasDeadlock() ) {
3195 // Retry after a randomized delay
3196 usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
3197 } else {
3198 // Throw the error back up
3199 throw $e;
3200 }
3201 }
3202 } while ( --$tries > 0 );
3203
3204 if ( $tries <= 0 ) {
3205 // Too many deadlocks; give up
3206 $this->rollback( __METHOD__ );
3207 throw $e;
3208 } else {
3209 $this->commit( __METHOD__ );
3210
3211 return $retVal;
3212 }
3213 }
3214
3215 public function masterPosWait( DBMasterPos $pos, $timeout ) {
3216 # Real waits are implemented in the subclass.
3217 return 0;
3218 }
3219
3220 public function getReplicaPos() {
3221 # Stub
3222 return false;
3223 }
3224
3225 public function getMasterPos() {
3226 # Stub
3227 return false;
3228 }
3229
3230 public function serverIsReadOnly() {
3231 return false;
3232 }
3233
3234 final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
3235 if ( !$this->trxLevel ) {
3236 throw new DBUnexpectedError( $this, "No transaction is active." );
3237 }
3238 $this->trxEndCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3239 }
3240
3241 final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
3242 if ( !$this->trxLevel && $this->getTransactionRoundId() ) {
3243 // Start an implicit transaction similar to how query() does
3244 $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
3245 $this->trxAutomatic = true;
3246 }
3247
3248 $this->trxIdleCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3249 if ( !$this->trxLevel ) {
3250 $this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
3251 }
3252 }
3253
3254 final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
3255 if ( !$this->trxLevel && $this->getTransactionRoundId() ) {
3256 // Start an implicit transaction similar to how query() does
3257 $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
3258 $this->trxAutomatic = true;
3259 }
3260
3261 if ( $this->trxLevel ) {
3262 $this->trxPreCommitCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
3263 } else {
3264 // No transaction is active nor will start implicitly, so make one for this callback
3265 $this->startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
3266 try {
3267 call_user_func( $callback );
3268 $this->endAtomic( __METHOD__ );
3269 } catch ( Exception $e ) {
3270 $this->cancelAtomic( __METHOD__ );
3271 throw $e;
3272 }
3273 }
3274 }
3275
3279 private function currentAtomicSectionId() {
3280 if ( $this->trxLevel && $this->trxAtomicLevels ) {
3281 $levelInfo = end( $this->trxAtomicLevels );
3282
3283 return $levelInfo[1];
3284 }
3285
3286 return null;
3287 }
3288
3295 ) {
3296 foreach ( $this->trxPreCommitCallbacks as $key => $info ) {
3297 if ( $info[2] === $old ) {
3298 $this->trxPreCommitCallbacks[$key][2] = $new;
3299 }
3300 }
3301 foreach ( $this->trxIdleCallbacks as $key => $info ) {
3302 if ( $info[2] === $old ) {
3303 $this->trxIdleCallbacks[$key][2] = $new;
3304 }
3305 }
3306 foreach ( $this->trxEndCallbacks as $key => $info ) {
3307 if ( $info[2] === $old ) {
3308 $this->trxEndCallbacks[$key][2] = $new;
3309 }
3310 }
3311 }
3312
3317 private function modifyCallbacksForCancel( array $sectionIds ) {
3318 // Cancel the "on commit" callbacks owned by this savepoint
3319 $this->trxIdleCallbacks = array_filter(
3320 $this->trxIdleCallbacks,
3321 function ( $entry ) use ( $sectionIds ) {
3322 return !in_array( $entry[2], $sectionIds, true );
3323 }
3324 );
3325 $this->trxPreCommitCallbacks = array_filter(
3326 $this->trxPreCommitCallbacks,
3327 function ( $entry ) use ( $sectionIds ) {
3328 return !in_array( $entry[2], $sectionIds, true );
3329 }
3330 );
3331 // Make "on resolution" callbacks owned by this savepoint to perceive a rollback
3332 foreach ( $this->trxEndCallbacks as $key => $entry ) {
3333 if ( in_array( $entry[2], $sectionIds, true ) ) {
3334 $callback = $entry[0];
3335 $this->trxEndCallbacks[$key][0] = function () use ( $callback ) {
3336 return $callback( self::TRIGGER_ROLLBACK );
3337 };
3338 }
3339 }
3340 }
3341
3342 final public function setTransactionListener( $name, callable $callback = null ) {
3343 if ( $callback ) {
3344 $this->trxRecurringCallbacks[$name] = $callback;
3345 } else {
3346 unset( $this->trxRecurringCallbacks[$name] );
3347 }
3348 }
3349
3358 final public function setTrxEndCallbackSuppression( $suppress ) {
3359 $this->trxEndCallbacksSuppressed = $suppress;
3360 }
3361
3371 public function runOnTransactionIdleCallbacks( $trigger ) {
3372 if ( $this->trxEndCallbacksSuppressed ) {
3373 return;
3374 }
3375
3376 $autoTrx = $this->getFlag( self::DBO_TRX ); // automatic begin() enabled?
3378 $e = null; // first exception
3379 do { // callbacks may add callbacks :)
3380 $callbacks = array_merge(
3381 $this->trxIdleCallbacks,
3382 $this->trxEndCallbacks // include "transaction resolution" callbacks
3383 );
3384 $this->trxIdleCallbacks = []; // consumed (and recursion guard)
3385 $this->trxEndCallbacks = []; // consumed (recursion guard)
3386 foreach ( $callbacks as $callback ) {
3387 try {
3388 list( $phpCallback ) = $callback;
3389 $this->clearFlag( self::DBO_TRX ); // make each query its own transaction
3390 call_user_func_array( $phpCallback, [ $trigger ] );
3391 if ( $autoTrx ) {
3392 $this->setFlag( self::DBO_TRX ); // restore automatic begin()
3393 } else {
3394 $this->clearFlag( self::DBO_TRX ); // restore auto-commit
3395 }
3396 } catch ( Exception $ex ) {
3397 call_user_func( $this->errorLogger, $ex );
3398 $e = $e ?: $ex;
3399 // Some callbacks may use startAtomic/endAtomic, so make sure
3400 // their transactions are ended so other callbacks don't fail
3401 if ( $this->trxLevel() ) {
3402 $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
3403 }
3404 }
3405 }
3406 } while ( count( $this->trxIdleCallbacks ) );
3407
3408 if ( $e instanceof Exception ) {
3409 throw $e; // re-throw any first exception
3410 }
3411 }
3412
3422 $e = null; // first exception
3423 do { // callbacks may add callbacks :)
3424 $callbacks = $this->trxPreCommitCallbacks;
3425 $this->trxPreCommitCallbacks = []; // consumed (and recursion guard)
3426 foreach ( $callbacks as $callback ) {
3427 try {
3428 list( $phpCallback ) = $callback;
3429 call_user_func( $phpCallback );
3430 } catch ( Exception $ex ) {
3431 call_user_func( $this->errorLogger, $ex );
3432 $e = $e ?: $ex;
3433 }
3434 }
3435 } while ( count( $this->trxPreCommitCallbacks ) );
3436
3437 if ( $e instanceof Exception ) {
3438 throw $e; // re-throw any first exception
3439 }
3440 }
3441
3451 public function runTransactionListenerCallbacks( $trigger ) {
3452 if ( $this->trxEndCallbacksSuppressed ) {
3453 return;
3454 }
3455
3457 $e = null; // first exception
3458
3459 foreach ( $this->trxRecurringCallbacks as $phpCallback ) {
3460 try {
3461 $phpCallback( $trigger, $this );
3462 } catch ( Exception $ex ) {
3463 call_user_func( $this->errorLogger, $ex );
3464 $e = $e ?: $ex;
3465 }
3466 }
3467
3468 if ( $e instanceof Exception ) {
3469 throw $e; // re-throw any first exception
3470 }
3471 }
3472
3483 protected function doSavepoint( $identifier, $fname ) {
3484 $this->query( 'SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
3485 }
3486
3497 protected function doReleaseSavepoint( $identifier, $fname ) {
3498 $this->query( 'RELEASE SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
3499 }
3500
3511 protected function doRollbackToSavepoint( $identifier, $fname ) {
3512 $this->query( 'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
3513 }
3514
3519 private function nextSavepointId( $fname ) {
3520 $savepointId = self::$SAVEPOINT_PREFIX . ++$this->trxAtomicCounter;
3521 if ( strlen( $savepointId ) > 30 ) {
3522 // 30 == Oracle's identifier length limit (pre 12c)
3523 // With a 22 character prefix, that puts the highest number at 99999999.
3524 throw new DBUnexpectedError(
3525 $this,
3526 'There have been an excessively large number of atomic sections in a transaction'
3527 . " started by $this->trxFname (at $fname)"
3528 );
3529 }
3530
3531 return $savepointId;
3532 }
3533
3534 final public function startAtomic(
3535 $fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE
3536 ) {
3537 $savepointId = $cancelable === self::ATOMIC_CANCELABLE ? self::$NOT_APPLICABLE : null;
3538
3539 if ( !$this->trxLevel ) {
3540 $this->begin( $fname, self::TRANSACTION_INTERNAL );
3541 // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
3542 // in all changes being in one transaction to keep requests transactional.
3543 if ( $this->getFlag( self::DBO_TRX ) ) {
3544 // Since writes could happen in between the topmost atomic sections as part
3545 // of the transaction, those sections will need savepoints.
3546 $savepointId = $this->nextSavepointId( $fname );
3547 $this->doSavepoint( $savepointId, $fname );
3548 } else {
3549 $this->trxAutomaticAtomic = true;
3550 }
3551 } elseif ( $cancelable === self::ATOMIC_CANCELABLE ) {
3552 $savepointId = $this->nextSavepointId( $fname );
3553 $this->doSavepoint( $savepointId, $fname );
3554 }
3555
3556 $sectionId = new AtomicSectionIdentifier;
3557 $this->trxAtomicLevels[] = [ $fname, $sectionId, $savepointId ];
3558
3559 return $sectionId;
3560 }
3561
3562 final public function endAtomic( $fname = __METHOD__ ) {
3563 if ( !$this->trxLevel || !$this->trxAtomicLevels ) {
3564 throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
3565 }
3566
3567 // Check if the current section matches $fname
3568 $pos = count( $this->trxAtomicLevels ) - 1;
3569 list( $savedFname, $sectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
3570
3571 if ( $savedFname !== $fname ) {
3572 throw new DBUnexpectedError(
3573 $this,
3574 "Invalid atomic section ended (got $fname but expected $savedFname)."
3575 );
3576 }
3577
3578 // Remove the last section (no need to re-index the array)
3579 array_pop( $this->trxAtomicLevels );
3580
3581 if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
3582 $this->commit( $fname, self::FLUSHING_INTERNAL );
3583 } elseif ( $savepointId !== null && $savepointId !== self::$NOT_APPLICABLE ) {
3584 $this->doReleaseSavepoint( $savepointId, $fname );
3585 }
3586
3587 // Hoist callback ownership for callbacks in the section that just ended;
3588 // all callbacks should have an owner that is present in trxAtomicLevels.
3589 $currentSectionId = $this->currentAtomicSectionId();
3590 if ( $currentSectionId ) {
3591 $this->reassignCallbacksForSection( $sectionId, $currentSectionId );
3592 }
3593 }
3594
3595 final public function cancelAtomic(
3596 $fname = __METHOD__, AtomicSectionIdentifier $sectionId = null
3597 ) {
3598 if ( !$this->trxLevel || !$this->trxAtomicLevels ) {
3599 throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
3600 }
3601
3602 if ( $sectionId !== null ) {
3603 // Find the (last) section with the given $sectionId
3604 $pos = -1;
3605 foreach ( $this->trxAtomicLevels as $i => list( $asFname, $asId, $spId ) ) {
3606 if ( $asId === $sectionId ) {
3607 $pos = $i;
3608 }
3609 }
3610 if ( $pos < 0 ) {
3611 throw new DBUnexpectedError( "Atomic section not found (for $fname)" );
3612 }
3613 // Remove all descendant sections and re-index the array
3614 $excisedIds = [];
3615 $len = count( $this->trxAtomicLevels );
3616 for ( $i = $pos + 1; $i < $len; ++$i ) {
3617 $excisedIds[] = $this->trxAtomicLevels[$i][1];
3618 }
3619 $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 );
3620 $this->modifyCallbacksForCancel( $excisedIds );
3621 }
3622
3623 // Check if the current section matches $fname
3624 $pos = count( $this->trxAtomicLevels ) - 1;
3625 list( $savedFname, $savedSectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
3626
3627 if ( $savedFname !== $fname ) {
3628 throw new DBUnexpectedError(
3629 $this,
3630 "Invalid atomic section ended (got $fname but expected $savedFname)."
3631 );
3632 }
3633
3634 // Remove the last section (no need to re-index the array)
3635 array_pop( $this->trxAtomicLevels );
3636 $this->modifyCallbacksForCancel( [ $savedSectionId ] );
3637
3638 if ( $savepointId !== null ) {
3639 // Rollback the transaction to the state just before this atomic section
3640 if ( $savepointId === self::$NOT_APPLICABLE ) {
3641 $this->rollback( $fname, self::FLUSHING_INTERNAL );
3642 } else {
3643 $this->doRollbackToSavepoint( $savepointId, $fname );
3644 $this->trxStatus = self::STATUS_TRX_OK; // no exception; recovered
3645 $this->trxStatusIgnoredCause = null;
3646 }
3647 } elseif ( $this->trxStatus > self::STATUS_TRX_ERROR ) {
3648 // Put the transaction into an error state if it's not already in one
3649 $this->trxStatus = self::STATUS_TRX_ERROR;
3650 $this->trxStatusCause = new DBUnexpectedError(
3651 $this,
3652 "Uncancelable atomic section canceled (got $fname)."
3653 );
3654 }
3655
3656 $this->affectedRowCount = 0; // for the sake of consistency
3657 }
3658
3659 final public function doAtomicSection(
3660 $fname, callable $callback, $cancelable = self::ATOMIC_NOT_CANCELABLE
3661 ) {
3662 $sectionId = $this->startAtomic( $fname, $cancelable );
3663 try {
3664 $res = call_user_func_array( $callback, [ $this, $fname ] );
3665 } catch ( Exception $e ) {
3666 $this->cancelAtomic( $fname, $sectionId );
3667
3668 throw $e;
3669 }
3670 $this->endAtomic( $fname );
3671
3672 return $res;
3673 }
3674
3675 final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
3676 static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
3677 if ( !in_array( $mode, $modes, true ) ) {
3678 throw new DBUnexpectedError( $this, "$fname: invalid mode parameter '$mode'." );
3679 }
3680
3681 // Protect against mismatched atomic section, transaction nesting, and snapshot loss
3682 if ( $this->trxLevel ) {
3683 if ( $this->trxAtomicLevels ) {
3684 $levels = $this->flatAtomicSectionList();
3685 $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
3686 throw new DBUnexpectedError( $this, $msg );
3687 } elseif ( !$this->trxAutomatic ) {
3688 $msg = "$fname: Explicit transaction already active (from {$this->trxFname}).";
3689 throw new DBUnexpectedError( $this, $msg );
3690 } else {
3691 $msg = "$fname: Implicit transaction already active (from {$this->trxFname}).";
3692 throw new DBUnexpectedError( $this, $msg );
3693 }
3694 } elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
3695 $msg = "$fname: Implicit transaction expected (DBO_TRX set).";
3696 throw new DBUnexpectedError( $this, $msg );
3697 }
3698
3699 // Avoid fatals if close() was called
3700 $this->assertOpen();
3701
3702 $this->doBegin( $fname );
3703 $this->trxStatus = self::STATUS_TRX_OK;
3704 $this->trxStatusIgnoredCause = null;
3705 $this->trxAtomicCounter = 0;
3706 $this->trxTimestamp = microtime( true );
3707 $this->trxFname = $fname;
3708 $this->trxDoneWrites = false;
3709 $this->trxAutomaticAtomic = false;
3710 $this->trxAtomicLevels = [];
3711 $this->trxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
3712 $this->trxWriteDuration = 0.0;
3713 $this->trxWriteQueryCount = 0;
3714 $this->trxWriteAffectedRows = 0;
3715 $this->trxWriteAdjDuration = 0.0;
3716 $this->trxWriteAdjQueryCount = 0;
3717 $this->trxWriteCallers = [];
3718 // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
3719 // Get an estimate of the replication lag before any such queries.
3720 $this->trxReplicaLag = null; // clear cached value first
3721 $this->trxReplicaLag = $this->getApproximateLagStatus()['lag'];
3722 // T147697: make explicitTrxActive() return true until begin() finishes. This way, no
3723 // caller will think its OK to muck around with the transaction just because startAtomic()
3724 // has not yet completed (e.g. setting trxAtomicLevels).
3725 $this->trxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
3726 }
3727
3734 protected function doBegin( $fname ) {
3735 $this->query( 'BEGIN', $fname );
3736 $this->trxLevel = 1;
3737 }
3738
3739 final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
3740 static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
3741 if ( !in_array( $flush, $modes, true ) ) {
3742 throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'." );
3743 }
3744
3745 if ( $this->trxLevel && $this->trxAtomicLevels ) {
3746 // There are still atomic sections open; this cannot be ignored
3747 $levels = $this->flatAtomicSectionList();
3748 throw new DBUnexpectedError(
3749 $this,
3750 "$fname: Got COMMIT while atomic sections $levels are still open."
3751 );
3752 }
3753
3754 if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
3755 if ( !$this->trxLevel ) {
3756 return; // nothing to do
3757 } elseif ( !$this->trxAutomatic ) {
3758 throw new DBUnexpectedError(
3759 $this,
3760 "$fname: Flushing an explicit transaction, getting out of sync."
3761 );
3762 }
3763 } else {
3764 if ( !$this->trxLevel ) {
3765 $this->queryLogger->error(
3766 "$fname: No transaction to commit, something got out of sync." );
3767 return; // nothing to do
3768 } elseif ( $this->trxAutomatic ) {
3769 throw new DBUnexpectedError(
3770 $this,
3771 "$fname: Expected mass commit of all peer transactions (DBO_TRX set)."
3772 );
3773 }
3774 }
3775
3776 // Avoid fatals if close() was called
3777 $this->assertOpen();
3778
3780 $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
3781 $this->doCommit( $fname );
3782 $this->trxStatus = self::STATUS_TRX_NONE;
3783 if ( $this->trxDoneWrites ) {
3784 $this->lastWriteTime = microtime( true );
3785 $this->trxProfiler->transactionWritingOut(
3786 $this->server,
3787 $this->dbName,
3788 $this->trxShortId,
3789 $writeTime,
3790 $this->trxWriteAffectedRows
3791 );
3792 }
3793
3794 $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
3795 $this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
3796 }
3797
3804 protected function doCommit( $fname ) {
3805 if ( $this->trxLevel ) {
3806 $this->query( 'COMMIT', $fname );
3807 $this->trxLevel = 0;
3808 }
3809 }
3810
3811 final public function rollback( $fname = __METHOD__, $flush = '' ) {
3812 $trxActive = $this->trxLevel;
3813
3814 if ( $flush !== self::FLUSHING_INTERNAL && $flush !== self::FLUSHING_ALL_PEERS ) {
3815 if ( $this->getFlag( self::DBO_TRX ) ) {
3816 throw new DBUnexpectedError(
3817 $this,
3818 "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)."
3819 );
3820 }
3821 }
3822
3823 if ( $trxActive ) {
3824 // Avoid fatals if close() was called
3825 $this->assertOpen();
3826
3827 $this->doRollback( $fname );
3828 $this->trxStatus = self::STATUS_TRX_NONE;
3829 $this->trxAtomicLevels = [];
3830 if ( $this->trxDoneWrites ) {
3831 $this->trxProfiler->transactionWritingOut(
3832 $this->server,
3833 $this->dbName,
3834 $this->trxShortId
3835 );
3836 }
3837 }
3838
3839 // Clear any commit-dependant callbacks. They might even be present
3840 // only due to transaction rounds, with no SQL transaction being active
3841 $this->trxIdleCallbacks = [];
3842 $this->trxPreCommitCallbacks = [];
3843
3844 if ( $trxActive ) {
3845 try {
3846 $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
3847 } catch ( Exception $e ) {
3848 // already logged; finish and let LoadBalancer move on during mass-rollback
3849 }
3850 try {
3851 $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
3852 } catch ( Exception $e ) {
3853 // already logged; let LoadBalancer move on during mass-rollback
3854 }
3855
3856 $this->affectedRowCount = 0; // for the sake of consistency
3857 }
3858 }
3859
3866 protected function doRollback( $fname ) {
3867 if ( $this->trxLevel ) {
3868 # Disconnects cause rollback anyway, so ignore those errors
3869 $ignoreErrors = true;
3870 $this->query( 'ROLLBACK', $fname, $ignoreErrors );
3871 $this->trxLevel = 0;
3872 }
3873 }
3874
3875 public function flushSnapshot( $fname = __METHOD__ ) {
3876 if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
3877 // This only flushes transactions to clear snapshots, not to write data
3878 $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
3879 throw new DBUnexpectedError(
3880 $this,
3881 "$fname: Cannot flush snapshot because writes are pending ($fnames)."
3882 );
3883 }
3884
3885 $this->commit( $fname, self::FLUSHING_INTERNAL );
3886 }
3887
3888 public function explicitTrxActive() {
3889 return $this->trxLevel && ( $this->trxAtomicLevels || !$this->trxAutomatic );
3890 }
3891
3893 $oldName, $newName, $temporary = false, $fname = __METHOD__
3894 ) {
3895 throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
3896 }
3897
3898 public function listTables( $prefix = null, $fname = __METHOD__ ) {
3899 throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
3900 }
3901
3902 public function listViews( $prefix = null, $fname = __METHOD__ ) {
3903 throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
3904 }
3905
3906 public function timestamp( $ts = 0 ) {
3907 $t = new ConvertibleTimestamp( $ts );
3908 // Let errors bubble up to avoid putting garbage in the DB
3909 return $t->getTimestamp( TS_MW );
3910 }
3911
3912 public function timestampOrNull( $ts = null ) {
3913 if ( is_null( $ts ) ) {
3914 return null;
3915 } else {
3916 return $this->timestamp( $ts );
3917 }
3918 }
3919
3920 public function affectedRows() {
3921 return ( $this->affectedRowCount === null )
3922 ? $this->fetchAffectedRowCount() // default to driver value
3924 }
3925
3929 abstract protected function fetchAffectedRowCount();
3930
3944 protected function resultObject( $result ) {
3945 if ( !$result ) {
3946 return false;
3947 } elseif ( $result instanceof ResultWrapper ) {
3948 return $result;
3949 } elseif ( $result === true ) {
3950 // Successful write query
3951 return $result;
3952 } else {
3953 return new ResultWrapper( $this, $result );
3954 }
3955 }
3956
3957 public function ping( &$rtt = null ) {
3958 // Avoid hitting the server if it was hit recently
3959 if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
3960 if ( !func_num_args() || $this->rttEstimate > 0 ) {
3961 $rtt = $this->rttEstimate;
3962 return true; // don't care about $rtt
3963 }
3964 }
3965
3966 // This will reconnect if possible or return false if not
3967 $this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
3968 $ok = ( $this->query( self::PING_QUERY, __METHOD__, true ) !== false );
3969 $this->restoreFlags( self::RESTORE_PRIOR );
3970
3971 if ( $ok ) {
3972 $rtt = $this->rttEstimate;
3973 }
3974
3975 return $ok;
3976 }
3977
3984 protected function replaceLostConnection( $fname ) {
3985 $this->closeConnection();
3986 $this->opened = false;
3987 $this->conn = false;
3988 try {
3989 $this->open( $this->server, $this->user, $this->password, $this->dbName );
3990 $this->lastPing = microtime( true );
3991 $ok = true;
3992
3993 $this->connLogger->warning(
3994 $fname . ': lost connection to {dbserver}; reconnected',
3995 [
3996 'dbserver' => $this->getServer(),
3997 'exception' => new RuntimeException()
3998 ]
3999 );
4000 } catch ( DBConnectionError $e ) {
4001 $ok = false;
4002
4003 $this->connLogger->error(
4004 $fname . ': lost connection to {dbserver} permanently',
4005 [ 'dbserver' => $this->getServer() ]
4006 );
4007 }
4008
4009 $this->handleSessionLoss();
4010
4011 return $ok;
4012 }
4013
4014 public function getSessionLagStatus() {
4015 return $this->getRecordedTransactionLagStatus() ?: $this->getApproximateLagStatus();
4016 }
4017
4031 final protected function getRecordedTransactionLagStatus() {
4032 return ( $this->trxLevel && $this->trxReplicaLag !== null )
4033 ? [ 'lag' => $this->trxReplicaLag, 'since' => $this->trxTimestamp() ]
4034 : null;
4035 }
4036
4043 protected function getApproximateLagStatus() {
4044 return [
4045 'lag' => $this->getLBInfo( 'replica' ) ? $this->getLag() : 0,
4046 'since' => microtime( true )
4047 ];
4048 }
4049
4069 public static function getCacheSetOptions( IDatabase $db1, IDatabase $db2 = null ) {
4070 $res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
4071 foreach ( func_get_args() as $db ) {
4073 $status = $db->getSessionLagStatus();
4074 if ( $status['lag'] === false ) {
4075 $res['lag'] = false;
4076 } elseif ( $res['lag'] !== false ) {
4077 $res['lag'] = max( $res['lag'], $status['lag'] );
4078 }
4079 $res['since'] = min( $res['since'], $status['since'] );
4080 $res['pending'] = $res['pending'] ?: $db->writesPending();
4081 }
4082
4083 return $res;
4084 }
4085
4086 public function getLag() {
4087 return 0;
4088 }
4089
4090 public function maxListLen() {
4091 return 0;
4092 }
4093
4094 public function encodeBlob( $b ) {
4095 return $b;
4096 }
4097
4098 public function decodeBlob( $b ) {
4099 if ( $b instanceof Blob ) {
4100 $b = $b->fetch();
4101 }
4102 return $b;
4103 }
4104
4105 public function setSessionOptions( array $options ) {
4106 }
4107
4108 public function sourceFile(
4109 $filename,
4110 callable $lineCallback = null,
4111 callable $resultCallback = null,
4112 $fname = false,
4113 callable $inputCallback = null
4114 ) {
4115 Wikimedia\suppressWarnings();
4116 $fp = fopen( $filename, 'r' );
4117 Wikimedia\restoreWarnings();
4118
4119 if ( false === $fp ) {
4120 throw new RuntimeException( "Could not open \"{$filename}\".\n" );
4121 }
4122
4123 if ( !$fname ) {
4124 $fname = __METHOD__ . "( $filename )";
4125 }
4126
4127 try {
4128 $error = $this->sourceStream(
4129 $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
4130 } catch ( Exception $e ) {
4131 fclose( $fp );
4132 throw $e;
4133 }
4134
4135 fclose( $fp );
4136
4137 return $error;
4138 }
4139
4140 public function setSchemaVars( $vars ) {
4141 $this->schemaVars = $vars;
4142 }
4143
4144 public function sourceStream(
4145 $fp,
4146 callable $lineCallback = null,
4147 callable $resultCallback = null,
4148 $fname = __METHOD__,
4149 callable $inputCallback = null
4150 ) {
4151 $delimiterReset = new ScopedCallback(
4152 function ( $delimiter ) {
4153 $this->delimiter = $delimiter;
4154 },
4156 );
4157 $cmd = '';
4158
4159 while ( !feof( $fp ) ) {
4160 if ( $lineCallback ) {
4161 call_user_func( $lineCallback );
4162 }
4163
4164 $line = trim( fgets( $fp ) );
4165
4166 if ( $line == '' ) {
4167 continue;
4168 }
4169
4170 if ( '-' == $line[0] && '-' == $line[1] ) {
4171 continue;
4172 }
4173
4174 if ( $cmd != '' ) {
4175 $cmd .= ' ';
4176 }
4177
4178 $done = $this->streamStatementEnd( $cmd, $line );
4179
4180 $cmd .= "$line\n";
4181
4182 if ( $done || feof( $fp ) ) {
4183 $cmd = $this->replaceVars( $cmd );
4184
4185 if ( $inputCallback ) {
4186 $callbackResult = call_user_func( $inputCallback, $cmd );
4187
4188 if ( is_string( $callbackResult ) || !$callbackResult ) {
4189 $cmd = $callbackResult;
4190 }
4191 }
4192
4193 if ( $cmd ) {
4194 $res = $this->query( $cmd, $fname );
4195
4196 if ( $resultCallback ) {
4197 call_user_func( $resultCallback, $res, $this );
4198 }
4199
4200 if ( false === $res ) {
4201 $err = $this->lastError();
4202
4203 return "Query \"{$cmd}\" failed with error code \"$err\".\n";
4204 }
4205 }
4206 $cmd = '';
4207 }
4208 }
4209
4210 ScopedCallback::consume( $delimiterReset );
4211 return true;
4212 }
4213
4221 public function streamStatementEnd( &$sql, &$newLine ) {
4222 if ( $this->delimiter ) {
4223 $prev = $newLine;
4224 $newLine = preg_replace(
4225 '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
4226 if ( $newLine != $prev ) {
4227 return true;
4228 }
4229 }
4230
4231 return false;
4232 }
4233
4254 protected function replaceVars( $ins ) {
4255 $vars = $this->getSchemaVars();
4256 return preg_replace_callback(
4257 '!
4258 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
4259 \'\{\$ (\w+) }\' | # 3. addQuotes
4260 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
4261 /\*\$ (\w+) \*/ # 5. leave unencoded
4262 !x',
4263 function ( $m ) use ( $vars ) {
4264 // Note: Because of <https://bugs.php.net/bug.php?id=51881>,
4265 // check for both nonexistent keys *and* the empty string.
4266 if ( isset( $m[1] ) && $m[1] !== '' ) {
4267 if ( $m[1] === 'i' ) {
4268 return $this->indexName( $m[2] );
4269 } else {
4270 return $this->tableName( $m[2] );
4271 }
4272 } elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
4273 return $this->addQuotes( $vars[$m[3]] );
4274 } elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
4275 return $this->addIdentifierQuotes( $vars[$m[4]] );
4276 } elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
4277 return $vars[$m[5]];
4278 } else {
4279 return $m[0];
4280 }
4281 },
4282 $ins
4283 );
4284 }
4285
4292 protected function getSchemaVars() {
4293 if ( $this->schemaVars ) {
4294 return $this->schemaVars;
4295 } else {
4296 return $this->getDefaultSchemaVars();
4297 }
4298 }
4299
4308 protected function getDefaultSchemaVars() {
4309 return [];
4310 }
4311
4312 public function lockIsFree( $lockName, $method ) {
4313 // RDBMs methods for checking named locks may or may not count this thread itself.
4314 // In MySQL, IS_FREE_LOCK() returns 0 if the thread already has the lock. This is
4315 // the behavior choosen by the interface for this method.
4316 return !isset( $this->namedLocksHeld[$lockName] );
4317 }
4318
4319 public function lock( $lockName, $method, $timeout = 5 ) {
4320 $this->namedLocksHeld[$lockName] = 1;
4321
4322 return true;
4323 }
4324
4325 public function unlock( $lockName, $method ) {
4326 unset( $this->namedLocksHeld[$lockName] );
4327
4328 return true;
4329 }
4330
4331 public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
4332 if ( $this->writesOrCallbacksPending() ) {
4333 // This only flushes transactions to clear snapshots, not to write data
4334 $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
4335 throw new DBUnexpectedError(
4336 $this,
4337 "$fname: Cannot flush pre-lock snapshot because writes are pending ($fnames)."
4338 );
4339 }
4340
4341 if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
4342 return null;
4343 }
4344
4345 $unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
4346 if ( $this->trxLevel() ) {
4347 // There is a good chance an exception was thrown, causing any early return
4348 // from the caller. Let any error handler get a chance to issue rollback().
4349 // If there isn't one, let the error bubble up and trigger server-side rollback.
4351 function () use ( $lockKey, $fname ) {
4352 $this->unlock( $lockKey, $fname );
4353 },
4354 $fname
4355 );
4356 } else {
4357 $this->unlock( $lockKey, $fname );
4358 }
4359 } );
4360
4361 $this->commit( $fname, self::FLUSHING_INTERNAL );
4362
4363 return $unlocker;
4364 }
4365
4366 public function namedLocksEnqueue() {
4367 return false;
4368 }
4369
4371 return true;
4372 }
4373
4374 final public function lockTables( array $read, array $write, $method ) {
4375 if ( $this->writesOrCallbacksPending() ) {
4376 throw new DBUnexpectedError( $this, "Transaction writes or callbacks still pending." );
4377 }
4378
4379 if ( $this->tableLocksHaveTransactionScope() ) {
4380 $this->startAtomic( $method );
4381 }
4382
4383 return $this->doLockTables( $read, $write, $method );
4384 }
4385
4394 protected function doLockTables( array $read, array $write, $method ) {
4395 return true;
4396 }
4397
4398 final public function unlockTables( $method ) {
4399 if ( $this->tableLocksHaveTransactionScope() ) {
4400 $this->endAtomic( $method );
4401
4402 return true; // locks released on COMMIT/ROLLBACK
4403 }
4404
4405 return $this->doUnlockTables( $method );
4406 }
4407
4414 protected function doUnlockTables( $method ) {
4415 return true;
4416 }
4417
4425 public function dropTable( $tableName, $fName = __METHOD__ ) {
4426 if ( !$this->tableExists( $tableName, $fName ) ) {
4427 return false;
4428 }
4429 $sql = "DROP TABLE " . $this->tableName( $tableName ) . " CASCADE";
4430
4431 return $this->query( $sql, $fName );
4432 }
4433
4434 public function getInfinity() {
4435 return 'infinity';
4436 }
4437
4438 public function encodeExpiry( $expiry ) {
4439 return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
4440 ? $this->getInfinity()
4441 : $this->timestamp( $expiry );
4442 }
4443
4444 public function decodeExpiry( $expiry, $format = TS_MW ) {
4445 if ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) {
4446 return 'infinity';
4447 }
4448
4449 return ConvertibleTimestamp::convert( $format, $expiry );
4450 }
4451
4452 public function setBigSelects( $value = true ) {
4453 // no-op
4454 }
4455
4456 public function isReadOnly() {
4457 return ( $this->getReadOnlyReason() !== false );
4458 }
4459
4463 protected function getReadOnlyReason() {
4464 $reason = $this->getLBInfo( 'readOnlyReason' );
4465
4466 return is_string( $reason ) ? $reason : false;
4467 }
4468
4469 public function setTableAliases( array $aliases ) {
4470 $this->tableAliases = $aliases;
4471 }
4472
4473 public function setIndexAliases( array $aliases ) {
4474 $this->indexAliases = $aliases;
4475 }
4476
4488 protected function getBindingHandle() {
4489 if ( !$this->conn ) {
4490 throw new DBUnexpectedError(
4491 $this,
4492 'DB connection was already closed or the connection dropped.'
4493 );
4494 }
4495
4496 return $this->conn;
4497 }
4498
4503 public function __toString() {
4504 return (string)$this->conn;
4505 }
4506
4511 public function __clone() {
4512 $this->connLogger->warning(
4513 "Cloning " . static::class . " is not recommended; forking connection",
4514 [ 'exception' => new RuntimeException() ]
4515 );
4516
4517 if ( $this->isOpen() ) {
4518 // Open a new connection resource without messing with the old one
4519 $this->opened = false;
4520 $this->conn = false;
4521 $this->trxEndCallbacks = []; // don't copy
4522 $this->handleSessionLoss(); // no trx or locks anymore
4523 $this->open( $this->server, $this->user, $this->password, $this->dbName );
4524 $this->lastPing = microtime( true );
4525 }
4526 }
4527
4533 public function __sleep() {
4534 throw new RuntimeException( 'Database serialization may cause problems, since ' .
4535 'the connection is not restored on wakeup.' );
4536 }
4537
4541 public function __destruct() {
4542 if ( $this->trxLevel && $this->trxDoneWrites ) {
4543 trigger_error( "Uncommitted DB writes (transaction from {$this->trxFname})." );
4544 }
4545
4546 $danglingWriters = $this->pendingWriteAndCallbackCallers();
4547 if ( $danglingWriters ) {
4548 $fnames = implode( ', ', $danglingWriters );
4549 trigger_error( "DB transaction writes or callbacks still pending ($fnames)." );
4550 }
4551
4552 if ( $this->conn ) {
4553 // Avoid connection leaks for sanity. Normally, resources close at script completion.
4554 // The connection might already be closed in zend/hhvm by now, so suppress warnings.
4555 Wikimedia\suppressWarnings();
4556 $this->closeConnection();
4557 Wikimedia\restoreWarnings();
4558 $this->conn = false;
4559 $this->opened = false;
4560 }
4561 }
4562}
4563
4564class_alias( Database::class, 'DatabaseBase' ); // b/c for old name
4565class_alias( Database::class, 'Database' ); // b/c global alias
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings,...
Definition Setup.php:112
$line
Definition cdb.php:59
if( $line===false) $args
Definition cdb.php:64
interface is intended to be more or less compatible with the PHP memcached client.
Definition BagOStuff.php:47
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.
Definition DBError.php:30
Class to handle database/prefix specification for IDatabase domains.
Relational database abstraction object.
Definition Database.php:48
bool $cliMode
Whether this PHP instance is for a CLI script.
Definition Database.php:91
flushSnapshot( $fname=__METHOD__)
Commit any transaction but error out if writes or callbacks are pending.
getServerInfo()
A string describing the current software version, and possibly other details in a user-friendly way.
Definition Database.php:562
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...
object string $profiler
Class name or object With profileIn/profileOut methods.
Definition Database.php:270
runOnTransactionPreCommitCallbacks()
Actually run and consume any "on transaction pre-commit" callbacks.
pendingWriteRowsAffected()
Get the number of affected rows from pending write queries.
Definition Database.php:720
integer null $affectedRowCount
Rows affected by the last query to query() or its CRUD wrappers.
Definition Database.php:144
begin( $fname=__METHOD__, $mode=self::TRANSACTION_EXPLICIT)
Begin a transaction.
makeUpdateOptions( $options)
Make UPDATE options for the Database::update function.
deadlockLoop()
Perform a deadlock-prone transaction.
makeInsertOptions( $options)
Helper for Database::insert().
array[] $trxIdleCallbacks
List of (callable, method name, atomic section id)
Definition Database.php:113
static string $SAVEPOINT_PREFIX
Prefix to the atomic section counter used to make savepoint IDs.
Definition Database.php:280
restoreErrorHandler()
Restore the previous error handler and return the last PHP error for this DB.
Definition Database.php:849
getApproximateLagStatus()
Get a replica DB lag estimate for this server.
bool $trxDoneWrites
Record if possible write queries were done in the last transaction started.
Definition Database.php:198
insert( $table, $a, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
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...
const DEADLOCK_DELAY_MIN
Minimum time to wait before retry, in microseconds.
Definition Database.php:52
int[] $priorFlags
Prior flags member variable values.
Definition Database.php:267
doInitConnection()
Actually connect to the database over the wire (or to local files)
Definition Database.php:365
conditional( $cond, $trueVal, $falseVal)
Returns an SQL expression for a simple conditional.
selectDB( $db)
Change the current database.
trxTimestamp()
Get the UNIX timestamp of the time that the transaction was established.
Definition Database.php:581
reassignCallbacksForSection(AtomicSectionIdentifier $old, AtomicSectionIdentifier $new)
addQuotes( $s)
Adds quotes and backslashes.
fieldNameWithAlias( $name, $alias=false)
Get an aliased field name e.g.
tablePrefix( $prefix=null)
Get/set the table prefix.
Definition Database.php:593
setLogger(LoggerInterface $logger)
Set the PSR-3 logger interface to use for query logging.
Definition Database.php:558
int $trxWriteQueryCount
Number of write queries for the current transaction.
Definition Database.php:237
isInsertSelectSafe(array $insertOptions, array $selectOptions)
indexUnique( $table, $index)
Determines if a given index is unique.
fieldNamesWithAlias( $fields)
Gets an array of aliased field names.
getSessionLagStatus()
Get the replica DB lag when the current transaction started or a general lag estimate if not transact...
listTables( $prefix=null, $fname=__METHOD__)
List all tables on the database.
fieldExists( $table, $field, $fname=__METHOD__)
Determines whether a field exists in a table.
nextSequenceValue( $seqName)
Deprecated method, calls should be removed.
nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname=__METHOD__, $insertOptions=[], $selectOptions=[], $selectJoinConds=[])
Native server-side implementation of insertSelect() for situations where we don't want to select ever...
__destruct()
Run a few simple sanity checks and close dangling connections.
dbSchema( $schema=null)
Get/set the db schema.
Definition Database.php:605
wasLockTimeout()
Determines if the last failure was due to a lock 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.
string $agent
Agent name for query profiling.
Definition Database.php:93
indexName( $index)
Allows for index remapping in queries where this is not consistent across DBMS.
callable[] $trxRecurringCallbacks
Map of (name => callable)
Definition Database.php:119
makeUpdateOptionsArray( $options)
Make UPDATE options array for Database::makeUpdateOptions.
listViews( $prefix=null, $fname=__METHOD__)
Lists all the VIEWs in the database.
assertOpen()
Make sure isOpen() returns true as a sanity check.
Definition Database.php:980
array[] $trxPreCommitCallbacks
List of (callable, method name, atomic section id)
Definition Database.php:115
doCommit( $fname)
Issues the COMMIT command to the database server.
closeConnection()
Closes underlying database connection.
getSchemaVars()
Get schema variables.
runTransactionListenerCallbacks( $trigger)
Actually run any "transaction listener" callbacks.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by wfTimestamp() to the format used for inserting ...
string $user
User that this instance is currently connected under the name of.
Definition Database.php:81
static getCacheSetOptions(IDatabase $db1, IDatabase $db2=null)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
getRecordedTransactionLagStatus()
Get the replica DB lag when the current transaction started.
array $sessionTempTables
Map of (table name => 1) for TEMPORARY tables.
Definition Database.php:258
lastDoneWrites()
Returns the last time the connection may have been used for write queries.
Definition Database.php:663
isTransactableQuery( $sql)
Determine whether a SQL statement is sensitive to isolation level.
int $trxStatus
Transaction status.
Definition Database.php:149
replaceLostConnection( $fname)
Close any existing (dead) database connection and open a new connection.
getServerUptime()
Determines how long the server has been up.
bitAnd( $fieldLeft, $fieldRight)
setIndexAliases(array $aliases)
Convert certain index names to alternative names before querying the DB.
getDefaultSchemaVars()
Get schema variables to use if none have been set via setSchemaVars().
doBegin( $fname)
Issues the BEGIN command to the database server.
writesOrCallbacksPending()
Returns true if there is a transaction/round open with possible write queries or transaction pre-comm...
Definition Database.php:671
buildGroupConcatField( $delim, $table, $field, $conds='', $join_conds=[])
Build a GROUP_CONCAT or equivalent statement for a query.
initConnection()
Initialize the connection to the database over the wire (or to local files)
Definition Database.php:345
onTransactionResolution(callable $callback, $fname=__METHOD__)
Run a callback as soon as the current transaction commits or rolls back.
unionConditionPermutations( $table, $vars, array $permute_conds, $extra_conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Construct a UNION query for permutations of conditions.
string[] $trxWriteCallers
Track the write query callers of the current transaction.
Definition Database.php:229
tableNames()
Fetch a number of table names into an array This is handy when you need to construct SQL for joins.
onTransactionIdle(callable $callback, $fname=__METHOD__)
Run a callback as soon as there is no transaction pending.
setBigSelects( $value=true)
Allow or deny "big selects" for this session only.
string[] $indexAliases
Map of (index alias => index)
Definition Database.php:89
array null $trxStatusIgnoredCause
If wasKnownStatementRollbackError() prevented trxStatus from being set, the relevant details are stor...
Definition Database.php:158
wasConnectionLoss()
Determines if the last query error was due to a dropped connection.
databasesAreIndependent()
Returns true if DBs are assumed to be on potentially different servers.
lock( $lockName, $method, $timeout=5)
Acquire a named lock.
wasConnectionError( $errno)
Do not use this method outside of Database/DBError classes.
int $trxWriteAffectedRows
Number of rows affected by write queries for the current transaction.
Definition Database.php:241
deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname=__METHOD__)
DELETE where the condition is a join.
unionSupportsOrderAndLimit()
Returns true if current database backend supports ORDER BY or LIMIT for separate subqueries within th...
duplicateTableStructure( $oldName, $newName, $temporary=false, $fname=__METHOD__)
Creates a new table with structure copied from existing table.
doRollbackToSavepoint( $identifier, $fname)
Rollback to a savepoint.
doneWrites()
Returns true if the connection may have been used for write queries.
Definition Database.php:659
anyChar()
Returns a token for buildLike() that denotes a '_' to be used in a LIKE query.
restoreFlags( $state=self::RESTORE_PRIOR)
Restore the flags to their prior state before the last setFlag/clearFlag call.
Definition Database.php:784
doAtomicSection( $fname, callable $callback, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Perform an atomic section of reversable SQL statements from a callback.
modifyCallbacksForCancel(array $sectionIds)
setTrxEndCallbackSuppression( $suppress)
Whether to disable running of post-COMMIT/ROLLBACK callbacks.
float $rttEstimate
RTT time estimate.
Definition Database.php:253
installErrorHandler()
Set a custom error handler for logging errors during database connection.
Definition Database.php:838
affectedRows()
Get the number of rows affected by the last write query.
buildSubstring( $input, $startPosition, $length=null)
getReplicaPos()
Get the replication position of this replica DB.
wasReadOnlyError()
Determines if the last failure was due to the database being read-only.
tableName( $name, $format='quoted')
Format a table name ready for use in constructing an SQL query.
replaceVars( $ins)
Database independent variable replacement.
tableNameWithAlias( $table, $alias=false)
Get an aliased table name.
streamStatementEnd(&$sql, &$newLine)
Called by sourceStream() to check if we've reached a statement end.
float null $trxTimestamp
The UNIX time that the transaction started.
Definition Database.php:181
makeList( $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.
wasDeadlock()
Determines if the last failure was due to a deadlock.
static factory( $dbType, $p=[], $connect=self::NEW_CONNECTED)
Construct a Database subclass instance given a database type and parameters.
Definition Database.php:422
buildConcat( $stringList)
Build a concatenation list to feed into a SQL query.
ignoreIndexClause( $index)
IGNORE INDEX clause.
string $trxFname
Remembers the function name given for starting the most recent transaction via begin().
Definition Database.php:191
tableNamesWithAlias( $tables)
Gets an array of aliased table names.
bitOr( $fieldLeft, $fieldRight)
clearFlag( $flag, $remember=self::REMEMBER_NOTHING)
Clear a flag for this connection.
Definition Database.php:773
useIndexClause( $index)
USE INDEX clause.
DatabaseDomain $currentDomain
Definition Database.php:142
namedLocksEnqueue()
Check to see if a named lock used by lock() use blocking queues.
connectionErrorLogger( $errno, $errstr)
Error handler for logging errors during database connection This method should not be used outside of...
Definition Database.php:879
maxListLen()
Return the maximum number of items allowed in a list, or 0 for unlimited.
array $connectionParams
Parameters used by initConnection() to establish a connection.
Definition Database.php:95
LoggerInterface $queryLogger
Definition Database.php:101
int $trxAtomicCounter
Counter for atomic savepoint identifiers.
Definition Database.php:211
pendingWriteAndCallbackCallers()
Get the list of method names that have pending write queries or callbacks for this transaction.
Definition Database.php:730
bool $trxEndCallbacksSuppressed
Whether to suppress triggering of transaction end callbacks.
Definition Database.php:121
Exception null $trxStatusCause
The last error that caused the status to become STATUS_TRX_ERROR.
Definition Database.php:153
sourceStream( $fp, callable $lineCallback=null, callable $resultCallback=null, $fname=__METHOD__, callable $inputCallback=null)
Read and execute commands from an open file handle.
qualifiedTableComponents( $name)
Get the table components needed for a query given the currently selected database.
tableNamesN()
Fetch a number of table names into an zero-indexed numerical array This is handy when you need to con...
callback $errorLogger
Error logging callback.
Definition Database.php:103
indexExists( $table, $index, $fname=__METHOD__)
Determines whether an index exists Usually throws a DBQueryError on failure If errors are explicitly ...
limitResult( $sql, $limit, $offset=false)
Construct a LIMIT query with optional offset.
pendingWriteCallers()
Get the list of method names that did write queries for this transaction.
Definition Database.php:716
pendingWriteQueryDuration( $type=self::ESTIMATE_TOTAL)
Get the time spend running write queries for this transaction.
Definition Database.php:694
startAtomic( $fname=__METHOD__, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Begin an atomic section of SQL statements.
getWikiID()
Alias for getDomainID()
Definition Database.php:814
nonNativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname=__METHOD__, $insertOptions=[], $selectOptions=[], $selectJoinConds=[])
Implementation of insertSelect() based on select() and insert()
commit( $fname=__METHOD__, $flush=self::FLUSHING_ONE)
Commits a transaction previously started using begin().
getLBInfo( $name=null)
Get properties passed down from the server info array of the load balancer.
Definition Database.php:614
aggregateValue( $valuedata, $valuename='value')
Return aggregated value alias.
array[] $tableAliases
Map of (table => (dbname, schema, prefix) map)
Definition Database.php:87
getMasterPos()
Get the position of this master.
buildSelectSubquery( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Equivalent to IDatabase::selectSQLText() except wraps the result in Subqyery.
__sleep()
Called by serialize.
addIdentifierQuotes( $s)
Quotes an identifier using backticks or "double quotes" depending on the database type.
doSavepoint( $identifier, $fname)
Create a savepoint.
dropTable( $tableName, $fName=__METHOD__)
Delete a table.
isWriteQuery( $sql)
Determine whether a query writes to the DB.
indexInfo( $table, $index, $fname=__METHOD__)
Get information about an index into an object.
getLogContext(array $extras=[])
Create a log context to pass to PSR-3 logger functions.
Definition Database.php:889
resource null $conn
Database connection.
Definition Database.php:108
trxLevel()
Gets the current transaction level.
Definition Database.php:577
prependDatabaseOrSchema( $namespace, $relation, $format)
const DEADLOCK_DELAY_MAX
Maximum time to wait before retry.
Definition Database.php:54
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
runOnTransactionIdleCallbacks( $trigger)
Actually run and consume any "on transaction idle/resolution" callbacks.
const DEADLOCK_TRIES
Number of times to re-try an operation in case of deadlock.
Definition Database.php:50
unlockTables( $method)
Unlock all tables locked via lockTables()
setSchemaVars( $vars)
Set variables to be used in sourceFile/sourceStream, in preference to the ones in $GLOBALS.
reportConnectionError( $error='Unknown error')
Definition Database.php:997
nativeReplace( $table, $rows, $fname)
REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE statement.
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:762
setTableAliases(array $aliases)
Make certain table names use their own database, schema, and table prefix when passed into SQL querie...
lastQuery()
Return the last query that went through IDatabase::query()
Definition Database.php:655
static getClass( $dbType, $driver=null)
Definition Database.php:493
setLazyMasterHandle(IDatabase $conn)
Set a lazy-connecting DB handle to the master DB (for replication status purposes)
Definition Database.php:634
rollback( $fname=__METHOD__, $flush='')
Rollback a transaction previously started using begin().
sourceFile( $filename, callable $lineCallback=null, callable $resultCallback=null, $fname=false, callable $inputCallback=null)
Read and execute SQL commands from a file.
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.
static attributesFromType( $dbType, $driver=null)
Definition Database.php:479
IDatabase null $lazyMasterHandle
Lazy handle to the master DB this server replicates from.
Definition Database.php:261
float bool $lastWriteTime
UNIX timestamp of last write query.
Definition Database.php:75
getServer()
Get the server hostname or IP address.
getLag()
Get the amount of replication lag for this database server.
selectSQLText( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
The equivalent of IDatabase::select() except that the constructed SQL is returned,...
string $lastQuery
SQL query.
Definition Database.php:73
onTransactionPreCommitOrIdle(callable $callback, $fname=__METHOD__)
Run a callback before the current transaction commits or now if there is none.
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.
string $password
Password used to establish the current connection.
Definition Database.php:83
tableLocksHaveTransactionScope()
Checks if table locks acquired by lockTables() are transaction-bound in their scope.
setLBInfo( $name, $value=null)
Set the LB info array, or a member of it.
Definition Database.php:626
escapeLikeInternal( $s, $escapeChar='`')
bool $trxAutomatic
Record if the current transaction was started implicitly due to DBO_TRX being set.
Definition Database.php:205
upsert( $table, array $rows, array $uniqueIndexes, array $set, $fname=__METHOD__)
INSERT ON DUPLICATE KEY UPDATE wrapper, upserts an array into a table.
ping(&$rtt=null)
Ping the server and try to reconnect if it there is no connection.
int $trxLevel
Either 1 if a transaction is active or 0 otherwise.
Definition Database.php:165
__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:138
implicitGroupby()
Returns true if this database does an implicit sort when doing GROUP BY.
Definition Database.php:647
anyString()
Returns a token for buildLike() that denotes a '' to be used in a LIKE query.
LoggerInterface $connLogger
Definition Database.php:99
string $trxShortId
Either a short hexidecimal string if a transaction is active or "".
Definition Database.php:172
bool $trxAutomaticAtomic
Record if the current transaction was started implicitly by Database::startAtomic.
Definition Database.php:223
bufferResults( $buffer=null)
Turns buffering of SQL result sets on (true) or off (false).
Definition Database.php:566
getInfinity()
Find out when 'infinity' is.
assertTransactionStatus( $sql, $fname)
string $server
Server that this instance is currently connected to.
Definition Database.php:79
resultObject( $result)
Take the result from a query, and wrap it in a ResultWrapper if necessary.
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.
setTransactionListener( $name, callable $callback=null)
Run a callback each time any transaction commits or rolls back.
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.
lockIsFree( $lockName, $method)
Check to see if a named lock is not locked by any thread (non-blocking)
isOpen()
Is a connection to the database open?
Definition Database.php:758
getFlag( $flag)
Returns a boolean whether the flag $flag is set for this connection.
Definition Database.php:797
query( $sql, $fname=__METHOD__, $tempIgnore=false)
Run an SQL query and return the result.
masterPosWait(DBMasterPos $pos, $timeout)
Wait for the replica DB to catch up to a given master position.
array $trxAtomicLevels
Array of levels of atomicity within transactions.
Definition Database.php:217
float $trxWriteAdjDuration
Like trxWriteQueryCount but excludes lock-bound, easy to replicate, queries.
Definition Database.php:245
float $trxReplicaLag
Lag estimate at the time of BEGIN.
Definition Database.php:183
const PING_TTL
How long before it is worth doing a dummy query to test the connection.
Definition Database.php:57
timestampOrNull( $ts=null)
Convert a timestamp in one of the formats accepted by wfTimestamp() to the format used for inserting ...
array $namedLocksHeld
Map of (name => 1) for locks obtained via lock()
Definition Database.php:256
float $lastPing
UNIX timestamp.
Definition Database.php:264
array[] $trxEndCallbacks
List of (callable, method name, atomic section id)
Definition Database.php:117
int $trxWriteAdjQueryCount
Number of write queries counted in trxWriteAdjDuration.
Definition Database.php:249
TransactionProfiler $trxProfiler
Definition Database.php:272
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.
handleSessionLoss()
Clean things up after session (and thus transaction) loss.
callback $deprecationLogger
Deprecation logging callback.
Definition Database.php:105
BagOStuff $srvCache
APC cache.
Definition Database.php:97
float $trxWriteDuration
Seconds spent in write queries for the current transaction.
Definition Database.php:233
getBindingHandle()
Get the underlying binding connection handle.
static string $NOT_APPLICABLE
Idiom used when a cancelable atomic section started the transaction.
Definition Database.php:278
replace( $table, $uniqueIndexes, $rows, $fname=__METHOD__)
REPLACE query wrapper.
buildLike()
LIKE statement wrapper, receives a variable-length argument list with parts of pattern to match conta...
doQuery( $sql)
Run a query and return a DBMS-dependent wrapper (that has all IResultWrapper methods)
implicitOrderby()
Returns true if this database does an implicit order by when the column has an index For example: SEL...
Definition Database.php:651
makeOrderBy( $options)
Returns an optional ORDER BY.
strreplace( $orig, $old, $new)
Returns a command for str_replace function in SQL query.
__construct(array $params)
Definition Database.php:293
string $dbName
Database that this instance is currently connected to.
Definition Database.php:85
textFieldSize( $table, $field)
Returns the size of a text field, or -1 for "unlimited".
getDBname()
Get the current DB name.
close()
Close the database connection.
Definition Database.php:900
Used by Database::buildLike() to represent characters that have special meaning in SQL LIKE clauses a...
Definition LikeMatch.php:10
Result wrapper for grabbing data queried from an IDatabase object.
Helper class that detects high-contention DB queries via profiling calls.
We use the convention $dbr for read and $dbw for write to help you keep track of whether the database object is a the world will explode Or to be a subsequent write query which succeeded on the master may fail when replicated to the slave due to a unique key collision Replication on the slave will stop and it may take hours to repair the database and get it back online Setting read_only in my cnf on the slave will avoid this but given the dire we prefer to have as many checks as possible We provide a but the wrapper functions like select() and insert() are usually more convenient. They take care of things like table prefixes and escaping for you. If you really need to make your own SQL
$res
Definition database.txt:21
For a write query
Definition database.txt:26
We use the convention $dbr for read and $dbw for write to help you keep track of whether the database object is a the world will explode Or to be a subsequent write query which succeeded on the master may fail when replicated to the slave due to a unique key collision Replication on the slave will stop and it may take hours to repair the database and get it back online Setting read_only in my cnf on the slave will avoid this but given the dire we prefer to have as many checks as possible We provide a but the wrapper functions like please read the documentation for tableName() and addQuotes(). You will need both of them. ------------------------------------------------------------------------ Basic query optimisation ------------------------------------------------------------------------ MediaWiki developers who need to write DB queries should have some understanding of databases and the performance issues associated with them. Patches containing unacceptably slow features will not be accepted. Unindexed queries are generally not welcome in MediaWiki
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add in any and then calling but I prefer the flexibility This should also do the output encoding The system allocates a global one in $wgOut Title Represents the title of an and does all the work of translating among various forms such as plain database etc For and for historical it also represents a few features of articles that don t involve their such as access rights See also title txt Article Encapsulates access to the page table of the database The object represents a an and maintains state such as flags
Definition design.txt:34
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add in any and then calling but I prefer the flexibility This should also do the output encoding The system allocates a global one in $wgOut Title Represents the title of an and does all the work of translating among various forms such as plain database key
Definition design.txt:26
the array() calling protocol came about after MediaWiki 1.4rc1.
static configuration should be added through ResourceLoaderGetConfigVars instead & $vars
Definition hooks.txt:2228
namespace being checked & $result
Definition hooks.txt:2323
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction $rows
Definition hooks.txt:2783
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account incomplete not yet checked for validity & $retval
Definition hooks.txt:266
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action or null $user:User who performed the tagging when the tagging is subsequent to the action or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy: boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1051
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist & $tables
Definition hooks.txt:1015
This code would result in ircNotify being run twice when an article is and once for brion Hooks can return three possible true was required This is the default since MediaWiki *some string
Definition hooks.txt:181
presenting them properly to the user as errors is done by the caller return true use this to change the list i e rollback
Definition hooks.txt:1774
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:2001
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition hooks.txt:2005
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and insert
Definition hooks.txt:2091
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
returning false will NOT prevent logging $e
Definition hooks.txt:2176
const LIST_OR
Definition Defines.php:56
const LIST_AND
Definition Defines.php:53
An object representing a master or replica DB position in a replicated setup.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
lastErrno()
Get the last error number.
lastError()
Get a description of the last error.
fetchObject( $res)
Fetch the next row from the given result object, in object form.
getServerVersion()
A string describing the current software version, like from mysql_get_server_info().
open( $server, $user, $password, $dbName)
Open a new connection to the database (closing any existing one)
Advanced database interface for IDatabase handles that include maintenance methods.
fieldInfo( $table, $field)
mysql_fetch_field() wrapper Returns false if the field doesn't exist
$buffer
if(is_array($mode)) switch( $mode) $input
const DBO_IGNORE
Definition defines.php:11
const DBO_TRX
Definition defines.php:12
$params