Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions config/migration-snapshot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

return [
/*
|--------------------------------------------------------------------------
| Which environments to implicitly dump/load
|--------------------------------------------------------------------------
|
| Comma separated list of environments which are safe to implicitly dump or
| load when executing `php artisan migrate`.
|
*/

'environments' => env('MIGRATION_SNAPSHOT_ENVIRONMENTS', 'development,local,testing'),

/*
|--------------------------------------------------------------------------
| Whether to reorder the `migrations` rows for consistency.
|--------------------------------------------------------------------------
|
| The order migrations are applied in development may vary from person to
| person, especially as they are created in parallel. This option reorders
| the migration records for consistency so the output file can be managed
| in source control.
|
| If the order migrations are applied will produce significant differences,
| such as changing the behavior of the app, then this should be left
| disabled. In such cases `migrate:fresh --database=test` followed by
| `migrate` or `migrate:dump` can achieve similar consistency.
|
*/

'reorder' => env('MIGRATION_SNAPSHOT_REORDER', false),
];
90 changes: 76 additions & 14 deletions src/Commands/MigrateDumpCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,35 @@ public function handle()
$this->info('Dumped schema');
}

private static function reorderMigrationRows(array $output) : array
{
if (config('migration-snapshot.reorder')) {
$reordered = [];
$new_id = 1;
foreach ($output as $line) {
// Extract parts of "INSERT ... VALUES ([id],'[ver]',[batch])
// where version begins with "YYYY_MM_DD_HHMMSS".
$occurrences = preg_match(
"/^(.*?VALUES\s*)\([0-9]+,\s*'([0-9_]{17})(.*?),\s*[0-9]+\s*\)\s*;\s*$/iu",
$line,
$m
);
if (1 !== $occurrences) {
throw new \UnexpectedValueException(
'Only insert rows supported:' . PHP_EOL . var_export($line, 1)
);
}
// Reassemble parts with new values and index by timestamp of
// version string to sort.
$reordered[$m[2]] = "$m[1]($new_id,'$m[2]$m[3],0);";
$new_id += 1;
}
return $reordered;
}

return $output;
}

/**
* @param array $db_config like ['host' => , 'port' => ].
* @param string $schema_sql_path like '.../schema.sql'
Expand Down Expand Up @@ -101,13 +130,22 @@ private static function mysqlDump(array $db_config, string $schema_sql_path) : i
}

// Include migration rows to avoid unnecessary reruns conflicting.
// CONSIDER: How this could be done as consistent snapshot with
// dump of structure, and avoid duplicate "SET" comments.
passthru(
$command_prefix . ' migrations --no-create-info --skip-extended-insert >> '
. escapeshellarg($schema_sql_path),
exec(
$command_prefix . ' migrations --no-create-info --skip-extended-insert --compact',
$output,
$exit_code
);
if (0 !== $exit_code) {
return $exit_code;
}

$output = self::reorderMigrationRows($output);

file_put_contents(
$schema_sql_path,
implode(PHP_EOL, $output),
FILE_APPEND
);

return $exit_code;
}
Expand All @@ -130,7 +168,6 @@ private static function pgsqlDump(array $db_config, string $schema_sql_path) : i
. ' --port=' . escapeshellarg($db_config['port'])
. ' --username=' . escapeshellarg($db_config['username'])
. ' ' . escapeshellarg($db_config['database']);
// TODO: Suppress warning about insecure password.
passthru(
$command_prefix
. ' --file=' . escapeshellarg($schema_sql_path)
Expand All @@ -142,13 +179,22 @@ private static function pgsqlDump(array $db_config, string $schema_sql_path) : i
}

// Include migration rows to avoid unnecessary reruns conflicting.
// CONSIDER: How this could be done as consistent snapshot with
// dump of structure, and avoid duplicate "SET" comments.
passthru(
$command_prefix . ' --table=migrations --data-only --inserts >> '
. escapeshellarg($schema_sql_path),
exec(
$command_prefix . ' --table=migrations --data-only --inserts',
$output,
$exit_code
);
if (0 !== $exit_code) {
return $exit_code;
}

$output = self::reorderMigrationRows($output);

file_put_contents(
$schema_sql_path,
implode(PHP_EOL, $output),
FILE_APPEND
);

return $exit_code;
}
Expand All @@ -167,24 +213,40 @@ private static function sqliteDump(array $db_config, string $schema_sql_path) :
// Since Sqlite lacks Information Schema, and dumping everything may be
// too slow or memory intense, just query tables and dump them
// individually.
// CONSIDER: Using Laravel's `Schema` code instead.
exec($command_prefix . ' .tables', $output, $exit_code);
if (0 !== $exit_code) {
return $exit_code;
}
$tables = preg_split('/\s+/', implode(' ', $output));

file_put_contents($schema_sql_path, '');

foreach ($tables as $table) {
// Only migrations should dump data with schema.
$sql_command = 'migrations' === $table ? '.dump' : '.schema';

passthru(
$command_prefix . ' ' . escapeshellarg("$sql_command $table")
. ' >> ' . escapeshellarg($schema_sql_path),
$output = [];
exec(
$command_prefix . ' ' . escapeshellarg("$sql_command $table"),
$output,
$exit_code
);
if (0 !== $exit_code) {
return $exit_code;
}

if ('migrations' === $table) {
$insert_rows = array_slice($output, 4, -1);
$sorted = self::reorderMigrationRows($insert_rows);
array_splice($output, 4, -1, $sorted);
}

file_put_contents(
$schema_sql_path,
implode(PHP_EOL, $output) . PHP_EOL,
FILE_APPEND
);
}

return $exit_code;
Expand Down
5 changes: 2 additions & 3 deletions src/Handlers/MigrateFinishedHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ public function handle(CommandFinished $event)
'migrate' === $event->command // CONSIDER: Also `migrate:fresh`.
&& ! $event->input->hasParameterOption(['--help', '--pretend', '-V', '--version'])
&& env('MIGRATION_SNAPSHOT', true)
// CONSIDER: Making configurable blacklist of environments.
&& 'production' !== app()->environment()
&& in_array(app()->environment(), explode(',', config('migration-snapshot.environments')), true)
) {
$options = MigrateStartingHandler::inputToArtisanOptions($event->input);
$database = $options['--database'] ?? env('DB_CONNECTION');
Expand All @@ -28,4 +27,4 @@ public function handle(CommandFinished $event)
\Artisan::call('migrate:dump', $options, $event->output);
}
}
}
}
5 changes: 2 additions & 3 deletions src/Handlers/MigrateStartingHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ public function handle(CommandStarting $event)
&& env('MIGRATION_SNAPSHOT', true) // CONSIDER: Config option.
// Never implicitly load fresh (from file) in production since it
// would need to drop first, and that would be destructive.
// CONSIDER: Making configurable blacklist of environments.
&& 'production' !== app()->environment()
&& in_array(app()->environment(), explode(',', config('migration-snapshot.environments')), true)
// No point in implicitly loading when it's not present.
&& file_exists(database_path() . MigrateDumpCommand::SCHEMA_SQL_PATH_SUFFIX)
) {
Expand Down Expand Up @@ -149,4 +148,4 @@ private static function inputValidateWorkaround($input) : bool

return true;
}
}
}
8 changes: 7 additions & 1 deletion src/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ class ServiceProvider extends \Illuminate\Support\ServiceProvider
public function boot()
{
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__ . '/../config/migration-snapshot.php' => config_path('migration-snapshot.php'),
], 'config');

$this->commands([
MigrateDumpCommand::class,
MigrateLoadCommand::class,
Expand All @@ -19,6 +23,8 @@ public function boot()

public function register()
{
$this->mergeConfigFrom(__DIR__ . '/../config/migration-snapshot.php', 'migration-snapshot');

$this->app->register(EventServiceProvider::class);
}
}
}
2 changes: 1 addition & 1 deletion tests/MigrateHookTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@ public function test_handle_doesNotLoadWhenDbHasMigrated()

$this->assertEquals(1, \DB::table('test_ms')->count());
}
}
}
2 changes: 1 addition & 1 deletion tests/Sqlite/MigrateLoadTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ public function test_handle()

$this->assertNull(\DB::table('test_ms')->value('name'));
}
}
}