Skip to content

Commit 4b0a7ff

Browse files
authored
Merge pull request #12 from doesntmattr/feature/replay
Add a --replay option to ::execute to avoid mongodb unique index error
2 parents 5ebeb21 + cd1d6da commit 4b0a7ff

File tree

4 files changed

+131
-12
lines changed

4 files changed

+131
-12
lines changed

src/AntiMattr/MongoDB/Migrations/Tools/Console/Command/ExecuteCommand.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ protected function configure()
3333
->addArgument('version', InputArgument::REQUIRED, 'The version to execute.', null)
3434
->addOption('up', null, InputOption::VALUE_NONE, 'Execute the migration up.')
3535
->addOption('down', null, InputOption::VALUE_NONE, 'Execute the migration down.')
36+
->addOption('replay', null, InputOption::VALUE_NONE, 'Replay an \'up\' migration and avoid the duplicate exception.')
3637
->setHelp(<<<'EOT'
3738
The <info>%command.name%</info> command executes a single migration version up or down manually:
3839
@@ -59,12 +60,13 @@ public function execute(InputInterface $input, OutputInterface $output)
5960
{
6061
$version = $input->getArgument('version');
6162
$direction = $input->getOption('down') ? 'down' : 'up';
63+
$replay = $input->getOption('replay');
6264

6365
$configuration = $this->getMigrationConfiguration($input, $output);
6466
$version = $configuration->getVersion($version);
6567

6668
if (!$input->isInteractive()) {
67-
$version->execute($direction);
69+
$version->execute($direction, $replay);
6870
} else {
6971
$question = new ConfirmationQuestion(
7072
'<question>WARNING! You are about to execute a database migration that could result in data lost. Are you sure you wish to continue? (y/n)</question> ',
@@ -76,7 +78,7 @@ public function execute(InputInterface $input, OutputInterface $output)
7678
->ask($input, $output, $question);
7779

7880
if ($confirmation === true) {
79-
$version->execute($direction);
81+
$version->execute($direction, $replay);
8082
} else {
8183
$output->writeln('<error>Migration cancelled!</error>');
8284
}

src/AntiMattr/MongoDB/Migrations/Version.php

+26-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use AntiMattr\MongoDB\Migrations\Collection\Statistics;
1515
use AntiMattr\MongoDB\Migrations\Configuration\Configuration;
1616
use AntiMattr\MongoDB\Migrations\Exception\SkipException;
17+
use AntiMattr\MongoDB\Migrations\Exception\AbortException;
1718
use Doctrine\MongoDB\Collection;
1819
use Doctrine\MongoDB\Database;
1920
use Exception;
@@ -169,11 +170,18 @@ public function analyze(Collection $collection)
169170
* Execute this migration version up or down and and return the SQL.
170171
*
171172
* @param string $direction The direction to execute the migration
173+
* @param bool $replay If the migration is being replayed
172174
*
173175
* @throws \Exception when migration fails
174176
*/
175-
public function execute($direction)
177+
public function execute($direction, $replay = false)
176178
{
179+
if ('down' === $direction && $replay) {
180+
throw new AbortException(
181+
'Cannot run \'down\' and replay it. Use replay with \'up\''
182+
);
183+
}
184+
177185
try {
178186
$start = microtime(true);
179187

@@ -194,7 +202,7 @@ public function execute($direction)
194202
$this->updateStatisticsAfter();
195203

196204
if ($direction === 'up') {
197-
$this->markMigrated();
205+
$this->markMigrated($replay);
198206
} else {
199207
$this->markNotMigrated();
200208
}
@@ -276,13 +284,27 @@ public function executeScript(Database $db, $file)
276284
return $result;
277285
}
278286

279-
public function markMigrated()
287+
/**
288+
* markMigrated.
289+
*
290+
* @param bool $replay This is a replayed migration, do an update instead of an insert
291+
*/
292+
public function markMigrated($replay = false)
280293
{
281294
$this->configuration->createMigrationCollection();
282295
$collection = $this->configuration->getCollection();
283296

284297
$document = array('v' => $this->version, 't' => $this->createMongoTimestamp());
285-
$collection->insert($document);
298+
299+
if ($replay) {
300+
$query = array('v' => $this->version);
301+
// If the user asked for a 'replay' of a migration that
302+
// has not been run, it will be inserted anew
303+
$options = array('upsert' => true);
304+
$collection->update($query, $document, $options);
305+
} else {
306+
$collection->insert($document);
307+
}
286308
}
287309

288310
public function markNotMigrated()

tests/AntiMattr/Tests/MongoDB/Migrations/Tools/Console/Command/ExecuteCommandTest.php

+47-2
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public function testExecuteDownWithoutInteraction()
5959

6060
$this->version->expects($this->once())
6161
->method('execute')
62-
->with('down')
62+
->with('down', false)
6363
;
6464

6565
// Run command, run.
@@ -117,7 +117,52 @@ public function testExecuteUpWithInteraction()
117117

118118
$this->version->expects($this->once())
119119
->method('execute')
120-
->with('up')
120+
->with('up', false)
121+
;
122+
123+
// Run command, run.
124+
$this->command->run(
125+
$input,
126+
$this->output
127+
);
128+
}
129+
130+
public function testExecuteReplayWithoutInteraction()
131+
{
132+
// Variables and Objects
133+
$application = new Application();
134+
$numVersion = '11235713';
135+
$interactive = false;
136+
137+
// Arguments and Options
138+
$input = new ArgvInput(
139+
array(
140+
'application-name',
141+
ExecuteCommand::NAME,
142+
$numVersion,
143+
'--up',
144+
'--replay',
145+
)
146+
);
147+
148+
// Set properties on objects
149+
$this->command->setApplication($application);
150+
$this->command->setMigrationConfiguration($this->config);
151+
$input->setInteractive($interactive);
152+
153+
// Expectations
154+
$this->config->expects($this->once())
155+
->method('getVersion')
156+
->with($numVersion)
157+
->will(
158+
$this->returnValue($this->version)
159+
)
160+
;
161+
162+
$replay = true;
163+
$this->version->expects($this->once())
164+
->method('execute')
165+
->with('up', $replay)
121166
;
122167

123168
// Run command, run.

tests/AntiMattr/Tests/MongoDB/Migrations/VersionTest.php

+54-4
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ protected function setUp()
4747

4848
public function testConstructor()
4949
{
50-
$this->assertEquals($this->configuration, $this->version->getConfiguration());
51-
$this->assertEquals(Version::STATE_NONE, $this->version->getState());
52-
$this->assertEquals($this->versionName, $this->version->getVersion());
50+
$this->assertSame($this->configuration, $this->version->getConfiguration());
51+
$this->assertSame(Version::STATE_NONE, $this->version->getState());
52+
$this->assertSame($this->versionName, $this->version->getVersion());
5353
$this->assertEquals($this->versionName, (string) $this->version);
5454
$this->assertNotNull($this->version->getMigration());
5555
}
@@ -122,6 +122,36 @@ public function testMarkMigrated()
122122
$this->version->markMigrated();
123123
}
124124

125+
public function testMarkMigratedWithReplay()
126+
{
127+
$timestamp = $this->buildMock('MongoTimestamp');
128+
$this->version->setTimestamp($timestamp);
129+
130+
$collection = $this->buildMock('Doctrine\MongoDB\Collection');
131+
$this->configuration->expects($this->once())
132+
->method('createMigrationCollection');
133+
134+
$this->configuration->expects($this->once())
135+
->method('getCollection')
136+
->will($this->returnValue($collection));
137+
138+
$query = array(
139+
'v' => $this->versionName,
140+
);
141+
142+
$update = array(
143+
'v' => $this->versionName,
144+
't' => $timestamp,
145+
);
146+
147+
$collection->expects($this->once())
148+
->method('update')
149+
->with($query, $update);
150+
151+
$replay = true;
152+
$this->version->markMigrated($replay);
153+
}
154+
125155
public function testMarkNotMigrated()
126156
{
127157
$timestamp = $this->buildMock('MongoTimestamp');
@@ -200,12 +230,32 @@ public function testIsMigrated()
200230
$this->version->isMigrated();
201231
}
202232

233+
/**
234+
* @test
235+
*
236+
* testExecuteDownWithReplayThrowsException
237+
*
238+
* @expectedException \AntiMattr\MongoDB\Migrations\Exception\AbortException
239+
*/
240+
public function testExecuteDownWithReplayThrowsException()
241+
{
242+
// These methods will not be called
243+
$this->migration->expects($this->never())->method('down');
244+
$this->configuration->expects($this->never())
245+
->method('createMigrationCollection');
246+
$this->configuration->expects($this->never())
247+
->method('getCollection');
248+
249+
$replay = true;
250+
$this->version->execute('down', $replay);
251+
}
252+
203253
/**
204254
* @dataProvider provideDirection
205255
*/
206256
public function testExecuteThrowsSkipException($direction)
207257
{
208-
$expectedException = $this->buildMock('AntiMattr\MongoDB\Migrations\Exception\SkipException');
258+
$expectedException = new \AntiMattr\MongoDB\Migrations\Exception\SkipException();
209259

210260
$this->migration->expects($this->once())
211261
->method($direction)

0 commit comments

Comments
 (0)