Skip to content
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

Merged
merged 1 commit into from
May 16, 2018
Merged

Record when a migration was executed. #675

merged 1 commit into from
May 16, 2018

Conversation

jwage
Copy link
Member

@jwage jwage commented May 12, 2018

Q A
Type feature
BC Break no
Fixed issues #331

Summary

Log when a migration was executed.

TODO

  • Implement MigrationTable, MigrationTableStatus and MigrationTableUpdater class.
  • If the table does not exist, default notnull to true.
  • Show the executed at date in the console commands output.
  • Update documentation.

@jwage jwage added this to the 2.0 milestone May 12, 2018
@jwage jwage self-assigned this May 12, 2018
@jwage jwage force-pushed the migration-created-at branch from 52cb187 to 093347f Compare May 12, 2018 03:34
Copy link
Contributor

@localheinz localheinz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@jwage jwage force-pushed the migration-created-at branch 15 times, most recently from 9299575 to ab0b4bd Compare May 15, 2018 01:28
@@ -23,6 +23,7 @@
'migrations_namespace',
'table_name',
'column_name',
'created_at_column_name',
Copy link
Contributor

@localheinz localheinz May 15, 2018

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?

@jwage jwage force-pushed the migration-created-at branch 4 times, most recently from fa07fd9 to 6fa5668 Compare May 15, 2018 15:46
Copy link
Contributor

@localheinz localheinz left a 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());
Copy link
Contributor

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;

?

Copy link
Contributor

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';
Copy link
Contributor

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 () {
Copy link
Contributor

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";
Copy link
Contributor

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";
Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

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

Copy link
Member Author

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.

Copy link
Member Author

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) {
Copy link
Contributor

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) {
Copy link
Contributor

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.

@@ -194,4 +199,17 @@ public function markVersion(string $direction) : void
);
}
}

private function getExecutedAt() : DateTime
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DateTimeImmutable please

Copy link
Member

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


private function getExecutedAtDatabaseValue() : string
{
return Type::getType('datetime')->convertToDatabaseValue(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

datetime_immutable please

Copy link
Member

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'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

datetime_immutable please

@jwage jwage force-pushed the migration-created-at branch 2 times, most recently from ce34441 to 89f1c1b Compare May 15, 2018 18:31
Copy link
Member

@lcobucci lcobucci left a 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) {
Copy link
Member

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])) {
Copy link
Member

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;
Copy link
Member

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.

Copy link
Member Author

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();
Copy link
Member

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
Copy link
Member

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

Copy link
Member Author

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
Copy link
Member

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
Copy link
Member

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 😄)

@@ -194,4 +199,17 @@ public function markVersion(string $direction) : void
);
}
}

private function getExecutedAt() : DateTime
Copy link
Member

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


private function getExecutedAtDatabaseValue() : string
{
return Type::getType('datetime')->convertToDatabaseValue(
Copy link
Member

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)
Copy link
Member

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

@jwage jwage force-pushed the migration-created-at branch 8 times, most recently from 8881f9c to 4a347e5 Compare May 15, 2018 23:24
@jwage jwage requested review from lcobucci and Majkl578 May 15, 2018 23:25
@jwage jwage force-pushed the migration-created-at branch from 4a347e5 to 95485c6 Compare May 15, 2018 23:32

public function getQuotedMigrationsExecutedAtColumnName() : string
{
return $this->getDependencyFactory()
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Member Author

@jwage jwage May 16, 2018

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)) {
Copy link
Contributor

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) {
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

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

Copy link
Member Author

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 👍

@jwage jwage force-pushed the migration-created-at branch 2 times, most recently from 46305a3 to 203db81 Compare May 16, 2018 01:38

$queries = $fromSchema->getMigrateToSql($toSchema, $this->platform);

$this->connection->beginTransaction();
Copy link
Member

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)

Copy link
Member Author

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?

Copy link
Member

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.

@jwage jwage force-pushed the migration-created-at branch 4 times, most recently from c7d6f03 to e08e9b1 Compare May 16, 2018 16:18
@jwage jwage force-pushed the migration-created-at branch from e08e9b1 to fb26b48 Compare May 16, 2018 16:20
@jwage jwage merged commit ead074a into master May 16, 2018
@jwage jwage deleted the migration-created-at branch May 16, 2018 18:29
@jwage jwage removed the WIP label May 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants