-
-
Notifications
You must be signed in to change notification settings - Fork 388
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Record when a migration was executed. #675
Conversation
52cb187
to
093347f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
9299575
to
ab0b4bd
Compare
@@ -23,6 +23,7 @@ | |||
'migrations_namespace', | |||
'table_name', | |||
'column_name', | |||
'created_at_column_name', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe executed_at_column_name
, and consequently, a rename of the corresponding methods (and fields), would be more appropriate?
After all, this reflects the time of when the migration was executed, rather then when the migration (file) was created.
What do you think?
fa07fd9
to
6fa5668
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! 👍
continue; | ||
} | ||
|
||
$table->dropColumn($column->getName()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understood correctly, then on the happy path we would not need to drop any columns from the definition - what do you think about modifying to
diff --git a/lib/Doctrine/Migrations/MigrationTableManager.php b/lib/Doctrine/Migrations/MigrationTableManager.php
index 175c2da..721116c 100644
--- a/lib/Doctrine/Migrations/MigrationTableManager.php
+++ b/lib/Doctrine/Migrations/MigrationTableManager.php
@@ -189,11 +189,9 @@ class MigrationTableManager
// remove columns from the table definition that we don't care about
// so we don't try to drop those columns
foreach ($table->getColumns() as $column) {
- if (in_array($column->getName(), $columnNames, true)) {
- continue;
+ if (!in_array($column->getName(), $columnNames, true)) {
+ $table->dropColumn($column->getName());
}
-
- $table->dropColumn($column->getName());
}
return $table;
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That goes against early-exit rule in conditions/loops in Doctrine CS. :)
@@ -42,6 +42,9 @@ class Configuration | |||
/** @var int */ | |||
private $migrationsColumnLength = 255; | |||
|
|||
/** @var string */ | |||
private $migrationsExecutedAtColumnName = 'executedAt'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably be executed_at
to match table name and also avoid case sensitivity mess.
{ | ||
return $this->getDependency(MigrationTableCreator::class, function () { | ||
return new MigrationTableCreator( | ||
return $this->getDependency(MigrationTableManager::class, function () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Closure needs a return type.
@@ -53,15 +64,24 @@ private function getVersionUpdateQuery(string $version, string $direction) : str | |||
{ | |||
if ($direction === VersionDirection::DOWN) { | |||
$query = "DELETE FROM %s WHERE %s = '%s';\n"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary variable.
} | ||
|
||
$query = "INSERT INTO %s (%s, %s) VALUES ('%s', %s);\n"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary variable.
/** | ||
* @internal | ||
*/ | ||
class MigrationTableManager |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason to not be final
? We should try to let people extend abstractions and not concrete implementations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was wracking my brain trying to think of an applicable name. Maybe MigrationTableCreator
? It also updates the table too so that name isn't 100% clear. I feel like MigrationTableManager
is a relevant name as it is managing the migration table :) Open to other ideas though for something better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lcobucci I had everything final to begin with, but that makes unit testing impossible. I think we need a standard solution for how to handle this in Doctrine projects before we start enforcing final everywhere.
|
||
public function isMigrationTableUpToDate() : bool | ||
{ | ||
if ($this->migrationTableUpToDate === null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inverting the condition will decrease the depth of this method.
} | ||
|
||
if ($this->isMigrationTableCreated()) { | ||
if ($this->isMigrationTableUpToDate() === false) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strict comparison seems unnecessary for boolean-only method.
lib/Doctrine/Migrations/Version.php
Outdated
@@ -194,4 +199,17 @@ public function markVersion(string $direction) : void | |||
); | |||
} | |||
} | |||
|
|||
private function getExecutedAt() : DateTime |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DateTimeImmutable please
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I don't think we need a method for this if we only care about the DB value
lib/Doctrine/Migrations/Version.php
Outdated
|
||
private function getExecutedAtDatabaseValue() : string | ||
{ | ||
return Type::getType('datetime')->convertToDatabaseValue( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
datetime_immutable please
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About the scrutinizer issue, I've submitted an issue and they're about to release a new version that works properly with assert()
so it might be an idea to use it here
{ | ||
return new Column( | ||
$this->executedAtColumnName, | ||
Type::getType('datetime'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
datetime_immutable please
ce34441
to
89f1c1b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jwage great job, it would be significantly easier to review if we had smaller commits though
|
||
public function isMigrationTableCreated() : bool | ||
{ | ||
if ($this->migrationTableCreated === null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inverting the condition would simplify things here as well
if ($this->migrationTableCreated === null) { | ||
$this->configuration->connect(); | ||
|
||
if ($this->schemaManager->tablesExist([$this->tableName])) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this is simpler than this if/else
:
$this->migrationTableCreated = $this->schemaManager->tablesExist([$this->tableName]);
private $platform; | ||
|
||
/** @var string */ | ||
private $tableName; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it might be time to have a class to represent the migration table, which encapsulates: name, column name (needs a better name), column length (better name as well?), and executed at column name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. I will make that change.
|
||
public function createMigrationTable() : bool | ||
{ | ||
$this->configuration->validate(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this is a bit too much for this method? I mean, the validation of the configuration and the check for dry-run mode could be done in a higher level. This would simplify things and maybe even remove the need of passing the configuration to this class
return $this->migrationTableUpToDate; | ||
} | ||
|
||
public function createMigrationTable() : bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason to have this returning a boolean? It would be amazing if we could keep away from the internal classes and rely on exceptions for error handling
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possibly. I will make an issue to revisit this and maybe implement exceptions for these cases instead of returning bool.
/** | ||
* @internal | ||
*/ | ||
class MigrationTableManager |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason to not be final
? We should try to let people extend abstractions and not concrete implementations
return true; | ||
} | ||
|
||
public function getMigrationsColumn() : Column |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good candidate for extraction (the class I've mentioned earlier 😄)
lib/Doctrine/Migrations/Version.php
Outdated
@@ -194,4 +199,17 @@ public function markVersion(string $direction) : void | |||
); | |||
} | |||
} | |||
|
|||
private function getExecutedAt() : DateTime |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I don't think we need a method for this if we only care about the DB value
lib/Doctrine/Migrations/Version.php
Outdated
|
||
private function getExecutedAtDatabaseValue() : string | ||
{ | ||
return Type::getType('datetime')->convertToDatabaseValue( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About the scrutinizer issue, I've submitted an issue and they're about to release a new version that works properly with assert()
so it might be an idea to use it here
@@ -34,6 +35,89 @@ public function testLoadChecksCurrentWorkingDirectory() : void | |||
chdir($cwd); | |||
} | |||
|
|||
public function testSetConfiguration() : void | |||
{ | |||
$fileConfiguration = $this->getMockBuilder(TestAbstractFileConfiguration::class) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This really hurts my eyes, but thanks for being pragmatic and facing this beast... Maybe using $this->createPartialMock()
might simplify things a bit
8881f9c
to
4a347e5
Compare
4a347e5
to
95485c6
Compare
|
||
public function getQuotedMigrationsExecutedAtColumnName() : string | ||
{ | ||
return $this->getDependencyFactory() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be honest, this thing is just a service locator now... Wiring everything should imho be done in the SF Bundle / Zend Module and/or in a factory/builder object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can be done, but not without a big BC break. The only place it is being used like a service locator is in places where I wanted to maintain BC. Like here. Also, these are all internal services so it feels weird to put the burden on the end user to wire all these classes up that they should not even know about or have access to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be clear, I am referring to developers who don't use Symfony, Zend, etc. and have their app integrated with Doctrine DBAL and Migrations. Today all that is required to configure migrations is setting up one service that is passed your DBAL connection:
$configuration = new Configuration($connection);
// configure your migrations
$configuration->...;
In 2.0 this is the same. All the existing setups will just continue to work. All the services that make up the internals are not meant to be public facing services. If I put the wiring in the framework integrations, wouldn't this mean that when I refactor the internals, it'd be a BC break in the framework integrations due to the wiring needing to be updated?
$this->upToDate = true; | ||
|
||
foreach ($this->migrationTable->getColumnNames() as $columnName) { | ||
if (! $table->hasColumn($columnName)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can be inverted with continue
|
||
$queries = $fromSchema->getMigrateToSql($toSchema, $this->platform); | ||
|
||
foreach ($queries as $query) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this actually run inside transaction sice there are multiple queries?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are DDL statements. I am honestly not sure of which databases today support rollbacks for DDL statements. Do you know? I guess it doesn't hurt and if the database supports it, it will work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From my experience, PostgreSQL definitely does, MySQL definitely does not.
More vendors documented here: https://wiki.postgresql.org/wiki/Transactional_DDL_in_PostgreSQL:_A_Competitive_Analysis
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added the transaction, it won't hurt 👍
46305a3
to
203db81
Compare
|
||
$queries = $fromSchema->getMigrateToSql($toSchema, $this->platform); | ||
|
||
$this->connection->beginTransaction(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be done only if the platform supports transational DDL IMO (which DBAL knows)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DBAL doesn't know this currently. It only has the supportsTransactions() method. We discussed submitting an enhancement to the DBAL for this though. For now though, we figured having this here for everyone wouldn't hurt. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah indeed, I though it was something known by DBAL and used by doctrine/migrations, but doctrine/migrations always wraps the migration in a transaction, even if it would not be able to rollback it fully due to non transactional DDL (and migrations can ask to disable the usage of the transaction).
So yeah, keep it like that.
c7d6f03
to
e08e9b1
Compare
e08e9b1
to
fb26b48
Compare
Summary
Log when a migration was executed.
TODO
MigrationTable
,MigrationTableStatus
andMigrationTableUpdater
class.