MigrateCommand.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. <?php
  2. /**
  3. * MigrateCommand class file.
  4. *
  5. * @author Qiang Xue <qiang.xue@gmail.com>
  6. * @link http://www.yiiframework.com/
  7. * @copyright 2008-2013 Yii Software LLC
  8. * @license http://www.yiiframework.com/license/
  9. */
  10. /**
  11. * MigrateCommand manages the database migrations.
  12. *
  13. * The implementation of this command and other supporting classes referenced
  14. * the yii-dbmigrations extension ((https://github.com/pieterclaerhout/yii-dbmigrations),
  15. * authored by Pieter Claerhout.
  16. *
  17. * Since version 1.1.11 this command will exit with the following exit codes:
  18. * <ul>
  19. * <li>0 on success</li>
  20. * <li>1 on general error</li>
  21. * <li>2 on failed migration.</li>
  22. * </ul>
  23. *
  24. * @author Qiang Xue <qiang.xue@gmail.com>
  25. * @package system.cli.commands
  26. * @since 1.1.6
  27. */
  28. class MigrateCommand extends CConsoleCommand
  29. {
  30. const BASE_MIGRATION='m000000_000000_base';
  31. /**
  32. * @var string the directory that stores the migrations. This must be specified
  33. * in terms of a path alias, and the corresponding directory must exist.
  34. * Defaults to 'application.migrations' (meaning 'protected/migrations').
  35. */
  36. public $migrationPath='application.migrations';
  37. /**
  38. * @var string the name of the table for keeping applied migration information.
  39. * This table will be automatically created if not exists. Defaults to 'tbl_migration'.
  40. * The table structure is: (version varchar(180) primary key, apply_time integer)
  41. */
  42. public $migrationTable='tbl_migration';
  43. /**
  44. * @var string the application component ID that specifies the database connection for
  45. * storing migration information. Defaults to 'db'.
  46. */
  47. public $connectionID='db';
  48. /**
  49. * @var string the path of the template file for generating new migrations. This
  50. * must be specified in terms of a path alias (e.g. application.migrations.template).
  51. * If not set, an internal template will be used.
  52. */
  53. public $templateFile;
  54. /**
  55. * @var string the default command action. It defaults to 'up'.
  56. */
  57. public $defaultAction='up';
  58. /**
  59. * @var boolean whether to execute the migration in an interactive mode. Defaults to true.
  60. * Set this to false when performing migration in a cron job or background process.
  61. */
  62. public $interactive=true;
  63. public function beforeAction($action,$params)
  64. {
  65. $path=Yii::getPathOfAlias($this->migrationPath);
  66. if($path===false || !is_dir($path))
  67. {
  68. echo 'Error: The migration directory does not exist: '.$this->migrationPath."\n";
  69. exit(1);
  70. }
  71. $this->migrationPath=$path;
  72. $yiiVersion=Yii::getVersion();
  73. echo "\nYii Migration Tool v1.0 (based on Yii v{$yiiVersion})\n\n";
  74. return parent::beforeAction($action,$params);
  75. }
  76. public function actionUp($args)
  77. {
  78. if(($migrations=$this->getNewMigrations())===array())
  79. {
  80. echo "No new migration found. Your system is up-to-date.\n";
  81. return 0;
  82. }
  83. $total=count($migrations);
  84. $step=isset($args[0]) ? (int)$args[0] : 0;
  85. if($step>0)
  86. $migrations=array_slice($migrations,0,$step);
  87. $n=count($migrations);
  88. if($n===$total)
  89. echo "Total $n new ".($n===1 ? 'migration':'migrations')." to be applied:\n";
  90. else
  91. echo "Total $n out of $total new ".($total===1 ? 'migration':'migrations')." to be applied:\n";
  92. foreach($migrations as $migration)
  93. echo " $migration\n";
  94. echo "\n";
  95. if($this->confirm('Apply the above '.($n===1 ? 'migration':'migrations')."?"))
  96. {
  97. foreach($migrations as $migration)
  98. {
  99. if($this->migrateUp($migration)===false)
  100. {
  101. echo "\nMigration failed. All later migrations are canceled.\n";
  102. return 2;
  103. }
  104. }
  105. echo "\nMigrated up successfully.\n";
  106. }
  107. }
  108. public function actionDown($args)
  109. {
  110. $step=isset($args[0]) ? (int)$args[0] : 1;
  111. if($step<1)
  112. {
  113. echo "Error: The step parameter must be greater than 0.\n";
  114. return 1;
  115. }
  116. if(($migrations=$this->getMigrationHistory($step))===array())
  117. {
  118. echo "No migration has been done before.\n";
  119. return 0;
  120. }
  121. $migrations=array_keys($migrations);
  122. $n=count($migrations);
  123. echo "Total $n ".($n===1 ? 'migration':'migrations')." to be reverted:\n";
  124. foreach($migrations as $migration)
  125. echo " $migration\n";
  126. echo "\n";
  127. if($this->confirm('Revert the above '.($n===1 ? 'migration':'migrations')."?"))
  128. {
  129. foreach($migrations as $migration)
  130. {
  131. if($this->migrateDown($migration)===false)
  132. {
  133. echo "\nMigration failed. All later migrations are canceled.\n";
  134. return 2;
  135. }
  136. }
  137. echo "\nMigrated down successfully.\n";
  138. }
  139. }
  140. public function actionRedo($args)
  141. {
  142. $step=isset($args[0]) ? (int)$args[0] : 1;
  143. if($step<1)
  144. {
  145. echo "Error: The step parameter must be greater than 0.\n";
  146. return 1;
  147. }
  148. if(($migrations=$this->getMigrationHistory($step))===array())
  149. {
  150. echo "No migration has been done before.\n";
  151. return 0;
  152. }
  153. $migrations=array_keys($migrations);
  154. $n=count($migrations);
  155. echo "Total $n ".($n===1 ? 'migration':'migrations')." to be redone:\n";
  156. foreach($migrations as $migration)
  157. echo " $migration\n";
  158. echo "\n";
  159. if($this->confirm('Redo the above '.($n===1 ? 'migration':'migrations')."?"))
  160. {
  161. foreach($migrations as $migration)
  162. {
  163. if($this->migrateDown($migration)===false)
  164. {
  165. echo "\nMigration failed. All later migrations are canceled.\n";
  166. return 2;
  167. }
  168. }
  169. foreach(array_reverse($migrations) as $migration)
  170. {
  171. if($this->migrateUp($migration)===false)
  172. {
  173. echo "\nMigration failed. All later migrations are canceled.\n";
  174. return 2;
  175. }
  176. }
  177. echo "\nMigration redone successfully.\n";
  178. }
  179. }
  180. public function actionTo($args)
  181. {
  182. if(!isset($args[0]))
  183. $this->usageError('Please specify which version, timestamp or datetime to migrate to.');
  184. if((string)(int)$args[0]==$args[0])
  185. return $this->migrateToTime($args[0]);
  186. elseif(($time=strtotime($args[0]))!==false)
  187. return $this->migrateToTime($time);
  188. else
  189. return $this->migrateToVersion($args[0]);
  190. }
  191. private function migrateToTime($time)
  192. {
  193. $data=$this->getDbConnection()->createCommand()
  194. ->select('version,apply_time')
  195. ->from($this->migrationTable)
  196. ->where('apply_time<=:time',array(':time'=>$time))
  197. ->order('apply_time DESC')
  198. ->limit(1)
  199. ->queryRow();
  200. if($data===false)
  201. {
  202. echo "Error: Unable to find a version before ".date('Y-m-d H:i:s',$time).".\n";
  203. return 1;
  204. }
  205. else
  206. {
  207. echo "Found version ".$data['version']." applied at ".date('Y-m-d H:i:s',$data['apply_time']).", it is before ".date('Y-m-d H:i:s',$time).".\n";
  208. return $this->migrateToVersion(substr($data['version'],1,13));
  209. }
  210. }
  211. private function migrateToVersion($version)
  212. {
  213. $originalVersion=$version;
  214. if(preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/',$version,$matches))
  215. $version='m'.$matches[1];
  216. else
  217. {
  218. echo "Error: The version option must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table).\n";
  219. return 1;
  220. }
  221. // try migrate up
  222. $migrations=$this->getNewMigrations();
  223. foreach($migrations as $i=>$migration)
  224. {
  225. if(strpos($migration,$version.'_')===0)
  226. return $this->actionUp(array($i+1));
  227. }
  228. // try migrate down
  229. $migrations=array_keys($this->getMigrationHistory(-1));
  230. foreach($migrations as $i=>$migration)
  231. {
  232. if(strpos($migration,$version.'_')===0)
  233. {
  234. if($i===0)
  235. {
  236. echo "Already at '$originalVersion'. Nothing needs to be done.\n";
  237. return 0;
  238. }
  239. else
  240. return $this->actionDown(array($i));
  241. }
  242. }
  243. echo "Error: Unable to find the version '$originalVersion'.\n";
  244. return 1;
  245. }
  246. public function actionMark($args)
  247. {
  248. if(isset($args[0]))
  249. $version=$args[0];
  250. else
  251. $this->usageError('Please specify which version to mark to.');
  252. $originalVersion=$version;
  253. if(preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/',$version,$matches))
  254. $version='m'.$matches[1];
  255. else {
  256. echo "Error: The version option must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table).\n";
  257. return 1;
  258. }
  259. $db=$this->getDbConnection();
  260. // try mark up
  261. $migrations=$this->getNewMigrations();
  262. foreach($migrations as $i=>$migration)
  263. {
  264. if(strpos($migration,$version.'_')===0)
  265. {
  266. if($this->confirm("Set migration history at $originalVersion?"))
  267. {
  268. $command=$db->createCommand();
  269. for($j=0;$j<=$i;++$j)
  270. {
  271. $command->insert($this->migrationTable, array(
  272. 'version'=>$migrations[$j],
  273. 'apply_time'=>time(),
  274. ));
  275. }
  276. echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n";
  277. }
  278. return 0;
  279. }
  280. }
  281. // try mark down
  282. $migrations=array_keys($this->getMigrationHistory(-1));
  283. foreach($migrations as $i=>$migration)
  284. {
  285. if(strpos($migration,$version.'_')===0)
  286. {
  287. if($i===0)
  288. echo "Already at '$originalVersion'. Nothing needs to be done.\n";
  289. else
  290. {
  291. if($this->confirm("Set migration history at $originalVersion?"))
  292. {
  293. $command=$db->createCommand();
  294. for($j=0;$j<$i;++$j)
  295. $command->delete($this->migrationTable, $db->quoteColumnName('version').'=:version', array(':version'=>$migrations[$j]));
  296. echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n";
  297. }
  298. }
  299. return 0;
  300. }
  301. }
  302. echo "Error: Unable to find the version '$originalVersion'.\n";
  303. return 1;
  304. }
  305. public function actionHistory($args)
  306. {
  307. $limit=isset($args[0]) ? (int)$args[0] : -1;
  308. $migrations=$this->getMigrationHistory($limit);
  309. if($migrations===array())
  310. echo "No migration has been done before.\n";
  311. else
  312. {
  313. $n=count($migrations);
  314. if($limit>0)
  315. echo "Showing the last $n applied ".($n===1 ? 'migration' : 'migrations').":\n";
  316. else
  317. echo "Total $n ".($n===1 ? 'migration has' : 'migrations have')." been applied before:\n";
  318. foreach($migrations as $version=>$time)
  319. echo " (".date('Y-m-d H:i:s',$time).') '.$version."\n";
  320. }
  321. }
  322. public function actionNew($args)
  323. {
  324. $limit=isset($args[0]) ? (int)$args[0] : -1;
  325. $migrations=$this->getNewMigrations();
  326. if($migrations===array())
  327. echo "No new migrations found. Your system is up-to-date.\n";
  328. else
  329. {
  330. $n=count($migrations);
  331. if($limit>0 && $n>$limit)
  332. {
  333. $migrations=array_slice($migrations,0,$limit);
  334. echo "Showing $limit out of $n new ".($n===1 ? 'migration' : 'migrations').":\n";
  335. }
  336. else
  337. echo "Found $n new ".($n===1 ? 'migration' : 'migrations').":\n";
  338. foreach($migrations as $migration)
  339. echo " ".$migration."\n";
  340. }
  341. }
  342. public function actionCreate($args)
  343. {
  344. if(isset($args[0]))
  345. $name=$args[0];
  346. else
  347. $this->usageError('Please provide the name of the new migration.');
  348. if(!preg_match('/^\w+$/',$name)) {
  349. echo "Error: The name of the migration must contain letters, digits and/or underscore characters only.\n";
  350. return 1;
  351. }
  352. $name='m'.gmdate('ymd_His').'_'.$name;
  353. $content=strtr($this->getTemplate(), array('{ClassName}'=>$name));
  354. $file=$this->migrationPath.DIRECTORY_SEPARATOR.$name.'.php';
  355. if($this->confirm("Create new migration '$file'?"))
  356. {
  357. file_put_contents($file, $content);
  358. echo "New migration created successfully.\n";
  359. }
  360. }
  361. public function confirm($message,$default=false)
  362. {
  363. if(!$this->interactive)
  364. return true;
  365. return parent::confirm($message,$default);
  366. }
  367. protected function migrateUp($class)
  368. {
  369. if($class===self::BASE_MIGRATION)
  370. return;
  371. echo "*** applying $class\n";
  372. $start=microtime(true);
  373. $migration=$this->instantiateMigration($class);
  374. if($migration->up()!==false)
  375. {
  376. $this->getDbConnection()->createCommand()->insert($this->migrationTable, array(
  377. 'version'=>$class,
  378. 'apply_time'=>time(),
  379. ));
  380. $time=microtime(true)-$start;
  381. echo "*** applied $class (time: ".sprintf("%.3f",$time)."s)\n\n";
  382. }
  383. else
  384. {
  385. $time=microtime(true)-$start;
  386. echo "*** failed to apply $class (time: ".sprintf("%.3f",$time)."s)\n\n";
  387. return false;
  388. }
  389. }
  390. protected function migrateDown($class)
  391. {
  392. if($class===self::BASE_MIGRATION)
  393. return;
  394. echo "*** reverting $class\n";
  395. $start=microtime(true);
  396. $migration=$this->instantiateMigration($class);
  397. if($migration->down()!==false)
  398. {
  399. $db=$this->getDbConnection();
  400. $db->createCommand()->delete($this->migrationTable, $db->quoteColumnName('version').'=:version', array(':version'=>$class));
  401. $time=microtime(true)-$start;
  402. echo "*** reverted $class (time: ".sprintf("%.3f",$time)."s)\n\n";
  403. }
  404. else
  405. {
  406. $time=microtime(true)-$start;
  407. echo "*** failed to revert $class (time: ".sprintf("%.3f",$time)."s)\n\n";
  408. return false;
  409. }
  410. }
  411. protected function instantiateMigration($class)
  412. {
  413. $file=$this->migrationPath.DIRECTORY_SEPARATOR.$class.'.php';
  414. require_once($file);
  415. $migration=new $class;
  416. $migration->setDbConnection($this->getDbConnection());
  417. return $migration;
  418. }
  419. /**
  420. * @var CDbConnection
  421. */
  422. private $_db;
  423. protected function getDbConnection()
  424. {
  425. if($this->_db!==null)
  426. return $this->_db;
  427. elseif(($this->_db=Yii::app()->getComponent($this->connectionID)) instanceof CDbConnection)
  428. return $this->_db;
  429. echo "Error: CMigrationCommand.connectionID '{$this->connectionID}' is invalid. Please make sure it refers to the ID of a CDbConnection application component.\n";
  430. exit(1);
  431. }
  432. protected function getMigrationHistory($limit)
  433. {
  434. $db=$this->getDbConnection();
  435. if($db->schema->getTable($this->migrationTable,true)===null)
  436. {
  437. $this->createMigrationHistoryTable();
  438. }
  439. return CHtml::listData($db->createCommand()
  440. ->select('version, apply_time')
  441. ->from($this->migrationTable)
  442. ->order('version DESC')
  443. ->limit($limit)
  444. ->queryAll(), 'version', 'apply_time');
  445. }
  446. protected function createMigrationHistoryTable()
  447. {
  448. $db=$this->getDbConnection();
  449. echo 'Creating migration history table "'.$this->migrationTable.'"...';
  450. $db->createCommand()->createTable($this->migrationTable,array(
  451. 'version'=>'varchar(180) NOT NULL PRIMARY KEY',
  452. 'apply_time'=>'integer',
  453. ));
  454. $db->createCommand()->insert($this->migrationTable,array(
  455. 'version'=>self::BASE_MIGRATION,
  456. 'apply_time'=>time(),
  457. ));
  458. echo "done.\n";
  459. }
  460. protected function getNewMigrations()
  461. {
  462. $applied=array();
  463. foreach($this->getMigrationHistory(-1) as $version=>$time)
  464. $applied[substr($version,1,13)]=true;
  465. $migrations=array();
  466. $handle=opendir($this->migrationPath);
  467. while(($file=readdir($handle))!==false)
  468. {
  469. if($file==='.' || $file==='..')
  470. continue;
  471. $path=$this->migrationPath.DIRECTORY_SEPARATOR.$file;
  472. if(preg_match('/^(m(\d{6}_\d{6})_.*?)\.php$/',$file,$matches) && is_file($path) && !isset($applied[$matches[2]]))
  473. $migrations[]=$matches[1];
  474. }
  475. closedir($handle);
  476. sort($migrations);
  477. return $migrations;
  478. }
  479. public function getHelp()
  480. {
  481. return <<<EOD
  482. USAGE
  483. yiic migrate [action] [parameter]
  484. DESCRIPTION
  485. This command provides support for database migrations. The optional
  486. 'action' parameter specifies which specific migration task to perform.
  487. It can take these values: up, down, to, create, history, new, mark.
  488. If the 'action' parameter is not given, it defaults to 'up'.
  489. Each action takes different parameters. Their usage can be found in
  490. the following examples.
  491. EXAMPLES
  492. * yiic migrate
  493. Applies ALL new migrations. This is equivalent to 'yiic migrate up'.
  494. * yiic migrate create create_user_table
  495. Creates a new migration named 'create_user_table'.
  496. * yiic migrate up 3
  497. Applies the next 3 new migrations.
  498. * yiic migrate down
  499. Reverts the last applied migration.
  500. * yiic migrate down 3
  501. Reverts the last 3 applied migrations.
  502. * yiic migrate to 101129_185401
  503. Migrates up or down to version 101129_185401.
  504. * yiic migrate to 1392447720
  505. Migrates to the given UNIX timestamp. This means that all the versions
  506. applied after the specified timestamp will be reverted. Versions applied
  507. before won't be touched.
  508. * yiic migrate to "2014-02-15 13:00:50"
  509. Migrates to the given datetime parseable by the strtotime() function.
  510. This means that all the versions applied after the specified datetime
  511. will be reverted. Versions applied before won't be touched.
  512. * yiic migrate mark 101129_185401
  513. Modifies the migration history up or down to version 101129_185401.
  514. No actual migration will be performed.
  515. * yiic migrate history
  516. Shows all previously applied migration information.
  517. * yiic migrate history 10
  518. Shows the last 10 applied migrations.
  519. * yiic migrate new
  520. Shows all new migrations.
  521. * yiic migrate new 10
  522. Shows the next 10 migrations that have not been applied.
  523. EOD;
  524. }
  525. protected function getTemplate()
  526. {
  527. if($this->templateFile!==null)
  528. return file_get_contents(Yii::getPathOfAlias($this->templateFile).'.php');
  529. else
  530. return <<<EOD
  531. <?php
  532. class {ClassName} extends CDbMigration
  533. {
  534. public function up()
  535. {
  536. }
  537. public function down()
  538. {
  539. echo "{ClassName} does not support migration down.\\n";
  540. return false;
  541. }
  542. /*
  543. // Use safeUp/safeDown to do migration with transaction
  544. public function safeUp()
  545. {
  546. }
  547. public function safeDown()
  548. {
  549. }
  550. */
  551. }
  552. EOD;
  553. }
  554. }