├── .gitattributes ├── .gitignore ├── .travis.yml ├── composer.json ├── phpunit.xml ├── readme.md └── src ├── MigrationSquasher.php ├── SquasherServiceProvider.php ├── TableBuilder.php ├── commands ├── .gitkeep └── SquashMigrations.php ├── database ├── Column.php ├── Relationship.php └── Table.php └── tests ├── ColumnTest.php ├── MigrationSquasherTest.php ├── RelationshipTest.php ├── TableTest.php └── data ├── MigrationTestData.php └── output └── Expected.php /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bootstrap/compiled.php 2 | /vendor 3 | composer.phar 4 | composer.lock 5 | .DS_Store 6 | Thumbs.db -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | # list any PHP version you want to test against 4 | php: 5 | # aliased to a recent 5.4.x version 6 | - 5.4 7 | # aliased to a recent 5.5.x version 8 | - 5.5 9 | 10 | # execute any number of scripts before the test run, custom env's are available as variables 11 | before_script: 12 | - composer install --dev --prefer-source 13 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cytracom/squasher", 3 | "description": "Aggregate your incremental Laravel migration files into single migration for each table. This eliminates all alter columns and makes testing via sqlite a possibility.", 4 | "keywords": ["framework", "laravel", "migrations", "migrate", "migration", "squash", "sqlite", "testing"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Jacob Barber", 9 | "email": "open-source@cytracom.com" 10 | } 11 | ], 12 | "require": { 13 | "laravel/framework": "4.0.*" 14 | }, 15 | "require-dev": { 16 | "phpunit/phpunit": "3.7.*" 17 | }, 18 | "autoload": { 19 | "psr-0": { 20 | "Cytracom\\Squasher": "src/" 21 | }, 22 | "classmap": [ 23 | "src", 24 | "src/database", 25 | "src/commands" 26 | ] 27 | }, 28 | "config": { 29 | "preferred-install": "dist" 30 | }, 31 | "minimum-stability": "dev" 32 | } 33 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./src/tests/ 15 | 16 | 17 | 18 | 19 | ./src 20 | 21 | ./src/tests 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Aggregate your incremental Laravel migration files into single migration for each table. This can be beneficial when needing to compress large migration sets into a single migration for each table. 2 | 3 | This package also eliminates all alter columns since all migartions are aggregated into a single file making testing via sqlite a possibility. 4 | 5 | [![Build Status](https://travis-ci.org/Cytracom/laravel-migration-squasher.png)](https://travis-ci.org/Cytracom/laravel-migration-squasher) 6 | 7 | To install simply require 8 | ``` 9 | "cytracom/squasher": "dev-master" 10 | ``` 11 | Then, add the service provider to your app/config/app.php to enable artisan functionality: 12 | ``` 13 | 'Cytracom\Squasher\SquasherServiceProvider' 14 | ``` 15 | NOTE: this is not required if you do not wish to have the commandline interface. If you want to use the squasher just for testing, then you can ignore this service provider, and call the squasher directly. This way, the squasher can be in your require-dev and not be a part of your production stack. 16 | 17 | 18 | 19 | Commandline usage: 20 | ``` 21 | php artisan migrate:squash [-p|--path[="..."]] [-o|--output[="..."]] [-mv|--move-to[="..."]] 22 | 23 | Options: 24 | --path (-p) The path to the migrations folder (default: "app/database/migrations") 25 | --output (-o) The path to the output folder of squashes (default: "app/tests/migrations") 26 | --move-to (-mv) The path where old migrations will be moved. (default: "app/database/migrations") 27 | ``` 28 | 29 | 30 | Usage in php: 31 | ```php 32 | $squasher = new \Cytracom\Squasher\MigrationSquasher($pathToMigrations, $outputForSquashedMigrations [, $moveOldToThisPath = null]); 33 | $squasher->squash(); 34 | ``` 35 | 36 | 37 | The squasher does not currently support composite keys, or indexes. If you find anything else I missed, please raise an issue! Or, even better, attempt to integrate it! 38 | 39 | The migration squasher will take several migrations and create a single, final migration that reflects what the database schema should be after all migrations have run. 40 | 41 | Keep in mind that the squasher was made for testing, not for incremental database changes. Using the squasher will drop any non-migration related functionality in your code. The goal is to get rid of all alter columns, to enable sqlite testing. 42 | 43 | The table squasher can handle simple migration statements, written in a normal, not insane way. Like this: 44 | 45 | ```php 46 | Schema::create('my_table', function (Blueprint $table) { 47 | $table->integer("my_int",10)->unsigned()->unique(); 48 | $table->increments("id"); 49 | $table->string("test",255); 50 | $table->myEnum("oldArrayInit", array("val1","val2")); 51 | $table->myEnum("newArrayInit", ["val1","val2"]); 52 | 53 | DB::update('ALTER TABLE `my_table` MODIFY COLUMN `test` blob(500);'); 54 | //etc; 55 | }); 56 | ``` 57 | This also works for dropping and modifying schemas. For a more detailed view on what it can handle, look at the sample test data in src/tests/data/MigrationTestData.php 58 | 59 | The table squasher will NOT handle things like 60 | ```php 61 | $myStringColumns = ["col1","col2","col3"]; 62 | foreach($myStringColumns as $column){ 63 | $table->string($column); 64 | } 65 | ``` 66 | And it never ever will. Migrations shouldn't be written this way, and writing a php parser in php is no small task. 67 | 68 | 69 | Here is how you can use this for your tests 70 | 71 | While setting up the test case, we run 72 | 73 | ```php 74 | recursiveDelete(base_path('app/tests/migrations')); 75 | $squash = new \Cytracom\Squasher\MigrationSquasher("app/database/migrations", "app/tests/migrations"); 76 | $squash->squash(); 77 | \Artisan::call('migrate', ['--path' => 'app/tests/migrations']); 78 | 79 | /** 80 | * Delete a file or recursively delete a directory 81 | * 82 | * @param string $str Path to file or directory 83 | * @return bool 84 | */ 85 | function recursiveDelete($str){ 86 | if(is_file($str)){ 87 | return @unlink($str); 88 | } 89 | elseif(is_dir($str)){ 90 | $scan = glob(rtrim($str,'/').'/*'); 91 | foreach($scan as $index=>$path){ 92 | recursiveDelete($path); 93 | } 94 | return @rmdir($str); 95 | } 96 | } 97 | ``` 98 | We delete all of the migrations before squashing again, to get rid of old squashed migrations that may be there. 99 | 100 | Again, please raise an issue if you find one, and feel free to make pull requests for review! Our goal is to make testing with sqlite much more of a possibility, to enable fast testing. Help from the community is always appreciated. 101 | -------------------------------------------------------------------------------- /src/MigrationSquasher.php: -------------------------------------------------------------------------------- 1 | migrationPath = trim(($pathToMigrations), '/') . '/'; 63 | $this->outputPath = $this->setupFolder($outputMigrations); 64 | $this->moveToPath = $moveToPath == null ? null : $this->setupFolder($moveToPath); 65 | $this->migrations = scandir($this->migrationPath); 66 | $this->tables = []; 67 | } 68 | 69 | /** 70 | * Begin squashing all migrations in the migration path. 71 | */ 72 | public function squash() 73 | { 74 | echo "Beginning migration squash\n"; 75 | 76 | $this->parseMigrations(); 77 | 78 | $sortedTableNames = $this->resolveTableDependencies(); 79 | $date = date('Y_m_d'); 80 | foreach ($sortedTableNames as $key => $table) { 81 | echo "Squashing $table\n"; 82 | 83 | file_put_contents($this->outputPath . $date . '_' . str_pad($key, 6, '0', STR_PAD_LEFT) . 84 | "_squashed_" . $table . 85 | "_table.php", TableBuilder::build($this->tables[$table])); 86 | } 87 | 88 | echo "Squash complete!" . (trim($this->moveToPath, '/') === trim($this->migrationPath, '/') ? '' : 89 | " Old migrations have been moved to " . $this->moveToPath) . "\n"; 90 | echo "New migrations are located in $this->outputPath\n"; 91 | } 92 | 93 | /** 94 | * Begin parsing each file. 95 | */ 96 | protected function parseMigrations() 97 | { 98 | foreach ($this->migrations as $migration) { 99 | if (!is_dir($migration)) { 100 | echo "Parsing migration $migration\n"; 101 | if ($this->parseFile($migration) && $this->moveToPath !== null) { 102 | rename($this->migrationPath . $migration, base_path($this->moveToPath . $migration)); 103 | } 104 | } 105 | } 106 | } 107 | 108 | /** 109 | * Parse the given file. 110 | * 111 | * @param $filePath 112 | * @return bool true/false if the file was a migration 113 | */ 114 | protected function parseFile($filePath) 115 | { 116 | $file = file_get_contents($this->migrationPath . $filePath); 117 | $file = str_replace("\n","",$file); 118 | $file = str_replace("{","{\n", $file); 119 | $file = str_replace(";",";\n", $file); 120 | $file = str_replace("}","}\n", $file); 121 | $fileLines = explode(PHP_EOL, file_get_contents($this->migrationPath . $filePath)); 122 | return $this->parseLines($fileLines); 123 | } 124 | 125 | /** 126 | * Parse each string from the given array of strings 127 | * 128 | * @param $fileLines 129 | * @return bool true/false if the file was a migration 130 | */ 131 | protected function parseLines($fileLines) 132 | { 133 | $table = null; 134 | $migration = false; 135 | foreach ($fileLines as $line) { 136 | if (preg_match('/public function down\(.*\)/', $line)) { 137 | break; 138 | } 139 | 140 | if (str_contains($line, "}")) { 141 | $table = null; 142 | } 143 | if ($this->lineContainsDbStatement($line) && 144 | preg_match_all('/ALTER TABLE *`?([^` ]*)`? *(?>MODIFY|CHANGE) *COLUMN `?([^ `]*)`? *([^;( ]*)(\(([^)]*))?\)? *([^\';]*)/i', 145 | $line, $matches1) 146 | ) { 147 | $this->createColumnFromDbStatement($matches1); 148 | }elseif(preg_match_all('/Schema::rename\((\'|")([^\'"]*)[^,]*,(\'|")([^\'"]*)/', $line, $matches3)){ 149 | $name = $matches3[2][0]; 150 | $newName = $matches3[4][0]; 151 | $this->tables[$newName] = $this->tables[$name]; 152 | $this->tables[$newName]->name = $newName; 153 | unset($this->tables[$name]); 154 | $table = $this->tables[$newName]; 155 | } 156 | elseif (preg_match('/Schema::(d|c|t|[^(]*\((\'|")(.*)(\'|"))*/', $line, $matches2)) { 157 | $table = $this->parseTable($matches2); 158 | $migration = true; 159 | } 160 | elseif ($table !== null) { 161 | $this->parseField($table, $line); 162 | } 163 | } 164 | return $migration; 165 | } 166 | 167 | /** 168 | * Pull the table out of the given regex matches. 169 | * 170 | * @param $matches 171 | * @return null|Table 172 | */ 173 | protected function parseTable($matches) 174 | { 175 | preg_match('/(\'|").*(\'|")/', $matches[0], $tableMatch); 176 | $tableMatch = preg_replace("/'|\"/", "", $tableMatch[0]); 177 | 178 | if (str_contains($matches[0], '::drop')) { 179 | unset($this->tables[$tableMatch]); 180 | return null; 181 | } 182 | 183 | return isset($this->tables[$tableMatch]) ? $this->tables[$tableMatch] : 184 | $this->tables[$tableMatch] = new Table($tableMatch); 185 | } 186 | 187 | /** 188 | * Parse the given line and set the values in the given table. 189 | * 190 | * @param Table $table 191 | * @param $line 192 | */ 193 | protected function parseField(Table $table, $line) 194 | { 195 | if (preg_match('/\$[^->]*->engine/', $line)) { 196 | $table->setEngine(preg_replace("/'|;| |\"/", "", explode("=", $line)[1])); 197 | return; 198 | } 199 | elseif ($matches = $this->lineContainsFunctionCall($line)) { 200 | $this->createMigrationFunctionCall($table, $line, $matches[0]); 201 | } 202 | } 203 | 204 | 205 | /** 206 | * Create the function call based on the column on the line. 207 | * 208 | * @param Table $table 209 | * @param $line 210 | * @param $matches 211 | */ 212 | protected function createMigrationFunctionCall(Table $table, $line, $matches) 213 | { 214 | $line = str_replace('"', "'", $line); 215 | $segments = explode("'", $line); 216 | $matches[0] = preg_replace('/>| |,/', '', $matches[0]); 217 | switch ($matches[0]) { 218 | case 'primary' : 219 | $table->setPrimaryKey($segments[1]); 220 | break; 221 | case 'unique' : 222 | $table->getColumn($segments[1])->unique = true; 223 | break; 224 | case 'renameColumn': 225 | $table->alterColumn($segments[1], "name", $segments[3]); 226 | break; 227 | case 'foreign': 228 | $table->addRelationship(new Relationship($segments[1], $segments[3], $segments[5])); 229 | break; 230 | case 'dropColumn': 231 | case 'dropIfExists' : 232 | $table->dropColumn($segments[1]); 233 | break; 234 | case 'dropForeign': 235 | $table->dropRelationship($segments[1]); 236 | break; 237 | case 'dropSoftDeletes' : 238 | $table->dropColumn('softDeletes'); 239 | break; 240 | case 'dropTimestamps' : 241 | $table->dropColumn('timestamps'); 242 | break; 243 | case 'timestamps' : 244 | case 'softDeletes' : 245 | case 'nullableTimestamps' : 246 | $segments[1] = $matches[0]; 247 | case 'string' : 248 | case 'integer' : 249 | case 'increments' : 250 | case 'bigIncrements' : 251 | case 'bigInteger' : 252 | case 'smallInteger' : 253 | case 'float' : 254 | case 'double' : 255 | case 'decimal' : 256 | case 'boolean' : 257 | case 'date' : 258 | case 'dateTime' : 259 | case 'time' : 260 | case 'timestamp' : 261 | case 'text' : 262 | case 'binary' : 263 | case 'morphs' : 264 | case 'mediumText' : 265 | case 'longText' : 266 | case 'mediumInteger' : 267 | case 'tinyInteger' : 268 | case 'unsignedBigInteger' : 269 | case 'unsignedInteger' : 270 | case 'enum' : 271 | $table->addColumn($this->createStandardColumn($matches, $segments, $line)); 272 | break; 273 | } 274 | $matches = null; 275 | } 276 | 277 | /** 278 | * A generic function for creating a plain old column. 279 | * 280 | * @param $matches 281 | * @param $segments 282 | * @param $line 283 | * @return \Cytracom\Squasher\Database\Column 284 | */ 285 | protected function createStandardColumn($matches, $segments, $line) 286 | { 287 | $col = new Column($matches[0], isset($segments[1]) ? $segments[1] : null); 288 | foreach ($matches as $key => $match) { 289 | if ($key === 0) { 290 | continue; 291 | } 292 | if (str_contains($match, 'unsigned')) { 293 | $col->unsigned = true; 294 | } 295 | elseif (str_contains($match, 'unique')) { 296 | $col->unique = true; 297 | } 298 | elseif (str_contains($match, 'nullable')) { 299 | $col->nullable = true; 300 | } 301 | elseif (str_contains($match, 'default')) { 302 | preg_match_all('/default\(([^)]*)/', $line, $default); 303 | $col->default = $default[1][0]; 304 | } 305 | } 306 | array_shift($segments); 307 | array_shift($segments); 308 | $segments = implode("'",$segments); 309 | if (isset($segments)) { 310 | $col->parameters = 311 | preg_match('/, *.*?\)(-|;)/', $segments, $lineSize) ? 312 | trim(substr(preg_replace('/\)(-|;)/', '', $lineSize[0], 1),1),' ') : 313 | null; 314 | } 315 | return $col; 316 | } 317 | 318 | /** 319 | * Return an array of function calls on the given line, or false if there are none. 320 | * 321 | * @param $line 322 | * @return array|bool 323 | */ 324 | protected function lineContainsFunctionCall($line) 325 | { 326 | if (preg_match_all('/[^->]*>[^(]*/', $line, $match)) { 327 | return $match; 328 | } 329 | return false; 330 | } 331 | 332 | /** 333 | * Return an array of function calls on the given line, or false if there are none. 334 | * 335 | * @param $line 336 | * @return array|bool 337 | */ 338 | protected function lineContainsDbStatement($line) 339 | { 340 | return str_contains($line, "::update"); 341 | } 342 | 343 | /** 344 | * Create the given folder recursively, and return the correctly formatted folder path. 345 | * 346 | * @param $folder 347 | * @return string 348 | */ 349 | protected function setupFolder($folder) 350 | { 351 | $folder = trim($folder, '/'); 352 | if (!is_dir($folder)) { 353 | echo "Creating output folder $folder\n"; 354 | mkdir($folder, 0777, true); 355 | } 356 | $folder .= '/'; 357 | return $folder; 358 | } 359 | 360 | /** 361 | * Return an array that is the correct order that tables should be created. 362 | * 363 | * @return array 364 | */ 365 | protected function resolveTableDependencies() 366 | { 367 | echo "Resolving foreign key relationships...\n"; 368 | $sortedTables = []; 369 | $count = count($this->tables); 370 | while (count($sortedTables) !== $count) { 371 | { 372 | foreach ($this->tables as $table) { 373 | if (in_array($table->name, $sortedTables)) { 374 | continue; 375 | } 376 | 377 | $resolved = true; 378 | foreach ($table->getRelationships() as $relationship) { 379 | if (!in_array($relationship->relationshipTable, $sortedTables)) { 380 | $resolved = false; 381 | break; 382 | } 383 | } 384 | if ($resolved) { 385 | array_push($sortedTables, $table->name); 386 | } 387 | } 388 | } 389 | } 390 | echo "Done!\n"; 391 | return $sortedTables; 392 | } 393 | 394 | /** 395 | * @param $matches 396 | * @return mixed 397 | */ 398 | protected function createColumnFromDbStatement($matches) 399 | { 400 | $table = $matches[1][0]; 401 | $column = $matches[2][0]; 402 | $type = $this->convertMySqlTypeToLaravelType(strtolower($matches[3][0])); 403 | $params = $matches[5][0]; 404 | 405 | $attributes = strtolower($matches[6][0]); 406 | 407 | if ($this->tables[$table]->hasColumn($column)) { 408 | if (str_contains($attributes, 'auto_increment')) { 409 | if ($type === "bigInteger") { 410 | $type = "bigIncrements"; 411 | } 412 | elseif ($type === "integer") { 413 | $type = "increments"; 414 | } 415 | } 416 | 417 | $col = new Column($type, $column); 418 | $col->nullable = str_contains($attributes, "not null") ? false : true; 419 | 420 | switch ($type) { 421 | case 'string' : 422 | $col->parameters = $params; 423 | break; 424 | case 'double' : 425 | case 'decimal' : 426 | $col->parameters = $params; 427 | } 428 | 429 | $this->tables[$table]->addColumn($col); 430 | }else{ 431 | echo "\n======================================================================\nWARNING: You have a mysql query modifying a non-existent column.\n$column\n======================================================================\n"; 432 | } 433 | 434 | } 435 | 436 | protected function convertMySqlTypeToLaravelType($type) 437 | { 438 | switch ($type) { 439 | case 'char' : 440 | case 'varchar' : 441 | return 'string'; 442 | case 'bigint' : 443 | case 'biginteger': 444 | return 'bigInteger'; 445 | case 'int': 446 | return 'integer'; 447 | case 'smallint' : 448 | case 'smallinteger' : 449 | return 'smallInteger'; 450 | case 'blob' : 451 | return 'binary'; 452 | 453 | } 454 | return $type; 455 | } 456 | } 457 | 458 | -------------------------------------------------------------------------------- /src/SquasherServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerMigrationSquasher(); 31 | 32 | $this->commands( 33 | 'migrate:squash' 34 | ); 35 | } 36 | 37 | /** 38 | * Register generate:model 39 | * 40 | * @return \Cytracom\Squasher\Command\SquashMigrations 41 | */ 42 | protected function registerMigrationSquasher() 43 | { 44 | $this->app['migrate:squash'] = $this->app->share(function($app) 45 | { 46 | return new SquashMigrations(); 47 | }); 48 | } 49 | } -------------------------------------------------------------------------------- /src/TableBuilder.php: -------------------------------------------------------------------------------- 1 | table = $table; 38 | } 39 | 40 | /** 41 | * Build a migration file using a given table. 42 | * 43 | * @param Table $table 44 | * @return mixed 45 | */ 46 | public static function build(Table $table) 47 | { 48 | $squasher = new self($table); 49 | $squasher->content = $squasher->init(); 50 | $squasher->fillInTableData(); 51 | $squasher->content .= $squasher->close(); 52 | return str_replace("\n", PHP_EOL, $squasher->content); 53 | } 54 | 55 | /** 56 | * Fill out the table data. 57 | */ 58 | public function fillInTableData() 59 | { 60 | $this->createColumns(); 61 | $this->createPrimaryKey(); 62 | $this->createRelationships(); 63 | $this->content .= " \$table->engine = '" . $this->table->getEngine() . "';\n"; 64 | 65 | } 66 | 67 | /** 68 | * Create all of the column for the given table, and put nameless columns last (timestamps, softDeletes). 69 | */ 70 | protected function createColumns() 71 | { 72 | $doLater = []; 73 | foreach ($this->table->getColumns() as $column) { 74 | //if it is a generic column such as timestamps, soft deletes, etc; put at the end of the column list. 75 | if ($column->name === '' || $column->name === null || $column->name === $column->type) { 76 | array_push($doLater, $column); 77 | } 78 | else { 79 | $this->content .= $this->createColumn($column); 80 | } 81 | } 82 | 83 | foreach ($doLater as $column) { 84 | $column->name = null; 85 | $this->content .= $this->createColumn($column); 86 | } 87 | } 88 | 89 | /** 90 | * Set the primary key if this tables PK is specified. 91 | */ 92 | protected function createPrimaryKey() 93 | { 94 | if ($this->table->getPrimaryKey() !== null) { 95 | $this->content .= " \$table->primary('" . $this->table->getPrimaryKey() . "');\n"; 96 | } 97 | } 98 | 99 | /** 100 | * Create all of the tables relationships. 101 | */ 102 | protected function createRelationships() 103 | { 104 | foreach ($this->table->getRelationships() as $relationship) { 105 | $this->content .= " \$table->foreign('$relationship->tableColumn')->" . 106 | "references('$relationship->relationshipColumn')->on('$relationship->relationshipTable');\n"; 107 | } 108 | } 109 | 110 | /** 111 | * Create the given column and apply it's attributes. 112 | * 113 | * @param $column 114 | * @return string 115 | */ 116 | public function createColumn($column) 117 | { 118 | $line = " \$table->$column->type("; 119 | 120 | $line .= $this->appendColumnName($column); 121 | $line .= $this->appendColumnParameters($column) . ')'; 122 | $line .= $this->appendColumnSign($column); 123 | $line .= $this->appendColumnUnique($column); 124 | $line .= $this->appendColumnNullability($column); 125 | $line .= $this->appendColumnDefault($column); 126 | $line .= ";\n"; 127 | return $line; 128 | } 129 | 130 | /** 131 | * Add the column size if specified. 132 | * 133 | * @param $column 134 | * @return string 135 | */ 136 | protected function appendColumnParameters($column) 137 | { 138 | if ($column->parameters !== null) { 139 | return ", " . $column->parameters; 140 | } 141 | return ''; 142 | } 143 | 144 | /** 145 | * Add the column name if specified. 146 | * 147 | * @param $column 148 | * @return string 149 | */ 150 | protected function appendColumnName($column) 151 | { 152 | if ($column->name !== null) { 153 | return "'$column->name'"; 154 | } 155 | return ''; 156 | } 157 | 158 | /** 159 | * Add the column sign if specified. 160 | * 161 | * @param $column 162 | * @return string 163 | */ 164 | protected function appendColumnSign($column) 165 | { 166 | if ($column->unsigned) { 167 | return "->unsigned()"; 168 | } 169 | return ''; 170 | } 171 | 172 | /** 173 | * Mark if the column is unique or not. 174 | * 175 | * @param $column 176 | * @return string 177 | */ 178 | protected function appendColumnUnique($column) 179 | { 180 | if ($column->unique) { 181 | return "->unique()"; 182 | } 183 | return ''; 184 | } 185 | 186 | /** 187 | * Mark if the column is nullable. 188 | * 189 | * @param $column 190 | * @return string 191 | */ 192 | protected function appendColumnNullability($column) 193 | { 194 | if ($column->nullable) { 195 | return "->nullable()"; 196 | } 197 | return ''; 198 | } 199 | 200 | /** 201 | * Mark if the column is nullable. 202 | * 203 | * @param $column 204 | * @return string 205 | */ 206 | protected function appendColumnDefault($column) 207 | { 208 | if ($column->default !== null) { 209 | return "->default($column->default)"; 210 | } 211 | return ''; 212 | } 213 | 214 | /** 215 | * Creates the base template for a migration file. 216 | * 217 | * @return string 218 | */ 219 | public function init() 220 | { 221 | return 222 | "table->name) . "Table extends Migration\n" . 228 | "{\n" . 229 | "\n" . 230 | " /**\n" . 231 | " * Run the migrations.\n" . 232 | " *\n" . 233 | " * @return void\n" . 234 | " */\n" . 235 | " public function up()\n" . 236 | " {\n" . 237 | " Schema::create(\"{$this->table->name}\", function (Blueprint \$table) {\n"; 238 | } 239 | 240 | /** 241 | * Closes out the migration file. 242 | * 243 | * @return string 244 | */ 245 | public function close() 246 | { 247 | return " });\n }\n}"; 248 | } 249 | } -------------------------------------------------------------------------------- /src/commands/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cytracom/laravel-migration-squasher/ff80d15c8ed30b42580ea9fe5108a5d6bb8a2e07/src/commands/.gitkeep -------------------------------------------------------------------------------- /src/commands/SquashMigrations.php: -------------------------------------------------------------------------------- 1 | option('path'), $this->option('output'), $this->option('move-to')))->squash(); 41 | } 42 | 43 | /** 44 | * Get the console command arguments. 45 | * 46 | * @return array 47 | */ 48 | protected function getArguments() 49 | { 50 | return array(); 51 | } 52 | 53 | /** 54 | * Get the console command options. 55 | * 56 | * @return array 57 | */ 58 | protected function getOptions() 59 | { 60 | return array( 61 | array('path', 'p', InputOption::VALUE_OPTIONAL, 'The path to the migrations folder', 62 | 'app/database/migrations'), 63 | array('output', 'o', InputOption::VALUE_OPTIONAL, 'The path to the output folder of squashes', 64 | 'app/tests/migrations'), 65 | array('move-to', 'mv', InputOption::VALUE_OPTIONAL, 'The path where old migrations will be moved.', 66 | 'app/database/migrations') 67 | ); 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /src/database/Column.php: -------------------------------------------------------------------------------- 1 | name = $name; 79 | $this->type = $type; 80 | $this->unsigned = $unsigned; 81 | $this->parameters = $size; 82 | $this->unique = $unique; 83 | $this->nullable = $nullable; 84 | $this->default = $default; 85 | } 86 | } -------------------------------------------------------------------------------- /src/database/Relationship.php: -------------------------------------------------------------------------------- 1 | tableColumn = $tblCol; 46 | $this->relationshipColumn = $relCol; 47 | $this->relationshipTable = $relTbl; 48 | } 49 | } -------------------------------------------------------------------------------- /src/database/Table.php: -------------------------------------------------------------------------------- 1 | name = $tableName; 62 | $this->columns = []; 63 | $this->relationships = []; 64 | $this->engine = $engine; 65 | } 66 | 67 | /** 68 | * Inserts the given column into the table. 69 | * 70 | * @param Column $col 71 | */ 72 | public function addColumn(Column $col) 73 | { 74 | $this->columns[$col->name] = $col; 75 | } 76 | 77 | /** 78 | * Changes the given columns attribute (key) to the given value. 79 | * 80 | * @param $columnName 81 | * @param $key 82 | * @param $value 83 | */ 84 | public function alterColumn($columnName, $key, $value) 85 | { 86 | $this->columns[$columnName]->{$key} = $value; 87 | } 88 | 89 | /** 90 | * Removes the column from the table. 91 | * 92 | * @param $columnName 93 | */ 94 | public function dropColumn($columnName) 95 | { 96 | unset($this->columns[$columnName]); 97 | } 98 | 99 | /** 100 | * Returns an array of all of the columns in the table. Columns are indexed by their column name. 101 | * TODO: Make a better system for indexing columns (affects any operations on columns) 102 | * 103 | * @return array 104 | */ 105 | public function getColumns() 106 | { 107 | return $this->columns; 108 | } 109 | 110 | /** 111 | * Returns the column with the given column name. 112 | * 113 | * @param $columnName 114 | * @return mixed 115 | */ 116 | public function getColumn($columnName) 117 | { 118 | return $this->columns[$columnName]; 119 | } 120 | 121 | /** 122 | * Returns true or false if the column exists or not. 123 | * 124 | * @param $columnName 125 | * @return bool 126 | */ 127 | public function hasColumn($columnName) 128 | { 129 | return isset($this->columns[$columnName]) ? true : false; 130 | } 131 | 132 | /** 133 | * Set this table's database engine. 134 | * 135 | * @param string $engine 136 | */ 137 | public function setEngine($engine) 138 | { 139 | $this->engine = $engine; 140 | } 141 | 142 | /** 143 | * Get this table's database engine. 144 | * 145 | * @return string 146 | */ 147 | public function getEngine() 148 | { 149 | return $this->engine; 150 | } 151 | 152 | /** 153 | * Add the relationship to the table. 154 | * 155 | * @param Relationship $rel 156 | */ 157 | public function addRelationship(Relationship $rel) 158 | { 159 | $this->relationships[$this->getRelationshipName($rel)] = $rel; 160 | } 161 | 162 | /** 163 | * Get the relationship from the table. 164 | * Relationships follow laravel's FK naming convention ('table_name'_'column_name'_foreign). 165 | * 166 | * @param Relationship $rel 167 | * @return string 168 | */ 169 | protected function getRelationshipName(Relationship $rel) 170 | { 171 | return $this->name . "_" . $rel->tableColumn . "_foreign"; 172 | } 173 | 174 | /** 175 | * Drop a foreign key using the foreign key name. 176 | * 177 | * @param $fkName 178 | */ 179 | public function dropRelationship($fkName) 180 | { 181 | unset($this->relationships[$fkName]); 182 | } 183 | 184 | /** 185 | * Get an array of all of the relationship objects for this table. 186 | * 187 | * @return array 188 | */ 189 | public function getRelationships() 190 | { 191 | return $this->relationships; 192 | } 193 | 194 | /** 195 | * Retrieve the relationship object with the given foreign key. 196 | * 197 | * @param $relName 198 | * @return Relationship 199 | */ 200 | public function getRelationship($relName) 201 | { 202 | return $this->relationships[$relName]; 203 | } 204 | 205 | /** 206 | * Set the primary key for this table. 207 | * 208 | * @param $columnName 209 | */ 210 | public function setPrimaryKey($columnName) 211 | { 212 | $this->primary = $columnName; 213 | } 214 | 215 | /** 216 | * Get the primary key for this table. 217 | * 218 | * @return null|string 219 | */ 220 | public function getPrimaryKey() 221 | { 222 | return $this->primary; 223 | } 224 | } -------------------------------------------------------------------------------- /src/tests/ColumnTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('string', $col->type); 21 | $this->assertEquals('test', $col->name); 22 | $this->assertTrue($col->unsigned); 23 | $this->assertEquals(10, $col->parameters); 24 | $this->assertFalse($col->nullable); 25 | $this->assertFalse($col->unique); 26 | } 27 | } -------------------------------------------------------------------------------- /src/tests/MigrationSquasherTest.php: -------------------------------------------------------------------------------- 1 | squash(); 29 | 30 | $date = date('Y_m_d'); 31 | $file = preg_replace('/\n| /','',file_get_contents(__DIR__ . '/data/output/'.$date.'_000000_squashed_renamed_table.php')); 32 | $exp = preg_replace('/\n| /','',file_get_contents(__DIR__ . '/data/output/Expected.php')); 33 | $this->assertEquals($file, $exp); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /src/tests/RelationshipTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('person_id', $rel->tableColumn); 21 | $this->assertEquals('id', $rel->relationshipColumn); 22 | $this->assertEquals('person', $rel->relationshipTable); 23 | } 24 | } 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/tests/TableTest.php: -------------------------------------------------------------------------------- 1 | assertEquals("test_table", $tbl->name); 27 | $this->assertEquals("MyISAM", $tbl->getEngine()); 28 | } 29 | 30 | public function testAddColumn() 31 | { 32 | $tbl = new Table("test_table", 'MyISAM'); 33 | 34 | $tbl->addColumn($this->getMock('Cytracom\Squasher\Database\Column',[],['string','myColumn'])); 35 | $cols = $tbl->getColumns(); 36 | 37 | $this->assertEquals(1, count($cols)); 38 | } 39 | 40 | public function testGetColumn() 41 | { 42 | $tbl = new Table("test_table", 'MyISAM'); 43 | 44 | $tbl->addColumn($this->getMock('Cytracom\Squasher\Database\Column',[],['string','myColumn'])); 45 | $col = $tbl->getColumn("myColumn"); 46 | 47 | $this->assertEquals('string', $col->type); 48 | } 49 | 50 | public function testGetColumnInvalidColumnName() 51 | { 52 | $tbl = new Table("test_table", 'MyISAM'); 53 | 54 | $error = false; 55 | try{ 56 | $col = $tbl->getColumn("myBadColumn"); 57 | }catch(\Exception $e){ 58 | $error = true; 59 | } 60 | 61 | $this->assertTrue($error); 62 | } 63 | 64 | public function testDropColumn() 65 | { 66 | $tbl = new Table("test_table", 'MyISAM'); 67 | 68 | $tbl->addColumn($this->getMock('Cytracom\Squasher\Database\Column',[],['string','myColumn'])); 69 | 70 | $cols = $tbl->getColumns(); 71 | $this->assertEquals(1, count($cols)); 72 | 73 | $tbl->dropColumn('myColumn'); 74 | 75 | $cols = $tbl->getColumns(); 76 | $this->assertEquals(0, count($cols)); 77 | } 78 | 79 | public function testDropColumnInvalidColumnName() 80 | { 81 | $tbl = new Table("test_table", 'MyISAM'); 82 | 83 | $tbl->addColumn($this->getMock('Cytracom\Squasher\Database\Column',[],['string','myColumn'])); 84 | 85 | $cols = $tbl->getColumns(); 86 | $this->assertEquals(1, count($cols)); 87 | 88 | $tbl->dropColumn('bad column name'); 89 | 90 | $cols = $tbl->getColumns(); 91 | $this->assertEquals(1, count($cols)); 92 | } 93 | 94 | public function testAddRelationship() 95 | { 96 | $tbl = new Table("test_table", 'MyISAM'); 97 | 98 | $tbl->addRelationship($this->getMock('Cytracom\Squasher\Database\Relationship',[],['col_id','id','other_table'])); 99 | 100 | $rels = $tbl->getRelationships(); 101 | $this->assertEquals(1, count($rels)); 102 | } 103 | 104 | public function testGetRelationship() 105 | { 106 | $tbl = new Table("test_table", 'MyISAM'); 107 | 108 | $tbl->addRelationship($this->getMock('Cytracom\Squasher\Database\Relationship',[],['col_id','id','other_table'])); 109 | 110 | $rel = $tbl->getRelationship('test_table_col_id_foreign'); 111 | $this->assertEquals('col_id', $rel->tableColumn); 112 | $this->assertEquals('id', $rel->relationshipColumn); 113 | $this->assertEquals('other_table', $rel->relationshipTable); 114 | } 115 | 116 | public function testGetRelationshipInvalidRelationshipName() 117 | { 118 | $tbl = new Table("test_table", 'MyISAM'); 119 | 120 | $error = false; 121 | try{ 122 | $col = $tbl->getRelationship("myBadRelationship"); 123 | }catch(\Exception $e){ 124 | $error = true; 125 | } 126 | 127 | $this->assertTrue($error); 128 | } 129 | 130 | public function testDropRelationship() 131 | { 132 | $tbl = new Table("test_table", 'MyISAM'); 133 | 134 | $tbl->addRelationship($this->getMock('Cytracom\Squasher\Database\Relationship',[],['col_id','id','other_table'])); 135 | $tbl->dropRelationship('test_table_col_id_foreign'); 136 | $rels = $tbl->getRelationships(); 137 | $this->assertEquals(0, count($rels)); 138 | } 139 | 140 | public function testSetPrimaryKey() 141 | { 142 | $tbl = new Table("test_table", 'MyISAM'); 143 | 144 | $tbl->setPrimaryKey("primary test"); 145 | $this->assertEquals("primary test", $tbl->getPrimaryKey()); 146 | } 147 | 148 | } -------------------------------------------------------------------------------- /src/tests/data/MigrationTestData.php: -------------------------------------------------------------------------------- 1 | increments('thisWillBeDropped'); 18 | }); 19 | Schema::drop("droppedSchema"); 20 | Schema::create("test", function (Blueprint $table) { 21 | $table->string("test"); 22 | $table->string("size",2); 23 | $table->integer("int"); 24 | $table->smallInteger("smallInt"); 25 | $table->bigInteger("bigInt"); 26 | $table->increments("inc"); 27 | $table->bigIncrements("bigInc"); 28 | $table->binary("bin"); 29 | $table->nullableTimestamps(); 30 | $table->softDeletes(); 31 | $table->dropSoftDeletes(); 32 | $table->softDeletes(); 33 | $table->boolean("bool")->default(true); 34 | $table->date("dte"); 35 | $table->double("doub"); 36 | $table->decimal("deci"); 37 | $table->engine = "TestEngine"; 38 | $table->float("flat"); 39 | $table->float("unsigned and unique")->unsigned()->unique(); 40 | $table->integer("to_be_dropped"); 41 | $table->unsignedBigInteger('ubigInt', true)->unique(); 42 | $table->unsignedInteger('uint'); 43 | $table->text('txt'); 44 | $table->mediumText('medtext'); 45 | $table->mediumInteger('medint'); 46 | $table->longText('longText'); 47 | $table->dropColumn("to_be_dropped"); 48 | $table->enum('testEnum1', ['val1','val2','val3']); 49 | $table->enum('testEnum2', array('val1','val2','val3')); 50 | 51 | 52 | DB::update('ALTER TABLE `test` MODIFY COLUMN `longText` int(11);'); 53 | DB::update('ALTER TABLE test MODIFY COLUMN `bin` BLOB(5000);'); 54 | DB::update('ALTER TABLE `test` MODIFY COLUMN doub double(11,12);'); 55 | DB::update('ALTER TABLE test MODIFY COLUMN bigInc bigint(11) AUTO_INCREMENT;'); 56 | DB::update('ALTER TABLE `test` MODIFY COLUMN `test` string(255);'); 57 | DB::update('ALTER TABLE `test` MODIFY COLUMN `dte` datetime NULL AUTO_INCREMENT;'); 58 | }); 59 | Schema::rename('test','renamed'); 60 | } 61 | } -------------------------------------------------------------------------------- /src/tests/data/output/Expected.php: -------------------------------------------------------------------------------- 1 | string('test', 255)->nullable(); 18 | $table->string('size', 2); 19 | $table->integer('int'); 20 | $table->smallInteger('smallInt'); 21 | $table->bigInteger('bigInt'); 22 | $table->increments('inc'); 23 | $table->bigIncrements('bigInc')->nullable(); 24 | $table->binary('bin')->nullable(); 25 | $table->boolean('bool')->default(true); 26 | $table->datetime('dte')->nullable(); 27 | $table->double('doub', 11,12)->nullable(); 28 | $table->decimal('deci'); 29 | $table->float('flat'); 30 | $table->float('unsigned and unique')->unsigned()->unique(); 31 | $table->unsignedBigInteger('ubigInt', true)->unique(); 32 | $table->unsignedInteger('uint'); 33 | $table->text('txt'); 34 | $table->mediumText('medtext'); 35 | $table->mediumInteger('medint'); 36 | $table->integer('longText')->nullable(); 37 | $table->enum('testEnum1', ['val1','val2','val3']); 38 | $table->enum('testEnum2', array('val1','val2','val3')); 39 | $table->nullableTimestamps(); 40 | $table->softDeletes(); 41 | $table->engine = 'TestEngine'; 42 | }); 43 | } 44 | } --------------------------------------------------------------------------------