diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index 2f5264a..0ce6c89 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -222,22 +222,9 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -254,7 +241,6 @@
-
diff --git a/src/Migrator.php b/src/Migrator.php
index 22dd179..63b4ce5 100644
--- a/src/Migrator.php
+++ b/src/Migrator.php
@@ -8,6 +8,7 @@
use Cycle\Database\DatabaseInterface;
use Cycle\Database\DatabaseManager;
use Cycle\Database\DatabaseProviderInterface;
+use Cycle\Database\Schema\AbstractTable;
use Cycle\Database\Table;
use Cycle\Migrations\Config\MigrationConfig;
use Cycle\Migrations\Exception\MigrationException;
@@ -62,19 +63,12 @@ public function configure(): void
}
foreach ($this->getDatabases() as $db) {
- $schema = $db->table($this->config->getTable())->getSchema();
+ $table = $db->table($this->config->getTable());
+ \assert($table instanceof Table);
+ $schema = $table->getSchema();
// Schema update will automatically sync all needed data
- $schema->primary('id');
- $schema->string('migration', 191)->nullable(false);
- $schema->datetime('time_executed')->datetime();
- $schema->datetime('created_at')->datetime();
- $schema->index(['migration', 'created_at'])
- ->unique(true);
-
- if ($schema->hasIndex(['migration'])) {
- $schema->dropIndex(['migration']);
- }
+ $this->declareMigrationTableSchema($schema);
$schema->save();
}
@@ -229,7 +223,16 @@ protected function checkMigrationTableStructure(Database $db): bool
}
}
- return !(!$table->hasIndex(['migration', 'created_at']));
+ if (!$table->hasIndex(['migration', 'created_at'])) {
+ return false;
+ }
+
+ // The table may have been created by a previous version of the package
+ // with a different column definition (e.g. datetime precision).
+ $schema = $table->getSchema();
+ $this->declareMigrationTableSchema($schema);
+
+ return !$schema->getComparator()->hasChanges();
}
/**
@@ -242,7 +245,7 @@ protected function fetchMigrationData(MigrationInterface $migration): ?array
->where(
[
'migration' => $migration->getState()->getName(),
- 'created_at' => $this->getMigrationCreatedAtForDb($migration)->format(self::DB_DATE_FORMAT),
+ 'created_at' => $this->getMigrationCreatedAtForDb($migration),
],
)
->run()
@@ -307,6 +310,26 @@ protected function getMigrationCreatedAtForDb(MigrationInterface $migration): \D
);
}
+ /**
+ * Declare the desired structure of the migration table on the given schema.
+ */
+ private function declareMigrationTableSchema(AbstractTable $schema): void
+ {
+ $schema->primary('id');
+ $schema->string('migration', 191)->nullable(false);
+ // Second precision is enough for migrations; size is set to keep the column
+ // type compatible with the `withDatetimeMicroseconds` driver option
+ // (on SQL Server the legacy DATETIME type rejects values with microseconds)
+ $schema->datetime('time_executed')->datetime(6);
+ $schema->datetime('created_at')->datetime(6);
+ $schema->index(['migration', 'created_at'])
+ ->unique(true);
+
+ if ($schema->hasIndex(['migration'])) {
+ $schema->dropIndex(['migration']);
+ }
+ }
+
/**
* @return iterable
*/
diff --git a/tests/Migrations/DatetimeMicrosecondsTest.php b/tests/Migrations/DatetimeMicrosecondsTest.php
new file mode 100644
index 0000000..e5aeaa5
--- /dev/null
+++ b/tests/Migrations/DatetimeMicrosecondsTest.php
@@ -0,0 +1,110 @@
+driver)) {
+ $config = clone self::$config[static::DRIVER];
+ $config->options['withDatetimeMicroseconds'] = true;
+
+ $this->driver = $config->driver::create($config);
+ }
+
+ if (self::$config['debug'] && $this->driver instanceof LoggerAwareInterface) {
+ $this->driver->setLogger(new TestLogger());
+ }
+
+ return $this->driver;
+ }
+
+ public function testMigrationResolvedAsExecutedAfterRun(): void
+ {
+ $this->migrator->configure();
+
+ $schema = $this->schema('sample');
+ $schema->primary('id');
+ $schema->integer('value');
+ $this->atomize('migration1', [$schema]);
+
+ $migration = $this->migrator->run();
+
+ $this->assertInstanceOf(Migration::class, $migration);
+ $this->assertSame(State::STATUS_EXECUTED, $migration->getState()->getStatus());
+ }
+
+ public function testSecondRunHasNothingToExecute(): void
+ {
+ $this->migrator->configure();
+
+ $schema = $this->schema('sample');
+ $schema->primary('id');
+ $schema->integer('value');
+ $this->atomize('migration1', [$schema]);
+
+ $this->migrator->run();
+
+ // The only migration has been executed, nothing is pending
+ $this->assertNull($this->migrator->run());
+ }
+
+ public function testUpgradeFromLegacyTableStructure(): void
+ {
+ // The migration table as it was created by previous versions of the package:
+ // datetime columns without precision
+ $schema = $this->db->table('migrations')->getSchema();
+ $schema->primary('id');
+ $schema->string('migration', 191)->nullable(false);
+ $schema->datetime('time_executed')->datetime();
+ $schema->datetime('created_at')->datetime();
+ $schema->index(['migration', 'created_at'])->unique(true);
+ $schema->save();
+
+ $this->migrator->configure();
+ $this->assertTrue($this->migrator->isConfigured());
+
+ $schema = $this->schema('sample');
+ $schema->primary('id');
+ $schema->integer('value');
+ $this->atomize('migration1', [$schema]);
+
+ $migration = $this->migrator->run();
+
+ $this->assertInstanceOf(Migration::class, $migration);
+ $this->assertSame(State::STATUS_EXECUTED, $migration->getState()->getStatus());
+ $this->assertNull($this->migrator->run());
+ }
+
+ public function testRollbackAfterRun(): void
+ {
+ $this->migrator->configure();
+
+ $schema = $this->schema('sample');
+ $schema->primary('id');
+ $schema->integer('value');
+ $this->atomize('migration1', [$schema]);
+
+ $this->migrator->run();
+ $this->assertTrue($this->db->hasTable('sample'));
+
+ $migration = $this->migrator->rollback();
+
+ $this->assertInstanceOf(Migration::class, $migration);
+ $this->assertFalse($this->db->hasTable('sample'));
+ }
+}
diff --git a/tests/Migrations/MySQL/DatetimeMicrosecondsTest.php b/tests/Migrations/MySQL/DatetimeMicrosecondsTest.php
new file mode 100644
index 0000000..569b616
--- /dev/null
+++ b/tests/Migrations/MySQL/DatetimeMicrosecondsTest.php
@@ -0,0 +1,14 @@
+