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

Add possibility to define bidirectional migrations #62

Closed
wants to merge 1 commit into from

Conversation

psliwa
Copy link
Contributor

@psliwa psliwa commented Nov 24, 2016

This PR is for #57.

DSL hasn't changed, api is simple. If you want to define backward migration, you can use backTo function on Migrator. I've assumed not for every forward migration has to exist backward migration (so backTo invocation is optional), because when forward migration eg. adds new field, backward migration is not needed. The second reason for optional backTo migration is just not using backward migrations in the project.

Usage from tests:

val migrator = from[String, V1]
   .to[V2](_ + "V2")
   .backTo[V1](_.replace("V2", ""))
   .to[V3](_ + "V3")
   .backTo[V2](_.replace("V3", ""))
   .to[V4](_ + "V4")
   .backTo[V3](_.replace("V4", ""))
   .to[V5](_ + "V5")

@psliwa
Copy link
Contributor Author

psliwa commented Dec 14, 2016

@agemooij Any chance to take a look at this PR soon?

@agemooij
Copy link
Contributor

@raboof what do you think of this PR?

Copy link
Contributor

@agemooij agemooij left a comment

Choose a reason for hiding this comment

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

This code seems to always apply all forward and backward migrations instead of allowing them to cancel each other out. Have you considered removing migrations if they would be cancelled out by a backward one?

def &&(secondMigration: Migration[T]): Migration[T] = {
(value: T) ⇒ secondMigration(firstMigration(value))
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why was this removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because it is not used anymore.

* Creates a Migrator[T, V1] that can function as a builder for
* creating Migrator[T, V2], etc. Its migration will be the identity
* function so calling its migrate function will not have any effect.
*/
def from[T, V <: V1: VersionInfo]: Migrator[T, V] = new Migrator[T, V](Map(Version.numberFor[V] → identityMigration[T]))
def from[T, V <: V1: VersionInfo]: Migrator[T, V] = new Migrator[T, V](Map(Version.numberFor[V] → identityMigration[T]), Map.empty)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not make the empty map a default argument of the Migrator class?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because backward migrations are empty when we start defining migration - so empty map is natural initial value.

@psliwa
Copy link
Contributor Author

psliwa commented Dec 22, 2016

This code seems to always apply all forward and backward migrations instead of allowing them to cancel each other out. Have you considered removing migrations if they would be cancelled out by a backward one?

I am not sure what you mean under always apply all forward and backward migrations instead of allowing them to cancel each other out. The idea is to have migrations decoupled from persisters, so I can use migrations alone. When I want to migrate something from version X to Y, Migrator should apply transformations. If those transformations are forward or backward - it depends on given versions.

Have you considered removing migrations if they would be cancelled out by a backward one?
I don't fully understand, there is no sense in removing migrations when backward one is added. They coexists. Which one is used (backward or forward) depends on if fromVersion is greater or lower than toVersion. In Persisters only forward migrations are used, backward migrations we would like to use in our rollback tool.

@psliwa
Copy link
Contributor Author

psliwa commented Mar 1, 2017

@agemooij ping

@psliwa
Copy link
Contributor Author

psliwa commented Mar 29, 2017

Any progress? Maybe something is unclear?

@psliwa
Copy link
Contributor Author

psliwa commented Apr 15, 2017

@agemooij @raboof ? 🙏

@agemooij
Copy link
Contributor

@psliwa Sorry man. Stamina has been so completely out of scope for me lately that it's hard to spend time on it .

I had some minor review comments before that you haven't answered yet but overall I'm ok with merging this.

@raboof? What do you think?

Copy link
Member

@raboof raboof left a comment

Choose a reason for hiding this comment

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

@agemooij @psliwa sorry for taking so long to respond too.

This looks reasonable, might be good to address the couple of small comments given on the PR, and perhaps add some documentation? But otherwise LGTM.

class Migrator[R, V <: Version: VersionInfo] private[stamina] (migrations: Map[Int, Migration[R]] = Map.empty) {
def canMigrate(fromVersion: Int): Boolean = migrations.contains(fromVersion)
class Migrator[R, V <: Version: VersionInfo] private[stamina] (forwardMigrations: Map[Int, Migration[R]] = Map.empty, backwardMigrations: Map[Int, Migration[R]] = Map.empty) {
def canMigrate(fromVersion: Int, toVersion: Int): Boolean = forwardMigrations.contains(fromVersion) && forwardMigrations.contains(toVersion)
Copy link
Member

Choose a reason for hiding this comment

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

Is this correct? It's not obvious to me that we now also need forwardMigrations.contains(toVersion), and shouldn't this also take into account that we can support backwards migrations?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is a simple code from tests:

val migrator = from[String, V1]
   .to[V2](_ + "V2")
   .backTo[V1](_.replace("V2", ""))
   .to[V3](_ + "V3")
   .backTo[V2](_.replace("V3", ""))
   .to[V4](_ + "V4")
   .backTo[V3](_.replace("V4", ""))
   .to[V5](_ + "V5")

The assumption is to define continuous forward migrations, so you cannot define migrations only for version 1 and version 3. You have to define migration also for version 2. So in case when we are going to migrate from version 2 to version 5, we need migrations from 2 to 3, from 3 to 4 and from 4 to 5. Migration from 4 to 5 is under 5 key, it is toVersion, so it is needed as well. canMigrate function checks in this case existence of 2 -> 3 and 4 -> 5 migrations, because forward migrations are defined continuously. Backward migrations are optional, so they doesn't affect canMigrate function at all. If backward migration is not defined for particular version, identity function is used.

@psliwa
Copy link
Contributor Author

psliwa commented Jun 8, 2017

@raboof ping

@agemooij
Copy link
Contributor

@psliwa we are currently waiting on your replies to our comments.


import stamina._

class MigratorSpec extends StaminaSpec {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This test suits to scalacheck, do you think I can add scalacheck as dependency and rewrite this test?

def &&(secondMigration: Migration[T]): Migration[T] = {
(value: T) ⇒ secondMigration(firstMigration(value))
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because it is not used anymore.

* Creates a Migrator[T, V1] that can function as a builder for
* creating Migrator[T, V2], etc. Its migration will be the identity
* function so calling its migrate function will not have any effect.
*/
def from[T, V <: V1: VersionInfo]: Migrator[T, V] = new Migrator[T, V](Map(Version.numberFor[V] → identityMigration[T]))
def from[T, V <: V1: VersionInfo]: Migrator[T, V] = new Migrator[T, V](Map(Version.numberFor[V] → identityMigration[T]), Map.empty)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because backward migrations are empty when we start defining migration - so empty map is natural initial value.

class Migrator[R, V <: Version: VersionInfo] private[stamina] (migrations: Map[Int, Migration[R]] = Map.empty) {
def canMigrate(fromVersion: Int): Boolean = migrations.contains(fromVersion)
class Migrator[R, V <: Version: VersionInfo] private[stamina] (forwardMigrations: Map[Int, Migration[R]] = Map.empty, backwardMigrations: Map[Int, Migration[R]] = Map.empty) {
def canMigrate(fromVersion: Int, toVersion: Int): Boolean = forwardMigrations.contains(fromVersion) && forwardMigrations.contains(toVersion)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is a simple code from tests:

val migrator = from[String, V1]
   .to[V2](_ + "V2")
   .backTo[V1](_.replace("V2", ""))
   .to[V3](_ + "V3")
   .backTo[V2](_.replace("V3", ""))
   .to[V4](_ + "V4")
   .backTo[V3](_.replace("V4", ""))
   .to[V5](_ + "V5")

The assumption is to define continuous forward migrations, so you cannot define migrations only for version 1 and version 3. You have to define migration also for version 2. So in case when we are going to migrate from version 2 to version 5, we need migrations from 2 to 3, from 3 to 4 and from 4 to 5. Migration from 4 to 5 is under 5 key, it is toVersion, so it is needed as well. canMigrate function checks in this case existence of 2 -> 3 and 4 -> 5 migrations, because forward migrations are defined continuously. Backward migrations are optional, so they doesn't affect canMigrate function at all. If backward migration is not defined for particular version, identity function is used.

@psliwa
Copy link
Contributor Author

psliwa commented Jun 12, 2017

@agemooij Sorry, I am not sure how github conversations work. I added comments long ago, but they were marked as Pending and probably you weren't able to see them. Right now you should be able to see them, if you have any more questions - go ahead.

@agemooij
Copy link
Contributor

Looks like you started a review but did not finish it. This is a common mistake made by quite a few people I know since Github introduced this feature.

@raboof could you also have a look at the review comments?

@psliwa
Copy link
Contributor Author

psliwa commented Aug 3, 2017

@agemooij @raboof ping

@knirski
Copy link

knirski commented Dec 18, 2017

We will close this PR (@psliwa ping) in favor of #71, which is already proven to be working on production :)

@psliwa psliwa closed this Dec 20, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants