Skip to content

Commit 7dd0580

Browse files
authored
Fix handling of executed migrations with the withDatetimeMicroseconds driver option (#67)
2 parents c1712b6 + 7532343 commit 7dd0580

7 files changed

Lines changed: 202 additions & 27 deletions

File tree

‎psalm-baseline.xml‎

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -222,22 +222,9 @@
222222
<MixedAssignment>
223223
<code><![CDATA[$migrationData]]></code>
224224
<code><![CDATA[$migrationData]]></code>
225-
<code><![CDATA[$schema]]></code>
226225
</MixedAssignment>
227226
<MixedMethodCall>
228227
<code><![CDATA[count]]></code>
229-
<code><![CDATA[datetime]]></code>
230-
<code><![CDATA[datetime]]></code>
231-
<code><![CDATA[datetime]]></code>
232-
<code><![CDATA[datetime]]></code>
233-
<code><![CDATA[dropIndex]]></code>
234-
<code><![CDATA[hasIndex]]></code>
235-
<code><![CDATA[index]]></code>
236-
<code><![CDATA[nullable]]></code>
237-
<code><![CDATA[primary]]></code>
238-
<code><![CDATA[save]]></code>
239-
<code><![CDATA[string]]></code>
240-
<code><![CDATA[unique]]></code>
241228
<code><![CDATA[where]]></code>
242229
</MixedMethodCall>
243230
<MoreSpecificReturnType>
@@ -254,7 +241,6 @@
254241
<code><![CDATA[select]]></code>
255242
</TooManyArguments>
256243
<UndefinedInterfaceMethod>
257-
<code><![CDATA[getSchema]]></code>
258244
<code><![CDATA[select]]></code>
259245
</UndefinedInterfaceMethod>
260246
<UnnecessaryVarAnnotation>

‎src/Migrator.php‎

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Cycle\Database\DatabaseInterface;
99
use Cycle\Database\DatabaseManager;
1010
use Cycle\Database\DatabaseProviderInterface;
11+
use Cycle\Database\Schema\AbstractTable;
1112
use Cycle\Database\Table;
1213
use Cycle\Migrations\Config\MigrationConfig;
1314
use Cycle\Migrations\Exception\MigrationException;
@@ -62,19 +63,12 @@ public function configure(): void
6263
}
6364

6465
foreach ($this->getDatabases() as $db) {
65-
$schema = $db->table($this->config->getTable())->getSchema();
66+
$table = $db->table($this->config->getTable());
67+
\assert($table instanceof Table);
68+
$schema = $table->getSchema();
6669

6770
// Schema update will automatically sync all needed data
68-
$schema->primary('id');
69-
$schema->string('migration', 191)->nullable(false);
70-
$schema->datetime('time_executed')->datetime();
71-
$schema->datetime('created_at')->datetime();
72-
$schema->index(['migration', 'created_at'])
73-
->unique(true);
74-
75-
if ($schema->hasIndex(['migration'])) {
76-
$schema->dropIndex(['migration']);
77-
}
71+
$this->declareMigrationTableSchema($schema);
7872

7973
$schema->save();
8074
}
@@ -229,7 +223,16 @@ protected function checkMigrationTableStructure(Database $db): bool
229223
}
230224
}
231225

232-
return !(!$table->hasIndex(['migration', 'created_at']));
226+
if (!$table->hasIndex(['migration', 'created_at'])) {
227+
return false;
228+
}
229+
230+
// The table may have been created by a previous version of the package
231+
// with a different column definition (e.g. datetime precision).
232+
$schema = $table->getSchema();
233+
$this->declareMigrationTableSchema($schema);
234+
235+
return !$schema->getComparator()->hasChanges();
233236
}
234237

235238
/**
@@ -242,7 +245,7 @@ protected function fetchMigrationData(MigrationInterface $migration): ?array
242245
->where(
243246
[
244247
'migration' => $migration->getState()->getName(),
245-
'created_at' => $this->getMigrationCreatedAtForDb($migration)->format(self::DB_DATE_FORMAT),
248+
'created_at' => $this->getMigrationCreatedAtForDb($migration),
246249
],
247250
)
248251
->run()
@@ -307,6 +310,26 @@ protected function getMigrationCreatedAtForDb(MigrationInterface $migration): \D
307310
);
308311
}
309312

313+
/**
314+
* Declare the desired structure of the migration table on the given schema.
315+
*/
316+
private function declareMigrationTableSchema(AbstractTable $schema): void
317+
{
318+
$schema->primary('id');
319+
$schema->string('migration', 191)->nullable(false);
320+
// Second precision is enough for migrations; size is set to keep the column
321+
// type compatible with the `withDatetimeMicroseconds` driver option
322+
// (on SQL Server the legacy DATETIME type rejects values with microseconds)
323+
$schema->datetime('time_executed')->datetime(6);
324+
$schema->datetime('created_at')->datetime(6);
325+
$schema->index(['migration', 'created_at'])
326+
->unique(true);
327+
328+
if ($schema->hasIndex(['migration'])) {
329+
$schema->dropIndex(['migration']);
330+
}
331+
}
332+
310333
/**
311334
* @return iterable<DatabaseInterface>
312335
*/
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\Migrations\Tests;
6+
7+
use Cycle\Database\Driver\DriverInterface;
8+
use Cycle\Migrations\Migration;
9+
use Cycle\Migrations\State;
10+
use Psr\Log\LoggerAwareInterface;
11+
12+
/**
13+
* The Migrator must correctly resolve the state of executed migrations
14+
* when the driver is configured with the `withDatetimeMicroseconds` option.
15+
*
16+
* @see https://github.com/cycle/migrations/issues/66
17+
*/
18+
abstract class DatetimeMicrosecondsTest extends BaseTest
19+
{
20+
public function getDriver(): DriverInterface
21+
{
22+
if (!isset($this->driver)) {
23+
$config = clone self::$config[static::DRIVER];
24+
$config->options['withDatetimeMicroseconds'] = true;
25+
26+
$this->driver = $config->driver::create($config);
27+
}
28+
29+
if (self::$config['debug'] && $this->driver instanceof LoggerAwareInterface) {
30+
$this->driver->setLogger(new TestLogger());
31+
}
32+
33+
return $this->driver;
34+
}
35+
36+
public function testMigrationResolvedAsExecutedAfterRun(): void
37+
{
38+
$this->migrator->configure();
39+
40+
$schema = $this->schema('sample');
41+
$schema->primary('id');
42+
$schema->integer('value');
43+
$this->atomize('migration1', [$schema]);
44+
45+
$migration = $this->migrator->run();
46+
47+
$this->assertInstanceOf(Migration::class, $migration);
48+
$this->assertSame(State::STATUS_EXECUTED, $migration->getState()->getStatus());
49+
}
50+
51+
public function testSecondRunHasNothingToExecute(): void
52+
{
53+
$this->migrator->configure();
54+
55+
$schema = $this->schema('sample');
56+
$schema->primary('id');
57+
$schema->integer('value');
58+
$this->atomize('migration1', [$schema]);
59+
60+
$this->migrator->run();
61+
62+
// The only migration has been executed, nothing is pending
63+
$this->assertNull($this->migrator->run());
64+
}
65+
66+
public function testUpgradeFromLegacyTableStructure(): void
67+
{
68+
// The migration table as it was created by previous versions of the package:
69+
// datetime columns without precision
70+
$schema = $this->db->table('migrations')->getSchema();
71+
$schema->primary('id');
72+
$schema->string('migration', 191)->nullable(false);
73+
$schema->datetime('time_executed')->datetime();
74+
$schema->datetime('created_at')->datetime();
75+
$schema->index(['migration', 'created_at'])->unique(true);
76+
$schema->save();
77+
78+
$this->migrator->configure();
79+
$this->assertTrue($this->migrator->isConfigured());
80+
81+
$schema = $this->schema('sample');
82+
$schema->primary('id');
83+
$schema->integer('value');
84+
$this->atomize('migration1', [$schema]);
85+
86+
$migration = $this->migrator->run();
87+
88+
$this->assertInstanceOf(Migration::class, $migration);
89+
$this->assertSame(State::STATUS_EXECUTED, $migration->getState()->getStatus());
90+
$this->assertNull($this->migrator->run());
91+
}
92+
93+
public function testRollbackAfterRun(): void
94+
{
95+
$this->migrator->configure();
96+
97+
$schema = $this->schema('sample');
98+
$schema->primary('id');
99+
$schema->integer('value');
100+
$this->atomize('migration1', [$schema]);
101+
102+
$this->migrator->run();
103+
$this->assertTrue($this->db->hasTable('sample'));
104+
105+
$migration = $this->migrator->rollback();
106+
107+
$this->assertInstanceOf(Migration::class, $migration);
108+
$this->assertFalse($this->db->hasTable('sample'));
109+
}
110+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\Migrations\Tests\MySQL;
6+
7+
/**
8+
* @group driver
9+
* @group driver-mysql
10+
*/
11+
class DatetimeMicrosecondsTest extends \Cycle\Migrations\Tests\DatetimeMicrosecondsTest
12+
{
13+
public const DRIVER = 'mysql';
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\Migrations\Tests\Postgres;
6+
7+
/**
8+
* @group driver
9+
* @group driver-postgres
10+
*/
11+
class DatetimeMicrosecondsTest extends \Cycle\Migrations\Tests\DatetimeMicrosecondsTest
12+
{
13+
public const DRIVER = 'postgres';
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\Migrations\Tests\SQLServer;
6+
7+
/**
8+
* @group driver
9+
* @group driver-sqlserver
10+
*/
11+
class DatetimeMicrosecondsTest extends \Cycle\Migrations\Tests\DatetimeMicrosecondsTest
12+
{
13+
public const DRIVER = 'sqlserver';
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\Migrations\Tests\SQLite;
6+
7+
/**
8+
* @group driver
9+
* @group driver-sqlite
10+
*/
11+
class DatetimeMicrosecondsTest extends \Cycle\Migrations\Tests\DatetimeMicrosecondsTest
12+
{
13+
public const DRIVER = 'sqlite';
14+
}

0 commit comments

Comments
 (0)