Merge "resourceloader: Add test coverage for DerivativeResourceLoaderContext"
[lhc/web/wiklou.git] / includes / db / Database.php
1 <?php
2
3 /**
4 * @defgroup Database Database
5 *
6 * This file deals with database interface functions
7 * and query specifics/optimisations.
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License along
20 * with this program; if not, write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22 * http://www.gnu.org/copyleft/gpl.html
23 *
24 * @file
25 * @ingroup Database
26 */
27
28 /**
29 * Database abstraction object
30 * @ingroup Database
31 */
32 abstract class DatabaseBase implements IDatabase {
33 /** Number of times to re-try an operation in case of deadlock */
34 const DEADLOCK_TRIES = 4;
35 /** Minimum time to wait before retry, in microseconds */
36 const DEADLOCK_DELAY_MIN = 500000;
37 /** Maximum time to wait before retry */
38 const DEADLOCK_DELAY_MAX = 1500000;
39
40 /** How long before it is worth doing a dummy query to test the connection */
41 const PING_TTL = 1.0;
42
43 /** @var string SQL query */
44 protected $mLastQuery = '';
45 /** @var bool */
46 protected $mDoneWrites = false;
47 /** @var string|bool */
48 protected $mPHPError = false;
49 /** @var string */
50 protected $mServer;
51 /** @var string */
52 protected $mUser;
53 /** @var string */
54 protected $mPassword;
55 /** @var string */
56 protected $mDBname;
57
58 /** @var BagOStuff APC cache */
59 protected $srvCache;
60
61 /** @var resource Database connection */
62 protected $mConn = null;
63 /** @var bool */
64 protected $mOpened = false;
65
66 /** @var array[] List of (callable, method name) */
67 protected $mTrxIdleCallbacks = [];
68 /** @var array[] List of (callable, method name) */
69 protected $mTrxPreCommitCallbacks = [];
70 /** @var array[] List of (callable, method name) */
71 protected $mTrxEndCallbacks = [];
72 /** @var bool Whether to suppress triggering of post-commit callbacks */
73 protected $suppressPostCommitCallbacks = false;
74
75 /** @var string */
76 protected $mTablePrefix;
77 /** @var string */
78 protected $mSchema;
79 /** @var integer */
80 protected $mFlags;
81 /** @var bool */
82 protected $mForeign;
83 /** @var array */
84 protected $mLBInfo = [];
85 /** @var bool|null */
86 protected $mDefaultBigSelects = null;
87 /** @var array|bool */
88 protected $mSchemaVars = false;
89 /** @var array */
90 protected $mSessionVars = [];
91 /** @var array|null */
92 protected $preparedArgs;
93 /** @var string|bool|null Stashed value of html_errors INI setting */
94 protected $htmlErrors;
95 /** @var string */
96 protected $delimiter = ';';
97
98 /**
99 * Either 1 if a transaction is active or 0 otherwise.
100 * The other Trx fields may not be meaningfull if this is 0.
101 *
102 * @var int
103 */
104 protected $mTrxLevel = 0;
105
106 /**
107 * Either a short hexidecimal string if a transaction is active or ""
108 *
109 * @var string
110 * @see DatabaseBase::mTrxLevel
111 */
112 protected $mTrxShortId = '';
113
114 /**
115 * The UNIX time that the transaction started. Callers can assume that if
116 * snapshot isolation is used, then the data is *at least* up to date to that
117 * point (possibly more up-to-date since the first SELECT defines the snapshot).
118 *
119 * @var float|null
120 * @see DatabaseBase::mTrxLevel
121 */
122 private $mTrxTimestamp = null;
123
124 /** @var float Lag estimate at the time of BEGIN */
125 private $mTrxSlaveLag = null;
126
127 /**
128 * Remembers the function name given for starting the most recent transaction via begin().
129 * Used to provide additional context for error reporting.
130 *
131 * @var string
132 * @see DatabaseBase::mTrxLevel
133 */
134 private $mTrxFname = null;
135
136 /**
137 * Record if possible write queries were done in the last transaction started
138 *
139 * @var bool
140 * @see DatabaseBase::mTrxLevel
141 */
142 private $mTrxDoneWrites = false;
143
144 /**
145 * Record if the current transaction was started implicitly due to DBO_TRX being set.
146 *
147 * @var bool
148 * @see DatabaseBase::mTrxLevel
149 */
150 private $mTrxAutomatic = false;
151
152 /**
153 * Array of levels of atomicity within transactions
154 *
155 * @var array
156 */
157 private $mTrxAtomicLevels = [];
158
159 /**
160 * Record if the current transaction was started implicitly by DatabaseBase::startAtomic
161 *
162 * @var bool
163 */
164 private $mTrxAutomaticAtomic = false;
165
166 /**
167 * Track the write query callers of the current transaction
168 *
169 * @var string[]
170 */
171 private $mTrxWriteCallers = [];
172
173 /**
174 * Track the seconds spent in write queries for the current transaction
175 *
176 * @var float
177 */
178 private $mTrxWriteDuration = 0.0;
179
180 /** @var array Map of (name => 1) for locks obtained via lock() */
181 private $mNamedLocksHeld = [];
182
183 /** @var IDatabase|null Lazy handle to the master DB this server replicates from */
184 private $lazyMasterHandle;
185
186 /**
187 * @since 1.21
188 * @var resource File handle for upgrade
189 */
190 protected $fileHandle = null;
191
192 /**
193 * @since 1.22
194 * @var string[] Process cache of VIEWs names in the database
195 */
196 protected $allViews = null;
197
198 /** @var float UNIX timestamp */
199 protected $lastPing = 0.0;
200
201 /** @var int[] Prior mFlags values */
202 private $priorFlags = [];
203
204 /** @var Profiler */
205 protected $profiler;
206 /** @var TransactionProfiler */
207 protected $trxProfiler;
208
209 public function getServerInfo() {
210 return $this->getServerVersion();
211 }
212
213 /**
214 * @return string Command delimiter used by this database engine
215 */
216 public function getDelimiter() {
217 return $this->delimiter;
218 }
219
220 /**
221 * Boolean, controls output of large amounts of debug information.
222 * @param bool|null $debug
223 * - true to enable debugging
224 * - false to disable debugging
225 * - omitted or null to do nothing
226 *
227 * @return bool|null Previous value of the flag
228 */
229 public function debug( $debug = null ) {
230 return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
231 }
232
233 public function bufferResults( $buffer = null ) {
234 if ( is_null( $buffer ) ) {
235 return !(bool)( $this->mFlags & DBO_NOBUFFER );
236 } else {
237 return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
238 }
239 }
240
241 /**
242 * Turns on (false) or off (true) the automatic generation and sending
243 * of a "we're sorry, but there has been a database error" page on
244 * database errors. Default is on (false). When turned off, the
245 * code should use lastErrno() and lastError() to handle the
246 * situation as appropriate.
247 *
248 * Do not use this function outside of the Database classes.
249 *
250 * @param null|bool $ignoreErrors
251 * @return bool The previous value of the flag.
252 */
253 protected function ignoreErrors( $ignoreErrors = null ) {
254 return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
255 }
256
257 public function trxLevel() {
258 return $this->mTrxLevel;
259 }
260
261 public function trxTimestamp() {
262 return $this->mTrxLevel ? $this->mTrxTimestamp : null;
263 }
264
265 public function tablePrefix( $prefix = null ) {
266 return wfSetVar( $this->mTablePrefix, $prefix );
267 }
268
269 public function dbSchema( $schema = null ) {
270 return wfSetVar( $this->mSchema, $schema );
271 }
272
273 /**
274 * Set the filehandle to copy write statements to.
275 *
276 * @param resource $fh File handle
277 */
278 public function setFileHandle( $fh ) {
279 $this->fileHandle = $fh;
280 }
281
282 public function getLBInfo( $name = null ) {
283 if ( is_null( $name ) ) {
284 return $this->mLBInfo;
285 } else {
286 if ( array_key_exists( $name, $this->mLBInfo ) ) {
287 return $this->mLBInfo[$name];
288 } else {
289 return null;
290 }
291 }
292 }
293
294 public function setLBInfo( $name, $value = null ) {
295 if ( is_null( $value ) ) {
296 $this->mLBInfo = $name;
297 } else {
298 $this->mLBInfo[$name] = $value;
299 }
300 }
301
302 /**
303 * Set a lazy-connecting DB handle to the master DB (for replication status purposes)
304 *
305 * @param IDatabase $conn
306 * @since 1.27
307 */
308 public function setLazyMasterHandle( IDatabase $conn ) {
309 $this->lazyMasterHandle = $conn;
310 }
311
312 /**
313 * @return IDatabase|null
314 * @see setLazyMasterHandle()
315 * @since 1.27
316 */
317 public function getLazyMasterHandle() {
318 return $this->lazyMasterHandle;
319 }
320
321 /**
322 * @return TransactionProfiler
323 */
324 protected function getTransactionProfiler() {
325 return $this->trxProfiler;
326 }
327
328 /**
329 * @param TransactionProfiler $profiler
330 * @since 1.27
331 */
332 public function setTransactionProfiler( TransactionProfiler $profiler ) {
333 $this->trxProfiler = $profiler;
334 }
335
336 /**
337 * Returns true if this database supports (and uses) cascading deletes
338 *
339 * @return bool
340 */
341 public function cascadingDeletes() {
342 return false;
343 }
344
345 /**
346 * Returns true if this database supports (and uses) triggers (e.g. on the page table)
347 *
348 * @return bool
349 */
350 public function cleanupTriggers() {
351 return false;
352 }
353
354 /**
355 * Returns true if this database is strict about what can be put into an IP field.
356 * Specifically, it uses a NULL value instead of an empty string.
357 *
358 * @return bool
359 */
360 public function strictIPs() {
361 return false;
362 }
363
364 /**
365 * Returns true if this database uses timestamps rather than integers
366 *
367 * @return bool
368 */
369 public function realTimestamps() {
370 return false;
371 }
372
373 public function implicitGroupby() {
374 return true;
375 }
376
377 public function implicitOrderby() {
378 return true;
379 }
380
381 /**
382 * Returns true if this database can do a native search on IP columns
383 * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
384 *
385 * @return bool
386 */
387 public function searchableIPs() {
388 return false;
389 }
390
391 /**
392 * Returns true if this database can use functional indexes
393 *
394 * @return bool
395 */
396 public function functionalIndexes() {
397 return false;
398 }
399
400 public function lastQuery() {
401 return $this->mLastQuery;
402 }
403
404 public function doneWrites() {
405 return (bool)$this->mDoneWrites;
406 }
407
408 public function lastDoneWrites() {
409 return $this->mDoneWrites ?: false;
410 }
411
412 public function writesPending() {
413 return $this->mTrxLevel && $this->mTrxDoneWrites;
414 }
415
416 public function writesOrCallbacksPending() {
417 return $this->mTrxLevel && (
418 $this->mTrxDoneWrites || $this->mTrxIdleCallbacks || $this->mTrxPreCommitCallbacks
419 );
420 }
421
422 public function pendingWriteQueryDuration() {
423 return $this->mTrxLevel ? $this->mTrxWriteDuration : false;
424 }
425
426 public function pendingWriteCallers() {
427 return $this->mTrxLevel ? $this->mTrxWriteCallers : [];
428 }
429
430 public function isOpen() {
431 return $this->mOpened;
432 }
433
434 public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
435 if ( $remember === self::REMEMBER_PRIOR ) {
436 array_push( $this->priorFlags, $this->mFlags );
437 }
438 $this->mFlags |= $flag;
439 }
440
441 public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
442 if ( $remember === self::REMEMBER_PRIOR ) {
443 array_push( $this->priorFlags, $this->mFlags );
444 }
445 $this->mFlags &= ~$flag;
446 }
447
448 public function restoreFlags( $state = self::RESTORE_PRIOR ) {
449 if ( !$this->priorFlags ) {
450 return;
451 }
452
453 if ( $state === self::RESTORE_INITIAL ) {
454 $this->mFlags = reset( $this->priorFlags );
455 $this->priorFlags = [];
456 } else {
457 $this->mFlags = array_pop( $this->priorFlags );
458 }
459 }
460
461 public function getFlag( $flag ) {
462 return !!( $this->mFlags & $flag );
463 }
464
465 public function getProperty( $name ) {
466 return $this->$name;
467 }
468
469 public function getWikiID() {
470 if ( $this->mTablePrefix ) {
471 return "{$this->mDBname}-{$this->mTablePrefix}";
472 } else {
473 return $this->mDBname;
474 }
475 }
476
477 /**
478 * Return a path to the DBMS-specific SQL file if it exists,
479 * otherwise default SQL file
480 *
481 * @param string $filename
482 * @return string
483 */
484 private function getSqlFilePath( $filename ) {
485 global $IP;
486 $dbmsSpecificFilePath = "$IP/maintenance/" . $this->getType() . "/$filename";
487 if ( file_exists( $dbmsSpecificFilePath ) ) {
488 return $dbmsSpecificFilePath;
489 } else {
490 return "$IP/maintenance/$filename";
491 }
492 }
493
494 /**
495 * Return a path to the DBMS-specific schema file,
496 * otherwise default to tables.sql
497 *
498 * @return string
499 */
500 public function getSchemaPath() {
501 return $this->getSqlFilePath( 'tables.sql' );
502 }
503
504 /**
505 * Return a path to the DBMS-specific update key file,
506 * otherwise default to update-keys.sql
507 *
508 * @return string
509 */
510 public function getUpdateKeysPath() {
511 return $this->getSqlFilePath( 'update-keys.sql' );
512 }
513
514 /**
515 * Get information about an index into an object
516 * @param string $table Table name
517 * @param string $index Index name
518 * @param string $fname Calling function name
519 * @return mixed Database-specific index description class or false if the index does not exist
520 */
521 abstract function indexInfo( $table, $index, $fname = __METHOD__ );
522
523 /**
524 * Wrapper for addslashes()
525 *
526 * @param string $s String to be slashed.
527 * @return string Slashed string.
528 */
529 abstract function strencode( $s );
530
531 /**
532 * Constructor.
533 *
534 * FIXME: It is possible to construct a Database object with no associated
535 * connection object, by specifying no parameters to __construct(). This
536 * feature is deprecated and should be removed.
537 *
538 * DatabaseBase subclasses should not be constructed directly in external
539 * code. DatabaseBase::factory() should be used instead.
540 *
541 * @param array $params Parameters passed from DatabaseBase::factory()
542 */
543 function __construct( array $params ) {
544 global $wgDBprefix, $wgDBmwschema, $wgCommandLineMode;
545
546 $this->srvCache = ObjectCache::getLocalServerInstance( 'hash' );
547
548 $server = $params['host'];
549 $user = $params['user'];
550 $password = $params['password'];
551 $dbName = $params['dbname'];
552 $flags = $params['flags'];
553 $tablePrefix = $params['tablePrefix'];
554 $schema = $params['schema'];
555 $foreign = $params['foreign'];
556
557 $this->mFlags = $flags;
558 if ( $this->mFlags & DBO_DEFAULT ) {
559 if ( $wgCommandLineMode ) {
560 $this->mFlags &= ~DBO_TRX;
561 } else {
562 $this->mFlags |= DBO_TRX;
563 }
564 }
565
566 $this->mSessionVars = $params['variables'];
567
568 /** Get the default table prefix*/
569 if ( $tablePrefix === 'get from global' ) {
570 $this->mTablePrefix = $wgDBprefix;
571 } else {
572 $this->mTablePrefix = $tablePrefix;
573 }
574
575 /** Get the database schema*/
576 if ( $schema === 'get from global' ) {
577 $this->mSchema = $wgDBmwschema;
578 } else {
579 $this->mSchema = $schema;
580 }
581
582 $this->mForeign = $foreign;
583
584 $this->profiler = isset( $params['profiler'] )
585 ? $params['profiler']
586 : Profiler::instance(); // @TODO: remove global state
587 $this->trxProfiler = isset( $params['trxProfiler'] )
588 ? $params['trxProfiler']
589 : new TransactionProfiler();
590
591 if ( $user ) {
592 $this->open( $server, $user, $password, $dbName );
593 }
594
595 }
596
597 /**
598 * Called by serialize. Throw an exception when DB connection is serialized.
599 * This causes problems on some database engines because the connection is
600 * not restored on unserialize.
601 */
602 public function __sleep() {
603 throw new MWException( 'Database serialization may cause problems, since ' .
604 'the connection is not restored on wakeup.' );
605 }
606
607 /**
608 * Given a DB type, construct the name of the appropriate child class of
609 * DatabaseBase. This is designed to replace all of the manual stuff like:
610 * $class = 'Database' . ucfirst( strtolower( $dbType ) );
611 * as well as validate against the canonical list of DB types we have
612 *
613 * This factory function is mostly useful for when you need to connect to a
614 * database other than the MediaWiki default (such as for external auth,
615 * an extension, et cetera). Do not use this to connect to the MediaWiki
616 * database. Example uses in core:
617 * @see LoadBalancer::reallyOpenConnection()
618 * @see ForeignDBRepo::getMasterDB()
619 * @see WebInstallerDBConnect::execute()
620 *
621 * @since 1.18
622 *
623 * @param string $dbType A possible DB type
624 * @param array $p An array of options to pass to the constructor.
625 * Valid options are: host, user, password, dbname, flags, tablePrefix, schema, driver
626 * @throws MWException If the database driver or extension cannot be found
627 * @return DatabaseBase|null DatabaseBase subclass or null
628 */
629 final public static function factory( $dbType, $p = [] ) {
630 $canonicalDBTypes = [
631 'mysql' => [ 'mysqli', 'mysql' ],
632 'postgres' => [],
633 'sqlite' => [],
634 'oracle' => [],
635 'mssql' => [],
636 ];
637
638 $driver = false;
639 $dbType = strtolower( $dbType );
640 if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
641 $possibleDrivers = $canonicalDBTypes[$dbType];
642 if ( !empty( $p['driver'] ) ) {
643 if ( in_array( $p['driver'], $possibleDrivers ) ) {
644 $driver = $p['driver'];
645 } else {
646 throw new MWException( __METHOD__ .
647 " cannot construct Database with type '$dbType' and driver '{$p['driver']}'" );
648 }
649 } else {
650 foreach ( $possibleDrivers as $posDriver ) {
651 if ( extension_loaded( $posDriver ) ) {
652 $driver = $posDriver;
653 break;
654 }
655 }
656 }
657 } else {
658 $driver = $dbType;
659 }
660 if ( $driver === false ) {
661 throw new MWException( __METHOD__ .
662 " no viable database extension found for type '$dbType'" );
663 }
664
665 // Determine schema defaults. Currently Microsoft SQL Server uses $wgDBmwschema,
666 // and everything else doesn't use a schema (e.g. null)
667 // Although postgres and oracle support schemas, we don't use them (yet)
668 // to maintain backwards compatibility
669 $defaultSchemas = [
670 'mssql' => 'get from global',
671 ];
672
673 $class = 'Database' . ucfirst( $driver );
674 if ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) {
675 // Resolve some defaults for b/c
676 $p['host'] = isset( $p['host'] ) ? $p['host'] : false;
677 $p['user'] = isset( $p['user'] ) ? $p['user'] : false;
678 $p['password'] = isset( $p['password'] ) ? $p['password'] : false;
679 $p['dbname'] = isset( $p['dbname'] ) ? $p['dbname'] : false;
680 $p['flags'] = isset( $p['flags'] ) ? $p['flags'] : 0;
681 $p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
682 $p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global';
683 if ( !isset( $p['schema'] ) ) {
684 $p['schema'] = isset( $defaultSchemas[$dbType] ) ? $defaultSchemas[$dbType] : null;
685 }
686 $p['foreign'] = isset( $p['foreign'] ) ? $p['foreign'] : false;
687
688 return new $class( $p );
689 } else {
690 return null;
691 }
692 }
693
694 protected function installErrorHandler() {
695 $this->mPHPError = false;
696 $this->htmlErrors = ini_set( 'html_errors', '0' );
697 set_error_handler( [ $this, 'connectionErrorHandler' ] );
698 }
699
700 /**
701 * @return bool|string
702 */
703 protected function restoreErrorHandler() {
704 restore_error_handler();
705 if ( $this->htmlErrors !== false ) {
706 ini_set( 'html_errors', $this->htmlErrors );
707 }
708 if ( $this->mPHPError ) {
709 $error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
710 $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
711
712 return $error;
713 } else {
714 return false;
715 }
716 }
717
718 /**
719 * @param int $errno
720 * @param string $errstr
721 */
722 public function connectionErrorHandler( $errno, $errstr ) {
723 $this->mPHPError = $errstr;
724 }
725
726 /**
727 * Create a log context to pass to wfLogDBError or other logging functions.
728 *
729 * @param array $extras Additional data to add to context
730 * @return array
731 */
732 protected function getLogContext( array $extras = [] ) {
733 return array_merge(
734 [
735 'db_server' => $this->mServer,
736 'db_name' => $this->mDBname,
737 'db_user' => $this->mUser,
738 ],
739 $extras
740 );
741 }
742
743 public function close() {
744 if ( $this->mConn ) {
745 if ( $this->trxLevel() ) {
746 if ( !$this->mTrxAutomatic ) {
747 wfWarn( "Transaction still in progress (from {$this->mTrxFname}), " .
748 " performing implicit commit before closing connection!" );
749 }
750
751 $this->commit( __METHOD__, self::FLUSHING_INTERNAL );
752 }
753
754 $closed = $this->closeConnection();
755 $this->mConn = false;
756 } elseif ( $this->mTrxIdleCallbacks || $this->mTrxEndCallbacks ) { // sanity
757 throw new MWException( "Transaction callbacks still pending." );
758 } else {
759 $closed = true;
760 }
761 $this->mOpened = false;
762
763 return $closed;
764 }
765
766 /**
767 * Make sure isOpen() returns true as a sanity check
768 *
769 * @throws DBUnexpectedError
770 */
771 protected function assertOpen() {
772 if ( !$this->isOpen() ) {
773 throw new DBUnexpectedError( $this, "DB connection was already closed." );
774 }
775 }
776
777 /**
778 * Closes underlying database connection
779 * @since 1.20
780 * @return bool Whether connection was closed successfully
781 */
782 abstract protected function closeConnection();
783
784 function reportConnectionError( $error = 'Unknown error' ) {
785 $myError = $this->lastError();
786 if ( $myError ) {
787 $error = $myError;
788 }
789
790 # New method
791 throw new DBConnectionError( $this, $error );
792 }
793
794 /**
795 * The DBMS-dependent part of query()
796 *
797 * @param string $sql SQL query.
798 * @return ResultWrapper|bool Result object to feed to fetchObject,
799 * fetchRow, ...; or false on failure
800 */
801 abstract protected function doQuery( $sql );
802
803 /**
804 * Determine whether a query writes to the DB.
805 * Should return true if unsure.
806 *
807 * @param string $sql
808 * @return bool
809 */
810 protected function isWriteQuery( $sql ) {
811 return !preg_match( '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
812 }
813
814 /**
815 * Determine whether a SQL statement is sensitive to isolation level.
816 * A SQL statement is considered transactable if its result could vary
817 * depending on the transaction isolation level. Operational commands
818 * such as 'SET' and 'SHOW' are not considered to be transactable.
819 *
820 * @param string $sql
821 * @return bool
822 */
823 protected function isTransactableQuery( $sql ) {
824 $verb = substr( $sql, 0, strcspn( $sql, " \t\r\n" ) );
825 return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ] );
826 }
827
828 public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
829 global $wgUser;
830
831 $priorWritesPending = $this->writesOrCallbacksPending();
832 $this->mLastQuery = $sql;
833
834 $isWrite = $this->isWriteQuery( $sql );
835 if ( $isWrite ) {
836 $reason = $this->getReadOnlyReason();
837 if ( $reason !== false ) {
838 throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
839 }
840 # Set a flag indicating that writes have been done
841 $this->mDoneWrites = microtime( true );
842 }
843
844 # Add a comment for easy SHOW PROCESSLIST interpretation
845 if ( is_object( $wgUser ) && $wgUser->isItemLoaded( 'name' ) ) {
846 $userName = $wgUser->getName();
847 if ( mb_strlen( $userName ) > 15 ) {
848 $userName = mb_substr( $userName, 0, 15 ) . '...';
849 }
850 $userName = str_replace( '/', '', $userName );
851 } else {
852 $userName = '';
853 }
854
855 // Add trace comment to the begin of the sql string, right after the operator.
856 // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (bug 42598)
857 $commentedSql = preg_replace( '/\s|$/', " /* $fname $userName */ ", $sql, 1 );
858
859 # Start implicit transactions that wrap the request if DBO_TRX is enabled
860 if ( !$this->mTrxLevel && $this->getFlag( DBO_TRX )
861 && $this->isTransactableQuery( $sql )
862 ) {
863 $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
864 $this->mTrxAutomatic = true;
865 }
866
867 # Keep track of whether the transaction has write queries pending
868 if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWrite ) {
869 $this->mTrxDoneWrites = true;
870 $this->getTransactionProfiler()->transactionWritingIn(
871 $this->mServer, $this->mDBname, $this->mTrxShortId );
872 }
873
874 if ( $this->debug() ) {
875 wfDebugLog( 'queries', sprintf( "%s: %s", $this->mDBname, $commentedSql ) );
876 }
877
878 # Avoid fatals if close() was called
879 $this->assertOpen();
880
881 # Send the query to the server
882 $ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
883
884 # Try reconnecting if the connection was lost
885 if ( false === $ret && $this->wasErrorReissuable() ) {
886 $recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
887 # Stash the last error values before anything might clear them
888 $lastError = $this->lastError();
889 $lastErrno = $this->lastErrno();
890 # Update state tracking to reflect transaction loss due to disconnection
891 $this->handleTransactionLoss();
892 wfDebug( "Connection lost, reconnecting...\n" );
893 if ( $this->reconnect() ) {
894 wfDebug( "Reconnected\n" );
895 $msg = __METHOD__ . ": lost connection to {$this->getServer()}; reconnected";
896 wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
897
898 if ( !$recoverable ) {
899 # Callers may catch the exception and continue to use the DB
900 $this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
901 } else {
902 # Should be safe to silently retry the query
903 $ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
904 }
905 } else {
906 wfDebug( "Failed\n" );
907 }
908 }
909
910 if ( false === $ret ) {
911 # Deadlocks cause the entire transaction to abort, not just the statement.
912 # http://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
913 # https://www.postgresql.org/docs/9.1/static/explicit-locking.html
914 if ( $this->wasDeadlock() ) {
915 if ( $this->explicitTrxActive() || $priorWritesPending ) {
916 $tempIgnore = false; // not recoverable
917 }
918 # Update state tracking to reflect transaction loss
919 $this->handleTransactionLoss();
920 }
921
922 $this->reportQueryError(
923 $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
924 }
925
926 $res = $this->resultObject( $ret );
927
928 return $res;
929 }
930
931 private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) {
932 $isMaster = !is_null( $this->getLBInfo( 'master' ) );
933 # generalizeSQL() will probably cut down the query to reasonable
934 # logging size most of the time. The substr is really just a sanity check.
935 if ( $isMaster ) {
936 $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
937 } else {
938 $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
939 }
940
941 # Include query transaction state
942 $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
943
944 $startTime = microtime( true );
945 $this->profiler->profileIn( $queryProf );
946 $ret = $this->doQuery( $commentedSql );
947 $this->profiler->profileOut( $queryProf );
948 $queryRuntime = microtime( true ) - $startTime;
949
950 unset( $queryProfSection ); // profile out (if set)
951
952 if ( $ret !== false ) {
953 $this->lastPing = $startTime;
954 if ( $isWrite && $this->mTrxLevel ) {
955 $this->mTrxWriteDuration += $queryRuntime;
956 $this->mTrxWriteCallers[] = $fname;
957 }
958 }
959
960 $this->getTransactionProfiler()->recordQueryCompletion(
961 $queryProf, $startTime, $isWrite, $this->affectedRows()
962 );
963 MWDebug::query( $sql, $fname, $isMaster, $queryRuntime );
964
965 return $ret;
966 }
967
968 private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
969 # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
970 # Dropped connections also mean that named locks are automatically released.
971 # Only allow error suppression in autocommit mode or when the lost transaction
972 # didn't matter anyway (aside from DBO_TRX snapshot loss).
973 if ( $this->mNamedLocksHeld ) {
974 return false; // possible critical section violation
975 } elseif ( $sql === 'COMMIT' ) {
976 return !$priorWritesPending; // nothing written anyway? (T127428)
977 } elseif ( $sql === 'ROLLBACK' ) {
978 return true; // transaction lost...which is also what was requested :)
979 } elseif ( $this->explicitTrxActive() ) {
980 return false; // don't drop atomocity
981 } elseif ( $priorWritesPending ) {
982 return false; // prior writes lost from implicit transaction
983 }
984
985 return true;
986 }
987
988 private function handleTransactionLoss() {
989 $this->mTrxLevel = 0;
990 $this->mTrxIdleCallbacks = []; // bug 65263
991 $this->mTrxPreCommitCallbacks = []; // bug 65263
992 try {
993 // Handle callbacks in mTrxEndCallbacks
994 $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
995 return null;
996 } catch ( Exception $e ) {
997 // Already logged; move on...
998 return $e;
999 }
1000 }
1001
1002 public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
1003 if ( $this->ignoreErrors() || $tempIgnore ) {
1004 wfDebug( "SQL ERROR (ignored): $error\n" );
1005 } else {
1006 $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
1007 wfLogDBError(
1008 "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
1009 $this->getLogContext( [
1010 'method' => __METHOD__,
1011 'errno' => $errno,
1012 'error' => $error,
1013 'sql1line' => $sql1line,
1014 'fname' => $fname,
1015 ] )
1016 );
1017 wfDebug( "SQL ERROR: " . $error . "\n" );
1018 throw new DBQueryError( $this, $error, $errno, $sql, $fname );
1019 }
1020 }
1021
1022 /**
1023 * Intended to be compatible with the PEAR::DB wrapper functions.
1024 * http://pear.php.net/manual/en/package.database.db.intro-execute.php
1025 *
1026 * ? = scalar value, quoted as necessary
1027 * ! = raw SQL bit (a function for instance)
1028 * & = filename; reads the file and inserts as a blob
1029 * (we don't use this though...)
1030 *
1031 * @param string $sql
1032 * @param string $func
1033 *
1034 * @return array
1035 */
1036 protected function prepare( $sql, $func = 'DatabaseBase::prepare' ) {
1037 /* MySQL doesn't support prepared statements (yet), so just
1038 * pack up the query for reference. We'll manually replace
1039 * the bits later.
1040 */
1041 return [ 'query' => $sql, 'func' => $func ];
1042 }
1043
1044 /**
1045 * Free a prepared query, generated by prepare().
1046 * @param string $prepared
1047 */
1048 protected function freePrepared( $prepared ) {
1049 /* No-op by default */
1050 }
1051
1052 /**
1053 * Execute a prepared query with the various arguments
1054 * @param string $prepared The prepared sql
1055 * @param mixed $args Either an array here, or put scalars as varargs
1056 *
1057 * @return ResultWrapper
1058 */
1059 public function execute( $prepared, $args = null ) {
1060 if ( !is_array( $args ) ) {
1061 # Pull the var args
1062 $args = func_get_args();
1063 array_shift( $args );
1064 }
1065
1066 $sql = $this->fillPrepared( $prepared['query'], $args );
1067
1068 return $this->query( $sql, $prepared['func'] );
1069 }
1070
1071 /**
1072 * For faking prepared SQL statements on DBs that don't support it directly.
1073 *
1074 * @param string $preparedQuery A 'preparable' SQL statement
1075 * @param array $args Array of Arguments to fill it with
1076 * @return string Executable SQL
1077 */
1078 public function fillPrepared( $preparedQuery, $args ) {
1079 reset( $args );
1080 $this->preparedArgs =& $args;
1081
1082 return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
1083 [ &$this, 'fillPreparedArg' ], $preparedQuery );
1084 }
1085
1086 /**
1087 * preg_callback func for fillPrepared()
1088 * The arguments should be in $this->preparedArgs and must not be touched
1089 * while we're doing this.
1090 *
1091 * @param array $matches
1092 * @throws DBUnexpectedError
1093 * @return string
1094 */
1095 protected function fillPreparedArg( $matches ) {
1096 switch ( $matches[1] ) {
1097 case '\\?':
1098 return '?';
1099 case '\\!':
1100 return '!';
1101 case '\\&':
1102 return '&';
1103 }
1104
1105 list( /* $n */, $arg ) = each( $this->preparedArgs );
1106
1107 switch ( $matches[1] ) {
1108 case '?':
1109 return $this->addQuotes( $arg );
1110 case '!':
1111 return $arg;
1112 case '&':
1113 # return $this->addQuotes( file_get_contents( $arg ) );
1114 throw new DBUnexpectedError(
1115 $this,
1116 '& mode is not implemented. If it\'s really needed, uncomment the line above.'
1117 );
1118 default:
1119 throw new DBUnexpectedError(
1120 $this,
1121 'Received invalid match. This should never happen!'
1122 );
1123 }
1124 }
1125
1126 public function freeResult( $res ) {
1127 }
1128
1129 public function selectField(
1130 $table, $var, $cond = '', $fname = __METHOD__, $options = []
1131 ) {
1132 if ( $var === '*' ) { // sanity
1133 throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1134 }
1135
1136 if ( !is_array( $options ) ) {
1137 $options = [ $options ];
1138 }
1139
1140 $options['LIMIT'] = 1;
1141
1142 $res = $this->select( $table, $var, $cond, $fname, $options );
1143 if ( $res === false || !$this->numRows( $res ) ) {
1144 return false;
1145 }
1146
1147 $row = $this->fetchRow( $res );
1148
1149 if ( $row !== false ) {
1150 return reset( $row );
1151 } else {
1152 return false;
1153 }
1154 }
1155
1156 public function selectFieldValues(
1157 $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1158 ) {
1159 if ( $var === '*' ) { // sanity
1160 throw new DBUnexpectedError( $this, "Cannot use a * field" );
1161 } elseif ( !is_string( $var ) ) { // sanity
1162 throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1163 }
1164
1165 if ( !is_array( $options ) ) {
1166 $options = [ $options ];
1167 }
1168
1169 $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1170 if ( $res === false ) {
1171 return false;
1172 }
1173
1174 $values = [];
1175 foreach ( $res as $row ) {
1176 $values[] = $row->$var;
1177 }
1178
1179 return $values;
1180 }
1181
1182 /**
1183 * Returns an optional USE INDEX clause to go after the table, and a
1184 * string to go at the end of the query.
1185 *
1186 * @param array $options Associative array of options to be turned into
1187 * an SQL query, valid keys are listed in the function.
1188 * @return array
1189 * @see DatabaseBase::select()
1190 */
1191 public function makeSelectOptions( $options ) {
1192 $preLimitTail = $postLimitTail = '';
1193 $startOpts = '';
1194
1195 $noKeyOptions = [];
1196
1197 foreach ( $options as $key => $option ) {
1198 if ( is_numeric( $key ) ) {
1199 $noKeyOptions[$option] = true;
1200 }
1201 }
1202
1203 $preLimitTail .= $this->makeGroupByWithHaving( $options );
1204
1205 $preLimitTail .= $this->makeOrderBy( $options );
1206
1207 // if (isset($options['LIMIT'])) {
1208 // $tailOpts .= $this->limitResult('', $options['LIMIT'],
1209 // isset($options['OFFSET']) ? $options['OFFSET']
1210 // : false);
1211 // }
1212
1213 if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1214 $postLimitTail .= ' FOR UPDATE';
1215 }
1216
1217 if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1218 $postLimitTail .= ' LOCK IN SHARE MODE';
1219 }
1220
1221 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1222 $startOpts .= 'DISTINCT';
1223 }
1224
1225 # Various MySQL extensions
1226 if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1227 $startOpts .= ' /*! STRAIGHT_JOIN */';
1228 }
1229
1230 if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
1231 $startOpts .= ' HIGH_PRIORITY';
1232 }
1233
1234 if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1235 $startOpts .= ' SQL_BIG_RESULT';
1236 }
1237
1238 if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1239 $startOpts .= ' SQL_BUFFER_RESULT';
1240 }
1241
1242 if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1243 $startOpts .= ' SQL_SMALL_RESULT';
1244 }
1245
1246 if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1247 $startOpts .= ' SQL_CALC_FOUND_ROWS';
1248 }
1249
1250 if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
1251 $startOpts .= ' SQL_CACHE';
1252 }
1253
1254 if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
1255 $startOpts .= ' SQL_NO_CACHE';
1256 }
1257
1258 if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1259 $useIndex = $this->useIndexClause( $options['USE INDEX'] );
1260 } else {
1261 $useIndex = '';
1262 }
1263
1264 return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail ];
1265 }
1266
1267 /**
1268 * Returns an optional GROUP BY with an optional HAVING
1269 *
1270 * @param array $options Associative array of options
1271 * @return string
1272 * @see DatabaseBase::select()
1273 * @since 1.21
1274 */
1275 public function makeGroupByWithHaving( $options ) {
1276 $sql = '';
1277 if ( isset( $options['GROUP BY'] ) ) {
1278 $gb = is_array( $options['GROUP BY'] )
1279 ? implode( ',', $options['GROUP BY'] )
1280 : $options['GROUP BY'];
1281 $sql .= ' GROUP BY ' . $gb;
1282 }
1283 if ( isset( $options['HAVING'] ) ) {
1284 $having = is_array( $options['HAVING'] )
1285 ? $this->makeList( $options['HAVING'], LIST_AND )
1286 : $options['HAVING'];
1287 $sql .= ' HAVING ' . $having;
1288 }
1289
1290 return $sql;
1291 }
1292
1293 /**
1294 * Returns an optional ORDER BY
1295 *
1296 * @param array $options Associative array of options
1297 * @return string
1298 * @see DatabaseBase::select()
1299 * @since 1.21
1300 */
1301 public function makeOrderBy( $options ) {
1302 if ( isset( $options['ORDER BY'] ) ) {
1303 $ob = is_array( $options['ORDER BY'] )
1304 ? implode( ',', $options['ORDER BY'] )
1305 : $options['ORDER BY'];
1306
1307 return ' ORDER BY ' . $ob;
1308 }
1309
1310 return '';
1311 }
1312
1313 // See IDatabase::select for the docs for this function
1314 public function select( $table, $vars, $conds = '', $fname = __METHOD__,
1315 $options = [], $join_conds = [] ) {
1316 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1317
1318 return $this->query( $sql, $fname );
1319 }
1320
1321 public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1322 $options = [], $join_conds = []
1323 ) {
1324 if ( is_array( $vars ) ) {
1325 $vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1326 }
1327
1328 $options = (array)$options;
1329 $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1330 ? $options['USE INDEX']
1331 : [];
1332
1333 if ( is_array( $table ) ) {
1334 $from = ' FROM ' .
1335 $this->tableNamesWithUseIndexOrJOIN( $table, $useIndexes, $join_conds );
1336 } elseif ( $table != '' ) {
1337 if ( $table[0] == ' ' ) {
1338 $from = ' FROM ' . $table;
1339 } else {
1340 $from = ' FROM ' .
1341 $this->tableNamesWithUseIndexOrJOIN( [ $table ], $useIndexes, [] );
1342 }
1343 } else {
1344 $from = '';
1345 }
1346
1347 list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) =
1348 $this->makeSelectOptions( $options );
1349
1350 if ( !empty( $conds ) ) {
1351 if ( is_array( $conds ) ) {
1352 $conds = $this->makeList( $conds, LIST_AND );
1353 }
1354 $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
1355 } else {
1356 $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
1357 }
1358
1359 if ( isset( $options['LIMIT'] ) ) {
1360 $sql = $this->limitResult( $sql, $options['LIMIT'],
1361 isset( $options['OFFSET'] ) ? $options['OFFSET'] : false );
1362 }
1363 $sql = "$sql $postLimitTail";
1364
1365 if ( isset( $options['EXPLAIN'] ) ) {
1366 $sql = 'EXPLAIN ' . $sql;
1367 }
1368
1369 return $sql;
1370 }
1371
1372 public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1373 $options = [], $join_conds = []
1374 ) {
1375 $options = (array)$options;
1376 $options['LIMIT'] = 1;
1377 $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1378
1379 if ( $res === false ) {
1380 return false;
1381 }
1382
1383 if ( !$this->numRows( $res ) ) {
1384 return false;
1385 }
1386
1387 $obj = $this->fetchObject( $res );
1388
1389 return $obj;
1390 }
1391
1392 public function estimateRowCount(
1393 $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
1394 ) {
1395 $rows = 0;
1396 $res = $this->select( $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options );
1397
1398 if ( $res ) {
1399 $row = $this->fetchRow( $res );
1400 $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1401 }
1402
1403 return $rows;
1404 }
1405
1406 public function selectRowCount(
1407 $tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1408 ) {
1409 $rows = 0;
1410 $sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
1411 $res = $this->query( "SELECT COUNT(*) AS rowcount FROM ($sql) tmp_count", $fname );
1412
1413 if ( $res ) {
1414 $row = $this->fetchRow( $res );
1415 $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1416 }
1417
1418 return $rows;
1419 }
1420
1421 /**
1422 * Removes most variables from an SQL query and replaces them with X or N for numbers.
1423 * It's only slightly flawed. Don't use for anything important.
1424 *
1425 * @param string $sql A SQL Query
1426 *
1427 * @return string
1428 */
1429 protected static function generalizeSQL( $sql ) {
1430 # This does the same as the regexp below would do, but in such a way
1431 # as to avoid crashing php on some large strings.
1432 # $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
1433
1434 $sql = str_replace( "\\\\", '', $sql );
1435 $sql = str_replace( "\\'", '', $sql );
1436 $sql = str_replace( "\\\"", '', $sql );
1437 $sql = preg_replace( "/'.*'/s", "'X'", $sql );
1438 $sql = preg_replace( '/".*"/s', "'X'", $sql );
1439
1440 # All newlines, tabs, etc replaced by single space
1441 $sql = preg_replace( '/\s+/', ' ', $sql );
1442
1443 # All numbers => N,
1444 # except the ones surrounded by characters, e.g. l10n
1445 $sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
1446 $sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
1447
1448 return $sql;
1449 }
1450
1451 public function fieldExists( $table, $field, $fname = __METHOD__ ) {
1452 $info = $this->fieldInfo( $table, $field );
1453
1454 return (bool)$info;
1455 }
1456
1457 public function indexExists( $table, $index, $fname = __METHOD__ ) {
1458 if ( !$this->tableExists( $table ) ) {
1459 return null;
1460 }
1461
1462 $info = $this->indexInfo( $table, $index, $fname );
1463 if ( is_null( $info ) ) {
1464 return null;
1465 } else {
1466 return $info !== false;
1467 }
1468 }
1469
1470 public function tableExists( $table, $fname = __METHOD__ ) {
1471 $table = $this->tableName( $table );
1472 $old = $this->ignoreErrors( true );
1473 $res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname );
1474 $this->ignoreErrors( $old );
1475
1476 return (bool)$res;
1477 }
1478
1479 public function indexUnique( $table, $index ) {
1480 $indexInfo = $this->indexInfo( $table, $index );
1481
1482 if ( !$indexInfo ) {
1483 return null;
1484 }
1485
1486 return !$indexInfo[0]->Non_unique;
1487 }
1488
1489 /**
1490 * Helper for DatabaseBase::insert().
1491 *
1492 * @param array $options
1493 * @return string
1494 */
1495 protected function makeInsertOptions( $options ) {
1496 return implode( ' ', $options );
1497 }
1498
1499 public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
1500 # No rows to insert, easy just return now
1501 if ( !count( $a ) ) {
1502 return true;
1503 }
1504
1505 $table = $this->tableName( $table );
1506
1507 if ( !is_array( $options ) ) {
1508 $options = [ $options ];
1509 }
1510
1511 $fh = null;
1512 if ( isset( $options['fileHandle'] ) ) {
1513 $fh = $options['fileHandle'];
1514 }
1515 $options = $this->makeInsertOptions( $options );
1516
1517 if ( isset( $a[0] ) && is_array( $a[0] ) ) {
1518 $multi = true;
1519 $keys = array_keys( $a[0] );
1520 } else {
1521 $multi = false;
1522 $keys = array_keys( $a );
1523 }
1524
1525 $sql = 'INSERT ' . $options .
1526 " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
1527
1528 if ( $multi ) {
1529 $first = true;
1530 foreach ( $a as $row ) {
1531 if ( $first ) {
1532 $first = false;
1533 } else {
1534 $sql .= ',';
1535 }
1536 $sql .= '(' . $this->makeList( $row ) . ')';
1537 }
1538 } else {
1539 $sql .= '(' . $this->makeList( $a ) . ')';
1540 }
1541
1542 if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
1543 return false;
1544 } elseif ( $fh !== null ) {
1545 return true;
1546 }
1547
1548 return (bool)$this->query( $sql, $fname );
1549 }
1550
1551 /**
1552 * Make UPDATE options array for DatabaseBase::makeUpdateOptions
1553 *
1554 * @param array $options
1555 * @return array
1556 */
1557 protected function makeUpdateOptionsArray( $options ) {
1558 if ( !is_array( $options ) ) {
1559 $options = [ $options ];
1560 }
1561
1562 $opts = [];
1563
1564 if ( in_array( 'LOW_PRIORITY', $options ) ) {
1565 $opts[] = $this->lowPriorityOption();
1566 }
1567
1568 if ( in_array( 'IGNORE', $options ) ) {
1569 $opts[] = 'IGNORE';
1570 }
1571
1572 return $opts;
1573 }
1574
1575 /**
1576 * Make UPDATE options for the DatabaseBase::update function
1577 *
1578 * @param array $options The options passed to DatabaseBase::update
1579 * @return string
1580 */
1581 protected function makeUpdateOptions( $options ) {
1582 $opts = $this->makeUpdateOptionsArray( $options );
1583
1584 return implode( ' ', $opts );
1585 }
1586
1587 function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
1588 $table = $this->tableName( $table );
1589 $opts = $this->makeUpdateOptions( $options );
1590 $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
1591
1592 if ( $conds !== [] && $conds !== '*' ) {
1593 $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
1594 }
1595
1596 return $this->query( $sql, $fname );
1597 }
1598
1599 public function makeList( $a, $mode = LIST_COMMA ) {
1600 if ( !is_array( $a ) ) {
1601 throw new DBUnexpectedError( $this, 'DatabaseBase::makeList called with incorrect parameters' );
1602 }
1603
1604 $first = true;
1605 $list = '';
1606
1607 foreach ( $a as $field => $value ) {
1608 if ( !$first ) {
1609 if ( $mode == LIST_AND ) {
1610 $list .= ' AND ';
1611 } elseif ( $mode == LIST_OR ) {
1612 $list .= ' OR ';
1613 } else {
1614 $list .= ',';
1615 }
1616 } else {
1617 $first = false;
1618 }
1619
1620 if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) {
1621 $list .= "($value)";
1622 } elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) {
1623 $list .= "$value";
1624 } elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) {
1625 // Remove null from array to be handled separately if found
1626 $includeNull = false;
1627 foreach ( array_keys( $value, null, true ) as $nullKey ) {
1628 $includeNull = true;
1629 unset( $value[$nullKey] );
1630 }
1631 if ( count( $value ) == 0 && !$includeNull ) {
1632 throw new MWException( __METHOD__ . ": empty input for field $field" );
1633 } elseif ( count( $value ) == 0 ) {
1634 // only check if $field is null
1635 $list .= "$field IS NULL";
1636 } else {
1637 // IN clause contains at least one valid element
1638 if ( $includeNull ) {
1639 // Group subconditions to ensure correct precedence
1640 $list .= '(';
1641 }
1642 if ( count( $value ) == 1 ) {
1643 // Special-case single values, as IN isn't terribly efficient
1644 // Don't necessarily assume the single key is 0; we don't
1645 // enforce linear numeric ordering on other arrays here.
1646 $value = array_values( $value )[0];
1647 $list .= $field . " = " . $this->addQuotes( $value );
1648 } else {
1649 $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
1650 }
1651 // if null present in array, append IS NULL
1652 if ( $includeNull ) {
1653 $list .= " OR $field IS NULL)";
1654 }
1655 }
1656 } elseif ( $value === null ) {
1657 if ( $mode == LIST_AND || $mode == LIST_OR ) {
1658 $list .= "$field IS ";
1659 } elseif ( $mode == LIST_SET ) {
1660 $list .= "$field = ";
1661 }
1662 $list .= 'NULL';
1663 } else {
1664 if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
1665 $list .= "$field = ";
1666 }
1667 $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
1668 }
1669 }
1670
1671 return $list;
1672 }
1673
1674 public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
1675 $conds = [];
1676
1677 foreach ( $data as $base => $sub ) {
1678 if ( count( $sub ) ) {
1679 $conds[] = $this->makeList(
1680 [ $baseKey => $base, $subKey => array_keys( $sub ) ],
1681 LIST_AND );
1682 }
1683 }
1684
1685 if ( $conds ) {
1686 return $this->makeList( $conds, LIST_OR );
1687 } else {
1688 // Nothing to search for...
1689 return false;
1690 }
1691 }
1692
1693 /**
1694 * Return aggregated value alias
1695 *
1696 * @param array $valuedata
1697 * @param string $valuename
1698 *
1699 * @return string
1700 */
1701 public function aggregateValue( $valuedata, $valuename = 'value' ) {
1702 return $valuename;
1703 }
1704
1705 public function bitNot( $field ) {
1706 return "(~$field)";
1707 }
1708
1709 public function bitAnd( $fieldLeft, $fieldRight ) {
1710 return "($fieldLeft & $fieldRight)";
1711 }
1712
1713 public function bitOr( $fieldLeft, $fieldRight ) {
1714 return "($fieldLeft | $fieldRight)";
1715 }
1716
1717 public function buildConcat( $stringList ) {
1718 return 'CONCAT(' . implode( ',', $stringList ) . ')';
1719 }
1720
1721 public function buildGroupConcatField(
1722 $delim, $table, $field, $conds = '', $join_conds = []
1723 ) {
1724 $fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
1725
1726 return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
1727 }
1728
1729 public function selectDB( $db ) {
1730 # Stub. Shouldn't cause serious problems if it's not overridden, but
1731 # if your database engine supports a concept similar to MySQL's
1732 # databases you may as well.
1733 $this->mDBname = $db;
1734
1735 return true;
1736 }
1737
1738 public function getDBname() {
1739 return $this->mDBname;
1740 }
1741
1742 public function getServer() {
1743 return $this->mServer;
1744 }
1745
1746 /**
1747 * Format a table name ready for use in constructing an SQL query
1748 *
1749 * This does two important things: it quotes the table names to clean them up,
1750 * and it adds a table prefix if only given a table name with no quotes.
1751 *
1752 * All functions of this object which require a table name call this function
1753 * themselves. Pass the canonical name to such functions. This is only needed
1754 * when calling query() directly.
1755 *
1756 * @note This function does not sanitize user input. It is not safe to use
1757 * this function to escape user input.
1758 * @param string $name Database table name
1759 * @param string $format One of:
1760 * quoted - Automatically pass the table name through addIdentifierQuotes()
1761 * so that it can be used in a query.
1762 * raw - Do not add identifier quotes to the table name
1763 * @return string Full database name
1764 */
1765 public function tableName( $name, $format = 'quoted' ) {
1766 global $wgSharedDB, $wgSharedPrefix, $wgSharedTables, $wgSharedSchema;
1767 # Skip the entire process when we have a string quoted on both ends.
1768 # Note that we check the end so that we will still quote any use of
1769 # use of `database`.table. But won't break things if someone wants
1770 # to query a database table with a dot in the name.
1771 if ( $this->isQuotedIdentifier( $name ) ) {
1772 return $name;
1773 }
1774
1775 # Lets test for any bits of text that should never show up in a table
1776 # name. Basically anything like JOIN or ON which are actually part of
1777 # SQL queries, but may end up inside of the table value to combine
1778 # sql. Such as how the API is doing.
1779 # Note that we use a whitespace test rather than a \b test to avoid
1780 # any remote case where a word like on may be inside of a table name
1781 # surrounded by symbols which may be considered word breaks.
1782 if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
1783 return $name;
1784 }
1785
1786 # Split database and table into proper variables.
1787 # We reverse the explode so that database.table and table both output
1788 # the correct table.
1789 $dbDetails = explode( '.', $name, 3 );
1790 if ( count( $dbDetails ) == 3 ) {
1791 list( $database, $schema, $table ) = $dbDetails;
1792 # We don't want any prefix added in this case
1793 $prefix = '';
1794 } elseif ( count( $dbDetails ) == 2 ) {
1795 list( $database, $table ) = $dbDetails;
1796 # We don't want any prefix added in this case
1797 # In dbs that support it, $database may actually be the schema
1798 # but that doesn't affect any of the functionality here
1799 $prefix = '';
1800 $schema = null;
1801 } else {
1802 list( $table ) = $dbDetails;
1803 if ( $wgSharedDB !== null # We have a shared database
1804 && $this->mForeign == false # We're not working on a foreign database
1805 && !$this->isQuotedIdentifier( $table ) # Prevent shared tables listing '`table`'
1806 && in_array( $table, $wgSharedTables ) # A shared table is selected
1807 ) {
1808 $database = $wgSharedDB;
1809 $schema = $wgSharedSchema === null ? $this->mSchema : $wgSharedSchema;
1810 $prefix = $wgSharedPrefix === null ? $this->mTablePrefix : $wgSharedPrefix;
1811 } else {
1812 $database = null;
1813 $schema = $this->mSchema; # Default schema
1814 $prefix = $this->mTablePrefix; # Default prefix
1815 }
1816 }
1817
1818 # Quote $table and apply the prefix if not quoted.
1819 # $tableName might be empty if this is called from Database::replaceVars()
1820 $tableName = "{$prefix}{$table}";
1821 if ( $format == 'quoted' && !$this->isQuotedIdentifier( $tableName ) && $tableName !== '' ) {
1822 $tableName = $this->addIdentifierQuotes( $tableName );
1823 }
1824
1825 # Quote $schema and merge it with the table name if needed
1826 if ( strlen( $schema ) ) {
1827 if ( $format == 'quoted' && !$this->isQuotedIdentifier( $schema ) ) {
1828 $schema = $this->addIdentifierQuotes( $schema );
1829 }
1830 $tableName = $schema . '.' . $tableName;
1831 }
1832
1833 # Quote $database and merge it with the table name if needed
1834 if ( $database !== null ) {
1835 if ( $format == 'quoted' && !$this->isQuotedIdentifier( $database ) ) {
1836 $database = $this->addIdentifierQuotes( $database );
1837 }
1838 $tableName = $database . '.' . $tableName;
1839 }
1840
1841 return $tableName;
1842 }
1843
1844 /**
1845 * Fetch a number of table names into an array
1846 * This is handy when you need to construct SQL for joins
1847 *
1848 * Example:
1849 * extract( $dbr->tableNames( 'user', 'watchlist' ) );
1850 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1851 * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1852 *
1853 * @return array
1854 */
1855 public function tableNames() {
1856 $inArray = func_get_args();
1857 $retVal = [];
1858
1859 foreach ( $inArray as $name ) {
1860 $retVal[$name] = $this->tableName( $name );
1861 }
1862
1863 return $retVal;
1864 }
1865
1866 /**
1867 * Fetch a number of table names into an zero-indexed numerical array
1868 * This is handy when you need to construct SQL for joins
1869 *
1870 * Example:
1871 * list( $user, $watchlist ) = $dbr->tableNamesN( 'user', 'watchlist' );
1872 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1873 * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1874 *
1875 * @return array
1876 */
1877 public function tableNamesN() {
1878 $inArray = func_get_args();
1879 $retVal = [];
1880
1881 foreach ( $inArray as $name ) {
1882 $retVal[] = $this->tableName( $name );
1883 }
1884
1885 return $retVal;
1886 }
1887
1888 /**
1889 * Get an aliased table name
1890 * e.g. tableName AS newTableName
1891 *
1892 * @param string $name Table name, see tableName()
1893 * @param string|bool $alias Alias (optional)
1894 * @return string SQL name for aliased table. Will not alias a table to its own name
1895 */
1896 public function tableNameWithAlias( $name, $alias = false ) {
1897 if ( !$alias || $alias == $name ) {
1898 return $this->tableName( $name );
1899 } else {
1900 return $this->tableName( $name ) . ' ' . $this->addIdentifierQuotes( $alias );
1901 }
1902 }
1903
1904 /**
1905 * Gets an array of aliased table names
1906 *
1907 * @param array $tables [ [alias] => table ]
1908 * @return string[] See tableNameWithAlias()
1909 */
1910 public function tableNamesWithAlias( $tables ) {
1911 $retval = [];
1912 foreach ( $tables as $alias => $table ) {
1913 if ( is_numeric( $alias ) ) {
1914 $alias = $table;
1915 }
1916 $retval[] = $this->tableNameWithAlias( $table, $alias );
1917 }
1918
1919 return $retval;
1920 }
1921
1922 /**
1923 * Get an aliased field name
1924 * e.g. fieldName AS newFieldName
1925 *
1926 * @param string $name Field name
1927 * @param string|bool $alias Alias (optional)
1928 * @return string SQL name for aliased field. Will not alias a field to its own name
1929 */
1930 public function fieldNameWithAlias( $name, $alias = false ) {
1931 if ( !$alias || (string)$alias === (string)$name ) {
1932 return $name;
1933 } else {
1934 return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
1935 }
1936 }
1937
1938 /**
1939 * Gets an array of aliased field names
1940 *
1941 * @param array $fields [ [alias] => field ]
1942 * @return string[] See fieldNameWithAlias()
1943 */
1944 public function fieldNamesWithAlias( $fields ) {
1945 $retval = [];
1946 foreach ( $fields as $alias => $field ) {
1947 if ( is_numeric( $alias ) ) {
1948 $alias = $field;
1949 }
1950 $retval[] = $this->fieldNameWithAlias( $field, $alias );
1951 }
1952
1953 return $retval;
1954 }
1955
1956 /**
1957 * Get the aliased table name clause for a FROM clause
1958 * which might have a JOIN and/or USE INDEX clause
1959 *
1960 * @param array $tables ( [alias] => table )
1961 * @param array $use_index Same as for select()
1962 * @param array $join_conds Same as for select()
1963 * @return string
1964 */
1965 protected function tableNamesWithUseIndexOrJOIN(
1966 $tables, $use_index = [], $join_conds = []
1967 ) {
1968 $ret = [];
1969 $retJOIN = [];
1970 $use_index = (array)$use_index;
1971 $join_conds = (array)$join_conds;
1972
1973 foreach ( $tables as $alias => $table ) {
1974 if ( !is_string( $alias ) ) {
1975 // No alias? Set it equal to the table name
1976 $alias = $table;
1977 }
1978 // Is there a JOIN clause for this table?
1979 if ( isset( $join_conds[$alias] ) ) {
1980 list( $joinType, $conds ) = $join_conds[$alias];
1981 $tableClause = $joinType;
1982 $tableClause .= ' ' . $this->tableNameWithAlias( $table, $alias );
1983 if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
1984 $use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
1985 if ( $use != '' ) {
1986 $tableClause .= ' ' . $use;
1987 }
1988 }
1989 $on = $this->makeList( (array)$conds, LIST_AND );
1990 if ( $on != '' ) {
1991 $tableClause .= ' ON (' . $on . ')';
1992 }
1993
1994 $retJOIN[] = $tableClause;
1995 } elseif ( isset( $use_index[$alias] ) ) {
1996 // Is there an INDEX clause for this table?
1997 $tableClause = $this->tableNameWithAlias( $table, $alias );
1998 $tableClause .= ' ' . $this->useIndexClause(
1999 implode( ',', (array)$use_index[$alias] )
2000 );
2001
2002 $ret[] = $tableClause;
2003 } else {
2004 $tableClause = $this->tableNameWithAlias( $table, $alias );
2005
2006 $ret[] = $tableClause;
2007 }
2008 }
2009
2010 // We can't separate explicit JOIN clauses with ',', use ' ' for those
2011 $implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
2012 $explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
2013
2014 // Compile our final table clause
2015 return implode( ' ', [ $implicitJoins, $explicitJoins ] );
2016 }
2017
2018 /**
2019 * Get the name of an index in a given table.
2020 *
2021 * @param string $index
2022 * @return string
2023 */
2024 protected function indexName( $index ) {
2025 // Backwards-compatibility hack
2026 $renamed = [
2027 'ar_usertext_timestamp' => 'usertext_timestamp',
2028 'un_user_id' => 'user_id',
2029 'un_user_ip' => 'user_ip',
2030 ];
2031
2032 if ( isset( $renamed[$index] ) ) {
2033 return $renamed[$index];
2034 } else {
2035 return $index;
2036 }
2037 }
2038
2039 public function addQuotes( $s ) {
2040 if ( $s instanceof Blob ) {
2041 $s = $s->fetch();
2042 }
2043 if ( $s === null ) {
2044 return 'NULL';
2045 } else {
2046 # This will also quote numeric values. This should be harmless,
2047 # and protects against weird problems that occur when they really
2048 # _are_ strings such as article titles and string->number->string
2049 # conversion is not 1:1.
2050 return "'" . $this->strencode( $s ) . "'";
2051 }
2052 }
2053
2054 /**
2055 * Quotes an identifier using `backticks` or "double quotes" depending on the database type.
2056 * MySQL uses `backticks` while basically everything else uses double quotes.
2057 * Since MySQL is the odd one out here the double quotes are our generic
2058 * and we implement backticks in DatabaseMysql.
2059 *
2060 * @param string $s
2061 * @return string
2062 */
2063 public function addIdentifierQuotes( $s ) {
2064 return '"' . str_replace( '"', '""', $s ) . '"';
2065 }
2066
2067 /**
2068 * Returns if the given identifier looks quoted or not according to
2069 * the database convention for quoting identifiers .
2070 *
2071 * @note Do not use this to determine if untrusted input is safe.
2072 * A malicious user can trick this function.
2073 * @param string $name
2074 * @return bool
2075 */
2076 public function isQuotedIdentifier( $name ) {
2077 return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
2078 }
2079
2080 /**
2081 * @param string $s
2082 * @return string
2083 */
2084 protected function escapeLikeInternal( $s ) {
2085 return addcslashes( $s, '\%_' );
2086 }
2087
2088 public function buildLike() {
2089 $params = func_get_args();
2090
2091 if ( count( $params ) > 0 && is_array( $params[0] ) ) {
2092 $params = $params[0];
2093 }
2094
2095 $s = '';
2096
2097 foreach ( $params as $value ) {
2098 if ( $value instanceof LikeMatch ) {
2099 $s .= $value->toString();
2100 } else {
2101 $s .= $this->escapeLikeInternal( $value );
2102 }
2103 }
2104
2105 return " LIKE {$this->addQuotes( $s )} ";
2106 }
2107
2108 public function anyChar() {
2109 return new LikeMatch( '_' );
2110 }
2111
2112 public function anyString() {
2113 return new LikeMatch( '%' );
2114 }
2115
2116 public function nextSequenceValue( $seqName ) {
2117 return null;
2118 }
2119
2120 /**
2121 * USE INDEX clause. Unlikely to be useful for anything but MySQL. This
2122 * is only needed because a) MySQL must be as efficient as possible due to
2123 * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
2124 * which index to pick. Anyway, other databases might have different
2125 * indexes on a given table. So don't bother overriding this unless you're
2126 * MySQL.
2127 * @param string $index
2128 * @return string
2129 */
2130 public function useIndexClause( $index ) {
2131 return '';
2132 }
2133
2134 public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2135 $quotedTable = $this->tableName( $table );
2136
2137 if ( count( $rows ) == 0 ) {
2138 return;
2139 }
2140
2141 # Single row case
2142 if ( !is_array( reset( $rows ) ) ) {
2143 $rows = [ $rows ];
2144 }
2145
2146 // @FXIME: this is not atomic, but a trx would break affectedRows()
2147 foreach ( $rows as $row ) {
2148 # Delete rows which collide
2149 if ( $uniqueIndexes ) {
2150 $sql = "DELETE FROM $quotedTable WHERE ";
2151 $first = true;
2152 foreach ( $uniqueIndexes as $index ) {
2153 if ( $first ) {
2154 $first = false;
2155 $sql .= '( ';
2156 } else {
2157 $sql .= ' ) OR ( ';
2158 }
2159 if ( is_array( $index ) ) {
2160 $first2 = true;
2161 foreach ( $index as $col ) {
2162 if ( $first2 ) {
2163 $first2 = false;
2164 } else {
2165 $sql .= ' AND ';
2166 }
2167 $sql .= $col . '=' . $this->addQuotes( $row[$col] );
2168 }
2169 } else {
2170 $sql .= $index . '=' . $this->addQuotes( $row[$index] );
2171 }
2172 }
2173 $sql .= ' )';
2174 $this->query( $sql, $fname );
2175 }
2176
2177 # Now insert the row
2178 $this->insert( $table, $row, $fname );
2179 }
2180 }
2181
2182 /**
2183 * REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE
2184 * statement.
2185 *
2186 * @param string $table Table name
2187 * @param array|string $rows Row(s) to insert
2188 * @param string $fname Caller function name
2189 *
2190 * @return ResultWrapper
2191 */
2192 protected function nativeReplace( $table, $rows, $fname ) {
2193 $table = $this->tableName( $table );
2194
2195 # Single row case
2196 if ( !is_array( reset( $rows ) ) ) {
2197 $rows = [ $rows ];
2198 }
2199
2200 $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
2201 $first = true;
2202
2203 foreach ( $rows as $row ) {
2204 if ( $first ) {
2205 $first = false;
2206 } else {
2207 $sql .= ',';
2208 }
2209
2210 $sql .= '(' . $this->makeList( $row ) . ')';
2211 }
2212
2213 return $this->query( $sql, $fname );
2214 }
2215
2216 public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
2217 $fname = __METHOD__
2218 ) {
2219 if ( !count( $rows ) ) {
2220 return true; // nothing to do
2221 }
2222
2223 if ( !is_array( reset( $rows ) ) ) {
2224 $rows = [ $rows ];
2225 }
2226
2227 if ( count( $uniqueIndexes ) ) {
2228 $clauses = []; // list WHERE clauses that each identify a single row
2229 foreach ( $rows as $row ) {
2230 foreach ( $uniqueIndexes as $index ) {
2231 $index = is_array( $index ) ? $index : [ $index ]; // columns
2232 $rowKey = []; // unique key to this row
2233 foreach ( $index as $column ) {
2234 $rowKey[$column] = $row[$column];
2235 }
2236 $clauses[] = $this->makeList( $rowKey, LIST_AND );
2237 }
2238 }
2239 $where = [ $this->makeList( $clauses, LIST_OR ) ];
2240 } else {
2241 $where = false;
2242 }
2243
2244 $useTrx = !$this->mTrxLevel;
2245 if ( $useTrx ) {
2246 $this->begin( $fname, self::TRANSACTION_INTERNAL );
2247 }
2248 try {
2249 # Update any existing conflicting row(s)
2250 if ( $where !== false ) {
2251 $ok = $this->update( $table, $set, $where, $fname );
2252 } else {
2253 $ok = true;
2254 }
2255 # Now insert any non-conflicting row(s)
2256 $ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
2257 } catch ( Exception $e ) {
2258 if ( $useTrx ) {
2259 $this->rollback( $fname );
2260 }
2261 throw $e;
2262 }
2263 if ( $useTrx ) {
2264 $this->commit( $fname, self::TRANSACTION_INTERNAL );
2265 }
2266
2267 return $ok;
2268 }
2269
2270 public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2271 $fname = __METHOD__
2272 ) {
2273 if ( !$conds ) {
2274 throw new DBUnexpectedError( $this,
2275 'DatabaseBase::deleteJoin() called with empty $conds' );
2276 }
2277
2278 $delTable = $this->tableName( $delTable );
2279 $joinTable = $this->tableName( $joinTable );
2280 $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2281 if ( $conds != '*' ) {
2282 $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
2283 }
2284 $sql .= ')';
2285
2286 $this->query( $sql, $fname );
2287 }
2288
2289 /**
2290 * Returns the size of a text field, or -1 for "unlimited"
2291 *
2292 * @param string $table
2293 * @param string $field
2294 * @return int
2295 */
2296 public function textFieldSize( $table, $field ) {
2297 $table = $this->tableName( $table );
2298 $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
2299 $res = $this->query( $sql, 'DatabaseBase::textFieldSize' );
2300 $row = $this->fetchObject( $res );
2301
2302 $m = [];
2303
2304 if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
2305 $size = $m[1];
2306 } else {
2307 $size = -1;
2308 }
2309
2310 return $size;
2311 }
2312
2313 /**
2314 * A string to insert into queries to show that they're low-priority, like
2315 * MySQL's LOW_PRIORITY. If no such feature exists, return an empty
2316 * string and nothing bad should happen.
2317 *
2318 * @return string Returns the text of the low priority option if it is
2319 * supported, or a blank string otherwise
2320 */
2321 public function lowPriorityOption() {
2322 return '';
2323 }
2324
2325 public function delete( $table, $conds, $fname = __METHOD__ ) {
2326 if ( !$conds ) {
2327 throw new DBUnexpectedError( $this, 'DatabaseBase::delete() called with no conditions' );
2328 }
2329
2330 $table = $this->tableName( $table );
2331 $sql = "DELETE FROM $table";
2332
2333 if ( $conds != '*' ) {
2334 if ( is_array( $conds ) ) {
2335 $conds = $this->makeList( $conds, LIST_AND );
2336 }
2337 $sql .= ' WHERE ' . $conds;
2338 }
2339
2340 return $this->query( $sql, $fname );
2341 }
2342
2343 public function insertSelect( $destTable, $srcTable, $varMap, $conds,
2344 $fname = __METHOD__,
2345 $insertOptions = [], $selectOptions = []
2346 ) {
2347 $destTable = $this->tableName( $destTable );
2348
2349 if ( !is_array( $insertOptions ) ) {
2350 $insertOptions = [ $insertOptions ];
2351 }
2352
2353 $insertOptions = $this->makeInsertOptions( $insertOptions );
2354
2355 if ( !is_array( $selectOptions ) ) {
2356 $selectOptions = [ $selectOptions ];
2357 }
2358
2359 list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
2360
2361 if ( is_array( $srcTable ) ) {
2362 $srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
2363 } else {
2364 $srcTable = $this->tableName( $srcTable );
2365 }
2366
2367 $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
2368 " SELECT $startOpts " . implode( ',', $varMap ) .
2369 " FROM $srcTable $useIndex ";
2370
2371 if ( $conds != '*' ) {
2372 if ( is_array( $conds ) ) {
2373 $conds = $this->makeList( $conds, LIST_AND );
2374 }
2375 $sql .= " WHERE $conds";
2376 }
2377
2378 $sql .= " $tailOpts";
2379
2380 return $this->query( $sql, $fname );
2381 }
2382
2383 /**
2384 * Construct a LIMIT query with optional offset. This is used for query
2385 * pages. The SQL should be adjusted so that only the first $limit rows
2386 * are returned. If $offset is provided as well, then the first $offset
2387 * rows should be discarded, and the next $limit rows should be returned.
2388 * If the result of the query is not ordered, then the rows to be returned
2389 * are theoretically arbitrary.
2390 *
2391 * $sql is expected to be a SELECT, if that makes a difference.
2392 *
2393 * The version provided by default works in MySQL and SQLite. It will very
2394 * likely need to be overridden for most other DBMSes.
2395 *
2396 * @param string $sql SQL query we will append the limit too
2397 * @param int $limit The SQL limit
2398 * @param int|bool $offset The SQL offset (default false)
2399 * @throws DBUnexpectedError
2400 * @return string
2401 */
2402 public function limitResult( $sql, $limit, $offset = false ) {
2403 if ( !is_numeric( $limit ) ) {
2404 throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
2405 }
2406
2407 return "$sql LIMIT "
2408 . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
2409 . "{$limit} ";
2410 }
2411
2412 public function unionSupportsOrderAndLimit() {
2413 return true; // True for almost every DB supported
2414 }
2415
2416 public function unionQueries( $sqls, $all ) {
2417 $glue = $all ? ') UNION ALL (' : ') UNION (';
2418
2419 return '(' . implode( $glue, $sqls ) . ')';
2420 }
2421
2422 public function conditional( $cond, $trueVal, $falseVal ) {
2423 if ( is_array( $cond ) ) {
2424 $cond = $this->makeList( $cond, LIST_AND );
2425 }
2426
2427 return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
2428 }
2429
2430 public function strreplace( $orig, $old, $new ) {
2431 return "REPLACE({$orig}, {$old}, {$new})";
2432 }
2433
2434 public function getServerUptime() {
2435 return 0;
2436 }
2437
2438 public function wasDeadlock() {
2439 return false;
2440 }
2441
2442 public function wasLockTimeout() {
2443 return false;
2444 }
2445
2446 public function wasErrorReissuable() {
2447 return false;
2448 }
2449
2450 public function wasReadOnlyError() {
2451 return false;
2452 }
2453
2454 /**
2455 * Determines if the given query error was a connection drop
2456 * STUB
2457 *
2458 * @param integer|string $errno
2459 * @return bool
2460 */
2461 public function wasConnectionError( $errno ) {
2462 return false;
2463 }
2464
2465 /**
2466 * Perform a deadlock-prone transaction.
2467 *
2468 * This function invokes a callback function to perform a set of write
2469 * queries. If a deadlock occurs during the processing, the transaction
2470 * will be rolled back and the callback function will be called again.
2471 *
2472 * Avoid using this method outside of Job or Maintenance classes.
2473 *
2474 * Usage:
2475 * $dbw->deadlockLoop( callback, ... );
2476 *
2477 * Extra arguments are passed through to the specified callback function.
2478 * This method requires that no transactions are already active to avoid
2479 * causing premature commits or exceptions.
2480 *
2481 * Returns whatever the callback function returned on its successful,
2482 * iteration, or false on error, for example if the retry limit was
2483 * reached.
2484 *
2485 * @return mixed
2486 * @throws DBUnexpectedError
2487 * @throws Exception
2488 */
2489 public function deadlockLoop() {
2490 $args = func_get_args();
2491 $function = array_shift( $args );
2492 $tries = self::DEADLOCK_TRIES;
2493
2494 $this->begin( __METHOD__ );
2495
2496 $retVal = null;
2497 /** @var Exception $e */
2498 $e = null;
2499 do {
2500 try {
2501 $retVal = call_user_func_array( $function, $args );
2502 break;
2503 } catch ( DBQueryError $e ) {
2504 if ( $this->wasDeadlock() ) {
2505 // Retry after a randomized delay
2506 usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
2507 } else {
2508 // Throw the error back up
2509 throw $e;
2510 }
2511 }
2512 } while ( --$tries > 0 );
2513
2514 if ( $tries <= 0 ) {
2515 // Too many deadlocks; give up
2516 $this->rollback( __METHOD__ );
2517 throw $e;
2518 } else {
2519 $this->commit( __METHOD__ );
2520
2521 return $retVal;
2522 }
2523 }
2524
2525 public function masterPosWait( DBMasterPos $pos, $timeout ) {
2526 # Real waits are implemented in the subclass.
2527 return 0;
2528 }
2529
2530 public function getSlavePos() {
2531 # Stub
2532 return false;
2533 }
2534
2535 public function getMasterPos() {
2536 # Stub
2537 return false;
2538 }
2539
2540 public function serverIsReadOnly() {
2541 return false;
2542 }
2543
2544 final public function onTransactionResolution( callable $callback ) {
2545 if ( !$this->mTrxLevel ) {
2546 throw new DBUnexpectedError( $this, "No transaction is active." );
2547 }
2548 $this->mTrxEndCallbacks[] = [ $callback, wfGetCaller() ];
2549 }
2550
2551 final public function onTransactionIdle( callable $callback ) {
2552 $this->mTrxIdleCallbacks[] = [ $callback, wfGetCaller() ];
2553 if ( !$this->mTrxLevel ) {
2554 $this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
2555 }
2556 }
2557
2558 final public function onTransactionPreCommitOrIdle( callable $callback ) {
2559 if ( $this->mTrxLevel ) {
2560 $this->mTrxPreCommitCallbacks[] = [ $callback, wfGetCaller() ];
2561 } else {
2562 // If no transaction is active, then make one for this callback
2563 $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
2564 try {
2565 call_user_func( $callback );
2566 $this->commit( __METHOD__ );
2567 } catch ( Exception $e ) {
2568 $this->rollback( __METHOD__ );
2569 throw $e;
2570 }
2571 }
2572 }
2573
2574 /**
2575 * Whether to disable running of post-commit callbacks
2576 *
2577 * This method should not be used outside of Database/LoadBalancer
2578 *
2579 * @param bool $suppress
2580 * @since 1.28
2581 */
2582 final public function setPostCommitCallbackSupression( $suppress ) {
2583 $this->suppressPostCommitCallbacks = $suppress;
2584 }
2585
2586 /**
2587 * Actually run and consume any "on transaction idle/resolution" callbacks.
2588 *
2589 * This method should not be used outside of Database/LoadBalancer
2590 *
2591 * @param integer $trigger IDatabase::TRIGGER_* constant
2592 * @since 1.20
2593 * @throws Exception
2594 */
2595 public function runOnTransactionIdleCallbacks( $trigger ) {
2596 if ( $this->suppressPostCommitCallbacks ) {
2597 return;
2598 }
2599
2600 $autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled?
2601 /** @var Exception $e */
2602 $e = null; // first exception
2603 do { // callbacks may add callbacks :)
2604 $callbacks = array_merge(
2605 $this->mTrxIdleCallbacks,
2606 $this->mTrxEndCallbacks // include "transaction resolution" callbacks
2607 );
2608 $this->mTrxIdleCallbacks = []; // consumed (and recursion guard)
2609 $this->mTrxEndCallbacks = []; // consumed (recursion guard)
2610 foreach ( $callbacks as $callback ) {
2611 try {
2612 list( $phpCallback ) = $callback;
2613 $this->clearFlag( DBO_TRX ); // make each query its own transaction
2614 call_user_func_array( $phpCallback, [ $trigger ] );
2615 if ( $autoTrx ) {
2616 $this->setFlag( DBO_TRX ); // restore automatic begin()
2617 } else {
2618 $this->clearFlag( DBO_TRX ); // restore auto-commit
2619 }
2620 } catch ( Exception $ex ) {
2621 MWExceptionHandler::logException( $ex );
2622 $e = $e ?: $ex;
2623 // Some callbacks may use startAtomic/endAtomic, so make sure
2624 // their transactions are ended so other callbacks don't fail
2625 if ( $this->trxLevel() ) {
2626 $this->rollback( __METHOD__ );
2627 }
2628 }
2629 }
2630 } while ( count( $this->mTrxIdleCallbacks ) );
2631
2632 if ( $e instanceof Exception ) {
2633 throw $e; // re-throw any first exception
2634 }
2635 }
2636
2637 /**
2638 * Actually run and consume any "on transaction pre-commit" callbacks.
2639 *
2640 * This method should not be used outside of Database/LoadBalancer
2641 *
2642 * @since 1.22
2643 * @throws Exception
2644 */
2645 public function runOnTransactionPreCommitCallbacks() {
2646 $e = null; // first exception
2647 do { // callbacks may add callbacks :)
2648 $callbacks = $this->mTrxPreCommitCallbacks;
2649 $this->mTrxPreCommitCallbacks = []; // consumed (and recursion guard)
2650 foreach ( $callbacks as $callback ) {
2651 try {
2652 list( $phpCallback ) = $callback;
2653 call_user_func( $phpCallback );
2654 } catch ( Exception $ex ) {
2655 MWExceptionHandler::logException( $ex );
2656 $e = $e ?: $ex;
2657 }
2658 }
2659 } while ( count( $this->mTrxPreCommitCallbacks ) );
2660
2661 if ( $e instanceof Exception ) {
2662 throw $e; // re-throw any first exception
2663 }
2664 }
2665
2666 final public function startAtomic( $fname = __METHOD__ ) {
2667 if ( !$this->mTrxLevel ) {
2668 $this->begin( $fname, self::TRANSACTION_INTERNAL );
2669 $this->mTrxAutomatic = true;
2670 // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
2671 // in all changes being in one transaction to keep requests transactional.
2672 if ( !$this->getFlag( DBO_TRX ) ) {
2673 $this->mTrxAutomaticAtomic = true;
2674 }
2675 }
2676
2677 $this->mTrxAtomicLevels[] = $fname;
2678 }
2679
2680 final public function endAtomic( $fname = __METHOD__ ) {
2681 if ( !$this->mTrxLevel ) {
2682 throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." );
2683 }
2684 if ( !$this->mTrxAtomicLevels ||
2685 array_pop( $this->mTrxAtomicLevels ) !== $fname
2686 ) {
2687 throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." );
2688 }
2689
2690 if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
2691 $this->commit( $fname, self::FLUSHING_INTERNAL );
2692 }
2693 }
2694
2695 final public function doAtomicSection( $fname, callable $callback ) {
2696 $this->startAtomic( $fname );
2697 try {
2698 $res = call_user_func_array( $callback, [ $this, $fname ] );
2699 } catch ( Exception $e ) {
2700 $this->rollback( $fname );
2701 throw $e;
2702 }
2703 $this->endAtomic( $fname );
2704
2705 return $res;
2706 }
2707
2708 final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
2709 // Protect against mismatched atomic section, transaction nesting, and snapshot loss
2710 if ( $this->mTrxLevel ) {
2711 if ( $this->mTrxAtomicLevels ) {
2712 $levels = implode( ', ', $this->mTrxAtomicLevels );
2713 $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
2714 throw new DBUnexpectedError( $this, $msg );
2715 } elseif ( !$this->mTrxAutomatic ) {
2716 $msg = "$fname: Explicit transaction already active (from {$this->mTrxFname}).";
2717 throw new DBUnexpectedError( $this, $msg );
2718 } else {
2719 // @TODO: make this an exception at some point
2720 $msg = "$fname: Implicit transaction already active (from {$this->mTrxFname}).";
2721 wfLogDBError( $msg );
2722 return; // join the main transaction set
2723 }
2724 } elseif ( $this->getFlag( DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
2725 // @TODO: make this an exception at some point
2726 wfLogDBError( "$fname: Implicit transaction expected (DBO_TRX set)." );
2727 return; // let any writes be in the main transaction
2728 }
2729
2730 // Avoid fatals if close() was called
2731 $this->assertOpen();
2732
2733 $this->doBegin( $fname );
2734 $this->mTrxTimestamp = microtime( true );
2735 $this->mTrxFname = $fname;
2736 $this->mTrxDoneWrites = false;
2737 $this->mTrxAutomatic = false;
2738 $this->mTrxAutomaticAtomic = false;
2739 $this->mTrxAtomicLevels = [];
2740 $this->mTrxShortId = wfRandomString( 12 );
2741 $this->mTrxWriteDuration = 0.0;
2742 $this->mTrxWriteCallers = [];
2743 // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
2744 // Get an estimate of the slave lag before then, treating estimate staleness
2745 // as lag itself just to be safe
2746 $status = $this->getApproximateLagStatus();
2747 $this->mTrxSlaveLag = $status['lag'] + ( microtime( true ) - $status['since'] );
2748 }
2749
2750 /**
2751 * Issues the BEGIN command to the database server.
2752 *
2753 * @see DatabaseBase::begin()
2754 * @param string $fname
2755 */
2756 protected function doBegin( $fname ) {
2757 $this->query( 'BEGIN', $fname );
2758 $this->mTrxLevel = 1;
2759 }
2760
2761 final public function commit( $fname = __METHOD__, $flush = '' ) {
2762 if ( $this->mTrxLevel && $this->mTrxAtomicLevels ) {
2763 // There are still atomic sections open. This cannot be ignored
2764 $levels = implode( ', ', $this->mTrxAtomicLevels );
2765 throw new DBUnexpectedError(
2766 $this,
2767 "$fname: Got COMMIT while atomic sections $levels are still open."
2768 );
2769 }
2770
2771 if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
2772 if ( !$this->mTrxLevel ) {
2773 return; // nothing to do
2774 } elseif ( !$this->mTrxAutomatic ) {
2775 throw new DBUnexpectedError(
2776 $this,
2777 "$fname: Flushing an explicit transaction, getting out of sync."
2778 );
2779 }
2780 } else {
2781 if ( !$this->mTrxLevel ) {
2782 wfWarn( "$fname: No transaction to commit, something got out of sync." );
2783 return; // nothing to do
2784 } elseif ( $this->mTrxAutomatic ) {
2785 // @TODO: make this an exception at some point
2786 wfLogDBError( "$fname: Explicit commit of implicit transaction." );
2787 return; // wait for the main transaction set commit round
2788 }
2789 }
2790
2791 // Avoid fatals if close() was called
2792 $this->assertOpen();
2793
2794 $this->runOnTransactionPreCommitCallbacks();
2795 $writeTime = $this->pendingWriteQueryDuration();
2796 $this->doCommit( $fname );
2797 if ( $this->mTrxDoneWrites ) {
2798 $this->mDoneWrites = microtime( true );
2799 $this->getTransactionProfiler()->transactionWritingOut(
2800 $this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime );
2801 }
2802
2803 $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
2804 }
2805
2806 /**
2807 * Issues the COMMIT command to the database server.
2808 *
2809 * @see DatabaseBase::commit()
2810 * @param string $fname
2811 */
2812 protected function doCommit( $fname ) {
2813 if ( $this->mTrxLevel ) {
2814 $this->query( 'COMMIT', $fname );
2815 $this->mTrxLevel = 0;
2816 }
2817 }
2818
2819 final public function rollback( $fname = __METHOD__, $flush = '' ) {
2820 if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
2821 if ( !$this->mTrxLevel ) {
2822 return; // nothing to do
2823 }
2824 } else {
2825 if ( !$this->mTrxLevel ) {
2826 wfWarn( "$fname: No transaction to rollback, something got out of sync." );
2827 return; // nothing to do
2828 } elseif ( $this->getFlag( DBO_TRX ) ) {
2829 throw new DBUnexpectedError(
2830 $this,
2831 "$fname: Expected mass rollback of all peer databases (DBO_TRX set)."
2832 );
2833 }
2834 }
2835
2836 // Avoid fatals if close() was called
2837 $this->assertOpen();
2838
2839 $this->doRollback( $fname );
2840 $this->mTrxAtomicLevels = [];
2841 if ( $this->mTrxDoneWrites ) {
2842 $this->getTransactionProfiler()->transactionWritingOut(
2843 $this->mServer, $this->mDBname, $this->mTrxShortId );
2844 }
2845
2846 $this->mTrxIdleCallbacks = []; // clear
2847 $this->mTrxPreCommitCallbacks = []; // clear
2848 $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
2849 }
2850
2851 /**
2852 * Issues the ROLLBACK command to the database server.
2853 *
2854 * @see DatabaseBase::rollback()
2855 * @param string $fname
2856 */
2857 protected function doRollback( $fname ) {
2858 if ( $this->mTrxLevel ) {
2859 # Disconnects cause rollback anyway, so ignore those errors
2860 $ignoreErrors = true;
2861 $this->query( 'ROLLBACK', $fname, $ignoreErrors );
2862 $this->mTrxLevel = 0;
2863 }
2864 }
2865
2866 public function explicitTrxActive() {
2867 return $this->mTrxLevel && ( $this->mTrxAtomicLevels || !$this->mTrxAutomatic );
2868 }
2869
2870 /**
2871 * Creates a new table with structure copied from existing table
2872 * Note that unlike most database abstraction functions, this function does not
2873 * automatically append database prefix, because it works at a lower
2874 * abstraction level.
2875 * The table names passed to this function shall not be quoted (this
2876 * function calls addIdentifierQuotes when needed).
2877 *
2878 * @param string $oldName Name of table whose structure should be copied
2879 * @param string $newName Name of table to be created
2880 * @param bool $temporary Whether the new table should be temporary
2881 * @param string $fname Calling function name
2882 * @throws MWException
2883 * @return bool True if operation was successful
2884 */
2885 public function duplicateTableStructure( $oldName, $newName, $temporary = false,
2886 $fname = __METHOD__
2887 ) {
2888 throw new MWException(
2889 'DatabaseBase::duplicateTableStructure is not implemented in descendant class' );
2890 }
2891
2892 function listTables( $prefix = null, $fname = __METHOD__ ) {
2893 throw new MWException( 'DatabaseBase::listTables is not implemented in descendant class' );
2894 }
2895
2896 /**
2897 * Reset the views process cache set by listViews()
2898 * @since 1.22
2899 */
2900 final public function clearViewsCache() {
2901 $this->allViews = null;
2902 }
2903
2904 /**
2905 * Lists all the VIEWs in the database
2906 *
2907 * For caching purposes the list of all views should be stored in
2908 * $this->allViews. The process cache can be cleared with clearViewsCache()
2909 *
2910 * @param string $prefix Only show VIEWs with this prefix, eg. unit_test_
2911 * @param string $fname Name of calling function
2912 * @throws MWException
2913 * @return array
2914 * @since 1.22
2915 */
2916 public function listViews( $prefix = null, $fname = __METHOD__ ) {
2917 throw new MWException( 'DatabaseBase::listViews is not implemented in descendant class' );
2918 }
2919
2920 /**
2921 * Differentiates between a TABLE and a VIEW
2922 *
2923 * @param string $name Name of the database-structure to test.
2924 * @throws MWException
2925 * @return bool
2926 * @since 1.22
2927 */
2928 public function isView( $name ) {
2929 throw new MWException( 'DatabaseBase::isView is not implemented in descendant class' );
2930 }
2931
2932 public function timestamp( $ts = 0 ) {
2933 return wfTimestamp( TS_MW, $ts );
2934 }
2935
2936 public function timestampOrNull( $ts = null ) {
2937 if ( is_null( $ts ) ) {
2938 return null;
2939 } else {
2940 return $this->timestamp( $ts );
2941 }
2942 }
2943
2944 /**
2945 * Take the result from a query, and wrap it in a ResultWrapper if
2946 * necessary. Boolean values are passed through as is, to indicate success
2947 * of write queries or failure.
2948 *
2949 * Once upon a time, DatabaseBase::query() returned a bare MySQL result
2950 * resource, and it was necessary to call this function to convert it to
2951 * a wrapper. Nowadays, raw database objects are never exposed to external
2952 * callers, so this is unnecessary in external code.
2953 *
2954 * @param bool|ResultWrapper|resource|object $result
2955 * @return bool|ResultWrapper
2956 */
2957 protected function resultObject( $result ) {
2958 if ( !$result ) {
2959 return false;
2960 } elseif ( $result instanceof ResultWrapper ) {
2961 return $result;
2962 } elseif ( $result === true ) {
2963 // Successful write query
2964 return $result;
2965 } else {
2966 return new ResultWrapper( $this, $result );
2967 }
2968 }
2969
2970 public function ping() {
2971 if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
2972 return true;
2973 }
2974
2975 $ignoreErrors = true;
2976 $this->clearFlag( DBO_TRX, self::REMEMBER_PRIOR );
2977 // This will reconnect if possible or return false if not
2978 $ok = (bool)$this->query( "SELECT 1 AS ping", __METHOD__, $ignoreErrors );
2979 $this->restoreFlags( self::RESTORE_PRIOR );
2980
2981 return $ok;
2982 }
2983
2984 /**
2985 * @return bool
2986 */
2987 protected function reconnect() {
2988 $this->closeConnection();
2989 $this->mOpened = false;
2990 $this->mConn = false;
2991 try {
2992 $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
2993 $this->lastPing = microtime( true );
2994 $ok = true;
2995 } catch ( DBConnectionError $e ) {
2996 $ok = false;
2997 }
2998
2999 return $ok;
3000 }
3001
3002 public function getSessionLagStatus() {
3003 return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
3004 }
3005
3006 /**
3007 * Get the slave lag when the current transaction started
3008 *
3009 * This is useful when transactions might use snapshot isolation
3010 * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
3011 * is this lag plus transaction duration. If they don't, it is still
3012 * safe to be pessimistic. This returns null if there is no transaction.
3013 *
3014 * @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
3015 * @since 1.27
3016 */
3017 public function getTransactionLagStatus() {
3018 return $this->mTrxLevel
3019 ? [ 'lag' => $this->mTrxSlaveLag, 'since' => $this->trxTimestamp() ]
3020 : null;
3021 }
3022
3023 /**
3024 * Get a slave lag estimate for this server
3025 *
3026 * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of estimate)
3027 * @since 1.27
3028 */
3029 public function getApproximateLagStatus() {
3030 return [
3031 'lag' => $this->getLBInfo( 'slave' ) ? $this->getLag() : 0,
3032 'since' => microtime( true )
3033 ];
3034 }
3035
3036 /**
3037 * Merge the result of getSessionLagStatus() for several DBs
3038 * using the most pessimistic values to estimate the lag of
3039 * any data derived from them in combination
3040 *
3041 * This is information is useful for caching modules
3042 *
3043 * @see WANObjectCache::set()
3044 * @see WANObjectCache::getWithSetCallback()
3045 *
3046 * @param IDatabase $db1
3047 * @param IDatabase ...
3048 * @return array Map of values:
3049 * - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
3050 * - since: oldest UNIX timestamp of any of the DB lag estimates
3051 * - pending: whether any of the DBs have uncommitted changes
3052 * @since 1.27
3053 */
3054 public static function getCacheSetOptions( IDatabase $db1 ) {
3055 $res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
3056 foreach ( func_get_args() as $db ) {
3057 /** @var IDatabase $db */
3058 $status = $db->getSessionLagStatus();
3059 if ( $status['lag'] === false ) {
3060 $res['lag'] = false;
3061 } elseif ( $res['lag'] !== false ) {
3062 $res['lag'] = max( $res['lag'], $status['lag'] );
3063 }
3064 $res['since'] = min( $res['since'], $status['since'] );
3065 $res['pending'] = $res['pending'] ?: $db->writesPending();
3066 }
3067
3068 return $res;
3069 }
3070
3071 public function getLag() {
3072 return 0;
3073 }
3074
3075 function maxListLen() {
3076 return 0;
3077 }
3078
3079 public function encodeBlob( $b ) {
3080 return $b;
3081 }
3082
3083 public function decodeBlob( $b ) {
3084 if ( $b instanceof Blob ) {
3085 $b = $b->fetch();
3086 }
3087 return $b;
3088 }
3089
3090 public function setSessionOptions( array $options ) {
3091 }
3092
3093 /**
3094 * Read and execute SQL commands from a file.
3095 *
3096 * Returns true on success, error string or exception on failure (depending
3097 * on object's error ignore settings).
3098 *
3099 * @param string $filename File name to open
3100 * @param bool|callable $lineCallback Optional function called before reading each line
3101 * @param bool|callable $resultCallback Optional function called for each MySQL result
3102 * @param bool|string $fname Calling function name or false if name should be
3103 * generated dynamically using $filename
3104 * @param bool|callable $inputCallback Optional function called for each
3105 * complete line sent
3106 * @throws Exception|MWException
3107 * @return bool|string
3108 */
3109 public function sourceFile(
3110 $filename, $lineCallback = false, $resultCallback = false, $fname = false, $inputCallback = false
3111 ) {
3112 MediaWiki\suppressWarnings();
3113 $fp = fopen( $filename, 'r' );
3114 MediaWiki\restoreWarnings();
3115
3116 if ( false === $fp ) {
3117 throw new MWException( "Could not open \"{$filename}\".\n" );
3118 }
3119
3120 if ( !$fname ) {
3121 $fname = __METHOD__ . "( $filename )";
3122 }
3123
3124 try {
3125 $error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
3126 } catch ( Exception $e ) {
3127 fclose( $fp );
3128 throw $e;
3129 }
3130
3131 fclose( $fp );
3132
3133 return $error;
3134 }
3135
3136 /**
3137 * Get the full path of a patch file. Originally based on archive()
3138 * from updaters.inc. Keep in mind this always returns a patch, as
3139 * it fails back to MySQL if no DB-specific patch can be found
3140 *
3141 * @param string $patch The name of the patch, like patch-something.sql
3142 * @return string Full path to patch file
3143 */
3144 public function patchPath( $patch ) {
3145 global $IP;
3146
3147 $dbType = $this->getType();
3148 if ( file_exists( "$IP/maintenance/$dbType/archives/$patch" ) ) {
3149 return "$IP/maintenance/$dbType/archives/$patch";
3150 } else {
3151 return "$IP/maintenance/archives/$patch";
3152 }
3153 }
3154
3155 public function setSchemaVars( $vars ) {
3156 $this->mSchemaVars = $vars;
3157 }
3158
3159 /**
3160 * Read and execute commands from an open file handle.
3161 *
3162 * Returns true on success, error string or exception on failure (depending
3163 * on object's error ignore settings).
3164 *
3165 * @param resource $fp File handle
3166 * @param bool|callable $lineCallback Optional function called before reading each query
3167 * @param bool|callable $resultCallback Optional function called for each MySQL result
3168 * @param string $fname Calling function name
3169 * @param bool|callable $inputCallback Optional function called for each complete query sent
3170 * @return bool|string
3171 */
3172 public function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
3173 $fname = __METHOD__, $inputCallback = false
3174 ) {
3175 $cmd = '';
3176
3177 while ( !feof( $fp ) ) {
3178 if ( $lineCallback ) {
3179 call_user_func( $lineCallback );
3180 }
3181
3182 $line = trim( fgets( $fp ) );
3183
3184 if ( $line == '' ) {
3185 continue;
3186 }
3187
3188 if ( '-' == $line[0] && '-' == $line[1] ) {
3189 continue;
3190 }
3191
3192 if ( $cmd != '' ) {
3193 $cmd .= ' ';
3194 }
3195
3196 $done = $this->streamStatementEnd( $cmd, $line );
3197
3198 $cmd .= "$line\n";
3199
3200 if ( $done || feof( $fp ) ) {
3201 $cmd = $this->replaceVars( $cmd );
3202
3203 if ( ( $inputCallback && call_user_func( $inputCallback, $cmd ) ) || !$inputCallback ) {
3204 $res = $this->query( $cmd, $fname );
3205
3206 if ( $resultCallback ) {
3207 call_user_func( $resultCallback, $res, $this );
3208 }
3209
3210 if ( false === $res ) {
3211 $err = $this->lastError();
3212
3213 return "Query \"{$cmd}\" failed with error code \"$err\".\n";
3214 }
3215 }
3216 $cmd = '';
3217 }
3218 }
3219
3220 return true;
3221 }
3222
3223 /**
3224 * Called by sourceStream() to check if we've reached a statement end
3225 *
3226 * @param string $sql SQL assembled so far
3227 * @param string $newLine New line about to be added to $sql
3228 * @return bool Whether $newLine contains end of the statement
3229 */
3230 public function streamStatementEnd( &$sql, &$newLine ) {
3231 if ( $this->delimiter ) {
3232 $prev = $newLine;
3233 $newLine = preg_replace( '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
3234 if ( $newLine != $prev ) {
3235 return true;
3236 }
3237 }
3238
3239 return false;
3240 }
3241
3242 /**
3243 * Database independent variable replacement. Replaces a set of variables
3244 * in an SQL statement with their contents as given by $this->getSchemaVars().
3245 *
3246 * Supports '{$var}' `{$var}` and / *$var* / (without the spaces) style variables.
3247 *
3248 * - '{$var}' should be used for text and is passed through the database's
3249 * addQuotes method.
3250 * - `{$var}` should be used for identifiers (e.g. table and database names).
3251 * It is passed through the database's addIdentifierQuotes method which
3252 * can be overridden if the database uses something other than backticks.
3253 * - / *_* / or / *$wgDBprefix* / passes the name that follows through the
3254 * database's tableName method.
3255 * - / *i* / passes the name that follows through the database's indexName method.
3256 * - In all other cases, / *$var* / is left unencoded. Except for table options,
3257 * its use should be avoided. In 1.24 and older, string encoding was applied.
3258 *
3259 * @param string $ins SQL statement to replace variables in
3260 * @return string The new SQL statement with variables replaced
3261 */
3262 protected function replaceVars( $ins ) {
3263 $vars = $this->getSchemaVars();
3264 return preg_replace_callback(
3265 '!
3266 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
3267 \'\{\$ (\w+) }\' | # 3. addQuotes
3268 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
3269 /\*\$ (\w+) \*/ # 5. leave unencoded
3270 !x',
3271 function ( $m ) use ( $vars ) {
3272 // Note: Because of <https://bugs.php.net/bug.php?id=51881>,
3273 // check for both nonexistent keys *and* the empty string.
3274 if ( isset( $m[1] ) && $m[1] !== '' ) {
3275 if ( $m[1] === 'i' ) {
3276 return $this->indexName( $m[2] );
3277 } else {
3278 return $this->tableName( $m[2] );
3279 }
3280 } elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
3281 return $this->addQuotes( $vars[$m[3]] );
3282 } elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
3283 return $this->addIdentifierQuotes( $vars[$m[4]] );
3284 } elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
3285 return $vars[$m[5]];
3286 } else {
3287 return $m[0];
3288 }
3289 },
3290 $ins
3291 );
3292 }
3293
3294 /**
3295 * Get schema variables. If none have been set via setSchemaVars(), then
3296 * use some defaults from the current object.
3297 *
3298 * @return array
3299 */
3300 protected function getSchemaVars() {
3301 if ( $this->mSchemaVars ) {
3302 return $this->mSchemaVars;
3303 } else {
3304 return $this->getDefaultSchemaVars();
3305 }
3306 }
3307
3308 /**
3309 * Get schema variables to use if none have been set via setSchemaVars().
3310 *
3311 * Override this in derived classes to provide variables for tables.sql
3312 * and SQL patch files.
3313 *
3314 * @return array
3315 */
3316 protected function getDefaultSchemaVars() {
3317 return [];
3318 }
3319
3320 public function lockIsFree( $lockName, $method ) {
3321 return true;
3322 }
3323
3324 public function lock( $lockName, $method, $timeout = 5 ) {
3325 $this->mNamedLocksHeld[$lockName] = 1;
3326
3327 return true;
3328 }
3329
3330 public function unlock( $lockName, $method ) {
3331 unset( $this->mNamedLocksHeld[$lockName] );
3332
3333 return true;
3334 }
3335
3336 public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
3337 if ( $this->writesOrCallbacksPending() ) {
3338 // This only flushes transactions to clear snapshots, not to write data
3339 throw new DBUnexpectedError(
3340 $this,
3341 "$fname: Cannot COMMIT to clear snapshot because writes are pending."
3342 );
3343 }
3344
3345 if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
3346 return null;
3347 }
3348
3349 $unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
3350 if ( $this->trxLevel() ) {
3351 // There is a good chance an exception was thrown, causing any early return
3352 // from the caller. Let any error handler get a chance to issue rollback().
3353 // If there isn't one, let the error bubble up and trigger server-side rollback.
3354 $this->onTransactionResolution( function () use ( $lockKey, $fname ) {
3355 $this->unlock( $lockKey, $fname );
3356 } );
3357 } else {
3358 $this->unlock( $lockKey, $fname );
3359 }
3360 } );
3361
3362 $this->commit( __METHOD__, self::FLUSHING_INTERNAL );
3363
3364 return $unlocker;
3365 }
3366
3367 public function namedLocksEnqueue() {
3368 return false;
3369 }
3370
3371 /**
3372 * Lock specific tables
3373 *
3374 * @param array $read Array of tables to lock for read access
3375 * @param array $write Array of tables to lock for write access
3376 * @param string $method Name of caller
3377 * @param bool $lowPriority Whether to indicate writes to be LOW PRIORITY
3378 * @return bool
3379 */
3380 public function lockTables( $read, $write, $method, $lowPriority = true ) {
3381 return true;
3382 }
3383
3384 /**
3385 * Unlock specific tables
3386 *
3387 * @param string $method The caller
3388 * @return bool
3389 */
3390 public function unlockTables( $method ) {
3391 return true;
3392 }
3393
3394 /**
3395 * Delete a table
3396 * @param string $tableName
3397 * @param string $fName
3398 * @return bool|ResultWrapper
3399 * @since 1.18
3400 */
3401 public function dropTable( $tableName, $fName = __METHOD__ ) {
3402 if ( !$this->tableExists( $tableName, $fName ) ) {
3403 return false;
3404 }
3405 $sql = "DROP TABLE " . $this->tableName( $tableName );
3406 if ( $this->cascadingDeletes() ) {
3407 $sql .= " CASCADE";
3408 }
3409
3410 return $this->query( $sql, $fName );
3411 }
3412
3413 /**
3414 * Get search engine class. All subclasses of this need to implement this
3415 * if they wish to use searching.
3416 *
3417 * @return string
3418 */
3419 public function getSearchEngine() {
3420 return 'SearchEngineDummy';
3421 }
3422
3423 public function getInfinity() {
3424 return 'infinity';
3425 }
3426
3427 public function encodeExpiry( $expiry ) {
3428 return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3429 ? $this->getInfinity()
3430 : $this->timestamp( $expiry );
3431 }
3432
3433 public function decodeExpiry( $expiry, $format = TS_MW ) {
3434 return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3435 ? 'infinity'
3436 : wfTimestamp( $format, $expiry );
3437 }
3438
3439 public function setBigSelects( $value = true ) {
3440 // no-op
3441 }
3442
3443 public function isReadOnly() {
3444 return ( $this->getReadOnlyReason() !== false );
3445 }
3446
3447 /**
3448 * @return string|bool Reason this DB is read-only or false if it is not
3449 */
3450 protected function getReadOnlyReason() {
3451 $reason = $this->getLBInfo( 'readOnlyReason' );
3452
3453 return is_string( $reason ) ? $reason : false;
3454 }
3455
3456 /**
3457 * @since 1.19
3458 * @return string
3459 */
3460 public function __toString() {
3461 return (string)$this->mConn;
3462 }
3463
3464 /**
3465 * Run a few simple sanity checks
3466 */
3467 public function __destruct() {
3468 if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
3469 trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
3470 }
3471 $danglingCallbacks = array_merge(
3472 $this->mTrxIdleCallbacks,
3473 $this->mTrxPreCommitCallbacks,
3474 $this->mTrxEndCallbacks
3475 );
3476 if ( $danglingCallbacks ) {
3477 $callers = [];
3478 foreach ( $danglingCallbacks as $callbackInfo ) {
3479 $callers[] = $callbackInfo[1];
3480 }
3481 $callers = implode( ', ', $callers );
3482 trigger_error( "DB transaction callbacks still pending (from $callers)." );
3483 }
3484 }
3485 }
3486
3487 /**
3488 * @since 1.27
3489 */
3490 abstract class Database extends DatabaseBase {
3491 // B/C until nothing type hints for DatabaseBase
3492 // @TODO: finish renaming DatabaseBase => Database
3493 }