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