sql.php 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. <?php
  2. /**
  3. * Send SQL queries from the specified file to the database, performing
  4. * variable replacement along the way.
  5. *
  6. * This program is free software; you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation; either version 2 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License along
  17. * with this program; if not, write to the Free Software Foundation, Inc.,
  18. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. * http://www.gnu.org/copyleft/gpl.html
  20. *
  21. * @file
  22. * @ingroup Maintenance
  23. */
  24. require_once __DIR__ . '/Maintenance.php';
  25. use MediaWiki\MediaWikiServices;
  26. use Wikimedia\Rdbms\ResultWrapper;
  27. use Wikimedia\Rdbms\IDatabase;
  28. use Wikimedia\Rdbms\DBQueryError;
  29. /**
  30. * Maintenance script that sends SQL queries from the specified file to the database.
  31. *
  32. * @ingroup Maintenance
  33. */
  34. class MwSql extends Maintenance {
  35. public function __construct() {
  36. parent::__construct();
  37. $this->addDescription( 'Send SQL queries to a MediaWiki database. ' .
  38. 'Takes a file name containing SQL as argument or runs interactively.' );
  39. $this->addOption( 'query',
  40. 'Run a single query instead of running interactively', false, true );
  41. $this->addOption( 'json', 'Output the results as JSON instead of PHP objects' );
  42. $this->addOption( 'cluster', 'Use an external cluster by name', false, true );
  43. $this->addOption( 'wikidb',
  44. 'The database wiki ID to use if not the current one', false, true );
  45. $this->addOption( 'replicadb',
  46. 'Replica DB server to use instead of the master DB (can be "any")', false, true );
  47. }
  48. public function execute() {
  49. global $IP;
  50. // We wan't to allow "" for the wikidb, meaning don't call select_db()
  51. $wiki = $this->hasOption( 'wikidb' ) ? $this->getOption( 'wikidb' ) : false;
  52. // Get the appropriate load balancer (for this wiki)
  53. $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
  54. if ( $this->hasOption( 'cluster' ) ) {
  55. $lb = $lbFactory->getExternalLB( $this->getOption( 'cluster' ) );
  56. } else {
  57. $lb = $lbFactory->getMainLB( $wiki );
  58. }
  59. // Figure out which server to use
  60. $replicaDB = $this->getOption( 'replicadb', $this->getOption( 'slave', '' ) );
  61. if ( $replicaDB === 'any' ) {
  62. $index = DB_REPLICA;
  63. } elseif ( $replicaDB != '' ) {
  64. $index = null;
  65. $serverCount = $lb->getServerCount();
  66. for ( $i = 0; $i < $serverCount; ++$i ) {
  67. if ( $lb->getServerName( $i ) === $replicaDB ) {
  68. $index = $i;
  69. break;
  70. }
  71. }
  72. if ( $index === null ) {
  73. $this->fatalError( "No replica DB server configured with the name '$replicaDB'." );
  74. }
  75. } else {
  76. $index = DB_MASTER;
  77. }
  78. /** @var IDatabase $db DB handle for the appropriate cluster/wiki */
  79. $db = $lb->getConnection( $index, [], $wiki );
  80. if ( $replicaDB != '' && $db->getLBInfo( 'master' ) !== null ) {
  81. $this->fatalError( "The server selected ({$db->getServer()}) is not a replica DB." );
  82. }
  83. if ( $index === DB_MASTER ) {
  84. $updater = DatabaseUpdater::newForDB( $db, true, $this );
  85. $db->setSchemaVars( $updater->getSchemaVars() );
  86. }
  87. if ( $this->hasArg( 0 ) ) {
  88. $file = fopen( $this->getArg( 0 ), 'r' );
  89. if ( !$file ) {
  90. $this->fatalError( "Unable to open input file" );
  91. }
  92. $error = $db->sourceStream( $file, null, [ $this, 'sqlPrintResult' ] );
  93. if ( $error !== true ) {
  94. $this->fatalError( $error );
  95. } else {
  96. exit( 0 );
  97. }
  98. }
  99. if ( $this->hasOption( 'query' ) ) {
  100. $query = $this->getOption( 'query' );
  101. $this->sqlDoQuery( $db, $query, /* dieOnError */ true );
  102. wfWaitForSlaves();
  103. return;
  104. }
  105. if (
  106. function_exists( 'readline_add_history' ) &&
  107. Maintenance::posix_isatty( 0 /*STDIN*/ )
  108. ) {
  109. $historyFile = isset( $_ENV['HOME'] ) ?
  110. "{$_ENV['HOME']}/.mwsql_history" : "$IP/maintenance/.mwsql_history";
  111. readline_read_history( $historyFile );
  112. } else {
  113. $historyFile = null;
  114. }
  115. $wholeLine = '';
  116. $newPrompt = '> ';
  117. $prompt = $newPrompt;
  118. $doDie = !Maintenance::posix_isatty( 0 );
  119. while ( ( $line = Maintenance::readconsole( $prompt ) ) !== false ) {
  120. if ( !$line ) {
  121. # User simply pressed return key
  122. continue;
  123. }
  124. $done = $db->streamStatementEnd( $wholeLine, $line );
  125. $wholeLine .= $line;
  126. if ( !$done ) {
  127. $wholeLine .= ' ';
  128. $prompt = ' -> ';
  129. continue;
  130. }
  131. if ( $historyFile ) {
  132. # Delimiter is eated by streamStatementEnd, we add it
  133. # up in the history (T39020)
  134. readline_add_history( $wholeLine . ';' );
  135. readline_write_history( $historyFile );
  136. }
  137. $this->sqlDoQuery( $db, $wholeLine, $doDie );
  138. $prompt = $newPrompt;
  139. $wholeLine = '';
  140. }
  141. wfWaitForSlaves();
  142. }
  143. protected function sqlDoQuery( IDatabase $db, $line, $dieOnError ) {
  144. try {
  145. $res = $db->query( $line );
  146. $this->sqlPrintResult( $res, $db );
  147. } catch ( DBQueryError $e ) {
  148. if ( $dieOnError ) {
  149. $this->fatalError( $e );
  150. } else {
  151. $this->error( $e );
  152. }
  153. }
  154. }
  155. /**
  156. * Print the results, callback for $db->sourceStream()
  157. * @param ResultWrapper|bool $res
  158. * @param IDatabase $db
  159. */
  160. public function sqlPrintResult( $res, $db ) {
  161. if ( !$res ) {
  162. // Do nothing
  163. return;
  164. } elseif ( is_object( $res ) && $res->numRows() ) {
  165. $out = '';
  166. foreach ( $res as $row ) {
  167. $out .= print_r( $row, true );
  168. $rows[] = $row;
  169. }
  170. if ( $this->hasOption( 'json' ) ) {
  171. $out = json_encode( $rows, JSON_PRETTY_PRINT );
  172. }
  173. $this->output( $out . "\n" );
  174. } else {
  175. $affected = $db->affectedRows();
  176. $this->output( "Query OK, $affected row(s) affected\n" );
  177. }
  178. }
  179. /**
  180. * @return int DB_TYPE constant
  181. */
  182. public function getDbType() {
  183. return Maintenance::DB_ADMIN;
  184. }
  185. }
  186. $maintClass = MwSql::class;
  187. require_once RUN_MAINTENANCE_IF_MAIN;