-
Notifications
You must be signed in to change notification settings - Fork 96
/
Copy pathMigrate.php
395 lines (347 loc) · 12.4 KB
/
Migrate.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
<?php
/**
* Ruckusing
*
* @category Ruckusing
* @package Task
* @subpackage Db
* @author Cody Caughlan <codycaughlan % gmail . com>
* @link https://github.com/ruckus/ruckusing-migrations
*/
define('STYLE_REGULAR', 1);
define('STYLE_OFFSET', 2);
/**
* Task_DB_Migrate.
* This is the primary work-horse method, it runs all migrations available,
* up to the current version.
*
* @category Ruckusing
* @package Task
* @subpackage Db
* @author Cody Caughlan <codycaughlan % gmail . com>
* @link https://github.com/ruckus/ruckusing-migrations
*/
class Task_Db_Migrate extends Ruckusing_Task_Base implements Ruckusing_Task_Interface
{
/**
* migrator util
*
* @var Ruckusing_Util_Migrator
*/
private $_migrator_util = null;
/**
* Current Adapter
*
* @var Ruckusing_Adapter_Base
*/
private $_adapter = null;
/**
* migrator directories
*
* @var string
*/
private $_migratorDirs = null;
/**
* The task arguments
*
* @var array
*/
private $_task_args = array();
/**
* debug
*
* @var boolean
*/
private $_debug = false;
/**
* Return executed string
*
* @var string
*/
private $_return = '';
/**
* Creates an instance of Task_DB_Migrate
*
* @param Ruckusing_Adapter_Base $adapter The current adapter being used
*
* @return Task_DB_Migrate
*/
public function __construct($adapter)
{
parent::__construct($adapter);
$this->_adapter = $adapter;
$this->_migrator_util = new Ruckusing_Util_Migrator($this->_adapter);
}
/**
* Primary task entry point
*
* @param array $args The current supplied options.
*/
public function execute($args)
{
if (!$this->_adapter->supports_migrations()) {
throw new Ruckusing_Exception(
"This database does not support migrations.",
Ruckusing_Exception::MIGRATION_NOT_SUPPORTED
);
}
$this->_task_args = $args;
$this->_return .= "Started: " . date('Y-m-d g:ia T') . "\n\n";
$this->_return .= "[db:migrate]: \n";
try {
// Check that the schema_version table exists, and if not, automatically create it
$this->verify_environment();
$target_version = null;
$style = STYLE_REGULAR;
//did the user specify an explicit version?
if (array_key_exists('version', $this->_task_args)) {
$target_version = trim($this->_task_args['version']);
}
// did the user specify a relative offset, e.g. "-2" or "+3" ?
if ($target_version !== null) {
if (preg_match('/^([\-\+])(\d+)$/', $target_version, $matches)) {
if (count($matches) == 3) {
$direction = $matches[1] == '-' ? 'down' : 'up';
$steps = intval($matches[2]);
$style = STYLE_OFFSET;
}
}
}
//determine our direction and target version
$current_version = $this->_migrator_util->get_max_version();
if ($style == STYLE_REGULAR) {
if (is_null($target_version)) {
$this->prepare_to_migrate($target_version, 'up');
} elseif ($current_version > $target_version) {
$this->prepare_to_migrate($target_version, 'down');
} else {
$this->prepare_to_migrate($target_version, 'up');
}
}
if ($style == STYLE_OFFSET) {
$this->migrate_from_offset($steps, $current_version, $direction);
}
// Completed - display accumulated output
if (!empty($output)) {
$this->_return .= "\n\n";
}
} catch (Ruckusing_Exception $ex) {
if ($ex->getCode() == Ruckusing_Exception::MISSING_SCHEMA_INFO_TABLE) {
$this->_return .= "\tSchema info table does not exist. I tried creating it but failed. Check permissions.";
} else {
throw $ex;
}
}
$this->_return .= "\n\nFinished: " . date('Y-m-d g:ia T') . "\n\n";
return $this->_return;
}
/**
* Migrate to a specific version using steps from current version
*
* @param integer $steps number of versions to jump to
* @param string $current_version current version
* @param $string $direction direction to migrate to 'up'/'down'
*/
private function migrate_from_offset($steps, $current_version, $direction)
{
$migrations = $this->_migrator_util->get_migration_files($this->_migratorDirs, $direction);
$current_index = $this->_migrator_util->find_version($migrations, $current_version, true);
$current_index = $current_index !== null ? $current_index : -1;
if ($this->_debug == true) {
$this->_return .= print_r($migrations, true);
$this->_return .= "\ncurrent_index: " . $current_index . "\n";
$this->_return .= "\ncurrent_version: " . $current_version . "\n";
$this->_return .= "\nsteps: " . $steps . " $direction\n";
}
// If we are not at the bottom then adjust our index (to satisfy array_slice)
if ($current_index == -1 && $direction === 'down') {
$available = array();
} else {
if ($direction === 'up') {
$current_index += 1;
} else {
$current_index += $steps;
}
// check to see if we have enough migrations to run - the user
// might have asked to run more than we have available
$available = array_slice($migrations, $current_index, $steps);
}
$target = end($available);
if ($this->_debug == true) {
$this->_return .= "\n------------- TARGET ------------------\n";
$this->_return .= print_r($target, true);
}
$this->prepare_to_migrate(isset($target['version']) ? $target['version'] : null, $direction);
}
/**
* Prepare to do a migration
*
* @param string $destination version to migrate to
* @param $string $direction direction to migrate to 'up'/'down'
*/
private function prepare_to_migrate($destination, $direction)
{
try {
$this->_return .= "\tMigrating " . strtoupper($direction);
if (!is_null($destination)) {
$this->_return .= " to: {$destination}\n";
} else {
$this->_return .= ":\n";
}
$migrations = $this->_migrator_util->get_runnable_migrations(
$this->_migratorDirs,
$direction,
$destination
);
if (count($migrations) == 0) {
$this->_return .= "\nNo relevant migrations to run. Exiting...\n";
return;
}
$result = $this->run_migrations($migrations, $direction, $destination);
} catch (Exception $ex) {
throw $ex;
}
}
/**
* Run migrations
*
* @param array $migrations nigrations to run
* @param $string $target_method direction to migrate to 'up'/'down'
* @param string $destination version to migrate to
*
* @return array
*/
private function run_migrations($migrations, $target_method, $destination)
{
$last_version = -1;
foreach ($migrations as $file) {
$full_path = $this->_migratorDirs[$file['module']] . DIRECTORY_SEPARATOR . $file['file'];
if (is_file($full_path) && is_readable($full_path) ) {
require_once $full_path;
$klass = Ruckusing_Util_Naming::class_from_migration_file($file['file']);
$obj = new $klass($this->_adapter);
$start = $this->start_timer();
try {
//start transaction
$this->_adapter->start_transaction();
$result = $obj->$target_method();
//successfully ran migration, update our version and commit
$this->_migrator_util->resolve_current_version($file['version'], $target_method);
$this->_adapter->commit_transaction();
} catch (Ruckusing_Exception $e) {
$this->_adapter->rollback_transaction();
//wrap the caught exception in our own
throw new Ruckusing_Exception(
sprintf("%s - %s", $file['class'], $e->getMessage()),
Ruckusing_Exception::MIGRATION_FAILED
);
}
$end = $this->end_timer();
$diff = $this->diff_timer($start, $end);
$this->_return .= sprintf("========= %s ======== (%.2f)\n", $file['class'], $diff);
$last_version = $file['version'];
$exec = true;
}
}
//update the schema info
$result = array('last_version' => $last_version);
return $result;
}
/**
* Start Timer
*
* @return int
*/
private function start_timer()
{
return microtime(true);
}
/**
* End Timer
*
* @return int
*/
private function end_timer()
{
return microtime(true);
}
/**
* Calculate the time difference
*
* @param int $s the start time
* @param int $e the end time
*
* @return int
*/
private function diff_timer($s, $e)
{
return $e - $s;
}
/**
* Check the environment and create the migration dir if it doesn't exists
*/
private function verify_environment()
{
if (!$this->_adapter->table_exists($this->_adapter->get_schema_version_table_name()) ) {
$this->_return .= "\n\tSchema version table does not exist. Auto-creating.";
$this->auto_create_schema_info_table();
}
$this->_migratorDirs = $this->get_framework()->migrations_directories();
// create the migrations directory if it doesnt exist
foreach ($this->_migratorDirs as $name => $path) {
if (!is_dir($path)) {
$this->_return .= sprintf("\n\tMigrations directory (%s) doesn't exist, attempting to create.", $path);
if (mkdir($path, 0755, true) === FALSE) {
$this->_return .= sprintf("\n\tUnable to create migrations directory at %s, check permissions?", $path);
} else {
$this->_return .= sprintf("\n\tCreated OK");
}
}
}
}
/**
* Create the schema
*
* @return boolean
*/
private function auto_create_schema_info_table()
{
try {
$this->_return .= sprintf("\n\tCreating schema version table: %s", $this->_adapter->get_schema_version_table_name() . "\n\n");
$this->_adapter->create_schema_version_table();
return true;
} catch (Exception $e) {
throw new Ruckusing_Exception(
"\nError auto-creating 'schema_info' table: " . $e->getMessage() . "\n\n",
Ruckusing_Exception::MIGRATION_FAILED
);
}
}
/**
* Return the usage of the task
*
* @return string
*/
public function help()
{
$output =<<<USAGE
\tTask: db:migrate [VERSION]
\tThe primary purpose of the framework is to run migrations, and the
\texecution of migrations is all handled by just a regular ol' task.
\tVERSION can be specified to go up (or down) to a specific
\tversion, based on the current version. If not specified,
\tall migrations greater than the current database version
\twill be executed.
\tExample A: The database is fresh and empty, assuming there
\tare 5 actual migrations, but only the first two should be run.
\t\tphp {$_SERVER['argv'][0]} db:migrate VERSION=20101006114707
\tExample B: The current version of the DB is 20101006114707
\tand we want to go down to 20100921114643
\t\tphp {$_SERVER['argv'][0]} db:migrate VERSION=20100921114643
\tExample C: You can also use relative number of revisions
\t(positive migrate up, negative migrate down).
\t\tphp {$_SERVER['argv'][0]} db:migrate VERSION=-2
USAGE;
return $output;
}
}